《Learning Scrapy》(中文版)第3章 爬蟲基礎(chǔ)


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


本章非常重要,你可能需要讀幾遍,或是從中查找解決問題的方法。我們會從如何安裝Scrapy講起,然后在案例中講解如何編寫爬蟲。開始之前,說幾個注意事項(xiàng)。

因?yàn)槲覀凂R上要進(jìn)入有趣的編程部分,使用本書中的代碼段會十分重要。當(dāng)你看到:

$ echo hello world
hello world

是要讓你在終端中輸入echo hello world(忽略$),第二行是看到結(jié)果。

當(dāng)你看到:

>>> print 'hi'
hi

是讓你在Python或Scrapy界面進(jìn)行輸入(忽略>>>)。同樣的,第二行是輸出結(jié)果。

你還需要對文件進(jìn)行編輯。編輯工具取決于你的電腦環(huán)境。如果你使用Vagrant(強(qiáng)烈推薦),你可以是用Notepad、Notepad++、Sublime Text、TextMate,Eclipse、或PyCharm等文本編輯器。如果你更熟悉Linux/Unix,你可以用控制臺自帶的vim或emacs。這兩個編輯器功能強(qiáng)大,但是有一定的學(xué)習(xí)曲線。如果你是初學(xué)者,可以選擇適合初學(xué)者的nano編輯器。

安裝Scrapy

Scrapy的安裝相對簡單,但這還取決于讀者的電腦環(huán)境。為了支持更多的人,本書安裝和使用Scrapy的方法是用Vagrant,它可以讓你在Linux盒中使用所有的工具,而無關(guān)于操作系統(tǒng)。下面提供了Vagrant和一些常見操作系統(tǒng)的指導(dǎo)。

MacOS

為了輕松跟隨本書學(xué)習(xí),請參照后面的Vagrant說明。如果你想在MacOS中安裝Scrapy,只需控制臺中輸入:

$ easy_install scrapy

然后,所有事就可以交給電腦了。安裝過程中,可能會向你詢問密碼或是否安裝Xcode,只需同意即可。

Windows
在Windows中安裝Scrapy要麻煩些。另外,在Windows安裝本書中所有的軟件也很麻煩。我們都為你想到了可能的問題。有Virtualbox的Vagrant可以在所有64位電腦上順利運(yùn)行。翻閱相關(guān)章節(jié),只需幾分鐘就可以安裝好。如果真要在Windows中安裝,請參考本書網(wǎng)站http://scrapybook.com/上面的資料。

Linux
你可能會在多種Linux服務(wù)器上安裝Scrapy,步驟如下:

提示:確切的安裝依賴變化很快。寫作本書時,Scrapy的版本是1.0.3(翻譯此書時是1.4)。下面只是對不同服務(wù)器的建議方法。

Ubuntu或Debian Linux
為了在Ubuntu(測試機(jī)是Ubuntu 14.04 Trusty Tahr - 64 bit)或是其它使用apt的服務(wù)器上安裝Scrapy,可以使用下面三條命令:

$ sudo apt-get update
$ sudo apt-get install python-pip python-lxml python-crypto python-
cssselect python-openssl python-w3lib python-twisted python-dev libxml2-
dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev
$ sudo pip install scrapy

這個方法需要進(jìn)行編譯,可能隨時中斷,但可以安裝PyPI上最新版本的Scrapy。如果想避開編譯,安裝不是最新版本的話,可以搜索“install Scrapy Ubuntu packages”,按照官方文檔安裝。

Red Hat或CentOS Linux
在使用yum的Linux上安裝Scrapy也很簡單(測試機(jī)是Ubuntu 14.04 Trusty Tahr - 64 bit)。只需三條命令:

sudo yum update
sudo yum -y install libxslt-devel pyOpenSSL python-lxml python-devel gcc
sudo easy_install scrapy

從GitHub安裝
按照前面的指導(dǎo),就可以安裝好Scrapy的依賴了。Scrapy是純Python寫成的,如果你想編輯源代碼或是測試最新版,可以從https://github.com/scrapy/scrapy克隆最新版,只需命令行輸入:

$ git clonehttps://github.com/scrapy/scrapy.git
$ cd scrapy
$ python setup.py install

我猜如果你是這類用戶,就不需要我提醒安裝virtualenv了。

升級Scrapy
Scrapy升級相當(dāng)頻繁。如果你需要升級Scrapy,可以使用pip、easy_install或aptitude:

$ sudo pip install --upgrade Scrapy

$ sudo easy_install --upgrade scrapy

如果你想降級或安裝指定版本的Scrapy,可以:

$ sudo pip install Scrapy==1.0.0

$ sudo easy_install scrapy==1.0.0

Vagrant:本書案例的運(yùn)行方法
本書有的例子比較復(fù)雜,有的例子使用了許多東西。無論你是什么水平,都可以嘗試運(yùn)行所有例子。只需一句命令,就可以用Vagrant搭建操作環(huán)境。

本書使用的系統(tǒng)

在Vagrant中,你的電腦被稱作“主機(jī)”。Vagrant在主機(jī)中創(chuàng)建一個虛擬機(jī)。這樣就可以讓我們忽略主機(jī)的軟硬件,來運(yùn)行案例了。

本書大多數(shù)章節(jié)使用了兩個服務(wù)——開發(fā)機(jī)和網(wǎng)絡(luò)機(jī)。我們在開發(fā)機(jī)中登錄運(yùn)行Scrapy,在網(wǎng)絡(luò)機(jī)中進(jìn)行抓取。后面的章節(jié)會使用更多的服務(wù),包括數(shù)據(jù)庫和大數(shù)據(jù)處理引擎。

根據(jù)附錄A安裝必備,安裝Vagrant,直到安裝好git和Vagrant。打開命令行,輸入以下命令獲取本書的代碼:

