AWS lambda を使ってWebスクレイピングしたった


# Background 
[AWS Lambda を使ってFizzBuzzしたった](https://qiita.com/satsukiya/items/f8fae1b4fccae0430985)の続き。
今回はスクレイピングで外部のWebページからデータを取得してみた。

# AWS Architecture
![archtecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/92a0214d-37f3-2125-e1ed-f89bc196b532.png)

- S3(データ保存)
- AWS Lambda(データ処理)
- Amazon EventBridge(定期実行)

の3つのサービスを使っています。

# Setting

### S3

データ保存用のバケットを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/e8934c1e-418a-9d49-84da-9d65990e1348.png" width="75%">

バケット名のみ入力して、その他の設定はデフォルトのままにします。(リージョンは適宜選択する。)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/a9abe3e7-cfe4-7470-13af-5b4c374192a2.png" width="75%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/dcc70294-4ff2-6bb9-05d8-9c25f711ee6b.png" width="75%">

バケットの作成は完了。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/b9319156-3caa-e081-49e1-525e6cf6519c.png" width="75%">

### Lambda

データ処理用のlambdaを作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/20907418-bfbd-9764-873a-23e0850920ff.png" width="75%">

一から作成、、、ではなく、
ここでは「設計図の使用」のなかの「s3-get-object-python」を使います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/fc56c702-da71-1f48-cebe-35632328e2de.png" width="75%">

関数名・ロール名を入力。
今回はS3にファイルをuploadするので「読み取り専用のアクセス」のポリシーテンプレートを削除します。
S3トリガーですが、バケット名には先ほど作成したバケットを入力します。

あとは、**プレフィックス - オプションには任意の文字を入力してください。**
ここで言っている任意の文字とは、lambdaにてファイルを作成するのですがファイル名の先頭と重複しない文字です。
もし、未入力や重複する文字を入力した場合は **lambdaへ無限ループにトリガーを作動させ、多額の料金が発生する:scream:** ことになるので重要です。

他の対処法としては、イベントタイプをコピーのみにするなどの制限を加えるのもいいと思います。

全ての入力が終わったあとは、「関数の作成」を押します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/6f19926e-e71d-e91e-ac97-3931f39fd134.png" width="75%">

テンプレートが作成されますが、このまま開発を進めてデプロイ、テストしても権限エラーが発生します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/2788e5c2-51d6-a210-7df4-9386e437f4af.png" width="75%">

##### S3の設定
まずS3を有効化させます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/0879aa26-fd01-87a2-f452-cb4fe1479080.png" width="75%">

S3でバケットのプロパティ内にあるイベント通知を見ると追加されているのが分かります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/d5f8bb19-7869-e159-1470-905398dc6179.png" width="75%">

##### ロールに権限の追加
IAM→ロールを選ぶとロールリストが表示されます。ここで先ほどlambda作成時に記載したロール名を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/d0a3a2a7-33d4-fbe8-895b-a8b6c4f8a115.png" width="75%">

何も考えずに「ポリシーをアタッチします」を押す。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/4f01e667-acf9-49ac-7dd4-5bac1549723b.png" width="75%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/4f01e667-acf9-49ac-7dd4-5bac1549723b.png" width="75%">

"LambdaFull"でフィルタをかけ、「AWSLambdaFullAccess」を選んで「ポリシーのアタッチ」を押します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/7d70f17d-d149-0bd4-9801-24f0e2fa8006.png" width="75%">

これで権限の追加が完了しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/126353b0-d44e-1044-9036-e99bd307c30d.png" width="75%">

##### 基本設定
メモリが小さいと処理落ちしました。
てことでメモリ:256MB、タイムアウトを10秒にセットします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/12618754-329f-e4ba-7c39-0a994a548aaa.png" width="75%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/92dcfc58-24c6-90f3-a300-49c15dc16aa5.png" width="75%">

これで完了。

# Development (ファイル送信)

S3バケットの送受信にはboto3パッケージを使います。 lambda作成時に`s3-get-object-python`を選ぶと付属でついてきます。最初からパッケージをuploadするとbote3自体の容量が大きく10MB以上になるので既存のものを使った方が良さげです。

```Python
import json
import urllib.parse
import boto3
import datetime

def lambda_handler(event, context):

    try:
        # Get the object from the event and show its content type
        s3 = boto3.resource('s3')
    
        bucket = '[バケット名]'    
        key = 'test_{}.txt'.format(datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))
        file_contents = 'Lambda test'  
        
        obj = s3.Object(bucket,key)
        obj.put( Body=file_contents )
        
    except Exception as e:
        print(e)
        raise e
```

あとはデプロイして、テストするとバケットにファイルがuploadされます。
あと、テストイベントの設定ですが空のjsonでも起動します。

# Development (Webスクレイピング)

スクレイピングするには、`requests` `beautifulsoup`がいるのですがpipでインストールしているパッケージをlambdaにuploadする必要があります。

方法はフォルダにpipでパッケージをインストールし、zipでフォルダを圧縮化します。
ここで実行ファイルを作成し、lambdaで書いたコードをコピーします。

```bash
mkdir packages
cd packages
pip install requests -t ./
pip install beautifulsoup -t ./
touch lambda_function.py
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/5fe00551-7977-1e7e-aedb-8b5441e2720b.png" width="75%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/80fa7e37-27fc-fbc7-a152-a4a10b5a4360.png" width="75%">

プロジェクト内にパッケージが置かれます。
で、`packages`配下のフォルダ・ファイルをひとつ上の階層の`articleStore`に移動させます。
その後で、デプロイしてテストするとS3にファイルが追加されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/90bd593e-b2ab-179c-6ce5-df7a439e1a82.png" width="75%">

あとは、webスクレイピングするだけです。
ここでは今日付の[毎日新聞の社説](https://mainichi.jp/editorial/)を取得してみます。

```Python
import json
import urllib.parse
import boto3
import datetime
from datetime import timedelta, timezone
import random
import os
import requests
from bs4 import BeautifulSoup

print('Loading function')

s3 = boto3.resource('s3')

def lambda_handler(event, context):
    # Get the object from the event and show its content type
    JST = timezone(timedelta(hours=+9), 'JST')
    dt_now = datetime.datetime.now(JST)
    date_str = dt_now.strftime('%Y年%m月%d日')

    response = requests.get('https://mainichi.jp/editorial/')

    soup = BeautifulSoup(response.text)
    pages = soup.find("ul", class_="list-typeD")

    articles = pages.find_all("article")
    
    links = [ "https:" + a.a.get("href") for a in articles if date_str in a.time.text ]
    
    for i, link in enumerate(links):
        bucket_name = "[バケット名]"
        folder_path = "/tmp/"
        filename = 'article_{0}_{1}.txt'.format(dt_now.strftime('%Y-%m-%d'), i + 1)
        
        try:
            bucket = s3.Bucket(bucket_name)
            
            with open(folder_path + filename, 'w') as fout:
                fout.write(extract_article(link))
            
            bucket.upload_file(folder_path + filename, filename)
            os.remove(folder_path + filename)
    
        except Exception as e:
            print(e)
            raise e

    return {
        "date" : dt_now.strftime('%Y-%m-%d %H:%M:%S')
    }

#社説を抽出
def extract_article(src):

    response = requests.get(src)
    soup = BeautifulSoup(response.text)

    text_area = soup.find(class_="main-text") 
    title = soup.h1.text.strip()
    sentence = "".join([txt.text.strip() for txt in text_area.find_all(class_="txt")])

    return title + "\n" + sentence
```

これで、「デプロイ」→「テスト」でS3バケットに抽出した記事が書かれた2つのテキストファイルが追加されます。

とても、長くなりましたがLambdaの設定は完了です。

# Amazon EventBridge

処理はできたのですが、毎朝「テスト」ボタンを押すのはマジだるです。
そのため、Amazon EventBridgeを使って定期実行を設定します。

Amazon EventBridge → イベント → ルール を選んで、
「ルールの作成」を押します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/97cbd45d-c6f2-205a-86eb-c0097e3e57ac.png" width="75%">

ルール名・説明を書き、cron式は標準時間で実行されるので`0 22 * * ? *`として日本時間午前7時に実行するようにします。
ターゲットで対象のlambda名を選び、作成します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/9322eaff-6dca-5b43-83d2-0ee4e32ee07d.png" width="75%">

これで完了。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288206/88b20422-2196-51a3-3a42-f3d4269d73b4.png" width="75%">

# Post-Scripting
この後の予定として、いくつかの新聞社の社説を1年分ストックして機械学習してみようと思っています。
`requests`で全てのページを取得できる場合はいいのですが、ページロード時にさらにロードして一覧表示しているサイト(例えば、朝日新聞)がある場合は `selenium`でブラウザをコントロールする必要があります。