當前位置:
首頁 > 最新 > 高效Python爬蟲技巧

高效Python爬蟲技巧

參與文末每日話題討論,贈送非同步新書

在本文中,我們將看到更多特殊的例子,以便讓你更加熟悉Scrapy的兩個最重要的類——和。

1.1 需要登錄的爬蟲

通常情況下,你會發現自己想要抽取數據的網站存在登錄機制。大部分情況下,網站會要求你提供用戶名和密碼用於登錄。你可以從(從dev機器訪問)或(從宿主機瀏覽器訪問)找到我們要使用的例子。如果使用"user"作為用戶名,"pass"作為密碼的話,你就可以訪問到包含3個房產頁面鏈接的網頁。不過現在的問題是,要如何使用Scrapy執行相同的操作?

讓我們使用Google Chrome瀏覽器的開發者工具來嘗試理解登錄的工作過程(見圖1.1)。首先,打開Network選項卡(1)。然後,填寫用戶名和密碼,並單擊Login(2)。如果用戶名和密碼正確,你將會看到包含3個鏈接的頁面。如果用戶名和密碼不匹配,將會看到一個錯誤頁。

圖1.1 登錄網站時的請求和響應

當按下Login按鈕時,會在Google Chrome瀏覽器開發者工具的Network選項卡中看到一個包含Request Method: POST的請求,其目的地址為。

當你單擊該請求時(3),可以看到發送給服務端的數據,包括Form Data(4),其中包含了我們輸入的用戶名和密碼。這些數據都是以文本形式傳輸給服務端的。Chrome瀏覽器只是將其組織起來,向我們更好地顯示這些數據。服務端的響應是302 Found(5),使我們跳轉到一個新的頁面:。該頁面只有在登錄成功後才會出現。如果嘗試直接訪問,而不輸入正確的用戶名和密碼的話,服務端會發現你在作弊,並跳轉到錯誤頁,其地址是。服務端是如何知道你和你的密碼的呢?如果你單擊開發者工具左側的gated(6),就會發現在Request Headers區域下面(7)設置了一個Cookie值(8)。

總之,即使是一個單一的操作,比如登錄,也可能涉及包括POST請求和HTTP跳轉的多次服務端往返。Scrapy能夠自動處理大部分操作,而我們需要編寫的代碼也很簡單。

我們從第3章中名為的爬蟲開始,創建一個新的爬蟲,命名為,保留原有文件,並修改爬蟲中的屬性(如下所示):

classLoginSpider(CrawlSpider):

name ="login"

我們需要通過執行到的POST請求,發送登錄的初始請求。這將通過Scrapy的類實現該功能。要想使用該類,首先需要引入如下模塊。

fromscrapy.httpimportFormRequest

然後,將語句替換為方法。這樣做是因為在本例中,我們需要從一些更加定製化的請求開始,而不僅僅是幾個URL。更確切地說就是,我們從該函數中創建並返回一個。

# Start with a login request

defstart_requests(self):

return[

FormRequest(

"http://web:9312/dynamic/login",

formdata={"user":"user","pass":"pass"}

)]

雖然聽起來不可思議,但是(的基類)默認的方法確實處理了,並且仍然能夠使用第3章中的和。我們只編寫了非常少的額外代碼,這是因為Scrapy為我們透明處理了Cookie,並且一旦我們登錄成功,就會在後續的請求中傳輸這些Cookie,就和瀏覽器執行的方式一樣。接下來可以像平常一樣,使用運行。

scrapy crawl login

INFO: Scrapy 1.0.3 started (bot: properties)

...

DEBUG: Redirecting (302) to from

DEBUG: Crawled (200)

