Scrapyことはじめ

f:id:kimoppy126:20181111033229j:plain:w700

はじめに

Scrapyは、Python製のスクレイピング用フレームワークです。
特徴として、以下の3つが挙げられています。

  • Fast and powerful
    データをスクレイピングするためのルールさえ書けば、あとはScrapy側が処理してくれる。

  • Easily extensible
    拡張が容易で、核心部分を変更することなく拡張することができる。

  • Portable, Python
    Pythonで書かれており、プラットフォームに依存しない。

どれも間違ってないです。
webスクレイピング自体は、BeautifulSoupや、Seleniumを使用すれば大抵問題なくできてしまうのですが、Scrapyを使うと、いろいろと便利だったりします(並列処理、ログ取得、エラー処理等)。
なんだかんだ結構な頻度で使っているのですが、いまだにちゃんとドキュメントを読んでいなかったのでした。
今回、SeleniumとScrapyの合わせ技的なことをやろうとしているので、ここは1つチュートリアルでもやってみよう。の巻です。

Scrapyチラ見

Scrapyのチュートリアルは、Scrapy at a glanceという項目で紹介がされています。
割と簡単に、Scrapyのすごみが体験できるらしい。

ここでは、エンジニアの皆さんなら一度はお世話になるであろう StackOverFlowをクローリングする例が紹介されています。

import scrapy


class StackOverflowSpider(scrapy.Spider):
    name = 'stackoverflow'
    start_urls = ['http://stackoverflow.com/questions?sort=votes']

    def parse(self, response):
        for href in response.css('.question-summary h3 a::attr(href)'):
            full_url = response.urljoin(href.extract())
            yield scrapy.Request(full_url, callback=self.parse_question)

    def parse_question(self, response):
        yield {
            'title': response.css('h1 a::text').extract()[0],
            'votes': response.css('.question .vote-count-post::text').extract()[0],
            'body': response.css('.question .post-text').extract()[0],
            'tags': response.css('.question .post-tag::text').extract(),
            'link': response.url,
        }

stackoverflow_spider.pyという名前で保存したら、実行コマンドは以下になります。

scrapy runspider stackoverflow_spider.py -o top-stackoverflow-questions.json

結果は

[{
    "body": "... LONG HTML HERE ...",
    "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array",
    "tags": ["java", "c++", "performance", "optimization"],
    "title": "Why is processing a sorted array faster than an unsorted array?",
    "votes": "9924"
},
{
    "body": "... LONG HTML HERE ...",
    "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule",
    "tags": ["git", "git-submodules"],
    "title": "How do I remove a Git submodule?",
    "votes
": "1764"
},
...]

こんな感じにきれーにJSON形式でクローリング結果が返されます。

いったい何が起きたのか。

crawler engineを介してscrapy.Spiderクラス内で定義された関数が実行されただけです。
具体的に見ていくと、、

  1. start_urls で定義したURLにアクセスします。
  2. まずデフォルトで設定されている、parse関数が実行されます。ここでは、CSS セレクタを使用してquestionのページへのURLを回収しています。
  3. parse関数のcall backメソッドとして、parse_questionが指定されていることにより、parse関数が返り値を返すと順次、parse_questionが実行され、各ページのスクレイピング結果の辞書型での回収が行われます。
  4. Scrapyに渡された辞書型オブジェクトは良しなに解釈され、JSON形式で出力されます。

出力形式について

コマンドラインで指定した拡張子からScrapyが自動認識し、JSON形式で出力してくれています。
このほかにも、XMLやCSV等、好きな出力形式を選択することができる上に、FTPやAmazonS3にも格納可能です。
データベースへの格納には、Scrapyの機能であるitem pipelineという機能を使用します。

Scrapyを使用するメリット

Requestのスケジューリングと、非同期実行ができることが一番大きいかと思います。 これができると、以下のようなメリットが生まれます。

  • あるRequestが完了するのを待たずして、別のRequestを投げることができる。
  • いくつかのRequestが失敗もしくはエラーになっても、ほかのリクエストを送り続けることができる。
  • 結果として、高速なクローリングが可能となる。
  • いくつか設定をするだけで、スクレイピングを安全に(法律を侵さずに)実行することができる。

積極的に使っていきたいですねぇ。

Scrapyチラ見 v1.5 (追記 2018.11.11)

ふと見たら上記はv1.0の事例でした。。
一応v1.5のScrapy at a glanceであるhttp://quotes.toscrape.com/からデータをスクレイピングする例も見ておきましょう。

コードは以下になります。

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/tag/humor/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').extract_first(),
                'author': quote.xpath('span/small/text()').extract_first(),
            }

        next_page = response.css('li.next a::attr("href")').extract_first()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

quotes_spider.pyという名前で保存したら、実行コマンドは以下になります。

scrapy runspider quotes_spider.py -o quotes.json

結果は

[{
    "author": "Jane Austen",
    "text": "\u201cThe person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.\u201d"
},
{
    "author": "Groucho Marx",
    "text": "\u201cOutside of a dog, a book is man's best friend. Inside of a dog it's too dark to read.\u201d"
},
{
    "author": "Steve Martin",
    "text": "\u201cA day without sunshine is like, you know, night.\u201d"
},
...]

やはりこんな感じにきれーにJSON形式で出力されます。

ここで行われていることは、

  1. start_urls で定義したURLにアクセスします。
  2. まずデフォルトで設定されている、parse関数が実行されます。ここでは、CSS セレクタを使用してclass="quote"のついたdivタグの要素それぞれについて、textauthorを回収していましす。
  3. 全てのdivタグについて処理が投げ終わると、response.follow関数により、再度次のページについてparse関数を呼び出しています。これが次のページがなくなるまで繰り返されます。
  4. Scrapyに渡された辞書型オブジェクトは良しなに解釈され、JSON形式で出力されます。

v1.0の時とやってることが微妙に違いますが、共通して並列でクローリング、スクレイピングが行われています。

v1,0のstackoverflow_spider.pyでは、「1つめのページでURLを取得、取得した各URLについて、順次スクレイピング」していたのに対し、
v1.5のquotes_spider.pyでは、「ページ全体をスクレイピング後、取得した次ページについて、順次スクレイピング」しています。

v1.5で使用した例が変わっているのは、自作したURLのほうが扱いやすい(変更しない)ためでしょう。