$ git clone https://github.com/scalingexcellence/scrapybook.git
$ cd scrapybook

打開Vagrant:

$ vagrant up --no-parallel

第一次打開Vagrant會需要些時間,這取決于你的網(wǎng)絡(luò)。第二次打開就會比較快。打開之后,登錄你的虛擬機(jī),通過:

$ vagrant ssh

代碼已經(jīng)從主機(jī)中復(fù)制到了開發(fā)機(jī),現(xiàn)在可以在book的目錄中看到:

$ cd book
$ ls
$ ch03 ch04 ch05 ch07 ch08 ch09 ch10 ch11 ...

可以打開幾個窗口輸入vagrant ssh,這樣就可以打開幾個終端。輸入vagrant halt可以關(guān)閉系統(tǒng),vagrantstatus可以檢查狀態(tài)。vagrant halt不能關(guān)閉虛擬機(jī)。如果在VirtualBox中碰到問題,可以手動關(guān)閉,或是使用vagrant global-status查找id,用vagrant halt <ID> 暫停。大多數(shù)例子可以離線運(yùn)行,這是Vagrant的一大優(yōu)點(diǎn)。

安裝好環(huán)境之后,就可以開始學(xué)習(xí)Scrapy了。

UR2IM——基礎(chǔ)抓取過程
每個網(wǎng)站都是不同的,對每個網(wǎng)站進(jìn)行額外的研究不可避免,碰到特別生僻的問題,也許還要用Scrapy的郵件列表咨詢。尋求解答,去哪里找、怎么找,前提是要熟悉整個過程和相關(guān)術(shù)語。Scrapy的基本過程,可以寫成字母縮略語UR2IM,見下圖。

The URL
一切都從URL開始。你需要目標(biāo)網(wǎng)站的URL。我的例子是https://www.gumtree.com/,Gumtree分類網(wǎng)站。

例如,訪問倫敦房地產(chǎn)首頁http://www.gumtree.com/flats-houses/london,你就可以找到許多房子的URL。右鍵復(fù)制鏈接地址,就可以復(fù)制URL。其中一個URL可能是這樣的:https://www.gumtree.com/p/studios-bedsits-rent/split-level。但是,Gumtree的網(wǎng)站變動之后,URL的XPath表達(dá)式會失效。不添加用戶頭的話,Gumtree也不會響應(yīng)。這個留給以后再說,現(xiàn)在如果你想加載一個網(wǎng)頁,你可以使用Scrapy終端,如下所示:

scrapy shell -s USER_AGENT="Mozilla/5.0" <your url here  e.g. http://www.gumtree.com/p/studios-bedsits-rent/...>

要進(jìn)行調(diào)試,可以在Scrapy語句后面添加 –pdb,例如:

scrapy shell --pdb https://gumtree.com

我們不想讓大家如此頻繁的點(diǎn)擊Gumtree網(wǎng)站,并且Gumtree網(wǎng)站上URL失效很快,不適合做例子。我們還希望大家能在離線的情況下,多多練習(xí)書中的例子。這就是為什么Vagrant開發(fā)環(huán)境內(nèi)嵌了一個網(wǎng)絡(luò)服務(wù)器,可以生成和Gumtree類似的網(wǎng)頁。這些網(wǎng)頁可能并不好看,但是從爬蟲開發(fā)者的角度,是完全合格的。如果想在Vagrant上訪問Gumtree,可以在Vagrant開發(fā)機(jī)上訪問http://web:9312/,或是在瀏覽器中訪問http://localhost:9312/。

讓我們在這個網(wǎng)頁上嘗試一下Scrapy,在Vagrant開發(fā)機(jī)上輸入:

$ scrapy shell http://web:9312/properties/property_000000.html
...
[s] Available Scrapy objects:
[s]   crawler    <scrapy.crawler.Crawler object at 0x2d4fb10>
[s]   item       {}
[s]   request    <GET http:// web:9312/.../property_000000.html>
[s]   response   <200 http://web:9312/.../property_000000.html>
[s]   settings   <scrapy.settings.Settings object at 0x2d4fa90>
[s]   spider     <DefaultSpider 'default' at 0x3ea0bd0>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local...
[s]   view(response)    View response in a browser
>>>

得到一些輸出,加載頁面之后,就進(jìn)入了Python(可以使用Ctrl+D退出)。

請求和響應(yīng)
在前面的輸出日志中,Scrapy自動為我們做了一些工作。我們輸入了一條地址,Scrapy做了一個GET請求,并得到一個成功響應(yīng)值200。這說明網(wǎng)頁信息已經(jīng)成功加載,并可以使用了。如果要打印reponse.body的前50個字母,我們可以得到:

>>> response.body[:50]
'<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8"'

這就是這個Gumtree網(wǎng)頁的HTML文檔。有時請求和響應(yīng)會很復(fù)雜,第5章會對其進(jìn)行講解,現(xiàn)在只講最簡單的情況。

抓取對象
下一步是從響應(yīng)文件中提取信息,輸入到Item。因?yàn)檫@是個HTML文檔,我們用XPath來做。首先來看一下這個網(wǎng)頁:

頁面上的信息很多,但大多是關(guān)于版面的:logo、搜索框、按鈕等等。從抓取的角度,它們不重要。我們關(guān)注的是,例如,列表的標(biāo)題、地址、電話。它們都對應(yīng)著HTML里的元素,我們要在HTML中定位,用上一章所學(xué)的提取出來。先從標(biāo)題開始。

在標(biāo)題上右鍵點(diǎn)擊,選擇檢查元素。在自動定位的HTML上再次右鍵點(diǎn)擊,選擇復(fù)制XPath。Chrome給的XPath總是很復(fù)雜,并且容易失效。我們要對其進(jìn)行簡化。我們只取最后面的h1。這是因?yàn)閺腟EO的角度,每頁HTML只有一個h1最好,事實(shí)上大多是網(wǎng)頁只有一個h1,所以不用擔(dān)心重復(fù)。

