1. 程式人生 > >高階網頁抓取:如何繞過雷區,抓取成功

高階網頁抓取:如何繞過雷區,抓取成功

介紹

我不會真的考慮網站刮我的愛好或任何東西,但我想我做了很多。看起來我所處理的許多事情都要求我掌握不能以任何其他方式獲得的資料。我需要對Intoli的遊戲進行靜態分析,因此我需要搜尋Google Play商店才能找到新遊戲並下載遊戲。尖尖的球擴充套件需要從不同的網站和最簡單的方式聚集夢幻足球預測是寫一個刮刀。當我想起它時,我可能已經寫了大約40-50個刮板。我並不是在向我的家人說謊我囤積了多少TB的資料......但我很接近。

我已經嘗試過X光 / cheerionokogiri和其他一些,但我總是回到我個人的最愛:scrapy在我看來,scrapy是一款優秀的軟體。我不會輕易地丟擲這種明確的讚譽,但它感覺非常直觀,並且有很好的學習曲線。

您可以閱讀The Scrapy教程並讓您的第一個刮板在幾分鐘內執行。然後,當你需要做一些更復雜的事情時,你很可能會發現有一個內建的和有據可查的方式來做到這一點。很大的權力建立在但框架的結構使得它保持你的出路,直到你需要它。當你最終確實需要某些預設情況下不存在的內容時,可以使用布隆過濾器進行重複資料刪除,因為您訪問的URL過多,無法儲存到記憶體中,那麼通常就像子類化其中一個元件並進行一些小改動一樣簡單。一切都感覺如此簡單,這在我的書中確實是一個很好的軟體設計的標誌。

我已經玩了一段時間編寫高階scrapy教程的想法。這些東西可以讓我有機會展示它的一些可擴充套件性,同時解決實際中出​​現的現實挑戰。

儘管我想這樣做,但我無法擺脫這樣一個事實,即它似乎是一個決定性的舉動,想要釋出一些可能會導致某人的伺服器受到bot流量攻擊的東西。

晚上我可以睡得很好,只要遵循一些基本規則,就可以積極地嘗試防止刮擦。也就是說,我保持我的請求率與我手動瀏覽時的請求率相當,並且我不會對資料做任何事情。這使得執行刮板基本上無法以任何重要的方式手動收集資料。即使我親自遵守這些規則,對於人們可能真正想要抓取的特定網站,如何做指導仍然是一個過分的步驟。

因此,直到我遇到一個名為Zipru的洪流網站時,它仍然只是一個模糊的想法。它有多種機制,需要先進的抓取技術,但其robots.txt檔案允許抓取。此外,沒有理由刮掉它它有一個公共API,可用於獲取所有相同的資料。

如果您有興趣獲取torrent資料,那麼只需使用API​​; 這很好。

在本文的其餘部分中,我將帶領您撰寫一個可以處理驗證碼和我們在Zipru網站上遇到的各種其他挑戰的刮板。該程式碼不會完全按照書面方式工作,因為Zipru不是一個真正的網站,但所採用的技術廣泛適用於現實世界的抓取並且程式碼完整。我假設你對python有基本的瞭解,但是我會盡力讓這些對scrapy很少或根本不瞭解的人來說。如果事情一開始太快,那麼花幾分鐘的時間閱讀The Scrapy教程,其中將深入介紹介紹性內容。

設定專案

我們將在一個virtualenv內工作,這讓我們可以封裝我們的依賴關係。我們先來設定一個virtualenv ~/scrapers/zipru並安裝scrapy。

mkdir ~/scrapers/zipru
cd ~/scrapers/zipru
virtualenv env
. env/bin/activate
pip install scrapy

您執行那些終端現在將被配置為使用本地virtualenv。如果你開啟另一個終端,那麼你需要. ~/scrapers/zipru/env/bin/active再次執行(否則你可能會得到有關命令或模組未被發現的錯誤)。

您現在可以通過執行建立一個新的專案腳手架

scrapy startproject zipru_scraper

這將建立以下目錄結構。

└── zipru_scraper
    ├── zipru_scraper
    │   ├── __init__.py
    │   ├── items.py
    │   ├── middlewares.py
    │   ├── pipelines.py
    │   ├── settings.py
    │   └── spiders
    │       └── __init__.py
    └── scrapy.cfg

大多數這些檔案預設情況下並未實際使用,他們只是提出了一種理想的方式來構建我們的程式碼。從現在開始,您應該將其~/scrapers/zipru/zipru_scraper視為專案的頂級目錄。這是任何scrapy命令應該執行的地方,也是任何相對路徑的根源。

新增一個基本的蜘蛛

