Intro to Scrapy, CrawlerSpider

2016-09-16

前言

以前自己寫爬蟲,由於 scale 都很小,所以也沒考慮要用 framework,都很單純的使用 requests + BeautifulSoup 來完成。這次突然興起想把唐詩宋詞都爬下來,就拿來練手練手。至於為什麼要爬唐詩宋詞?那又是另一個故事了…

1. 環境配置

為了不讓本身的環境紊亂,我都習慣先開一個 virtualenv ,並把需求寫在 requirements.txt 裡面,一來比較清楚明瞭,二來也可以指定版本號。

1
2
3
mkvirtualenv crawler # 產生環境
workon crawler # 切換到該環境
deactive crawler # 跳出該環境

以上是大致上會用到的指令,至於如何在你的作業系統上安裝 virtualenv這個部分就不贅述了。

再來就是比較主要的部份,安裝這個範例要使用的package。

1
pip install Scrapy peewee

peewee 可以選擇裝或不裝,後面範例是用 sqlite 來操作的。

2. 撰寫爬蟲

ScrapyDjango 一樣,有指令集可以使用,整個結構不必用手刻。

1
scrapy startproject tutorial

OK,專案建立完成後你可以看到結構如下,今天會接觸到 items.pypipelines.pysettings.py 以及 spiders/... ,基本上就是比較基礎的都會講到。

1
2
3
4
5
6
7
8
9
10
tutorial/
scrapy.cfg # deploy configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
...

這次要爬的對象是 唐詩宋詞 ,拿個 李商隱 - 春日寄懷 來做範例,可以看到我們要的東西有標題、作者、朝代、內容,因此第一步就是定義 items.py ,沒錯就是這麼簡單!

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-
from scrapy.item import Item, Field

class Poem(Item):
title = Field()
author = Field()
dynasty = Field()
content = Field()

接下來實際撰寫爬蟲 <root_dir>/tutorial/spiders/poems.py

Scrapy 裡面有兩種爬蟲,一種是最基本的Spider 只會 request 給定的 start_urls;另一種則是CrawlSpider ,透過定義一些rule 來提供 request 對象的方法,適合用來操作多頁面的,而今天的主角正是後者。

先看一下完整的程式碼:

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
# -*- coding: utf-8 -*-

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.http import Request
from ezreal.items import Poem

class PoemsSpider(CrawlSpider):
name = "poems"
allowed_domains = ["tw.18dao.net"]
start_urls = (
'http://tw.18dao.net/%E5%94%90%E8%A9%A9%E5%AE%8B%E8%A9%9E/%E5%94%90%E6%9C%9D%E4%BD%9C%E8%80%85%E5%88%97%E8%A1%A8',
)

rules = (
Rule(LinkExtractor(allow=('/%E5%94%90%E8%A9%A9%E5%AE%8B%E8%A9%9E.*?')), callback='parse_links', follow=False),
)

def parse_links(self, response):
sel = scrapy.Selector(response)
for link in sel.xpath('//*[@id="mw-content-text"]/ol/li/a/@href').extract():
yield Request('http://tw.18dao.net' + link, callback=self.parse_poems)

def parse_poems(self, response):
sel = scrapy.Selector(response)
item = Poem()
item['title'] = sel.xpath('//*[@id="mw-content-text"]/p[1]').extract()[0].strip()
item['author'] = sel.xpath('//*[@id="mw-content-text"]/p[2]').extract()[0].strip()
item['dynasty'] = sel.xpath('//*[@id="mw-content-text"]/p[3]').extract()[0].strip()
item['content'] = sel.xpath('//*[@id="mw-content-text"]/p[4]').extract()[0].strip()
return item

看了上面的 code 你可能不清楚他是怎麼運作的,可以稍微看一下流程圖

了解流程以後來敘述一下那些物件分別負責甚麼。

I. rules

1
2
3
rules = (
Rule(LinkExtractor(allow=('/%E5%94%90%E8%A9%A9%E5%AE%8B%E8%A9%9E.*?')), callback='parse_links', follow=False),
)

rules 是 CrawlSpider 的一個特點,讓你可以很簡單的定義規則並parse。上面的意思就是

抓取 唐詩宋詞 頁面中符合 allow 條件的網址,並在 request 後丟到 parse_links 去做後續處理。

至於 RuleLinkExtractor 官方文件都有,可見 reference。