提示:SEO是搜索引擎優(yōu)化的意思:通過對網(wǎng)頁代碼、內(nèi)容、鏈接的優(yōu)化,提升對搜索引擎的支持。

讓我們看看h1標(biāo)簽行不行:

>>> response.xpath('//h1/text()').extract()
[u'set unique family well']

很好,完全行得通。我在h1后面加上了text(),表示只提取h1標(biāo)簽里的文字。沒有添加text()的話,就會這樣:

>>> response.xpath('//h1').extract()
[u'<h1 itemprop="name" class="space-mbs">set unique family well</h1>']

我們已經(jīng)成功得到了title,但是再仔細(xì)看看,還能發(fā)現(xiàn)更簡便的方法。

Gumtree為標(biāo)簽添加了屬性,就是itemprop=name。所以XPath可以簡化為//*[@itemprop="name"][1]/text()。在XPath中,切記數(shù)組是從1開始的,所以這里[]里面是1。

選擇itemprop="name"這個屬性,是因?yàn)镚umtree用這個屬性命名了許多其他的內(nèi)容,比如“You may also like”,用數(shù)組序號提取會很方便。

接下來看價格。價格在HTML中的位置如下:

<strong class="ad-price txt-xlarge txt-emphasis" itemprop="price">£334.39pw</strong>

我們又看到了itemprop="name"這個屬性,XPath表達(dá)式為//*[@itemprop="price"][1]/text()。驗(yàn)證一下:

>>> response.xpath('//*[@itemprop="price"][1]/text()').extract()
[u'\xa3334.39pw']

注意Unicode字符(£符號)和價格350.00pw。這說明要對數(shù)據(jù)進(jìn)行清理。在這個例子中,我們用正則表達(dá)式提取數(shù)字和小數(shù)點(diǎn)。使用正則方法如下:

>>> response.xpath('//*[@itemprop="price"][1]/text()').re('[.0-9]+')
[u'334.39']

提取房屋描述的文字、房屋的地址也很類似,如下:

//*[@itemprop="description"][1]/text()
//*[@itemtype="http://schema.org/Place"][1]/text()

相似的,抓取圖片可以用//img[@itemprop="image"][1]/@src。注意這里沒使用text(),因?yàn)槲覀冎幌胍獔D片的URL。

假如這就是我們要提取的所有信息,整理如下:

目標(biāo) XPath表達(dá)式
title //*[@itemprop="name"][1]/text()
Example value: [u'set unique family well']
Price //*[@itemprop="price"][1]/text()
Example value (using re()):[u'334.39']
description //*[@itemprop="description"][1]/text()
Example value: [u'website court warehouse\r\npool...']
Address //*[@itemtype="http://schema.org/Place"][1]/text()
Example value: [u'Angel, London']
Image_URL //*[@itemprop="image"][1]/@src
Example value: [u'../images/i01.jpg']

這張表很重要,因?yàn)橐苍S只要稍加改變表達(dá)式,就可以抓取其他頁面。另外,如果要爬取數(shù)十個網(wǎng)站時,使用這樣的表可以進(jìn)行區(qū)分。

目前為止,使用的還只是HTML和XPath,接下來用Python來做一個項(xiàng)目。

一個Scrapy項(xiàng)目
目前為止,我們只是在Scrapy shell中進(jìn)行操作。學(xué)過前面的知識,現(xiàn)在開始一個Scrapy項(xiàng)目,Ctrl+D退出Scrapy shell。Scrapy shell只是操作網(wǎng)頁、XPath表達(dá)式和Scrapy對象的工具,不要在上面浪費(fèi)太多,因?yàn)橹灰煌顺?,寫過的代碼就會消失。我們創(chuàng)建一個名字是properties的項(xiàng)目:

$ scrapy startproject properties
$ cd properties
$ tree
.
├── properties
│   ├── __init__.py
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       └── __init__.py
└── scrapy.cfg
2 directories, 6 files

先看看這個Scrapy項(xiàng)目的文件目錄。文件夾內(nèi)包含一個同名的文件夾,里面有三個文件items.py, pipelines.py, 和settings.py。還有一個子文件夾spiders,里面現(xiàn)在是空的。后面的章節(jié)會詳談settings、pipelines和scrapy.cfg文件。

定義items
用編輯器打開items.py。里面已經(jīng)有代碼,我們要對其修改下。用之前的表里的內(nèi)容重新定義class PropertiesItem。

還要添加些后面會用到的內(nèi)容。后面會深入講解。這里要注意的是,聲明一個字段,并不要求一定要填充。所以放心添加你認(rèn)為需要的字段,后面還可以修改。

字段 Python表達(dá)式
images pipeline根據(jù)image_URL會自動填充這里。后面詳解。
Location 地理編碼會填充這里。后面詳解。

我們還會加入一些雜務(wù)字段,也許和現(xiàn)在的項(xiàng)目關(guān)系不大,但是我個人很感興趣,以后或許能用到。你可以選擇添加或不添加。觀察一下這些項(xiàng)目,你就會明白,這些項(xiàng)目是怎么幫助我找到何地(server,url),何時(date),還有(爬蟲)如何進(jìn)行抓取的。它們可以幫助我取消項(xiàng)目,制定新的重復(fù)抓取,或忽略爬蟲的錯誤。這里看不明白不要緊,后面會細(xì)講。

雜務(wù)字段 Python表達(dá)式
url response.url
Example value: ‘http://web.../property_000000.html'
project self.settings.get('BOT_NAME')
Example value: 'properties'
spider self.name
Example value: 'basic'
server server socket.gethostname()
Example value: 'scrapyserver1'
date datetime.datetime.now()
Example value: datetime.datetime(2015, 6, 25...)

利用這個表修改PropertiesItem這個類。修改文件properties/items.py如下:

