AWS_Lambdaを用いた集計_前編

## 【はじめに】
本記事は、AWS LamdaおよびQuickSightを用いた、Qiita投稿記事投稿状況の可視化例をご紹介します。
記事内容に関しては前編と後編に分けてご紹介予定です。
※投稿状況の集計対象となるQiita投稿者アカウントのトークンを知っていることが前提となります。
※投稿状況の集計は、Qiitaが公開しているAPIを用います。

## 【実装内容】
今回の記事においては
・Lambda
・S3
・EventBridge
・QuickSight
・Glue
・Athena
上記AWSサービスを用いる。

最終的な実装イメージ図は以下の通り、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/1c7273bf-5a51-662d-3585-bd9b2e9bd285.png)



## 【前編での実装の流れ】
①S3バケットの作成
②Lambda関数の作成
③ETL確認作業
④EventBridgeの設定
前編の実装イメージ図は以下の通り、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/2b3bc2ba-85b9-8e3c-202b-f68627a48eca.png)


## 【実装】
### ①S3バケットの作成
S3バケットを作成する。
AWSコンソールのサービス名「S3」を選択し「バケット作成」を実行する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/a95b86e0-8320-6ced-b3c5-a5adc484c773.png)

バケット名を「qiita-data-collection」とし、バケットキーは「無効にする」を選択する。ほかの設定はデフォルトのまま「バケット作成」を押下する。

【バケット名設定】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/01289afd-9143-049c-280d-0b7ec38eec00.png)

【バケットキー設定】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/7089031b-ad22-002b-9ab3-2c717cac18bf.png)

【バケット作成後】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/7386819b-10ee-1637-de0d-899535bbd370.png)

### ②バケット内にオブジェクトを作成
QiitaのAPIを用いて取得したデータを格納するオブジェクトを作成する。
オブジェクトの構成は以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/78e3beaf-3544-2ca1-e402-c5f68617c66d.png)
また各オブジェクトに格納するファイルは以下とする。

【result】
一日ごとの記事情報を個人トークから取得し、JSONファイルとして格納する。
【tag】
タグ別の集計データCSVファイルを格納する。
【title】
タイトル別の集計データCSVファイルを格納する。
【daily】
resultに格納するJSONファイルを整形し、CSVファイルとして格納する。

### ③Lambda関数を作成
①Lambda関数内にconfigフォルダを作成し、Qiita_APIを使用し、投稿情報を取得するための個人メンバーのトークン情報をconfig.pyに記載し配置する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/ebcdd2b6-5db3-fb38-f32e-5a7d9990557f.png)
config.pyの内容は以下のような形
```
    qiita_token = {
        '氏名A'：'QiitaトークンA',
        '氏名B'：'QiitaトークンB',
        '氏名C'：'QiitaトークンC',
        ・
        ・
        ・
        '氏名Z'：'QiitaトークンZ'
    }
```
②Lambda関数においてpythonにてコード記述を行う。
　コード記述内容は以下の通り。
　①トークン情報からユーザーごとのQiita記事情報を取得する。
　②取得した記事情報をJSON形式でS3のresultオブジェクトに格納する。
　③取得した記事情報データを整形後CSVファイルとして保存し、
 　　各オブジェクトtag、title、dailyに格納する。
　④QuickSightのSPICEを更新する。

