YouTubeで指定したチャンネルに投稿された動画の情報を自動で集める

# やったこと（概要）

1. 指定したチャンネルに投稿された動画リストを取得し、API経由で動画情報を取得してS3に保存するスクリプトの作成
1. 上記を毎日0時(UTC)に定期実行するようにlambdaを設定

# やったこと(詳細)

## 1. 動画情報を取得するスクリプトの作成

大まかに、下記の流れで動画情報を取得します
1. チャンネルIDから、そのチャンネルに投稿された動画が全て入っているプレイリスト（「アップロード動画」）のIDを取得
2. プレイリストIDから、そのプレイリストに含まれる動画のIDを取得
3. 2.で得られた動画IDのリストについて、順次APIから動画情報を取得して保存

なお、今回利用するYouTube Data API v3について、最初の登録に当たっては下記を参考にしました
（一年前にやったので状況が変わっている可能性もあります）
https://qiita.com/moshisora/items/4ea23d5abd7b4d852955

### 1.1 「アップロード動画」のIDを取得

YouTube Data API v3を使う際は、基本的に下記の3つを指定することで必要な情報を取得します

* APIの種別 (チャンネル、プレイリスト、動画 など）
* データの指定方法 （各種IDなど)
* データの種類(part) (snippet, contentDetailsなど)

今回はchannel IDからアップロード動画(uploads)のIDが必要なので下記を選択しました

* APIの種別: channels
* データの指定方法: id (channel_id)
* データの種類(part): contentDetails

これらのパラメータをGETで渡せばuploadsのIDを取得することが可能です。
どのAPIに何を渡すと何が得られるかは公式ドキュメントを参考にしてください。
各APIについて、「概要」にpartで何を指定すると何が得られるか、「list」にデータの指定をどうするかが記載してあります。
channels API: https://developers.google.com/youtube/v3/docs/channels?hl=ja

なお、APIのrate limitに至るまでの利用可能回数は指定したpartによっても変動するため、不要なpartは記載しない方がrate limit上は望ましいです。

上記指定について、requestsモジュールを使うと下記のようにかけます

```python

import requests

def get_channel_info(channel_id):
    channel_url = 'https://www.googleapis.com/youtube/v3/channels'
    param = {
        'key': 【Browser Key】
        , 'id': channel_id
        , 'part': 'contentDetails'
        # チャンネル名がとりたい場合はsnippet, 登録数がとりたい場合はstatisticsも指定する
        #, 'part': 'snippet, contentDetails, statistics'
    }

    req = requests.get(channel_url, params=param)
    return req.json()

channel_info = get_channel_info(channel_id)

# これを次で使う
playlist_id = channel_info["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
```

基本的にchannel IDはそのチャンネルのURLに含まれる文字列です。
例えば、 https://www.youtube.com/channel/UCllKI7VjyANuS1RXatizfLQ の場合、`UCllKI7VjyANuS1RXatizfLQ`がchannel IDになります。

しかし、チャンネルurlは現状下記の3つが存在しており、タイプによって対応を変える必要があります。

1. https://www.youtube.com/channel/xxx <br> この場合は前述のようにchannel/xxxのxxxの部分channel IDになるのでそれを使って↑の要領でAPIに渡してください。
2. https://www.youtube.com/user/yyy <br> この場合はuser/yyyのyyyの部分はusernameと呼ばれており、APIを呼び出す際にidをforUsernameに差し替えるとチャンネル情報の取得ができます。コードはこの後示します。
3. https://www.youtube.com/c/zzz <br> の場合は現状APIからchannel_idの取得はできないようです。しかし、「youtube上のリンクから」該当のチャンネルにアクセスすると、1.の形式でurlがわかるのでそこからchannel_idの取得自体は可能です。
4. https://www.youtube.com/@handle <br> YouTubeハンドルを使っている場合、YouTubeのページ内から確認する方法はないように思います。videosのlistAPIにpart=snippetを渡すと、その戻り値からチャンネルIDを取得可能なので、対象のチャンネルに投稿されている動画を一つ選んでAPIを実行するとよさそうです。
YouTube Data APIの公式リファレンスからテスト実行可能なので、こちらを使うのがお手軽です
https://developers.google.com/youtube/v3/docs/videos/list

2.のパターンの場合は、下記のようなコードでデータが取得可能です。

