双六工場日誌

平凡な日常を淡々と綴ります。

Python製クローラー「Scrapy」の始め方メモ

下書きのまま半年ぐらいほったらかしだったものからサルベージ。内容が古くなっているかもですがご容赦ください。

Python製クローラー「Scrapy」は、さくっとクローラーをつくる際に非常に便利なんですが、フレームワークの全体像を理解するところで時間がかかったので、クローラーをつくる際のとっかかりのところをメモとしてまとめました。

インストール

まずは、依存パッケージのインストール。以下は、「Amazon Linux AMI 2015.09」の場合。

$ sudo yum groupinstall "Development tools" $ sudo yum install python-devel libffi-devel openssl-devel libxml2-devel libxslt-devel

続いて、本体インストール。service_identityは、MITM攻撃を防ぐためのパッケージで、一緒に入れておくのが推奨とのことなので、最初に入れておきます。System Pythonにそのまま追加する手順になっていますが、virtualenv等で環境を分けている場合は、適宜読み替えてください。

$ sudo pip install scrapy service_identity

雛形の作成

ScrapyのProject雛形をつくる

Scrapyを入れたら、続いてクローラーの雛形を作っていきます。まずは、クローラーや各種設定ファイルを入れるは異なる「プロジェクト」を作成します。"scrapy"コマンドが使えるようになっているはずなので、「scrapy startproject <プロジェクト名>」で、カレントディレクトリにプロジェクトの雛形ができます。

$ scrapy startproject example_project

コマンドが成功すると以下のような雛形ができます。

$ tree example_project/
example_project/
├── example_project
│   ├── __init__.py
│   ├── items.py: 収集対象の項目を定義するファイル
│   ├── pipelines.py: データ収集後のアクションを定義するファイル
│   ├── settings.py: アクセス間隔などのクローラーの設定を行うファイル
│   └── spiders
│       └── __init__.py
└── scrapy.cfg: プロジェクト全体の設定ファイル

プロジェクトの中にクローラーの雛形をつくる

続いて、プロジェクトの中にクローラーの雛形を作ります。クローラーをつくるコマンドは「scrapy genspider -t <Template名> <クローラー名> <収集対象サイトドメイン>」です。

<Template名>は、クローラーのひな形の指定。以下の4つのうちのどれかが指定できますが、Webサイトを順に辿って情報を取得する一般的なクローラーをつくる際には、「crawl」オプションを指定します。

  • basic
  • crawl
  • csvfeed
  • xmlfeed

<クローラー名>は、あとでクローラーを走らせる時に指定する名前です。<収集対象サイトドメイン>は、ひな形の中の"allow_domain"、"start_urls"に利用されるドメイン名です。収集したいサイトのドメイン(URLから"http://"を取ったもの)を指定します。

$ cd example_project $ scrapy genspider -t crawl example-crawler www.example.com

ここまでで終わると以下のような雛形ができます。*1

example_project/
├── example_project
│   ├── __init__.py
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── example_crawler.py: クローラーの定義を行うファイル
└── scrapy.cfg

これで作った雛形のうち、以下の3つに具体的な設定を入れることでクローラー完成です。

  • settings.py
  • items.py
  • example_crawler.py

具体的なクローリング設定

収集間隔の設定(settings.py)

最初にやっておくべきなのは、settings.pyでの収集間隔の設定です。

クローラーを動かす際、適切な収集間隔が設定されておらず、休みなくそのサイトにアクセスしてしまうと、そのサービスに迷惑をかけてしまうことになりかねません。そのため、最初に以下をsettings.pyに書き込んで、1秒程度*2の間隔を空けてアクセスするようにします。

DOWNLOAD_DELAY=1

収集対象項目の定義(items.py)

続いて、収集対象項目を設定します。scrapyでは、"scrapy.Item"を継承したクラスで、出力対象項目を定義します。items.pyの雛形を開くと以下のようになっています。

import scrapy

class ExampleProjectItem(scrapy.Item):
     # define the fields for your item here like:
     # name = scrapy.Field()
     pass

ここでは、"name"のコメントを外し、"body"を追加してみます。ここで急にクラスが出てくるところがわかりにくい点だと思いますが、内容がわからずとも、ここに「<項目名> = scrapy.Field()」を追加しておけば、取得対象項目を増やすことができることがわかっていれば十分です。

