官术网_书友最值得收藏!

第1章 回顧Python編程

本書(shū)所要講解的爬蟲(chóng)技術(shù)是基于Python語(yǔ)言進(jìn)行開(kāi)發(fā)的,擁有Python編程能力對(duì)于本書(shū)的學(xué)習(xí)是至關(guān)重要的,因此本章的目標(biāo)是幫助之前接觸過(guò)Python語(yǔ)言的讀者回顧一下Python編程中的內(nèi)容,尤其是與爬蟲(chóng)技術(shù)相關(guān)的內(nèi)容。

1.1 安裝Python

Python是跨平臺(tái)語(yǔ)言,它可以運(yùn)行在Windows、Mac和各種Linux/Unix系統(tǒng)上。在Windows上編寫(xiě)的程序,可以在Mac和Linux上正常運(yùn)行。Python是一種面向?qū)ο蟆⒔忉屝陀?jì)算機(jī)程序設(shè)計(jì)語(yǔ)言,需要Python解釋器進(jìn)行解釋運(yùn)行。目前,Python有兩個(gè)版本,一個(gè)是2.x版,一個(gè)是3.x版,這兩個(gè)版本是不兼容的。現(xiàn)在Python的整體方向是朝著3.x發(fā)展的,但是在發(fā)展過(guò)程中,大量針對(duì)2.x版本的代碼都需要修改才能運(yùn)行,導(dǎo)致現(xiàn)在許多第三方庫(kù)無(wú)法在3.x版本上直接使用,因此現(xiàn)在大部分的云服務(wù)器默認(rèn)的Python版本依然是2.x版。考慮到上述原因,本書(shū)采用的Python版本為2.x,確切地說(shuō)是2.7版本。

1.1.1 Windows上安裝Python

首先,從Python的官方網(wǎng)站www.python.org下載最新的2.7.12版本,地址是https://www.python.org/ftp/python/2.7.12/python-2.7.12.msi。然后,運(yùn)行下載的MSI安裝包,在選擇安裝組件時(shí),勾選上所有的組件,如圖1-1所示。

圖1-1 Python安裝界面

特別要注意勾選pip和Add python.exe to Path,然后一路點(diǎn)擊Next即可完成安裝。

pip是Python安裝擴(kuò)展模塊的工具,通常會(huì)用pip下載擴(kuò)展模塊的源代碼并編譯安裝。

Add python.exe to Path是將Python添加到Windows環(huán)境中。

安裝完成后,打開(kāi)命令提示窗口,輸入python后出現(xiàn)如圖1-2情況,說(shuō)明Python安裝成功。

圖1-2 Python命令行窗口

當(dāng)看到提示符“>>>”就表示我們已經(jīng)在Python交互式環(huán)境中了,可以輸入任何Python代碼,回車(chē)后會(huì)立刻得到執(zhí)行結(jié)果。現(xiàn)在,輸入exit()并回車(chē),就可以退出Python交互式環(huán)境。

1.1.2 Ubuntu上的Python

本書(shū)采用Ubuntu 16.04版本,系統(tǒng)自帶了Python 2.7.11的環(huán)境,如圖1-3所示,所以不需要額外進(jìn)行安裝。

圖1-3 Python環(huán)境

擁有了Python環(huán)境,但為了以后方便安裝擴(kuò)展模塊,還需要安裝python-pip和python-dev,在shell中執(zhí)行:sudo apt-get install python-pip python-dev即可安裝,如圖1-4所示。

圖1-4 安裝pip和python-dev

1.2 搭建開(kāi)發(fā)環(huán)境

俗話(huà)說(shuō):“工欲善其事必先利其器”,在做Python爬蟲(chóng)開(kāi)發(fā)之前,一個(gè)好的IDE將會(huì)使編程效率得到大幅度提高。下面主要介紹兩種IDE:Eclipse和PyCharm,并以在Windows 7上安裝為例進(jìn)行介紹。

1.2.1 Eclipse+PyDev

Eclipse是一個(gè)強(qiáng)大的編輯器,并通過(guò)插件的方式不斷拓展功能。Eclipse比較常見(jiàn)的功能是編寫(xiě)Java程序,但是通過(guò)擴(kuò)展PyDev插件,Eclipse就具有了編寫(xiě)Python程序的功能。所以本書(shū)搭建的開(kāi)發(fā)環(huán)境是Eclipset+PyDev。

Eclipse是運(yùn)行在Java虛擬機(jī)上的,所以要先安裝Java環(huán)境。

第一步,安裝Java環(huán)境。Java JDK的下載地址為:http://www.oracle.com/technetwork/java/javase/downloads/index.html。下載頁(yè)面如圖1-5所示。

圖1-5 JDK下載界面

下載好JDK之后,雙擊進(jìn)行安裝,一直點(diǎn)擊“下一步”即可完成安裝,安裝界面如圖1-6所示。

圖1-6 JDK安裝界面

安裝完JDK,需要配置Java環(huán)境變量。

1)首先右鍵“我的電腦”,選擇“屬性”,如圖1-7所示。

圖1-7 電腦屬性

2)接著在出現(xiàn)的對(duì)話(huà)框中選擇“高級(jí)系統(tǒng)設(shè)置”,如圖1-8所示。

圖1-8 高級(jí)系統(tǒng)設(shè)置

3)在出現(xiàn)的對(duì)話(huà)框中選擇“環(huán)境變量”,如圖1-9所示。

圖1-9 環(huán)境變量

4)新建名為classpath的變量名,變量的值可以設(shè)置為:.; %JAVA_HOME\lib; %JAVA_HOME\lib\tools.jar,如圖1-10所示。

圖1-10 classpath環(huán)境變量

5)新建名為JAVA_HOME的變量名,變量的值為之前安裝的JDK路徑位置,默認(rèn)是C:\Program Files\Java\jdk1.8.0_101\,如圖1-11所示。

圖1-11 JAVA_HOME環(huán)境變量

6)在已有的系統(tǒng)變量path的變量值中加上:; %JAVA_HOME%\bin; %JAVA_HOME%\jre\bin,如圖1-12所示,自此配置完成。

圖1-12 path環(huán)境變量

下面檢驗(yàn)是否配置成功,運(yùn)行cmd命令,在出現(xiàn)的對(duì)話(huà)框中輸入“java-version”命令,如果出現(xiàn)圖1-13的結(jié)果,則表明配置成功。

圖1-13 java-version

第二步,下載Eclipse,下載地址為:http://www.eclipse.org/downloads/eclipse-packages/,下載完后,解壓就可以直接使用,Eclipse不需要安裝。下載界面如圖1-14所示。

圖1-14 下載界面

第三步,在Eclipse中安裝pydev插件。啟動(dòng)Eclipse,點(diǎn)擊Help->Install New Software...,如圖1-15所示。

圖1-15 安裝新軟件

在彈出的對(duì)話(huà)框中,點(diǎn)擊Add按鈕。在Name中填:Pydev,在Location中填http://pydev.org/updates,然后一步一步安裝下去。過(guò)程如圖1-16和圖1-17所示。

圖1-16 安裝過(guò)程1

圖1-17 安裝過(guò)程2

第四步,安裝完pydev插件后,需要配置pydev解釋器。在Eclipse菜單欄中,點(diǎn)擊Windows→Preferences。在對(duì)話(huà)框中,點(diǎn)擊PyDev→Interpreter-Python。點(diǎn)擊New按鈕,選擇python.exe的路徑,打開(kāi)后顯示出一個(gè)包含很多復(fù)選框的窗口,點(diǎn)擊OK即可,如圖1-18所示。

圖1-18 配置PyDev

經(jīng)過(guò)上述四個(gè)步驟,Eclipse就可以進(jìn)行Python開(kāi)發(fā)了。如需創(chuàng)建一個(gè)新的項(xiàng)目,選擇File→New→Projects...,再選擇PyDev→PyDevProject并輸入項(xiàng)目名稱(chēng),點(diǎn)擊Finish即可完成項(xiàng)目的創(chuàng)建,如圖1-19所示。

圖1-19 新建Python工程

然后新建PyDev Package,就可以寫(xiě)代碼了,如圖1-20所示。

圖1-20 新建Python包

1.2.2 PyCharm

PyCharm是本人用過(guò)的Python編輯器中,比較順手,而且可以跨平臺(tái),在MacOS、Linux和Windows下都可以用。PyCharm主要分為專(zhuān)業(yè)版和社區(qū)版,兩者的區(qū)別在于專(zhuān)業(yè)版一開(kāi)始有30天的試用期,之后就要收費(fèi);社區(qū)版一直免費(fèi),當(dāng)然專(zhuān)業(yè)版的功能更加強(qiáng)大。我們進(jìn)行Python爬蟲(chóng)開(kāi)發(fā),社區(qū)版基本上可以滿(mǎn)足需要,所以接下來(lái)就以社區(qū)版為例。大家可以根據(jù)自己的系統(tǒng)版本,進(jìn)行下載安裝,下載地址為:http://www.jetbrains.com/pycharm/download/#。下載界面如圖1-21所示。

圖1-21 下載界面

以Windows為例,下載后雙擊進(jìn)行安裝,一步一步點(diǎn)擊Next,即可完成安裝。安裝界面如圖1-22所示。

圖1-22 安裝界面

安裝完成后,運(yùn)行PyCharm,創(chuàng)建Python項(xiàng)目就可以進(jìn)行Python開(kāi)發(fā)了,如圖1-23所示。

圖1-23 創(chuàng)建項(xiàng)目開(kāi)發(fā)

