【Python】AWS S3バケットにCSVファイルをアップロードしたり、データを読み込んだりする

#やりたいこと
タイトルのことをしたい。（と思い、できたので、備忘録としてまとめました）
背景として、AWS Lambda関数（WebAPI）からイベントをトリガーして、1日1回ファイルを更新する処理を作りたいと思いました。

try and error の経緯は、以下の感じです。

1. WebAPIの内部ファイルに書き込もうとする
2. `/tmp/`フォルダ内のファイルに書き込もうとする
3. S3バケットにファイルを保存してそれを参照する　←***今ここ***

1.では、そもそもWebAPIの内部ファイルはread onlyとなっており、書き込みできませんでした。
2.では、`/tmp/`フォルダがAWSにキャッシュとして存在しており、時間で（インスタンス消去時に？）消えてしまうため、後からファイルにアクセスできませんでした。

※ WebAPIのイベントの設定方法もまとめておりますので、ご参考ください。
[zappa経由でデプロイしたAWSLambda関数（WebAPI）にCloudWatch Eventsを設定する](https://qiita.com/nemutas/items/383a107c2dc146c98919)


#フロー
1. スクレイピングしたデータを、csvデータとして`/tmp/`フォルダに保存する
2. `/tmp/`フォルダに保存したcsvデータを、S3バケットにアップロードする
3. S3バケットのcsvデータを参照する

#スクレイピングしたデータを、csvデータとして`/tmp/`フォルダに保存する
以下のような形式のリストデータを保存します。
ここでは、コミックスの新刊情報を取得しています。

```.py
datas = [[data1], [data2], [data3], [data4],...]
```
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/cca59023-aa55-5fd3-1e9d-517891f19145.png)

----------------------------------------------------
書き込み処理

