ざっくり理解するScrapyの使い方

なんとなく分かってきたのでまとめる。

Scrapyとは

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

上記リンクに書いてある通り、スクレイピングとクローリングを行うフレームワークで、Pythonで動きます。

スクレイピングとクローリング

スクレイピングとは、HTMLドキュメントから情報を取得することです。クローリングはHTMLなどのWebコンテンツを収集する処理のことです。スクレイピングとクローリングはごっちゃにされて語られることが多いのですが、厳密に言えばそのような分け方になります。

最初にお約束

スクレイピングやクロールはWebサーバにすごく負荷をかけるかもしれない処理です。Scrapyのデフォルト設定ではそこまで負荷がかかるような設定にはなっていない筈ですが、リクエストを短期間に送れるだけ送るみたいな処理は慎みましょう。偽計業務妨害にあたる可能性があります(岡崎市立中央図書館事件など)。

以下の例では当ブログをスクレイピングするようなコードを載せていますが、実際に試すときは自分のブログ等でやってくださいね。

インストール

$ pip install scrapy

基本

scrapyはscrapyのお作法に則ったディレクトリ構成・ソースコードから構成されています。実行するときはscrapyコマンドを利用します。

とりあえず使ってみる

$ scrapy

とタイプするととりあえず使い方が表示されます。とりあえずシェルを使ってみましょう。

In [1]: fetch("http://anopara.net/")
2017-02-24 08:49:40 [scrapy.core.engine] INFO: Spider opened
2017-02-24 08:49:40 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://anopara.net/robots.txt> (referer: None) ['partial']
2017-02-24 08:49:43 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://anopara.net/> (referer: None) ['partial']

クロールするときは必ず内部でrobots.txtを読み取ってよしなに従ってくれます。後述しますが、robots.txtに書いてあるsitemapがあればそれにしたがってクロールすることも可能です。

スクレイピングするためにBeautifulSoupとlxmlを使っています。CSSセレクタかXPathを使うことができます。

In [15]: response.css("h1 ::text").extract()
Out[15]:
['【質問#101】おウィッシュリスト',
 '【質問#100】anopara管理人の転職と人との出会いについて',
 '日野万願寺の謎',
 '【質問#99】車好きな人々からの価値観の押し付け',
 '【質問#98】チームリーダーを断りたい',
 'アイドリングストップ車のバッテリ延命策',
 '夢日記短編集',
 '2017年新型CX-5に試乗してきたので写真とか感想とか',
 '【質問#97】ストーリーがある夢を見るコツ',
 '【質問#96】クルマの安全装備について',
 '「雇ってください」の余談',
 '転職したいので誰か雇ってください(もしくは仕事承ります)',
 '【質問#95】可愛い車を教えてください',
 '冬は車のA/Cスイッチを入れるべき?',
 '【質問#94】i-DCDは今後どうなるのか',
 '【質問#93】独身寮から出るべきかどうか',
 '【質問#92】妻の大黒柱計画',
 '【質問#91】4WDの直進安定性について',
 '【質問#90】雪道山岳路で4WDは必要か',
 '【質問#89】セミ先輩について',
 '投稿ナビゲーション']

プロジェクトを作成する

さて、大体shellで使い方を覚えたら今度はプロジェクトを作ってみましょう。

 $ scrapy startproject helloscrapy
 $ tree helloscrapy/
helloscrapy/
├── helloscrapy
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

するとこんな感じのプロジェクトが出てきます。ここで、Scrapy独自の要素があるので簡単に説明します。

spider
クローラとスクレイパーが合体したような単位。どのようにクロールしてどのようにデータを取り出すかを規定する
items
取り出したデータを表すオブジェクト。spiderによって生成される。
pipelines
取り出したitemオブジェクトを処理していくパイプライン。前処理をやってデータベースに保存して…みたいな。
middlewares
便利なミドルウェア。URLのフィルタなど。
settings
設定。

データの流れとしては、まずspiderで最初に読み取るURLを指定し、そのspiderがスクレイピングしたitemをpipelineに従って処理していく、みたいなイメージです。

Spider

ではまずSpiderを定義します。Spiderを作るのもscrapyコマンドから実施します。

$ scrapy genspider AnoparaSpider anopara.net
Created spider 'AnoparaSpider' using template 'basic' in module:
  helloscrapy.spiders.AnoparaSpider

$ cat helloscrapy/spiders/AnoparaSpider.py
# -*- coding: utf-8 -*-
import scrapy


class AnoparaspiderSpider(scrapy.Spider):
    name = "AnoparaSpider"
    allowed_domains = ["anopara.net"]
    start_urls = ['http://anopara.net/']

    def parse(self, response):
        pass

という感じになってます。この、parseというメソッドが肝です。parseはresponseを受け取ってそこからデータ(Itemのサブクラス、辞書オブジェクトなど)と次に見るべきURL(Requestオブジェクト)をyieldで返していきます。たとえばリンクを総なめしてタイトルとURLを取得するなら以下のような感じですね。

# -*- coding: utf-8 -*-
import scrapy
from scrapy.http.request import Request

