《Learning Scrapy》(中文版)第5章 快速構(gòu)建爬蟲


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動(dòng)應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實(shí)時(shí)分析


第3章中,我們學(xué)習(xí)了如何從網(wǎng)頁(yè)提取信息并存儲(chǔ)到Items中。大多數(shù)情況都可以用這一章的知識(shí)處理。本章,我們要進(jìn)一步學(xué)習(xí)抓取流程UR2IM中兩個(gè)R,Request和Response。

一個(gè)具有登錄功能的爬蟲

你常常需要從具有登錄機(jī)制的網(wǎng)站抓取數(shù)據(jù)。多數(shù)時(shí)候,網(wǎng)站要你提供用戶名和密碼才能登錄。我們的例子,你可以在http://web:9312/dynamichttp://localhost:9312/dynamic找到。用用戶名“user”、密碼“pass”登錄之后,你會(huì)進(jìn)入一個(gè)有三條房產(chǎn)鏈接的網(wǎng)頁(yè)?,F(xiàn)在的問(wèn)題是,如何用Scrapy登錄?

讓我們使用谷歌Chrome瀏覽器的開發(fā)者工具搞清楚登錄的機(jī)制。首先,選擇Network標(biāo)簽(1)。然后,填入用戶名和密碼,點(diǎn)擊Login(2)。如果用戶名和密碼是正確的,你會(huì)進(jìn)入下一頁(yè)。如果是錯(cuò)誤的,會(huì)看到一個(gè)錯(cuò)誤頁(yè)。

一旦你點(diǎn)擊了Login,在開發(fā)者工具的Network標(biāo)簽欄中,你就會(huì)看到一個(gè)發(fā)往http://localhost:9312/dynamic/login的請(qǐng)求Request Method: POST。

提示:上一章的GET請(qǐng)求,通常用來(lái)獲取靜止數(shù)據(jù),例如簡(jiǎn)單的網(wǎng)頁(yè)和圖片。POST請(qǐng)求通常用來(lái)獲取的數(shù)據(jù),取決于我們發(fā)給服務(wù)器的數(shù)據(jù),例如這個(gè)例子中的用戶名和密碼。

點(diǎn)擊這個(gè)POST請(qǐng)求,你就可以看到發(fā)給服務(wù)器的數(shù)據(jù),其中包括表單信息,表單信息中有你剛才輸入的用戶名和密碼。所有數(shù)據(jù)都以文本的形式發(fā)給服務(wù)器。Chrome開發(fā)者工具將它們整理好并展示出來(lái)。服務(wù)器的響應(yīng)是302 FOUND(5),然后將我們重定向到新頁(yè)面:/dynamic/gated。只有登錄成功時(shí)才會(huì)出現(xiàn)此頁(yè)面。如果沒(méi)有正確輸入用戶名和密碼就前往http://localhost:9312/dynamic/gated,服務(wù)器會(huì)發(fā)現(xiàn)你作弊,并將你重定向到錯(cuò)誤頁(yè)面:http://localhost:9312/dynamic/error。服務(wù)器怎么知道你和密碼呢?如果你點(diǎn)擊左側(cè)的gated(6),你會(huì)發(fā)現(xiàn)在RequestHeaders(7)下有一個(gè)Cookie(8)。

提示:HTTP cookie是通常是一些服務(wù)器發(fā)送到瀏覽器的短文本或數(shù)字片段。反過(guò)來(lái),在每一個(gè)后續(xù)請(qǐng)求中,瀏覽器把它發(fā)送回服務(wù)器,以確定你、用戶和期限。這讓你可以執(zhí)行復(fù)雜的需要服務(wù)器端狀態(tài)信息的操作,如你購(gòu)物車中的商品或你的用戶名和密碼。

總結(jié)一下,單單一個(gè)操作,如登錄,可能涉及多個(gè)服務(wù)器往返操作,包括POST請(qǐng)求和HTTP重定向。Scrapy處理大多數(shù)這些操作是自動(dòng)的,我們需要編寫的代碼很簡(jiǎn)單。
我們將第3章名為easy的爬蟲重命名為login,并修改里面名字的屬性,如下:

class LoginSpider(CrawlSpider):
    name = 'login'

提示:本章的代碼github的ch05目錄中。這個(gè)例子位于ch05/properties。