from scrapy.item import Item, Field

class PropertiesItem(Item):
    # Primary fields
    title = Field()
    price = Field()
    description = Field()
    address = Field()
    image_URL = Field()
    
# Calculated fields
    images = Field()
    location = Field()
   
 # Housekeeping fields
    url = Field()
    project = Field()
    spider = Field()
    server = Field()
    date = Field()

這是我們的第一段代碼,要注意Python中是使用空格縮進(jìn)的。每個字段名之前都有四個空格或是一個tab。如果一行有四個空格,另一行有三個空格,就會報語法錯誤。如果一行是四個空格,另一行是一個tab,也會報錯??崭穹付诉@些項(xiàng)目是在PropertiesItem下面的。其他語言有的用花括號{},有的用begin – end,Python則使用空格。

編寫爬蟲
已經(jīng)完成了一半。現(xiàn)在來寫爬蟲。一般的,每個網(wǎng)站,或一個大網(wǎng)站的一部分,只有一個爬蟲。爬蟲代碼來成UR2IM流程。

當(dāng)然,你可以用文本編輯器一句一句寫爬蟲,但更便捷的方法是用scrapy genspider命令,如下所示:

$ scrapy genspider basic web

使用模塊中的模板“basic”創(chuàng)建了一個爬蟲“basic”:

  properties.spiders.basic

一個爬蟲文件basic.py就出現(xiàn)在目錄properties/spiders中。剛才的命令是,生成一個名字是basic的默認(rèn)文件,它的限制是在web上爬取URL。我們可以取消這個限制。這個爬蟲使用的是basic這個模板。你可以用scrapy genspider –l查看所有的模板,然后用參數(shù)–t利用模板生成想要的爬蟲,后面會介紹一個例子。

查看properties/spiders/basic.py file文件, 它的代碼如下:

import scrapy
class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
start_URL = (
        'http://www.web/',
    )
    def parse(self, response):
        pass

import命令可以讓我們使用Scrapy框架。然后定義了一個類BasicSpider,繼承自scrapy.Spider。繼承的意思是,雖然我們沒寫任何代碼,這個類已經(jīng)繼承了Scrapy框架中的類Spider的許多特性。這允許我們只需寫幾行代碼,就可以有一個功能完整的爬蟲。然后我們看到了一些爬蟲的參數(shù),比如名字和抓取域字段名。最后,我們定義了一個空函數(shù)parse(),它有兩個參數(shù)self和response。通過self,可以使用爬蟲一些有趣的功能。response看起來很熟悉,它就是我們在Scrapy shell中見到的響應(yīng)。

下面來開始編輯這個爬蟲。start_URL更改為在Scrapy命令行中使用過的URL。然后用爬蟲事先準(zhǔn)備的log()方法輸出內(nèi)容。修改后的properties/spiders/basic.py文件為:

import scrapy
class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_URL = (
        'http://web:9312/properties/property_000000.html',
    )
    def parse(self, response):
        self.log("title: %s" % response.xpath(
            '//*[@itemprop="name"][1]/text()').extract())
        self.log("price: %s" % response.xpath(
            '//*[@itemprop="price"][1]/text()').re('[.0-9]+'))
        self.log("description: %s" % response.xpath(
        '//*[@itemprop="description"][1]/text()').extract())
        self.log("address: %s" % response.xpath(
            '//*[@itemtype="http://schema.org/'
            'Place"][1]/text()').extract())
        self.log("image_URL: %s" % response.xpath(
            '//*[@itemprop="image"][1]/@src').extract())

總算到了運(yùn)行爬蟲的時間!讓爬蟲運(yùn)行的命令是scrapy crawl接上爬蟲的名字:

$ scrapy crawl basic
INFO: Scrapy 1.0.3 started (bot: properties)
...
INFO: Spider opened
DEBUG: Crawled (200) <GET http://...000.html>
DEBUG: title: [u'set unique family well']
DEBUG: price: [u'334.39']
DEBUG: description: [u'website...']
DEBUG: address: [u'Angel, London']
DEBUG: image_URL: [u'../images/i01.jpg']
INFO: Closing spider (finished)
...

成功了!不要被這么多行的命令嚇到,后面我們再仔細(xì)說明。現(xiàn)在,我們可以看到使用這個簡單的爬蟲,所有的數(shù)據(jù)都用XPath得到了。

來看另一個命令,scrapy parse。它可以讓我們選擇最合適的爬蟲來解析URL。用—spider命令可以設(shè)定爬蟲:

$ scrapy parse --spider=basic http://web:9312/properties/property_000001.html

你可以看到輸出的結(jié)果和前面的很像,但卻是關(guān)于另一個房產(chǎn)的。

填充一個項(xiàng)目
接下來稍稍修改一下前面的代碼。你會看到,盡管改動很小,卻可以解鎖許多新的功能。

首先,引入類PropertiesItem。它位于properties目錄中的item.py文件,因此在模塊properties.items中。它的導(dǎo)入命令是:

from properties.items import PropertiesItem

然后我們要實(shí)例化,并進(jìn)行返回。這很簡單。在parse()方法中,我們加入聲明item = PropertiesItem(),它產(chǎn)生了一個新項(xiàng)目,然后為它分配表達(dá)式:

item['title'] = response.xpath('//*[@itemprop="name"][1]/text()').extract()

最后,我們用return item返回項(xiàng)目。更新后的properties/spiders/basic.py文件如下:

import scrapy
from properties.items import PropertiesItem
class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_URL = (
        'http://web:9312/properties/property_000000.html',
    )
    def parse(self, response):
        item = PropertiesItem()
        item['title'] = response.xpath(
            '//*[@itemprop="name"][1]/text()').extract()
        item['price'] = response.xpath(
            '//*[@itemprop="price"][1]/text()').re('[.0-9]+')
        item['description'] = response.xpath(
            '//*[@itemprop="description"][1]/text()').extract()
        item['address'] = response.xpath(
            '//*[@itemtype="http://schema.org/'
            'Place"][1]/text()').extract()
        item['image_URL'] = response.xpath(
            '//*[@itemprop="image"][1]/@src').extract()
        return item