1.3 IO編程

IO在計(jì)算機(jī)中指的是Input/Output,也就是輸入輸出。凡是用到數(shù)據(jù)交換的地方,都會(huì)涉及IO編程,例如磁盤(pán)、網(wǎng)絡(luò)的數(shù)據(jù)傳輸。在IO編程中,Stream(流)是一種重要的概念,分為輸入流(Input Stream)和輸出流(Output Stream)。我們可以把流理解為一個(gè)水管,數(shù)據(jù)相當(dāng)于水管中的水,但是只能單向流動(dòng),所以數(shù)據(jù)傳輸過(guò)程中需要架設(shè)兩個(gè)水管,一個(gè)負(fù)責(zé)輸入,一個(gè)負(fù)責(zé)輸出,這樣讀寫(xiě)就可以實(shí)現(xiàn)同步。本節(jié)主要講解磁盤(pán)IO操作,網(wǎng)絡(luò)IO操作放到之后的1.5節(jié)進(jìn)行討論。

1.3.1 文件讀寫(xiě)

1.打開(kāi)文件

讀寫(xiě)文件是最常見(jiàn)的IO操作。Python內(nèi)置了讀寫(xiě)文件的函數(shù),方便了文件的IO操作。

文件讀寫(xiě)之前需要打開(kāi)文件,確定文件的讀寫(xiě)模式。open函數(shù)用來(lái)打開(kāi)文件,語(yǔ)法如下:

open(name[.mode[.buffering]])

open函數(shù)使用一個(gè)文件名作為唯一的強(qiáng)制參數(shù),然后返回一個(gè)文件對(duì)象。模式(mode)和緩沖區(qū)(buffering)參數(shù)都是可選的,默認(rèn)模式是讀模式,默認(rèn)緩沖區(qū)是無(wú)。

假設(shè)有個(gè)名為qiye.txt的文本文件,其存儲(chǔ)路徑是c:\text(或者是在Linux下的~/text),那么可以像下面這樣打開(kāi)文件。在交互式環(huán)境的提示符“>>>”下,輸入如下內(nèi)容:

>>> f = open(r'c:\text\qiye.txt')

如果文件不存在,將會(huì)看到一個(gè)類(lèi)似下面的異常回溯:

Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: 'C:\\qiye.txt'

2.文件模式

下面主要說(shuō)一下open函數(shù)中的mode參數(shù)(如表1-1所示),通過(guò)改變mode參數(shù)可以實(shí)現(xiàn)對(duì)文件的不同操作。

表1-1 open函數(shù)中的mode參數(shù)

這里主要是提醒一下’b’參數(shù)的使用,一般處理文本文件時(shí),是用不到’b’參數(shù)的,但處理一些其他類(lèi)型的文件(二進(jìn)制文件),比如mp3音樂(lè)或者圖像,那么應(yīng)該在模式參數(shù)中增加’b',這在爬蟲(chóng)中處理媒體文件很常用。參數(shù)’rb’可以用來(lái)讀取一個(gè)二進(jìn)制文件。

3.文件緩沖區(qū)

open函數(shù)中第三個(gè)可選參數(shù)buffering控制著文件的緩沖。如果參數(shù)是0, I/O操作就是無(wú)緩沖的,直接將數(shù)據(jù)寫(xiě)到硬盤(pán)上;如果參數(shù)是1, I/O操作就是有緩沖的,數(shù)據(jù)先寫(xiě)到內(nèi)存里,只有使用flush函數(shù)或者close函數(shù)才會(huì)將數(shù)據(jù)更新到硬盤(pán);如果參數(shù)為大于1的數(shù)字則代表緩沖區(qū)的大小(單位是字節(jié)), -1(或者是任何負(fù)數(shù))代表使用默認(rèn)緩沖區(qū)的大小。

4.文件讀取

文件讀取主要是分為按字節(jié)讀取和按行進(jìn)行讀取,經(jīng)常用到的方法有read()、readlines()、close()。

在“>>>”輸入f = open(r'c:\text\qiye.txt')后,如果成功打開(kāi)文本文件,接下來(lái)調(diào)用read()方法則可以一次性將文件內(nèi)容全部讀到內(nèi)存中,最后返回的是str類(lèi)型的對(duì)象:

>>> f.read()
"qiye"

最后一步調(diào)用close(),可以關(guān)閉對(duì)文件的引用。文件使用完畢后必須關(guān)閉,因?yàn)槲募?duì)象會(huì)占用操作系統(tǒng)資源,影響系統(tǒng)的IO操作。

>>> f.close()

由于文件操作可能會(huì)出現(xiàn)IO異常,一旦出現(xiàn)IO異常,后面的close()方法就不會(huì)調(diào)用。所以為了保證程序的健壯性,我們需要使用try ... finally來(lái)實(shí)現(xiàn)。

try:
    f = open(r'c:\text\qiye.txt', 'r')
    print f.read()
finally:
    if f:
        f.close()

上面的代碼略長(zhǎng),Python提供了一種簡(jiǎn)單的寫(xiě)法,使用with語(yǔ)句來(lái)替代try ... finally代碼塊和close()方法,如下所示:

with open(r'c:\text\qiye.txt', 'r') as fileReader:
    print fileReader.read()

調(diào)用read()一次將文件內(nèi)容讀到內(nèi)存,但是如果文件過(guò)大,將會(huì)出現(xiàn)內(nèi)存不足的問(wèn)題。一般對(duì)于大文件,可以反復(fù)調(diào)用read(size)方法,一次最多讀取size個(gè)字節(jié)。如果文件是文本文件,Python提供了更加合理的做法,調(diào)用readline()可以每次讀取一行內(nèi)容,調(diào)用readlines()一次讀取所有內(nèi)容并按行返回列表。大家可以根據(jù)自己的具體需求采取不同的讀取方式,例如小文件可以直接采取read()方法讀到內(nèi)存,大文件更加安全的方式是連續(xù)調(diào)用read(size),而對(duì)于配置文件等文本文件,使用readline()方法更加合理。

將上面的代碼進(jìn)行修改,采用readline()的方式實(shí)現(xiàn)如下所示:

with open(r'c:\text\qiye.txt', 'r') as fileReader:
    for line in fileReader.readlines():
        print line.strip()

5.文件寫(xiě)入

寫(xiě)文件和讀文件是一樣的,唯一的區(qū)別是在調(diào)用open方法時(shí),傳入標(biāo)識(shí)符’w’或者’wb'表示寫(xiě)入文本文件或者寫(xiě)入二進(jìn)制文件,示例如下:

f = open(r'c:\text\qiye.txt', 'w')
f.write('qiye')
f.close()

我們可以反復(fù)調(diào)用write()方法寫(xiě)入文件,最后必須使用close()方法來(lái)關(guān)閉文件。使用write()方法的時(shí)候,操作系統(tǒng)不是立即將數(shù)據(jù)寫(xiě)入文件中的,而是先寫(xiě)入內(nèi)存中緩存起來(lái),等到空閑時(shí)候再寫(xiě)入文件中,最后使用close()方法就將數(shù)據(jù)完整地寫(xiě)入文件中了。當(dāng)然也可以使用f.flush()方法,不斷將數(shù)據(jù)立即寫(xiě)入文件中,最后使用close()方法來(lái)關(guān)閉文件。和讀文件同樣道理,文件操作中可能會(huì)出現(xiàn)IO異常,所以還是推薦使用with語(yǔ)句:

with open(r'c:\text\qiye.txt', 'w') as fileWriter:
    fileWriter.write('qiye')

1.3.2 操作文件和目錄

在Python中對(duì)文件和目錄的操作經(jīng)常用到os模塊和shutil模塊。接下來(lái)主要介紹一些操作文件和目錄的常用方法:

□ 獲得當(dāng)前Python腳本工作的目錄路徑:os.getcwd()。

□ 返回指定目錄下的所有文件和目錄名:os.listdir()。例如返回C盤(pán)下的文件:os.listdir("C:\\")

□ 刪除一個(gè)文件:os.remove(filepath)。

□ 刪除多個(gè)空目錄:os.removedirs(r"d:\python")。

□ 檢驗(yàn)給出的路徑是否是一個(gè)文件:os.path.isfile(filepath)。

□ 檢驗(yàn)給出的路徑是否是一個(gè)目錄:os.path.isdir(filepath)。

□ 判斷是否是絕對(duì)路徑:os.path.isabs()。

□ 檢驗(yàn)路徑是否真的存在:os.path.exists()。例如檢測(cè)D盤(pán)下是否有Python文件夾:os.path.exists(r"d:\python")

□ 分離一個(gè)路徑的目錄名和文件名:os.path.split()。例如:os.path.split(r"/home/qiye/qiye.txt"),返回結(jié)果是一個(gè)元組:('/home/qiye', 'qiye.txt')。

□ 分離擴(kuò)展名:os.path.splitext()。例如os.path.splitext(r"/home/qiye/qiye.txt"),返回結(jié)果是一個(gè)元組:('/home/qiye/qiye', '.txt')。

□ 獲取路徑名:os.path.dirname(filetpah)。

□ 獲取文件名:os.path.basename(filepath)。

□ 讀取和設(shè)置環(huán)境變量:os.getenv()與os.putenv()。

□ 給出當(dāng)前平臺(tái)使用的行終止符:os.linesep。Windows使用’\r\n', Linux使用’\n’而Mac使用’\r'。