我們要在http://localhost:9312/dynamic/login上面模擬一個(gè)POST請(qǐng)求登錄。我們用Scrapy中的類FormRequest來(lái)做。這個(gè)類和第3章中的Request很像,但有一個(gè)額外的formdata,用來(lái)傳遞參數(shù)。要使用這個(gè)類,首先必須要引入:

from scrapy.http import FormRequest

我們?nèi)缓髮tart_URL替換為start_requests()方法。這么做是因?yàn)樵诒纠?,比起URL,我們要做一些自定義的工作。更具體地,用下面的函數(shù),我們創(chuàng)建并返回一個(gè)FormRequest:

# Start with a login request
def start_requests(self):
  return [
    FormRequest(
      "http://web:9312/dynamic/login",
      formdata={"user": "user", "pass": "pass"}
         )]

就是這樣。CrawlSpider的默認(rèn)parse()方法,即LoginSpider的基本類,負(fù)責(zé)處理響應(yīng),并如第3章中使用Rules和LinkExtractors。其余的代碼很少,因?yàn)镾crapy負(fù)責(zé)了cookies,當(dāng)我們登錄時(shí),Scrapy將cookies傳遞給后續(xù)請(qǐng)求,與瀏覽器的方式相同。還是用scrapy crawl運(yùn)行:

$ scrapy crawl login 
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Redirecting (302) to <GET .../gated> from <POST .../login >
DEBUG: Crawled (200) <GET .../data.php>
DEBUG: Crawled (200) <GET .../property_000001.html> (referer: .../data.
php)
DEBUG: Scraped from <200 .../property_000001.html>
  {'address': [u'Plaistow, London'],
   'date': [datetime.datetime(2015, 11, 25, 12, 7, 27, 120119)],
   'description': [u'features'],
   'image_URL': [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,

我們注意到登錄跳轉(zhuǎn)從dynamic/login到dynamic/gated,然后就可以像之前一樣抓取項(xiàng)目。在統(tǒng)計(jì)中,我們看到一個(gè)POST請(qǐng)求和四個(gè)GET請(qǐng)求;一個(gè)是dynamic/gated首頁(yè),三個(gè)是房產(chǎn)網(wǎng)頁(yè)。

提示:在本例中,我們不保護(hù)房產(chǎn)頁(yè),而是是這些網(wǎng)頁(yè)的鏈接。代碼在相反的情況下也是相同的。

如果我們使用了錯(cuò)誤的用戶名和密碼,我們將重定向到一個(gè)沒(méi)有URL的頁(yè)面,進(jìn)程并將在這里結(jié)束,如下所示:

$ scrapy crawl login
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Redirecting (302) to <GET .../dynamic/error > from <POST .../
dynamic/login>
DEBUG: Crawled (200) <GET .../dynamic/error>
...
INFO: Spider closed (closespider_itemcount)

這是一個(gè)簡(jiǎn)單的登錄示例,演示了基本的登錄機(jī)制。大多數(shù)網(wǎng)站可能有更復(fù)雜的機(jī)制,但Scrapy也處理的很好。例如一些網(wǎng)站在執(zhí)行POST請(qǐng)求時(shí),需要通過(guò)從表單頁(yè)面到登錄頁(yè)面?zhèn)鬟f某種形式的變量以確定cookies的啟用,讓你使用大量用戶名和密碼暴力破解時(shí)變得困難。

例如,如果你訪問(wèn)http://localhost:9312/dynamic/nonce,你會(huì)看到一個(gè)和之前一樣的網(wǎng)頁(yè),但如果你使用Chrome開發(fā)者工具,你會(huì)發(fā)現(xiàn)這個(gè)頁(yè)面的表單有一個(gè)叫做nonce的隱藏字段。當(dāng)你提交表單http://localhost:9312/dynamic/nonce-login時(shí),你必須既要提供正確的用戶名密碼,還要提交正確的瀏覽器發(fā)給你的nonce值。因?yàn)檫@個(gè)值是隨機(jī)且只能使用一次,你很難猜到。這意味著,如果要成功登陸,必須要進(jìn)行兩次請(qǐng)求。你必須訪問(wèn)表單、登錄頁(yè),然后傳遞數(shù)值。和以前一樣,Scrapy有內(nèi)建的功能可以解決這個(gè)問(wèn)題。

我們創(chuàng)建一個(gè)和之前相似的NonceLoginSpider爬蟲?,F(xiàn)在,在start_requests()中,我們要向表單頁(yè)返回一個(gè)簡(jiǎn)單的Request,并通過(guò)設(shè)定callback為名字是parse_welcome()的方法手動(dòng)處理響應(yīng)。在parse_welcome()中,我們使用FormRequest對(duì)象中的from_response()方法創(chuàng)建FormRequest,并將原始表單中的字段和值導(dǎo)入FormRequest。FormRequest.from_response()可以模擬提交表單。

提示:花時(shí)間看from_response()的文檔是十分值得的。他有許多有用的功能如formname和formnumber,它可以幫助你當(dāng)頁(yè)面有多個(gè)表單時(shí),選擇特定的表單。

它最大的功能是,一字不差地包含了表單中所有的隱藏字段。我們只需使用formdata參數(shù),填入user和pass字段,并返回FormRequest。代碼如下:

# Start on the welcome page
def start_requests(self):
    return [
        Request(
            "http://web:9312/dynamic/nonce",
            callback=self.parse_welcome)
    ]
# Post welcome page's first form with the given user/pass
def parse_welcome(self, response):
    return FormRequest.from_response(
        response,
        formdata={"user": "user", "pass": "pass"}
    )

像之前一樣運(yùn)行爬蟲:

$ scrapy crawl noncelogin 
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <GET .../dynamic/nonce>
DEBUG: Redirecting (302) to <GET .../dynamic/gated > from <POST .../
dynamic/login-nonce>
DEBUG: Crawled (200) <GET .../dynamic/gated>
...
INFO: Dumping Scrapy stats:
  {...
   'downloader/request_method_count/GET': 5,
   'downloader/request_method_count/POST': 1,
...
   'item_scraped_count': 3,

我們看到第一個(gè)GET請(qǐng)求先到/dynamic/nonce,然后POST,重定向到/dynamic/nonce-login之后,之后像之前一樣,訪問(wèn)了/dynamic/gated。登錄過(guò)程結(jié)束。這個(gè)例子的登錄含有兩步。只要有足夠的耐心,無(wú)論多少步的登錄過(guò)程,都可以完成。

使用JSON APIs和AJAX頁(yè)面的爬蟲

有時(shí),你會(huì)發(fā)現(xiàn)網(wǎng)頁(yè)的HTML找不到數(shù)據(jù)。例如,在http://localhost:9312/static/頁(yè)面上右鍵點(diǎn)擊檢查元素(1,2),你就可以在DOM樹種看到所有HTML元素?;蛘?,如果你使用scrapy shell或在Chrome中右鍵點(diǎn)擊查看網(wǎng)頁(yè)源代碼(3,4),你會(huì)看到這個(gè)網(wǎng)頁(yè)的HTML代碼不包含任何和值有關(guān)的信息。數(shù)據(jù)都是從何而來(lái)呢?

和以前一樣,在開發(fā)者工具中打開Network標(biāo)簽(5)查看發(fā)生了什么。左側(cè)列表中,可以看到所有的請(qǐng)求。在這個(gè)簡(jiǎn)單的頁(yè)面中,只有三個(gè)請(qǐng)求:static/我們已經(jīng)檢查過(guò)了,jquery.min.js是一個(gè)流行的JavaScript框架,api.json看起來(lái)不同。如果我們點(diǎn)擊它(6),然后在右側(cè)點(diǎn)擊Preview標(biāo)簽(7),我們可以看到它包含我們要找的信息。事實(shí)上,http://localhost:9312/properties/api.json包含IDs和名字(8),如下所示:

[{
    "id": 0,
    "title": "better set unique family well"
}, 
... {
    "id": 29,
    "title": "better portered mile"
}]

這是一個(gè)很簡(jiǎn)單的JSON API例子。更復(fù)雜的APIs可能要求你登錄,使用POST請(qǐng)求,或返回某種數(shù)據(jù)結(jié)結(jié)構(gòu)。任何時(shí)候,JSON都是最容易解析的格式,因?yàn)椴恍枰猉Path表達(dá)式就可以提取信息。

Python提供了一個(gè)強(qiáng)大的JSON解析庫(kù)。當(dāng)我們import json時(shí),我們可以使用json.loads(response.body)解析JSON,并轉(zhuǎn)換成等價(jià)的Python對(duì)象,語(yǔ)句、列表和字典。

復(fù)制第3章中的manual.py文件。這是最好的方法,因?yàn)槲覀円鶕?jù)JSON對(duì)象中的IDs手動(dòng)創(chuàng)建URL和Request。將這個(gè)文件重命名為api.py,重命名類為ApiSpider、名字是api。新的start_URL變成:

start_URL = (
    'http://web:9312/properties/api.json',
)

如果你要做POST請(qǐng)求或更復(fù)雜的操作,你可以使用start_requests()方法和前面幾章介紹的方法。這里,Scrapy會(huì)打開這個(gè)URL并使用Response作為參數(shù)調(diào)用parse()方法。我們可以import json,使用下面的代碼解析JSON:

def parse(self, response):
    base_url = "http://web:9312/properties/"
    js = json.loads(response.body)
    for item in js:
        id = item["id"]
        url = base_url + "property_%06d.html" % id
        yield Request(url, callback=self.parse_item)

這段代碼使用了json.loads(response.body)將響應(yīng)JSON對(duì)象轉(zhuǎn)換為Python列表,然后重復(fù)這個(gè)過(guò)程。對(duì)于列表中的每個(gè)項(xiàng),我們?cè)O(shè)置一個(gè)URL,它包含:base_url,property_%06d和.html.base_url,.html.base_url前面定義過(guò)的URL前綴。%06d是一個(gè)非常有用的Python詞,可以讓我們結(jié)合多個(gè)Python變量形成一個(gè)新的字符串。在本例中,用id變量替換%06d。id被當(dāng)做數(shù)字(%d的意思就是當(dāng)做數(shù)字進(jìn)行處理),并擴(kuò)展成6個(gè)字符,位數(shù)不夠時(shí)前面添加0。如果id的值是5,%06d會(huì)被替換為000005;id是34322時(shí),%06d會(huì)被替換為034322替換。最后的結(jié)果是可用的URL。和第3章中的yield一樣,我們用URL做一個(gè)新的Request請(qǐng)求。運(yùn)行爬蟲:

$ scrapy crawl api
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <GET ...properties/api.json>
DEBUG: Crawled (200) <GET .../property_000029.html>
...
INFO: Closing spider (finished)
INFO: Dumping Scrapy stats:
...
   'downloader/request_count': 31, ...
   'item_scraped_count': 30,

最后一共有31次請(qǐng)求,每個(gè)項(xiàng)目一次,api.json一次。

在響應(yīng)間傳遞參數(shù)

許多時(shí)候,你想把JSON APIs中的信息存儲(chǔ)到Item中。為了演示,在我們的例子中,對(duì)于一個(gè)項(xiàng),JSON API在返回它的名字時(shí),在前面加上“better”。例如,如果一個(gè)項(xiàng)的名字時(shí)“Covent Garden”,API會(huì)返回“Better Covent Garden”。我們要在Items中保存這些含有“bette”的名字。如何將數(shù)據(jù)從parse()傳遞到parse_item()中呢?

我們要做的就是在parse()方法產(chǎn)生的Request中進(jìn)行設(shè)置。然后,我們可以從parse_item()的的Response中取回。Request有一個(gè)名為meta的字典,在Response中可以直接訪問(wèn)。對(duì)于我們的例子,給字典設(shè)一個(gè)title值以存儲(chǔ)從JSON對(duì)象的返回值:

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

在parse_item()中,我們可以使用這個(gè)值,而不用XPath表達(dá)式:

l.add_value('title', response.meta['title'],
      MapCompose(unicode.strip, unicode.title))

你會(huì)注意到,我們從調(diào)用add_xpath()切換到add_value(),因?yàn)閷?duì)于這個(gè)字段不需要使用XPath。我們現(xiàn)在運(yùn)行爬蟲,就可以在PropertyItems中看到api.json中的標(biāo)題了。

一個(gè)加速30倍的項(xiàng)目爬蟲

當(dāng)你學(xué)習(xí)使用一個(gè)框架時(shí),這個(gè)框架越復(fù)雜,你用它做任何事都會(huì)很復(fù)雜??赡苣阌X得Scrapy也是這樣。當(dāng)你就要為XPath和其他方法變得抓狂時(shí),不妨停下來(lái)思考一下:我現(xiàn)在抓取網(wǎng)頁(yè)的方法是最簡(jiǎn)單的嗎?