我們現在需要新增一個蜘蛛,以便讓我們的刮板實際上做任何事情。蜘蛛是scrapy刮板的一部分,它處理解析文件以查詢新的URL以提取和提取資料。我將非常依賴預設的Spider實現來最大限度地減少我們必須編寫的程式碼量。這裡的東西可能看起來有點自動化,但如果您檢視文件,情況會更少。

首先,建立一個名為zipru_scraper/spiders/zipru_spider.py以下內容的檔案

import scrapy

class ZipruSpider(scrapy.Spider):
    name = 'zipru'
    start_urls = ['http://zipru.to/torrents.php?category=TV']

我們的蜘蛛繼承了scrapy.Spider它,提供了一種start_requests()方法,可以通過start_urls它來開始我們的搜尋。我們已經在start_urls這些點上提供了一個單獨的URL 到電視列表。他們看起來像這樣。

電視節目在zipru上顯示

在頂部,您可以看到有連結指向其他頁面。我們希望我們的刮板遵循這些連結並解析它們。為此,我們首先需要確定連結並找出它們指向的位置。

在這個階段DOM檢查員可以是一個巨大的幫助。如果要右鍵單擊其中一個頁面連結並在檢查器中檢視它,則會看到其他列表頁面的連結如下所示

<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>

接下來,我們需要為這些連結構造選擇器表示式。有一些類似的搜尋看起來更適合於css或xpath選擇器,所以我通常傾向於混合並連結它們,有點自由。如果您不知道它,我強烈建議學習xpath,但不幸的是這超出了本教程的範圍。我個人認為這對於拼湊,網路使用者介面測試,甚至一般的網頁開發來說都是不可或缺的。我會堅持使用CSS選擇器,因為它們可能對大多數人更為熟悉。

要選擇這些頁面連結,我們可以<a>使用a[title ~= page]css選擇器在標題中查詢帶有“頁面”的標籤如果你按下ctrl-fDOM檢查器,那麼你會發現你可以使用這個CSS表示式作為搜尋查詢(這也適用於xpath!)。這樣做可以讓您迴圈檢視所有比賽。這是檢查表示式是否有效的一種好方法,但也不是非常模糊以至於無意中與其他事物相匹配。我們的頁面連結選擇器滿足這兩個標準。

為了告訴我們的蜘蛛如何找到這些其他頁面,我們將新增一個parse(response)方法來ZipruSpider像這樣

    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)

當我們開始抓取時,我們新增的URL start_urls將被自動提取,並將響應反饋到此parse(response)方法中。我們的程式碼然後找到所有到其他列表頁面的連結,併產生附加到同一parse(response)回撥的新請求這些請求將轉化為響應物件,然後parse(response)只要URL尚未處理(由於過濾器),就會反饋回來

我們的刮板已經可以找到並請求所有不同的列表頁面,但我們仍然需要提取一些實際資料以使其有用。洪流列表坐在<table>一起class="list2at",然後每個單獨的列表是在一個<tr>class="lista2"這些行中的每一行又包含<td>對應於“類別”,“檔案”,“新增”,“大小”,“播種者”,“採集者”,“評論”和“上傳者”的8個標籤。在程式碼中檢視其他細節可能是最簡單的,因此這裡是我們更新的parse(response)方法。

    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)現在的方法也會生成字典,這些字典會根據型別自動區分請求。每個字典將被解釋為一個專案,並作為我們的刮板資料輸出的一部分。

如果我們只是在拼搶大多數網站,我們現在就會完成。我們可以跑

scrapy crawl zipru -o torrents.jl

幾分鐘後,我們將擁有一個很好的JSON Lines格式的torrents.jl檔案和我們所有的torrent資料。相反,我們得到這個(還有很多其他的東西)

[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)

Drats!我們必須更聰明地獲取我們完全可以從公共API獲得的資料,並且永遠不會真正抓取。

簡單的問題

我們的第一個請求會得到一個403被忽略響應,然後所有內容都會關閉,因為我們只通過一個網址對爬網進行播種。即使在沒有會話記錄的無痕模式下,相同的請求也可以在網路瀏覽器中正常工作,所以這必須由請求標頭中的一些差異引起。我們可以使用tcpdump來比較兩個請求的頭部,但這裡有一個常見的罪魁禍首,我們應該首先檢查:使用者代理。