□ 指示你正在使用的平臺(tái):os.name。對(duì)于Windows,它是’nt',而對(duì)于Linux/Unix用戶(hù),它是’posix'。

□ 重命名文件或者目錄:os.rename(old, new)。

□ 創(chuàng)建多級(jí)目錄:os.makedirs(r"c:\python\test")。

□ 創(chuàng)建單個(gè)目錄:os.mkdir("test")。

□ 獲取文件屬性:os.stat(file)。

□ 修改文件權(quán)限與時(shí)間戳:os.chmod(file)。

□ 獲取文件大小:os.path.getsize(filename)。

□ 復(fù)制文件夾:shutil.copytree("olddir", "newdir")。olddir和newdir都只能是目錄,且newdir必須不存在。

□ 復(fù)制文件:shutil.copyfile("oldfile", "newfile"), oldfile和newfile都只能是文件;shutil. copy("oldfile", "newfile"), oldfile只能是文件,newfile可以是文件,也可以是目標(biāo)目錄。

□ 移動(dòng)文件(目錄):shutil.move("oldpos", "newpos")。

□ 刪除目錄:os.rmdir("dir"),只能刪除空目錄;shutil.rmtree("dir"),空目錄、有內(nèi)容的目錄都可以刪。

1.3.3 序列化操作

對(duì)象的序列化在很多高級(jí)編程語(yǔ)言中都有相應(yīng)的實(shí)現(xiàn),Python也不例外。程序運(yùn)行時(shí),所有的變量都是在內(nèi)存中的,例如在程序中聲明一個(gè)dict對(duì)象,里面存儲(chǔ)著爬取的頁(yè)面的鏈接、頁(yè)面的標(biāo)題、頁(yè)面的摘要等信息:

d = dict(url='index.html', title=’首頁(yè)’, content=’首頁(yè)’)

在程序運(yùn)行的過(guò)程中爬取的頁(yè)面的鏈接會(huì)不斷變化,比如把url改成了second.html,但是程序一結(jié)束或意外中斷,程序中的內(nèi)存變量都會(huì)被操作系統(tǒng)進(jìn)行回收。如果沒(méi)有把修改過(guò)的url存儲(chǔ)起來(lái),下次運(yùn)行程序的時(shí)候,url被初始化為index.html,又是從首頁(yè)開(kāi)始,這是我們不愿意看到的。所以把內(nèi)存中的變量變成可存儲(chǔ)或可傳輸?shù)倪^(guò)程,就是序列化。

將內(nèi)存中的變量序列化之后,可以把序列化后的內(nèi)容寫(xiě)入磁盤(pán),或者通過(guò)網(wǎng)絡(luò)傳輸?shù)絼e的機(jī)器上,實(shí)現(xiàn)程序狀態(tài)的保存和共享。反過(guò)來(lái),把變量?jī)?nèi)容從序列化的對(duì)象重新讀取到內(nèi)存,稱(chēng)為反序列化。

在Python中提供了兩個(gè)模塊:cPickle和pickle來(lái)實(shí)現(xiàn)序列化,前者是由C語(yǔ)言編寫(xiě)的,效率比后者高很多,但是兩個(gè)模塊的功能是一樣的。一般編寫(xiě)程序的時(shí)候,采取的方案是先導(dǎo)入cPickle模塊,如果此模塊不存在,再導(dǎo)入pickle模塊。示例如下:

try:
    import cPickle as pickle
except ImportError:
    import pickle

pickle實(shí)現(xiàn)序列化主要使用的是dumps方法或dump方法。dumps方法可以將任意對(duì)象序列化成一個(gè)str,然后可以將這個(gè)str寫(xiě)入文件進(jìn)行保存。在Python Shell中示例如下:

>>> import cPickle as pickle
>>> d = dict(url='index.html', title=’首頁(yè)’, content=’首頁(yè)’)
>>> pickle.dumps(d)
"(dp1\nS'content'\np2\nS'\\xca\\xd7\\xd2\\xb3'\np3\nsS'url'\np4\nS'index.html'\n
p5\nsS'title'\np6\ng3\ns."

如果使用dump方法,可以將序列化后的對(duì)象直接寫(xiě)入文件中:

>>> f=open(r'D:\dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()

pickle實(shí)現(xiàn)反序列化使用的是loads方法或load方法。把序列化后的文件從磁盤(pán)上讀取為一個(gè)str,然后使用loads方法將這個(gè)str反序列化為對(duì)象,或者直接使用load方法將文件直接反序列化為對(duì)象,如下所示:

>>> f=open(r'D:\dump.txt', 'rb')
>>> d=pickle.load(f)
>>> f.close()
>>> d
{'content': '\xca\xd7\xd2\xb3', 'url': 'index.html', 'title': '\xca\xd7\xd2\xb3'}

通過(guò)反序列化,存儲(chǔ)為文件的dict對(duì)象,又重新恢復(fù)出來(lái),但是這個(gè)變量和原變量沒(méi)有什么關(guān)系,只是內(nèi)容一樣。以上就是序列化操作的整個(gè)過(guò)程。

假如我們想在不同的編程語(yǔ)言之間傳遞對(duì)象,把對(duì)象序列化為標(biāo)準(zhǔn)格式是關(guān)鍵,例如XML,但是現(xiàn)在更加流行的是序列化為JSON格式,既可以被所有的編程語(yǔ)言讀取解析,也可以方便地存儲(chǔ)到磁盤(pán)或者通過(guò)網(wǎng)絡(luò)傳輸。對(duì)于JSON的操作,將在第5章進(jìn)行講解。

1.4 進(jìn)程和線程

在爬蟲(chóng)開(kāi)發(fā)中,進(jìn)程和線程的概念是非常重要的。提高爬蟲(chóng)的工作效率,打造分布式爬蟲(chóng),都離不開(kāi)進(jìn)程和線程的身影。本節(jié)將從多進(jìn)程、多線程、協(xié)程和分布式進(jìn)程等四個(gè)方面,幫助大家回顧Python語(yǔ)言中進(jìn)程和線程中的常用操作,以便在接下來(lái)的爬蟲(chóng)開(kāi)發(fā)中靈活運(yùn)用進(jìn)程和線程。

1.4.1 多進(jìn)程

Python實(shí)現(xiàn)多進(jìn)程的方式主要有兩種,一種方法是使用os模塊中的fork方法,另一種方法是使用multiprocessing模塊。這兩種方法的區(qū)別在于前者僅適用于Unix/Linux操作系統(tǒng),對(duì)Windows不支持,后者則是跨平臺(tái)的實(shí)現(xiàn)方式。由于現(xiàn)在很多爬蟲(chóng)程序都是運(yùn)行在Unix/Linux操作系統(tǒng)上,所以本節(jié)對(duì)兩種方式都進(jìn)行講解。

1.使用os模塊中的fork方式實(shí)現(xiàn)多進(jìn)程

Python的os模塊封裝了常見(jiàn)的系統(tǒng)調(diào)用,其中就有fork方法。fork方法來(lái)自于Unix/Linux操作系統(tǒng)中提供的一個(gè)fork系統(tǒng)調(diào)用,這個(gè)方法非常特殊。普通的方法都是調(diào)用一次,返回一次,而fork方法是調(diào)用一次,返回兩次,原因在于操作系統(tǒng)將當(dāng)前進(jìn)程(父進(jìn)程)復(fù)制出一份進(jìn)程(子進(jìn)程),這兩個(gè)進(jìn)程幾乎完全相同,于是fork方法分別在父進(jìn)程和子進(jìn)程中返回。子進(jìn)程中永遠(yuǎn)返回0,父進(jìn)程中返回的是子進(jìn)程的ID。下面舉個(gè)例子,對(duì)Python使用fork方法創(chuàng)建進(jìn)程進(jìn)行講解。其中os模塊中的getpid方法用于獲取當(dāng)前進(jìn)程的ID, getppid方法用于獲取父進(jìn)程的ID。代碼如下:

import os
if __name__ == '__main__':
    print 'current Process (%s) start ...'%(os.getpid())
    pid = os.fork()
    if pid < 0:
        print 'error in fork'
    elif pid == 0:
        print 'I am child process(%s) and my parent process is (%s)', (os.getpid(),
              os.getppid())
    else:
        print 'I(%s) created a chlid process (%s).', (os.getpid(), pid)

運(yùn)行結(jié)果如下:

current Process (3052) start ...
I(3052) created a chlid process (3053).
I am child process(3053) and my parent process is (3052)

2.使用multiprocessing模塊創(chuàng)建多進(jìn)程

multiprocessing模塊提供了一個(gè)Process類(lèi)來(lái)描述一個(gè)進(jìn)程對(duì)象。創(chuàng)建子進(jìn)程時(shí),只需要傳入一個(gè)執(zhí)行函數(shù)和函數(shù)的參數(shù),即可完成一個(gè)Process實(shí)例的創(chuàng)建,用start()方法啟動(dòng)進(jìn)程,用join()方法實(shí)現(xiàn)進(jìn)程間的同步。下面通過(guò)一個(gè)例子來(lái)演示創(chuàng)建多進(jìn)程的流程,代碼如下:

import os
from multiprocessing import Process
# 子進(jìn)程要執(zhí)行的代碼
def run_proc(name):
    print 'Child process %s (%s) Running...' % (name, os.getpid())
if __name__ == '__main__':
    print 'Parent process %s.' % os.getpid()
    for i in range(5):
        p = Process(target=run_proc, args=(str(i), ))
        print 'Process will start.'
        p.start()
    p.join()
    print 'Process end.'

運(yùn)行結(jié)果如下:

Parent process 2392.
Process will start.
Process will start.
Process will start.
Process will start.
Process will start.
Child process 2 (10748) Running...
Child process 0 (5324) Running...
Child process 1 (3196) Running...
Child process 3 (4680) Running...
Child process 4 (10696) Running...
Process end.

以上介紹了創(chuàng)建進(jìn)程的兩種方法,但是要啟動(dòng)大量的子進(jìn)程,使用進(jìn)程池批量創(chuàng)建子進(jìn)程的方式更加常見(jiàn),因?yàn)楫?dāng)被操作對(duì)象數(shù)目不大時(shí),可以直接利用multiprocessing中的Process動(dòng)態(tài)生成多個(gè)進(jìn)程,如果是上百個(gè)、上千個(gè)目標(biāo),手動(dòng)去限制進(jìn)程數(shù)量卻又太過(guò)繁瑣,這時(shí)候進(jìn)程池Pool發(fā)揮作用的時(shí)候就到了。