如果你可以從索引頁(yè)中提取相同的信息,就可以避免抓取每一個(gè)列表頁(yè),這樣就可以節(jié)省大量的工作。

提示:許多網(wǎng)站的索引頁(yè)提供的項(xiàng)目數(shù)量是不同的。例如,一個(gè)網(wǎng)站可以通過(guò)調(diào)整一個(gè)參數(shù),例如&show=50,給每個(gè)索引頁(yè)面設(shè)置10、 50或100個(gè)列表項(xiàng)。如果是這樣的話,將其設(shè)置為可用的最大值。

例如,對(duì)于我們的例子,我們需要的所有信息都存在于索引頁(yè)中,包括標(biāo)題、描述、價(jià)格和圖片。這意味著我們抓取單個(gè)索引頁(yè),提取30個(gè)條目和下一個(gè)索引頁(yè)的鏈接。通過(guò)抓取100個(gè)索引頁(yè),我們得到3000個(gè)項(xiàng),但只有100個(gè)請(qǐng)求而不是3000個(gè)。

在真實(shí)的Gumtree網(wǎng)站上,索引頁(yè)的描述比列表頁(yè)的完整描述要短。這是可行的,或者是更推薦的。

提示:許多情況下,您不得不在數(shù)據(jù)質(zhì)量與請(qǐng)求數(shù)量間進(jìn)行折衷。很多網(wǎng)站都限制請(qǐng)求數(shù)量(后面章節(jié)詳解),所以減少請(qǐng)求可能解決另一個(gè)棘手的問(wèn)題。

在我們的例子中,如果我們查看一個(gè)索引頁(yè)的HTML,我們會(huì)發(fā)現(xiàn),每個(gè)列表頁(yè)有自己的節(jié)點(diǎn),itemtype="http://schema.org/Product"。節(jié)點(diǎn)有每個(gè)項(xiàng)的全部信息,如下所示:

讓我們?cè)赟crapy shell中加載索引首頁(yè),并用XPath處理:

$ scrapy shell http://web:9312/properties/index_00000.html
While within the Scrapy shell, let's try to select everything with the Product tag:
>>> p=response.xpath('//*[@itemtype="http://schema.org/Product"]')
>>> len(p)
30
>>> p
[<Selector xpath='//*[@itemtype="http://schema.org/Product"]' data=u'<li 
class="listing-maxi" itemscopeitemt'...]

我們得到了一個(gè)包含30個(gè)Selector對(duì)象的表,每個(gè)都指向一個(gè)列表。Selector對(duì)象和Response對(duì)象很像,我們可以用XPath表達(dá)式從它們指向的對(duì)象中提取信息。不同的是,表達(dá)式為有相關(guān)性的XPath表達(dá)式。相關(guān)性XPath表達(dá)式與我們之前見過(guò)的很像,不同之處是它們前面有一個(gè)點(diǎn)“.”。然我們看看如何用.//*[@itemprop="name"][1]/text()提取標(biāo)題的:

>>> selector = p[3]
>>> selector
<Selector xpath='//*[@itemtype="http://schema.org/Product"]' ... '>
>>> selector.xpath('.//*[@itemprop="name"][1]/text()').extract()
[u'l fun broadband clean people brompton european']

我們可以在Selector對(duì)象表中用for循環(huán)提取一個(gè)索引頁(yè)的所有30個(gè)項(xiàng)目信息。還是從第3章中的maunal.py文件開始,重命名為fast.py。重復(fù)使用大部分代碼,修改parse()和parse_item()方法。更新的方法如下所示:

def parse(self, response):
    # Get the next index URL and yield Requests
    next_sel = response.xpath('//*[contains(@class,"next")]//@href')
    for url in next_sel.extract():
        yield Request(urlparse.urljoin(response.url, url))
    # Iterate through products and create PropertiesItems
    selectors = response.xpath(
        '//*[@itemtype="http://schema.org/Product"]')
    for selector in selectors:
        yield self.parse_item(selector, response)

第一部分中用于產(chǎn)生下一條索引請(qǐng)求的代碼沒(méi)有變動(dòng)。不同的地方是第二部分,我們重復(fù)使用選擇器調(diào)用parse_item()方法,而不是用yield創(chuàng)建請(qǐng)求。這和原先使用的源代碼很像:

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_URL', './/*[@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()

我們做出的變動(dòng)是:

  • ItemLoader現(xiàn)在使用selector作為源,不使用Response。這么做可以讓ItemLoader更便捷,可以讓我們從特定的區(qū)域而不是整個(gè)頁(yè)面抓取信息。
  • 通過(guò)在前面添加“.”使XPath表達(dá)式變?yōu)橄嚓P(guān)XPath。