③Lambda関数のコード内容は下記の通り、
```
    from config import config
import json
import datetime
import pprint
import csv
import itertools
import pandas as pd
import os
import boto3
###Lambda側でレイヤー追加が必要
import requests

def lambda_handler(event, context):
    s3 = boto3.resource('s3')
    qs = boto3.client('quicksight')
    
    # 変数定義
    ##テーブル更新用
    today1 = (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime('%Y%m%d')
    today2 = (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime('%Y-%m-%d')
    BUCKET_NAME = 'qiita-data-collection'
    OBJECT_KEY_NAME = 'result/{}_result.json'.format(today1)
    bucket = s3.Bucket(BUCKET_NAME)
    obj = bucket.Object(OBJECT_KEY_NAME)
    
    ##SPICE更新用
    today3 = (datetime.datetime.utcnow() + datetime.timedelta(hours=9)).strftime('%Y%m%d%H%M%S')
    accountid='{#アカウントIDを記載する}'
    dsid_list={'title_qiita':'{quick_sightの#データセットID}','tag_qiita':'{quick_sightの#データセットID}','qiita_daily':'{quick_sightの#データセットID}'}
    
    #########qiita記事集計作業##################
  
    # # メンバー全員の投稿アカウント毎の個人トークンの定義
    tokens = config.qiita_token
    
    # https のパラメータ情報
    params = {
        "page": "1",
        "per_page": "100",
    }
    
    
    # 関数定義（投稿した記事のView数の取得）
    def get_view(jsonlist, headers):
    
        item_value=[]
    
        for i, item in enumerate(jsonlist):
            item_key=['User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'Title']
    
            res2 = requests.get('https://qiita.com/api/v2/items/' + item['id'], headers=headers)
            rec = json.loads(res2.text)
            
            tags = [value['name'] for value in rec['tags']]
            item_key.extend([f'tag_{idx+1}' for idx, value in enumerate(tags)])
            item_key.append('update_day')
                    
            data = [rec['user']['id'], rec['url'], rec['page_views_count'], rec['likes_count'], rec['created_at'], rec['updated_at'], rec['title']]
            data.extend([value for value in tags])
            #　update_day(取得日)に記事内容を取得した日本時間の日付を格納
            data.append(today2)
            
            item_value.append(dict(zip(item_key, data)))
        return item_value
    
    result = {}
    
    # 関数定義（取得した記事情報をjosn形式に整える）
    def get_post_list():
        
        for user, token in tokens.items() :
    
            # https のヘッダー情報 with 個人トークン
            headers = {
                "Authorization": "Bearer " + token
            }
    
            # 個人トークンを利用してのその個人の投稿記事情報の取得
            res1 = requests.get('https://qiita.com/api/v2/authenticated_user/items', params=params, headers=headers)
            jsonlist = json.loads(res1.text)
    
            result[user] = get_view(jsonlist, headers)
        
        
        r = obj.put(Body = json.dumps(result, ensure_ascii=False, indent=2))
        print("【記事情報】" + str(json.dumps(result)))
        print("【S3格納情報】】" + str(json.dumps(r)))

    # 関数実行
    get_post_list()
    
    ###############################################
    #########データ整形、s3格納作業##################
    
    # 実行結果を整える
    iterator_list = list(itertools.chain.from_iterable(result.values()))
    
    # json形式ファイルをデータフレームに格納
    df = pd.io.json.json_normalize(iterator_list)
    
    # データフレームにtag1~5までない場合にtagを追加する
    col =df.columns
    for i in range(5):
        if  'tag_' + str(i+1) not in col:
            df['tag_' + str(i+1)] = pd.Series()

    # データフレームのカラムの順序を入れ替える
    df = df.loc[:,[ 'Title','tag_1', 'tag_2', 'tag_3', 'tag_4', 'tag_5','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day']]
    df['truncate_at'] = pd.to_datetime(df['created_at']).dt.round('D')
    df = df.astype({'truncate_at':'str'})
    df['truncate_at']= df['truncate_at'].str[0:11]
    df['truncate_at'] = pd.to_datetime(df['truncate_at'])
    
    daily_df = df
    
    # 日々の推移データ用のs3にcsvを格納する
    d_output_file = 'qiita_' + today1 + '.csv'
    d_output_path = '/tmp/{}'.format(d_output_file)
    daily_df.to_csv(d_output_path, index=False, encoding='utf-8_sig', quoting=csv.QUOTE_ALL)
    bucket.upload_file(d_output_path, 'daily/' + '{}'.format(d_output_file))
    print(os.listdir('/tmp/'))
    for i in os.listdir('/tmp/'):
        os.remove('/tmp/'+i)
    print(os.listdir('/tmp/'))

    
    # タイトルの閲覧ランキング用のs3にcsvを格納する
    title_df = df
    t_output_file = 'title_qiita.csv'
    t_output_path = '/tmp/{}'.format(t_output_file)
    title_df.to_csv(t_output_path, index=False, encoding='utf-8_sig', quoting=csv.QUOTE_ALL)

    bucket.upload_file(t_output_path, 'title/' + '{}'.format(t_output_file))
    print(os.listdir('/tmp/'))
    for i in os.listdir('/tmp/'):
        os.remove('/tmp/'+i)
    print(os.listdir('/tmp/'))

    # タグごとに集計するために各タグのデータフレームを作成しユニオン
    df['post_counts'] =1
    df1 = df.loc[:,[ 'Title','tag_1','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day', 'truncate_at', 'post_counts']]
    df2 = df.loc[:,[ 'Title','tag_2','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day', 'truncate_at', 'post_counts']]
    df2 = df2.rename(columns = {'tag_2':'tag_1'})
    df3 = df.loc[:,[ 'Title','tag_3','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day', 'truncate_at', 'post_counts']]
    df3 = df3.rename(columns = {'tag_3':'tag_1'})
    df4 = df.loc[:,[ 'Title','tag_4','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day', 'truncate_at', 'post_counts']]
    df4 = df4.rename(columns = {'tag_4':'tag_1'})
    df5 = df.loc[:,[ 'Title','tag_5','User', 'url', 'page_views_count', 'likes_count', 'created_at', 'updated_at', 'update_day', 'truncate_at', 'post_counts']]
    df5 = df5.rename(columns = {'tag_5':'tag_1'})
    tag_df = pd.concat([df1,df2,df3,df4,df5])
    
    # tag_1 カラムの空白行を削除し、集計
    tag_df = tag_df.dropna(subset=['tag_1'])
    tag_df = tag_df.groupby(['tag_1','truncate_at'],as_index=False).sum()
    
    # # タグの閲覧ランキング用のs3にcsvを格納する
    g_output_file = 'tag_qiita.csv'
    g_output_path = '/tmp/{}'.format(g_output_file)
    tag_df.to_csv(g_output_path, index=False, encoding='utf-8_sig', quoting=csv.QUOTE_ALL)

    bucket.upload_file(g_output_path, 'tag/' + '{}'.format(g_output_file))
    print(os.listdir('/tmp/'))
    for i in os.listdir('/tmp/'):
        os.remove('/tmp/'+i)
    print(os.listdir('/tmp/'))
    
    ###############################################
    #########SPICE更新作業##################
    
    for l,n in dsid_list.items():
        print('更新テーブル:' + l)
        response = qs.create_ingestion(
            DataSetId=n,
            IngestionId= today3 + l,
            AwsAccountId= accountid
        )
        print(response)
```