3.multiprocessing模塊提供了一個(gè)Pool類(lèi)來(lái)代表進(jìn)程池對(duì)象

Pool可以提供指定數(shù)量的進(jìn)程供用戶(hù)調(diào)用,默認(rèn)大小是CPU的核數(shù)。當(dāng)有新的請(qǐng)求提交到Pool中時(shí),如果池還沒(méi)有滿(mǎn),那么就會(huì)創(chuàng)建一個(gè)新的進(jìn)程用來(lái)執(zhí)行該請(qǐng)求;但如果池中的進(jìn)程數(shù)已經(jīng)達(dá)到規(guī)定最大值,那么該請(qǐng)求就會(huì)等待,直到池中有進(jìn)程結(jié)束,才會(huì)創(chuàng)建新的進(jìn)程來(lái)處理它。下面通過(guò)一個(gè)例子來(lái)演示進(jìn)程池的工作流程,代碼如下:

from multiprocessing import Pool
import os, time, random
def run_task(name):
    print 'Task %s (pid = %s) is running...' % (name, os.getpid())
    time.sleep(random.random() * 3)
    print 'Task %s end.' % name
if __name__=='__main__':
    print 'Current process %s.' % os.getpid()
    p = Pool(processes=3)
    for i in range(5):
        p.apply_async(run_task, args=(i, ))
    print 'Waiting for all subprocesses done...'
    p.close()
    p.join()
    print 'All subprocesses done.'

運(yùn)行結(jié)果如下:

Current process 9176.
Waiting for all subprocesses done...
Task 0 (pid = 11012) is running...
Task 1 (pid = 12464) is running...
Task 2 (pid = 11260) is running...
Task 2 end.
Task 3 (pid = 11260) is running...
Task 0 end.
Task 4 (pid = 11012) is running...
Task 1 end.
Task 3 end.
Task 4 end.
All subprocesses done.

上述程序先創(chuàng)建了容量為3的進(jìn)程池,依次向進(jìn)程池中添加了5個(gè)任務(wù)。從運(yùn)行結(jié)果中可以看到雖然添加了5個(gè)任務(wù),但是一開(kāi)始只運(yùn)行了3個(gè),而且每次最多運(yùn)行3個(gè)進(jìn)程。當(dāng)一個(gè)任務(wù)結(jié)束了,新的任務(wù)依次添加進(jìn)來(lái),任務(wù)執(zhí)行使用的進(jìn)程依然是原來(lái)的進(jìn)程,這一點(diǎn)通過(guò)進(jìn)程的pid就可以看出來(lái)。

注意

Pool對(duì)象調(diào)用join()方法會(huì)等待所有子進(jìn)程執(zhí)行完畢,調(diào)用join()之前必須先調(diào)用close(),調(diào)用close()之后就不能繼續(xù)添加新的Process了。

4.進(jìn)程間通信

假如創(chuàng)建了大量的進(jìn)程,那進(jìn)程間通信是必不可少的。Python提供了多種進(jìn)程間通信的方式,例如Queue、Pipe、Value+Array等。本節(jié)主要講解Queue和Pipe這兩種方式。Queue和Pipe的區(qū)別在于Pipe常用來(lái)在兩個(gè)進(jìn)程間通信,Queue用來(lái)在多個(gè)進(jìn)程間實(shí)現(xiàn)通信。

首先講解一下Queue通信方式。Queue是多進(jìn)程安全的隊(duì)列,可以使用Queue實(shí)現(xiàn)多進(jìn)程之間的數(shù)據(jù)傳遞。有兩個(gè)方法:Put和Get可以進(jìn)行Queue操作:

□ Put方法用以插入數(shù)據(jù)到隊(duì)列中,它還有兩個(gè)可選參數(shù):blocked和timeout。如果blocked為T(mén)rue(默認(rèn)值),并且timeout為正值,該方法會(huì)阻塞timeout指定的時(shí)間,直到該隊(duì)列有剩余的空間。如果超時(shí),會(huì)拋出Queue.Full異常。如果blocked為False,但該Queue已滿(mǎn),會(huì)立即拋出Queue.Full異常。

□ Get方法可以從隊(duì)列讀取并且刪除一個(gè)元素。同樣,Get方法有兩個(gè)可選參數(shù):blocked和timeout。如果blocked為T(mén)rue(默認(rèn)值),并且timeout為正值,那么在等待時(shí)間內(nèi)沒(méi)有取到任何元素,會(huì)拋出Queue.Empty異常。如果blocked為False,分兩種情況:如果Queue有一個(gè)值可用,則立即返回該值;否則,如果隊(duì)列為空,則立即拋出Queue.Empty異常。

下面通過(guò)一個(gè)例子進(jìn)行說(shuō)明:在父進(jìn)程中創(chuàng)建三個(gè)子進(jìn)程,兩個(gè)子進(jìn)程往Queue中寫(xiě)入數(shù)據(jù),一個(gè)子進(jìn)程從Queue中讀取數(shù)據(jù)。程序示例如下:

from multiprocessing import Process, Queue
import os, time, random

# 寫(xiě)數(shù)據(jù)進(jìn)程執(zhí)行的代碼:
def proc_write(q, urls):
    print('Process(%s) is writing...' % os.getpid())
    for url in urls:
        q.put(url)
        print('Put %s to queue...' % url)
        time.sleep(random.random())

# 讀數(shù)據(jù)進(jìn)程執(zhí)行的代碼:
def proc_read(q):
    print('Process(%s) is reading...' % os.getpid())
    while True:
        url = q.get(True)
        print('Get %s from queue.' % url)

if __name__=='__main__':
    # 父進(jìn)程創(chuàng)建Queue,并傳給各個(gè)子進(jìn)程:
    q = Queue()
    proc_writer1 = Process(target=proc_write, args=(q, ['url_1', 'url_2', 'url_3']))
    proc_writer2 = Process(target=proc_write, args=(q, ['url_4', 'url_5', 'url_6']))
    proc_reader = Process(target=proc_read, args=(q, ))
    # 啟動(dòng)子進(jìn)程proc_writer,寫(xiě)入:
    proc_writer1.start()
    proc_writer2.start()
    # 啟動(dòng)子進(jìn)程proc_reader,讀取:
    proc_reader.start()
    # 等待proc_writer結(jié)束:
    proc_writer1.join()
    proc_writer2.join()
    # proc_reader進(jìn)程里是死循環(huán),無(wú)法等待其結(jié)束,只能強(qiáng)行終止:
    proc_reader.terminate()

運(yùn)行結(jié)果如下:

Process(9968) is writing...
Process(9512) is writing...
Put url_1 to queue...
Put url_4 to queue...
Process(1124) is reading...
Get url_1 from queue.
Get url_4 from queue.
Put url_5 to queue...
Get url_5 from queue.
Put url_2 to queue...
Get url_2 from queue.
Put url_6 to queue...
Get url_6 from queue.
Put url_3 to queue...
Get url_3 from queue.

最后介紹一下Pipe的通信機(jī)制,Pipe常用來(lái)在兩個(gè)進(jìn)程間進(jìn)行通信,兩個(gè)進(jìn)程分別位于管道的兩端。

Pipe方法返回(conn1, conn2)代表一個(gè)管道的兩個(gè)端。Pipe方法有duplex參數(shù),如果duplex參數(shù)為T(mén)rue(默認(rèn)值),那么這個(gè)管道是全雙工模式,也就是說(shuō)conn1和conn2均可收發(fā)。若duplex為False, conn1只負(fù)責(zé)接收消息,conn2只負(fù)責(zé)發(fā)送消息。send和recv方法分別是發(fā)送和接收消息的方法。例如,在全雙工模式下,可以調(diào)用conn1.send發(fā)送消息,conn1.recv接收消息。如果沒(méi)有消息可接收,recv方法會(huì)一直阻塞。如果管道已經(jīng)被關(guān)閉,那么recv方法會(huì)拋出EOFError。

下面通過(guò)一個(gè)例子進(jìn)行說(shuō)明:創(chuàng)建兩個(gè)進(jìn)程,一個(gè)子進(jìn)程通過(guò)Pipe發(fā)送數(shù)據(jù),一個(gè)子進(jìn)程通過(guò)Pipe接收數(shù)據(jù)。程序示例如下:

import multiprocessing
import random
import time, os

def proc_send(pipe, urls):
    for url in urls:
        print "Process(%s) send: %s" %(os.getpid(), url)
        pipe.send(url)
        time.sleep(random.random())