提示:碰巧的是,在我們的例子中,XPath表達(dá)式在索引頁(yè)和介紹頁(yè)中是相同的。不同的時(shí)候,你需要按照索引頁(yè)修改XPath表達(dá)式。

  • 在response.url給我們列表頁(yè)的URL之前,我們必須自己編輯Item的URL。然后,它才能返回我們抓取網(wǎng)頁(yè)的URL。我們必須用.//*[@itemprop="url"][1]/@href提取URL,然后將它用MapCompose轉(zhuǎn)化為URL絕對(duì)路徑。

這些小小大量的工作的改動(dòng)可以節(jié)省大量的工作。現(xiàn)在,用以下命令運(yùn)行爬蟲:

$ scrapy crawl fast -s CLOSESPIDER_PAGECOUNT=3
...
INFO: Dumping Scrapy stats:
   'downloader/request_count': 3, ...
   'item_scraped_count': 90,...

就像之前說(shuō)的,我們用三個(gè)請(qǐng)求,就抓取了90個(gè)項(xiàng)目。不從索引開始的話,就要用93個(gè)請(qǐng)求。

如果你想用scrapy parse來(lái)調(diào)試,你需要如下設(shè)置spider參數(shù):

$ scrapy parse --spider=fast http://web:9312/properties/index_00000.html
...
>>> STATUS DEPTH LEVEL 1 <<<
# Scraped Items  --------------------------------------------
[{'address': [u'Angel, London'],
... 30 items...
# Requests  ---------------------------------------------------
[<GET http://web:9312/properties/index_00001.html>]

正如所料,parse()返回了30個(gè)Items和下一個(gè)索引頁(yè)的請(qǐng)求。你還可以繼續(xù)試驗(yàn)scrapy parse,例如,設(shè)置—depth=2。

可以抓取Excel文件的爬蟲

大多數(shù)時(shí)候,你每抓取一個(gè)網(wǎng)站就使用一個(gè)爬蟲,但如果要從多個(gè)網(wǎng)站抓取時(shí),不同之處就是使用不同的XPath表達(dá)式。為每一個(gè)網(wǎng)站配置一個(gè)爬蟲工作太大。能不能只使用一個(gè)爬蟲呢?答案是可以。

新建一個(gè)項(xiàng)目抓取不同的東西。當(dāng)前我們是在ch05的properties目錄,向上一級(jí):

$ pwd
/root/book/ch05/properties
$ cd ..
$ pwd
/root/book/ch05

新建一個(gè)項(xiàng)目,命名為generic,再創(chuàng)建一個(gè)名為fromcsv的爬蟲:

$ scrapy startproject generic
$ cd generic
$ scrapy genspider fromcsv example.com

新建一個(gè).csv文件,它是我們抓取的目標(biāo)。我們可以用Excel表建這個(gè)文件。如下表所示,填入U(xiǎn)RL和XPath表達(dá)式,在爬蟲的目錄中(有scrapy.cfg的文件夾)保存為todo.csv。保存格式是csv:

一切正常的話,就可以在終端看見這個(gè)文件:

$ cat todo.csv 
url,name,price
a.html,"http://*[@id=""itemTitle""]/text()","http://*[@id=""prcIsum""]/text()"
b.html,//h1/text(),//span/strong/text()
c.html,"http://*[@id=""product-desc""]/span/text()"

Python中有csv文件的內(nèi)建庫(kù)。只需import csv,就可以用后面的代碼一行一行以dict的形式讀取這個(gè)csv文件。在當(dāng)前目錄打開Python命令行,然后輸入:

$ pwd
/root/book/ch05/generic2
$ python
>>> import csv
>>> with open("todo.csv", "rU") as f:
        reader = csv.DictReader(f)
        for line in reader:
            print line

文件的第一行會(huì)被自動(dòng)作為header,從而導(dǎo)出dict的鍵名。對(duì)于下面的每一行,我們得到一個(gè)包含數(shù)據(jù)的dict。用for循環(huán)執(zhí)行每一行。前面代碼的結(jié)果如下:

{'url': ' http://a.html', 'price': '//*[@id="prcIsum"]/text()', 'name': '//*[@id="itemTitle"]/text()'}
{'url': ' http://b.html', 'price': '//span/strong/text()', 'name': '//h1/text()'}
{'url': ' http://c.html', 'price': '', 'name': '//*[@id="product-desc"]/span/text()'}

很好。現(xiàn)在編輯generic/spiders/fromcsv.py爬蟲。我們使用.csv文件中的URL,并且不希望遇到域名限制的情況。因此第一件事是移除start_URL和allowed_domains。然后再讀.csv文件。

因?yàn)閺奈募凶x取的URL是我們事先不了解的,所以使用一個(gè)start_requests()方法。對(duì)于每一行,我們都會(huì)創(chuàng)建Request。我們還要從request,meta的csv存儲(chǔ)字段名和XPath,以便在我們的parse()函數(shù)中使用。然后,我們使用Item和ItemLoader填充Item的字段。下面是所有代碼:

import csv
import scrapy
from scrapy.http import Request
from scrapy.loader import ItemLoader
from scrapy.item import Item, Field
class FromcsvSpider(scrapy.Spider):
    name = "fromcsv"
def start_requests(self):
    with open("todo.csv", "rU") as f:
        reader = csv.DictReader(f)
        for line in reader:
            request = Request(line.pop('url'))
            request.meta['fields'] = line
            yield request
def parse(self, response):
    item = Item()
    l = ItemLoader(item=item, response=response)
    for name, xpath in response.meta['fields'].iteritems():
        if xpath:
      item.fields[name] = Field()
            l.add_xpath(name, xpath)
    return l.load_item()

運(yùn)行爬蟲,輸出文件保存為csv:

$ scrapy crawl fromcsv -o out.csv
INFO: Scrapy 0.0.3 started (bot: generic)
...
DEBUG: Scraped from <200 a.html>
{'name': [u'My item'], 'price': [u'128']}
DEBUG: Scraped from <200 b.html>
{'name': [u'Getting interesting'], 'price': [u'300']}
DEBUG: Scraped from <200 c.html>
{'name': [u'Buy this now']}
...
INFO: Spider closed (finished)
$ cat out.csv 
price,name
128,My item
300,Getting interesting
,Buy this now

有幾點(diǎn)要注意。項(xiàng)目中沒(méi)有定義一個(gè)整個(gè)項(xiàng)目的Items,我們必須手動(dòng)向ItemLoader提供一個(gè):

item = Item()
l = ItemLoader(item=item, response=response)

我們還用Item的fields成員變量添加了動(dòng)態(tài)字段。添加一個(gè)新的動(dòng)態(tài)字段,并用ItemLoader填充,使用下面的方法:

item.fields[name] = Field()
l.add_xpath(name, xpath)

最后讓代碼再漂亮些。硬編碼todo.csv不是很好。Scrapy提供了一種便捷的向爬蟲傳遞參數(shù)的方法。如果我們使用-a參數(shù),例如,-a variable=value,就創(chuàng)建了一個(gè)爬蟲項(xiàng),可以用self.variable取回。為了檢查變量(沒(méi)有的話,提供一個(gè)默認(rèn)變量),我們使用Python的getattr()方法:getattr(self, 'variable', 'default')。總之,原來(lái)的with open…替換為:

with open(getattr(self, "file", "todo.csv"), "rU") as f:

現(xiàn)在,todo.csv是默認(rèn)文件,除非使用參數(shù)-a,用一個(gè)源文件覆蓋它。如果還有一個(gè)文件,another_todo.csv,我們可以運(yùn)行:

$ scrapy crawl fromcsv -a file=another_todo.csv -o out.csv

總結(jié)

在本章中,我們進(jìn)一步學(xué)習(xí)了Scrapy爬蟲。我們使用FormRequest進(jìn)行登錄,用請(qǐng)求/響應(yīng)中的meta傳遞變量,使用了相關(guān)的XPath表達(dá)式和Selectors,使用.csv文件作為數(shù)據(jù)源等等。

接下來(lái)在第6章學(xué)習(xí)在Scrapinghub云部署爬蟲,在第7章學(xué)習(xí)關(guān)于Scrapy的設(shè)置。


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動(dòng)應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實(shí)時(shí)分析


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容