class AnoparaspiderSpider(scrapy.Spider):
    name = "AnoparaSpider"
    allowed_domains = ["anopara.net"]
    start_urls = ['http://anopara.net/']

    def parse(self, response):
        for url in response.css("a::attr(href)").extract():
            if "http" not in url:
                yield Request("http:" + url)
            else:
                yield Request(url)

        yield { "title": response.css("title::text").extract()[0], "url": response.request.url }

ここで、"http" not in url...とあるあたりは、当ブログがhttpとhttpsの両方に対応している関係上、スキームを厳密に指定していない表記をしているためです。

Itemをちゃんと定義する

先の例ではItemを定義するのが面倒なので辞書オブジェくトを返却しました。ちゃんとItemを定義してItemを返すならば次のページを参照してください。

Items

Itemを定義するメリットは、

While convenient and familiar, Python dicts lack structure: it is easy to make a typo in a field name or return inconsistent data, especially in a larger project with many spiders.

などと書かれていますが、んなこと言うんだったらそもそもPythonなんか使わずに静的型付け言語使えや、って感じなんで私個人としてはItemをわざわざ定義するメリットって無いかなという気がします。

パイプライン処理

生成された直後のソースはこんな感じになってます。process_itemでitemを受け取ってデータを整形したりデータベースに保存したりして、また次のパイプラインに渡すためのitemを返却します。

$ cat helloscrapy/pipelines.py
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html


class HelloscrapyPipeline(object):
    def process_item(self, item, spider):
        return item

ここは好きなように書いてくださいって感じなので詳しくは省略。ドキュメントにはMongo DBにぶっこむサンプルなどが書いてありますので見てください。

作成したパイプラインをどのように実行させていくかは、settings.pyに以下のように書きます。

ITEM_PIPELINES = {
    'helloscrapy.pipelines.HelloscrapyPipeline': 300,
    'helloscrapy.pipelines.HelloscrapyPipeline2': 800,
}

ここに書いた整数値が小さいものから大きなものに向かって順に実行させていくようになるそうです。

運用

Spiderを動かすホスティングサービスであるScrapy Cloudを使うとか、Scrapydというデーモンを使うとかになります。そこまで大規模なことをしないのであれば、cronで叩くとかでも十分な気が。

その他Tipsなど

フィルタ

スクレイピングするなら、同じURLは1回だけアクセスしたいとかありますよね。そういう処理を実現するのがフィルタです。settings.pyでDUPEFILTER_CLASSを設定すると使用でき、デフォルトは'scrapy.dupefilters.RFPDupeFilter'が設定されるみたいです。ただ、こいつはプロセスが終了したらスクレイピングしたページも忘れるような実装になっています。

URLが同じだけど内容は毎回更新される、みたいなサイトをスクレイピングするときはこの実装で良いと思いますが、スクレイピングした結果はファイルやDBに保存して二回目以降であっても一回訪れたページはスクレイピングさせたくないということでしたら、これを独自実装する必要があります。その場合、

import os

from scrapy.dupefilters import RFPDupeFilter
from scrapy.utils.request import request_fingerprint
import pymysql.cursors

class CustomFilter(RFPDupeFilter):

    def request_seen(self, request):
        // すでに見たページならTrue、そうでないページならFalseを返す処理を書く
        pass

のようにRFPDupeFilterを継承してrequest_seenをオーバーライドします。そして、作ったCustomFilterをDUPEFILTER_CLASSに設定します。

xmlのサイトマップを使ってスクレイピング

私はこれが使いたくてScrapyを選びました。本当に楽です。

# -*- coding: utf-8 -*-
import scrapy

class AnoparaSpider(scrapy.spiders.SitemapSpider):
    name = "hoge"
    allowed_domains = ["hoge.net"]
    sitemap_urls = ['http://hoge.net/robots.txt']

    def parse(self, response):
        pass

これだけでOKです。すると、robots.txtに書かれたsitemapを読み取ってうまいこと処理してくれます。sitemap.xmlから別のxmlをさらに呼び出していたりとか、gzip圧縮されてたりとか、そういう場合でも全て裏側で対処してくれます。

XPathでnamespaceが指定されてるXMLを処理する

このあたりを参照してください。

Selector examples on XML response

こういうのが面倒くさいからXMLって好きじゃないんだよな。

RSSフィードからスクレイピング

このあたりを参照してください。

XMLFeedSpider

ソースコードを見ると分かりますが、大したことはやってません。実際使い始めるとこれじゃ対処できないパターンもたくさん出てくると思うので、このソースを参考にしつつBasicSpiderから構成していった方が良い気がします。

Request時に独自の属性や値を設定しておきたい

Requestを作るときには分かるんだけど、RequestからRepsponseを作ってからでは分からない、つまりリンク元に記載されてある情報をなんか取得したいみたいな場合は、Requestに何とかして属性を入れ込んでおきます。すると、parseメソッドでresponseを受け取った時、response.requestから辿って取得できます。

値を追加する方法は、

  • Request.metaに値をセットしておく
     - Requestを継承して追加のパラメータを設定できるようにする

など。

まとめ

Scrapyは、scrapyコマンドの使い方とかitem/spider/pipeline/filterあたりの概念を理解するとかで少し戸惑ってしまうかもしれませんが、理解できてしまえばあとは色々応用できそうです。