AWS lambda+scrapyで定期的にサーバレスなスクレイピング その1.8


はじめに

先に結論を書いておくと、Lambda上で動かすところまで行けませんでした。
他の方法のあてはあるので、そちらがうまくいったら追記、もしくは別記事としてあげようと思います。

今回は、前回(その1)作成したweather_spider.pyをAWSlambdaに乗せてサーバレスで実行できるようにしていきます。
前回からだいぶ時間が空いてしまいましたが、理由は後ほど・・・。

目標

Lambdaを使用して、Yahoo!天気(東京)のデータを6時間おきに取得する。

方法

今回は、サーバレスアプリケーションモデル(SAM)を使用して、lambda諸々を構築していきます。

SAMについてはこちらをご覧ください。

以下はawsコマンド,samコマンドが実行できることを前提としています。

やってみる

1.SAM Projectの作成(sam init)

今回はPython3.7で実装していくため、runtimeにpython3.7を指定してsam initします。

$ sam init --runtime python3.7
[+] Initializing project structure...

Project generated: ./sam-app

Steps you can take next within the project folder
===================================================
[*] Invoke Function: sam local invoke HelloWorldFunction --event event.json
[*] Start API Gateway locally: sam local start-api

Read sam-app/README.md for further instructions

[*] Project initialization is now complete

こんな感じで一瞬でsam projectが作成されます。

フォルダ構成は以下。


sam-app
├── README.md
├── events
│   └── event.json
├── hello_world
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-37.pyc
│   │   └── app.cpython-37.pyc
│   ├── app.py
│   └── requirements.txt
├── template.yaml
└── tests
    └── unit
        ├── __init__.py
        ├── __pycache__
        │   ├── __init__.cpython-37.pyc
        │   └── test_handler.cpython-37.pyc
        └── test_handler.py

sam-app直下に前回作成したyahoo_weather_crawlをコピー

$ cp -r yahoo_weather_crawl sam-app/

$ cd sam-app/
$ ls
README.md       hello_world     tests
events          template.yaml       yahoo_weather_crawl

2.weather_spider.pyを修正

lambdaからキックできるように、handlerを追加します。

spider/weather_spider.py

# -*- coding: utf-8 -*-
import scrapy
from yahoo_weather_crawl.items import YahooWeatherCrawlItem
from scrapy.crawler import CrawlerProcess

# spider
class YahooWeatherSpider(scrapy.Spider):

    name = "yahoo_weather_crawler"
    allowed_domains = ['weather.yahoo.co.jp']
    start_urls = ["https://weather.yahoo.co.jp/weather/jp/13/4410.html"]

    # レスポンスに対する抽出処理
    def parse(self, response):
        # 発表日時
        yield YahooWeatherCrawlItem(announcement_date = response.xpath('//*[@id="week"]/p/text()').extract_first())
        table = response.xpath('//*[@id="yjw_week"]/table')

        # 日付ループ
        for day in range(2, 7):

            yield YahooWeatherCrawlItem(
                # データ抽出
                date=table.xpath('//tr[1]/td[%d]/small/text()' % day).extract_first(),
                weather=table.xpath('//tr[2]/td[%d]/small/text()' % day).extract_first(),
                temperature=table.xpath('//tr[3]/td[%d]/small/font/text()' % day).extract(),
                rainy_percent=table.xpath('//tr[4]/td[%d]/small/text()' % day).extract_first(),
                )

    # lambda handler
    def lambda_handler(event,context):
        process = CrawlerProcess({
            'FEED_FORMAT': 'json',
            'FEED_URI': '/tmp/result.json'
        })

        process.crawl(YahooWeatherCrawler)
        process.start()
        print('crawl success')

3.template.yamlの修正

先ほどsam initコマンドで作成されたtamplate.yamlを修正します。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Yahoo weather crawler template on SAM

Globals:
  Function:
    Timeout: 3

