使用Scrapy建立一個網站抓取器
使用Scrapy建立一個網站抓取器
Scrapy是一個用于爬行網站以及在數據挖掘、信息處理和歷史檔案等大量應用范圍內抽取結構化數據的應用程序框架,廣泛用于工業。
在本文中我們將建立一個從Hacker News爬取數據的爬蟲,并將數據按我們的要求存儲在數據庫中。
安裝
我們將需要Scrapy以及 BeautifulSoup用于屏幕抓取,SQLAlchemy用于存儲數據.
如果你使用ubuntu已經其他發行版的unix可以通過pip命令安裝Scrapy。
pip install Scrapy
如果你使用Windows,你需要手工安裝scrapy的一些依賴。
Windows用戶需要pywin32、pyOpenSSL、Twisted、lxml和zope.interface。你可以下載這些包的編譯版本來完成簡易安裝。
可以參照官方文檔查看詳情指導。
都安裝好后,通過在python命令行下輸入下面的命令驗證你的安裝:
>> import scrapy >>
如果沒有返回內容,那么你的安裝已就緒。
安裝HNScrapy
為了創建一個新項目,在終端里輸入以下命令
$ scrapy startproject hn
這將會創建一系列的文件幫助你更容易的開始,cd 到 hn 目錄然后打開你最喜歡的文本編輯器。
在items.py文件里,scrapy需要我們定義一個容器用于放置爬蟲抓取的數據。如果你原來用過Django tutorial,你會發現items.py與Django中的models.py類似。
你將會發現class HnItem已經存在了,它繼承自Item--一個scrapy已經為我們準備好的預定義的對象。
讓我們添加一些我們真正想抓取的條目。我們給它們賦值為Field()是因為這樣我們才能把元數據(metadata)指定給scrapy。
from scrapy.item import Item, Field class HnItem(Item): title = Field() link = Field()
沒什么難的--恩,就是這樣。在scrapy里,沒有別的filed類型,這點和Django不同。所以,我們和Field()杠上了。
scrapy的 Item類的行為類似于Python里面的dictionary,你能從中獲取key和value。
開始寫爬蟲
在spiders文件夾下創建一個hn_spider.py文件。這是奇跡發生的地方--這正是我們告訴scrapy如何找到我們尋找的確切數據的地方。正如你所想的那樣,一個爬蟲只針對一個特定網頁。它可能不會在其他網站上工作。
在ht_spider.py里,我們將定義一個類,HnSpider以及一些通用屬性,例如name和urls。
首先,我們先建立HnSpider類以及一些屬性(在類內部定義的變量,也被稱為field)。我們將從scrapy的BaseSpider繼承:
from scrapy.spider import BaseSpider
from scrapy.selector import Selector
class HnSpider(BaseSpider):
name = 'hn'
allowed_domains = []
start_urls = ['http://news.ycombinator.com']
def parse(self, response):
sel = Selector(response)
sites = sel.xpath('//td[@class="title"]')
for site in sites:
title = site.xpath('a/text()').extract()
link = site.xpath('a/@href').extract()
print title, link 前面的幾個變量是自解釋的:name定義了爬蟲的名字,allowed_domains列出了 供爬蟲爬行的允許域名(allowed domain)的base-URL,start_urls 列出了爬蟲從這里開始爬行的URL。后續的URL將從爬蟲從start_urls下載的數據的URL開始。
接著,scrapy使用XPath選擇器從網站獲取數據--通過一個給定的XPath從HTML數據的特定部分進行選擇。正如它們的文檔所說,"XPath 是一種用于從XML選擇節點的語言,它也可以被用于HTML"。你也可以閱讀它們的文檔了解更多關于XPath選擇器的信息。
注意 在抓取你自己的站點并嘗試計算 XPath 時, Chrome的 開發工具 提供了檢查html元素的能力, 可以讓你拷貝出任何你想要的元素的xpath. 它也提供了檢測xpath的能力,只需要在javascript控制臺中使用 $x, 例如 $x("http://img"). 而在這個教程就不多深究這個了, Firefox 有一個插件, FirePath 同樣也可以編輯,檢查和生成XPath.
我們一般會基于一個定義好的Xpath來告訴 scrapy 到哪里去開始尋找數據. 讓我們瀏覽我們的 Hacker News 站點,并右擊選擇”查看源代碼“:
你會看到那個 sel.xpath('//td[@class="title"]') 有點貌似我們見過的HTML的代碼. 從它們的 文檔中你可以解讀出構造XPath 并使用相對 XPath 的方法. 但本質上, '//td[@class="title"]' 是在說: 所有的 <td> 元素中, 如果一個 <a class="title"></a> 被展現了出來,那就到 <td> 元素里面去尋找那個擁有一個被稱作title的類型的<a>元素.
parse()方法使用了一個參數: response. 嘿,等一下 – 這個 self 是干什么的 – 看起來像是有兩個參數!
每一個實體方法(在這種情況下, parse() 是一個實體方法 ) 接受一個對它自身的引用作為其第一個參數. 為了方便就叫做“self”.
response 參數是抓取器在像Hacker News發起一次請求之后所要返回的東西. 我們會用我們的XPaths轉換那個響應.
現在我們將使用 BeautifulSoup 來進行轉換. Beautiful Soup 將會轉換任何你給它的東西 .
下載 BeautifulSoup 并在抓取器目錄里面創建 soup.py 文件,將代碼復制到其中.
在你的hn_spider.py文件里面引入beautifulSoup 和來自 items.py的 Hnitem,并且像下面這樣修改轉換方法.
from soup import BeautifulSoup as bs
from scrapy.http import Request
from scrapy.spider import BaseSpider
from hn.items import HnItem
class HnSpider(BaseSpider):
name = 'hn'
allowed_domains = []
start_urls = ['http://news.ycombinator.com']
def parse(self, response):
if 'news.ycombinator.com' in response.url:
soup = bs(response.body)
items = [(x[0].text, x[0].get('href')) for x in
filter(None, [
x.findChildren() for x in
soup.findAll('td', {'class': 'title'})
])]
for item in items:
print item
hn_item = HnItem()
hn_item['title'] = item[0]
hn_item['link'] = item[1]
try:
yield Request(item[1], callback=self.parse)
except ValueError:
yield Request('http://news.ycombinator.com/' + item[1], callback=self.parse)
yield hn_item
我們正在迭代這個items,并且給標題和鏈接賦上抓取來的數據.
現在就試試對Hacker News域名進行抓取,你會看到連接和標題被打印在你的控制臺上.
scrapy crawl hn
2013-12-12 16:57:06+0530 [scrapy] INFO: Scrapy 0.20.2 started (bot: hn)
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Optional features available: ssl, http11, django
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Overridden settings: {'NEWSPIDER_MODULE': 'hn.spiders', 'SPIDER_MODULES': ['hn.spiders'], 'BOT_NAME': 'hn'}
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Enabled extensions: LogStats, TelnetConsole, CloseSpider, WebService, CoreStats, SpiderState
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Enabled downloader middlewares: HttpAuthMiddleware, DownloadTimeoutMiddleware, UserAgentMiddleware, RetryMiddleware, DefaultHeadersMiddleware
, MetaRefreshMiddleware, HttpCompressionMiddleware, RedirectMiddleware, CookiesMiddleware, ChunkedTransferMiddleware, DownloaderStats
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Enabled spider middlewares: HttpErrorMiddleware, OffsiteMiddleware, RefererMiddleware, UrlLengthMiddleware, DepthMiddleware
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Enabled item pipelines:
2013-12-12 16:57:06+0530 [hn] INFO: Spider opened
2013-12-12 16:57:06+0530 [hn] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Telnet console listening on 0.0.0.0:6023
2013-12-12 16:57:06+0530 [scrapy] DEBUG: Web service listening on 0.0.0.0:6080
2013-12-12 16:57:07+0530 [hn] DEBUG: Redirecting (301) to <GET https://news.ycombinator.com/> from <GET http://news.ycombinator.com>
2013-12-12 16:57:08+0530 [hn] DEBUG: Crawled (200) <GET https://news.ycombinator.com/> (referer: None)
(u'Caltech Announces Open Access Policy | Caltech', u'http://www.caltech.edu/content/caltech-announces-open-access-policy')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://www.caltech.edu/content/caltech-announces-open-access-policy',
'title': u'Caltech Announces Open Access Policy | Caltech'}
(u'Coinbase Raises $25 Million From Andreessen Horowitz', u'http://blog.coinbase.com/post/69775463031/coinbase-raises-25-million-from-andreessen-horowitz')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://blog.coinbase.com/post/69775463031/coinbase-raises-25-million-from-andreessen-horowitz',
'title': u'Coinbase Raises $25 Million From Andreessen Horowitz'}
(u'Backpacker stripped of tech gear at Auckland Airport', u'http://www.nzherald.co.nz/nz/news/article.cfm?c_id=1&objectid=11171475')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://www.nzherald.co.nz/nz/news/article.cfm?c_id=1&objectid=11171475',
'title': u'Backpacker stripped of tech gear at Auckland Airport'}
(u'How I introduced a 27-year-old computer to the web', u'http://www.keacher.com/1216/how-i-introduced-a-27-year-old-computer-to-the-web/')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://www.keacher.com/1216/how-i-introduced-a-27-year-old-computer-to-the-web/',
'title': u'How I introduced a 27-year-old computer to the web'}
(u'Show HN: Bitcoin Pulse - Tracking Bitcoin Adoption', u'http://www.bitcoinpulse.com')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://www.bitcoinpulse.com',
'title': u'Show HN: Bitcoin Pulse - Tracking Bitcoin Adoption'}
(u'Why was this secret?', u'http://sivers.org/ws')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://sivers.org/ws', 'title': u'Why was this secret?'}
(u'PostgreSQL Exercises', u'http://pgexercises.com/')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://pgexercises.com/', 'title': u'PostgreSQL Exercises'}
(u'What it feels like being an ipad on a stick on wheels', u'http://labs.spotify.com/2013/12/12/what-it-feels-like-being-an-ipad-on-a-stick-on-wheels/')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://labs.spotify.com/2013/12/12/what-it-feels-like-being-an-ipad-on-a-stick-on-wheels/',
'title': u'What it feels like being an ipad on a stick on wheels'}
(u'Prototype ergonomic mechanical keyboards', u'http://blog.fsck.com/2013/12/better-and-better-keyboards.html')
2013-12-12 16:57:08+0530 [hn] DEBUG: Scraped from <200 https://news.ycombinator.com/>
{'link': u'http://blog.fsck.com/2013/12/better-and-better-keyboards.html',
'title': u'Prototype ergonomic mechanical keyboards'}
(u'H5N1', u'http://blog.samaltman.com/h5n1')
.............
.............
.............
2013-12-12 16:58:41+0530 [hn] INFO: Closing spider (finished)
2013-12-12 16:58:41+0530 [hn] INFO: Dumping Scrapy stats:
{'downloader/exception_count': 2,
'downloader/exception_type_count/twisted.internet.error.DNSLookupError': 2,
'downloader/request_bytes': 22401,
'downloader/request_count': 71,
'downloader/request_method_count/GET': 71,
'downloader/response_bytes': 1482842,
'downloader/response_count': 69,
'downloader/response_status_count/200': 61,
'downloader/response_status_count/301': 4,
'downloader/response_status_count/302': 3,
'downloader/response_status_count/404': 1,
'finish_reason': 'finished',
'finish_time': datetime.datetime(2013, 12, 12, 11, 28, 41, 289000),
'item_scraped_count': 63,
'log_count/DEBUG': 141,
'log_count/INFO': 4,
'request_depth_max': 2,
'response_received_count': 62,
'scheduler/dequeued': 71,
'scheduler/dequeued/memory': 71,
'scheduler/enqueued': 71,
'scheduler/enqueued/memory': 71,
'start_time': datetime.datetime(2013, 12, 12, 11, 27, 6, 843000)}
2013-12-12 16:58:41+0530 [hn] INFO: Spider closed (finished)
你將會在終端上看到大約400行的大量輸出 ( 上面的輸出之所以這么短,目的是為了方便觀看 ).
你可以通過下面這個小命令將輸出包裝成JSON格式
$ scrapy crawl hn -o items.json -t json
現在我們已經基于正在找尋的項目實現了我們抓取器.
保存抓取到的數據
我們開始的步驟是創建一個保存我們抓取到的數據的數據庫。打開 settings.py 并且像下面展現的代碼一樣定義數據庫配置。
BOT_NAME = 'hn'
SPIDER_MODULES = ['hn.spiders']
NEWSPIDER_MODULE = 'hn.spiders'
DATABASE = {'drivername': 'xxx',
'username': 'yyy',
'password': 'zzz',
'database': 'vvv'} 再在 hn 目錄下創建一個 mdels.py 文件。我們將要使用SQLAlchemy作為ORM框架建立數據庫模型。
首先,我們需要定義一個直接連接到數據庫的方法。為此,我們需要引入 SQLAlchemy 以及settings.py文件。
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.engine.url import URL
import settings
DeclarativeBase = declarative_base()
def db_connect():
return create_engine(URL(**settings.DATABASE))
def create_hn_table(engine):
DeclarativeBase.metadata.create_all(engine)
class Hn(DeclarativeBase):
__tablename__ = "hn"
id = Column(Integer, primary_key=True)
title = Column('title', String(200))
link = Column('link', String(200)) 在開始下一步之前,我還想說明一下在 URL() 方法里兩個星號的用法: **settings.DATABASE。首先,我們通過 settings.py 里的變量來訪問數據庫。這個 ** 實際上會取出所有在 DATABASE 路徑下的值。URL 方法,一個在SQLAlchemy里定義的構造器,將會把key和value映射成一個SQLAlchemy能明白的URL來連接我們的數據庫。
接著,URL() 方法將會解析其他元素,然后創建一個下面這樣的將被 create_engine() 方法讀取的URL。
'postgresql://xxx:yyy@zzz/vvv'
接下來,我們要為我們的ORM創建一個表。我們需要 從 SQLAlchemy 引入declarative_base()以便把我們為表結構定義的類映射到Postgres上,以及一個從表的元數據里創建我們所需要的表的方法,還有我們已經定義好的用于存儲數據的表和列。
管道管理
我們已經建立了用來抓取和解析HTML的抓取器, 并且已經設置了保存這些數據的數據庫 . 現在我們需要通過一個管道來將兩者連接起來.
打開 pipelines.py 并引入 SQLAlchemy的 sessionmaker 功能,用來綁定數據庫 (創建那個連接), 當然也要引入我們的模型.
from sqlalchemy.orm import sessionmaker from models import Hn, db_connect, create_hn_table class HnPipeline(object): def __init__(self): engine = db_connect() create_hn_table(engine) self.Session = sessionmaker(bind=engine) def process_item(self, item, spider): session = self.Session() hn = Hn(**item) session.add(hn) session.commit() return item
我們在這里創建了一個類, HnPipeline(). 我們有一個構造器函數 def __init__(self) 來通過定義引擎初始化這個類, hn表格,還使用定義的這個引擎綁定/連接到數據庫.
然后我們定義 _process_item() 來獲取參數, _item_ 和 _spider_. 我們建立了一個同數據庫的會話, 然后打開一個我們的Hn()模型中的數據項. 然后我們通過電泳session.add()來將 Hn 添加到我們的數據庫中 – 在這一步, 它還沒有被保存到數據庫中 – 它仍然處于 SQLAlchemy 級別. 然后, 通過調用 session.commit(), 它就將被放入數據庫中,過程也將會被提交 .
我們這里幾乎還沒有向 settings.py 中添加一個變量來告訴抓取器在處理數據時到哪里去找到我們的管道.
那就在 settings.py加入另外一個變量, ITEM_PIPELINES:
ITEM_PIPELINES = {
'hn.pipelines.HnPipeline':300
}
這就是我們剛才所定義管道的目錄/模塊的路徑.
現在我們就可以將所有抓取到的數據放到我們的數據庫中, 讓我們試試看我們獲取到了什么,
再一次運行 crawl命令,并一直等到所有的處理過程完畢為止.
定時任務
如果我們不得不定期手動去執行這個腳本,那將會是很煩人的. 所有這里需要加入定時任務 .
定時任務將會在你指定的任何時間自動運行. 但是! 它只會在你的計算機處在運行狀態時 (并不是在休眠或者關機的時候),并且特定于這段腳本需要是在和互聯網處于聯通狀態時,才能運行. 為了不管你的計算機是出在何種狀態都能運行這個定時任務, 你應該將 hn 代碼 和bash 腳本,還有 cron 任務放在分開的將一直處在”運行“狀態的服務器上伺服.
總結
這是有關抓取的最簡短小巧的教程,而scrapy擁有提供高級功能和可用性的更多特性.
從 Github 下載整個源代碼.

