akiyoko blog

akiyoko の IT技術系ブログです

ゼロからはじめる Scrapy(AWS でお手軽データ分析 その1/3)

Python で Web クローリング・スクレイピングするためのツールといえば、 今や Scrapy が真っ先に候補に上がりますよね。


などを見ると、Beautiful Soup4pyquery も有力候補に見えますが、Google Trends を見てみるとやはり「Scrapy」が一番人気っぽいです。







目的

最終的には下図のように、Scrapy → Amazon S3 → Amazon Athena → Amazon QuickSight という流れで、AWS のいろいろなサービスを使ってお手軽データ分析をしてみたいと考えています。

f:id:akiyoko:20170311224916p:plain

今回は以下のように、Scrapy で Web スクレイピングして作成した CSVファイルを、S3 に格納するところまでを試してみます。

f:id:akiyoko:20170311224711p:plain:w400



まずは試しに、

http://www3.boj.or.jp/market/jp/stat/jx170301.htm
f:id:akiyoko:20170304235233p:plain

をスクレイピングしてみます。


 

事前準備

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 を使ってデータ分析していきたいと考えています。