Resources:
  WeatherCrawlerFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./yahoo_weather_crawl/spiders
      Handler: weather_spider.lambda_handler
      Runtime: python3.7
      Events:
        WeatherCrawlEvent:
          Type: Schedule
          Properties:
            #毎日6時間おきに実行
            Schedule: cron(0 */6 * * ? *)

ここでは、Eventsに毎日六時間おきに実行するcronを仕込んで、
Cloudwatch eventsから起動するようにします。

4.build(デプロイするモジュールをまとめる)用shellの作成

AWS上にデプロイする際に必要なモジュールをbuildという名前のフォルダに纏めます。

が、その前に、今回scrapyをインポートしてPythonを実行しますが、scrapyの依存ライブラリ
の中に、lxmlというライブラリが有ります。

pip install scrapyとすると、lxmlも自動的にインストールはしてくれるのですが、
Python3.7ランタイムのAWS Lambdaにアップロードすると、そのままではモジュールを読み込むことができません。
(ここに苦戦して、かなり時間がかかりました・・・。)

そこで、今回はこちらの記事で作成した、秘伝のタレ(EC2上でコンパイルしたlxmlライブラリ、詳しくは記事参照)をlibという名前のフォルダに保存しておき、buildシェル内でbuildフォルダにコピーするようにします。

build.sh
# build

dir=yahoo_weather_crawl

echo '仮想環境を作成します'
python3 -m venv .venv

echo '仮想環境を有効化します'
. .venv/bin/activate

rm -rf ${dir}/build

# buildフォルダの作成
echo '${dir}をbuildします'
mkdir ${dir}/build

# buildフォルダにpip install
echo 'requirements.txtからpip installします'
pip3 install -r ${dir}/requirements.txt -t ${dir}/build

# libフォルダからbuildフォルダへコピー
echo 'libフォルダからbuildフォルダへ必要モジュールをコピーします'
cp -rf ./lib/* ${dir}/build

# pyファイルのコピー
echo 'pyファイルをbuildフォルダにコピーします'
cp -f ${dir}/*.py ${dir}/build
cp -f ${dir}/spiders/*.py ${dir}/build

# echo '仮想環境を無効化します'
deactivate

echo 'ビルドが完了しました'

5.sam-deploy用のshellを作成

コマンドからデプロイできるようにdeploy用のshellを作成します。

deploy.sh

# build
echo 'YahooWeatherCrawlerをビルドします'
sh build.sh

# templateをuploadするためのS3バケットの作成
# バケット名は世界中で一意にする必要があるので、コピペで作成する場合はバケット名を変更してください。
if  aws s3 ls "s3://weather-crawl-bucket" 2>&1 | grep -q 'NoSuchBucket' ; then
    echo "weather-crawl-bucketを作成します。"
    aws s3 mb s3://weather-crawl-bucket
else
    echo "weather_crawl-bucketを空にします。"
    aws s3 rm s3://weather-crawl-bucket --recursive
fi

# デプロイ用パッケージの作成
# 作成されたパッケージをS3にuploadします。作成したバケット名を指定してください。
echo "デプロイ用のパッケージを作成します。"
aws cloudformation package --template-file template.yaml \
--output-template-file output-template.yaml \
--s3-bucket weather-crawl-bucket

# デプロイ
aws cloudformation deploy --template-file output-template.yaml \
--stack-name weather-crawler \
--capabilities CAPABILITY_IAM

6.デプロイ

sam-app $sh deploy.sh
..
Successfully created/updated stack - weather-crawler

7.実行

AWSコンソール上に移動して、実行!

今度は別のモジュールのImportError...
Macのローカル上でビルドするのは少し大変そうなので別の方法を考えたいと思います。

終わりに

毎週Qiitaに記事を投稿すると決めてから一月以上経ちましたが、
今年は結局三記事しかかけませんでした。(一回ハマると抜け出せない!)

来年も?引き続き頑張っていきますので、よろしくお願いいたします。