```python

import requests

def get_channel_info(user_name):
    channel_url = 'https://www.googleapis.com/youtube/v3/channels'
    param = {
        'key': 【Browser Key】
        , 'forUsername': user_name # ここを変える
        , 'part': 'contentDetails'
        # チャンネル名がとりたい場合はsnippet, 登録数がとりたい場合はstatisticsも指定する
        #, 'part': 'snippet, contentDetails, statistics'
    }

    req = requests.get(channel_url, params=param)
    return req.json()

channel_info = get_channel_info(user_name)

# これを次で使う
playlist_id = channel_info["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
```


また、参考までに先ほど例にしめしたチャンネルIDの場合、APIの戻り値としてどのような結果になるかを下記に示します。
partとしてはcontentDetailsだけではなく、snippetやstatisticsも含まれています。
kind, etag, idはpartに記載しなくても入ってきます。

```UCllKI7VjyANuS1RXatizfLQ.json
{
  "kind": "youtube#channelListResponse",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/H539w7-hq_pSS8ne7t58G7iIBbc\"",
  "pageInfo": {
    "totalResults": 1,
    "resultsPerPage": 1
  },
  "items": [
    {
      "kind": "youtube#channel",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/7lsjQIBo8ElaiiNhg9LX5OAT0Qw\"",
      "id": "UCllKI7VjyANuS1RXatizfLQ",
      "snippet": {
        "title": "山神 カルタ / Karuta Yamagami",
        "description": "みなさまはじめまして\nにじさんじ所属バーチャルライバー 山神カルタと申します。\n普段は修行をしているので山の中にいることが多いです。\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\nにじさんじ所属バーチャルライバー 山神 カルタ\n\n見習いの烏天狗。\n立派な天狗となるべく、日々山で修業に勤しむ。\n休みの日は、内緒で人里まで降りたりしているとか、いないとか…\n翼は自由に出し入れできる(万能)\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/",
        "publishedAt": "2019-08-26T10:03:13.000Z",
        "thumbnails": {
          "default": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s88-c-k-c0xffffffff-no-rj-mo",
            "width": 88,
            "height": 88
          },
          "medium": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s240-c-k-c0xffffffff-no-rj-mo",
            "width": 240,
            "height": 240
          },
          "high": {
            "url": "https://yt3.ggpht.com/a/AGF-l7-9QUEt-T7wZaw_pYIUYckwhdVXbAkHcuNZHw=s800-c-k-c0xffffffff-no-rj-mo",
            "width": 800,
            "height": 800
          }
        },
        "localized": {
          "title": "山神 カルタ / Karuta Yamagami",
          "description": "みなさまはじめまして\nにじさんじ所属バーチャルライバー 山神カルタと申します。\n普段は修行をしているので山の中にいることが多いです。\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\nにじさんじ所属バーチャルライバー 山神 カルタ\n\n見習いの烏天狗。\n立派な天狗となるべく、日々山で修業に勤しむ。\n休みの日は、内緒で人里まで降りたりしているとか、いないとか…\n翼は自由に出し入れできる(万能)\n\n⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨⌒¨\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/"
        },
        "country": "JP"
      },
      "contentDetails": {
        "relatedPlaylists": {
          "uploads": "UUllKI7VjyANuS1RXatizfLQ",
          "watchHistory": "HL",
          "watchLater": "WL"
        }
      },
      "statistics": {
        "viewCount": "979371",
        "commentCount": "0",
        "subscriberCount": "44300",
        "hiddenSubscriberCount": false,
        "videoCount": "70"
      }
    }
  ]
}
```

### 1.2 プレイリストIDから、動画のIDを取得

今回はplaylist IDから、そこに含まれる動画のリストが必要なので下記を選択しました

* APIの種別: playlistItems
* データの指定方法: playlistId
* データの種類(part): snippet, contentDetails

また、指定したチャンネルが多数の動画をアップロードしていた場合に備えて下記のパラメータを指定します

* maxResults: 50 (一度のAPI呼び出しで得られる結果(今回は動画)の上限、50が最大値)
* pageToken: 一度のAPI呼び出しで結果を全て得られなかった場合に、続きからデータを取得する際に指定


```python

def get_playlist_info(playlist_id, pageToken):
    playlist_url = 'https://www.googleapis.com/youtube/v3/playlistItems'
    param = {
        'key': 【Browser Key】
        , 'playlistId': playlist_id
        , 'part': 'contentDetails'
        # 動画のタイトルとかもとりたい場合はsnippetも指定する
        #, 'part': 'snippet, contentDetails'
        , 'maxResults': '50'
        , 'pageToken': pageToken
    }

    req = requests.get(playlist_url, params=param)
    return req.json()


# 動画IDのリスト格納用の変数
uploaded_video_id_list = []

# pageTokenに空文字列を渡すと何も指定していないのと同じになる
pageToken = ""
while True:
    playlist_result = get_playlist_info(playlist_id, pageToken)

    # 今までの結果と今回の結果をマージする
    uploaded_video_id_list += [ item["contentDetails"]["videoId"] for item in playlist_result["items"] ]

    # 残りのアイテム数がmaxResultsを超えている場合はnextPageTokenが帰ってくる
    if "nextPageToken" in playlist_result:
        pageToken = playlist_result["nextPageToken"]
    else:
        break
```

