一文帶你深度解密Python的字節(jié)碼
楔子
當(dāng)我們想要執(zhí)行一個(gè) py 文件的時(shí)候,只需要在命令行中輸入 python xxx.py 即可,但你有沒有想過(guò)這背后的流程是怎樣的呢?
首先 py 文件不是一上來(lái)就直接執(zhí)行的,而是會(huì)先有一個(gè)編譯的過(guò)程,整個(gè)步驟如下:
這里我們看到了 Python 編譯器、Python 虛擬機(jī),而且我們平常還會(huì)說(shuō) Python 解釋器,那么三者之間有什么區(qū)別呢?
Python 編譯器負(fù)責(zé)將 Python 源代碼編譯成 PyCodeObject 對(duì)象,然后交給 Python 虛擬機(jī)來(lái)執(zhí)行。
那么 Python 編譯器和 Python 虛擬機(jī)都在什么地方呢?如果打開 Python 的安裝目錄,會(huì)發(fā)現(xiàn)有一個(gè) python.exe,點(diǎn)擊的時(shí)候會(huì)通過(guò)它來(lái)啟動(dòng)一個(gè)終端。
但問(wèn)題是這個(gè)文件大小還不到 100K,不可能容納一個(gè)編譯器加一個(gè)虛擬機(jī),所以下面還有一個(gè) python38.dll。沒錯(cuò),編譯器、虛擬機(jī)都藏身于 python38.dll 當(dāng)中。
因此 Python 雖然是解釋型語(yǔ)言,但也有編譯的過(guò)程。源代碼會(huì)被編譯器編譯成 PyCodeObject 對(duì)象,然后再交給虛擬機(jī)來(lái)執(zhí)行。而之所以要存在編譯,是為了讓虛擬機(jī)能更快速地執(zhí)行,比如在編譯階段常量都會(huì)提前分配好,而且還可以盡早檢測(cè)出語(yǔ)法上的錯(cuò)誤。
pyc 文件是什么
在 Python 開發(fā)時(shí),我們肯定都見過(guò)這個(gè) pyc 文件,它一般位于 __pycache__ 目錄中,那么 pyc 文件和 PyCodeObject 之間有什么關(guān)系呢?
首先我們都知道字節(jié)碼,虛擬機(jī)的執(zhí)行實(shí)際上就是對(duì)字節(jié)碼不斷解析的一個(gè)過(guò)程。然而除了字節(jié)碼之外,還應(yīng)該包含一些其它的信息,這些信息也是 Python 運(yùn)行的時(shí)候所必需的,比如常量、變量名等等。
我們常聽到 py 文件被編譯成字節(jié)碼,這句話其實(shí)不太嚴(yán)謹(jǐn),因?yàn)樽止?jié)碼只是一個(gè) PyBytesObject 對(duì)象、或者說(shuō)一段字節(jié)序列。但很明顯,光有字節(jié)碼是不夠的,還有很多的靜態(tài)信息也需要被收集起來(lái),它們整體被稱為 PyCodeObject。
而 PyCodeObject 對(duì)象中有一個(gè)成員 co_code,它是一個(gè)指針,指向了這段字節(jié)序列。但是這個(gè)對(duì)象除了有 co_code 指向的字節(jié)碼之外,還有很多其它成員,負(fù)責(zé)保存代碼涉及到的常量、變量(名字、符號(hào))等等。
但是問(wèn)題來(lái)了,難道每一次執(zhí)行都要將源文件編譯一遍嗎?如果沒有對(duì)源文件進(jìn)行修改的話,那么完全可以使用上一次的編譯結(jié)果。相信此時(shí)你能猜到 pyc 文件是干什么的了,它就是負(fù)責(zé)保存編譯之后的 PyCodeObject 對(duì)象。
所以我們知道了,pyc 文件里面的內(nèi)容是 PyCodeObject 對(duì)象。對(duì)于 Python 編譯器來(lái)說(shuō),PyCodeObject 對(duì)象是對(duì)源代碼編譯之后的結(jié)果,而 pyc 文件則是這個(gè)對(duì)象在硬盤上的表現(xiàn)形式。
當(dāng)下一次運(yùn)行的時(shí)候,Python 會(huì)根據(jù) pyc 文件中記錄的編譯結(jié)果直接建立內(nèi)存中的 PyCodeObject 對(duì)象,而不需要再重新編譯了,當(dāng)然前提是沒有對(duì)源文件進(jìn)行修改。
PyCodeObject 底層結(jié)構(gòu)
我們來(lái)看一下這個(gè)結(jié)構(gòu)體長(zhǎng)什么樣子,它的定義位于 Include/code.h 中。
typedef?struct?{ ????//頭部信息,我們看到真的一切皆對(duì)象,字節(jié)碼也是個(gè)對(duì)象 ????PyObject_HEAD???? ????//可以通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù) ????int?co_argcount;???????????? ????//只能通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù),Python3.8新增 ????int?co_posonlyargcount;????? ????//只能通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù)個(gè)數(shù) ????int?co_kwonlyargcount;????? ????//代碼塊中局部變量的個(gè)數(shù),也包括參數(shù) ????int?co_nlocals;????????????? ????//執(zhí)行該段代碼塊需要的棧空間 ????int?co_stacksize;?????? ????//參數(shù)類型標(biāo)識(shí)???? ????int?co_flags;??????????? ????//代碼塊在文件中的行號(hào)?? ????int?co_firstlineno;????????? ????//指令集,也就是字節(jié)碼,它是一個(gè)bytes對(duì)象? ????PyObject?*co_code;????????? ????//常量池,一個(gè)元組,保存代碼塊中的所有常量? ????PyObject?*co_consts;??????? ????//一個(gè)元組,保存代碼塊中引用的其它作用域的變量 ????PyObject?*co_names;?????? ????//一個(gè)元組,保存當(dāng)前作用域中的變量??? ????PyObject?*co_varnames;???? ????//內(nèi)層函數(shù)引用的外層函數(shù)的作用域中的變量 ????PyObject?*co_freevars;?????? ????//外層函數(shù)的作用域中被內(nèi)層函數(shù)引用的變量 ????//本質(zhì)上和co_freevars是一樣的 ????PyObject?*co_cellvars;?????? ????//無(wú)需關(guān)注 ????Py_ssize_t?*co_cell2arg;???? ????//代碼塊所在的文件名 ????PyObject?*co_filename;???? ????//代碼塊的名字,通常是函數(shù)名、類名,或者文件名 ????PyObject?*co_name;????????? ????//字節(jié)碼指令與python源代碼的行號(hào)之間的對(duì)應(yīng)關(guān)系 ????//以PyByteObject的形式存在? ????PyObject?*co_lnotab;???????? }?PyCodeObject;
這里面的每一個(gè)成員,我們一會(huì)兒都會(huì)逐一演示進(jìn)行說(shuō)明??傊?Python 編譯器在對(duì)源代碼進(jìn)行編譯的時(shí)候,對(duì)于代碼中的每一個(gè) block,都會(huì)創(chuàng)建一個(gè) PyCodeObject 與之對(duì)應(yīng)。
但多少代碼才算得上是一個(gè) block 呢?事實(shí)上,Python 有一個(gè)簡(jiǎn)單而清晰的規(guī)則:當(dāng)進(jìn)入一個(gè)新的名字空間,或者說(shuō)作用域時(shí),就算是進(jìn)入了一個(gè)新的 block 了。舉個(gè)例子:
class?A: ????a?=?123 def?foo(): ????a?=?[]
我們仔細(xì)觀察一下上面這個(gè)文件,它在編譯完之后會(huì)有三個(gè) PyCodeObject 對(duì)象,一個(gè)是對(duì)應(yīng)整個(gè) py 文件(模塊)的,一個(gè)是對(duì)應(yīng) class A 的,一個(gè)是對(duì)應(yīng) def foo 的。因?yàn)檫@是三個(gè)不同的作用域,所以會(huì)有三個(gè) PyCodeObject 對(duì)象。
所以一個(gè) code block 對(duì)應(yīng)一個(gè)作用域、同時(shí)也對(duì)應(yīng)一個(gè) PyCodeObject 對(duì)象。Python 的類、函數(shù)、模塊都有自己獨(dú)立的作用域,因此在編譯時(shí)也都會(huì)有一個(gè) PyCodeObject 對(duì)象與之對(duì)應(yīng)。
PyCodeObject 代碼演示
PyCodeObject 我們知道它是干什么的了,那如何才能拿到這個(gè)對(duì)象呢?首先該對(duì)象在 Python 里面的類型是 <class 'code'>,但是底層沒有將這個(gè)類暴露給我們,因此 code 這個(gè)名字在 Python 里面只是一個(gè)沒有定義的變量罷了。
但是我們可以通過(guò)其它的方式進(jìn)行獲取,比如函數(shù)。
def?func(): ????pass print(func.__code__)??#?<code?object?...... print(type(func.__code__))??#?<class?'code'>
我們可以通過(guò)函數(shù)的 __code__ 屬性拿到底層對(duì)應(yīng)的PyCodeObject對(duì)象,當(dāng)然也可以獲取里面的成員,我們來(lái)演示一下。
co_argcount:可以通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù)
def?foo(a,?b,?c=3): ????pass print(foo.__code__.co_argcount)??#?3 def?bar(a,?b,?*args): ????pass print(bar.__code__.co_argcount)??#?2 def?func(a,?b,?*args,?c): ????pass print(func.__code__.co_argcount)??#?2
foo 中的參數(shù) a、b、c 都可以通過(guò)位置參數(shù)傳遞,所以結(jié)果是 3;對(duì)于 bar,則是兩個(gè),這里不包括 *args;而函數(shù) func,顯然也是兩個(gè),因?yàn)閰?shù) c 只能通過(guò)關(guān)鍵字參數(shù)傳遞。
co_posonlyargcount:只能通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù),Python3.8 新增
def?foo(a,?b,?c): ????pass print(foo.__code__.co_posonlyargcount)??#?0 def?bar(a,?b,?/,?c): ????pass print(bar.__code__.co_posonlyargcount)??#?2
注意:這里是只能通過(guò)位置參數(shù)傳遞的參數(shù)個(gè)數(shù)。對(duì)于 foo 而言,里面的三個(gè)參數(shù)既可以通過(guò)位置參數(shù)、也可以通過(guò)關(guān)鍵字參數(shù)傳遞;而函數(shù) bar,里面的 a、b 只能通過(guò)位置參數(shù)傳遞。
co_kwonlyargcount:只能通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù)個(gè)數(shù)
def?foo(a,?b=1,?c=2,?*,?d,?e): ????pass print(foo.__code__.co_kwonlyargcount)??#?2
這里是 d 和 e,它們必須通過(guò)關(guān)鍵字參數(shù)傳遞。
co_nlocals:代碼塊中局部變量的個(gè)數(shù),也包括參數(shù)
def?foo(a,?b,?*,?c): ????name?=?"xxx" ????age?=?16 ????gender?=?"f" ????c?=?33 print(foo.__code__.co_nlocals)??#?6
局部變量有 a、b、c、name、age、gender,所以我們看到在編譯之后,函數(shù)的局部變量就已經(jīng)確定了,因?yàn)樗鼈兪庆o態(tài)存儲(chǔ)的。
co_stacksize:執(zhí)行該段代碼塊需要的棧空間
def?foo(a,?b,?*,?c): ????name?=?"xxx" ????age?=?16 ????gender?=?"f" ????c?=?33 print(foo.__code__.co_stacksize)??#?1
這個(gè)暫時(shí)不需要太關(guān)注。
co_flags:參數(shù)類型標(biāo)識(shí)
標(biāo)識(shí)函數(shù)的參數(shù)類型,如果一個(gè)函數(shù)的參數(shù)出現(xiàn)了 *args,那么 co_flags & 0x04 為真;如果一個(gè)函數(shù)的參數(shù)出現(xiàn)了 **kwargs,那么 co_flags & 0x08 為真;
def?foo1(): ????pass #?結(jié)果全部為假 print(foo1.__code__.co_flags?&?0x04)??#?0 print(foo1.__code__.co_flags?&?0x08)??#?0 def?foo2(*args): ????pass #?co_flags?&?0x04?為真,因?yàn)槌霈F(xiàn)了?*args print(foo2.__code__.co_flags?&?0x04)??#?4 print(foo2.__code__.co_flags?&?0x08)??#?0 def?foo3(*args,?**kwargs): ????pass #?顯然?co_flags?&?0x04?和?co_flags?&?0x08?均為真 print(foo3.__code__.co_flags?&?0x04)??#?4 print(foo3.__code__.co_flags?&?0x08)??#?8
當(dāng)然啦,co_flags 可以做的事情并不止這么簡(jiǎn)單,它還能檢測(cè)一個(gè)函數(shù)的類型。比如函數(shù)內(nèi)部出現(xiàn)了 yield,那么它就是一個(gè)生成器函數(shù),調(diào)用之后可以得到一個(gè)生成器;使用 async def 定義,那么它就是一個(gè)協(xié)程函數(shù),調(diào)用之后可以得到一個(gè)協(xié)程。
這些在詞法分析的時(shí)候就可以檢測(cè)出來(lái),編譯之后會(huì)體現(xiàn)在 co_flags 這個(gè)成員中。
#?如果是生成器函數(shù) #?那么?co_flags?&?0x20?為真 def?foo1(): ????yield print(foo1.__code__.co_flags?&?0x20)??#?32 #?如果是協(xié)程函數(shù) #?那么?co_flags?&?0x80?為真 async?def?foo2(): ????pass print(foo2.__code__.co_flags?&?0x80)??#?128 #?顯然?foo2?不是生成器函數(shù) #?所以?co_flags?&?0x20?為假 print(foo2.__code__.co_flags?&?0x20)??#?0 #?如果是異步生成器函數(shù) #?那么?co_flags?&?0x200?為真 async?def?foo3(): ????yield print(foo3.__code__.co_flags?&?0x200)??#?512 #?顯然它不是生成器函數(shù)、也不是協(xié)程函數(shù) #?因此和?0x20、0x80?按位與之后,結(jié)果都為假 print(foo3.__code__.co_flags?&?0x20)??#?0 print(foo3.__code__.co_flags?&?0x80)??#?0
在判斷函數(shù)種類時(shí),這種方式是最優(yōu)雅的。
co_firstlineno:代碼塊在對(duì)應(yīng)文件的起始行
def?foo(a,?b,?*,?c): ????pass #?顯然是文件的第一行 #?或者理解為?def?所在的行 print(foo.__code__.co_firstlineno)??#?1
如果函數(shù)出現(xiàn)了調(diào)用呢?
def?foo(): ????return?bar def?bar(): ????pass print(foo().__code__.co_firstlineno)??#?4
如果執(zhí)行 foo,那么會(huì)返回函數(shù) bar,最終得到的就是 bar 的字節(jié)碼,因此最終結(jié)果是 def bar(): 所在的行數(shù)。所以每個(gè)函數(shù)都有自己的作用域,以及 PyCodeObject 對(duì)象。
co_names:符號(hào)表,一個(gè)元組,保存代碼塊中引用的其它作用域的變量
c?=?1 def?foo(a,?b): ????print(a,?b,?c) ????d?=?(list,?int,?str) print( ????foo.__code__.co_names )??#?('print',?'c',?'list',?'int',?'str')
一切皆對(duì)象,但看到的都是指向?qū)ο蟮淖兞?,所?print, c, list, int, str 都是變量,它們都不在當(dāng)前 foo 函數(shù)的作用域中。
co_varnames:符號(hào)表,一個(gè)元組,保存在當(dāng)前作用域中的變量
c?=?1 def?foo(a,?b): ????print(a,?b,?c) ????d?=?(list,?int,?str) print(foo.__code__.co_varnames)??#?('a',?'b',?'d')
a、b、d 是位于當(dāng)前 foo 函數(shù)的作用域當(dāng)中的,所以編譯階段便確定了局部變量是什么。
co_consts:常量池,一個(gè)元組,保存代碼塊中的所有常量
x?=?123 def?foo(a,?b): ????c?=?"abc" ????print(x) ????print(True,?False,?list,?[1,?2,?3],?{"a":?1}) ????return?">>>" print( ????foo.__code__.co_consts )??#?(None,?'abc',?True,?False,?1,?2,?3,?'a',?'>>>')
co_consts 里面出現(xiàn)的都是常量,但 [1, 2, 3] 和 {"a": 1} 卻沒有出現(xiàn),由此我們可以得出,列表和字典絕不是在編譯階段構(gòu)建的。編譯時(shí),只是收集了里面的元素,然后等到運(yùn)行時(shí)再去動(dòng)態(tài)構(gòu)建。
不過(guò)問(wèn)題來(lái)了,在構(gòu)建的時(shí)候解釋器怎么知道是要構(gòu)建列表、還是字典、亦或是其它的什么對(duì)象呢?所以這就依賴于字節(jié)碼了,解釋字節(jié)碼的時(shí)候,會(huì)判斷到底要構(gòu)建什么樣的對(duì)象。
因此解釋器執(zhí)行的是字節(jié)碼,核心邏輯都體現(xiàn)在字節(jié)碼中。但是光有字節(jié)碼還不夠,它包含的只是程序的主干邏輯,至于變量、常量,則從符號(hào)表和常量池里面獲取。
co_name:代碼塊的名字
def?foo(): ????pass #?這里就是函數(shù)名 print(foo.__code__.co_name)??#?foo
co_code:字節(jié)碼
def?foo(a,?b,?/,?c,?*,?d,?e): ????f?=?123 ????g?=?list() ????g.extend([tuple,?getattr,?print]) print(foo.__code__.co_code) #b'd\x01}\x05t\x00\x83\x00}\x06|\x06......'
這便是字節(jié)碼,它只保存了要操作的指令,因此光有字節(jié)碼是肯定不夠的,還需要其它的靜態(tài)信息。顯然這些信息連同字節(jié)碼一樣,都位于 PyCodeObject 中。
字節(jié)碼與反編譯
Python 執(zhí)行源代碼之前會(huì)先編譯得到 PyCodeObject 對(duì)象,里面的 co_code 指向了字節(jié)碼序列。
虛擬機(jī)會(huì)根據(jù)這些字節(jié)碼序列來(lái)進(jìn)行一系列的操作(當(dāng)然也依賴其它的靜態(tài)信息),從而完成對(duì)程序的執(zhí)行。
每個(gè)操作都對(duì)應(yīng)一個(gè)操作指令、也叫操作碼,總共有120多種,定義在 Include/opcode.h 中。
#define?POP_TOP???????????????????1 #define?ROT_TWO???????????????????2 #define?ROT_THREE?????????????????3 #define?DUP_TOP???????????????????4 #define?DUP_TOP_TWO???????????????5 #define?NOP???????????????????????9 #define?UNARY_POSITIVE???????????10 #define?UNARY_NEGATIVE???????????11 #define?UNARY_NOT????????????????12 #define?UNARY_INVERT?????????????15 #define?BINARY_MATRIX_MULTIPLY???16 #define?INPLACE_MATRIX_MULTIPLY??17 #define?BINARY_POWER?????????????19 #define?BINARY_MULTIPLY??????????20 #define?BINARY_MODULO????????????22 #define?BINARY_ADD???????????????23 #define?BINARY_SUBTRACT??????????24 #define?BINARY_SUBSCR????????????25 #define?BINARY_FLOOR_DIVIDE??????26 #define?BINARY_TRUE_DIVIDE???????27 #define?INPLACE_FLOOR_DIVIDE?????28 //?... //?...
操作指令只是一個(gè)整數(shù),然后我們可以通過(guò)反編譯的方式查看每行 Python 代碼都對(duì)應(yīng)哪些操作指令:
#?Python中的dis模塊專門負(fù)責(zé)干這件事情 import?dis def?foo(a,?b): ????c?=?a?+?b ????return?c #?里面接收一個(gè)字節(jié)碼 #?當(dāng)然函數(shù)也是可以的,會(huì)自動(dòng)獲取co_code dis.dis(foo) """ ??5???????????0?LOAD_FAST????????????????0?(a) ??????????????2?LOAD_FAST????????????????1?(b) ??????????????4?BINARY_ADD ??????????????6?STORE_FAST???????????????2?(c) ??6???????????8?LOAD_FAST????????????????2?(c) ?????????????10?RETURN_VALUE """
字節(jié)碼反編譯后的結(jié)果多么像匯編語(yǔ)言,其中第一列是源代碼行號(hào),第二列是字節(jié)碼偏移量,第三列是操作指令(也叫操作碼),第四列是指令參數(shù)(也叫操作數(shù))。Python 的字節(jié)碼指令都是成對(duì)出現(xiàn)的,每個(gè)指令都會(huì)帶有一個(gè)指令參數(shù)。
查看字節(jié)碼也可以使用 opcode 模塊:
from?opcode?import?opmap opmap?=?{v:?k?for?k,?v?in?opmap.items()} def?foo(a,?b): ????c?=?a?+?b ????return?c code?=?foo.__code__.co_code for?i?in?range(0,?len(code),?2): ????print("操作碼:?{:<12}?操作數(shù):?{}".format( ????????opmap[code[i]],?code[i+1] ????)) """ 操作碼:?LOAD_FAST????操作數(shù):?0 操作碼:?LOAD_FAST????操作數(shù):?1 操作碼:?BINARY_ADD???操作數(shù):?0 操作碼:?STORE_FAST???操作數(shù):?2 操作碼:?LOAD_FAST????操作數(shù):?2 操作碼:?RETURN_VALUE?操作數(shù):?0 """
總之字節(jié)碼就是一段字節(jié)序列,轉(zhuǎn)成列表之后就是一堆數(shù)字。偶數(shù)位置表示指令本身,而每個(gè)指令后面都會(huì)跟一個(gè)指令參數(shù),也就是奇數(shù)位置表示指令參數(shù)。
所以指令本質(zhì)上只是一個(gè)整數(shù):
虛擬機(jī)會(huì)根據(jù)不同的指令執(zhí)行不同的邏輯,說(shuō)白了 Python 虛擬機(jī)執(zhí)行字節(jié)碼的邏輯就是把自己想象成一顆 CPU,并內(nèi)置了一個(gè)巨型的 switch case 語(yǔ)句,其中每個(gè)指令都對(duì)應(yīng)一個(gè) case 分支。
然后遍歷整條字節(jié)碼,拿到每一個(gè)指令和指令參數(shù)。然后對(duì)指令進(jìn)行判斷,不同的指令進(jìn)入不同的 case 分支,執(zhí)行不同的處理邏輯,直到字節(jié)碼全部執(zhí)行完畢或者程序出錯(cuò)。
到此這篇關(guān)于一文帶你深度解密Python的字節(jié)碼的文章就介紹到這了,更多相關(guān)Python字節(jié)碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python連接mysql數(shù)據(jù)庫(kù)并讀取數(shù)據(jù)的實(shí)現(xiàn)
這篇文章主要介紹了python連接mysql數(shù)據(jù)庫(kù)并讀取數(shù)據(jù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09python實(shí)現(xiàn)機(jī)器學(xué)習(xí)之元線性回歸
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)機(jī)器學(xué)習(xí)之元線性回歸,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09educoder之Python數(shù)值計(jì)算庫(kù)Numpy圖像處理詳解
這篇文章主要為大家介紹了educoder之Python數(shù)值計(jì)算庫(kù)Numpy圖像處理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04python實(shí)現(xiàn)簡(jiǎn)單貪吃蛇小游戲
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)簡(jiǎn)單貪吃蛇小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06Python破解極驗(yàn)滑動(dòng)驗(yàn)證碼詳細(xì)步驟
學(xué)習(xí)python知識(shí)越來(lái)越多,大家都知道極驗(yàn)驗(yàn)證碼應(yīng)用非常廣泛,今天小編就給大家分享Python破解極驗(yàn)滑動(dòng)驗(yàn)證碼的詳細(xì)步驟,對(duì)Python極驗(yàn)滑動(dòng)驗(yàn)證碼相關(guān)知識(shí)感興趣的朋友一起看看吧2021-05-05python雙向鏈表實(shí)現(xiàn)實(shí)例代碼
python雙向鏈表和單鏈表類似,只不過(guò)是增加了一個(gè)指向前面一個(gè)元素的指針,下面的代碼實(shí)例了python雙向鏈表的方法2013-11-11Python 網(wǎng)頁(yè)解析HTMLParse的實(shí)例詳解
這篇文章主要介紹了Python 網(wǎng)頁(yè)解析HTMLParse的實(shí)例詳解的相關(guān)資料,python里提供了一個(gè)簡(jiǎn)單的解析模塊HTMLParser類,使用起來(lái)也是比較簡(jiǎn)單的,解析語(yǔ)法沒有用到XPath類似的簡(jiǎn)潔模式,需要的朋友可以參考下2017-08-08python中使用xlrd讀excel使用xlwt寫excel的實(shí)例代碼
這篇文章主要介紹了python中使用xlrd讀excel使用xlwt寫excel的實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01