def proc_recv(pipe):
    while True:
        print "Process(%s) rev:%s" %(os.getpid(), pipe.recv())
        time.sleep(random.random())

if __name__ == "__main__":
    pipe = multiprocessing.Pipe()
    p1 = multiprocessing.Process(target=proc_send, args=(pipe[0], ['url_'+str(i)
          for i in range(10) ]))
    p2 = multiprocessing.Process(target=proc_recv, args=(pipe[1], ))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

運(yùn)行結(jié)果如下:

Process(10448) send: url_0
Process(5832) rev:url_0
Process(10448) send: url_1
Process(5832) rev:url_1
Process(10448) send: url_2
Process(5832) rev:url_2
Process(10448) send: url_3
Process(10448) send: url_4
Process(5832) rev:url_3
Process(10448) send: url_5
Process(10448) send: url_6
Process(5832) rev:url_4
Process(5832) rev:url_5
Process(10448) send: url_7
Process(10448) send: url_8
Process(5832) rev:url_6
Process(5832) rev:url_7
Process(10448) send: url_9
Process(5832) rev:url_8
Process(5832) rev:url_9

注意

以上多進(jìn)程程序運(yùn)行結(jié)果的打印順序在不同的系統(tǒng)和硬件條件下略有不同。

1.4.2 多線程

多線程類(lèi)似于同時(shí)執(zhí)行多個(gè)不同程序,多線程運(yùn)行有如下優(yōu)點(diǎn):

□ 可以把運(yùn)行時(shí)間長(zhǎng)的任務(wù)放到后臺(tái)去處理。

□ 用戶(hù)界面可以更加吸引人,比如用戶(hù)點(diǎn)擊了一個(gè)按鈕去觸發(fā)某些事件的處理,可以彈出一個(gè)進(jìn)度條來(lái)顯示處理的進(jìn)度。

□ 程序的運(yùn)行速度可能加快。

□ 在一些需要等待的任務(wù)實(shí)現(xiàn)上,如用戶(hù)輸入、文件讀寫(xiě)和網(wǎng)絡(luò)收發(fā)數(shù)據(jù)等,線程就比較有用了。在這種情況下我們可以釋放一些珍貴的資源,如內(nèi)存占用等。

Python的標(biāo)準(zhǔn)庫(kù)提供了兩個(gè)模塊:thread和threading, thread是低級(jí)模塊,threading是高級(jí)模塊,對(duì)thread進(jìn)行了封裝。絕大多數(shù)情況下,我們只需要使用threading這個(gè)高級(jí)模塊。

1.用threading模塊創(chuàng)建多線程

threading模塊一般通過(guò)兩種方式創(chuàng)建多線程:第一種方式是把一個(gè)函數(shù)傳入并創(chuàng)建Thread實(shí)例,然后調(diào)用start方法開(kāi)始執(zhí)行;第二種方式是直接從threading.Thread繼承并創(chuàng)建線程類(lèi),然后重寫(xiě)__init__方法和run方法。

首先介紹第一種方法,通過(guò)一個(gè)簡(jiǎn)單例子演示創(chuàng)建多線程的流程,程序如下:

import random
import time, threading
# 新線程執(zhí)行的代碼:
def thread_run(urls):
    print 'Current %s is running...' % threading.current_thread().name
    for url in urls:
        print '%s ---->>> %s' % (threading.current_thread().name, url)
        time.sleep(random.random())
    print '%s ended.' % threading.current_thread().name

print '%s is running...' % threading.current_thread().name
t1 = threading.Thread(target=thread_run, name='Thread_1', args=(['url_1', 'url_2', '
    url_3'], ))
t2 = threading.Thread(target=thread_run, name='Thread_2', args=(['url_4', 'url_5', '
    url_6'], ))
t1.start()
t2.start()
t1.join()
t2.join()
print '%s ended.' % threading.current_thread().name

運(yùn)行結(jié)果如下:

MainThread is running...
Current Thread_1 is running...
Thread_1---->>> url_1
Current Thread_2 is running...
Thread_2---->>> url_4
Thread_1---->>> url_2
Thread_2---->>> url_5
Thread_2---->>> url_6
Thread_1---->>> url_3
Thread_1 ended.
Thread_2 ended.
MainThread ended.

第二種方式從threading.Thread繼承創(chuàng)建線程類(lèi),下面將方法一的程序進(jìn)行重寫(xiě),程序如下:

import random
import threading
import time
class myThread(threading.Thread):
    def __init__(self, name, urls):
        threading.Thread.__init__(self, name=name)
        self.urls = urls

    def run(self):
        print 'Current %s is running...' % threading.current_thread().name
        for url in self.urls:
              print '%s ---->>> %s' % (threading.current_thread().name, url)
              time.sleep(random.random())
        print '%s ended.' % threading.current_thread().name
print '%s is running...' % threading.current_thread().name
t1 = myThread(name='Thread_1', urls=['url_1', 'url_2', 'url_3'])
t2 = myThread(name='Thread_2', urls=['url_4', 'url_5', 'url_6'])
t1.start()
t2.start()
t1.join()
t2.join()
print '%s ended.' % threading.current_thread().name

運(yùn)行結(jié)果如下:

MainThread is running...
Current Thread_1 is running...
Thread_1---->>> url_1
Current Thread_2 is running...
Thread_2---->>> url_4
Thread_2---->>> url_5
Thread_1---->>> url_2
Thread_1---->>> url_3
Thread_2---->>> url_6
Thread_2 ended.
Thread_1 ended.

2.線程同步

如果多個(gè)線程共同對(duì)某個(gè)數(shù)據(jù)修改,則可能出現(xiàn)不可預(yù)料的結(jié)果,為了保證數(shù)據(jù)的正確性,需要對(duì)多個(gè)線程進(jìn)行同步。使用Thread對(duì)象的Lock和RLock可以實(shí)現(xiàn)簡(jiǎn)單的線程同步,這兩個(gè)對(duì)象都有acquire方法和release方法,對(duì)于那些每次只允許一個(gè)線程操作的數(shù)據(jù),可以將其操作放到acquire和release方法之間。

對(duì)于Lock對(duì)象而言,如果一個(gè)線程連續(xù)兩次進(jìn)行acquire操作,那么由于第一次acquire之后沒(méi)有release,第二次acquire將掛起線程。這會(huì)導(dǎo)致Lock對(duì)象永遠(yuǎn)不會(huì)release,使得線程死鎖。RLock對(duì)象允許一個(gè)線程多次對(duì)其進(jìn)行acquire操作,因?yàn)樵谄鋬?nèi)部通過(guò)一個(gè)counter變量維護(hù)著線程acquire的次數(shù)。而且每一次的acquire操作必須有一個(gè)release操作與之對(duì)應(yīng),在所有的release操作完成之后,別的線程才能申請(qǐng)?jiān)揜Lock對(duì)象。下面通過(guò)一個(gè)簡(jiǎn)單的例子演示線程同步的過(guò)程:

import threading
mylock = threading.RLock()
num=0
class myThread(threading.Thread):

    def __init__(self, name):
        threading.Thread.__init__(self, name=name)
    def run(self):
        global num
        while True:
              mylock.acquire()
              print '%s locked, Number: %d'%(threading.current_thread().name, num)
              if num>=4:
                  mylock.release()
                  print '%s released, Number: %d'%(threading.current_thread().name, num)
                  break
              num+=1
              print '%s released, Number: %d'%(threading.current_thread().name, num)
              mylock.release()

if __name__== '__main__':
    thread1 = myThread('Thread_1')
    thread2 = myThread('Thread_2')
    thread1.start()
    thread2.start()

運(yùn)行結(jié)果如下:

Thread_1 locked, Number: 0
Thread_1 released, Number: 1
Thread_1 locked, Number: 1
Thread_1 released, Number: 2
Thread_2 locked, Number: 2
Thread_2 released, Number: 3
Thread_1 locked, Number: 3
Thread_1 released, Number: 4
Thread_2 locked, Number: 4
Thread_2 released, Number: 4
Thread_1 locked, Number: 4
Thread_1 released, Number: 4

3.全局解釋器鎖(GIL)

在Python的原始解釋器CPython中存在著GIL(Global Interpreter Lock,全局解釋器鎖),因此在解釋執(zhí)行Python代碼時(shí),會(huì)產(chǎn)生互斥鎖來(lái)限制線程對(duì)共享資源的訪問(wèn),直到解釋器遇到I/O操作或者操作次數(shù)達(dá)到一定數(shù)目時(shí)才會(huì)釋放GIL。由于全局解釋器鎖的存在,在進(jìn)行多線程操作的時(shí)候,不能調(diào)用多個(gè)CPU內(nèi)核,只能利用一個(gè)內(nèi)核,所以在進(jìn)行CPU密集型操作的時(shí)候,不推薦使用多線程,更加傾向于多進(jìn)程。那么多線程適合什么樣的應(yīng)用場(chǎng)景呢?對(duì)于IO密集型操作,多線程可以明顯提高效率,例如Python爬蟲(chóng)的開(kāi)發(fā),絕大多數(shù)時(shí)間爬蟲(chóng)是在等待socket返回?cái)?shù)據(jù),網(wǎng)絡(luò)IO的操作延時(shí)比CPU大得多。

1.4.3 協(xié)程

協(xié)程(coroutine),又稱(chēng)微線程,纖程,是一種用戶(hù)級(jí)的輕量級(jí)線程。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來(lái)的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧。因此協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài),每次過(guò)程重入時(shí),就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài)。在并發(fā)編程中,協(xié)程與線程類(lèi)似,每個(gè)協(xié)程表示一個(gè)執(zhí)行單元,有自己的本地?cái)?shù)據(jù),與其他協(xié)程共享全局?jǐn)?shù)據(jù)和其他資源。