1.1で取得したuploadsに入っている動画を取得すると下記のようになります

```json
{
  "kind": "youtube#playlistItemListResponse",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/rZQivmgvfKhGrH-c9M2Vvk9d2G8\"",
  "nextPageToken": "CDIQAA",
  "pageInfo": {
    "totalResults": 71,
    "resultsPerPage": 50
  },
  "items": [
    {
      "kind": "youtube#playlistItem",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/LlKDbwqU0P3jM4uaTlSNqAe1RcA\"",
      "id": "VVVsbEtJN1ZqeUFOdVMxUlhhdGl6ZkxRLm95NVIxc0IycmRN",
      "snippet": {
        "publishedAt": "2020-01-13T10:21:23.000Z",
        "channelId": "UCllKI7VjyANuS1RXatizfLQ",
        "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
        "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー　\n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど！💌\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "山神 カルタ / Karuta Yamagami",
        "playlistId": "UUllKI7VjyANuS1RXatizfLQ",
        "position": 0,
        "resourceId": {
          "kind": "youtube#video",
          "videoId": "oy5R1sB2rdM"
        }
      },
      "contentDetails": {
        "videoId": "oy5R1sB2rdM",
        "videoPublishedAt": "2020-01-13T10:21:23.000Z"
      }
    },
    : (略)
    {
      "kind": "youtube#playlistItem",
      "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/YpEY5UumudZmpMS82RezGcBMidg\"",
      "id": "VVVsbEtJN1ZqeUFOdVMxUlhhdGl6ZkxRLjBTY3Zzalhnbmo0",
      "snippet": {
        "publishedAt": "2019-11-13T13:57:12.000Z",
        "channelId": "UCllKI7VjyANuS1RXatizfLQ",
        "title": "【練習】うたのれんしゅう【山神カルタ】",
        "description": "練習つきあってくれ～～～\n\n____\n\nにじさんじ所属バーチャルライバー　\n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n\n配信タグ→#山神カタル\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\n【Pixiv fanbox】\nhttps://www.pixiv.net/fanbox/creator/24113985\n\n【にじさんじ公式Twitter】\nhttps://twitter.com/nijisanji_app\n《@nijisanji_app 》\n\n【にじさんじ公式HP】\nhttps://nijisanji.ichikara.co.jp/\n\n#にじさんじ　#山神カルタ",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/0ScvsjXgnj4/hqdefault.jpg",
            "width": 480,
            "height": 360
          }
        },
        "channelTitle": "山神 カルタ / Karuta Yamagami",
        "playlistId": "UUllKI7VjyANuS1RXatizfLQ",
        "position": 49,
        "resourceId": {
          "kind": "youtube#video",
          "videoId": "0ScvsjXgnj4"
        }
      },
      "contentDetails": {
        "videoId": "0ScvsjXgnj4",
        "videoPublishedAt": "2019-11-13T13:57:12.000Z"
      }
    }
  ]
}
```



### 1.3 動画IDのリストから、動画情報を取得し、保存する

今回はvideo IDから、その動画の情報を取得したいので下記を選択しました

* APIの種別: videos
* データの指定方法: playlistId
* データの種類(part): snippet


```python

def get_video_list(video_id_list):
    video_url = 'https://www.googleapis.com/youtube/v3/videos'
    param = {
        'key': 【Browser Key】
        , 'id': ','.join(video_id_list)
        , 'part': 'snippet'
    }

    req = requests.get(video_url, params=param)
    return req.json()

# 取得する動画について順番にAPIを叩いていく
# uploaded_video_id_listにvideo IDが格納されている前提
# maxResultsに合わせて50単位でループを回していく
for start_index in range(0, len(uploaded_video_id_list), 50):
    end_index = min(start_index + 50, len(new_video_id_list))
    video_list_result = get_video_list(uploaded_video_id_list[start_index:end_index])

    # APIの戻り値の["items"]の各要素がそれぞれの動画を表すので、ループ処理する
    for item in video_list_result["items"]:
       # ここでitemを保存したりする
       print(item)
```

実際に取得できるjsonは下記のようになります