1
2
3
4
def parse_links(self, response):
sel = scrapy.Selector(response)
for link in sel.xpath('//*[@id="mw-content-text"]/ol/li/a/@href').extract():
yield Request('http://tw.18dao.net' + link, callback=self.parse_poems)

這是一個我們自定義的 callback function,目的是要抓取該作者底下每一篇詩詞的網址,並產生一個 Request,在其請求完畢後丟到 parse_poems 去做處理。

III. parse_poems

1
2
3
4
5
6
7
8
def parse_poems(self, response):
sel = scrapy.Selector(response)
item = Poem()
item['title'] = sel.xpath('//*[@id="mw-content-text"]/p[1]').extract()[0].strip()
item['author'] = sel.xpath('//*[@id="mw-content-text"]/p[2]').extract()[0].strip()
item['dynasty'] = sel.xpath('//*[@id="mw-content-text"]/p[3]').extract()[0].strip()
item['content'] = sel.xpath('//*[@id="mw-content-text"]/p[4]').extract()[0].strip()
return item

這邊就是最底層,先產生一個Poem,然後將你要的資訊都塞進去,爬蟲的部分就算完成了!

李商隱 - 壬申七夕 來講爬出來的結果會是

1
2
3
4
5
6
{
"author": "【作者】:李商隱\n",
"title": "【標題】:壬申七夕\n",
"content": "【內容】:已駕七香車,心心待曉霞。風輕惟響珮,日薄不嫣花。\n",
"dynasty": "【朝代】:唐朝\n"
}

這種 Raw data 必須要處理一下,不能留有一些無用的資訊,此時我們就要仰賴 Pipeline 來完成此事。(當然你也可以在 parse_poems 就把它做完,端看個人喜好)

3. 撰寫 Pipeline

根據官方文件裡說的, Pipeline 適合用來:

  • cleansing HTML data
  • validating scraped data (checking that the items contain certain fields)
  • checking for duplicates (and dropping them)
  • storing the scraped item in a database

而每個 Pipeline 的主要 method 為

  • process_item(self, item, spider)
  • open_spider(self, spider)
  • close_spider(self, spider)
  • from_crawler(cls, crawler)

最常用的大概就是 process_item 了,這個 function 可以對 item 以及 當前的 spider 進行操作。OK接下來我們要清理 data 與將其儲存到資料庫的動作,分為兩個 class 去進行。

I. PreprocessingPipeline

1
2
3
4
5
6
7
8
class PreprocessingPipeline(object):
def process_item(self, item, spider):
pattern = r":(.*?)\n"
for key in item.keys():
match = re.search(pattern, item[key])
if match:
item[key] = match.group(1)
return item

這裡的 item 就是 parse_poems 所回傳的那個物件。
為了取出真正的值,利用 re 來協助,最後一樣回傳 item,讓下一個 Pipeline 執行。

II. SqlitePipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# models.py
from peewee import *
from playhouse.sqlite_ext import SqliteExtDatabase

db = SqliteExtDatabase('poems.db', journal_mode='WAL')

class BaseModel(Model):
class Meta:
database = db

class Poems(BaseModel):
author = CharField()
title = CharField()
content = CharField()
dynasty = CharField()

為了方便使用 ORM,我們得先寫好 Poems 這個 model,這部份可以詳見 peewee documentation

1
2
3
4
5
6
7
class SqlitePipeline(object):
def open_spider(self, spider):
Poems.create_table()

def process_item(self, item, spider):
Poems.create(**item)
return item

再來就是 SqlitePipeline 本身,這裡調用了兩個方法,open_spider和之前就提過的 process_itemopen_spider,當該 spider 啟動時如果有要做的事,可以寫在裡面執行一次。

4. 修改 settings

萬事具備只欠東風,最後一個步驟就是將已寫好的 pipeline 定義在 settings.py 裡頭。

1
2
3
4
5
6
# ...
ITEM_PIPELINES = {
'tutorial.pipelines.PreprocessingPipeline': 300,
'tutorial.pipelines.SqlitePipeline': 500,
}
# ...

上面的 key 是 pipeline 所在的位置,而後面的值則是優先度,越小越是優先。

5. 其他

其實還有很多東西沒寫進來,這也是第一次使用 Scrapy ,或許會有地方寫錯XD,如果您有發現歡迎提出,最後附上個 Scrapy Official Documentation

P.S. 完整的程式碼在 github