協(xié)程需要用戶(hù)自己來(lái)編寫(xiě)調(diào)度邏輯,對(duì)于CPU來(lái)說(shuō),協(xié)程其實(shí)是單線程,所以CPU不用去考慮怎么調(diào)度、切換上下文,這就省去了CPU的切換開(kāi)銷(xiāo),所以協(xié)程在一定程度上又好于多線程。那么在Python中是如何實(shí)現(xiàn)協(xié)程的呢?

Python通過(guò)yield提供了對(duì)協(xié)程的基本支持,但是不完全,而使用第三方gevent庫(kù)是更好的選擇,gevent提供了比較完善的協(xié)程支持。gevent是一個(gè)基于協(xié)程的Python網(wǎng)絡(luò)函數(shù)庫(kù),使用greenlet在libev事件循環(huán)頂部提供了一個(gè)有高級(jí)別并發(fā)性的API。主要特性有以下幾點(diǎn):

□ 基于libev的快速事件循環(huán),Linux上是epoll機(jī)制。

□ 基于greenlet的輕量級(jí)執(zhí)行單元。

□ API復(fù)用了Python標(biāo)準(zhǔn)庫(kù)里的內(nèi)容。

□ 支持SSL的協(xié)作式sockets。

□ 可通過(guò)線程池或c-ares實(shí)現(xiàn)DNS查詢(xún)。

□ 通過(guò)monkey patching功能使得第三方模塊變成協(xié)作式。

gevent對(duì)協(xié)程的支持,本質(zhì)上是greenlet在實(shí)現(xiàn)切換工作。greenlet工作流程如下:假如進(jìn)行訪問(wèn)網(wǎng)絡(luò)的IO操作時(shí),出現(xiàn)阻塞,greenlet就顯式切換到另一段沒(méi)有被阻塞的代碼段執(zhí)行,直到原先的阻塞狀況消失以后,再自動(dòng)切換回原來(lái)的代碼段繼續(xù)處理。因此,greenlet是一種合理安排的串行方式。

由于IO操作非常耗時(shí),經(jīng)常使程序處于等待狀態(tài),有了gevent為我們自動(dòng)切換協(xié)程,就保證總有g(shù)reenlet在運(yùn)行,而不是等待IO,這就是協(xié)程一般比多線程效率高的原因。由于切換是在IO操作時(shí)自動(dòng)完成,所以gevent需要修改Python自帶的一些標(biāo)準(zhǔn)庫(kù),將一些常見(jiàn)的阻塞,如socket、select等地方實(shí)現(xiàn)協(xié)程跳轉(zhuǎn),這一過(guò)程在啟動(dòng)時(shí)通過(guò)monkey patch完成。下面通過(guò)一個(gè)的例子來(lái)演示gevent的使用流程,代碼如下:

from gevent import monkey; monkey.patch_all()
import gevent
import urllib2

def run_task(url):
    print 'Visit --> %s' % url
    try:
        response = urllib2.urlopen(url)
        data = response.read()
        print '%d bytes received from %s.' % (len(data), url)
    except Exception, e:
        print e
if __name__=='__main__':
    urls = ['https://github.com/', 'https://www.python.org/', 'http://www.cnblogs.com/']
    greenlets = [gevent.spawn(run_task, url) for url in urls  ]
    gevent.joinall(greenlets)

運(yùn)行結(jié)果如下:

Visit --> https://github.com/
Visit --> https://www.python.org/
Visit --> http://www.cnblogs.com/
45740 bytes received from http://www.cnblogs.com/.
25482 bytes received from https://github.com/.
47445 bytes received from https://www.python.org/.

以上程序主要用了gevent中的spawn方法和joinall方法。spawn方法可以看做是用來(lái)形成協(xié)程,joinall方法就是添加這些協(xié)程任務(wù),并且啟動(dòng)運(yùn)行。從運(yùn)行結(jié)果來(lái)看,3個(gè)網(wǎng)絡(luò)操作是并發(fā)執(zhí)行的,而且結(jié)束順序不同,但其實(shí)只有一個(gè)線程。

gevent中還提供了對(duì)池的支持。當(dāng)擁有動(dòng)態(tài)數(shù)量的greenlet需要進(jìn)行并發(fā)管理(限制并發(fā)數(shù))時(shí),就可以使用池,這在處理大量的網(wǎng)絡(luò)和IO操作時(shí)是非常需要的。接下來(lái)使用gevent中pool對(duì)象,對(duì)上面的例子進(jìn)行改寫(xiě),程序如下:

from gevent import monkey
monkey.patch_all()
import urllib2
from gevent.pool import Pool
def run_task(url):
    print 'Visit --> %s' % url
    try:
        response = urllib2.urlopen(url)
        data = response.read()
        print '%d bytes received from %s.' % (len(data), url)
    except Exception, e:
        print e
    return 'url:%s --->finish'% url

if __name__=='__main__':
    pool = Pool(2)
    urls = ['https://github.com/', 'https://www.python.org/', 'http://www.cnblogs.com/']
    results = pool.map(run_task, urls)
    print results

運(yùn)行結(jié)果如下:

Visit --> https://github.com/
Visit --> https://www.python.org/
25482 bytes received from https://github.com/.
Visit --> http://www.cnblogs.com/
47445 bytes received from https://www.python.org/.
45687 bytes received from http://www.cnblogs.com/.
['url:https://github.com/ --->finish', 'url:https://www.python.org/ --->finish',
    'url:http://www.cnblogs.com/    --->finish']

通過(guò)運(yùn)行結(jié)果可以看出,Pool對(duì)象確實(shí)對(duì)協(xié)程的并發(fā)數(shù)量進(jìn)行了管理,先訪問(wèn)了前兩個(gè)網(wǎng)址,當(dāng)其中一個(gè)任務(wù)完成時(shí),才會(huì)執(zhí)行第三個(gè)。

1.4.4 分布式進(jìn)程

分布式進(jìn)程指的是將Process進(jìn)程分布到多臺(tái)機(jī)器上,充分利用多臺(tái)機(jī)器的性能完成復(fù)雜的任務(wù)。我們可以將這一點(diǎn)應(yīng)用到分布式爬蟲(chóng)的開(kāi)發(fā)中。

分布式進(jìn)程在Python中依然要用到multiprocessing模塊。multiprocessing模塊不但支持多進(jìn)程,其中managers子模塊還支持把多進(jìn)程分布到多臺(tái)機(jī)器上。可以寫(xiě)一個(gè)服務(wù)進(jìn)程作為調(diào)度者,將任務(wù)分布到其他多個(gè)進(jìn)程中,依靠網(wǎng)絡(luò)通信進(jìn)行管理。舉個(gè)例子:在做爬蟲(chóng)程序時(shí),常常會(huì)遇到這樣的場(chǎng)景,我們想抓取某個(gè)網(wǎng)站的所有圖片,如果使用多進(jìn)程的話(huà),一般是一個(gè)進(jìn)程負(fù)責(zé)抓取圖片的鏈接地址,將鏈接地址存放到Queue中,另外的進(jìn)程負(fù)責(zé)從Queue中讀取鏈接地址進(jìn)行下載和存儲(chǔ)到本地。現(xiàn)在把這個(gè)過(guò)程做成分布式,一臺(tái)機(jī)器上的進(jìn)程負(fù)責(zé)抓取鏈接,其他機(jī)器上的進(jìn)程負(fù)責(zé)下載存儲(chǔ)。那么遇到的主要問(wèn)題是將Queue暴露到網(wǎng)絡(luò)中,讓其他機(jī)器進(jìn)程都可以訪問(wèn),分布式進(jìn)程就是將這一個(gè)過(guò)程進(jìn)行了封裝,我們可以將這個(gè)過(guò)程稱(chēng)為本地隊(duì)列的網(wǎng)絡(luò)化。整體過(guò)程如圖1-24所示。

圖1-24 分布式進(jìn)程

要實(shí)現(xiàn)上面例子的功能,創(chuàng)建分布式進(jìn)程需要分為六個(gè)步驟:

1)建立隊(duì)列Queue,用來(lái)進(jìn)行進(jìn)程間的通信。服務(wù)進(jìn)程創(chuàng)建任務(wù)隊(duì)列task_queue,用來(lái)作為傳遞任務(wù)給任務(wù)進(jìn)程的通道;服務(wù)進(jìn)程創(chuàng)建結(jié)果隊(duì)列result_queue,作為任務(wù)進(jìn)程完成任務(wù)后回復(fù)服務(wù)進(jìn)程的通道。在分布式多進(jìn)程環(huán)境下,必須通過(guò)由Queuemanager獲得的Queue接口來(lái)添加任務(wù)。

2)把第一步中建立的隊(duì)列在網(wǎng)絡(luò)上注冊(cè),暴露給其他進(jìn)程(主機(jī)),注冊(cè)后獲得網(wǎng)絡(luò)隊(duì)列,相當(dāng)于本地隊(duì)列的映像。

3)建立一個(gè)對(duì)象(Queuemanager(BaseManager))實(shí)例manager,綁定端口和驗(yàn)證口令。

4)啟動(dòng)第三步中建立的實(shí)例,即啟動(dòng)管理manager,監(jiān)管信息通道。