```json
{
  "kind": "youtube#video",
  "etag": "\"OOFf3Zw2jDbxxHsjJ3l8u1U8dz4/kPHQ3DgOEmVwpAmO1NtWiI3J1t0\"",
  "id": "oy5R1sB2rdM",
  "snippet": {
    "publishedAt": "2020-01-13T10:21:23.000Z",
    "channelId": "UCllKI7VjyANuS1RXatizfLQ",
    "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
    "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー　\n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど！💌\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ",
    "thumbnails": {
      "default": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/default.jpg",
        "width": 120,
        "height": 90
      },
      "medium": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/mqdefault.jpg",
        "width": 320,
        "height": 180
      },
      "high": {
        "url": "https://i.ytimg.com/vi/oy5R1sB2rdM/hqdefault.jpg",
        "width": 480,
        "height": 360
      }
    },
    "channelTitle": "山神 カルタ / Karuta Yamagami",
    "categoryId": "20",
    "liveBroadcastContent": "none",
    "localized": {
      "title": "【Splatoon2】ウデマエC-からの脱出【にじさんじ/山神カルタ】",
      "description": "脱出劇\n\nサムネ→椛ウム(@momizi_umu)さま\n____\n\nにじさんじ所属バーチャルライバー　\n見習い烏天狗の山神カルタです。\n普段は山で修行をしています。\nいろんな時間をみんなと一緒に過ごせたら嬉しいなあ。\n____\n\n【Twitter】\n https://twitter.com/Karuta_Yamagami\n\n【お問い合わせやプレゼントはこちら】\nhttps://nijisanji.ichikara.co.jp/contact/\n\n【公式オンラインショップ】\nhttps://nijisanji.booth.pm/\n\nおてがみなど！💌\n〒175-0082\n東京都板橋区高島平6-2-1 ネットデポ高島平内\nいちから株式会社 山神カルタ宛\n\n#山神カルタ"
    }
  }
}
```

ここまででYouTube Data APIからのデータ取得は完了なのですが、定期実行をlambdaで実現する都合上、データの保存先をS3にすると取り回しやすいので、S3保存用のスクリプトも作成します。
参考: https://qiita.com/sokutou-metsu/items/5ba7531117224ee5e8af

```python

import boto3
import json


s3 = boto3.resource('s3')
bucket_name = 【バケット名】
bucket = s3.Bucket(bucket_name)


def save_to_s3(bucket, path_string, contents):

    obj = bucket.Object(path_string)
    body = json.dumps(contents, ensure_ascii=False, indent=2)

    response = obj.put(
        Body=body.encode('utf-8'),
        ContentEncoding='utf-8',
        ContentType='text/plane'
    )

    return response

save_to_s3(bucket, 【保存するパス】, 【保存したい変数】)

```

上で作ったスクリプトと合わせると下記のようになります。

```python

import boto3
import json

s3 = boto3.resource('s3')
bucket_name = 【バケット名】
bucket = s3.Bucket(bucket_name)

def save_to_s3(bucket, path_string, contents):

    obj = bucket.Object(path_string)
    body = json.dumps(contents, ensure_ascii=False, indent=2)

    response = obj.put(
        Body=body.encode('utf-8'),
        ContentEncoding='utf-8',
        ContentType='text/plane'
    )

    return response

def get_video_list(video_id_list):
    video_url = 'https://www.googleapis.com/youtube/v3/videos'
    param = {
        'key': 【Browser Key】
        , 'id': ','.join(video_id_list)
        , 'part': 'snippet'
    }

    req = requests.get(video_url, params=param)
    return req.json()

# 取得する動画について順番にAPIを叩いていく
# uploaded_video_id_listにvideo IDが格納されている前提
# maxResultsに合わせて50単位でループを回していく
for start_index in range(0, len(uploaded_video_id_list), 50):
    end_index = min(start_index + 50, len(new_video_id_list))
    video_list_result = get_video_list(uploaded_video_id_list[start_index:end_index])

    # APIの戻り値の["items"]の各要素がそれぞれの動画を表すので、ループ処理する
    for item in video_list_result["items"]:
        # videoID.jsonという名前で保存する
        video_id = item["id"]
        save_to_s3(bucket, video_id + ".json", item)
```

## 2. 1.で作ったスクリプトをlambda経由で定期実行させる

1.で作ったスクリプトをlambda経由で定期実行させます。
各種サービスの連携にあたってはAWS SAMを使用しました。

AWS SAMを使うのが初めてだったので、下記の流れで目標に近づけていきました
1. インストール/チュートリアルをやる
2. 自動でlambdaが起動するように設定
3. 動画情報取得スクリプトをlambdaに組み込む

