無駄と文化

実用的ブログ

Scrapy の start_urls をファイルから読み込む for v1.0.4

表題の通り、 Scrapy の start_urls を外部ファイルから読み込んで設定する方法を書き留めます。
Scrapyのバージョンは 1.0.4 を想定しています。


導入

まず、scrapy コマンドで生成されるプレーンなスパイダーを見てみましょう。

$ scrapy startproject scrapy_sample
$ cd scrapy_sample
$ scrapy genspider example exapmle.com

こんな感じで scrapy コマンドを叩くと、spiders ディレクトリの中に example.py が生成されます。


scrapy_sample/spiders/example.py

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


class ExampleSpider(scrapy.Spider):
    name = "example"
    allowed_domains = ["example.com"]
    start_urls = (
        "http://www.example.com",
    )

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

実際に動かしてみるために、あらかじめ perse() に少し書き足しています。
よくある例ですが、URL と <title> を抜き出して、辞書にして返すようにしました。


今回注目したいのは start_urls の部分ですね。
現状だとスクレイピング対象の URL はハードコーディングされています。
対象 URL をソースコードに直書きしつつ管理したいと思う人はあまり居ないのでしょう。

そんなわけで、この start_urls に指定する URL を外部ファイルから読み込んであげることにします。


start_urls 設定用ファイルの仕様

仕様といっても簡単なものです、プロジェクトディレクトリの直下に start_urls.txt というテキストファイルを作って、そこから URL を読み込むことにします。
URL は改行で区切ります。つまり、1行に1URLを書いていくスタイルです。


start_urls.txt

http://www.cnn.co.jp/
https://www.yahoo.com/
http://www.reuters.com/

ひとまず、当たり障りのない URL を並べておきます。


コンストラクタを記述

スパイダーの初期設定はコンストラクタの中でするのがふさわしいでしょう。ExampleSpider クラスの __init__() を記述します。
親クラスである scrapy.Spider クラスの __init__() を呼び出すのも忘れずに、まずはお決まりのコードを書きます。


scrapy_sample/spiders/example.py

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


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = (
        "http://www.example.com",
    )

    def __init__(self, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)
        # Do something.

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

このように、

さて、Do something. の所にテキストファイル読み込みの処理を書いていきましょう。


scrapy_sample/spiders/example.py

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


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = []

    def __init__(self, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)

        f = open("start_urls.txt")
        urls = f.readlines()
        f.close()

        # 各要素の末尾についた改行文字 "\n" を削除
        # 空行("\n" だけの行)も削除
        self.start_urls = [url.rstrip("\n") for url in urls if url != "\n"]

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

1行1URLにしたので、readlines() を呼ぶだけで URL の配列を取得できます。
self.start_urls に URL の配列を上書きしてあげればOKです。

ただし、

  • readlines() で取得した配列は各要素の末尾に改行文字 "\n" を含んでいる
  • もしかしたら空行があるかも知れない

という2点を考慮して, リスト内包表記で 末尾の改行文字 と 空行 を取り除く処理をはさんでいます。


ファイル名を settings.py で設定

さて、無事に外部ファイルから start_urls を読み込むことに成功しましたが、今度は "start_urls.txt" というファイル名をハードコーディングすることになってしまいました。

せっかくなんで、読み込むファイル名は settings.py に記述したいですよね。


settings.py に独自の項目を追加して使う方法は以前の記事で書いた方法をそのまま使います。

まずは settings.py の末尾に項目を追加しましょう。

scrapy_sample/settings.py

# -*- coding: utf-8 -*-

# Scrapy settings for scrapy_sample project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'scrapy_sample'

# ... 中略 ...

START_URLS = "start_urls.txt"

settings.py に記述した設定値は crawler.settings というオブジェクトに格納されます。
この crawler.settingsExampleSpider クラスの中で読めるようにしましょう。


from_crawler() メソッドを使うとクローラーから値を引き次ぐことができます。


scrapy_sample/spiders/example.py

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


class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = []

    def __init__(self, settings, *args, **kwargs):
        super(ExampleSpider, self).__init__(*args, **kwargs)

        # 読み込むファイルは settings.py の START_URLS で設定する
        f = open(settings.get("START_URLS"))
        urls = f.readlines()
        f.close()

        # 各要素の末尾についた改行文字 "\n" を削除
        # 空行("\n" だけの行)も削除
        self.start_urls = [url.rstrip("\n") for url in urls if url != "\n"]

    @classmethod
    def from_crawler(cls, crawler):
        # settings.py に記述された全ての設定項目を settings として格納
        return cls(settings = crawler.settings)

    def parse(self, response):
        url   = response.url
        title = response.xpath("//title/text()").extract_first()

        yield {"url": url, "title": title}

from_crawler() の中で cls()crawler.settings を渡してあげます。

それを受け取る __init__() の側では、第2引数に settings を追加しています。


これにて __init__() の中で settings にアクセスできるようになりました。
ファイル読み込み部分も f = open(settings.get("START_URLS")) と書き換えています。


実行

$ scrapy crawl example -o result.json

とすれば、結果が result.json に JSON 形式で保存されます。


result.json

[{"url": "http://www.cnn.co.jp/", "title": "CNN.co.jp"},
{"url": "https://www.yahoo.com/", "title": "Yahoo"},
{"url": "http://www.reuters.com/", "title": "Business & Financial News, Breaking US & International News | Reuters.com"}]

となっているので、上手く動いてくれているようです。


まとめ

Scrapy はシンプルで綺麗なアーキテクチャを持って設計されているフレームワークですが、公式ドキュメントを読むと「使用目的に合わせて好きなように組み替えて使ってくれぃ」というメッセージも感じます。

Scrapy の各クラスとメソッド群があれば、少ない記述で ある程度カスタマイズすることは可能です。
いい感じですね。


私からは以上です。