5)通過(guò)管理實(shí)例的方法獲得通過(guò)網(wǎng)絡(luò)訪問(wèn)的Queue對(duì)象,即再把網(wǎng)絡(luò)隊(duì)列實(shí)體化成可以使用的本地隊(duì)列。

6)創(chuàng)建任務(wù)到“本地”隊(duì)列中,自動(dòng)上傳任務(wù)到網(wǎng)絡(luò)隊(duì)列中,分配給任務(wù)進(jìn)程進(jìn)行處理。

接下來(lái)通過(guò)程序?qū)崿F(xiàn)上面的例子(Linux版),首先編寫(xiě)的是服務(wù)進(jìn)程(taskManager.py),代碼如下:

import random, time, Queue
from multiprocessing.managers import BaseManager
# 第一步:建立task_queue和result_queue,用來(lái)存放任務(wù)和結(jié)果
task_queue=Queue.Queue()
result_queue=Queue.Queue()

class Queuemanager(BaseManager):
    pass
# 第二步:把創(chuàng)建的兩個(gè)隊(duì)列注冊(cè)在網(wǎng)絡(luò)上,利用register方法,callable參數(shù)關(guān)聯(lián)了Queue對(duì)象,
# 將Queue對(duì)象在網(wǎng)絡(luò)中暴露
Queuemanager.register('get_task_queue', callable=lambda:task_queue)
Queuemanager.register('get_result_queue', callable=lambda:result_queue)

# 第三步:綁定端口8001,設(shè)置驗(yàn)證口令‘qiye'。這個(gè)相當(dāng)于對(duì)象的初始化
manager=Queuemanager(address=('',8001), authkey='qiye')

# 第四步:?jiǎn)?dòng)管理,監(jiān)聽(tīng)信息通道
manager.start()

# 第五步:通過(guò)管理實(shí)例的方法獲得通過(guò)網(wǎng)絡(luò)訪問(wèn)的Queue對(duì)象
task=manager.get_task_queue()
result=manager.get_result_queue()

# 第六步:添加任務(wù)
for url in ["ImageUrl_"+i for i in range(10)]:
    print 'put task %s ...' %url
    task.put(url)
# 獲取返回結(jié)果
print 'try get result...'
for i in range(10):
    print 'result is %s' %result.get(timeout=10)
# 關(guān)閉管理
manager.shutdown()

任務(wù)進(jìn)程已經(jīng)編寫(xiě)完成,接下來(lái)編寫(xiě)任務(wù)進(jìn)程(taskWorker.py),創(chuàng)建任務(wù)進(jìn)程的步驟相對(duì)較少,需要四個(gè)步驟:

1)使用QueueManager注冊(cè)用于獲取Queue的方法名稱(chēng),任務(wù)進(jìn)程只能通過(guò)名稱(chēng)來(lái)在網(wǎng)絡(luò)上獲取Queue。

2)連接服務(wù)器,端口和驗(yàn)證口令注意保持與服務(wù)進(jìn)程中完全一致。

3)從網(wǎng)絡(luò)上獲取Queue,進(jìn)行本地化。

4)從task隊(duì)列獲取任務(wù),并把結(jié)果寫(xiě)入result隊(duì)列。

程序taskWorker.py代碼(win/linux版)如下:

# coding:utf-8
import time
from multiprocessing.managers import BaseManager
# 創(chuàng)建類(lèi)似的QueueManager:
class QueueManager(BaseManager):
    pass
# 第一步:使用QueueManager注冊(cè)用于獲取Queue的方法名稱(chēng)
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
# 第二步:連接到服務(wù)器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和驗(yàn)證口令注意保持與服務(wù)進(jìn)程完全一致:
m = QueueManager(address=(server_addr, 8001), authkey='qiye')
# 從網(wǎng)絡(luò)連接:
m.connect()
# 第三步:獲取Queue的對(duì)象:
task = m.get_task_queue()
result = m.get_result_queue()
# 第四步:從task隊(duì)列獲取任務(wù),并把結(jié)果寫(xiě)入result隊(duì)列:
while(not task.empty()):
        image_url = task.get(True, timeout=5)
        print('run task download %s...' % image_url)
        time.sleep(1)
        result.put('%s--->success'%image_url)

# 處理結(jié)束:
print('worker exit.')

最后開(kāi)始運(yùn)行程序,先啟動(dòng)服務(wù)進(jìn)程taskManager.py,運(yùn)行結(jié)果如下:

put task ImageUrl_0 ...
put task ImageUrl_1 ...
put task ImageUrl_2 ...
put task ImageUrl_3 ...
put task ImageUrl_4 ...
put task ImageUrl_5 ...
put task ImageUrl_6 ...
put task ImageUrl_7 ...
put task ImageUrl_8 ...
put task ImageUrl_9 ...
try get result...

接著再啟動(dòng)任務(wù)進(jìn)程taskWorker.py,運(yùn)行結(jié)果如下:

Connect to server 127.0.0.1...
run task download ImageUrl_0...
run task download ImageUrl_1...
run task download ImageUrl_2...
run task download ImageUrl_3...
run task download ImageUrl_4...
run task download ImageUrl_5...
run task download ImageUrl_6...
run task download ImageUrl_7...
run task download ImageUrl_8...
run task download ImageUrl_9...
worker exit.

當(dāng)任務(wù)進(jìn)程運(yùn)行結(jié)束后,服務(wù)進(jìn)程運(yùn)行結(jié)果如下:

result is ImageUrl_0--->success
result is ImageUrl_1--->success
result is ImageUrl_2--->success
result is ImageUrl_3--->success
result is ImageUrl_4--->success
result is ImageUrl_5--->success
result is ImageUrl_6--->success
result is ImageUrl_7--->success
result is ImageUrl_8--->success
result is ImageUrl_9--->success

其實(shí)這就是一個(gè)簡(jiǎn)單但真正的分布式計(jì)算,把代碼稍加改造,啟動(dòng)多個(gè)worker,就可以把任務(wù)分布到幾臺(tái)甚至幾十臺(tái)機(jī)器上,實(shí)現(xiàn)大規(guī)模的分布式爬蟲(chóng)。

注意

由于平臺(tái)的特性,創(chuàng)建服務(wù)進(jìn)程的代碼在Linux和Windows上有一些不同,創(chuàng)建工作進(jìn)程的代碼是一致的。

taskManager.py程序在Windows版下的代碼如下:

# coding:utf-8
# taskManager.py for windows
import Queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
# 任務(wù)個(gè)數(shù)
task_number = 10
# 定義收發(fā)隊(duì)列
task_queue = Queue.Queue(task_number);
result_queue = Queue.Queue(task_number);
def get_task():
    return task_queue
def get_result():
      return result_queue
# 創(chuàng)建類(lèi)似的QueueManager:
class QueueManager(BaseManager):
    pass
def win_run():
    # Windows下綁定調(diào)用接口不能使用lambda,所以只能先定義函數(shù)再綁定
    QueueManager.register('get_task_queue', callable = get_task)
    QueueManager.register('get_result_queue', callable = get_result)
    # 綁定端口并設(shè)置驗(yàn)證口令,Windows下需要填寫(xiě)IP地址,Linux下不填默認(rèn)為本地
    manager = QueueManager(address = ('127.0.0.1',8001), authkey = 'qiye')
    # 啟動(dòng)
    manager.start()
    try:
        # 通過(guò)網(wǎng)絡(luò)獲取任務(wù)隊(duì)列和結(jié)果隊(duì)列
        task = manager.get_task_queue()
        result = manager.get_result_queue()
        # 添加任務(wù)
        for url in ["ImageUrl_"+str(i) for i in range(10)]:
              print 'put task %s ...' %url
              task.put(url)
        print 'try get result...'
        for i in range(10):
              print 'result is %s' %result.get(timeout=10)
    except:
        print('Manager error')
    finally:
        # 一定要關(guān)閉,否則會(huì)報(bào)管道未關(guān)閉的錯(cuò)誤
        manager.shutdown()

if __name__ == '__main__':
    # Windows下多進(jìn)程可能會(huì)有問(wèn)題,添加這句可以緩解
    freeze_support()
    win_run()

1.5 網(wǎng)絡(luò)編程

既然是做爬蟲(chóng)開(kāi)發(fā),必然需要了解Python網(wǎng)絡(luò)編程方面的知識(shí)。計(jì)算機(jī)網(wǎng)絡(luò)是把各個(gè)計(jì)算機(jī)連接到一起,讓網(wǎng)絡(luò)中的計(jì)算機(jī)可以互相通信。網(wǎng)絡(luò)編程就是如何在程序中實(shí)現(xiàn)兩臺(tái)計(jì)算機(jī)的通信。例如當(dāng)你使用瀏覽器訪問(wèn)谷歌網(wǎng)站時(shí),你的計(jì)算機(jī)就和谷歌的某臺(tái)服務(wù)器通過(guò)互聯(lián)網(wǎng)建立起了連接,然后谷歌服務(wù)器會(huì)把把網(wǎng)頁(yè)內(nèi)容作為數(shù)據(jù)通過(guò)互聯(lián)網(wǎng)傳輸?shù)侥愕碾娔X上。

網(wǎng)絡(luò)編程對(duì)所有開(kāi)發(fā)語(yǔ)言都是一樣的,Python也不例外。使用Python進(jìn)行網(wǎng)絡(luò)編程時(shí),實(shí)際上是在Python程序本身這個(gè)進(jìn)程內(nèi),連接到指定服務(wù)器進(jìn)程的通信端口進(jìn)行通信,所以網(wǎng)絡(luò)通信也可以看做兩個(gè)進(jìn)程間的通信。