import scrapy


class ExampleProjectItem(scrapy.Item):
     # define the fields for your item here like:
     name = scrapy.Field()
     body = scrapy.Field()

クローラーの設定(example_crawler.py)

example_crawler.pyは、以下のような中身になっています。「name」のところがクローラー雛形作成時に指定した<クローラー名>となっています。

class ExampleCrawlerSpider(CrawlSpider):
    name = 'example-crawler'
    allowed_domains = ['www.example.com']
    start_urls = ['http://www.www.example.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        i = ExampleProjectItem()
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return i
  • name: このクローラーの名前。最初につけた名前。
  • allowed_domains: クローリング対象となるドメイン。ここで指定されていないドメインにリンクが貼られていても収集対象にならない。
  • start_urls: クローリングの出発点となるURLのリスト
  • rules: クローリングしたページのうち、リンクを辿るURLとその動作を指定する設定です。
  • parse_item: クローリングしたベージからの情報抽出方法を指定。ページの内容は"response"という変数の中に格納されて渡されるので、その中身を"items.py"で定義したオブジェクトに入れてreturnで返すと、フレームワークがよしなに整形して出力してくれる。*3

こちらの最低限の変更点は、"rules"と"parse_item"メソッドです。

rulesは、ページに含まれるどのリンクを辿るかという設定です。Scrapyは、何も記述せずとも、<a>タグの"href"に書かれているリンク先をすべて取得してきてくれます。その取得したリンクのうち、どれを辿って、どれを辿らないのかを設定するのかをここで決めます。

rules = ( Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), )

デフォルトの「(allow=r'Items/')」は、収集対象ドメインの中で「items/」が含まれるURLを取得対象にするという意味になります。ここを対象のサイトに合わせて変更してください。

このような自動的にリンクを集めてきてくれるのは非常に便利なのですが、デフォルト動作を変えたい場合、若干ハードルが高いのが悩ましいところ。

parse_itemは、このrulesで引っかかったページデータの処理内容を書くメソッドです。引数「response」には、取得したページのデータが入っています。この取得したデータをitems.pyで定義した取得項目にマッピングします。items.pyでは、「name」と「body」を定義していたので、それとページの内容の対応付けを書き入れます。取得したページの内容はXPATHもしくはCSSのセレクタでの指定が可能です。取得対象を決める際には、取得対象のサイトの構造をChromeのデベロッパーツール等で確認すると便利です。

たとえば、以下のように変更します。そのほか具体的な指定方法は、公式サイトの例が詳しいので、そちら参照で。

def parse_item(self, response):
    i = ExampleProjectItem()
    i['name'] = response.xpath('//div[@id="name"]').extract()
    i['body'] = response.xpath('//body').extract()
    return i

ここまでで、最低限のクローラー作成は完了です。クセさえ掴んでしまえば、ほとんど自分でコードを書くことなくクローラーをつくることができます。

データ収集

作成したクローラーを動かすコマンドは、「scrapy crawl <クローラー名> -o <出力先ファイル名>」です。<クローラー名>はクローラーの雛形作成時に指定したものです。また、出力形式はCSV、JSON、JSONLinesから選択できますが、どの形式を使うかh<出力先ファイル名>に指定する、ファイルの拡張子から自動的に選択されます。

たとえば、以下のように拡張子「.jl」を使うと、JSONLinesでの出力となり、各ページが1行のJSONに対応する形でファイルにクローリング結果が書き込まれます。

$ scrapy crawl example_crawler -o output.jl

クローラーを作っていく際には、この出力結果と実際のサイトを比較して、items.pyとparse_itemsに必要なフィールドを足し引きしていくことになります。

まとめ

Scrapyでクローリングを始めるに当たって、最低限必要な内容を取り上げました。Scrapyは、非常に機能が豊富で、かつ、拡張性が高いので、これをベースにカスタマイズしていけばWeb上から有用な情報を手軽に収集することができるようになります。

これからWebクローリングを始めようとしている人の手助けになれば幸いです。

*1:*.pycは省略

*2:scrapyはデフォルトで、間隔をランダムにずらす機能が有効になっているため、実際には、ここで設定した数値×0.5〜1.5秒間となります

*3:Ruleの引数の中に「callback='parse_item'」と指定されている