現(xiàn)在如果再次運(yùn)行爬蟲,你會注意到一個不大但很重要的改動。被抓取的值不再打印出來,沒有“DEBUG:被抓取的值”了。你會看到:

DEBUG: Scraped from <200  
http://...000.html>
  {'address': [u'Angel, London'],
   'description': [u'website ... offered'],
   'image_URL': [u'../images/i01.jpg'],
   'price': [u'334.39'],
   'title': [u'set unique family well']}

這是從這個頁面抓取的PropertiesItem。這很好,因?yàn)镾crapy就是圍繞Items的概念構(gòu)建的,這意味著我們可以用pipelines填充豐富項(xiàng)目,或是用“Feed export”導(dǎo)出保存到不同的格式和位置。

保存到文件
試運(yùn)行下面:

$ scrapy crawl basic -o items.json
$ cat items.json
[{"price": ["334.39"], "address": ["Angel, London"], "description": 
["website court ... offered"], "image_URL": ["../images/i01.jpg"], 
"title": ["set unique family well"]}]
$ scrapy crawl basic -o items.jl
$ cat items.jl
{"price": ["334.39"], "address": ["Angel, London"], "description": 
["website court ... offered"], "image_URL": ["../images/i01.jpg"], 
"title": ["set unique family well"]}
$ scrapy crawl basic -o items.csv
$ cat items.csv 
description,title,url,price,spider,image_URL...
"...offered",set unique family well,,334.39,,../images/i01.jpg
$ scrapy crawl basic -o items.xml
$ cat items.xml 
<?xml version="1.0" encoding="utf-8"?>
<items><item><price><value>334.39</value></price>...</item></items>

不用我們寫任何代碼,我們就可以用這些格式進(jìn)行存儲。Scrapy可以自動識別輸出文件的后綴名,并進(jìn)行輸出。這段代碼中涵蓋了一些常用的格式。CSV和XML文件很流行,因?yàn)榭梢员籈xcel直接打開。JSON文件很流行是因?yàn)樗拈_放性和與JavaScript的密切關(guān)系。JSON和JSON Line格式的區(qū)別是.json文件是在一個大數(shù)組中存儲JSON對象。這意味著如果你有一個1GB的文件,你可能必須現(xiàn)在內(nèi)存中存儲,然后才能傳給解析器。相對的,.jl文件每行都有一個JSON對象,所以讀取效率更高。

不在文件系統(tǒng)中存儲生成的文件也很麻煩。利用下面例子的代碼,你可以讓Scrapy自動上傳文件到FTP或亞馬遜的S3 bucket。

$ scrapy crawl basic -o "ftp://user:pass@ftp.scrapybook.com/items.json "
$ scrapy crawl basic -o "s3://aws_key:aws_secret@scrapybook/items.json"

注意,證書和URL必須按照主機(jī)和S3更新,才能順利運(yùn)行。

另一個要注意的是,如果你現(xiàn)在使用scrapy parse,它會向你顯示被抓取的項(xiàng)目和抓取中新的請求:

$ scrapy parse --spider=basic http://web:9312/properties/property_000001.html
INFO: Scrapy 1.0.3 started (bot: properties)
...
INFO: Spider closed (finished)
>>> STATUS DEPTH LEVEL 1 <<<
# Scraped Items  ------------------------------------------------
[{'address': [u'Plaistow, London'],
  'description': [u'features'],
  'image_URL': [u'../images/i02.jpg'],
  'price': [u'388.03'],
  'title': [u'belsize marylebone...deal']}]
# Requests  ------------------------------------------------
[]

當(dāng)出現(xiàn)意外結(jié)果時,scrapy parse可以幫你進(jìn)行debug,你會更感嘆它的強(qiáng)大。

清洗——項(xiàng)目加載器和雜務(wù)字段
恭喜你,你已經(jīng)創(chuàng)建成功一個簡單爬蟲了!讓我們讓它看起來更專業(yè)些。

我們使用一個功能類,ItemLoader,以取代看起來雜亂的extract()和xpath()。我們的parse()進(jìn)行如下變化:

def parse(self, response):
    l = ItemLoader(item=PropertiesItem(), response=response)
    l.add_xpath('title', '//*[@itemprop="name"][1]/text()')
    l.add_xpath('price', './/*[@itemprop="price"]'
           '[1]/text()', re='[,.0-9]+')
    l.add_xpath('description', '//*[@itemprop="description"]'
           '[1]/text()')
    l.add_xpath('address', '//*[@itemtype='
           '"http://schema.org/Place"][1]/text()')
    l.add_xpath('image_URL', '//*[@itemprop="image"][1]/@src')
    return l.load_item()

是不是看起來好多了?事實(shí)上,它可不是看起來漂亮那么簡單。它指出了我們現(xiàn)在要干什么,并且后面的加載項(xiàng)很清晰。這提高了代碼的可維護(hù)性和自文檔化。(自文檔化,self-documenting,是說代碼的可讀性高,可以像文檔文件一樣閱讀)

ItemLoaders提供了許多有趣的方式整合數(shù)據(jù)、格式化數(shù)據(jù)、清理數(shù)據(jù)。它的更新很快,查閱文檔可以更好的使用它,http://doc.scrapy.org/en/latest/topics/loaders。通過不同的類處理器,ItemLoaders從XPath/CSS表達(dá)式傳參。處理器函數(shù)快速小巧。舉一個Join()的例子。//p表達(dá)式會選取所有段落,這個處理函數(shù)可以在一個入口中將所有內(nèi)容整合起來。另一個函數(shù)MapCompose(),可以與Python函數(shù)或Python函數(shù)鏈結(jié)合,實(shí)現(xiàn)復(fù)雜的功能。例如,MapCompose(float)可以將字符串轉(zhuǎn)化為數(shù)字,MapCompose(unicode.strip, unicode.title)可以去除多余的空格,并將單詞首字母大寫。讓我們看幾個處理函數(shù)的例子:

處理函數(shù) 功能
Join() 合并多個結(jié)果。
MapCompose(unicode.strip) 除去空格。
MapCompose(unicode.strip, unicode.title) 除去空格,單詞首字母大寫。
MapCompose(float) 將字符串轉(zhuǎn)化為數(shù)字。
MapCompose(lambda i: i.replace(',', ''), float) 將字符串轉(zhuǎn)化為數(shù)字,逗號替換為空格。
MapCompose(lambda i: urlparse.urljoin(response.url, i)) 使用response.url為開頭,將相對URL轉(zhuǎn)化為絕對URL。

你可以使用Python編寫處理函數(shù),或是將它們串聯(lián)起來。unicode.strip()和unicode.title()分別用單一參數(shù)實(shí)現(xiàn)了單一功能。其它函數(shù),如replace()和urljoin()需要多個參數(shù),我們可以使用Lambda函數(shù)。這是一個匿名函數(shù),可以不聲明函數(shù)就調(diào)用參數(shù):

myFunction = lambda i: i.replace(',', '')

可以取代下面的函數(shù):

def myFunction(i):
    return i.replace(',', '')

使用Lambda函數(shù),打包replace()和urljoin(),生成一個結(jié)果,只需一個參數(shù)即可。為了更清楚前面的表,來看幾個實(shí)例。在scrapy命令行打開任何URL,并嘗試:


>>> from scrapy.loader.processors import MapCompose, Join
>>> Join()(['hi','John'])
u'hi John'
>>> MapCompose(unicode.strip)([u'  I',u' am\n'])
[u'I', u'am']
>>> MapCompose(unicode.strip, unicode.title)([u'nIce cODe'])
[u'Nice Code']
>>> MapCompose(float)(['3.14'])
[3.14]
>>> MapCompose(lambda i: i.replace(',', ''), float)(['1,400.23'])
[1400.23]
>>> import urlparse
>>> mc = MapCompose(lambda i: urlparse.urljoin('http://my.com/test/abc', i))
>>> mc(['example.html#check'])
['http://my.com/test/example.html#check']
>>> mc(['http://absolute/url#help'])
['http://absolute/url#help']

要記住,處理函數(shù)是對XPath/CSS結(jié)果進(jìn)行后處理的的小巧函數(shù)。讓我們來看幾個我們爬蟲中的處理函數(shù)是如何清洗結(jié)果的:

def parse(self, response):
    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))
    l.add_xpath('image_URL', '//*[@itemprop="image"][1]/@src',
                MapCompose(
                lambda i: urlparse.urljoin(response.url, i)))

完整的列表在本章后面給出。如果你用scrapy crawl basic再運(yùn)行的話,你可以得到干凈的結(jié)果如下:

'price': [334.39],
'title': [u'Set Unique Family Well']

最后,我們可以用add_value()方法添加用Python(不用XPath/CSS表達(dá)式)計算得到的值。我們用它設(shè)置我們的“雜務(wù)字段”,例如URL、爬蟲名、時間戳等等。我們直接使用前面雜務(wù)字段表里總結(jié)的表達(dá)式,如下:

l.add_value('url', response.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())

記得import datetime和socket,以使用這些功能。

現(xiàn)在,我們的Items看起來就完美了。我知道你的第一感覺是,這可能太復(fù)雜了,值得嗎?回答是肯定的,這是因?yàn)榛蚨嗷蛏?,想抓取網(wǎng)頁信息并存到items里,這就是你要知道的全部。這段代碼如果用其他語言來寫,會非常難看,很快就不能維護(hù)了。用Scrapy,只要25行簡潔的代碼,它明確指明了意圖,你可以看清每行的意義,可以清晰的進(jìn)行修改、再利用和維護(hù)。

你的另一個感覺可能是處理函數(shù)和ItemLoaders太花費(fèi)精力。如果你是一名經(jīng)驗(yàn)豐富的Python開發(fā)者,你已經(jīng)會使用字符串操作、lambda表達(dá)構(gòu)造列表,再學(xué)習(xí)新的知識會覺得不舒服。然而,這只是對ItemLoader和其功能的簡單介紹,如果你再深入學(xué)習(xí)一點(diǎn),你就不會這么想了。ItemLoaders和處理函數(shù)是專為有抓取需求的爬蟲編寫者、維護(hù)者開發(fā)的工具集。如果你想深入學(xué)習(xí)爬蟲的話,它們是絕對值得學(xué)習(xí)的。

創(chuàng)建協(xié)議
協(xié)議有點(diǎn)像爬蟲的單元測試。它們能讓你快速知道錯誤。例如,假設(shè)你幾周以前寫了一個抓取器,它包含幾個爬蟲。你想快速檢測今天是否還是正確的。協(xié)議位于評論中,就在函數(shù)名后面,協(xié)議的開頭是@??聪旅孢@個協(xié)議:

def parse(self, response):
    """ This function parses a property page.
    @url http://web:9312/properties/property_000000.html
    @returns items 1
    @scrapes title price description address image_URL
    @scrapes url project spider server date
    """

這段代碼是說,檢查這個URL,你可以在找到一個項(xiàng)目,它在那些字段有值?,F(xiàn)在如果你運(yùn)行scrapy check,它會檢查協(xié)議是否被滿足:

$ scrapy check basic
----------------------------------------------------------------
Ran 3 contracts in 1.640s
OK
如果url的字段是空的(被注釋掉),你會得到一個描述性錯誤:
FAIL: [basic] parse (@scrapes post-hook)
------------------------------------------------------------------
ContractFail: 'url' field is missing

