はじめに
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
クラス内で定義された関数が実行されただけです。
具体的に見ていくと、、
- start_urls で定義したURLにアクセスします。
- まずデフォルトで設定されている、
parse
関数が実行されます。ここでは、CSS セレクタを使用してquestionのページへのURLを回収しています。 parse
関数のcall backメソッドとして、parse_question
が指定されていることにより、parse
関数が返り値を返すと順次、parse_question
が実行され、各ページのスクレイピング結果の辞書型での回収が行われます。- 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形式で出力されます。
ここで行われていることは、
- start_urls で定義したURLにアクセスします。
- まずデフォルトで設定されている、
parse
関数が実行されます。ここでは、CSS セレクタを使用してclass="quote"
のついたdivタグの要素それぞれについて、text
とauthor
を回収していましす。 - 全てのdivタグについて処理が投げ終わると、
response.follow
関数により、再度次のページについてparse
関数を呼び出しています。これが次のページがなくなるまで繰り返されます。 - Scrapyに渡された辞書型オブジェクトは良しなに解釈され、JSON形式で出力されます。
v1.0の時とやってることが微妙に違いますが、共通して並列でクローリング、スクレイピングが行われています。
v1,0のstackoverflow_spider.py
では、「1つめのページでURLを取得、取得した各URLについて、順次スクレイピング」していたのに対し、
v1.5のquotes_spider.py
では、「ページ全体をスクレイピング後、取得した次ページについて、順次スクレイピング」しています。
v1.5で使用した例が変わっているのは、自作したURLのほうが扱いやすい(変更しない)ためでしょう。