④Lambda関数のテスト実行を行う。
項目名：コードソースの「Test」ボタンを押下することでLambda関数が実行される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/731d6f35-b730-ac4b-7084-792e63df6676.png)

⑤テスト後各S3のオブジェクトにデータが生成されていることを確認する。
【result】
以下の通り、日付ごとにデータが格納されている。
※画像は一日おきに実行されたもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/85858600-4c9b-b7e9-c637-27a59b5dc520.png)

jsonの内容に関しては以下の通り辞書型で作成されます。
※一部抜粋しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/baccbd8f-6d70-c489-49b6-e899206a1214.png)
<br></br>
【tag】
tagオブジェクト内にCSVファイルが作成されているか確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/c29af474-f743-ed2b-4d32-6eee6f44a9a2.png)

CSVファイルの中身は以下の構成で記事ごとに記載される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/2530ea95-ce42-2e98-0edb-85abe819ff0b.png)
<br></br>
【title】
titleオブジェクト内にCSVファイルが作成されているか確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/325955d9-2b68-f518-411d-7338705b757a.png)

CSVファイルの中身は以下の構成で記事ごとに記載される。
※項目が長いので分割して画像を添付する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/c1634a58-722e-2d4b-b474-9caf7b2fc9bd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/1e4213a4-d09b-9384-3546-2187cab5911c.png)

<br></br>
【daily】
dailyオブジェクト内にCSVファイルが作成されているか確認
※画像は一日おきに実行されたもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/f3dd8821-40ea-8b39-2b91-7cd1c1dbf0ac.png)

CSVファイルの中身は以下の構成で記事ごとに記載される。
※項目が長いので分割して画像を添付する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/43df2334-41e6-42e9-5a06-db56fb71d7f5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/80b6c8a9-a6a4-40ab-a0d5-e25ba9f109c9.png)

⑥EventBridgeの設定を行い、毎日午前9時にLambda関数が実行されるようにする。

「トリガーを追加」を押下する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/2f9c33d7-76e9-4866-7884-d2254bfa6a78.png)

トリガー設定画面が表示されるため、下記画像の通りに設定し、「追加」を押下する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/c858faba-57db-c3e8-f999-a73844730425.png)

トリガーの設定完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3528832/ba0a031a-e35f-2037-694e-158c8ca86f43.png)

## 【まとめ】
今回はAWS Lamdaを用いたQiita記事の投稿状況の集計例についてご紹介いたしました。
後編では、集計データを用いたQuickSightによる投稿状況の可視化例をご紹介します。

株式会社ジールでは、「ITリテラシーがない」「初期費用がかけられない」「親切・丁寧な支援がほしい」「ノーコード・ローコードがよい」「運用・保守の手間をかけられない」などのお客様の声を受けて、オールインワン型データ活用プラットフォーム「ZEUSCloud」を月額利用料にてご提供しております。
ご興味がある方は是非下記のリンクをご覧ください：
https://www.zdh.co.jp/products-services/cloud-data/zeuscloud/?utm_source=qiita&utm_medium=referral&utm_campaign=qiita_zeuscloud_content-area