版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
用Python寫網(wǎng)絡(luò)爬蟲(第2版)目錄\h第1章網(wǎng)絡(luò)爬蟲簡(jiǎn)介\h1.1網(wǎng)絡(luò)爬蟲何時(shí)有用\h1.2網(wǎng)絡(luò)爬蟲是否合法\h1.3Python3\h1.4背景調(diào)研\(zhòng)h1.4.1檢查robots.txt\h1.4.2檢查網(wǎng)站地圖\h1.4.3估算網(wǎng)站大小\h1.4.4識(shí)別網(wǎng)站所用技術(shù)\h1.4.5尋找網(wǎng)站所有者\(yùn)h1.5編寫第一個(gè)網(wǎng)絡(luò)爬蟲\h1.5.1抓取與爬取的對(duì)比\h1.5.2下載網(wǎng)頁(yè)\h1.5.3網(wǎng)站地圖爬蟲\h1.5.4ID遍歷爬蟲\h1.5.5鏈接爬蟲\h1.5.6使用requests庫(kù)\h1.6本章小結(jié)\h第2章數(shù)據(jù)抓取\h2.1分析網(wǎng)頁(yè)\h2.23種網(wǎng)頁(yè)抓取方法\h2.2.1正則表達(dá)式\h2.2.2BeautifulSoup\h2.2.3Lxml\h2.3CSS選擇器和瀏覽器控制臺(tái)\h2.4XPath選擇器\h2.5LXML和家族樹\h2.6性能對(duì)比\h2.7抓取結(jié)果\h2.7.1抓取總結(jié)\h2.7.2為鏈接爬蟲添加抓取回調(diào)\h2.8本章小結(jié)\h第3章下載緩存\h3.1何時(shí)使用緩存\h3.2為鏈接爬蟲添加緩存支持\h3.3磁盤緩存\h3.3.1實(shí)現(xiàn)磁盤緩存\h3.3.2緩存測(cè)試\h3.3.3節(jié)省磁盤空間\h3.3.4清理過(guò)期數(shù)據(jù)\h3.3.5磁盤緩存缺點(diǎn)\h3.4鍵值對(duì)存儲(chǔ)緩存\h3.4.1鍵值對(duì)存儲(chǔ)是什么\h3.4.2安裝Redis\h3.4.3Redis概述\h3.4.4Redis緩存實(shí)現(xiàn)\h3.4.5壓縮\h3.4.6測(cè)試緩存\h3.4.7探索requests-cache\h3.5本章小結(jié)\h第4章并發(fā)下載\h4.1100萬(wàn)個(gè)網(wǎng)頁(yè)\h4.1.1解析Alexa列表\h4.2串行爬蟲\h4.3多線程爬蟲\h4.4線程和進(jìn)程如何工作\h4.4.1實(shí)現(xiàn)多線程爬蟲\h4.4.2多進(jìn)程爬蟲\h4.5性能\h4.5.1Python多進(jìn)程與GIL\h4.6本章小結(jié)\h第5章動(dòng)態(tài)內(nèi)容\h5.1動(dòng)態(tài)網(wǎng)頁(yè)示例\h5.2對(duì)動(dòng)態(tài)網(wǎng)頁(yè)進(jìn)行逆向工程\h5.2.1邊界情況\h5.3渲染動(dòng)態(tài)網(wǎng)頁(yè)\h5.3.1PyQt還是PySide\h5.3.2執(zhí)行JavaScript\h5.3.3使用WebKit與網(wǎng)站交互\h5.4渲染類\h5.4.1Selenium\h5.5本章小結(jié)\h第6章表單交互\h6.1登錄表單\h6.1.1從瀏覽器加載cookie\h6.2支持內(nèi)容更新的登錄腳本擴(kuò)展\h6.3使用Selenium實(shí)現(xiàn)自動(dòng)化表單處理\h6.3.1網(wǎng)絡(luò)抓取時(shí)的“人類化”方法\h6.4本章小結(jié)\h第7章驗(yàn)證碼處理\h7.1注冊(cè)賬號(hào)\h7.1.1加載驗(yàn)證碼圖像\h7.2光學(xué)字符識(shí)別\h7.2.1進(jìn)一步改善\h7.3處理復(fù)雜驗(yàn)證碼\h7.4使用驗(yàn)證碼處理服務(wù)\h7.4.19kw入門\h7.4.2報(bào)告錯(cuò)誤\h7.4.3與注冊(cè)功能集成\h7.5驗(yàn)證碼與機(jī)器學(xué)習(xí)\h7.6本章小結(jié)\h第8章Scrapy\h8.1安裝Scrapy\h8.2啟動(dòng)項(xiàng)目\h8.2.1定義模型\h8.2.2創(chuàng)建爬蟲\h8.3不同的爬蟲類型\h8.4使用shell命令抓取\h8.4.1檢查結(jié)果\h8.4.2中斷與恢復(fù)爬蟲\h8.5使用Portia編寫可視化爬蟲\h8.5.1安裝\h8.5.2標(biāo)注\h8.5.3運(yùn)行爬蟲\h8.5.4檢查結(jié)果\h8.6使用Scrapely實(shí)現(xiàn)自動(dòng)化抓取\h8.7本章小結(jié)\h第9章綜合應(yīng)用\h9.1Google搜索引擎\h9.2Facebook\h9.2.1網(wǎng)站\h9.2.2FacebookAPI\h9.3Gap\h9.4寶馬\h9.5本章小結(jié)第1章網(wǎng)絡(luò)爬蟲簡(jiǎn)介歡迎來(lái)到網(wǎng)絡(luò)爬蟲的廣闊天地!網(wǎng)絡(luò)爬蟲被用于許多領(lǐng)域,收集不太容易以其他格式獲取的數(shù)據(jù)。你可能是正在撰寫新報(bào)道的記者,也可能是正在抽取新數(shù)據(jù)集的數(shù)據(jù)科學(xué)家。即使你只是臨時(shí)的開(kāi)發(fā)人員,網(wǎng)絡(luò)爬蟲也是非常有用的工具,比如當(dāng)你需要檢查大學(xué)網(wǎng)站上最新的家庭作業(yè)并且希望通過(guò)郵件發(fā)送給你時(shí)。無(wú)論你的動(dòng)機(jī)是什么,我們都希望你已經(jīng)準(zhǔn)備好開(kāi)始學(xué)習(xí)了!在本章中,我們將介紹如下主題:網(wǎng)絡(luò)爬蟲領(lǐng)域簡(jiǎn)介;解釋合法性質(zhì)疑;介紹Python3安裝;對(duì)目標(biāo)網(wǎng)站進(jìn)行背景調(diào)研;逐步完善一個(gè)高級(jí)網(wǎng)絡(luò)爬蟲;使用非標(biāo)準(zhǔn)庫(kù)協(xié)助抓取網(wǎng)站。1.1網(wǎng)絡(luò)爬蟲何時(shí)有用假設(shè)我有一個(gè)鞋店,并且想要及時(shí)了解競(jìng)爭(zhēng)對(duì)手的價(jià)格。我可以每天訪問(wèn)他們的網(wǎng)站,與我店鋪中鞋子的價(jià)格進(jìn)行對(duì)比。但是,如果我店鋪中的鞋類品種繁多,或是希望能夠更加頻繁地查看價(jià)格變化的話,就需要花費(fèi)大量的時(shí)間,甚至難以實(shí)現(xiàn)。再舉一個(gè)例子,我看中了一雙鞋,想等到它促銷時(shí)再購(gòu)買。我可能需要每天訪問(wèn)這家鞋店的網(wǎng)站來(lái)查看這雙鞋是否降價(jià),也許需要等待幾個(gè)月的時(shí)間,我才能如愿盼到這雙鞋促銷。上述這兩個(gè)重復(fù)性的手工流程,都可以利用本書介紹的網(wǎng)絡(luò)爬蟲技術(shù)實(shí)現(xiàn)自動(dòng)化處理。在理想狀態(tài)下,網(wǎng)絡(luò)爬蟲并不是必需品,每個(gè)網(wǎng)站都應(yīng)該提供API,以結(jié)構(gòu)化的格式共享它們的數(shù)據(jù)。然而在現(xiàn)實(shí)情況中,雖然一些網(wǎng)站已經(jīng)提供了這種API,但是它們通常會(huì)限制可以抓取的數(shù)據(jù),以及訪問(wèn)這些數(shù)據(jù)的頻率。另外,網(wǎng)站開(kāi)發(fā)人員可能會(huì)變更、移除或限制其后端API??傊?,我們不能僅僅依賴于API去訪問(wèn)我們所需的在線數(shù)據(jù),而是應(yīng)該學(xué)習(xí)一些網(wǎng)絡(luò)爬蟲技術(shù)的相關(guān)知識(shí)。1.2網(wǎng)絡(luò)爬蟲是否合法盡管在過(guò)去20年間已經(jīng)做出了諸多相關(guān)裁決,不過(guò)網(wǎng)絡(luò)爬蟲及其使用時(shí)法律所允許的內(nèi)容仍然處于建設(shè)當(dāng)中。如果被抓取的數(shù)據(jù)用于個(gè)人用途,且在合理使用版權(quán)法的情況下,通常沒(méi)有問(wèn)題。但是,如果這些數(shù)據(jù)會(huì)被重新發(fā)布,并且抓取行為的攻擊性過(guò)強(qiáng)導(dǎo)致網(wǎng)站宕機(jī),或者其內(nèi)容受版權(quán)保護(hù),抓取行為違反了其服務(wù)條款的話,那么則有一些法律判例可以提及。在FeistPublications,Inc.起訴RuralTelephoneServiceCo.的案件中,美國(guó)聯(lián)邦最高法院裁定抓取并轉(zhuǎn)載真實(shí)數(shù)據(jù)(比如,電話清單)是允許的。在澳大利亞,TelstraCorporationLimited起訴PhoneDirectoriesCompanyPtyLtd這一類似案件中,則裁定只有擁有明確作者的數(shù)據(jù),才可以受到版權(quán)的保護(hù)。而在另一起發(fā)生于美國(guó)的美聯(lián)社起訴融文集團(tuán)的內(nèi)容抓取案件中,則裁定對(duì)美聯(lián)社新聞重新聚合為新產(chǎn)品的行為是侵犯版權(quán)的。此外,在歐盟的ofir.dk起訴home.dk一案中,最終裁定定期抓取和深度鏈接是允許的。還有一些案件中,原告控告一些公司抓取強(qiáng)度過(guò)大,嘗試通過(guò)法律手段停止其抓取行為。在最近的QVC訴訟Resultly的案件中,最終裁定除非抓取行為造成了私人財(cái)產(chǎn)損失,否則不能被認(rèn)定為故意侵害,即使爬蟲活動(dòng)導(dǎo)致了部分站點(diǎn)的可用性問(wèn)題。這些案件告訴我們,當(dāng)抓取的數(shù)據(jù)是現(xiàn)實(shí)生活中真實(shí)的公共數(shù)據(jù)(比如,營(yíng)業(yè)地址、電話清單)時(shí),在遵守合理的使用規(guī)則的情況下是允許轉(zhuǎn)載的。但是,如果是原創(chuàng)數(shù)據(jù)(比如,意見(jiàn)和評(píng)論或用戶隱私數(shù)據(jù)),通常就會(huì)受到版權(quán)限制,而不能轉(zhuǎn)載。無(wú)論如何,當(dāng)你抓取某個(gè)網(wǎng)站的數(shù)據(jù)時(shí),請(qǐng)記住自己是該網(wǎng)站的訪客,應(yīng)當(dāng)約束自己的抓取行為,否則他們可能會(huì)封禁你的IP,甚至采取更進(jìn)一步的法律行動(dòng)。這就要求下載請(qǐng)求的速度需要限定在一個(gè)合理值之內(nèi),并且還需要設(shè)定一個(gè)專屬的用戶代理來(lái)標(biāo)識(shí)自己的爬蟲。你還應(yīng)該設(shè)法查看網(wǎng)站的服務(wù)條款,確保你所獲取的數(shù)據(jù)不是私有或受版權(quán)保護(hù)的內(nèi)容。如果你還有疑慮或問(wèn)題,可以向媒體律師咨詢你所在地區(qū)的相關(guān)判例。你可以自行搜索下述法律案件的更多信息。FeistPublicationsInc.起訴RuralTelephoneServiceCo.的案件。TelstraCorporationLimited起訴PhoneDirectoriesCompanyPvtLtd的案件。美聯(lián)社起訴融文集團(tuán)的案件。ofir.dk起訴home.dk的案件。QVC起訴Resultly的案件。1.3Python3在本書中,我們將完全使用Python3進(jìn)行開(kāi)發(fā)。Python軟件基金會(huì)已經(jīng)宣布Python2將會(huì)被逐步淘汰,并且只支持到2020年;出于該原因,我們和許多其他Python愛(ài)好者一樣,已經(jīng)將開(kāi)發(fā)轉(zhuǎn)移到對(duì)Python3的支持當(dāng)中,在本書中我們將使用3.6版本。本書代碼將兼容Python3.4+的版本。如果你熟悉PythonVirtualEnvironments或Anaconda的使用,那么你可能已經(jīng)知道如何在一個(gè)新環(huán)境中創(chuàng)建Python3了。如果你希望以全局形式安裝Python3,那么我們推薦你搜索自己使用的操作系統(tǒng)的特定文檔。就我而言,我會(huì)直接使用VirtualEnvironmentWrapper(https://virtualenvwrapper.readthedocs.io/en/latest),這樣就可以很容易地對(duì)不同項(xiàng)目和Python版本使用多個(gè)不同的環(huán)境了。使用Conda環(huán)境或虛擬環(huán)境是最為推薦的,這樣你就可以輕松變更基于項(xiàng)目需求的依賴,而不會(huì)影響到你正在做的其他工作了。對(duì)于初學(xué)者來(lái)說(shuō),我推薦使用Conda,因?yàn)槠湫枰陌惭b工作更少一些。Conda的介紹文檔(https://conda.io/docs/intro.html)是一個(gè)不錯(cuò)的開(kāi)始!從此刻開(kāi)始,所有代碼和命令都假設(shè)你已正確安裝Python3并且正在使用Python3.4+的環(huán)境。如果你看到了導(dǎo)入或語(yǔ)法錯(cuò)誤,請(qǐng)檢查你是否處于正確的環(huán)境當(dāng)中,查看跟蹤信息中是否存在Python2.7的文件路徑。1.4背景調(diào)研在深入討論爬取一個(gè)網(wǎng)站之前,我們首先需要對(duì)目標(biāo)站點(diǎn)的規(guī)模和結(jié)構(gòu)進(jìn)行一定程度的了解。網(wǎng)站自身的robots.txt和Sitemap文件都可以為我們提供一定的幫助,此外還有一些能提供更詳細(xì)信息的外部工具,比如Google搜索和WHOIS。1.4.1檢查robots.txt大多數(shù)網(wǎng)站都會(huì)定義robots.txt文件,這樣可以讓爬蟲了解爬取該網(wǎng)站時(shí)存在哪些限制。這些限制雖然是僅僅作為建議給出,但是良好的網(wǎng)絡(luò)公民都應(yīng)當(dāng)遵守這些限制。在爬取之前,檢查robots.txt文件這一寶貴資源可以將爬蟲被封禁的可能性降至最低,而且還能發(fā)現(xiàn)和網(wǎng)站結(jié)構(gòu)相關(guān)的線索。關(guān)于robots.txt協(xié)議的更多信息可以參見(jiàn)。下面的代碼是我們的示例文件robots.txt中的內(nèi)容,可以訪問(wèn)/robots.txt獲取。#section1
User-agent:BadCrawler
Disallow:/
#section2
User-agent:*
Crawl-delay:5
Disallow:/trap
#section3
Sitemap:/sitemap.xml
在section1中,robots.txt文件禁止用戶代理為BadCrawler的爬蟲爬取該網(wǎng)站,不過(guò)這種寫法可能無(wú)法起到應(yīng)有的作用,因?yàn)閻阂馀老x根本不會(huì)遵從robots.txt的要求。本章后面的一個(gè)例子將會(huì)展示如何讓爬蟲自動(dòng)遵守robots.txt的要求。section2規(guī)定,無(wú)論使用哪種用戶代理,都應(yīng)該在兩次下載請(qǐng)求之間給出5秒的抓取延遲,我們需要遵從該建議以避免服務(wù)器過(guò)載。這里還有一個(gè)/trap鏈接,用于封禁那些爬取了不允許訪問(wèn)的鏈接的惡意爬蟲。如果你訪問(wèn)了這個(gè)鏈接,服務(wù)器就會(huì)封禁你的IP一分鐘!一個(gè)真實(shí)的網(wǎng)站可能會(huì)對(duì)你的IP封禁更長(zhǎng)時(shí)間,甚至是永久封禁。不過(guò)如果這樣設(shè)置的話,我們就無(wú)法繼續(xù)這個(gè)例子了。section3定義了一個(gè)Sitemap文件,我們將在下一節(jié)中了解如何檢查該文件。1.4.2檢查網(wǎng)站地圖網(wǎng)站提供的Sitemap文件(即網(wǎng)站地圖)可以幫助爬蟲定位網(wǎng)站最新的內(nèi)容,而無(wú)須爬取每一個(gè)網(wǎng)頁(yè)。如果想要了解更多信息,可以從/protocol.html獲取網(wǎng)站地圖標(biāo)準(zhǔn)的定義。許多網(wǎng)站發(fā)布平臺(tái)都有自動(dòng)生成網(wǎng)站地圖的能力。下面是在robots.txt文件中定位到的Sitemap文件的內(nèi)容。<?xmlversion="1.0"encoding="UTF-8"?>
<urlsetxmlns="/schemas/sitemap/0.9">
<url><loc>/view/Afghanistan-1</loc>
</url>
<url><loc>/view/Aland-Islands-2</loc>
</url>
<url><loc>/view/Albania-3</loc>
</url>
...
</urlset>
網(wǎng)站地圖提供了所有網(wǎng)頁(yè)的鏈接,我們會(huì)在后面的小節(jié)中使用這些信息,用于創(chuàng)建我們的第一個(gè)爬蟲。雖然Sitemap文件提供了一種爬取網(wǎng)站的有效方式,但是我們?nèi)孕鑼?duì)其謹(jǐn)慎處理,因?yàn)樵撐募赡艽嬖谌笔А⑦^(guò)期或不完整的問(wèn)題。1.4.3估算網(wǎng)站大小目標(biāo)網(wǎng)站的大小會(huì)影響我們?nèi)绾芜M(jìn)行爬取。如果是像我們的示例站點(diǎn)這樣只有幾百個(gè)URL的網(wǎng)站,效率并沒(méi)有那么重要;但如果是擁有數(shù)百萬(wàn)個(gè)網(wǎng)頁(yè)的站點(diǎn),使用串行下載可能需要持續(xù)數(shù)月才能完成,這時(shí)就需要使用第4章中介紹的分布式下載來(lái)解決了。估算網(wǎng)站大小的一個(gè)簡(jiǎn)便方法是檢查Google爬蟲的結(jié)果,因?yàn)镚oogle很可能已經(jīng)爬取過(guò)我們感興趣的網(wǎng)站。我們可以通過(guò)Google搜索的site關(guān)鍵詞過(guò)濾域名結(jié)果,從而獲取該信息。我們可以從/advanced_search了解到該接口及其他高級(jí)搜索參數(shù)的用法。在域名后面添加URL路徑,可以對(duì)結(jié)果進(jìn)行過(guò)濾,僅顯示網(wǎng)站的某些部分。同樣,你的結(jié)果可能會(huì)有所不同;不過(guò),這種附加的過(guò)濾條件非常有用,因?yàn)樵诶硐肭闆r下,你只希望爬取網(wǎng)站中包含有用數(shù)據(jù)的部分,而不是爬取網(wǎng)站的每個(gè)頁(yè)面。1.4.4識(shí)別網(wǎng)站所用技術(shù)構(gòu)建網(wǎng)站所使用的技術(shù)類型也會(huì)對(duì)我們?nèi)绾闻廊‘a(chǎn)生影響。有一個(gè)十分有用的工具可以檢查網(wǎng)站構(gòu)建的技術(shù)類型——detectem模塊,該模塊需要Python3.5+環(huán)境以及Docker。如果你還沒(méi)有安裝Docker,可以遵照/products/overview中你使用的操作系統(tǒng)所對(duì)應(yīng)的說(shuō)明操作。當(dāng)Docker安裝好后,你可以運(yùn)行如下命令。dockerpullscrapinghub/splash
pipinstalldetectem
上述操作將從ScrapingHub拉取最新的Docker鏡像,并通過(guò)pip安裝該庫(kù)。為了確保不受任何更新或改動(dòng)的影響,推薦使用Python虛擬環(huán)境(/3/library/venv.html)或Conda環(huán)境(https://conda.io/docs/using/envs.html),并查看項(xiàng)目的ReadMe頁(yè)面(/spectresearch/detectem)。為什么使用環(huán)境?假設(shè)你的項(xiàng)目使用了早期版本的庫(kù)進(jìn)行開(kāi)發(fā)(比如detectem),而在最新的版本中,detectem引入了一些向后不兼容的變更,造成你的項(xiàng)目無(wú)法正常工作。但是,你正在開(kāi)發(fā)的其他項(xiàng)目中,可能使用了更新的版本。如果你的項(xiàng)目使用系統(tǒng)中安裝的detectem,那么當(dāng)更新庫(kù)以支持其他項(xiàng)目時(shí),該項(xiàng)目就會(huì)無(wú)法運(yùn)行。IanBicking的virtualenv為解決該問(wèn)題提供了一個(gè)巧妙的解決方法,該方法通過(guò)復(fù)制系統(tǒng)中Python的可執(zhí)行程序及其依賴到一個(gè)本地目錄中,創(chuàng)建了一個(gè)獨(dú)立的Python環(huán)境。這就能夠讓一個(gè)項(xiàng)目安裝指定版本的Python庫(kù),而不依賴于外部系統(tǒng)。你還可以在不同的虛擬環(huán)境中使用不同的Python版本。Conda環(huán)境中使用了Anaconda的Python路徑,提供了相似的功能。detectem模塊基于許多擴(kuò)展模塊,使用一系列請(qǐng)求和響應(yīng),來(lái)探測(cè)網(wǎng)站使用的技術(shù)。它使用了Splash,這是由ScrapingHub開(kāi)發(fā)的一個(gè)腳本化瀏覽器。要想運(yùn)行該模塊,只需使用det命令即可。$det
[('jquery','1.11.0')]
我們可以看到示例網(wǎng)站使用了通用的JavaScript庫(kù),因此其內(nèi)容很可能嵌入在HTML當(dāng)中,相對(duì)來(lái)說(shuō)應(yīng)該比較容易抓取。detectem仍然相當(dāng)年輕,旨在成為Wappalyzer的Python對(duì)標(biāo)版本,Wappalyzer是一個(gè)基于Node.js的項(xiàng)目,支持解析不同后端、廣告網(wǎng)絡(luò)、JavaScript庫(kù)以及服務(wù)器設(shè)置。你也可以在Docker中運(yùn)行Wappalyzer。首先需要下載其Docker鏡像,運(yùn)行如下命令。$dockerpullwappalyzer/cli
然后,你可以從Docker實(shí)例中運(yùn)行腳本。$dockerrunwappalyzer/cli
輸出結(jié)果不太容易閱讀,不過(guò)當(dāng)我們將其拷貝到JSON解析器中,可以看到檢測(cè)出來(lái)的很多庫(kù)和技術(shù)。{'applications':
[{'categories':['JavascriptFrameworks'],
'confidence':'100',
'icon':'Modernizr.png',
'name':'Modernizr',
'version':''},
{'categories':['WebServers'],
'confidence':'100',
'icon':'Nginx.svg',
'name':'Nginx',
'version':''},
{'categories':['WebFrameworks'],
'confidence':'100',
'icon':'TwitterBootstrap.png',
'name':'TwitterBootstrap',
'version':''},
{'categories':['WebFrameworks'],
'confidence':'100',
'icon':'Web2py.png',
'name':'Web2py',
'version':''},
{'categories':['JavascriptFrameworks'],
'confidence':'100',
'icon':'jQuery.svg',
'name':'jQuery',
'version':''},
{'categories':['JavascriptFrameworks'],
'confidence':'100',
'icon':'jQueryUI.svg',
'name':'jQueryUI',
'version':'1.10.3'},
{'categories':['ProgrammingLanguages'],
'confidence':'100',
'icon':'Python.png',
'name':'Python',
'version':''}],
'originalUrl':'',
'url':''}
從上面可以看出,檢測(cè)結(jié)果認(rèn)為Python和web2py框架具有很高的可信度。我們還可以看到網(wǎng)站使用了前端CSS框架TwitterBootstrap。Wappalyzer還檢測(cè)到網(wǎng)站使用了Modernizer.js以及用于后端服務(wù)器的Nginx。由于網(wǎng)站只使用了JQuery和Modernizer,那么網(wǎng)站不太可能全部頁(yè)面都是通過(guò)JavaScript加載的。而如果改用AngularJS或React構(gòu)建該網(wǎng)站的話,此時(shí)的網(wǎng)站內(nèi)容很可能就是動(dòng)態(tài)加載的了。另外,如果網(wǎng)站使用了ASP.NET,那么在爬取網(wǎng)頁(yè)時(shí),就必須要用到會(huì)話管理和表單提交了。對(duì)于這些更加復(fù)雜的情況,我們會(huì)在第5章和第6章中進(jìn)行介紹。1.4.5尋找網(wǎng)站所有者對(duì)于一些網(wǎng)站,我們可能會(huì)關(guān)心其所有者是誰(shuí)。比如,我們已知網(wǎng)站的所有者會(huì)封禁網(wǎng)絡(luò)爬蟲,那么我們最好把下載速度控制得更加保守一些。為了找到網(wǎng)站的所有者,我們可以使用WHOIS協(xié)議查詢域名的注冊(cè)者是誰(shuí)。Python中有一個(gè)針對(duì)該協(xié)議的封裝庫(kù),其文檔地址為/pypi/python-whois,我們可以通過(guò)pip進(jìn)行安裝。pipinstallpython-whois
下面是使用該模塊對(duì)這個(gè)域名進(jìn)行WHOIS查詢時(shí)返回結(jié)果的核心部分。>>>importwhois
>>>print(whois.whois(''))
{
...
"name_servers":[
"NS1.GOOGLE.COM",
"NS2.GOOGLE.COM",
"NS3.GOOGLE.COM",
"NS4.GOOGLE.COM",
"",
"",
"",
""
],
"org":"GoogleInc.",
"emails":[
"abusecomplaints@",
"dns-admin@"
]
}
從結(jié)果中可以看出該域名歸屬于Google,實(shí)際上也確實(shí)如此。該域名是用于GoogleAppEngine服務(wù)的。Google經(jīng)常會(huì)阻斷網(wǎng)絡(luò)爬蟲,盡管實(shí)際上其自身就是一個(gè)網(wǎng)絡(luò)爬蟲業(yè)務(wù)。當(dāng)我們爬取該域名時(shí)需要十分小心,因?yàn)镚oogle經(jīng)常會(huì)阻斷抓取其服務(wù)過(guò)快的IP;而你,或與你生活或工作在一起的人,可能需要使用Google的服務(wù)。我經(jīng)歷過(guò)在使用Google服務(wù)一段時(shí)間后,被要求輸入驗(yàn)證碼的情況,甚至只是在對(duì)Google域名運(yùn)行了簡(jiǎn)單的搜索爬蟲之后。1.5編寫第一個(gè)網(wǎng)絡(luò)爬蟲為了抓取網(wǎng)站,我們首先需要下載包含有感興趣數(shù)據(jù)的網(wǎng)頁(yè),該過(guò)程一般稱為爬?。╟rawling)。爬取一個(gè)網(wǎng)站有很多種方法,而選用哪種方法更加合適,則取決于目標(biāo)網(wǎng)站的結(jié)構(gòu)。本章中,我們首先會(huì)探討如何安全地下載網(wǎng)頁(yè),然后會(huì)介紹如下3種爬取網(wǎng)站的常見(jiàn)方法:爬取網(wǎng)站地圖;使用數(shù)據(jù)庫(kù)ID遍歷每個(gè)網(wǎng)頁(yè);跟蹤網(wǎng)頁(yè)鏈接。到目前為止,我們交替使用了抓取和爬取這兩個(gè)術(shù)語(yǔ),接下來(lái)讓我們先來(lái)定義這兩種方法的相似點(diǎn)和不同點(diǎn)。1.5.1抓取與爬取的對(duì)比根據(jù)你所關(guān)注的信息以及站點(diǎn)內(nèi)容和結(jié)構(gòu)的不同,你可能需要進(jìn)行網(wǎng)絡(luò)抓取或是網(wǎng)站爬取。那么它們有什么區(qū)別呢?網(wǎng)絡(luò)抓取通常針對(duì)特定網(wǎng)站,并在這些站點(diǎn)上獲取指定信息。網(wǎng)絡(luò)抓取用于訪問(wèn)這些特定的頁(yè)面,如果站點(diǎn)發(fā)生變化或者站點(diǎn)中的信息位置發(fā)生變化的話,則需要進(jìn)行修改。例如,你可能想要通過(guò)網(wǎng)絡(luò)抓取查看你喜歡的當(dāng)?shù)夭蛷d的每日特色菜,為了實(shí)現(xiàn)該目的,你需要抓取其網(wǎng)站中日常更新該信息的部分。與之不同的是,網(wǎng)絡(luò)爬取通常是以通用的方式構(gòu)建的,其目標(biāo)是一系列頂級(jí)域名的網(wǎng)站或是整個(gè)網(wǎng)絡(luò)。爬取可以用來(lái)收集更具體的信息,不過(guò)更常見(jiàn)的情況是爬取網(wǎng)絡(luò),從許多不同的站點(diǎn)或頁(yè)面中獲取小而通用的信息,然后跟蹤鏈接到其他頁(yè)面中。除了爬取和抓取外,我們還會(huì)在第8章中介紹網(wǎng)絡(luò)爬蟲。爬蟲可以用來(lái)爬取指定的一系列網(wǎng)站,或是在多個(gè)站點(diǎn)甚至整個(gè)互聯(lián)網(wǎng)中進(jìn)行更廣泛的爬取。一般來(lái)說(shuō),我們會(huì)使用特定的術(shù)語(yǔ)反映我們的用例。在你開(kāi)發(fā)網(wǎng)絡(luò)爬蟲時(shí),可能會(huì)注意到它們?cè)谀阆胍褂玫募夹g(shù)、庫(kù)和包中的區(qū)別。在這些情況下,你對(duì)不同術(shù)語(yǔ)的理解,可以幫助你基于所使用的術(shù)語(yǔ)選擇適當(dāng)?shù)陌蚣夹g(shù)(例如,是否只用于抓???是否也適用于爬蟲?)。1.5.2下載網(wǎng)頁(yè)要想抓取網(wǎng)頁(yè),我們首先需要將其下載下來(lái)。下面的示例腳本使用Python的urllib模塊下載URL。importurllib.request
defdownload(url):
returnurllib.request.urlopen(url).read()
當(dāng)傳入U(xiǎn)RL參數(shù)時(shí),該函數(shù)將會(huì)下載網(wǎng)頁(yè)并返回其HTML。不過(guò),這個(gè)代碼片段存在一個(gè)問(wèn)題,即當(dāng)下載網(wǎng)頁(yè)時(shí),我們可能會(huì)遇到一些無(wú)法控制的錯(cuò)誤,比如請(qǐng)求的頁(yè)面可能不存在。此時(shí),urllib會(huì)拋出異常,然后退出腳本。安全起見(jiàn),下面再給出一個(gè)更穩(wěn)建的版本,可以捕獲這些異常。importurllib.request
fromurllib.errorimportURLError,HTTPError,ContentTooShortError
defdownload(url):
print('Downloading:',url)
try:
html=urllib.request.urlopen(url).read()
except(URLError,HTTPError,ContentTooShortError)ase:
print('Downloaderror:',e.reason)
html=None
returnhtml
現(xiàn)在,當(dāng)出現(xiàn)下載或URL錯(cuò)誤時(shí),該函數(shù)能夠捕獲到異常,然后返回None。在本書中,我們將假設(shè)你在文件中編寫代碼,而不是使用提示符的方式(如上述代碼所示)。當(dāng)你發(fā)現(xiàn)代碼以Python提示符>>>或IPython提示符In[1]:開(kāi)始時(shí),你需要將其輸入到正在使用的主文件中,或是保存文件后,在Python解釋器中導(dǎo)入這些函數(shù)和類。1.重試下載下載時(shí)遇到的錯(cuò)誤經(jīng)常是臨時(shí)性的,比如服務(wù)器過(guò)載時(shí)返回的503ServiceUnavailable錯(cuò)誤。對(duì)于此類錯(cuò)誤,我們可以在短暫等待后嘗試重新下載,因?yàn)檫@個(gè)服務(wù)器問(wèn)題現(xiàn)在可能已經(jīng)解決。不過(guò),我們不需要對(duì)所有錯(cuò)誤都嘗試重新下載。如果服務(wù)器返回的是404NotFound這種錯(cuò)誤,則說(shuō)明該網(wǎng)頁(yè)目前并不存在,再次嘗試同樣的請(qǐng)求一般也不會(huì)出現(xiàn)不同的結(jié)果?;ヂ?lián)網(wǎng)工程任務(wù)組(InternetEngineeringTaskForce)定義了HTTP錯(cuò)誤的完整列表,從中可以了解到4xx錯(cuò)誤發(fā)生在請(qǐng)求存在問(wèn)題時(shí),而5xx錯(cuò)誤則發(fā)生在服務(wù)端存在問(wèn)題時(shí)。所以,我們只需要確保download函數(shù)在發(fā)生5xx錯(cuò)誤時(shí)重試下載即可。下面是支持重試下載功能的新版本代碼。defdownload(url,num_retries=2):
print('Downloading:',url)
try:
html=urllib.request.urlopen(url).read()
except(URLError,HTTPError,ContentTooShortError)ase:
print('Downloaderror:',e.reason)
html=None
ifnum_retries>0:
ifhasattr(e,'code')and500<=e.code<600:
#recursivelyretry5xxHTTPerrors
returndownload(url,num_retries-1)
returnhtml
現(xiàn)在,當(dāng)download函數(shù)遇到5xx錯(cuò)誤碼時(shí),將會(huì)遞歸調(diào)用函數(shù)自身進(jìn)行重試。此外,該函數(shù)還增加了一個(gè)參數(shù),用于設(shè)定重試下載的次數(shù),其默認(rèn)值為兩次。我們?cè)谶@里限制網(wǎng)頁(yè)下載的嘗試次數(shù),是因?yàn)榉?wù)器錯(cuò)誤可能暫時(shí)還沒(méi)有恢復(fù)。想要測(cè)試該函數(shù),可以嘗試下載http://httpstat.us/500,該網(wǎng)址會(huì)始終返回500錯(cuò)誤碼。>>>download('http://httpstat.us/500')
Downloading:http://httpstat.us/500
Downloaderror:InternalServerError
Downloading:http://httpstat.us/500
Downloaderror:InternalServerError
Downloading:http://httpstat.us/500
Downloaderror:InternalServerError
從上面的返回結(jié)果可以看出,download函數(shù)的行為和預(yù)期一致,先嘗試下載網(wǎng)頁(yè),在接收到500錯(cuò)誤后,又進(jìn)行了兩次重試才放棄。2.設(shè)置用戶代理默認(rèn)情況下,urllib使用Python-urllib/3.x作為用戶代理下載網(wǎng)頁(yè)內(nèi)容,其中3.x是環(huán)境當(dāng)前所用Python的版本號(hào)。如果能使用可辨識(shí)的用戶代理則更好,這樣可以避免我們的網(wǎng)絡(luò)爬蟲碰到一些問(wèn)題。此外,也許是因?yàn)樵?jīng)歷過(guò)質(zhì)量不佳的Python網(wǎng)絡(luò)爬蟲造成的服務(wù)器過(guò)載,一些網(wǎng)站還會(huì)封禁這個(gè)默認(rèn)的用戶代理。因此,為了使下載網(wǎng)站更加可靠,我們需要控制用戶代理的設(shè)定。下面的代碼對(duì)download函數(shù)進(jìn)行了修改,設(shè)定了一個(gè)默認(rèn)的用戶代理‘wswp’(即WebScrapingwithPython的首字母縮寫)。defdownload(url,user_agent='wswp',num_retries=2):
print('Downloading:',url)
request=urllib.request.Request(url)
request.add_header('User-agent',user_agent)
try:
html=urllib.request.urlopen(request).read()
except(URLError,HTTPError,ContentTooShortError)ase:
print('Downloaderror:',e.reason)
html=None
ifnum_retries>0:
ifhasattr(e,'code')and500<=e.code<600:
#recursivelyretry5xxHTTPerrors
returndownload(url,num_retries-1)
returnhtml
現(xiàn)在,如果你再次嘗試訪問(wèn),就能夠看到一個(gè)合法的HTML了。我們的下載函數(shù)可以在后續(xù)代碼中得到復(fù)用,該函數(shù)能夠捕獲異常、在可能的情況下重試網(wǎng)站以及設(shè)置用戶代理。1.5.3網(wǎng)站地圖爬蟲在第一個(gè)簡(jiǎn)單的爬蟲中,我們將使用示例網(wǎng)站robots.txt文件中發(fā)現(xiàn)的網(wǎng)站地圖來(lái)下載所有網(wǎng)頁(yè)。為了解析網(wǎng)站地圖,我們將會(huì)使用一個(gè)簡(jiǎn)單的正則表達(dá)式,從<loc>標(biāo)簽中提取出URL。我們需要更新代碼以處理編碼轉(zhuǎn)換,因?yàn)槲覀兡壳暗膁ownload函數(shù)只是簡(jiǎn)單地返回了字節(jié)。而在下一章中,我們將會(huì)介紹一種更加穩(wěn)健的解析方法——CSS選擇器。下面是該示例爬蟲的代碼。importre
defdownload(url,user_agent='wswp',num_retries=2,charset='utf-8'):
print('Downloading:',url)
request=urllib.request.Request(url)
request.add_header('User-agent',user_agent)
try:
resp=urllib.request.urlopen(request)
cs=resp.headers.get_content_charset()
ifnotcs:
cs=charset
html=resp.read().decode(cs)
except(URLError,HTTPError,ContentTooShortError)ase:
print('Downloaderror:',e.reason)
html=None
ifnum_retries>0:
ifhasattr(e,'code')and500<=e.code<600:
#recursivelyretry5xxHTTPerrors
returndownload(url,num_retries-1)
returnhtml
defcrawl_sitemap(url):
#downloadthesitemapfile
sitemap=download(url)
#extractthesitemaplinks
links=re.findall('<loc>(.*?)</loc>',sitemap)
#downloadeachlink
forlinkinlinks:
html=download(link)
#scrapehtmlhere
#...
現(xiàn)在,運(yùn)行網(wǎng)站地圖爬蟲,從示例網(wǎng)站中下載所有國(guó)家或地區(qū)頁(yè)面。>>>crawl_sitemap('/sitemap.xml')
Downloading:/sitemap.xml
Downloading:/view/Afghanistan-1
Downloading:/view/Aland-Islands-2
Downloading:/view/Albania-3
...
正如上面代碼中的download方法所示,我們必須更新字符編碼才能利用正則表達(dá)式處理網(wǎng)站響應(yīng)。Python的read方法返回字節(jié),而正則表達(dá)式期望的則是字符串。我們的代碼依賴于網(wǎng)站維護(hù)者在響應(yīng)頭中包含適當(dāng)?shù)淖址幋a。如果沒(méi)有返回字符編碼頭部,我們將會(huì)把它設(shè)置為默認(rèn)值UTF-8,并抱有最大的希望。當(dāng)然,如果返回頭中的編碼不正確,或是編碼沒(méi)有設(shè)置并且也不是UTF-8的話,則會(huì)拋出錯(cuò)誤。還有一些更復(fù)雜的方式用于猜測(cè)編碼(參見(jiàn)/pypi/chardet),該方法非常容易實(shí)現(xiàn)。到目前為止,網(wǎng)站地圖爬蟲已經(jīng)符合預(yù)期。不過(guò)正如前文所述,我們無(wú)法依靠Sitemap文件提供每個(gè)網(wǎng)頁(yè)的鏈接。下一節(jié)中,我們將會(huì)介紹另一個(gè)簡(jiǎn)單的爬蟲,該爬蟲不再依賴于Sitemap文件。如果你在任何時(shí)候不想再繼續(xù)爬取,可以按下Ctrl+C或cmd+C退出Python解釋器或執(zhí)行的程序。1.5.4ID遍歷爬蟲本節(jié)中,我們將利用網(wǎng)站結(jié)構(gòu)的弱點(diǎn),更加輕松地訪問(wèn)所有內(nèi)容。下面是一些示例國(guó)家(或地區(qū))的URL。/view/Afghanistan-1/view/Australia-2/view/Brazil-3可以看出,這些URL只在URL路徑的最后一部分有所區(qū)別,包括國(guó)家(或地區(qū))名(作為頁(yè)面別名)和ID。在URL中包含頁(yè)面別名是非常普遍的做法,可以對(duì)搜索引擎優(yōu)化起到幫助作用。一般情況下,Web服務(wù)器會(huì)忽略這個(gè)字符串,只使用ID來(lái)匹配數(shù)據(jù)庫(kù)中的相關(guān)記錄。下面我們將其移除,查看/view/1,測(cè)試示例網(wǎng)站中的鏈接是否仍然可用。測(cè)試結(jié)果如圖1.1所示。圖1.1從圖1.1中可以看出,網(wǎng)頁(yè)依然可以加載成功,也就是說(shuō)該方法是有用的?,F(xiàn)在,我們就可以忽略頁(yè)面別名,只利用數(shù)據(jù)庫(kù)ID來(lái)下載所有國(guó)家(或地區(qū))的頁(yè)面了。下面是使用了該技巧的代碼片段。importitertools
defcrawl_site(url):
forpageinitertools.count(1):
pg_url='{}{}'.format(url,page)
html=download(pg_url)
ifhtmlisNone:
break
#success-canscrapetheresult
現(xiàn)在,我們可以使用該函數(shù)傳入基礎(chǔ)URL。>>>crawl_site('/view/-')
Downloading:/view/-1
Downloading:/view/-2
Downloading:/view/-3
Downloading:/view/-4
[...]
在這段代碼中,我們對(duì)ID進(jìn)行遍歷,直到出現(xiàn)下載錯(cuò)誤時(shí)停止,我們假設(shè)此時(shí)抓取已到達(dá)最后一個(gè)國(guó)家(或地區(qū))的頁(yè)面。不過(guò),這種實(shí)現(xiàn)方式存在一個(gè)缺陷,那就是某些記錄可能已被刪除,數(shù)據(jù)庫(kù)ID之間并不是連續(xù)的。此時(shí),只要訪問(wèn)到某個(gè)間隔點(diǎn),爬蟲就會(huì)立即退出。下面是這段代碼的改進(jìn)版本,在該版本中連續(xù)發(fā)生多次下載錯(cuò)誤后才會(huì)退出程序。defcrawl_site(url,max_errors=5):
forpageinitertools.count(1):
pg_url='{}{}'.format(url,page)
html=download(pg_url)
ifhtmlisNone:
num_errors+=1
ifnum_errors==max_errors:
#maxerrorsreached,exitloop
break
else:
num_errors=0
#success-canscrapetheresult
上面代碼中實(shí)現(xiàn)的爬蟲需要連續(xù)5次下載錯(cuò)誤才會(huì)停止遍歷,這樣就很大程度上降低了遇到記錄被刪除或隱藏時(shí)過(guò)早停止遍歷的風(fēng)險(xiǎn)。在爬取網(wǎng)站時(shí),遍歷ID是一個(gè)很便捷的方法,但是和網(wǎng)站地圖爬蟲一樣,這種方法也無(wú)法保證始終可用。比如,一些網(wǎng)站會(huì)檢查頁(yè)面別名是否在URL中,如果不是,則會(huì)返回404NotFound錯(cuò)誤。而另一些網(wǎng)站則會(huì)使用非連續(xù)大數(shù)作為ID,或是不使用數(shù)值作為ID,此時(shí)遍歷就難以發(fā)揮其作用了。例如,Amazon使用ISBN作為可用圖書的ID,這種編碼包含至少10位數(shù)字。使用ID對(duì)ISBN進(jìn)行遍歷需要測(cè)試數(shù)十億次可能的組合,因此這種方法肯定不是抓取該站內(nèi)容最高效的方法。正如你一直關(guān)注的那樣,你可能已經(jīng)注意到一些TOOMANYREQUESTS下載錯(cuò)誤信息。現(xiàn)在無(wú)須擔(dān)心它,我們將會(huì)在1.5.5節(jié)的“高級(jí)功能”部分中介紹更多處理該類型錯(cuò)誤的方法。1.5.5鏈接爬蟲到目前為止,我們已經(jīng)利用示例網(wǎng)站的結(jié)構(gòu)特點(diǎn)實(shí)現(xiàn)了兩個(gè)簡(jiǎn)單爬蟲,用于下載所有已發(fā)布的國(guó)家(或地區(qū))頁(yè)面。只要這兩種技術(shù)可用,就應(yīng)當(dāng)使用它們進(jìn)行爬取,因?yàn)檫@兩種方法將需要下載的網(wǎng)頁(yè)數(shù)量降至最低。不過(guò),對(duì)于另一些網(wǎng)站,我們需要讓爬蟲表現(xiàn)得更像普通用戶,跟蹤鏈接,訪問(wèn)感興趣的內(nèi)容。通過(guò)跟蹤每個(gè)鏈接的方式,我們可以很容易地下載整個(gè)網(wǎng)站的頁(yè)面。但是,這種方法可能會(huì)下載很多并不需要的網(wǎng)頁(yè)。例如,我們想要從一個(gè)在線論壇中抓取用戶賬號(hào)詳情頁(yè),那么此時(shí)我們只需要下載賬號(hào)頁(yè),而不需要下載討論貼的頁(yè)面。本章使用的鏈接爬蟲將使用正則表達(dá)式來(lái)確定應(yīng)當(dāng)下載哪些頁(yè)面。下面是這段代碼的初始版本。importre
deflink_crawler(start_url,link_regex):
"""CrawlfromthegivenstartURLfollowinglinksmatchedby
link_regex
"""
crawl_queue=[start_url]
whilecrawl_queue:
url=crawl_queue.pop()
html=download(url)
ifhtmlisnotNone:
continue
#filterforlinksmatchingourregularexpression
forlinkinget_links(html):
ifre.match(link_regex,link):
crawl_queue.append(link)
defget_links(html):
"""Returnalistoflinksfromhtml
"""
#aregularexpressiontoextractalllinksfromthewebpage
webpage_regex=pile("""<a[^>]+href=["'](.*?)["']""",
re.IGNORECASE)
#listofalllinksfromthewebpage
returnwebpage_regex.findall(html)
要運(yùn)行這段代碼,只需要調(diào)用link_crawler函數(shù),并傳入兩個(gè)參數(shù):要爬取的網(wǎng)站URL以及用于匹配你想跟蹤的鏈接的正則表達(dá)式。對(duì)于示例網(wǎng)站來(lái)說(shuō),我們想要爬取的是國(guó)家(或地區(qū))列表索引頁(yè)和國(guó)家(或地區(qū))頁(yè)面。我們查看站點(diǎn)可以得知索引頁(yè)鏈接遵循如下格式:/index/1/index/2國(guó)家(或地區(qū))頁(yè)遵循如下格式:/view/Afghanistan-1/view/Aland-Islands-2因此,我們可以用/(index|view)/這個(gè)簡(jiǎn)單的正則表達(dá)式來(lái)匹配這兩類網(wǎng)頁(yè)。當(dāng)爬蟲使用這些輸入?yún)?shù)運(yùn)行時(shí)會(huì)發(fā)生什么呢?你會(huì)得到如下所示的下載錯(cuò)誤。>>>link_crawler('','/(index|view)/')
Downloading:
Downloading:/index/1
Traceback(mostrecentcalllast):
...
ValueError:unknownurltype:/index/1
正則表達(dá)式是從字符串中抽取信息的非常好的工具,因此我推薦每名程序員都應(yīng)當(dāng)“學(xué)會(huì)如何閱讀和編寫一些正則表達(dá)式”。即便如此,它們往往會(huì)非常脆弱,容易失效。我們將在本書后續(xù)部分介紹更先進(jìn)的抽取鏈接和識(shí)別頁(yè)面的方式。可以看出,問(wèn)題出在下載/index/1時(shí),該鏈接只有網(wǎng)頁(yè)的路徑部分,而沒(méi)有協(xié)議和服務(wù)器部分,也就是說(shuō)這是一個(gè)相對(duì)鏈接。由于瀏覽器知道你正在瀏覽哪個(gè)網(wǎng)頁(yè),并且能夠采取必要的步驟處理這些鏈接,因此在瀏覽器瀏覽時(shí),相對(duì)鏈接是能夠正常工作的。但是,urllib并沒(méi)有上下文。為了讓urllib能夠定位網(wǎng)頁(yè),我們需要將鏈接轉(zhuǎn)換為絕對(duì)鏈接的形式,以便包含定位網(wǎng)頁(yè)的所有細(xì)節(jié)。如你所愿,Python的urllib中有一個(gè)模塊可以用來(lái)實(shí)現(xiàn)該功能,該模塊名為parse。下面是link_crawler的改進(jìn)版本,使用了urljoin方法來(lái)創(chuàng)建絕對(duì)路徑。fromurllib.parseimporturljoin
deflink_crawler(start_url,link_regex):
"""CrawlfromthegivenstartURLfollowinglinksmatchedby
link_regex
"""
crawl_queue=[start_url]
whilecrawl_queue:
url=crawl_queue.pop()
html=download(url)
ifnothtml:
continue
forlinkinget_links(html):
ifre.match(link_regex,link):
abs_link=urljoin(start_url,link)
crawl_queue.append(abs_link)
當(dāng)你運(yùn)行這段代碼時(shí),會(huì)看到雖然下載了匹配的網(wǎng)頁(yè),但是同樣的地點(diǎn)總是會(huì)被不斷下載到。產(chǎn)生該行為的原因是這些地點(diǎn)相互之間存在鏈接。比如,澳大利亞鏈接到了南極洲,而南極洲又鏈接回了澳大利亞,此時(shí)爬蟲就會(huì)繼續(xù)將這些URL放入隊(duì)列,永遠(yuǎn)不會(huì)到達(dá)隊(duì)列尾部。要想避免重復(fù)爬取相同的鏈接,我們需要記錄哪些鏈接已經(jīng)被爬取過(guò)。下面是修改后的link_crawler函數(shù),具備了存儲(chǔ)已發(fā)現(xiàn)URL的功能,可以避免重復(fù)下載。deflink_crawler(start_url,link_regex):
crawl_queue=[start_url]
#keeptrackwhichURL'shaveseenbefore
seen=set(crawl_queue)
whilecrawl_queue:
url=crawl_queue.pop()
html=download(url)
ifnothtml:
continue
forlinkinget_links(html):
#checkiflinkmatchesexpectedregex
ifre.match(link_regex,link):
abs_link=urljoin(start_url,link)
#checkifhavealreadyseenthislink
ifabs_linknotinseen:
seen.add(abs_link)
crawl_queue.append(abs_link)
當(dāng)運(yùn)行該腳本時(shí),它會(huì)爬取所有地點(diǎn),并且能夠如期停止。最終,我們得到了一個(gè)可用的鏈接爬蟲!高級(jí)功能現(xiàn)在,讓我們?yōu)殒溄优老x添加一些功能,使其在爬取其他網(wǎng)站時(shí)更加有用。1.解析robots.txt首先,我們需要解析robots.txt文件,以避免下載禁止爬取的URL。使用Python的urllib庫(kù)中的robotparser模塊,就可以輕松完成這項(xiàng)工作,如下面的代碼所示。>>>fromurllibimportrobotparser
>>>rp=robotparser.RobotFileParser()
>>>rp.set_url('/robots.txt')
>>>rp.read()
>>>url=''
>>>user_agent='BadCrawler'
>>>rp.can_fetch(user_agent,url)
False
>>>user_agent='GoodCrawler'
>>>rp.can_fetch(user_agent,url)
True
robotparser模塊首先加載robots.txt文件,然后通過(guò)can_fetch()函數(shù)確定指定的用戶代理是否允許訪問(wèn)網(wǎng)頁(yè)。在本例中,當(dāng)用戶代理設(shè)置為'BadCrawler'時(shí),robotparser模塊的返回結(jié)果表明無(wú)法獲取網(wǎng)頁(yè),正如我們?cè)谑纠W(wǎng)站的robots.txt文件中看到的定義一樣。為了將robotparser集成到鏈接爬蟲中,我們首先需要?jiǎng)?chuàng)建一個(gè)新函數(shù)用于返回robotparser對(duì)象。defget_robots_parser(robots_url):
"Returntherobotsparserobjectusingtherobots_url"
rp=robotparser.RobotFileParser()
rp.set_url(robots_url)
rp.read()
returnrp
我們需要可靠地設(shè)置robots_url,此時(shí)我們可以通過(guò)向函數(shù)傳遞額外的關(guān)鍵詞參數(shù)的方法實(shí)現(xiàn)這一目標(biāo)。我們還可以設(shè)置一個(gè)默認(rèn)值,防止用戶沒(méi)有傳遞該變量。假設(shè)從網(wǎng)站根目錄開(kāi)始爬取,那么我們可以簡(jiǎn)單地將robots.txt添加到URL的結(jié)尾處。此外,我們還需要定義user_agent。deflink_crawler(start_url,link_regex,robots_url=None,
user_agent='wswp'):
...
ifnotrobots_url:
robots_url='{}/robots.txt'.format(start_url)
rp=get_robots_parser(robots_url)
最后,我們?cè)赾rawl循環(huán)中添加解析器檢查。...
whilecrawl_queue:
url=crawl_queue.pop()
#checkurlpassesrobots.txtrestrictions
ifrp.can_fetch(user_agent,url):
html=download(url,user_agent=user_agent)
...
else:
print('Blockedbyrobots.txt:',url)
我們可以通過(guò)使用壞的用戶代理字符串來(lái)測(cè)試我們這個(gè)高級(jí)鏈接爬蟲以及robotparser的使用。>>>link_crawler('','/(index|view)/',
user_agent='BadCrawler')
Blockedbyrobots.txt:
2.支持代理有時(shí)我們需要使用代理訪問(wèn)某個(gè)網(wǎng)站。比如,Hulu在美國(guó)以外的很多國(guó)家被屏蔽,YouTube上的一些視頻也是。使用urllib支持代理并沒(méi)有想象中那么容易。我們將在后面的小節(jié)介紹一個(gè)對(duì)用戶更友好的PythonHTTP模塊——requests,該模塊同樣也能夠處理代理。下面是使用urllib支持代理的代碼。proxy=':1234'#examplestring
proxy_support=urllib.request.ProxyHandler({'http':proxy})
opener=urllib.request.build_opener(proxy_support)
urllib.request.install_opener(opener)
#nowrequestsviaurllib.requestwillbehandledviaproxy
下面是集成了該功能的新版本download函數(shù)。defdownload(url,user_agent='wswp',num_retries=2,charset='utf-8',
proxy=None):
print('Downloading:',url)
request=urllib.request.Request(url)
request.add_header('User-agent',user_agent)
try:
ifproxy:
proxy_support=urllib.request.ProxyHandler({'http':proxy})
opener=urllib.request.build_opener(proxy_support)
urllib.request.install_opener(opener)
resp=urllib.request.urlopen(request)
cs=resp.headers.get_content_charset()
ifnotcs:
cs=charset
html=resp.read().decode(cs)
except(URLError,HTTPError,ContentTooShortError)ase:
print('Downloaderror:',e.reason)
html=None
ifnum_retries>0:
ifhasattr(e,'code')and500<=e.code<600:
#recursivelyretry5xxHTTPerrors
returndownload(url,num_retries-1)
returnhtml
目前在默認(rèn)情況下(Python3.5),urllib模塊不支持https代理。該問(wèn)題可能會(huì)在Python未來(lái)的版本中發(fā)現(xiàn)變化,因此請(qǐng)查閱最新的文檔。此外,你還可以使用文檔推薦的訣竅(/recipes/456195),或繼續(xù)閱讀來(lái)學(xué)習(xí)如何使用requests庫(kù)。3.下載限速如果我們爬取網(wǎng)站的速度過(guò)快,就會(huì)面臨被封禁或是造成服務(wù)器過(guò)載的風(fēng)險(xiǎn)。為了降低這些風(fēng)險(xiǎn),我們可以在兩次下載之間添加一組延時(shí),從而對(duì)爬蟲限速。下面是實(shí)現(xiàn)了該功能的類的代碼。fromurllib.parseimporturlparse
importtime
classThrottle:
"""Addadelaybetweendownloadstothesamedomain
"""
def__init__(self,delay):
#amountofdelaybetweendownloadsforeachdomain
self.delay=delay
#timestampofwhenadomainwaslastaccessed
self.domains={}
defwait(self,url):
domain=urlparse(url).netloc
last_accessed=self.domains.get(domain)
ifself.delay>0andlast_accessedisnotNone:
sleep_secs=self.delay-(time.time()-last_accessed)
ifsleep_secs>0:
#domainhasbeenaccessedrecently
#soneedtosleep
time.sleep(sleep_secs)
#updatethelastaccessedtime
self.domains[domain]=time.time()
Throttle類記錄了每個(gè)域名上次訪問(wèn)的時(shí)間,如果當(dāng)前時(shí)間距離上次訪問(wèn)時(shí)間小于指定延時(shí),則執(zhí)行睡眠操作。我們可以在每次下載之前調(diào)用throttle對(duì)爬蟲進(jìn)行限速。throttle=Throttle(delay)
...
throttle.wait(url)
html=download(url,user_agent=user_agent,num_retries=num_retries,
proxy=proxy,charset=charset)
4.避免爬蟲陷阱目前,我們的爬蟲會(huì)跟蹤所有之前沒(méi)有訪問(wèn)過(guò)的鏈接。但是,一些網(wǎng)站會(huì)動(dòng)態(tài)生成頁(yè)面內(nèi)容,這樣就會(huì)出現(xiàn)無(wú)限多的網(wǎng)頁(yè)。比如,網(wǎng)站有一個(gè)在線日歷功能,提供了可以訪問(wèn)下個(gè)月和下一年的鏈接,那么下個(gè)月的頁(yè)面中同樣會(huì)包含訪問(wèn)再下個(gè)月的鏈接,這樣就會(huì)一直持續(xù)請(qǐng)求到部件設(shè)定的最大時(shí)間(可能會(huì)是很久之后的時(shí)間)。該站點(diǎn)可能還會(huì)在簡(jiǎn)單的分頁(yè)導(dǎo)航中提供相同的功能,本質(zhì)上是分頁(yè)請(qǐng)求不斷訪問(wèn)空的搜索結(jié)果頁(yè),直至達(dá)到最大頁(yè)數(shù)。這種情況被稱為爬蟲陷阱。想要避免陷入爬蟲陷阱,一個(gè)簡(jiǎn)單的方法是記錄到達(dá)當(dāng)前網(wǎng)頁(yè)經(jīng)過(guò)了多少個(gè)鏈接,也就是深度。當(dāng)?shù)竭_(dá)最大深度時(shí),爬蟲就不再向隊(duì)列中添加該網(wǎng)頁(yè)中的鏈接了。要實(shí)現(xiàn)最大深度的功能,我們需要修改seen變量。該變量原先只記錄訪問(wèn)過(guò)的網(wǎng)頁(yè)鏈接,現(xiàn)在修改為一個(gè)字典,增加了已發(fā)現(xiàn)鏈接的深度記錄。deflink_crawler(...,max_depth=4):
seen={}
...
ifrp.can_fetch(user_agent,url):
depth=seen.get(url,0)
ifdepth==max_depth:
print('Skipping%sduetodepth'%url)
continue
...
forlinkinget_links(html):
ifre.match(link_regex,link):
abs_link=urljoin(start_url,link)
ifabs_linknotinseen:
seen[abs_link]=depth+1
crawl_queue.append(abs_link)
有了該功能之后,我們就有信心爬蟲最終一定能夠完成了。如果想要禁用該功能,只需將max_depth設(shè)為一個(gè)負(fù)數(shù)即可,此時(shí)當(dāng)前深度永遠(yuǎn)不會(huì)與之相等。5.最終版本這個(gè)高級(jí)鏈接爬蟲的完整源代碼可以在異步社區(qū)中下載得到,其文件名為advanced_link_crawler.py。為了方便按照本書操作,可以派生該代碼庫(kù),并使用它對(duì)比及測(cè)試你自己的代碼。要測(cè)試該鏈接爬蟲,我們可以將用戶代理設(shè)置為BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那個(gè)用戶代理。從下面的運(yùn)行結(jié)果中可以看出,爬蟲確實(shí)被屏蔽了,代碼啟動(dòng)后馬上就會(huì)結(jié)束。>>>start_url='/index'
>>>link_regex='/(index|view)'
>>>link_crawler(start_url,link_regex,user_agent='BadCrawler')
Blockedbyrobots.txt:/
```
現(xiàn)在,讓我們使用默認(rèn)的用戶代理,并將最大深度設(shè)置為`1`,這樣只有主頁(yè)上的鏈接才會(huì)被下載。
```
>>>link_crawler(start_url,link_regex,max_depth=1)
Downloading://index
Downloading:/index/1
Downloading:/view/Antigua-and-Barbuda-10
Downloading:/view/Antarctica-9
Downloading:/view/Anguilla-8
Downloading:/view/Angola-7
Downloading:/view/Andorra-6
Downloading:/view/American-Samoa-5
Downloading:/view/Algeria-4
Downloading:/view/Albania-3
Downloading:/view/Aland-Islands-2
Downloading:/view/Afghanistan-1
和預(yù)期一樣,爬蟲在下載完國(guó)家(或地區(qū))列表的第一頁(yè)之后就停止了。1.5.6使用requests庫(kù)盡管我們只使用urllib就已經(jīng)實(shí)現(xiàn)了一個(gè)相對(duì)高級(jí)的解析器,不過(guò)目前Python編寫的主流爬蟲一般都會(huì)使用requests庫(kù)來(lái)管理復(fù)雜的HTTP請(qǐng)求。該項(xiàng)目起初只是以“人類可讀”的方式協(xié)助封裝urllib功能的小庫(kù),不過(guò)現(xiàn)如今已經(jīng)發(fā)展成為擁有數(shù)百名貢獻(xiàn)者的龐大項(xiàng)目。可用的一些功能包括內(nèi)置的編碼處理、對(duì)SSL和安全的重要更新以及對(duì)POST請(qǐng)求、JSON、cookie和代理的簡(jiǎn)單處理。本書在大部分情況下,都將使用requests庫(kù),因?yàn)樗銐蚝?jiǎn)單并且易于使用,而且它事實(shí)上也是大多數(shù)網(wǎng)絡(luò)爬蟲項(xiàng)目的標(biāo)準(zhǔn)。想要安裝requests,只需使用pip即可。pipinstallrequests
如果你想了解其所有功能的進(jìn)一步介紹,可以閱讀它的文檔,地址為,此外也可以瀏覽其源代碼,地址為/kennethreitz/requests。為了對(duì)比使用這兩種庫(kù)的區(qū)別,我還創(chuàng)建了一個(gè)使用requests的高級(jí)鏈接爬蟲。你可以在從異步社區(qū)中下載的源碼文件中找到并查看該代碼,其文件名為advanced_link_crawler_using_requests.py。在主要的download函數(shù)中,展示了其關(guān)鍵區(qū)別。requests版本如下所示。defdownload(url,user_agent='wswp',num_retries=2,proxies=None):
print('Downloading:',url)
headers={'User-Agent':user_agent}
try:
resp=requests.get(url,headers=headers,proxies=proxies)
html=resp.text
ifresp.status_code>=400:
print('Downloaderror:',resp.text)
html=None
ifnum_retriesand500<=resp.status_code<600:
#recursivelyretry5xxHTTPerrors
returndownload(url,num_retries-1)
exceptrequests.exceptions.RequestExceptionase:
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024年深海資源勘探開(kāi)發(fā)勘察合同
- 2024年版?zhèn)€人等額本金還款方式貸款合同一
- 2024年智能機(jī)器人控制系統(tǒng)軟件購(gòu)銷合同模板3篇
- 2024年版建筑工程土地使用協(xié)議3篇
- 匯編語(yǔ)言課程設(shè)計(jì)
- 2024年度學(xué)校食堂油煙排放設(shè)備維修協(xié)議3篇
- 2024年版各類勞務(wù)服務(wù)協(xié)議示例匯編一
- 甲苯換熱器課程設(shè)計(jì)
- 液氮儲(chǔ)罐課程設(shè)計(jì)
- 生物質(zhì)能發(fā)電的生物質(zhì)能發(fā)電站規(guī)劃考核試卷
- 2024-2030年中國(guó)散熱產(chǎn)業(yè)運(yùn)營(yíng)效益及投資前景預(yù)測(cè)報(bào)告
- 和父親斷絕聯(lián)系協(xié)議書范本
- 2024地理知識(shí)競(jìng)賽試題
- DL∕T 5776-2018 水平定向鉆敷設(shè)電力管線技術(shù)規(guī)定
- 廣東省中山市2023-2024學(xué)年高一下學(xué)期期末統(tǒng)考英語(yǔ)試題
- 古典時(shí)期鋼琴演奏傳統(tǒng)智慧樹知到期末考試答案章節(jié)答案2024年星海音樂(lè)學(xué)院
- 樂(lè)山市市中區(qū)2022-2023學(xué)年七年級(jí)上學(xué)期期末地理試題【帶答案】
- 兩人合伙人合作協(xié)議合同
- 蘇教版一年級(jí)上冊(cè)數(shù)學(xué)期末測(cè)試卷含答案(完整版)
- 2023年廣東省普通高中數(shù)學(xué)學(xué)業(yè)水平合格性考試真題卷含答案
- DZ/T 0462.5-2023 礦產(chǎn)資源“三率”指標(biāo)要求 第5部分:金、銀、鈮、鉭、鋰、鋯、鍶、稀土、鍺(正式版)
評(píng)論
0/150
提交評(píng)論