本文希望達到以下目標:
- 簡要介紹Scarpy
- 閱讀官網入門文件並實現文件中的範例
- 使用Scarpy優豆瓣爬蟲的抓取
- 制定下一步學習目標
初學Scrapy, 如有翻譯不當, 或者程式碼錯誤, 請指出, 非常感謝
1. Scrapy簡介
Scrapy是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。 可以應用在包括資料探勘,資訊處理或儲存歷史資料等一系列的程式中。
其最初是為了頁面抓取 (更確切來說, 網路抓取 )所設計的, 也可以應用在獲取API所返回的資料(例如 Amazon Associates Web Services ) 或者通用的網路爬蟲。Scrapy用途廣泛,可以用於資料探勘、監測和自動化測試
Scrapy 使用了 Twisted非同步網路庫來處理網路通訊。整體架構大致如下
Scrapy主要包括了以下元件:
- 引擎(Scrapy): 用來處理整個系統的資料流處理, 觸發事務(框架核心)
- 排程器(Scheduler): 用來接受引擎發過來的請求, 壓入佇列中, 並在引擎再次請求的時候返回. 可以想像成一個URL(抓取網頁的網址或者說是連結)的優先佇列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址
- 下載器(Downloader): 用於下載網頁內容, 並將網頁內容返回給蜘蛛(Scrapy下載器是建立在twisted這個高效的非同步模型上的)
- 爬蟲(Spiders): 爬蟲是主要幹活的, 用於從特定的網頁中提取自己需要的資訊, 即所謂的實體(Item)。使用者也可以從中提取出連結,讓Scrapy繼續抓取下一個頁面
- 專案管道(Pipeline): 負責處理爬蟲從網頁中抽取的實體,主要的功能是持久化實體、驗證實體的有效性、清除不需要的資訊。當頁面被爬蟲解析後,將被髮送到專案管道,並經過幾個特定的次序處理資料。
- 下載器中介軟體(Downloader Middlewares): 位於Scrapy引擎和下載器之間的框架,主要是處理Scrapy引擎與下載器之間的請求及響應。
- 爬蟲中介軟體(Spider Middlewares): 介於Scrapy引擎和爬蟲之間的框架,主要工作是處理蜘蛛的響應輸入和請求輸出。
- 排程中介軟體(Scheduler Middewares): 介於Scrapy引擎和排程之間的中介軟體,從Scrapy引擎傳送到排程的請求和響應。
Scrapy執行流程大概如下:
- 首先,引擎從排程器中取出一個連結(URL)用於接下來的抓取
- 引擎把URL封裝成一個請求(Request)傳給下載器,下載器把資源下載下來,並封裝成應答包(Response)
- 然後,爬蟲解析Response
- 若是解析出實體(Item),則交給實體管道進行進一步的處理。
- 若是解析出的是連結(URL),則把URL交給Scheduler等待抓取
2. 安裝Scrapy
使用以下命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
sudo pip install virtualenv #安裝虛擬環境工具 virtualenv ENV #建立一個虛擬環境目錄 source ./ENV/bin/active #啟用虛擬環境 pip install Scrapy #驗證是否安裝成功 pip list #輸出如下 cffi (0.8.6) cryptography (0.6.1) cssselect (0.9.1) lxml (3.4.1) pip (1.5.6) pycparser (2.10) pyOpenSSL (0.14) queuelib (1.2.2) Scrapy (0.24.4) setuptools (3.6) six (1.8.0) Twisted (14.0.2) w3lib (1.10.0) wsgiref (0.1.2) zope.interface (4.1.1) |
3. Scrapy Tutorial
在抓取之前, 你需要新建一個Scrapy工程. 進入一個你想用來儲存程式碼的目錄,然後執行:
1 |
$ scrapy startproject tutorial |
這個命令會在當前目錄下建立一個新目錄 tutorial, 它的結構如下:
1 2 3 4 5 6 7 8 9 |
. ├── scrapy.cfg └── tutorial ├── __init__.py ├── items.py ├── pipelines.py ├── settings.py └── spiders └── __init__.py |
這些檔案主要是:
- scrapy.cfg: 專案配置檔案
- tutorial/: 專案python模組, 之後您將在此加入程式碼
- tutorial/items.py: 專案items檔案
- tutorial/pipelines.py: 專案管道檔案
- tutorial/settings.py: 專案配置檔案
- tutorial/spiders: 放置spider的目錄
3.1. 定義Item
Items是將要裝載抓取的資料的容器,它工作方式像 python 裡面的字典,但它提供更多的保護,比如對未定義的欄位填充以防止拼寫錯誤。
通過建立scrapy.Item類, 並且定義型別為 scrapy.Field 的類屬性來宣告一個Item.
我們通過將需要的item模型化,來控制從 dmoz.org 獲得的站點資料,比如我們要獲得站點的名字,url 和網站描述,我們定義這三種屬性的域。在 tutorial 目錄下的 items.py 檔案編輯
1 2 3 4 5 6 7 8 |
from scrapy.item import Item, Field class DmozItem(Item): # define the fields for your item here like: name = Field() description = Field() url = Field() |
3.2. 編寫Spider
Spider 是使用者編寫的類, 用於從一個域(或域組)中抓取資訊, 定義了用於下載的URL的初步列表, 如何跟蹤連結,以及如何來解析這些網頁的內容用於提取items。
要建立一個 Spider,繼承 scrapy.Spider 基類,並確定三個主要的、強制的屬性:
- name:爬蟲的識別名,它必須是唯一的,在不同的爬蟲中你必須定義不同的名字.
- start_urls:包含了Spider在啟動時進行爬取的url列表。因此,第一個被獲取到的頁面將是其中之一。後續的URL則從初始的URL獲取到的資料中提取。我們可以利用正規表示式定義和過濾需要進行跟進的連結。
- parse():是spider的一個方法。被呼叫時,每個初始URL完成下載後生成的 Response 物件將會作為唯一的引數傳遞給該函式。該方法負責解析返回的資料(response data),提取資料(生成item)以及生成需要進一步處理的URL的 Request 物件。
這個方法負責解析返回的資料、匹配抓取的資料(解析為 item )並跟蹤更多的 URL。
在 /tutorial/tutorial/spiders 目錄下建立 dmoz_spider.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import scrapy class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/" ] def parse(self, response): filename = response.url.split("/")[-2] with open(filename, 'wb') as f: f.write(response.body) |
3.3. 爬取
當前專案結構
1 2 3 4 5 6 7 8 9 |
├── scrapy.cfg └── tutorial ├── __init__.py ├── items.py ├── pipelines.py ├── settings.py └── spiders ├── __init__.py └── dmoz_spider.py |
到專案根目錄, 然後執行命令:
1 |
$ scrapy crawl dmoz |
執行結果:
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 |
2014-12-15 09:30:59+0800 [scrapy] INFO: Scrapy 0.24.4 started (bot: tutorial) 2014-12-15 09:30:59+0800 [scrapy] INFO: Optional features available: ssl, http11 2014-12-15 09:30:59+0800 [scrapy] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'tutorial.spiders', 'SPIDER_MODULES': ['tutorial.spiders'], 'BOT_NAME': 'tutorial'} 2014-12-15 09:30:59+0800 [scrapy] INFO: Enabled extensions: LogStats, TelnetConsole, CloseSpider, WebService, CoreStats, SpiderState 2014-12-15 09:30:59+0800 [scrapy] INFO: Enabled downloader middlewares: HttpAuthMiddleware, DownloadTimeoutMiddleware, UserAgentMiddleware, RetryMiddleware, DefaultHeadersMiddleware, MetaRefreshMiddleware, HttpCompressionMiddleware, RedirectMiddleware, CookiesMiddleware, ChunkedTransferMiddleware, DownloaderStats 2014-12-15 09:30:59+0800 [scrapy] INFO: Enabled spider middlewares: HttpErrorMiddleware, OffsiteMiddleware, RefererMiddleware, UrlLengthMiddleware, DepthMiddleware 2014-12-15 09:30:59+0800 [scrapy] INFO: Enabled item pipelines: 2014-12-15 09:30:59+0800 [dmoz] INFO: Spider opened 2014-12-15 09:30:59+0800 [dmoz] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) 2014-12-15 09:30:59+0800 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023 2014-12-15 09:30:59+0800 [scrapy] DEBUG: Web service listening on 127.0.0.1:6080 2014-12-15 09:31:00+0800 [dmoz] DEBUG: Crawled (200) <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/> (referer: None) 2014-12-15 09:31:00+0800 [dmoz] DEBUG: Crawled (200) <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/> (referer: None) 2014-12-15 09:31:00+0800 [dmoz] INFO: Closing spider (finished) 2014-12-15 09:31:00+0800 [dmoz] INFO: Dumping Scrapy stats: {'downloader/request_bytes': 516, 'downloader/request_count': 2, 'downloader/request_method_count/GET': 2, 'downloader/response_bytes': 16338, 'downloader/response_count': 2, 'downloader/response_status_count/200': 2, 'finish_reason': 'finished', 'finish_time': datetime.datetime(2014, 12, 15, 1, 31, 0, 666214), 'log_count/DEBUG': 4, 'log_count/INFO': 7, 'response_received_count': 2, 'scheduler/dequeued': 2, 'scheduler/dequeued/memory': 2, 'scheduler/enqueued': 2, 'scheduler/enqueued/memory': 2, 'start_time': datetime.datetime(2014, 12, 15, 1, 30, 59, 533207)} 2014-12-15 09:31:00+0800 [dmoz] INFO: Spider closed (finished) |
3.4. 提取Items
3.4.1. 介紹Selector
從網頁中提取資料有很多方法。Scrapy使用了一種基於 XPath 或者 CSS 表示式機制: Scrapy Selectors
出XPath表示式的例子及對應的含義:
- /html/head/title: 選擇HTML文件中 標籤內的 元素
- /html/head/title/text(): 選擇 元素內的文字
- //td: 選擇所有的 元素
- //div[@class=”mine”]: 選擇所有具有class=”mine” 屬性的 div 元素等
多強大的功能使用可以檢視XPath tutorial
為了方便使用 XPaths,Scrapy 提供 Selector 類, 有四種方法 :
- xpath():返回selectors列表, 每一個selector表示一個xpath參數列達式選擇的節點.
- css() : 返回selectors列表, 每一個selector表示CSS參數列達式選擇的節點
- extract():返回一個unicode字串,該字串為XPath選擇器返回的資料
- re(): 返回unicode字串列表,字串作為引數由正規表示式提取出來
3.4.2. 取出資料
首先使用谷歌瀏覽器開發者工具, 檢視網站原始碼, 來看自己需要取出的資料形式(這種方法比較麻煩), 更簡單的方法是直接對感興趣的東西右鍵審查元素, 可以直接檢視網站原始碼
在檢視網站原始碼後, 網站資訊在第二個內
1 2 3 4 5 6 7 8 |
<ul class="directory-url" style="margin-left:0;"> <li><a href="http://www.pearsonhighered.com/educator/academic/product/0,,0130260363,00%2Ben-USS_01DBC.html" class="listinglink">Core Python Programming</a> - By Wesley J. Chun; Prentice Hall PTR, 2001, ISBN 0130260363. For experienced developers to improve extant skills; professional level examples. Starts by introducing syntax, objects, error handling, functions, classes, built-ins. [Prentice Hall] <div class="flag"><a href="/public/flag?cat=Computers%2FProgramming%2FLanguages%2FPython%2FBooks&url=http%3A%2F%2Fwww.pearsonhighered.com%2Feducator%2Facademic%2Fproduct%2F0%2C%2C0130260363%2C00%252Ben-USS_01DBC.html"><img src="/img/flag.png" alt="[!]" title="report an issue with this listing"></a></div> </li> ...省略部分... </ul> |
那麼就可以通過一下方式進行提取資料
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#通過如下命令選擇每個在網站中的 <li> 元素: sel.xpath('//ul/li') #網站描述: sel.xpath('//ul/li/text()').extract() #網站標題: sel.xpath('//ul/li/a/text()').extract() #網站連結: sel.xpath('//ul/li/a/@href').extract() #如前所述,每個 xpath() 呼叫返回一個 selectors 列表,所以我們可以結合 xpath() 去挖掘更深的節點。我們將會用到這些特性,所以: for sel in response.xpath('//ul/li') title = sel.xpath('a/text()').extract() link = sel.xpath('a/@href').extract() desc = sel.xpath('text()').extract() print title, link, desc |
在已有的爬蟲檔案中修改程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import scrapy class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/" ] def parse(self, response): for sel in response.xpath('//ul/li'): title = sel.xpath('a/text()').extract() link = sel.xpath('a/@href').extract() desc = sel.xpath('text()').extract() print title, link, desc |
3.4.3. 使用item
Item物件是自定義的python字典,可以使用標準的字典語法來獲取到其每個欄位的值(欄位即是我們之前用Field賦值的屬性)
1 2 3 4 |
>>> item = DmozItem() >>> item['title'] = 'Example title' >>> item['title'] 'Example title' |
一般來說,Spider將會將爬取到的資料以 Item 物件返回, 最後修改爬蟲類,使用 Item 來儲存資料,程式碼如下
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 |
from scrapy.spider import Spider from scrapy.selector import Selector from tutorial.items import DmozItem class DmozSpider(Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/", ] def parse(self, response): sel = Selector(response) sites = sel.xpath('//ul[@class="directory-url"]/li') items = [] for site in sites: item = DmozItem() item['name'] = site.xpath('a/text()').extract() item['url'] = site.xpath('a/@href').extract() item['description'] = site.xpath('text()').re('-\s[^\n]*\\r') items.append(item) return items |
3.5. 使用Item Pipeline
當Item在Spider中被收集之後,它將會被傳遞到Item Pipeline,一些元件會按照一定的順序執行對Item的處理。
每個item pipeline元件(有時稱之為ItemPipeline)是實現了簡單方法的Python類。他們接收到Item並通過它執行一些行為,同時也決定此Item是否繼續通過pipeline,或是被丟棄而不再進行處理。
以下是item pipeline的一些典型應用:
- 清理HTML資料
- 驗證爬取的資料(檢查item包含某些欄位)
- 查重(並丟棄)
- 將爬取結果儲存,如儲存到資料庫、XML、JSON等檔案中
編寫你自己的item pipeline很簡單,每個item pipeline元件是一個獨立的Python類,同時必須實現以下方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
process_item(item, spider) #每個item pipeline元件都需要呼叫該方法,這個方法必須返回一個 Item (或任何繼承類)物件,或是丟擲 DropItem異常,被丟棄的item將不會被之後的pipeline元件所處理。 #引數: item: 由 parse 方法返回的 Item 物件(Item物件) spider: 抓取到這個 Item 物件對應的爬蟲物件(Spider物件) open_spider(spider) #當spider被開啟時,這個方法被呼叫。 #引數: spider : (Spider object) – 被開啟的spider close_spider(spider) #當spider被關閉時,這個方法被呼叫,可以再爬蟲關閉後進行相應的資料處理。 #引數: spider : (Spider object) – 被關閉的spider |
為JSON檔案編寫一個items
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from scrapy.exceptions import DropItem class TutorialPipeline(object): # put all words in lowercase words_to_filter = ['politics', 'religion'] def process_item(self, item, spider): for word in self.words_to_filter: if word in unicode(item['description']).lower(): raise DropItem("Contains forbidden word: %s" % word) else: return item |
在 settings.py 中設定ITEM_PIPELINES啟用item pipeline,其預設為[]
1 |
ITEM_PIPELINES = {'tutorial.pipelines.FilterWordsPipeline': 1} |
3.6. 儲存資料
使用下面的命令儲存為json檔案格式
1 |
scrapy crawl dmoz -o items.json |
4. Scarpy優化豆瓣爬蟲的抓取
主要針對之間寫過的豆瓣爬蟲進行重構:
豆瓣有反爬蟲機制, 只成功了一次後, 就被baned後顯示403了, 下面說一下爬蟲結構
4.1. Item
1 2 3 4 5 6 7 8 9 |
from scrapy.item import Item, Field class DoubanItem(Item): # define the fields for your item here like: # name = scrapy.Field() name = Field() #電影名稱 description = Field() #電影描述 url = Field() #抓取的url |
4.2. Spider主程式
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 37 |
#!/usr/bin/env python # -*- coding:utf-8 -*- """ 一個簡單的Python 爬蟲, 用於抓取豆瓣電影Top前250的電影的名稱描述等 Anthor: Andrew Liu Version: 0.0.3 Date: 2014-12-17 Language: Python2.7.8 Editor: Sublime Text2 Operate: 具體操作請看README.md介紹 """ from scrapy.contrib.spiders import CrawlSpider, Rule from scrapy.selector import Selector from douban.items import DoubanItem from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor class DoubanSpider(CrawlSpider) : name = "douban" allowed_domains = ["movie.douban.com"] start_urls = ["http://movie.douban.com/top250"] rules = ( #將所有符合正規表示式的url加入到抓取列表中 Rule(SgmlLinkExtractor(allow = (r'http://movie\.douban\.com/top250\?start=\d+&filter=&type=',))), #將所有符合正規表示式的url請求後下載網頁程式碼, 形成response後呼叫自定義回撥函式 Rule(SgmlLinkExtractor(allow = (r'http://movie\.douban\.com/subject/\d+', )), callback = 'parse_page', follow = True), ) def parse_page(self, response) : sel = Selector(response) item = DoubanItem() item['name'] = sel.xpath('//h1/span[@property="v:itemreviewed"]/text()').extract() item['description'] = sel.xpath('//div/span[@property="v:summary"]/text()').extract() item['url'] = response.url return item |
4.3. 未來要解決的問題
- 頭部偽裝
- 表單提交
- 編碼轉換
豆瓣抓了一會兒, 還沒等我興奮就被禁掉了
1 2 3 4 5 |
... 2014-12-17 22:02:17+0800 [douban] DEBUG: Crawled (403) <GET http://www.douban.com/misc/sorry?original-url=http%3A%2F%2Fmovie.douban.com%2Fsubject%2F2209573%2F%3Ffrom%3Dsubject-page> (referer: http://movie.douban.com/subject/1849031/) 2014-12-17 22:02:17+0800 [douban] DEBUG: Ignoring response <403 http://www.douban.com/misc/sorry?original-url=http%3A%2F%2Fmovie.douban.com%2Fsubject%2F2209573%2F%3Ffrom%3Dsubject-page>: HTTP status code is not handled or not allowed 2014-12-17 22:02:17+0800 [douban] DEBUG: Crawled (403) <GET http://www.douban.com/misc/sorry?original-url=http%3A%2F%2Fmovie.douban.com%2Fsubject%2F1849031%2Fcomments%3Fsort%3Dtime> (referer: http://movie.douban.com/subject/1849031/) ... |