(今回の記事は、Scrapy入門① とりあえずクローラーを作って実行し、Webサイトからデータを取得してみるとこまでの続きです。Scrapyを使うのが初めてで、まだ第一回を見てない方はぜひ初めからチェックしてみてください♪)
今回は、Scrapyを使って「TechCrunch(http://jp.techcrunch.com/)」のサイトから、
- トップページに表示されている新着記事一覧を取得し、
- さらに各記事の詳細ページを開き、
- 詳細記事の本文を取得する
というクローラーを作成します。
やってること自体は前回とさほど変わらないのですが、今回はSpider単体ではなくプロジェクト単位でのクローラー開発を行い、
- クローリングに関する各種設定(settings.py)
- データ保持クラス・オブジェクトの活用(Items.py)
など、Scrapyに用意されている様々な便利機能たちの機能の使い方を学んでいきます。
もくじ
Scrapyプロジェクトの作成
前回はSpiderのみを単体で作りましたが、基本的にScrapyはプロジェクト単位でクローラーを作っていきます。
まずは以下のコマンドで、Scrapyのプロジェクトを作成してみましょう。
startprojectコマンドを実行するとScrapyプロジェクトが生成され、以下のようなメッセージが表示されます。
/scrapy/templates/project’, created in:
/your_path/tcproject
You can start your first spider with:
cd tcproject
scrapy genspider example example.com
作成したtcprojectのディレクトリに移動し、ファイル構成を見てみます。
projectのファイル構成を見るのは$ ls -R
とかでもいいんですけど、treeコマンドをbrewかなんかで入れとくと便利です。
$ tree .
.
├── scrapy.cfg
└── tcproject
├── __init__.py
├── __pycache__
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
├── __init__.py
└── __pycache__
役割ごとにいろいろなファイルがありますが、いきなり全部把握しなくても大丈夫なので、順を追って見ていきましょう。
今回の記事で作成するクローラで扱うのは、settings.py, items.py, spider(後で作成)のみです。
settings.py – Scrapyの設定ファイルの記述
Scrapyではクローリング時に必要となる様々な処理があらかじめオプションとして用意されており、設定ファイルに記述を追加するだけで、クローリング間隔の調整やユーザーエージェントの設定、キャッシュや並行処理などに至るまでの様々な機能を、わざわざ自分でコードを書くことなく簡単に利用することができます。
Scrapyの設定は、tcprojectの下にあるsettings.pyというファイルに記述します。
まずは忘れないうちに、クロール先に迷惑をかけないための設定を済ませておきましょう。
settings.pyを適当なエディタで開いて、以下の項目を追加します。
クロール間隔の調整をする
まずは、クロール先のサイトからデータをダウンロードする際の間隔を設定します。
settings.pyの中に、
とコメントアウトされている箇所があるはずですので、この行のコメントを外して設定を有効にします。
数字は秒数(ミリ秒ではない)を表しているので、この場合のクロール間隔は3秒になります。
クロール間隔をミリ秒(ms)で指定したい場合は以下。
なお、クロールの間隔は1秒以上空けることをお勧めします。
[info] クロール間隔をランダムにする上記のDOWNLOAD_DELAYと合わせて、
という項目を設定しておけば、クロール間隔をDOWNLOAD_DELAYで設定した時間の0.5~1.5倍の間でランダムに調整してくれます。
[/info]
robots.txtの指示に従う
次に、クロール先サイトのrobots.txtの指示に従う設定をしておきます。
Scrapyのstartprojectコマンドでプロジェクトを作成した場合は初めからTrueになっているはずですが、一応確認しておきましょう。
連絡先の明示
クロール先のサイトの管理者にわかるように、クローラーのユーザーエージェントにメールアドレスなどの連絡先を明記しておくとお行儀がよいです。
連絡がつくようにしておけば、何かあった際にいきなり訴えられたりされずに済むかもしれないので、未然にトラブルの可能性を回避するためにも以下の例を参考に連絡先を設定しておくといいでしょう。
jsonで出力した際の日本語がエスケープされないようにする
Scrapyで日本語を含む文字列をjsonに出力すると、文字列がエスケープされてそのままだと読めなくなってしまうので、以下の設定で文字コードを指定しておきましょう。
Items.py – データ格納クラスを作成する
Scrapyでは、取得したデータをItemというオブジェクトに格納します。
Itemオブジェクトを利用することで、
- データの種類をクラスで判別できる
- あらかじめ定義したフィールドに値を代入するため、間違えなくて済む
などのメリットがあります。
Itemは、Items.pyにクラスとして定義していきます。
Items.pyに保持したいデータのまとまりごとにクラスを定義し、そこで<フィールド名> = scrapy.Field()
とすることで、値を保持するフィールドを定義できます。
以下のサンプルでは、取得した記事データを扱うクラスArticleを定義し、その中で
- 記事タイトルを格納するtitle
- 記事本文を格納するbody
を定義しています。
1 2 3 4 5 6 7 8 9 |
import scrapy class Article(scrapy.Item): """ 記事から抜き出したタイトルと本文を格納するItem。 """ title = scrapy.Field() # 記事のタイトルを格納するフィールドtitleを定義 body = scrapy.Field() # 記事の本文を格納するフィールドbodyを定義 |
Items.pyに定義したデータ保持クラスは、spiderなどでItemオブジュクトとして利用できます。
Itemオブジェクトは、辞書(dict,他の言語でいうmap,hashmapなど連想配列)みたいにキーを指定して要素にアクセスすることができます。
item[‘title’] = ‘breaking: you are fired!’
ただし、Itemsオブジェクトはdictやmapと違い、定義されていないフィールドに値を追加することはできません。
Itemで定義されていないフィールドにアクセスしようとすると、例外が発生します。
Spiderの作成
次に、Spiderを作っていきます。
前回の記事で紹介した、Scrapyのgenspiderコマンドを使ってSpiderを生成してみましょう。
第1引数にSpiderの名前、第2引数にクロール対象のドメインを指定してgenspiderコマンドを実行します。
プロジェクトのspidersディレクトリに以下のようなtcspider.pyというファイルができているはずですので確認します。
1 2 3 4 5 6 7 8 9 10 11 |
# -*- coding: utf-8 -*- import scrapy class TcspiderSpider(scrapy.Spider): name = 'tcspider' allowed_domains = ['jp.techcrunch.com'] start_urls = ['http://jp.techcrunch.com/'] def parse(self, response): pass |
生成されたコードをベースに、必要な処理を加えていきます。
今回のサンプルで使うSpiderの完成品をまず以下に示しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import scrapy from tcproject.items import Article # ItemsからArticleクラスをインポートする class TcspiderSpider(scrapy.Spider): name = 'tcspider' # spiderの名前。実行時に指定する。 allowed_domains = ['jp.techcrunch.com'] # クロールを許可するドメインを指定する。 start_urls = ['http://jp.techcrunch.com/'] # クロールを開始するページのURLを指定する。 def parse(self, response): """ トップページから個別記事ページへのリンク文字列を抜き出して1つずつ順番に処理する """ for url in response.css('h2.post-title a::attr("href")').extract(): # ① yield scrapy.Request(url, self.parse_articles) # ② def parse_articles(self, response): """ 記事詳細ページからタイトルと本文を抜きだしてItemsに格納する """ item = Article() # items.pyで定義したArticleクラスのオブジェクトを作成 item['title'] = response.css('h1::text').extract_first() item['body'] = response.css('.article-entry.text').xpath('string()').extract_first() yield item |
サンプルの解説
まずgenspiderで生成されたspiderの一行目、utf−8とか書いているコメント行はPython3でScrapyを使う分には不要なので削除します。
①の箇所では、トップページから個別記事ページへのリンク文字列を抜き出し、1つずつ順番にparse_articlesに渡す処理を行います。
この行では、response.css(‘h2.post-title a::attr(“href”)’).extract()で取得した個別記事のURLのリストから、要素(url)を1つずつ取り出しforループにわたします。
そして②の行で、url(記事詳細ページへのリンクurl)をscrapy.Requestに渡し、レスポンス(記事詳細ページ)を自分自身のparse_articlesで処理します。
parse_articleメソッドでは、記事詳細ページからタイトルと本文を抜き出し、それぞれitemオブジェクトに格納して返します。
これを、トップページに表示されている記事一覧の数だけ繰り返します。
Spiderの実行
作成したSpiderは以下のコマンドで実行できます。
scrapy crawlコマンドに続いて実行するspiderの名前を指定し、
-o オプションで実行結果を保存する先のファイルを指定します。
実行結果の確認は以下のコマンドで。
1 2 3 |
{"title": "ロボットたちに、触れることを通して世界を学ぶことを教える","body":"\n\n\nゆっくりと、しかし、確実に、ロボットのBaxterは学んでいる。それは)\n\n\n\n\t\t\t\t\t\t\t\t.fb_iframe_widget{display: inline;}.social-flw .tw-follow{vertical-align:top;}\n\n\t\t\n\n\n TechCrunch Japanの最新記事を購読しよう\n \n \n \n !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');\n \n\n \n jQuery(function ($) {\n \t$('.social-button').popnSocialButton([ 'facebook', 'twitter', 'hatebu', 'gplus', 'pocket', 'linebu'], {\n \turl: $(location).attr(\"href\")\n \t});\n\t})\n \n\t\n\t\t\t\t\t\t\t"} {"title": "キヤノン、6D Mark IIを発表――フルサイズのデジタル一眼入門に絶好", "body": "\n\n\nキヤノンがフルサイズのデジタル一眼レフ、EOS 6D Mark IIを発表した。前モデルの\n\n\n\n\t\t\t\t\t\t\t\t.fb_iframe_widget{display: inline;}.social-flw .tw-follow{vertical-align:top;}\n\n\t\t\n\n\n TechCrunch Japanの最新記事を購読しよう\n \n \n \n !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');\n \n\n \n jQuery(function ($) {\n \t$('.social-button').popnSocialButton([ 'facebook', 'twitter', 'hatebu', 'gplus', 'pocket', 'linebu'], {\n \turl: $(location).attr(\"href\")\n \t});\n\t})\n \n\t\n\t\t\t\t\t\t\t"} {"title": "Lockheed MartinがスタートアップTerran Orbitalに投資してナノサテライトのブームに乗るつもり", "body": "\n\n\n航空宇宙産業のリーダーLockheed Martinが\n\n\n\n\t\t\t\t\t\t\t\t.fb_iframe_widget{display: inline;}.social-flw .tw-follow{vertical-align:top;}\n\n\t\t\n\n\n TechCrunch Japanの最新記事を購読しよう\n \n \n \n !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');\n \n\n \n jQuery(function ($) {\n \t$('.social-button').popnSocialButton([ 'facebook', 'twitter', 'hatebu', 'gplus', 'pocket', 'linebu'], {\n \turl: $(location).attr(\"href\")\n \t});\n\t})\n \n\t\n\t\t\t\t\t\t\t"} |
どうでしょう?うまくいけば、無事にタイトルと記事本文が取得できているはずです。
なお、記事本文以外にiframeタグの中身とかちょっといらんものも混じってしまうと思いますが、こういうのの処理はreadableとかhtml.parserなどを使えばよろしいかと。また次回にでも。
参考にしたもの
Webスクレイピングのノウハウを公開します
http://tech.respect-pal.jp/web-scraping/
scrapyのJSON出力を日本語にする方法
http://qiita.com/shiozaki/items/689713b4cfb869e7f54c