提到網(wǎng)絡(luò)編程,必須提到的一個(gè)概念是Socket。Socket(套接字)是網(wǎng)絡(luò)編程的一個(gè)抽象概念,通常我們用一個(gè)Socket表示“打開(kāi)了一個(gè)網(wǎng)絡(luò)鏈接”,而打開(kāi)一個(gè)Socket需要知道目標(biāo)計(jì)算機(jī)的IP地址和端口號(hào),再指定協(xié)議類(lèi)型即可。Python提供了兩個(gè)基本的Socket模塊:

□ Socket,提供了標(biāo)準(zhǔn)的BSD Sockets API。

□ SocketServer,提供了服務(wù)器中心類(lèi),可以簡(jiǎn)化網(wǎng)絡(luò)服務(wù)器的開(kāi)發(fā)。

下面講一下Socket模塊功能。

1.Socket類(lèi)型

套接字格式為:socket(family, type[, protocal]),使用給定的地址族、套接字類(lèi)型(如表1-2所示)、協(xié)議編號(hào)(默認(rèn)為0)來(lái)創(chuàng)建套接字。

表1-2 Socket類(lèi)型及說(shuō)明

2.Socket函數(shù)

表1-3列舉了Python網(wǎng)絡(luò)編程常用的函數(shù),其中包括了TCP和UDP。

表1-3 Socket函數(shù)及說(shuō)明

本節(jié)接下來(lái)主要介紹Python中TCP和UDP兩種網(wǎng)絡(luò)類(lèi)型的編程流程。

1.5.1 TCP編程

網(wǎng)絡(luò)編程一般包括兩部分:服務(wù)端和客戶(hù)端。TCP是一種面向連接的通信方式,主動(dòng)發(fā)起連接的叫客戶(hù)端,被動(dòng)響應(yīng)連接的叫服務(wù)端。首先說(shuō)一下服務(wù)端,創(chuàng)建和運(yùn)行TCP服務(wù)端一般需要五個(gè)步驟:

1)創(chuàng)建Socket,綁定Socket到本地IP與端口。

2)開(kāi)始監(jiān)聽(tīng)連接。

3)進(jìn)入循環(huán),不斷接收客戶(hù)端的連接請(qǐng)求。

4)接收傳來(lái)的數(shù)據(jù),并發(fā)送給對(duì)方數(shù)據(jù)。

5)傳輸完畢后,關(guān)閉Socket。

下面通過(guò)一個(gè)例子演示創(chuàng)建TCP服務(wù)端的過(guò)程,程序如下:

# coding:utf-8
import socket
import threading
import time
def dealClient(sock, addr):
    # 第四步:接收傳來(lái)的數(shù)據(jù),并發(fā)送給對(duì)方數(shù)據(jù)
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Hello, I am server! ')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
              break
        print '-->>%s! ' % data.decode('utf-8')
        sock.send(('Loop_Msg: %s! ' % data.decode('utf-8')).encode('utf-8'))
    # 第五步:關(guān)閉Socket
    sock.close()
    print('Connection from %s:%s closed.' % addr)
if __name__=="__main__":
    # 第一步:創(chuàng)建一個(gè)基于IPv4和TCP協(xié)議的Socket
    # Socket綁定的IP(127.0.0.1為本機(jī)IP)與端口
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('127.0.0.1', 9999))
    # 第二步:監(jiān)聽(tīng)連接
    s.listen(5)
    print('Waiting for connection...')
    while True:
        # 第三步:接收一個(gè)新連接:
        sock, addr = s.accept()
        # 創(chuàng)建新線程來(lái)處理TCP連接:
        t = threading.Thread(target=dealClient, args=(sock, addr))
        t.start()

接著編寫(xiě)客戶(hù)端,與服務(wù)端進(jìn)行交互,TCP客戶(hù)端的創(chuàng)建和運(yùn)行需要三個(gè)步驟:

1)創(chuàng)建Socket,連接遠(yuǎn)端地址。

2)連接后發(fā)送數(shù)據(jù)和接收數(shù)據(jù)。

3)傳輸完畢后,關(guān)閉Socket。

程序如下:

# coding:utf-8
import socket
# 初始化Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連接目標(biāo)的IP和端口
s.connect(('127.0.0.1', 9999))
# 接收消息
print('-->>'+s.recv(1024).decode('utf-8'))
# 發(fā)送消息
s.send(b'Hello, I am a client')
print('-->>'+s.recv(1024).decode('utf-8'))
s.send(b'exit')
# 關(guān)閉Socket
s.close()

最后看一下運(yùn)行結(jié)果,先啟動(dòng)服務(wù)端,再啟動(dòng)客戶(hù)端。服務(wù)端打印的信息如下:

Waiting for connection...
Accept new connection from 127.0.0.1:20164...
-->>Hello, I am a client!
Connection from 127.0.0.1:20164 closed.

客戶(hù)端輸出信息如下:

-->>Hello, I am server!
-->>Loop_Msg: Hello, I am a client!

以上完成了TCP客戶(hù)端與服務(wù)端的交互流程,用TCP協(xié)議進(jìn)行Socket編程在Python中十分簡(jiǎn)單。對(duì)于客戶(hù)端,要主動(dòng)連接服務(wù)器的IP和指定端口;對(duì)于服務(wù)器,要首先監(jiān)聽(tīng)指定端口,然后,對(duì)每一個(gè)新的連接,創(chuàng)建一個(gè)線程或進(jìn)程來(lái)處理。通常,服務(wù)器程序會(huì)無(wú)限運(yùn)行下去。

1.5.2 UDP編程

TCP通信需要一個(gè)建立可靠連接的過(guò)程,而且通信雙方以流的形式發(fā)送數(shù)據(jù)。相對(duì)于TCP, UDP則是面向無(wú)連接的協(xié)議。使用UDP協(xié)議時(shí),不需要建立連接,只需要知道對(duì)方的IP地址和端口號(hào),就可以直接發(fā)數(shù)據(jù)包,但是不關(guān)心是否能到達(dá)目的端。雖然用UDP傳輸數(shù)據(jù)不可靠,但是由于它沒(méi)有建立連接的過(guò)程,速度比TCP快得多,對(duì)于不要求可靠到達(dá)的數(shù)據(jù),就可以使用UDP協(xié)議。

使用UDP協(xié)議,和TCP一樣,也有服務(wù)端和客戶(hù)端之分。UDP編程相對(duì)于TCP編程比較簡(jiǎn)單,服務(wù)端創(chuàng)建和運(yùn)行只需要三個(gè)步驟:

1)創(chuàng)建Socket,綁定指定的IP和端口。

2)直接發(fā)送數(shù)據(jù)和接收數(shù)據(jù)。

3)關(guān)閉Socket。

示例程序如下:

# coding:utf-8
import socket
# 創(chuàng)建Socket,綁定指定的IP和端口
# SOCK_DGRAM指定了這個(gè)Socket的類(lèi)型是UDP,綁定端口和TCP示例一樣。
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 9999))
print('Bind UDP on 9999...')
while True:
    # 直接發(fā)送數(shù)據(jù)和接收數(shù)據(jù)
    data, addr = s.recvfrom(1024)
    print('Received from %s:%s.' % addr)
    s.sendto(b'Hello, %s! ' % data, addr)

客戶(hù)端的創(chuàng)建和運(yùn)行更加簡(jiǎn)單,創(chuàng)建Socket,直接可以與服務(wù)端進(jìn)行數(shù)據(jù)交換,示例如下:

# coding:utf-8
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Hello', b'World']:
    # 發(fā)送數(shù)據(jù):
    s.sendto(data, ('127.0.0.1', 9999))
    # 接收數(shù)據(jù):
    print(s.recv(1024).decode('utf-8'))
s.close()

以上就是UDP服務(wù)端和客戶(hù)端數(shù)據(jù)交互的流程,UDP的使用與TCP類(lèi)似,但是不需要建立連接。此外,服務(wù)器綁定UDP端口和TCP端口互不沖突,即UDP的9999端口與TCP的9999端口可以各自綁定。

1.6 小結(jié)

本章主要講解了Python的編程基礎(chǔ),包括IO編程、進(jìn)程和線程、網(wǎng)絡(luò)編程等三個(gè)方面。這三個(gè)方面在Python爬蟲(chóng)開(kāi)發(fā)中經(jīng)常用到,熟悉這些知識(shí)點(diǎn),對(duì)于之后的開(kāi)發(fā)將起到事半功倍的效果。如果對(duì)于Python編程基礎(chǔ)不是很熟練,希望能將本章講的三個(gè)知識(shí)點(diǎn)著重復(fù)習(xí),將書(shū)中的例子靈活運(yùn)用并加以改進(jìn)。

主站蜘蛛池模板: 禄丰县| 库尔勒市| 永嘉县| 青浦区| 昆山市| 鲁甸县| 清镇市| 壤塘县| 乐都县| 蒙山县| 乳山市| 鱼台县| 布拖县| 浑源县| 平凉市| 亚东县| 鲁山县| 兴隆县| 车致| 句容市| 蓬安县| 望城县| 荣昌县| 云和县| 东乡县| 额敏县| 桑植县| 台中市| 蕲春县| 和田市| 江陵县| 噶尔县| 会宁县| 塔城市| 加查县| 嘉鱼县| 黄梅县| 遵义市| 临湘市| 克什克腾旗| 金湖县|