Scrapy預設標識為“Scrapy / 1.3.3(+ http://scrapy.org)”,有些伺服器可能會阻止此功能,甚至會將有限數量的使用者代理列入白名單。您可以網上找到最常見的使用者代理列表,使用其中的一種往往足以繞開基本的防刮擦措施。選擇你喜歡的,然後開啟zipru_scraper/settings.py並更換

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'zipru_scraper (+http://www.yourdomain.com)'

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'

您可能會注意到,預設的scrapy設定在那裡做了一些刮擦。關於這個問題的意見不同,但我個人認為,如果你的刮板行為像使用普通網路瀏覽器的人一樣,可以將其識別為普通的網路瀏覽器。所以讓我們通過增加一點來減慢響應速度

CONCURRENT_REQUESTS = 1
DOWNLOAD_DELAY = 5

由於AutoThrottle擴充套件,這將建立一個有點逼真的瀏覽模式我們的刮板也將robots.txt預設尊重,所以我們真的在我們最好的行為。

現在再次執行刮刀scrapy crawl zipru -o torrents.jl應該會產生

[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)

這是真正的進步!我們有兩種200狀態,一種302是下載中介軟體知道如何處理。不幸的是,這表明302我們看起來有些不祥threat_defense.php不出所料,蜘蛛在那裡找不到任何好處,並且爬行終止。

下載中介軟體

在深入瞭解我們面臨的更大問題之前,瞭解一下在scrapy中如何處理請求和響應將會很有幫助。當我們建立我們的基本蜘蛛時,我們產生了scrapy.Request物件,然後它們以某種方式變成scrapy.Response與伺服器響應對應的物件。這種“不知何故”的很大一部分是下載中介軟體。

下載器中介軟體繼承scrapy.downloadermiddlewares.DownloaderMiddleware並實現兩者process_request(request, spider)process_response(request, response, spider)方法。你大概可以猜出那些人的名字。實際上有一大堆這些中介軟體預設是啟用的。這是標準配置的樣子(你當然可以禁用東西,新增東西或重新排列東西)。

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,
}

當一個請求到達伺服器時,它會通過process_request(request, spider)這些中介軟體方法來冒泡這發生在按順序的數字順序中,以便RobotsTxtMiddleware首先處理請求和HttpCacheMiddleware處理它的最後處理。然後,一旦產生了響應,它就會通過process_response(request, response, spider)任何啟用的中介軟體方法回彈這次以相反的順序發生,所以數字越高越接近伺服器,越低的數字越接近蜘蛛。

一個特別簡單的中介軟體是CookiesMiddleware它基本上檢查Set-Cookie傳入響應頭部並儲存cookie。然後,當響應正在出來時,它會Cookie適當地設定標題,以便它們包含在傳出請求中。比因過期和東西而複雜一點,但你明白了。

另一個相當基本的是RedirectMiddleware處理,等待它... 3XX重定向。這一個讓任何非3XX狀態程式碼響應愉快地冒泡,但如果有重定向呢?它能夠找出伺服器如何響應重定向URL的唯一方法是建立一個新的請求,所以這正是它的功能。當該process_response(request, response, spider)方法返回一個請求物件而不是一個響應時,那麼當前的響應將被丟棄,並且所有事情都會從新的請求開始。這就是RedirectMiddleware處理重定向的方式,這是我們即將使用的一項功能。

如果您驚訝地發現預設情況下啟用了這麼多的下載器中介軟體,那麼您可能有興趣檢視體系結構概述這事實上是一種很多其他的東西怎麼回事,但同樣,約scrapy偉大的事情之一是,你並不需要知道大部分東西。就像您甚至不需要知道下載器中介軟體存在編寫功能性蜘蛛一樣,您也不需要知道這些其他部分來編寫功能性下載器中介軟體。

難題(s)

回到我們的刮板,我們發現我們被重定向到一些threat_defense.php?defense=1&...URL而不是接收我們正在尋找的頁面。當我們在瀏覽器中訪問此頁面時,我們會在幾秒鐘內看到類似的內容

一個JavaScript重定向

在重定向到threat_defense.php?defense=2&...更像這樣頁面之前

驗證碼框

檢視第一頁的原始碼顯示,有一些JavaScript程式碼負責構建特殊的重定向URL,並且還負責手動構建瀏覽器Cookie。如果我們要解決這個問題,那麼我們必須處理這兩項任務。

那麼,當然,我們也必須解決驗證碼並提交答案。如果我們碰巧遇到了錯誤,那麼我們有時會重定向到另一個驗證碼頁面,而其他時候我們最終會看到像這樣的頁面

重試驗證碼

我們需要點選“點選此處”連結開始整個重定向週期。一塊蛋糕,對吧?

我們所有的問題都源於最初的302重定向,因此處理它們的自然地方是在重定向中介軟體的定製版本中我們希望我們的中介軟體像,除了有一個時,在所有情況下的正常重定向中介軟體302threat_defense.php頁面。當它遇到特殊情況時302,我們希望它繞過所有這些威脅防禦措施,將訪問cookie附加到會話中,最後重新請求原始頁面。如果我們能夠解決這個問題,那麼我們的蜘蛛並不需要知道任何這種業務,並且請求會“正常工作”。

因此,開啟zipru_scraper/middlewares.py並更換內容

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直接進行。