DEBUG: Crawled (200) (referer: .../data.

DEBUG: Scraped from

{"address": [u"Plaistow, London"],

"date": [datetime.datetime(2015, 11, 25, 12, 7, 27, 120119)],

"description": [u"features"],

"image_urls": [u"http://web:9312/images/i02.jpg"],

...

INFO: Closing spider (finished)

INFO: Dumping Scrapy stats:

{...

"downloader/request_method_count/GET": 4,

"downloader/request_method_count/POST": 1,

...

"item_scraped_count": 3,

我們可以在日誌中看到從到的跳轉,然後就會像平時那樣抓取Item了。在統計中,可以看到1個POST請求和4個GET請求(一個是前往索引頁,另外3個是房產頁面)。

如果使用了錯誤的用戶名和密碼,將會跳轉到一個沒有任何項目的頁面,並且此時爬取過程會被終止,如下面的執行情況所示。

scrapy crawl login

INFO: Scrapy 1.0.3 started (bot: properties)

...

DEBUG: Redirecting (302) to from

dynamic/login>

DEBUG: Crawled (200)

...

INFO: Spider closed (closespider_itemcount)

這是一個簡單的登錄示例,用於演示基本的登錄機制。大多數網站都會擁有一些更加複雜的機制,不過Scrapy也都能夠輕鬆處理。比如,一些網站要求你在執行POST請求時,將表單頁中的某些表單變數傳輸到登錄頁,以便確認Cookie是啟用的,同樣也會讓你在嘗試暴力破解成千上萬次用戶名/密碼的組合時更加困難。圖1.2所示即為此種情況的一個示例。

圖1.2 使用一次性隨機數的一個更加高級的登錄示例的請求和響應情況

比如,當訪問時,你會看到一個看起來一樣的頁面,但是當使用Chrome瀏覽器的開發者工具查看時,會發現頁面的表單中有一個叫作nonce的隱藏欄位。當提交該表單時(提交到),除非你既傳輸了正確的用戶名/密碼,又提交了服務端在你訪問該登錄頁時給你的值,否則登錄不會成功。你無法猜測該值,因為它通常是隨機且一次性的。這就表示要想成功登錄,現在就需要請求兩次了。你必須先訪問表單頁,然後再訪問登錄頁傳輸數據。當然,Scrapy同樣擁有內置函數可以幫助我們實現這一目的。

我們創建了一個和之前相似的爬蟲。現在,在中,將返回一個簡單的(不要忘記引入該模塊)到表單頁面中,並通過設置其屬性為處理方法手動處理響應。在中,使用了對象的輔助方法,以創建從原始表單中預填充所有欄位和值的對象。粗略模擬了一次在頁面的第一個表單上的提交單擊,此時所有欄位留空。

該方法對於我們來說非常有用,因為它能夠毫不費力地原樣包含表單中的所有隱藏欄位。我們所需要做的就是使用參數填充和欄位以及返回。下面是其相關代碼。

# Start on the welcome page

defstart_requests(self):

return[

Request(

"http://web:9312/dynamic/nonce",

callback=self.parse_welcome)

]

# Post welcome page"s first form with the given user/pass

defparse_welcome(self, response):

returnFormRequest.from_response(

response,

formdata={"user":"user","pass":"pass"}

)

我們可以像平時一樣運行爬蟲。

$ scrapy crawl noncelogin

INFO: Scrapy 1.0.3 started (bot: properties)

...

DEBUG: Crawled (200)

DEBUG: Redirecting (302) to from

dynamic/login-nonce>

DEBUG: Crawled (200)

...

INFO: Dumping Scrapy stats:

{...

"downloader/request_method_count/GET": 5,

"downloader/request_method_count/POST": 1,

...

"item_scraped_count": 3,

可以看到,第一個GET請求前往頁面,然後是POST請求,跳轉到頁面,之後像前面的例子一樣跳轉到頁面。關於登錄的討論就到這裡。該示例使用兩個步驟完成登錄。只要你有足夠的耐心,就可以形成任意長鏈,來執行幾乎所有的登錄操作。


有時,你會發現自己在頁面尋找的數據無法從HTML頁面中找到。比如,當訪問時(見圖5.3),在頁面任意位置右鍵單擊inspect element(1, 2),可以看到其中包含所有常見HTML元素的DOM樹。但是,當你使用請求,或是在Chrome瀏覽器中右鍵單擊View Page Source(3, 4)時,則會發現該頁面的HTML代碼中並不包含關於房產的任何信息。那麼,這些數據是從哪裡來的呢?

圖1.3 動態載入JSON對象時的頁面請求與響應

[{

"id":,

"title":"better set unique family well"

},

... {

"id":29,

"title":"better portered mile"

}]

這是一個非常簡單的JSON API的示例。更複雜的API可能需要你登錄,使用POST請求,或返回更有趣的數據結構。無論在哪種情況下,JSON都是最簡單的解析格式之一,因為你不需要編寫任何XPath表達式就可以從中抽取出數據。

Python提供了一個非常好的JSON解析庫。當我們執行時,就可以使用解析JSON,將其轉換為由Python原語、列表和字典組成的等效Python對象。

我們將第3章的拷貝過來,用於實現該功能。在本例中,這是最佳的起始選項,因為我們需要通過在JSON對象中找到的ID,手動創建房產URL以及對象。我們將該文件重命名為,並將爬蟲類重命名為,屬性修改為。新的將會是JSON API的URL,如下所示。

start_urls = (

"http://web:9312/properties/api.json",

)

如果你想執行POST請求,或是更複雜的操作,可以使用前一節中介紹的方法。此時,Scrapy將會打開該URL,並調用包含以為參數的方法。可以通過,使用如下代碼解析JSON對象。

defparse(self, response):

base_url ="http://web:9312/properties/"

js = json.loads(response.body)

foriteminjs:

id = item["id"]

url = base_url +"property_%06d.html"% id

yieldRequest(url, callback=self.parse_item)

前面的代碼使用了,將這個JSON對象解析為Python列表,然後迭代該列表。對於列表中的每一項,我們將URL的3個部分(、以及)組合到一起。是在前面定義的URL前綴。是Python語法中非常有用的一部分,它可以讓我們結合Python變數創建新的字元串。在本例中,將會被變數的值替換(本行結尾處%後面的變數)。將會被視為數字(表示視為數字),並且如果不滿6位,則會在前面加上0,擴展成6位字元。比如,值為5,將會被替換為000005,而如果為34322,則會被替換為034322。最終結果正是我們房產頁面的有效URL。我們使用該URL形成一個新的對象,並像第3章一樣使用。然後可以像平時那樣使用運行該示例。

scrapy crawl api

INFO: Scrapy 1.0.3 started (bot: properties)

...

DEBUG: Crawled (200)

DEBUG: Crawled (200)

...

INFO: Closing spider (finished)

INFO: Dumping Scrapy stats:

...

"downloader/request_count": 31, ...

"item_scraped_count": 30,

你可能會注意到結尾處的狀態是31個請求——每個Item一個請求,以及最初的的請求。

1.2.1 在響應間傳參

很多情況下,在JSON API中會有感興趣的信息,你可能想要將它們存儲到中。在我們的示例中,為了演示這種情況,JSON API會在給定房產信息的標題前面加上"better"。比如,房產標題是"Covent Garden",API就會將標題寫為"Better Covent Garden"。假設我們想要將這些"better"開頭的標題存儲到中,要如何將信息從方法傳遞到方法呢?

不要感到驚訝,通過在生成的中設置一些東西,就能實現該功能。之後,可以從接收到的中取得這些信息。有一個名為的字典,能夠直接訪問。比如在我們的例子中,可以在該字典中設置標題值,以存儲來自JSON對象的標題。

title = item["title"]

yield Request(url, meta={"title": title},callback=self.parse_item)

在內部,可以使用該值替代之前使用過的XPath表達式。

l.add_value("title", response.meta["title"],

MapCompose(unicode.strip, unicode.title))

你會發現我們不再調用,而是轉為調用,這是因為我們在該欄位中將不會再使用到任何XPath表達式。現在,可以使用運行這個新的爬蟲,並且可以在中看到來自的標題。


有這樣一種趨勢,當你開始使用一個框架時,做任何事情都可能會使用最複雜的方式。你在使用Scrapy時也會發現自己在做這樣的事情。在瘋狂於XPath等技術之前,值得停下來想一想:我選擇的方式是從網站中抽取數據最簡單的方式嗎?

如果你能從索引頁中抽取出基本相同的信息,就可以避免抓取每個房源頁,從而得到數量級的提升。

比如,在房產示例中,我們所需要的所有信息都存在於索引頁中,包括標題、描述、價格和圖片。這就意味著只抓取一個索引頁,就能抽取其中的30個條目以及前往下一頁的鏈接。通過爬取100個索引頁,我們只需要100個請求,而不是3000個請求,就能夠得到3000個條目。太棒了!

在真實的Gumtree網站中,索引頁的描述信息要比列表頁中完整的描述信息稍短一些。不過此時這種抓取方式可能也是可行的,甚至也能令人滿意。

在我們的例子中,當查看任何一個索引頁的HTML代碼時,就會發現索引頁中的每個房源都有其自己的節點,並使用來表示。在該節點中,我們擁有與詳情頁完全相同的方式為每個屬性註解的所有信息,如圖5.4所示。

圖5.4 從單一索引頁抽取多個房產信息

我們在Scrapy shell中載入第一個索引頁,並使用XPath表達式進行測試。

scrapy shellhttp://web:9312/properties/index_00000.html

在Scrapy shell中,嘗試選取所有帶有Product標籤的內容:

p=response.xpath("//*[@itemtype="http://schema.org/Product"]")

len(p)

30

p

[

class="listing-maxi" itemscopeitemt"...]

可以看到我們得到了一個包含30個對象的列表,每個對象指向一個房源。在某種意義上,對象與對象有些相似,我們可以在其中使用XPath表達式,並且只從它們指向的地方獲取信息。唯一需要說明的是,這些表達式應該是相對XPath表達式。相對XPath表達式與我們之前看到的基本一樣,不過在前面增加了一個"."點號。舉例說明,讓我們看一下使用這個相對XPath表達式,從第4個房源抽取標題時是如何工作的。

selector = p[3]

selector

selector.xpath(".//*[@itemprop="name"][1]/text()").extract()

[u"l fun broadband clean people brompton european"]

可以在對象的列表中使用循環,抽取索引頁中全部30個條目的信息。

為了實現該目的,我們再一次從第3章的著手,將爬蟲重命名為"fast",並重命名文件為。我們將復用大部分代碼,只在和方法中進行少量修改。最新方法的代碼如下。

defparse(self, response):

# Get the next index URLs and yield Requests

next_sel = response.xpath("//*[contains(@class,"next")]//@href")

forurlinnext_sel.extract():

yieldRequest(urlparse.urljoin(response.url, url))

# Iterate through products and create PropertiesItems

selectors = response.xpath(

"//*[@itemtype="http://schema.org/Product"]")

forselectorinselectors:

yieldself.parse_item(selector, response)

在代碼的第一部分中,對前往下一個索引頁的的操作的代碼沒有變化。唯一改變的內容在第二部分,不再使用為每個詳情頁創建請求,而是迭代選擇器並調用。其中,的代碼也和原始代碼非常相似,如下所示。

def parse_item(self, selector, response):

# Create the loader using the selector

l = ItemLoader(item=PropertiesItem(), selector=selector)

# Load fields using XPath expressions

l.add_xpath("title", ".//*[@itemprop="name"][1]/text()",

MapCompose(unicode.strip, unicode.title))

l.add_xpath("price", ".//*[@itemprop="price"][1]/text()",

MapCompose(lambda i: i.replace(",", ""), float),

re="[,.0-9]+")

l.add_xpath("description",

".//*[@itemprop="description"][1]/text()",

MapCompose(unicode.strip), Join())

l.add_xpath("address",

".//*[@itemtype="http://schema.org/Place"]"

"[1]/*/text()",

MapCompose(unicode.strip))

make_url = lambda i: urlparse.urljoin(response.url, i)

l.add_xpath("image_urls", ".//*[@itemprop="image"][1]/@src",

MapCompose(make_url))

# Housekeeping fields

l.add_xpath("url", ".//*[@itemprop="url"][1]/@href",

MapCompose(make_url))

l.add_value("project", self.settings.get("BOT_NAME"))

l.add_value("spider", self.name)

l.add_value("server", socket.gethostname())

l.add_value("date", datetime.datetime.now())

return l.load_item()

我們所做的細微變更如下所示。

現在使用作為源,而不再是。這是API一個非常便捷的功能,能夠讓我們從當前選取的部分(而不是整個頁面)抽取數據。

XPath表達式通過使用前綴點號(.)轉為相對XPath。

我們必須自己編輯的URL。之前,已經給出了房源頁的URL。而現在,它給出的是索引頁的URL,因為該頁面才是我們要爬取的。我們需要使用熟悉的這個XPath表達式抽取出房源的URL,然後使用處理器將其轉換為絕對URL。

小的改變能夠節省巨大的工作量。現在,我們可以使用如下代碼運行該爬蟲。

scrapy crawl fast -s CLOSESPIDER_PAGECOUNT=3

...

INFO: Dumping Scrapy stats:

"downloader/request_count": 3, ...

"item_scraped_count": 90,...

和預期一樣,只用了3個請求,就抓取了90個條目。如果我們沒有在索引頁中獲取到的話,則需要93個請求。這種方式太明智了!

如果你想使用進行調試,那麼現在必須設置參數,如下所示。

$ scrapy parse --spider=fast http://web:9312/properties/index_00000.html

...

STATUS DEPTH LEVEL 1

# Scraped Items --------------------------------------------

[{"address": [u"Angel, London"],

... 30 items...

# Requests ---------------------------------------------------

[]

正如期望的那樣,返回了個以及一個前往下一索引頁的。請使用隨意試驗,比如傳輸。

本文摘自《精通Python爬蟲框架Scrapy》

Scrapy是使用Python開發的一個快速、高層次的屏幕抓取和Web抓取框架,用於抓Web站點並從頁面中提取結構化的數據。本書以Scrapy 1.0版本為基礎,講解了Scrapy的基礎知識,以及如何使用Python和三方API提取、整理數據,以滿足自己的需求。

延伸推薦


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python 的精彩文章:

Python爬蟲的債市小試(二)

TAG:Python |