當(dāng)爬蟲代碼有錯,或是XPath表達(dá)式過期,協(xié)議就可能失效。當(dāng)然,協(xié)議不會特別詳細(xì),但是可以清楚的指出代碼的錯誤所在。

綜上所述,我們的第一個爬蟲如下所示:

from scrapy.loader.processors import MapCompose, Join
from scrapy.loader import ItemLoader
from properties.items import PropertiesItem
import datetime
import urlparse
import socket
import scrapy

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    # Start on a property page
    start_URL = (
        'http://web:9312/properties/property_000000.html',
    )
    def parse(self, response):
        """ This function parses a property page.
        @url http://web:9312/properties/property_000000.html
        @returns items 1
        @scrapes title price description address image_URL
        @scrapes url project spider server date
        """
        # Create the loader using the response
        l = ItemLoader(item=PropertiesItem(), response=response)
        # 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))
        l.add_xpath('image_URL', '//*[@itemprop="image"]'
                    '[1]/@src', MapCompose(
                    lambda i: urlparse.urljoin(response.url, i)))
        # Housekeeping fields
        l.add_value('url', response.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()

提取更多的URL
到目前為止,在爬蟲的start_URL中我們還是只加入了一條URL。因?yàn)檫@是一個元組,我們可以向里面加入多個URL,例如:

start_URL = (
    'http://web:9312/properties/property_000000.html',
    'http://web:9312/properties/property_000001.html',
    'http://web:9312/properties/property_000002.html',
)

不夠好。我們可以用一個文件當(dāng)做URL源文件:

start_URL = [i.strip() for i in  
open('todo.URL.txt').readlines()]

還是不夠好,但行得通。更常見的,網(wǎng)站可能既有索引頁也有列表頁。例如,Gumtree有索引頁:http://www.gumtree.com/flats-houses/london

一個典型的索引頁包含許多列表頁、一個分頁系統(tǒng),讓你可以跳轉(zhuǎn)到其它頁面。

因此,一個典型的爬蟲在兩個方向移動:

  • 水平——從索引頁到另一個索引頁
  • 垂直——從索引頁面到列表頁面提取項(xiàng)目

在本書中,我們稱前者為水平抓取,因?yàn)樗谕粚哟危ɡ缢饕┥献ト№撁?;后者為垂直抓取,因?yàn)樗鼜母邔哟危ɡ缢饕┮苿拥揭粋€較低的層次(例如列表)。

做起來要容易許多。我們只需要兩個XPath表達(dá)式。第一個,我們右鍵點(diǎn)擊Next page按鈕,URL位于li中,li的類名含有next。因此XPath表達(dá)式為//*[contains(@class,"next")]//@href。

對于第二個表達(dá)式,我們在列表的標(biāo)題上右鍵點(diǎn)擊,選擇檢查元素:

這個URL有一個屬性是itemprop="url。因此,表達(dá)式確定為//*[@itemprop="url"]/@href。打開scrapy命令行進(jìn)行確認(rèn):

$ scrapy shell http://web:9312/properties/index_00000.html
>>> URL = response.xpath('//*[contains(@class,"next")]//@href').extract()
>>> URL
[u'index_00001.html']
>>> import urlparse
>>> [urlparse.urljoin(response.url, i) for i in URL]
[u'http://web:9312/scrapybook/properties/index_00001.html']
>>> URL = response.xpath('//*[@itemprop="url"]/@href').extract()
>>> URL
[u'property_000000.html', ... u'property_000029.html']
>>> len(URL)
30
>>> [urlparse.urljoin(response.url, i) for i in URL]
[u'http://..._000000.html', ... /property_000029.html']

很好,我們看到有了這兩個表達(dá)式,就可以進(jìn)行水平和垂直抓取URL了。

使用爬蟲進(jìn)行二維抓取
將前一個爬蟲代碼復(fù)制到新的爬蟲manual.py中:

$ ls
properties  scrapy.cfg
$ cp properties/spiders/basic.py properties/spiders/manual.py

在properties/spiders/manual.py中,我們通過添加from scrapy.http import Request引入Request,將爬蟲的名字改為manual,將start_URL改為索引首頁,將parse()重命名為parse_item()。接下來寫心得parse()方法進(jìn)行水平和垂直的抓?。?/p>

def parse(self, response):
    # Get the next index URL and yield Requests
    next_selector = response.xpath('//*[contains(@class,'
                                   '"next")]//@href')
    for url in next_selector.extract():
        yield Request(urlparse.urljoin(response.url, url))

    # Get item URL and yield Requests
    item_selector = response.xpath('//*[@itemprop="url"]/@href')
    for url in item_selector.extract():
        yield Request(urlparse.urljoin(response.url, url),
                      callback=self.parse_item)

提示:你可能注意到了yield聲明。它和return很像,不同之處是return會退出循環(huán),而yield不會。從功能上講,前面的例子與下面很像

next_requests = []
for url in...
   next_requests.append(Request(...))
for url in...
   next_requests.append(Request(...))
return next_requests

yield可以大大提高Python編程的效率。

做好爬蟲了。但如果讓它運(yùn)行起來的話,它將抓取5萬張頁面。為了避免時間太長,我們可以通過命令-s CLOSESPIDER_ITEMCOUNT=90(更多的設(shè)定見第7章),設(shè)定爬蟲在一定數(shù)量(例如,90)之后停止運(yùn)行。開始運(yùn)行:

$ scrapy crawl manual -s CLOSESPIDER_ITEMCOUNT=90
INFO: Scrapy 1.0.3 started (bot: properties)
...
DEBUG: Crawled (200) <...index_00000.html> (referer: None)
DEBUG: Crawled (200) <...property_000029.html> (referer: ...index_00000.html)
DEBUG: Scraped from <200 ...property_000029.html>
  {'address': [u'Clapham, London'],
   'date': [datetime.datetime(2015, 10, 4, 21, 25, 22, 801098)],
   'description': [u'situated camden facilities corner'],
   'image_URL': [u'http://web:9312/images/i10.jpg'],
   'price': [223.88],
   'project': ['properties'],
  'server': ['scrapyserver1'],
   'spider': ['manual'],
   'title': [u'Portered Mile'],
   'url': ['http://.../property_000029.html']}
DEBUG: Crawled (200) <...property_000028.html> (referer: ...index_00000.
html)
...
DEBUG: Crawled (200) <...index_00001.html> (referer: ...)
DEBUG: Crawled (200) <...property_000059.html> (referer: ...)
...
INFO: Dumping Scrapy stats: ...
   'downloader/request_count': 94, ...
   'item_scraped_count': 90,

查看輸出,你可以看到我們得到了水平和垂直兩個方向的結(jié)果。首先讀取了index_00000.html, 然后產(chǎn)生了許多請求。執(zhí)行請求的過程中,debug信息指明了誰用URL發(fā)起了請求。例如,我們看到,property_000029.html, property_000028.html ... 和 index_00001.html都有相同的referer(即index_00000.html)。然后,property_000059.html和其它網(wǎng)頁的referer是index_00001,過程以此類推。

這個例子中,Scrapy處理請求的機(jī)制是后進(jìn)先出(LIFO),深度優(yōu)先抓取。最后提交的請求先被執(zhí)行。這個機(jī)制適用于大多數(shù)情況。例如,我們想先抓取完列表頁再取下一個索引頁。不然的話,我們必須消耗內(nèi)存存儲列表頁的URL。另外,許多時候你想用一個輔助的Requests執(zhí)行一個請求,下一章有例子。你需要Requests越早完成越好,以便爬蟲繼續(xù)下面的工作。

我們可以通過設(shè)定Request()參數(shù)修改默認(rèn)的順序,大于0時是高于默認(rèn)的優(yōu)先級,小于0時是低于默認(rèn)的優(yōu)先級。通常,Scrapy會先執(zhí)行高優(yōu)先級的請求,但不會花費(fèi)太多時間思考到底先執(zhí)行哪一個具體的請求。在你的大多數(shù)爬蟲中,你不會有超過一個或兩個的請求等級。因?yàn)閁RL會被多重過濾,如果我們想向一個URL多次請求,我們可以設(shè)定參數(shù)dont_filter Request()為True。

