Intro to Scrapy, CrawlerSpider

前言

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

1. 環境配置

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

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

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

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

pip install Scrapy peewee

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

2. 撰寫爬蟲

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

scrapy startproject tutorial

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

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 ,沒錯就是這麼簡單!

# -*- coding: utf-8 -*-
from scrapy.item import Item, Field

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

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

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

先看一下完整的程式碼:

# -*- 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/唐詩宋詞/唐朝作者列表',
    )

    rules = (
	    Rule(LinkExtractor(allow=('/唐詩宋詞.*?')), 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

rules = (
	Rule(LinkExtractor(allow=('/唐詩宋詞.*?')), callback='parse_links', follow=False),
)

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

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

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

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

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,然後將你要的資訊都塞進去,爬蟲的部分就算完成了!

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

{
	"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

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

# 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

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 裡頭。

# ...
ITEM_PIPELINES = {
    'tutorial.pipelines.PreprocessingPipeline': 300,
    'tutorial.pipelines.SqlitePipeline': 500,
}
# ...

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

5. 其他

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

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

Show Comments