Python で Web クローリング・スクレイピングするためのツールといえば、 今や Scrapy が真っ先に候補に上がりますよね。
- Pythonでクローリング・スクレイピングに使えるライブラリいろいろ - orangain flavor
- 「Pythonクローリング&スクレイピング」という本を書きました - orangain flavor
- PythonとBeautiful Soupでスクレイピング - Qiita
などを見ると、Beautiful Soup4 や pyquery も有力候補に見えますが、Google Trends を見てみるとやはり「Scrapy」が一番人気っぽいです。
目的
最終的には下図のように、Scrapy → Amazon S3 → Amazon Athena → Amazon QuickSight という流れで、AWS のいろいろなサービスを使ってお手軽データ分析をしてみたいと考えています。
今回は以下のように、Scrapy で Web スクレイピングして作成した CSVファイルを、S3 に格納するところまでを試してみます。
まずは試しに、
http://www3.boj.or.jp/market/jp/stat/jx170301.htm
をスクレイピングしてみます。
事前準備
Mac に Scrapy をインストールします。
使用している Python は 2系ですが、Scrapy 1.1 以降は Python 3 対応済み(ただし Windows はサポート外)なので、3系でも特に変わりはないはずです。
前提条件
ローカル環境は以下の通りです。
- MacOS Sierra 10.12.3
- Python 2.7.12 (Anaconda 4.2.0)
インストール
pip でインストールします。
$ pip install scrapy Scrapy (1.3.3)
プロジェクト作成
ひな形を作成
scrapy startproject コマンドを実行して、プロジェクトのひな形を作成します。
$ cd ~/PycharmProjects $ scrapy startproject marketstat $ cd marketstat $ tree . ├── marketstat │ ├── __init__.py │ ├── items.py │ ├── middlewares.py │ ├── pipelines.py │ ├── settings.py │ └── spiders │ └── __init__.py └── scrapy.cfg
Item を作成
まずはじめに、ひな形として作成された items.py を修正していきます。
Item クラスは、スクレイピングしたデータを入れるための箱になります。
以下のようなデータ項目を作成することにします。
項目名 | 変数名 |
---|---|
日付 | date |
項目 | name |
予想値 | expected_value |
速報値 | preliminary_value |
確報値 | confirmed_value |
marketstat/items.py
# -*- coding: utf-8 -*- import scrapy class MarketstatItem(scrapy.Item): date = scrapy.Field() name = scrapy.Field() expected_value = scrapy.Field() preliminary_value = scrapy.Field() confirmed_value = scrapy.Field()
Spider を作成
次に、以下のコマンドを実行して、Spider のひな形を作成します。
$ cd marketstat $ scrapy genspider boj www3.boj.or.jp
作成された Spider のひな形を以下のように修正します。
marketstat/spiders/boj.py
# -*- coding: utf-8 -*- from datetime import datetime import scrapy from pytz import timezone from ..items import MarketstatItem class BojSpider(scrapy.Spider): name = "boj" allowed_domains = ["www3.boj.or.jp"] date = datetime.now(timezone('Asia/Tokyo')).strftime('%y%m%d') start_urls = [ 'http://www3.boj.or.jp/market/jp/stat/jx{date}.htm'.format(date=date), ] def parse(self, response): for sel in response.css('table tr'): item = MarketstatItem() item['date'] = self.date item['name'] = sel.css('td::text').extract_first() item['expected_value'] = sel.css('td:nth-last-child(3)::text').extract_first() item['preliminary_value'] = sel.css('td:nth-last-child(2)::text').extract_first() item['confirmed_value'] = sel.css('td:nth-last-child(1)::text').extract_first() yield item
parse メソッドを修正して、スクレイピングしたデータを格納した Item オブジェクトを yeild するようにします。
ちなみに、scrapy shell コマンドを実行することで、ipython が立ち上がってスクレイピングのデバッグ実行ができるため、目的の Webページの CSS要素を特定するのに便利です。
$ scrapy shell http://www3.boj.or.jp/market/jp/stat/jx170301.htm ・ ・ In [1]: print(response.css('table tr')[1].css('td::text')[0].extract()) 銀行券要因
(参考)http://qiita.com/ttomoaki/items/05d3dc104a695f939d63
なお、
sel.css('td::text')[0].extract()
とせず、
sel.css('td::text').extract_first()
とすることで、指定した要素が見つからない場合に IndexError が発生せずに None が返されるので非常に使い勝手がよいです。また、「::text」を付けないと HTMLタグが含まれた状態で値が取れてしまうので、これも必須のテクでしょう。
(参考)Scrapy Tutorial — Scrapy 1.5.0 documentation
スクレイピング実行
crawl コマンドの引数に Spider名(今回の例では「boj」)を指定します。
なお、-o オプションで出力するファイル名を指定することができます。
$ scrapy crawl boj -o test.csv
test.csv
date,expected_value,preliminary_value,name,confirmed_value 170301,,,, 170301,-600,-600,銀行券要因, 170301,"-11,100","-13,300",財政等要因, 170301,"-11,700","-13,900",資金過不足, 170301,"9,000","9,000",国債買入, ・ ・
ここで、settings.py に
############### # OUTPUT FILE # ############### FEED_FORMAT = 'csv' FEED_URI = '/Users/akiyoko/tmp/test.csv'
と書いておけば、-o オプションをいちいち指定する必要はありません(どちらも指定した場合は、-o オプションの方が優先されるようです)。
またここで、作成した Spider のインスタンス変数名を「FEED_URI」のプレースホルダとして使うことも可能です。
FEED_URI = '%(date)s.csv'
S3 に格納する方法
S3 に格納するのも簡単です。
例えば「marketstat」というバケットに格納するなら、settings.py に
############### # OUTPUT FILE # ############### FEED_FORMAT = 'csv' FEED_URI = 's3://marketstat/%(name)s/%(date)s.csv' ########### # AWS KEY # ########### AWS_ACCESS_KEY_ID = 'AKIXXX' AWS_SECRET_ACCESS_KEY = 'XXXXX'
とすればよいです。簡単ですね。
Spider に任意の引数を渡す
ちょっとした小技ですが、-a オプションを指定することで、Spider に任意の引数を渡すことができます。
(参考)スパイダー — Scrapy 1.2.2 ドキュメント
$ scrapy crawl boj -a date=170303
Spider 側からは、__init__ メソッド内で引数を受け取ることができます。
また、__init__ 内でインスタンス変数を上書きすることで、parse メソッドが実行される前にインスタンス変数の値を書き換えておくことが可能です。
marketstat/spiders/boj.py
class BojSpider(scrapy.Spider): name = "boj" allowed_domains = ["www3.boj.or.jp"] date = datetime.now(timezone('Asia/Tokyo')).strftime('%y%m%d') start_urls = [ 'http://www3.boj.or.jp/market/jp/stat/jx{date}.htm'.format(date=date), ] def __init__(self, date=None, *args, **kwargs): super(BojSpider, self).__init__(*args, **kwargs) if date is not None: self.date = date self.start_urls = [ 'http://www3.boj.or.jp/market/jp/stat/jx{date}.htm'.format(date=self.date), ] def parse(self, response): for sel in response.css('table tr'): item = MarketstatItem() item['date'] = self.date item['name'] = sel.css('td::text').extract_first() item['expected_value'] = sel.css('td:nth-last-child(3)::text').extract_first() item['preliminary_value'] = sel.css('td:nth-last-child(2)::text').extract_first() item['confirmed_value'] = sel.css('td:nth-last-child(1)::text').extract_first() yield item
シェルを作成
日付を範囲指定してクローリングするなら、例えば以下のようなシェルを作成すればよいでしょう。
#!/bin/bash START_DATE=170220 END_DATE=170301 for (( DATE=$START_DATE ; $DATE <= $END_DATE ; DATE=`date -j -v+1d -f %y%m%d $DATE +%y%m%d` )) ; do echo $DATE scrapy crawl boj -a date=$DATE done
(参考)
なお、大量のリクエストが発生する場合は、settings.py に
DOWNLOAD_DELAY = 3 # or 30 ?
などと設定しておきましょう。
(参考)Settings — Scrapy 1.5.0 documentation
まとめ
今回、知識ゼロから Scrapy を使って Web スクレイピングを試してみました。
実際使ってみて、仕組み(どこを触ればいいか)さえ分かってしまえば、実装部分も少なくて非常に便利に使えることが実感できました。
次は、S3 に格納した CSVファイルを Amazon Athena と QuickSight を使ってデータ分析していきたいと考えています。