```.py
import csv

# キャッシュファイルパス
filepath = '/tmp/data.csv'

# ヘッダー
header = ['date', 'title', 'author', 'publisher', 'label']

# 書き込み
with open(filepath, 'w', newline='', encoding='utf-16', errors='ignore') as f:
    writer = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
    writer.writerow(header)
    writer.writerows(datas) # リスト形式のデータを一行ずつ書き込む
```
mode：'w'(書き込み・上書き)，'a'(書き込み・追記)，'r'(読み込み)
newline：改行設定
encoding：'utf-16'にすることで文字化けを回避できるようです。(https://akiyoko.hatenablog.jp/entry/2017/12/09/010411)
errors：エラーとなる行の回避設定
dialect：要素同士の区切り方
quoting：文字列化するデータ型の指定

----------------------------------------------------
作成した`data.csv`は、以下のようになります。
![無題2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b61c6765-f5e9-4eac-a7e5-abe444152ddb.png)

#`/temp/`フォルダに保存したcsvデータを、S3バケットにアップロードする
AWSコンソールで、S3バケットを作成します。
バケット名は、`release-comics`としました。

----------------------------------------------------
アップロード処理

```.py
import boto3

filepath = '/tmp/data.csv'
baket_name = 'release-comics'
savepath = 'data.csv'

s3 = boto3.resource('s3')
s3.meta.client.upload_file(filepath, baket_name, savepath)
```
バケット直下にcsvデータをアップロードするので、`savepath`はアップロードファイル名のみ指定しています。

#S3バケットのcsvデータを参照する

```.py
import csv
import boto3

baket_name = 'release-comics'
savepath = 'data.csv'

s3 = boto3.client('s3')
response = s3.get_object(Bucket=baket_name, Key=savepath)
lines = response['Body'].read().decode('utf-16').splitlines() # 改行で区切ってリスト化
reader = csv.DictReader(lines, dialect='excel-tab', quoting=csv.QUOTE_ALL)
datas = [row for row in reader]
```

#全体像

```sever.py
import s3
from flask import Flask, jsonify, request, abort

app = Flask(__name__)
app.config["JSON_AS_ASCII"] = False

# exp：http://127.0.0.1:5000/search
@app.route("/search")
def search():
    results = s3.read()
    return jsonify(results)

if __name__ == "__main__":
    app.run(debug=True, use_reloader=False)
```
```lambda_function.py
import os
import time, datetime
import s3, scraping

# Lambdaトリガーでスケジュール更新する
def update(event, context):
    print('------------------------------')
    today = datetime.datetime.fromtimestamp(time.time())
    t = today.strftime('%Y/%m/%d %H:%M:%S')
    date = t + '：' + 'called lambda_function.update'
    print(date)

    # スクレイピングをしてcsvデータを更新する（AWSのキャッシュフォルダに一時保存）
    filepath = scraping.analysis()
    print('update with scraping：' + filepath)

    # キャッシュフォルダからs3バケットに保存
    uploadpath = s3.upload(filepath)
    print('upload to s3：' + uploadpath)
    
    # キャッシュの削除
    os.remove(filepath)
    print('remove cache：' + filepath)
    print('------------------------------')
```

`print`は、AWSでログを確認する用です。

```scraping.py
import os
import re
import csv
import requests
from bs4 import BeautifulSoup

URI = 'https://hogehoge'
DATA_PATH = '/tmp/data.csv'

def analysis():
    # htmlデータの取得
    resp = requests.get(URI)
 
    # 全角半角変換
    zen = "".join(chr(0xff01 + i) for i in range(94))
    han = "".join(chr(0x21 + i) for i in range(94))
    zen += '　'
    han += ' '
    zen = zen.replace('～', '')
    han = han.replace('~', '')
    zen2han = str.maketrans(zen, han)
    # han2zen = str.maketrans(han, zen)

    pattern = re.compile(r'(\d+)月(\d+)日')

    # 解析
    soup = BeautifulSoup(resp.text, "html.parser")
    soup2 = soup.find_all("table", class_="stTableData01")

    datas = []
    for s2 in soup2:
        soup3 = s2.find("tbody")
        soup4 = soup3.find_all("tr")

        for s4 in soup4:
            line = [content.text.translate(zen2han) for content in s4.contents if content != '\n']

            if pattern.match(line[0]):
                datas.append(line)

    # データの書き込み
    header = ['date', 'title', 'author', 'publisher', 'label']

    with open(DATA_PATH, 'w', newline='', encoding='utf-16', errors='ignore') as f:
        writer = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
        writer.writerow(header)
        writer.writerows(datas)
```

説明に載せていないコードがありますが、やっていることは、`datas`リストを作成して、`/tmp/data.csv`に保存しています。

```s3.py
import os
import csv
import boto3

BAKET_NAME = 'search-comics'
SAVE_FILE = 'data.csv'

def upload(filepath):
    s3 = boto3.resource('s3')
    s3.meta.client.upload_file(filepath, BAKET_NAME, SAVE_FILE)
    return os.path.join(BAKET_NAME, SAVE_FILE)

def read():
    s3 = boto3.client('s3')
    response = s3.get_object(Bucket=BAKET_NAME, Key=SAVE_FILE)
    lines = response['Body'].read().decode('utf-16').splitlines() # 改行で区切ってリスト化
    reader = csv.DictReader(lines, dialect='excel-tab', quoting=csv.QUOTE_ALL)
    return [row for row in reader]
```

#確認
AWSコンソールで、動作の確認をします。

- S3バケットで、`data.csv`ファイルの更新日時の確認
- CloudWatchで、ログの確認

#参考
[csv文字化け対策](https://akiyoko.hatenablog.jp/entry/2017/12/09/010411)
[S3バケットから取得したCSVファイルを読み込むには？](https://stackoverrun.com/ja/q/12866746)
[【Lambda】python で s3 を使ってテキストの読み書きをしてみる](https://www.dev-dev.net/entry/2018/07/24/130727)

pandasを使う場合
[Lambda : s3のcsvファイルをpandasで変換し自動でDynamoへ格納する](https://qiita.com/MorinibuTakeshi/items/65b80e04a49cb24db4ec)