### 2.1 インストール/チュートリアル

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started.html のHello Worldのサンプルまでやっていきました。

余談ですが、途中、awsコマンドが入ってないことに気づき `pip3 install awscli` したのですが、brewでインストールしたpython3でpipしたモジュールにPATHを通すのに苦戦しました。結果、下記設定で通せました。

```bash
export PATH=/Users/xuser/Library/Python/3.6/bin/:$PATH
```

参考: https://stackoverflow.com/questions/26574232/aws-cli-path-settings
(最も評価が高い回答ではないので注意）

### 2.2 自動でlambdaが起動するように設定

https://qiita.com/hf7777hi/items/e68948c948eb303b1900 を参考にして、Hello Worldが定期実行できるようにtemplate.yamlを修正しました。チュートリアルだとAPI Gatewayとの連携が入っているのですが、それは今回不要なのでこのタイミングで削除しました

### 2.3 動画情報取得スクリプトをlambdaに組み込む

下記3箇所を修正します
1. モジュール名(チュートリアルのままならhello_world)/app.py
2. モジュール名(チュートリアルのままならhello_world)/requirements.txt
3. template.yaml

その上で、下記コマンドを叩けばlambda（と他に必要なものあればそこにも）デプロイされるはずです

```bash

$ sam build
$ sam deploy --guided
```

#### 2.3.1 app.pyの修正

ベタで書いていた部分(もしくは、__main__等に書いていた部分）を `lambda_handler` という関数の中に入れれば終わりです。
チュートリアルのままだとimport文が過不足していると思われるので適宜追加してください

#### 2.3.2 requirements.txt

`pip freeze > requirements.txt` とかで使うrequirements.txtと同じです。boto3など必要なライブラリを記載してください。チュートリアルのままであれば `requests` が最初から入っているかと思いますので、その感じで記載すればokです。

lambdaで外部ライブラリを使用する際はライブラリをzipで固めてアップロード。。。みたいな作業が必要なのですが、aws-sam-cliを使うとそのあたりをよしなにやってくれるので、あまり気にしなくても動きます

#### 2.3.3 template.yaml

こんな感じにします。
※Descriptionは消しました

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 900

Resources:
  HelloWorldFunction:
    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: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: '2012-10-17' # Policy Document
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject # 保存だけなら不要
                - s3:GetObjectACL # 保存だけなら不要
                - s3:PutObject
                - s3:PutObjectACL
              Resource: 'arn:aws:s3:::【バケット名】/*'
        # 保存だけなら次のPolicyは不要
        - Version: '2012-10-17' # Policy Document 
          Statement:
            - Effect: Allow
              Action:
                - s3:ListBucket
              Resource: 'arn:aws:s3:::*'
      Events:
        Run:
          Type: Schedule
          Properties:
            Schedule: cron(0 0 * * ? *)
            Name: run-schedule
            Description: trigger for collecting videos
            Enabled: True

Outputs:
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
```

ポイントはPoliciesで必要な権限を付与するところかと思います。
チュートリアルだとPoliciesの設定を特にしていないのですが、その場合デフォルトで`AWSLambdaBasicExecutionRole` が付与されるのでそれは残しつつ、S3関連のものを追加しました。

保存だけであればPutObject関連だけで良いと思われますが、実際に動かしているスクリプトではデータの取得やバケットに入ったファイルのリストも行なっているので追加でGetObjectやListBucketの権限も付与しています。

また、cronに関してはUTCでの時刻になるので日本時間基準で設定する場合は時差を考慮する必要があります。

この修正に当たっては下記を参考にしました

* template.yamlの書き方:<br> https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#schedule
* s3のポリシー関連:<br> https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/using-with-s3-actions.html

# まとめと所感

YouTube Data APIを使用したデータ取得の流れから、それを定期実行させるところまでを説明しました。
YouTube Data APIに関しては公式ドキュメントが比較的わかりやすいのと、OAuth等も不要で取得方法がシンプルなので比較的とっつきやすいAPIかと思います。とっつきやすさの割に動画の再生数やコメントなど使いたくなりそうなものは概ね取得できるので、使っていて楽しいAPIだなと感じています。

また、分析用データを収集しようとすると何らかの定期実行システムが必要になることもありますが、lambdaやS3を活用することでサーバを実際に借りるよりも安価な情報の取得・保持が可能となります。lambdaの設定をコンソールでぽちぽちやるとだるい部分も多いのですが、SAM(というかcloudformation）がいい感じにラップしてくれるのでその部分の負担も下がってきています。データの取得はやりたいことの本質じゃないことが多いので、その部分がサクッと組めると分析が楽しくできて良いのかなと思います。