用CrawlSpider二維抓取
如果你覺得這個二維抓取單調(diào)的話,說明你入門了。Scrapy試圖簡化這些瑣事,讓編程更容易。完成之前結(jié)果的更好方法是使用CrawlSpider,一個簡化抓取的類。我們用genspider命令,設(shè)定一個-t參數(shù),用爬蟲模板創(chuàng)建一個爬蟲:

$ scrapy genspider -t crawl easy web
Created spider 'crawl' using template 'crawl' in module:
  properties.spiders.easy

現(xiàn)在properties/spiders/easy.py文件包含如下所示:

...
class EasySpider(CrawlSpider):
    name = 'easy'
    allowed_domains = ['web']
    start_URL = ['http://www.web/']
    rules = (
        Rule(LinkExtractor(allow=r'Items/'),  
callback='parse_item', follow=True),
    )
    def parse_item(self, response):
        ...

這段自動生成的代碼和之前的很像,但是在類的定義中,這個爬蟲從CrawlSpider定義的,而不是Spider。CrawlSpider提供了一個包含變量rules的parse()方法,以完成之前我們手寫的內(nèi)容。

現(xiàn)在將start_URL設(shè)定為索引首頁,并將parse_item()方法替換。這次不再使用parse()方法,而是將rules變成兩個rules,一個負(fù)責(zé)水平抓取,一個負(fù)責(zé)垂直抓?。?/p>

rules = (
Rule(LinkExtractor(restrict_xpaths='//*[contains(@class,"next")]')),
Rule(LinkExtractor(restrict_xpaths='//*[@itemprop="url"]'),
         callback='parse_item')
)

兩個XPath表達(dá)式與之前相同,但沒有了a與href的限制。正如它們的名字,LinkExtractor專門抽取鏈接,默認(rèn)就是尋找a、href屬性。你可以設(shè)定tags和attrs自定義LinkExtractor()。對比前面的請求方法Requests(self.parse_item),回調(diào)的字符串中含有回調(diào)方法的名字(例如,parse_item)。最后,除非設(shè)定callback,一個Rule就會沿著抽取的URL掃描外鏈。設(shè)定callback之后,Rule才能返回。如果你想讓Rule跟隨外鏈,你應(yīng)該從callback方法return/yield,或設(shè)定Rule()的follow參數(shù)為True。當(dāng)你的列表頁既有Items又有其它有用的導(dǎo)航鏈接時非常有用。

你現(xiàn)在可以運(yùn)行這個爬蟲,它的結(jié)果與之前相同,但簡潔多了:

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90

總結(jié)
對所有學(xué)習(xí)Scrapy的人,本章也許是最重要的。你學(xué)習(xí)了爬蟲的基本流程UR2IM、如何自定義Items、使用ItemLoaders,XPath表達(dá)式、利用處理函數(shù)加載Items、如何yield請求。我們使用Requests水平抓取多個索引頁、垂直抓取列表頁。最后,我們學(xué)習(xí)了如何使用CrawlSpider和Rules簡化代碼。多度幾遍本章以加深理解、創(chuàng)建自己的爬蟲。

我們剛剛從一個網(wǎng)站提取了信息。它的重要性在哪呢?答案在下一章,我們只用幾頁就能制作一個移動app,并用Scrapy填充數(shù)據(jù)。


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


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

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

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