lambdaからpython実行してS3とファイルをやり取りした

# やったこと
[前回](https://qiita.com/koob/items/3a00895b9fe15d422455)lambdaからpythonを定期実行できるようにしました。
（実行したら[この記事](https://qiita.com/koob/items/02079246c3269d92a362)に書き込まれる）

今回はそれをまた改良し、

- AWSのS3上にあるcsvファイルをダウンロード
- csvに自分の記事の最新情報(View, LGTM, ストック数)を書き込む
- csvをS3へアップロード

までやってみました。
これで毎日自動的にView, LGTM, ストック数のデータが溜まっていきます。

S3上でバケットを作ったりファイルをアップロードする方法についてはここでは特段説明しません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/16f550bc-560c-7cfd-321f-b4f900cfb368.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403060/124cef69-77ee-969a-f5d3-5f52e970172c.png)

# ソースコード
前回と比較して以下を追加しています。

- バケットやファイル名をlambda上で環境変数に設定し読み込み
- S3とのやり取りをする関数を追加
- 読み込みモジュールの追加

また、前回まではサードパーティー製のモジュールを使いたい場合、zipファイルにまとめてlambda上に直接あげていました。
今回はlambdaのレイヤーを使用したので、デプロイまでがかなり楽でした。
手順等はページ下部の参考欄の記事を参考にさせていただきました。

<details><summary>モジュール読み込み、メンバ変数など</summary><div>

``` python:lambda_function.py
import os
import http.client
import json
import requests
import datetime
import pytz
import pandas as pd
import boto3
import sys

TOKEN = os.environ['TOKEN'] # Read&Write用
HEADERS = {'content-type': 'application/json',
           'Authorization': 'Bearer ' + TOKEN}
URL_BASE = 'https://qiita.com/api/v2'
ARTICLE_ID = os.environ['ARTICLE_ID']
BUCKET = os.environ['BUCKET']
KEY = os.environ['KEY'] 
```
</div></details>

<details><summary>lambda_handler</summary><div>

``` python:lambda_function.py
def lambda_handler(event, context):
    list_iteminfo = get_info()
    res = update_article(list_iteminfo)
    res = update_csv(list_iteminfo)
```
</div></details>

<details><summary>記事一覧のLGTM, View, ストック数を取得する関数</summary><div>

``` python:lambda_function.py
def get_info():
    url_authenticate = URL_BASE + '/authenticated_user/items'
    # 記事一覧を取得
    res = requests.get(url_authenticate, headers=HEADERS)
    list = res.json()

    # 不要な記事を除外
    list_item = []
    for item in list:
        # 限定記事は対象外
        if item['private']:
            continue
        # 投稿先の記事は対象外
        if item['id'] == ARTICLE_ID:
            continue
        
        list_item.append(item)

    num = 0
    insert_ymd = datetime.date.today()
    list_iteminfo = [[0 for i in range(5)] for j in range(len(list_item))]
    for item in list_item:

        # 各種項目を取得
        id = item['id']
        title = item['title']
        url = item['url']
        likes_count = item['likes_count']
        update_ymdhms = item['updated_at']

        # 記事の情報を取得
        url_item = URL_BASE + '/items/' + id
        res = requests.get(url_item, headers=HEADERS)
        json = res.json()

        # タイトル別のview数のセット
        page_views_count = json['page_views_count']

        i = 1
        # stock数の取得（最大1000件）
        while i < 10:

            url_stock = url_item + '/stockers?page=' + str(i) + '&per_page=100'
            res_stock = requests.get(url_stock, headers=HEADERS)
            json_stock = res_stock.json()
            stock_num = len(json_stock)

            if stock_num != 100:
                stock_count = (i * 100) - 100 + stock_num
                break
            else:
                i += 1

        list_iteminfo[num] = [id, title, url, page_views_count, likes_count, stock_count, update_ymdhms, insert_ymd]
        num += 1

    return list_iteminfo
```
</div></details>

<details><summary>Qiitaの記事を更新する関数</summary><div>

``` python:lambda_function.py
def update_article(list_iteminfo):
    item = {
            'body': '',
            'coediting': False,
            'private': False,
            'tags': [{'name': 'qiita'}],
            'title': '投稿記事のLGTM, View, ストック数一覧'
            }

    # 本文の作成（[記事タイトル](URL), LGTM数, View数, ストック数）
    now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))
    setdate = now.strftime('%Y/%m/%d %H:%M:%S')
    body = 'この記事は [' + setdate + '] に更新されました。\r\n'
    for info in list_iteminfo:
        body += '\r\n[' + str(info[0]) + '](' + str(info[1]) + ')'
        body += '\r\nView：' + str(info[2]) + '件, LGTM：' + str(info[3]) + '件, ストック：' + str(info[4]) + '件\r\n'

    item["body"] += body
    url = URL_BASE + '/items/' + ARTICLE_ID

    # 記事の更新
    res = requests.patch(url, headers=HEADERS, json=item)

    return res
```
</div></details>

<details><summary>S3とのやり取りをする関数（New!）</summary><div>

``` python:lambda_function.py
def update_csv(list_iteminfo):
    # S3からQiita情報のcsvをメモリ上にロード
    s3_client = boto3.client('s3')
    body = s3_client.get_object(Bucket=BUCKET, Key=KEY)['Body']
    # dataframe作成
    df_csv = pd.read_csv(body, names=('id', 'title', 'url', 'page_views_count', 'likes_count', 'stock_count', 'update_ymdhms', 'insert_ymd'))
    df_new = pd.DataFrame(list_iteminfo, columns=['id', 'title', 'url', 'page_views_count', 'likes_count', 'stock_count', 'update_ymdhms', 'insert_ymd'])
    df = pd.concat([df_csv, df_new])
    df = df.reset_index()

    #csv更新用の文字列を作成
    CRLF = '\r\n'
    text = ''
    for row in df.itertuples():
        s = row.id + ',' + row.title + ',' + row.url + ',' + \
            str(row.page_views_count) + ',' + str(row.likes_count) + ',' + str(row.stock_count) + ',' + \
            str(row.update_ymdhms) + ',' + str(row.insert_ymd)

        if(text==''):
            text += s
        else:
            text += CRLF + s

    # csvをs3へアップロード
    s3_resource = boto3.resource('s3')
    obj = s3_resource.Object(BUCKET, KEY)
    return obj.put(Body = text)
```
</div></details>

実行時はlambda実行用IAMのロールに`AmazonS3FullAccess`など付与するのを忘れずに。

# 最後に
現在[こちら](https://qiita.com/koob/items/02079246c3269d92a362)で公開している記事に、自分の記事やLGTM数・ストック数などを一覧化して自動投稿しています。
次は自動的に蓄積されていくデータをグラフ化して、視覚的に推移が追えるような記事が投稿できるようにする予定です。

[2020/12/13 追記]
matplotlibなど使用すればView数/LGTM数/ストック数の推移を折れ線グラフ化することはできましたが、Qiita上にアップロードするには単純にpngをjsonにシリアライズして渡すだけでは当然ダメでした。（なっがいjsonがそのまま本文として表示されるだけ）

本文の中に画像を配置する為には
`![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/{数字の羅列}/{ハイフンを含む英数字の羅列}.png)`
のような形でテキストにして渡す必要があるのだと思いますが、これ色々どうやるんだろ…。
QiitaのS3にプログラムからアップロードしてそのurlを得るみたいなことってできるんですかね。
今のところView数以外ほぼ変動がないのでグラフ化する意味はたいしてないのですが、せっかくグラフ化したのにもやもや。
とりあえずS3への保存だけ続けて、データが溜まってきたら改めてグラフ化して別記事にまとめますかね。

# 参考
[AWS LambdaでPython外部ライブラリのLayerを作る前に](https://qiita.com/polarbear08/items/202752d5ffcb65595bd9)