爬虫进阶:反反爬虫技巧
主要針對(duì)以下四種反爬技術(shù):Useragent過(guò)濾;模糊的Javascript重定向;驗(yàn)證碼;請(qǐng)求頭一致性檢查。
高級(jí)網(wǎng)絡(luò)爬蟲技術(shù):繞過(guò) “403 Forbidden”,驗(yàn)證碼等
爬蟲的完整代碼可以在 github 上對(duì)應(yīng)的倉(cāng)庫(kù)里找到。
簡(jiǎn)介
我從不把爬取網(wǎng)頁(yè)當(dāng)做是我的一個(gè)愛(ài)好或者其他什么東西,但是我確實(shí)用網(wǎng)絡(luò)爬蟲做過(guò)很多事情。因?yàn)槲宜幚淼脑S多工作都要求我得到無(wú)法以其他方式獲得的數(shù)據(jù)。我需要為 Intoli 做關(guān)于游戲數(shù)據(jù)的靜態(tài)分析,所以我爬取了Google應(yīng)用商店的數(shù)據(jù)來(lái)尋找最新被下載的APK。Pointy Ball插件需要聚合來(lái)自不同網(wǎng)站的夢(mèng)幻足球(游戲)的預(yù)測(cè)數(shù)據(jù),最簡(jiǎn)單的方式就是寫一個(gè)爬蟲。在我在考慮這個(gè)問(wèn)題的之前,我大概已經(jīng)寫了大約 40~50 個(gè)爬蟲了。我不太記得當(dāng)時(shí)我對(duì)我家人撒謊說(shuō)我已經(jīng)抓取了多少 TB 的數(shù)據(jù)….,但是我確實(shí)很接近那個(gè)數(shù)字了。
我嘗試使用?xray/cheerio、nokogiri?和一些其他的工具。但我總是會(huì)回到我個(gè)人的最愛(ài)?Scrapy。在我看來(lái),Scrapy是一個(gè)出色的軟件。我對(duì)這款軟件毫無(wú)保留的贊美是有原因的,它的用法非常符合直覺(jué),學(xué)習(xí)曲線也很平緩。
你可以閱讀Scrapy的教程,在幾分鐘內(nèi)就可以讓你的第一個(gè)爬蟲運(yùn)行起來(lái)。然后,當(dāng)你需要做一些更復(fù)雜的事情的時(shí)候,你就會(huì)發(fā)現(xiàn),有一個(gè)內(nèi)置的、有良好文檔說(shuō)明的方式來(lái)做到這一點(diǎn)。這個(gè)框架有大量的內(nèi)置功能,但是它的結(jié)構(gòu)使得在你用到這些功能之前,不會(huì)妨礙到你。當(dāng)你最終確實(shí)需要某些默認(rèn)不存在的功能的時(shí)候,比如說(shuō),因?yàn)樵L問(wèn)了太多的 URL 鏈接以至于無(wú)法存儲(chǔ)到內(nèi)存中,需要一個(gè)用于去重的 bloom filter(布隆過(guò)濾器),那么通常來(lái)說(shuō)這就和繼承其中的組件,然后做一點(diǎn)小改動(dòng)一樣簡(jiǎn)單。一切都感覺(jué)如此簡(jiǎn)單,而且scrapy是我書中一個(gè)關(guān)于良好軟件設(shè)計(jì)的例子。
我很久以前就想寫一個(gè)高級(jí)爬蟲教程了。這給我一個(gè)機(jī)會(huì)來(lái)展示scrapy的可擴(kuò)展性,同時(shí)解決實(shí)踐中出現(xiàn)的現(xiàn)實(shí)問(wèn)題。盡管我很想做這件事,但是我還是無(wú)法擺脫這樣一個(gè)事實(shí):因?yàn)榘l(fā)布一些可能導(dǎo)致他人服務(wù)器由于大量的機(jī)器人流量受到損害的文章,就像是一個(gè)十足的壞蛋。
只要遵循幾個(gè)基本的規(guī)則,我就可以在爬取那些有反爬蟲策略的網(wǎng)站的時(shí)候安心地睡個(gè)好覺(jué)。換句話說(shuō),我讓我的請(qǐng)求頻率和手動(dòng)瀏覽的訪問(wèn)頻率相當(dāng),并且我不會(huì)對(duì)數(shù)據(jù)做任何令人反感的事情。這樣就使得運(yùn)行爬蟲收集數(shù)據(jù)基本上和以其他主要的手動(dòng)收集數(shù)據(jù)的方法無(wú)法區(qū)分。但即使我遵守了這些規(guī)則,我仍感覺(jué)為人們實(shí)際想要爬取的網(wǎng)站寫一個(gè)教程有很大的難度。
直到我遇到一個(gè)叫做Zipru的BT下載網(wǎng)站,這件事情仍然只是我腦海里一個(gè)模糊的想法。這個(gè)網(wǎng)站有多個(gè)機(jī)制需要高級(jí)爬取技術(shù)來(lái)繞過(guò),但是它的 robots.txt 文件卻允許爬蟲爬取。此外,其實(shí)我們不必去爬取它。因?yàn)樗虚_放的API,同樣可以得到全部數(shù)據(jù)。如果你對(duì)于獲得torrent的數(shù)據(jù)感興趣,那就只需要使用這個(gè)API,這很方便。
在本文的剩余部分,我將帶領(lǐng)你寫一個(gè)爬蟲,處理驗(yàn)證碼和解決我們?cè)赯ipru網(wǎng)站遇到的各種不同的挑戰(zhàn)。樣例代碼無(wú)法被正常運(yùn)行因?yàn)?Zipru 不是一個(gè)真實(shí)存在的網(wǎng)站,但是爬蟲所使用的技術(shù)會(huì)被廣泛應(yīng)用于現(xiàn)實(shí)世界中的爬取中。因此這個(gè)代碼在另一個(gè)意義上來(lái)說(shuō)又是完整的。我們將假設(shè)你已經(jīng)對(duì) Python 有了基本的了解,但是我仍會(huì)盡力讓那些對(duì)于?Scrapy 一無(wú)所知的人看懂這篇文章。如果你覺(jué)得進(jìn)度太快,那么花幾分鐘的時(shí)間閱讀一下Scrapy官網(wǎng)教程吧。
號(hào):923414804
群里有志同道合的小伙伴,互幫互助,
群里有不錯(cuò)的視頻學(xué)習(xí)教程和PDF!
建立工程項(xiàng)目
我們會(huì)在?virtualenv?中建立我們的項(xiàng)目,這可以讓我們封裝一下依賴關(guān)系。首先我們?cè)趡/scrapers/zipru中創(chuàng)建一個(gè)virtualenv ,并且安裝scrapy包。
Python| 1 2 3 4 5 | mkdir ~/scrapers/zipru cd ~/scrapers/zipru virtualenv env . env/bin/activate pip install scrapy |
你運(yùn)行的終端將被配置為使用本地的virtualenv。如果你打開另一個(gè)終端,那么你就需要再次運(yùn)行. ~/scrapers/zipru/env/bin/active?命令 (否則你有可能得到命令或者模塊無(wú)法找到的錯(cuò)誤消息)。
現(xiàn)在你可以通過(guò)運(yùn)行下面的命令來(lái)創(chuàng)建一個(gè)新的項(xiàng)目框架:
Python| 1 | scrapy startproject zipru_scraper |
這樣就會(huì)創(chuàng)建下面的目錄結(jié)構(gòu)。
Python| 1 2 3 4 5 6 7 8 9 10 | └── zipru_scraper ????├── zipru_scraper ????│?? ├── __init__.py ????│?? ├── items.py ????│?? ├── middlewares.py ????│?? ├── pipelines.py ????│?? ├── settings.py ????│?? └── spiders ????│?????? └── __init__.py ????└── scrapy.cfg |
大多數(shù)默認(rèn)情況下產(chǎn)生的這些文件實(shí)際上不會(huì)被用到,它們只是建議以一種合理的方式來(lái)構(gòu)建我們的代碼。從現(xiàn)在開始,你應(yīng)該把?~/scrapers/zipru/zipru_scraper?當(dāng)做這個(gè)項(xiàng)目的根目錄。這里是任何scrapy命令運(yùn)行的目錄,同時(shí)也是所有相對(duì)路徑的根。
添加一個(gè)基本的爬蟲功能
現(xiàn)在我們需要添加一個(gè)Spieder類來(lái)讓我們的scrapy真正地做一些事情。Spider類是scrapy爬蟲用來(lái)解析文本,爬取新的url鏈接或是提取數(shù)據(jù)的一個(gè)類。我們非常依賴于默認(rèn)Spider類的實(shí)現(xiàn),以最大限度地減少我們必須要編寫的代碼量。這里要做的事情看起來(lái)有點(diǎn)自動(dòng)化,但假如你看過(guò)文檔,事情會(huì)變得更加簡(jiǎn)單。
首先,在zipru_scraper/spiders/目錄下創(chuàng)建一個(gè)文件,命名為?zipru_spider.py?,輸入下面內(nèi)容。
Python| 1 2 3 4 5 | import scrapy class ZipruSpider(scrapy.Spider): ????name = 'zipru' ????start_urls = ['http://zipru.to/torrents.php?category=TV'] |
你可以在上面的網(wǎng)頁(yè)中看到許多指向其他頁(yè)面的連接。我們想讓我們的爬蟲跟蹤這些鏈接,并且解析他們的內(nèi)容。為了完成這個(gè)任務(wù),我們首先需要識(shí)別出這些鏈接并且弄清楚他們指向的位置。
在這個(gè)階段,DOM檢查器將起到很大的助力。如果你右擊其中的一個(gè)頁(yè)面鏈接,在DOM檢查器里面查看它,然后你就會(huì)看到指向其他頁(yè)面的鏈接看起來(lái)像是這樣的:
| 1 2 3 | <a href="/torrents.php?...page=2" title="page 2">2</a> <a href="/torrents.php?...page=3" title="page 3">3</a> <a href="/torrents.php?...page=4" title="page 4">4</a> |
接下來(lái)我們需要為這些鏈接構(gòu)造一個(gè)選擇器表達(dá)式。有幾種類型似乎用css或者xpath選擇器進(jìn)行搜索更適合,所以我通常傾向于靈活地混合使用這幾種選擇器。我強(qiáng)烈推薦學(xué)習(xí)xpath,但是不幸的是,它有點(diǎn)超出了本教程的范圍。我個(gè)人認(rèn)為xpath對(duì)于網(wǎng)絡(luò)爬蟲,web UI 測(cè)試,甚至一般的web開發(fā)來(lái)說(shuō)都是不可或缺的。我接下來(lái)仍然會(huì)使用css選擇器,因?yàn)樗鼘?duì)于大多數(shù)人來(lái)說(shuō)可能比較熟悉。
要選擇這些頁(yè)面鏈接,我們可以把?a[title ~= page]?作為一個(gè) css 選擇器,來(lái)查找標(biāo)題中有 “page” 字符的?<a>標(biāo)簽。如果你在 DOM 檢查器中按?ctrl-f,那么你就會(huì)發(fā)現(xiàn)你也可以使用這個(gè)css表達(dá)式作為一條查找語(yǔ)句(也可以使用xpath)。這樣我們就可以循環(huán)查看所有的匹配項(xiàng)了。這是一個(gè)很棒的方法,可以用來(lái)檢查一個(gè)表達(dá)式是否有效,并且表達(dá)式足夠明確不會(huì)在不小心中匹配到其他的標(biāo)簽。我們的頁(yè)面鏈接選擇器同時(shí)滿足了這兩個(gè)條件。
為了講解我們的爬蟲是怎樣發(fā)現(xiàn)其他頁(yè)面的,我們?cè)?ZipruSpider?類中添加一個(gè)?parse(response)?方法,就像下面這樣:
Python| 1 2 3 4 5 | def parse(self, response): ????????# proceed to other pages of the listings ????????for page_url in response.css('a[title ~= page]::attr(href)').extract(): ????????????page_url = response.urljoin(page_url) ????????????yield scrapy.Request(url=page_url, callback=self.parse) |
當(dāng)我們開始爬取的時(shí)候,我們添加到?start_urls?中的鏈接將會(huì)被自動(dòng)獲取到,響應(yīng)內(nèi)容會(huì)被傳遞到?parse(response)?方法中。之后我們的代碼就會(huì)找到所有指向其他頁(yè)面的鏈接,并且產(chǎn)生新的請(qǐng)求對(duì)象,這些請(qǐng)求對(duì)象將使用同一個(gè)?parse(response)?作為回調(diào)函數(shù)。這些請(qǐng)求將被轉(zhuǎn)化成響應(yīng)對(duì)象,只要 url 仍然產(chǎn)生,響應(yīng)就會(huì)持續(xù)地返回到?parse(response)函數(shù)(感謝去重器)。
我們的爬蟲已經(jīng)可以找到了頁(yè)面中列出的所有不同的頁(yè)面,并且對(duì)它們發(fā)出了請(qǐng)求,但我們?nèi)匀恍枰崛∫恍?duì)爬蟲來(lái)說(shuō)有用的數(shù)據(jù)。torrent 列表位于?<table>?標(biāo)簽之內(nèi),并且有屬性?class="list2at"?,每個(gè)單獨(dú)的 torrent 都位于帶有屬性?class="lista2"?的?<tr>?標(biāo)簽,其中的每一行都包含 8 個(gè)?<td>標(biāo)簽,分別與 “類別”,“文件”,“添加時(shí)間”,“文件大小”,“保種的人”,“下載文件的人”,“文件描述”,和“上傳者”相對(duì)應(yīng)。在代碼中查看其它的細(xì)節(jié)可能是最簡(jiǎn)單的方法,下面是我們修改后的?parse(response)?方法:
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def parse(self, response): ????????# proceed to other pages of the listings ????????for page_url in response.xpath('//a[contains(@title, "page ")]/@href').extract(): ????????????page_url = response.urljoin(page_url) ????????????yield scrapy.Request(url=page_url, callback=self.parse) ????????# extract the torrent items ????????for tr in response.css('table.lista2t tr.lista2'): ????????????tds = tr.css('td') ????????????link = tds[1].css('a')[0] ????????????yield { ????????????????'title' : link.css('::attr(title)').extract_first(), ????????????????'url' : response.urljoin(link.css('::attr(href)').extract_first()), ????????????????'date' : tds[2].css('::text').extract_first(), ????????????????'size' : tds[3].css('::text').extract_first(), ????????????????'seeders': int(tds[4].css('::text').extract_first()), ????????????????'leechers': int(tds[5].css('::text').extract_first()), ????????????????'uploader': tds[7].css('::text').extract_first(), ????????????} |
我們的?parse(response)?方法現(xiàn)在能夠返回字典類型的數(shù)據(jù),并且根據(jù)它們的類型自動(dòng)區(qū)分請(qǐng)求。每個(gè)字典都會(huì)被解釋為一項(xiàng),并且作為爬蟲數(shù)據(jù)輸出的一部分。
如果我們只是爬取大多數(shù)常見(jiàn)的網(wǎng)站,那我們已經(jīng)完成了。我們只需要使用下面的命令來(lái)運(yùn)行:
Python| 1 | scrapy crawl zipru -o torrents.jl |
幾分鐘之后我們本應(yīng)該得到一個(gè) [JSON Lines] 格式?torrents.jl?文件,里面有我們所有的torrent 數(shù)據(jù)。取而代之的是我們得到下面的錯(cuò)誤信息(和一大堆其他的東西):
Python| 1 2 3 4 5 6 | [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023 [scrapy.core.engine] DEBUG: Crawled (403) <GET http://zipru.to/robots.txt> (referer: None) ['partial'] [scrapy.core.engine] DEBUG: Crawled (403) <GET http://zipru.to/torrents.php?category=TV> (referer: None) ['partial'] [scrapy.spidermiddlewares.httperror] INFO: Ignoring response <403 http://zipru.to/torrents.php?category=TV>: HTTP status code is not handled or not allowed [scrapy.core.engine] INFO: Closing spider (finished) |
我好氣啊!我們現(xiàn)在必須變得更聰明來(lái)獲得我們完全可以從公共API得到的數(shù)據(jù),因?yàn)樯厦娴拇a永遠(yuǎn)都無(wú)法爬取到那些數(shù)據(jù)。
簡(jiǎn)單的問(wèn)題
我們的第一個(gè)請(qǐng)求返回了一個(gè)?403?響應(yīng),所以這個(gè)url被爬蟲忽略掉了,然后一切都關(guān)閉了,因?yàn)槲覀冎唤o爬蟲提供了一個(gè) url 鏈接。同樣的請(qǐng)求在網(wǎng)頁(yè)瀏覽器里運(yùn)行正常,即使是在沒(méi)有會(huì)話(session)歷史的隱匿模式也可以,所以這一定是由于兩者請(qǐng)求頭信息的差異造成的。我們可以使用?tcpdump?來(lái)比較這兩個(gè)請(qǐng)求的頭信息,但其實(shí)有個(gè)常見(jiàn)錯(cuò)誤,所以我們應(yīng)該首先檢查: user agent 。
Scrapy 默認(rèn)把 user-agent 設(shè)置為 “Scrapy/1.3.3?(+http://scrapy.org)“,一些服務(wù)器可能會(huì)屏蔽這樣的請(qǐng)求,甚至使用白名單只允許少量的user agent 通過(guò)。你可以在線查看?最常見(jiàn)的 user agent?,使用其中任何一個(gè)通常就足以繞過(guò)基本反爬蟲策略。選擇一個(gè)你最喜歡的 User-agent ,然后打開?zipru_scraper/settings.py?,替換 User agent
Python| 1 2 | # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'zipru_scraper (+http://www.yourdomain.com)' |
使用下面內(nèi)容替換 USER_AGENT :
Python| 1 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36' |
你可能注意到了,默認(rèn)的scrapy設(shè)置中有一些令爬蟲蒙羞的事。關(guān)于這個(gè)問(wèn)題的觀點(diǎn)眾說(shuō)紛紜,但是我個(gè)人認(rèn)為假如你想讓爬蟲表現(xiàn)的像是一個(gè)人在使用普通的網(wǎng)頁(yè)瀏覽器,那么你就應(yīng)該把你的爬蟲設(shè)置地像普通的網(wǎng)絡(luò)瀏覽器那樣。所以讓我們一起添加下面的設(shè)置來(lái)降低一下爬蟲響應(yīng)速度:
Python| 1 2 | CONCURRENT_REQUESTS = 1 DOWNLOAD_DELAY = 5 |
通過(guò)?AutoThrottle 擴(kuò)展?,上面的設(shè)置會(huì)創(chuàng)建一個(gè)稍微真實(shí)一點(diǎn)的瀏覽模式。我們的爬蟲在默認(rèn)情況下會(huì)遵守?robots.txt,所以現(xiàn)在我們的行為非常檢點(diǎn)。
現(xiàn)在使用?scrapy crawl zipru -o torrents.jl?命令再次運(yùn)行爬蟲,應(yīng)該會(huì)產(chǎn)生下面的輸出:
Python| 1 2 3 4 | [scrapy.core.engine] DEBUG: Crawled (200) <GET http://zipru.to/robots.txt> (referer: None) [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://zipru.to/threat_defense.php?defense=1&r=78213556> from <GET http://zipru.to/torrents.php?category=TV> [scrapy.core.engine] DEBUG: Crawled (200) <GET http://zipru.to/threat_defense.php?defense=1&r=78213556> (referer: None) ['partial'] [scrapy.core.engine] INFO: Closing spider (finished) |
這是一個(gè)巨大的進(jìn)步!我們獲得了兩個(gè)?200?狀態(tài)碼和一個(gè)?302狀態(tài)碼,下載中間件知道如何處理?302?狀態(tài)碼。不幸的是,這個(gè)?302?將我們的請(qǐng)求重定向到了一個(gè)看起來(lái)不太吉利的頁(yè)面?threat_defense.php。不出所料,爬蟲沒(méi)有發(fā)現(xiàn)任何有用的東西,然后爬蟲就停止運(yùn)行了。
注: 假如網(wǎng)站檢測(cè)到你的爬蟲,那么網(wǎng)站就會(huì)把你的請(qǐng)求重定向到 threat_defense.php 頁(yè)面,使你的爬蟲失效,用來(lái)防止頻繁的爬蟲請(qǐng)求影響了網(wǎng)站正常用戶的使用。
下載中間件
在我們深入研究我們目前所面臨的更復(fù)雜的問(wèn)題之前,先了解一下請(qǐng)求和響應(yīng)在爬蟲中是怎樣被處理的,將會(huì)很有幫助。當(dāng)我們創(chuàng)建了我們基本的爬蟲,我們生成了一個(gè)?scrapy.Request?對(duì)象,然后這些請(qǐng)求會(huì)以某種方法轉(zhuǎn)化為與服務(wù)器的響應(yīng)相對(duì)應(yīng)的?scrapy.Response對(duì)象。這里的 “某種方法” 很大一部分是來(lái)自于下載中間件。
下載中間件繼承自?scrapy.downloadermiddlewares.DownloaderMiddleware?類并且實(shí)現(xiàn)了?process_request(request, spider)?和?process_response(request, response, spider)?方法。你大概可以從他們的名字中猜到他們是做什么的。實(shí)際上這里有一大堆的默認(rèn)開啟的中間件。下面是標(biāo)準(zhǔn)的中間件配置(你當(dāng)然可以禁用、添加或是重新設(shè)置這些選項(xiàng)):
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | DOWNLOADER_MIDDLEWARES_BASE = { ????'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100, ????'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300, ????'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350, ????'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400, ????'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500, ????'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, ????'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560, ????'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580, ????'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590, ????'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600, ????'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700, ????'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750, ????'scrapy.downloadermiddlewares.stats.DownloaderStats': 850, ????'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900, } |
當(dāng)一個(gè)請(qǐng)求到達(dá)服務(wù)器時(shí),他們會(huì)通過(guò)每個(gè)這些中間件的?process_request(request, spider)?方法。 這是按照數(shù)字順序發(fā)生的,RobotsTxtMiddleware?中間件首先產(chǎn)生請(qǐng)求,并且?HttpCacheMiddleware?中間件最后產(chǎn)生請(qǐng)求。一旦接收到一個(gè)響應(yīng),它就會(huì)通過(guò)任何已啟用的中間件的?process_response(request,response,spider)?方法來(lái)返回響應(yīng)。這次是以相反的順序發(fā)生的,所以數(shù)字越高越先發(fā)送到服務(wù)器,數(shù)字越低越先被爬蟲獲取到。
一個(gè)特別簡(jiǎn)單的中間件是?CookiesMiddleware。它簡(jiǎn)單地檢查響應(yīng)中請(qǐng)求頭的?Set-Cookie,并且保存 cookie 。然后當(dāng)響應(yīng)返回的時(shí)候,他們會(huì)適當(dāng)?shù)卦O(shè)置?Cookie?請(qǐng)求頭標(biāo)記,這樣這些標(biāo)記就會(huì)被包含在發(fā)出的請(qǐng)求中了。這個(gè)由于時(shí)間太久的原因要比我們說(shuō)的要稍微復(fù)雜些,但你會(huì)明白的。
另一個(gè)相對(duì)基本的就是?RedirectMiddleware?中間件,它是用來(lái)處理?3XX?重定向的。它讓一切不是?3XX?狀態(tài)碼的響應(yīng)都能夠成功的通過(guò),但假如響應(yīng)中還有重定向發(fā)生會(huì)怎樣? 唯一能夠弄清楚服務(wù)器如何響應(yīng)重定向URL的方法就是創(chuàng)建一個(gè)新的請(qǐng)求,而且這個(gè)中間件就是這么做的。當(dāng)?process_response(request, response, spider)?方法返回一個(gè)請(qǐng)求對(duì)象而不是響應(yīng)對(duì)象的時(shí)候,那么當(dāng)前響應(yīng)就會(huì)被丟棄,一切都會(huì)從新的請(qǐng)求開始。這就是?RedirectMiddleware?中間件怎樣處理重定向的,這個(gè)功能我們稍后會(huì)用到。
如果你對(duì)于有那么多的中間件默認(rèn)是開啟的感到驚訝的話,那么你可能有興趣看看?體系架構(gòu)概覽。實(shí)際上同時(shí)還有很多其他的事情在進(jìn)行,但是,再說(shuō)一次,scrapy的最大優(yōu)點(diǎn)之一就是你不需要知道它的大部分原理。你甚至不需要知道下載中間件的存在,卻能寫一個(gè)實(shí)用的爬蟲,你不必知道其他部分就可以寫一個(gè)實(shí)用的下載中間件。
困難的問(wèn)題
回到我們的爬蟲上來(lái),我們發(fā)現(xiàn)我們被重定向到某個(gè)?threat_defense.php?defense=1&...?URL上,而不是我們要找的頁(yè)面。當(dāng)我們?cè)跒g覽器里面訪問(wèn)這個(gè)頁(yè)面的時(shí)候,我們看到下面的東西停留了幾秒:
在被重定向到?threat_defense.php?defense=2&...?頁(yè)面之前,會(huì)出現(xiàn)像下面的提示:
看看第一個(gè)頁(yè)面的源代碼就會(huì)發(fā)現(xiàn),有一些 javascript 代碼負(fù)責(zé)構(gòu)造一個(gè)特殊的重定向URL,并且構(gòu)造瀏覽器的cookies。如果我們想要完成這個(gè)任務(wù),那我們就必須同時(shí)解決上面這兩個(gè)問(wèn)題。
接下來(lái),當(dāng)然我們也需要解決驗(yàn)證碼并提交答案。如果我們碰巧弄錯(cuò)了,那么我們有時(shí)會(huì)被重定向到另一個(gè)驗(yàn)證碼頁(yè)面,或者我們會(huì)在類似于下面的頁(yè)面上結(jié)束訪問(wèn):
在上面的頁(yè)面中,我們需要點(diǎn)擊 “Click here” 鏈接來(lái)開始整個(gè)重定向的循環(huán),小菜一碟,對(duì)吧?
我們所有的問(wèn)題都源于最開始的?302?重定向,因此處理它們的方法自然而然應(yīng)該是做一個(gè)自定義的?重定向中間件。我們想讓我們的中間件在所有情況下都像是正常重定向中間件一樣,除非有一個(gè)?302?狀態(tài)碼并且請(qǐng)求被重定向到?threat_defense.php?頁(yè)面。當(dāng)它遇到特殊的?302?狀態(tài)碼時(shí),我們希望它能夠繞過(guò)所有的防御機(jī)制,把訪問(wèn)cookie添加到 session 會(huì)話中,最后重新請(qǐng)求原來(lái)的頁(yè)面。如果我們能夠做到這一點(diǎn),那么我們的Spider類就不必知道這些事情,因?yàn)檎?qǐng)求會(huì)全部成功。
打開?zipru_scraper/middlewares.py?文件,并且把內(nèi)容替換成下面的代碼:
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import os, tempfile, time, sys, logging logger = logging.getLogger(__name__) import dryscrape import pytesseract from PIL import Image from scrapy.downloadermiddlewares.redirect import RedirectMiddleware class ThreatDefenceRedirectMiddleware(RedirectMiddleware): ????def _redirect(self, redirected, request, spider, reason): ????????# act normally if this isn't a threat defense redirect ????????if not self.is_threat_defense_url(redirected.url): ????????????return super()._redirect(redirected, request, spider, reason) ????????logger.debug(f'Zipru threat defense triggered for {request.url}') ????????request.cookies = self.bypass_threat_defense(redirected.url) ????????request.dont_filter = True # prevents the original link being marked a dupe ????????return request ????def is_threat_defense_url(self, url): ????????return '://zipru.to/threat_defense.php' in url |
你可能注意到我們繼承了?RedirectMiddleware?類,而不是直接繼承?DownloaderMiddleware?類。這樣就允許我們重用大部分的重定向處理函數(shù),并且把我們的代碼插入到?_redirect(redirected, request, spider, reason)?函數(shù)中,一旦有重定向的請(qǐng)求被創(chuàng)建,process_response(request, response, spider)?函數(shù)就會(huì)調(diào)用這個(gè)函數(shù)。我們只是把對(duì)于普通的重定向的處理推遲到父類進(jìn)行處理,但是對(duì)于特殊的威脅防御重定向的處理是不一樣的。我們到目前為止還沒(méi)有實(shí)現(xiàn)?bypass_threat_defense(url)?方法,但是我們可以知道它應(yīng)該返回訪問(wèn)cookies,并把它附加到原來(lái)的請(qǐng)求中,然后原來(lái)的請(qǐng)求將被重新處理。
為了開啟我們新的中間件,我們需要把下面的內(nèi)容添加到?zipru_scraper/settings.py中:
Python| 1 2 3 4 | DOWNLOADER_MIDDLEWARES = { ????'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': None, ????'zipru_scraper.middlewares.ThreatDefenceRedirectMiddleware': 600, } |
這會(huì)禁用默認(rèn)的重定向中間件,并且把我們的中間件添加在中間件堆棧中和默認(rèn)重定向中間件相同的位置。我們必須安裝一些額外的包,雖然我們現(xiàn)在沒(méi)有用到,但是稍后我們會(huì)導(dǎo)入它們:
Python| 1 2 3 | pip install dryscrape # headless webkit pip install Pillow # image processing pip install pytesseract # OCR |
請(qǐng)注意,這三個(gè)包都有 pip 無(wú)法處理的外部依賴,如果你運(yùn)行出錯(cuò),那么你可能需要訪問(wèn)?dryscrape,?Pillow, 和?pytesseract?的安裝教程,遵循平臺(tái)的具體說(shuō)明來(lái)解決。
我們的中間件現(xiàn)在應(yīng)該能夠替代原來(lái)的標(biāo)準(zhǔn)重定向中間件,現(xiàn)在我們只需要實(shí)現(xiàn)?bypass_thread_defense(url)?方法。我們可以解析 javascript 代碼來(lái)得到我們需要的變量,然后用 python 重建邏輯,但這看起來(lái)很不牢靠,而且需要大量的工作。讓我們采用更簡(jiǎn)單的方法,盡管可能還是比較笨重,使用無(wú)頭的 webkit 實(shí)例。有幾個(gè)不同選擇,但我個(gè)人比較喜歡?dryscrape?(我們已經(jīng)在上面安裝了)
首先,讓我們?cè)谥虚g件構(gòu)造函數(shù)中初始化一個(gè) dryscrape 會(huì)話。
Python| 1 2 3 4 5 6 7 8 | def __init__(self, settings): ????????super().__init__(settings) ????????# start xvfb to support headless scraping ????????if 'linux' in sys.platform: ????????????dryscrape.start_xvfb() ????????self.dryscrape_session = dryscrape.Session(base_url='http://zipru.to') |
你可以把這個(gè)會(huì)話對(duì)象當(dāng)做是一個(gè)單獨(dú)的瀏覽器標(biāo)簽頁(yè),它可以完成一切瀏覽器通常可以做的事情(例如:獲取外部資源,執(zhí)行腳本)。我們可以在新的標(biāo)簽頁(yè)中打開新的 URL 鏈接,點(diǎn)擊一些東西,或者在輸入框中輸入內(nèi)容,或是做其他的各種事情。Scrapy 支持并發(fā)請(qǐng)求和多項(xiàng)處理,但是響應(yīng)的處理是單線程的。這意味著我們可以使用這個(gè)單獨(dú)的 dryscrapy 會(huì)話,而不必?fù)?dān)心線程安全。
現(xiàn)在讓我們實(shí)現(xiàn)繞過(guò)威脅防御機(jī)制的基本邏輯。
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | def bypass_threat_defense(self, url=None): ????????# only navigate if any explicit url is provided ????????if url: ????????????self.dryscrape_session.visit(url) ????????# solve the captcha if there is one ????????captcha_images = self.dryscrape_session.css('img[src *= captcha]') ????????if len(captcha_images) > 0: ????????????return self.solve_captcha(captcha_images[0]) ????????# click on any explicit retry links ????????retry_links = self.dryscrape_session.css('a[href *= threat_defense]') ????????if len(retry_links) > 0: ????????????return self.bypass_threat_defense(retry_links[0].get_attr('href')) ????????# otherwise, we're on a redirect page so wait for the redirect and try again ????????self.wait_for_redirect() ????????return self.bypass_threat_defense() ????def wait_for_redirect(self, url = None, wait = 0.1, timeout=10): ????????url = url or self.dryscrape_session.url() ????????for i in range(int(timeout//wait)): ????????????time.sleep(wait) ????????????if self.dryscrape_session.url() != url: ????????????????return self.dryscrape_session.url() ????????logger.error(f'Maybe {self.dryscrape_session.url()} isn\'t a redirect URL?') ????????raise Exception('Timed out on the zipru redirect page.') |
這樣就處理了我們?cè)跒g覽器中遇到的所有不同的情況,并且完全符合人類在每種情況中的行為。在任何給定情況下采取的措施都取決于當(dāng)前頁(yè)面的情況,所以這種方法可以稍微優(yōu)雅一點(diǎn)地處理各種不同的情況。
最后一個(gè)難題是如果如何解決驗(yàn)證碼。網(wǎng)上提供了?驗(yàn)證碼識(shí)別?服務(wù),你可以在必要時(shí)使用它的API,但是這次的這些驗(yàn)證碼非常簡(jiǎn)單,我們只用 OCR 就可以解決它。使用 pytessertact 的 OCR 功能,最后我們可以添加?solve_captcha(img)?函數(shù),這樣就完善了?bypass_threat_defense()?函數(shù)。
Python| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def solve_captcha(self, img, width=1280, height=800): ????????# take a screenshot of the page ????????self.dryscrape_session.set_viewport_size(width, height) ????????filename = tempfile.mktemp('.png') ????????self.dryscrape_session.render(filename, width, height) ????????# inject javascript to find the bounds of the captcha ????????js = 'document.querySelector("img[src *= captcha]").getBoundingClientRect()' ????????rect = self.dryscrape_session.eval_script(js) ????????box = (int(rect['left']), int(rect['top']), int(rect['right']), int(rect['bottom'])) ????????# solve the captcha in the screenshot ????????image = Image.open(filename) ????????os.unlink(filename) ????????captcha_image = image.crop(box) ????????captcha = pytesseract.image_to_string(captcha_image) ????????logger.debug(f'Solved the Zipru captcha: "{captcha}"') ????????# submit the captcha ????????input = self.dryscrape_session.xpath('//input[@id = "solve_string"]')[0] ????????input.set(captcha) ????????button = self.dryscrape_session.xpath('//button[@id = "button_submit"]')[0] ????????url = self.dryscrape_session.url() ????????button.click() ????????# try again if it we redirect to a threat defense URL ????????if self.is_threat_defense_url(self.wait_for_redirect(url)): ????????????return self.bypass_threat_defense() ????????# otherwise return the cookies as a dict ????????cookies = {} ????????for cookie_string in self.dryscrape_session.cookies(): ????????????if 'domain=zipru.to' in cookie_string: ????????????????key, value = cookie_string.split(';')[0].split('=') ????????????????cookies[key] = value ????????return cookies |
你可能注意到如果驗(yàn)證碼因?yàn)槟承┰蜃R(shí)別失敗的話,它就會(huì)委托給?back to the bypass_threat_defense()?函數(shù)。這樣就給了我們多次識(shí)別驗(yàn)證碼的機(jī)會(huì),但重點(diǎn)是,我們會(huì)在得到正確結(jié)果之前一直在驗(yàn)證碼識(shí)別過(guò)程中循環(huán)。
這應(yīng)該足夠讓我們的爬蟲工作,但是它有可能陷入死循環(huán)中。
Python| 1 2 3 4 5 6 7 8 | [scrapy.core.engine] DEBUG: Crawled (200) <GET http://zipru.to/robots.txt> (referer: None) [zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV [zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: "UJM39" [zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV [zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: "TQ9OG" [zipru_scraper.middlewares] DEBUG: Zipru threat defense triggered for http://zipru.to/torrents.php?category=TV [zipru_scraper.middlewares] DEBUG: Solved the Zipru captcha: "KH9A8" ... |
至少看起來(lái)我們的中間件已經(jīng)成功地解決了驗(yàn)證碼,然后補(bǔ)發(fā)了請(qǐng)求。問(wèn)題在于,新的請(qǐng)求再次觸發(fā)了威脅防御機(jī)制。我第一個(gè)想法是我可能在怎樣解析或是添加cookie上面有錯(cuò)誤,但是我檢查了三次,代碼是正確的。這是另外一種情況 “唯一可能不同的事情就是請(qǐng)求頭” 。
很明顯,scrapy 和 dryscrape 的請(qǐng)求頭都繞過(guò)了最初的觸發(fā)?403?響應(yīng)的過(guò)濾器,因?yàn)槲覀儸F(xiàn)在不會(huì)得到任何?403?的響應(yīng)。這肯定是因?yàn)樗鼈兊恼?qǐng)求頭信息不一致導(dǎo)致的。我的猜測(cè)是其中一個(gè)加密的訪問(wèn)cookies包含了整個(gè)請(qǐng)求頭信息的散列值,如果這個(gè)散列不匹配,就會(huì)觸發(fā)威脅防御機(jī)制。這樣的目的可能是防止有人把瀏覽器的cookie復(fù)制到爬蟲中去,但是它只是增加了你需要解決的問(wèn)題而已。
所以讓我們?cè)?zipru_scraper/settings.py?中把請(qǐng)求頭信息修改成下面這個(gè)樣子。
Python| 1 2 3 4 5 6 7 | DEFAULT_REQUEST_HEADERS = { ????'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', ????'User-Agent': USER_AGENT, ????'Connection': 'Keep-Alive', ????'Accept-Encoding': 'gzip, deflate', ????'Accept-Language': 'en-US,*', } |
注意我們已經(jīng)把?User-Agent?頭信息修改成了我們之前定義的?USER_AGENT?中去.這個(gè)工作是由 user agent 中間件自動(dòng)添加進(jìn)去的,但是把所有的這些配置放到一個(gè)地方可以使得 dryscrape 更容易復(fù)制請(qǐng)求頭信息。我們可以通過(guò)修改?ThreatDefenceRedirectMiddleware?初始化函數(shù)像下面這樣:
Python| 1 2 3 4 5 6 7 8 9 10 11 12 | def __init__(self, settings): ????super().__init__(settings) ????# start xvfb to support headless scraping ????if 'linux' in sys.platform: ????????dryscrape.start_xvfb() ????self.dryscrape_session = dryscrape.Session(base_url='http://zipru.to') ????for key, value in settings['DEFAULT_REQUEST_HEADERS'].items(): ????????# seems to be a bug with how webkit-server handles accept-encoding ????????if key.lower() != 'accept-encoding': ????????????self.dryscrape_session.set_header(key, value) |
現(xiàn)在,當(dāng)我們可以通過(guò)命令?scrapy crawl zipru -o torrents.jl?再次運(yùn)行爬蟲。我們可以看到源源不斷的爬取的內(nèi)容,并且我們的?torrents.jl?文件記錄把爬取的內(nèi)容全部記錄了下來(lái)。我們已經(jīng)成功地繞過(guò)了所有的威脅防御機(jī)制。
總結(jié)
我們已經(jīng)成功地寫了一個(gè)能夠解決四種截然不同的威脅防御機(jī)制的爬蟲,這四種防御機(jī)制分別是:
我們的目標(biāo)網(wǎng)站 Zipru 可能是虛構(gòu)的,但是這些機(jī)制都是你會(huì)在真實(shí)網(wǎng)站上遇到的真實(shí)的反爬蟲技術(shù)。希望我們使用的方法對(duì)你自己爬蟲中遇到的挑戰(zhàn)有幫助。
轉(zhuǎn)載于:https://www.cnblogs.com/paisenpython/p/10303509.html
總結(jié)
以上是生活随笔為你收集整理的爬虫进阶:反反爬虫技巧的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 初级工程师职称怎么评?职称通过率高的人才
- 下一篇: DotC United Group赵漪涛