一文帶你搞懂Python中的pyc文件
pyc 文件的觸發(fā)
上一篇文章我們介紹了字節(jié)碼,當(dāng)時(shí)提到,py 文件在執(zhí)行的時(shí)候會(huì)先被編譯成 PyCodeObject 對(duì)象,并且該對(duì)象還會(huì)被保存到 pyc 文件中。
但不幸的是,事實(shí)并不總是這樣,有時(shí)當(dāng)我們運(yùn)行一個(gè)簡單的程序時(shí),并沒有產(chǎn)生 pyc 文件。因此我們猜測:有些 Python 程序只是臨時(shí)完成一些瑣碎的工作,這樣的程序僅僅只會(huì)運(yùn)行一次,然后就不會(huì)再使用了,因此也就沒有保存至 pyc 文件的必要。
如果我們?cè)诖a中加上了一個(gè) import abc 這樣的語句,再執(zhí)行你就會(huì)發(fā)現(xiàn) Python 為 abc.py 生成了 pyc 文件,這就說明 import 會(huì)觸發(fā) pyc 的生成。
實(shí)際上,在運(yùn)行過程中,如果碰到 import abc 這樣的語句,那么 Python 會(huì)在設(shè)定好的 path 中尋找 abc.pyc 或者 abc.pyd 文件。如果沒有這些文件,而是只發(fā)現(xiàn)了 abc.py,那么會(huì)先將 abc.py 編譯成 PyCodeObject,然后寫入到 pyc 文件中。
接下來,再對(duì) abc.pyc 進(jìn)行 import 動(dòng)作。對(duì)的,并不是編譯成 PyCodeObject 對(duì)象之后就直接使用。而是先寫到 pyc 文件里,然后再將 pyc 文件里面的 PyCodeObject 對(duì)象重新在內(nèi)存中復(fù)制出來。
當(dāng)然啦,觸發(fā) pyc 文件生成不僅可以通過 import,還可以通過 py_compile 模塊手動(dòng)生成。比如當(dāng)前有一個(gè) tools.py:
a?=?1 b?=?"你好啊"
如何將其編譯成 pyc 呢?
import?py_compile py_compile.compile("tools.py")
查看當(dāng)前目錄的 __pycache__ 目錄,會(huì)發(fā)現(xiàn) pyc 已經(jīng)生成了。
然后 py文件名.cpython-版本號(hào).pyc 為編譯之后的 pyc 文件名。
pyc 文件的導(dǎo)入
如果有一個(gè)現(xiàn)成的 pyc 文件,我們要如何導(dǎo)入它呢?
from?importlib.machinery?import?SourcelessFileLoader tools?=?SourcelessFileLoader( ????"tools",?"__pycache__/tools.cpython-38.pyc" ).load_module() print(tools.a)??#?1 print(tools.b)??#?你好啊
以上我們就成功手動(dòng)導(dǎo)入了 pyc 文件。
pyc 文件包含的內(nèi)容
pyc 文件在創(chuàng)建的時(shí)候都會(huì)往里面寫入哪些內(nèi)容呢?
1. magic number
這是 Python 定義的一個(gè)整數(shù)值,不同版本的 Python 會(huì)定義不同的 magic number,這個(gè)值是為了保證 Python 能夠加載正確的pyc。
比如 Python3.7 不會(huì)加載 3.6 版本的 pyc,因?yàn)?Python 在加載 pyc 文件的時(shí)候會(huì)首先檢測該 pyc 的 magic number。如果和自身的 magic number 不一致,則拒絕加載。
2. pyc 文件的寫入時(shí)間
這個(gè)很好理解,在加載 pyc 之前會(huì)先比較源代碼的最后修改時(shí)間和 pyc 文件的寫入時(shí)間。如果 pyc 文件的寫入時(shí)間比源代碼的修改時(shí)間要早,說明在生成 pyc 之后,源代碼被修改了,那么會(huì)重新編譯并寫入 pyc,而反之則會(huì)直接加載已存在的 pyc。
3. py 文件的大小
py 文件的大小也會(huì)被記錄在 pyc 文件中。
4. PyCodeObject 對(duì)象
編譯之后的 PyCodeObject 對(duì)象,這個(gè)不用說了,肯定是要存儲(chǔ)的,并且是序列化之后再存儲(chǔ)。
因此 pyc 文件的結(jié)構(gòu)如下:
注意:以上是 Python 3.7+ 的 pyc 文件結(jié)構(gòu),如果版本低于 3.7,那么開頭沒有 4 個(gè) \x00。我們實(shí)際驗(yàn)證一下:
import?struct from?importlib.util?import?MAGIC_NUMBER from?datetime?import?datetime with?open("__pycache__/tools.cpython-38.pyc",?"rb")?as?f: ????data?=?f.read() #?0?~?4?字節(jié)是?MAGIC?NUMBER print(data[:?4])??#?b'U\r\r\n' print(MAGIC_NUMBER)??#?b'U\r\r\n' #?4?~?8?字節(jié)是?4?個(gè)?\x00 print(data[4:?8])??#?b'\x00\x00\x00\x00' #?8?~?12?字節(jié)是?pyc?的寫入時(shí)間(小端存儲(chǔ)),一個(gè)時(shí)間戳 ts?=?struct.unpack("<I",?data[8:?12])[0] print(ts)??#?1671001724 print( ????datetime.fromtimestamp(ts) )??#?2022-12-14?20:32:23 #?12?~?16?字節(jié)是?py?文件的大小 print( ????struct.unpack("<I",?data[12:?16])[0] )??#?21
結(jié)果和我們分析的一樣,因此對(duì)于任何一個(gè) pyc 文件來說,前 16 字節(jié)是固定的(如果 Python 低于 3.7,那么前 12 個(gè)字節(jié)是固定的)。
16 個(gè)字節(jié)往后就是 PyCodeObject 對(duì)象,并且是序列化之后的,因?yàn)樵搶?duì)象顯然無法直接存在文件中。
import?marshal with?open("__pycache__/tools.cpython-38.pyc",?"rb")?as?f: ????data?=?f.read() #?通過?marshal.loads?可以反序列化 #?marshal.dumps?則表示序列化 code?=?marshal.loads(data[16:]) #?此時(shí)就拿到了?py?文件編譯之后的?PyCodeObject print(code) """ <code?object?<module>?at?0x...,?file?"tools.py",?line?1> """ #?查看常量池 print(code.co_consts)??#?(1,?'你好啊',?None) #?符號(hào)表 print(code.co_names)??#?('a',?'b')
問題來了,既然我們可以根據(jù) pyc 文件反推出 PyCodeObject,那么能否手動(dòng)構(gòu)建 PyCodeObject 然后生成 pyc 呢?來試一下。
a?=?1 b?=?2 c?=?3
上述代碼編譯之后的結(jié)果,就是我們要構(gòu)建的 PyCodeObject。
from?importlib.util?import?MAGIC_NUMBER import?struct import?time from?types?import?CodeType import?marshal from?opcode?import?opmap HEADER?=?MAGIC_NUMBER?+?b"\x00"?*?4 #?時(shí)間隨便寫 HEADER?+=?struct.pack("<I",?int(time.time())) #?大小隨便寫 HEADER?+=?struct.pack("<I",?30) #?構(gòu)建?PyCodeObject code?=?CodeType( ????0,????????????????#?co_argcount ????0,????????????????#?co_posonlyargcount ????0,????????????????#?co_kwonlyargcount ????3,????????????????#?co_nlocals ????1,????????????????#?co_stacksize ????0,????????????????#?co_flags ????bytes([ ????????#?a?=?1?分為兩步 ????????#?第一步:先通過 LOAD_CONST 將常量加載進(jìn)來 ????????#?因此指令是?LOAD_CONST,然后參數(shù)是?0 ????????#?表示加載常量池中索引為?0?的常量 ????????opmap["LOAD_CONST"],?0, ????????#?第二步:通過 STORE_NAME 將常量和符號(hào)綁定起來 ????????#?參數(shù)是?0,表示和符號(hào)表中索引為?0?的符號(hào)進(jìn)行綁定 ????????opmap["STORE_NAME"],?0, ????????#?b?=?2 ????????opmap["LOAD_CONST"],?1, ????????opmap["STORE_NAME"],?1, ????????#?c?=?3 ????????opmap["LOAD_CONST"],?2, ????????opmap["STORE_NAME"],?2, ????????#?結(jié)尾要?LOAD?一個(gè)?None,然后返回 ????????opmap["LOAD_CONST"],?3, ????????opmap["RETURN_VALUE"] ????]),???????????????#?co_code ????(1,?2,?3,?None),??#?co_consts ????("a",?"b",?"c"),??#?co_names ????(),???????????????#?co_varnames ????"build_pyc.py",???#?co_filename ????"<module>",???????#?co_name ????1,????????????????#?co_firstlineno ????b"",??????????????#?co_lnotab ????(),???????????????#?freevars ????()????????????????#?cellvars ) #?pyc?文件內(nèi)容 pyc_content?=?HEADER?+?marshal.dumps(code) #?生成?pyc?文件 with?open("build_pyc.pyc",?"wb")?as?f: ????f.write(pyc_content) #?然后加載生成的?pyc?文件 from?importlib.machinery?import?SourcelessFileLoader mod?=?SourcelessFileLoader( ????"build_pyc",?"build_pyc.pyc" ).load_module() print(mod)??#?<module?'build_pyc'?from?'build_pyc.pyc'> print(mod.a)??#?1 print(mod.b)??#?2 print(mod.c)??#?3
怎么樣,是不是很有趣呢?
pyc 文件的寫入
下面通過源碼來查看 pyc 文件的寫入過程,既然要寫入,那么肯定要有文件句柄。
//位置:Python/marshal.c //FILE是?C?自帶的文件句柄 //可以把WFILE看成是FILE的包裝 typedef?struct?{ ????FILE?*fp;??//文件句柄 ????//下面的字段在寫入信息的時(shí)候會(huì)看到 ????int?error;?? ????int?depth; ????PyObject?*str; ????char?*ptr; ????char?*end; ????char?*buf; ????_Py_hashtable_t?*hashtable; ????int?version; }?WFILE;
首先是寫入 magic number、創(chuàng)建時(shí)間和文件大小,它們會(huì)調(diào)用 PyMarshal_WriteLongToFile 函數(shù)進(jìn)行寫入:
void PyMarshal_WriteLongToFile(long?x,?FILE?*fp,?int?version) {?? ????//magic?number、創(chuàng)建時(shí)間和文件大小,只是一個(gè)整數(shù) ????//在寫入的時(shí)候,使用char?[4]來保存 ????char?buf[4]; ????//聲明一個(gè)WFILE類型變量wf ????WFILE?wf; ????//內(nèi)存初始化 ????memset(&wf,?0,?sizeof(wf)); ????//初始化內(nèi)部成員 ????wf.fp?=?fp; ????wf.ptr?=?wf.buf?=?buf; ????wf.end?=?wf.ptr?+?sizeof(buf); ????wf.error?=?WFERR_OK; ????wf.version?=?version; ????//調(diào)用w_long將x、也就是版本信息或者時(shí)間寫到wf里面去 ????w_long(x,?&wf); ????//刷到磁盤上 ????w_flush(&wf); }
所以該函數(shù)只是初始化了一個(gè) WFILE 對(duì)象,真正寫入則是調(diào)用的 w_long。
static?void w_long(long?x,?WFILE?*p) { ????w_byte((char)(?x??????&?0xff),?p); ????w_byte((char)((x>>?8)?&?0xff),?p); ????w_byte((char)((x>>16)?&?0xff),?p); ????w_byte((char)((x>>24)?&?0xff),?p); }
w_long 則是調(diào)用 w_byte 將 x 逐個(gè)字節(jié)地寫到文件里面去。
而寫入 PyCodeObject 對(duì)象則是調(diào)用 PyMarshal_WriteObjectToFile,它實(shí)際又會(huì)調(diào)用 w_object 進(jìn)行寫入。
static?void w_object(PyObject?*v,?WFILE?*p) { ????char?flag?=?'\0'; ????p->depth++; ????if?(p->depth?>?MAX_MARSHAL_STACK_DEPTH)?{ ????????p->error?=?WFERR_NESTEDTOODEEP; ????} ????else?if?(v?==?NULL)?{ ????????w_byte(TYPE_NULL,?p); ????} ????else?if?(v?==?Py_None)?{ ????????w_byte(TYPE_NONE,?p); ????} ????else?if?(v?==?PyExc_StopIteration)?{ ????????w_byte(TYPE_STOPITER,?p); ????} ????else?if?(v?==?Py_Ellipsis)?{ ????????w_byte(TYPE_ELLIPSIS,?p); ????} ????else?if?(v?==?Py_False)?{ ????????w_byte(TYPE_FALSE,?p); ????} ????else?if?(v?==?Py_True)?{ ????????w_byte(TYPE_TRUE,?p); ????} ????else?if?(!w_ref(v,?&flag,?p)) ????????w_complex_object(v,?flag,?p); ????p->depth--; }
可以看到本質(zhì)上還是調(diào)用了 w_byte,但這僅僅是一些特殊的對(duì)象。如果是列表、字典之類的數(shù)據(jù),那么會(huì)調(diào)用 w_complex_object,也就是代碼中的最后一個(gè) else if 分支。
w_complex_object 這個(gè)函數(shù)的源代碼很長,我們看一下整體結(jié)構(gòu),具體邏輯就不貼了,后面會(huì)單獨(dú)截取一部分進(jìn)行分析。
static?void w_complex_object(PyObject?*v,?char?flag,?WFILE?*p) { ????Py_ssize_t?i,?n; ????//如果是整數(shù)的話,執(zhí)行整數(shù)的寫入邏輯 ????if?(PyLong_CheckExact(v))?{ ????????//...... ????} ????//如果是浮點(diǎn)數(shù)的話,執(zhí)行浮點(diǎn)數(shù)的寫入邏輯 ????else?if?(PyFloat_CheckExact(v))?{ ????????if?(p->version?>?1)?{ ????????????//...... ????????} ????????else?{ ????????????//...... ????????} ????} ????//如果是復(fù)數(shù)的話,執(zhí)行復(fù)數(shù)的寫入邏輯 ????else?if?(PyComplex_CheckExact(v))?{ ????????if?(p->version?>?1)?{ ????????????//...... ????????} ????????else?{ ????????????//...... ????????} ????} ????//如果是字節(jié)序列的話,執(zhí)行字節(jié)序列的寫入邏輯 ????else?if?(PyBytes_CheckExact(v))?{ ????????//...... ????} ????//如果是字符串的話,執(zhí)行字符串的寫入邏輯 ????else?if?(PyUnicode_CheckExact(v))?{ ????????if?(p->version?>=?4?&&?PyUnicode_IS_ASCII(v))?{ ??????????????//...... ????????????} ????????????else?{ ????????????????//...... ????????????} ????????} ????????else?{ ????????????//...... ????????} ????} ????//如果是元組的話,執(zhí)行元組的寫入邏輯 ????else?if?(PyTuple_CheckExact(v))?{ ???????//...... ????} ????//如果是列表的話,執(zhí)行列表的寫入邏輯 ????else?if?(PyList_CheckExact(v))?{ ????????//...... ????} ????//如果是字典的話,執(zhí)行字典的寫入邏輯 ????else?if?(PyDict_CheckExact(v))?{ ????????//...... ????} ????//如果是集合的話,執(zhí)行集合的寫入邏輯 ????else?if?(PyAnySet_CheckExact(v))?{ ????????//...... ????} ????//如果是PyCodeObject對(duì)象的話 ????//執(zhí)行PyCodeObject對(duì)象的寫入邏輯 ????else?if?(PyCode_Check(v))?{ ????????//...... ????} ????//如果是Buffer的話,執(zhí)行Buffer的寫入邏輯 ????else?if?(PyObject_CheckBuffer(v))?{ ????????//...... ????} ????else?{ ????????W_TYPE(TYPE_UNKNOWN,?p); ????????p->error?=?WFERR_UNMARSHALLABLE; ????} }
源代碼雖然長,但是邏輯非常單純,就是對(duì)不同的對(duì)象、執(zhí)行不同的寫動(dòng)作,然而其最終目的都是通過 w_byte 寫到 pyc 文件中。了解完函數(shù)的整體結(jié)構(gòu)之后,我們?cè)倏匆幌戮唧w細(xì)節(jié),看看它在寫入對(duì)象的時(shí)候到底寫入了哪些內(nèi)容?
static?void w_complex_object(PyObject?*v,?char?flag,?WFILE?*p) { ????//...... ????else?if?(PyList_CheckExact(v))?{ ????????W_TYPE(TYPE_LIST,?p); ????????n?=?PyList_GET_SIZE(v); ????????W_SIZE(n,?p); ????????for?(i?=?0;?i?<?n;?i++)?{ ????????????w_object(PyList_GET_ITEM(v,?i),?p); ????????} ????} ????else?if?(PyDict_CheckExact(v))?{ ????????Py_ssize_t?pos; ????????PyObject?*key,?*value; ????????W_TYPE(TYPE_DICT,?p); ????????/*?This?one?is?NULL?object?terminated!?*/ ????????pos?=?0; ????????while?(PyDict_Next(v,?&pos,?&key,?&value))?{ ????????????w_object(key,?p); ????????????w_object(value,?p); ????????} ????????w_object((PyObject?*)NULL,?p); ????}???? ????//...... }
以列表和字典為例,它們?cè)趯懭氲臅r(shí)候?qū)嶋H上寫的是內(nèi)部的元素,其它對(duì)象也是類似的。
def?foo(): ????lst?=?[1,?2,?3] #?把列表內(nèi)的元素寫進(jìn)去了 print( ????foo.__code__.co_consts )??#?(None,?1,?2,?3)
但問題來了,如果只是寫入元素的話,那么Python在加載的時(shí)候怎么知道它是一個(gè)列表呢?所以在寫入的時(shí)候不能光寫數(shù)據(jù),類型信息也要寫進(jìn)去。我們?cè)倏匆幌律厦媪斜砗妥值涞膶懭脒壿嫞锩娑颊{(diào)用了W_TYPE,它負(fù)責(zé)將類型信息寫進(jìn)去。
因此無論對(duì)于哪種對(duì)象,在寫入具體數(shù)據(jù)之前,都會(huì)先調(diào)用W_TYPE將類型信息寫進(jìn)去。如果沒有類型信息,那么當(dāng)Python加載pyc文件的時(shí)候,只會(huì)得到一坨字節(jié)流,而無法解析字節(jié)流中隱藏的結(jié)構(gòu)和蘊(yùn)含的信息。
所以在往 pyc 文件里寫入數(shù)據(jù)之前,必須先寫入一個(gè)標(biāo)識(shí),諸如TYPE_LIST, TYPE_TUPLE, TYPE_DICT等等,這些標(biāo)識(shí)正是對(duì)應(yīng)的類型信息。
如果解釋器在 pyc 文件中發(fā)現(xiàn)了這樣的標(biāo)識(shí),則預(yù)示著上一個(gè)對(duì)象結(jié)束,新的對(duì)象開始,并且也知道新對(duì)象是什么樣的對(duì)象,從而也知道該執(zhí)行什么樣的構(gòu)建動(dòng)作。當(dāng)然,這些標(biāo)識(shí)也是可以看到的,在底層已經(jīng)定義好了。
到了這里可以看到,Python 對(duì) PyCodeObject 對(duì)象的導(dǎo)出實(shí)際上是不復(fù)雜的。因?yàn)椴还苁裁磳?duì)象,最后都為歸結(jié)為兩種簡單的形式,一種是數(shù)值寫入,一種是字符串寫入。
上面都是對(duì)數(shù)值的寫入,比較簡單,僅僅需要按照字節(jié)依次寫入 pyc 即可。然而在寫入字符串的時(shí)候,Python 設(shè)計(jì)了一種比較復(fù)雜的機(jī)制,有興趣可以自己閱讀源碼,這里不再介紹。
字節(jié)碼混淆
最后再來說一下字節(jié)碼混淆,我們知道 pyc 是可以反編譯的,而且目前也有現(xiàn)成的工具。但這些工具它會(huì)將每一個(gè)指令都解析出來,所以字節(jié)碼混淆的方式就是往里面插入一些惡意指令(比如加載超出范圍的數(shù)據(jù)),讓反編譯工具在解析的時(shí)候報(bào)錯(cuò),從而失去作用。
但插入的惡意指令還不能影響解釋器執(zhí)行,因此還要插入一些跳轉(zhuǎn)指令,從而讓解釋器跳過惡意指令。
混淆之后多了兩條指令,其中偏移量為 8 的指令,參數(shù)為 255,但執(zhí)行的時(shí)候會(huì)發(fā)生越界,因此反編譯的時(shí)候毫無疑問會(huì)報(bào)錯(cuò)。而解釋器在執(zhí)行的時(shí)候卻沒有問題,因?yàn)樵趫?zhí)行到偏移量為 6 的指令時(shí)出現(xiàn)了一個(gè)絕對(duì)跳轉(zhuǎn),直接跳到偏移量為 10 的指令了。
因此對(duì)于解釋器執(zhí)行來說,混淆前后是沒有區(qū)別的。但對(duì)于反編譯工具而言就會(huì)無法正常工作,因?yàn)樗鼤?huì)把每一個(gè)指令都解析一遍。
根據(jù)這個(gè)思路,我們可以插入很多很多的惡意指令,然后再用跳轉(zhuǎn)指令來跳過這些不合法指令。當(dāng)然混淆的手段并不止這些,我們還可以添加一下虛假的分支,然后在執(zhí)行時(shí)跳轉(zhuǎn)到真實(shí)的分支當(dāng)中。
而這一切的目的,都是為了防止別人根據(jù) pyc 文件反推出源代碼。不過這種做法屬于治標(biāo)不治本,如果真的想要保護(hù)源代碼的話,可以使用 Cython 將其編譯成 pyd ,這是最推薦的做法。
以上就是一文帶你搞懂Python中的pyc文件的詳細(xì)內(nèi)容,更多關(guān)于Python pyc文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
對(duì)python修改xml文件的節(jié)點(diǎn)值方法詳解
今天小編就為大家分享一篇對(duì)python修改xml文件的節(jié)點(diǎn)值方法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-12-12使用Python開發(fā)游戲運(yùn)行腳本成功調(diào)用大漠插件
閑來無事,想通過python來實(shí)現(xiàn)一些簡單的游戲輔助腳本,而游戲輔助腳本的主要原理就是通過程序來查找游戲程序窗口,模擬實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊和鍵盤按鍵等事件來實(shí)現(xiàn)游戲輔助的,對(duì)Python開發(fā)游戲運(yùn)行腳本相關(guān)知識(shí)感興趣的朋友跟隨小編一起看看吧2021-11-11python實(shí)現(xiàn)手機(jī)銷售管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)手機(jī)銷售管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03python分塊讀取大數(shù)據(jù),避免內(nèi)存不足的方法
今天小編就為大家分享一篇python分塊讀取大數(shù)據(jù),避免內(nèi)存不足的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-12-12python神經(jīng)網(wǎng)絡(luò)tensorflow利用訓(xùn)練好的模型進(jìn)行預(yù)測
這篇文章主要為大家介紹了python神經(jīng)網(wǎng)絡(luò)tensorflow利用訓(xùn)練好的模型進(jìn)行預(yù)測,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05YOLOv5目標(biāo)檢測之a(chǎn)nchor設(shè)定
在訓(xùn)練yolo網(wǎng)絡(luò)檢測目標(biāo)時(shí),需要根據(jù)待檢測目標(biāo)的位置大小分布情況對(duì)anchor進(jìn)行調(diào)整,使其檢測效果盡可能提高,下面這篇文章主要給大家介紹了關(guān)于YOLOv5目標(biāo)檢測之a(chǎn)nchor設(shè)定的相關(guān)資料,需要的朋友可以參考下2022-05-05PyQt5+serial模塊實(shí)現(xiàn)一個(gè)串口小工具
這篇文章主要為大家詳細(xì)介紹了如何利用PyQt5和serial模塊實(shí)現(xiàn)一個(gè)簡單的串口小工具,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-01-01梅爾倒譜系數(shù)(MFCC)實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了梅爾倒譜系數(shù)(MFCC)實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06