🎵

推しが歌枠で歌ったあの歌が聞きたい人へ(YouTubeAPIによるタイムスタンプの取得)

2022/06/23に公開

YouTubeAPIを使って、動画のコメント欄、概要欄にあるタイムスタンプ付きコメントを取得してみました。

コードはこちら
https://github.com/eteeeeeerminal/yt-timestamp-scraper

推しの歌枠の特定の歌を聴くには

技術の話が見たい人はこの章をスキップしてください

私は作業のお供にお歌配信(歌枠)を良く聞きます、で、ときどき、歌枠で歌われた特定の歌を、後から聞き直したいなぁって思うわけです。
どの配信かさえ分かっていれば、ファンの人が、タイムスタンプ(特定の時間から動画を再生するリンク)を残してくれてたりするので、それを見つければいいわけです。
しかし、どの配信かも分からないときって、探しようがないんですよね。どうにかしたい。

で、実際歌枠の特定の歌を聞く方法は以下の通り

  1. タイムスタンプをたどる
    • どの配信で歌われたか分かっているときはこれ
    • 1動画内ならコメントの検索ができるのでそれで見つける
  2. 有志による切り抜きを視聴する
    • 星街すいせいさん等、有志が1曲単位で小分けにした動画がある場合はこれ
  3. 有志のサービスを利用する
    • 有志が歌枠で歌われた各曲へのリンクをサイトにまとめて、検索できるようにしている場合はこれ
    • ググると、ホロライブさん関連のものがいくつか出てくる
  4. 人力で1つ1つ動画を開く
    • 有志によるデータベースはさすがになくとも、コメント欄にタイムスタンプが残されていることは多い。つまり、1つづつ動画ページを開いてコメ欄を検索すれば……
    • 論外、今回はこれを自動でやりたい

YouTubeAPI でやってみた

そんなわけで、YouTubeAPI を使って、

  • 特定チャンネルの動画一覧を取得
  • 動画のコメント欄・概要欄を取得
  • タイムスタンプ付きコメントを抽出して保存
  • あとは、ctrl+f検索なりアプリにするなりご自由な方法で検索
    していく

詳細な実装はリポジトリを参照。以下ではざっくりとだけ説明する

ちなみに、APIを使わなくても、スクレイピングが合法的にできる場合もあるが、難解な利用規約を解読できる上級者向けなので、おすすめしない

https://developers.google.com/youtube/v3/getting-started?hl=ja

コードに関して、こちらの記事も参考にしました
https://zenn.dev/yorifuji/articles/youtube-data-api-python

APIキーをゲット

なにはともあれ、APIキーを入手しなければ始まらない
手持ちの Google アカウントで、Google Cloud Platform にアクセスしてポチポチすればキーがもらえる

API キーの取得はこの辺を参考に
https://qiita.com/shinkai_/items/10a400c25de270cb02e4

Python で API を叩いてみる

Web API なんて、何で叩いてもいいじゃないかと思うんだけど、スクレイピングなら慣れた Python が楽で早いってことで、Python を使う

環境構築

API のラッパーと dotenv を入れます

pip install google-api-python-client
pip install python-dotenv

ラッパーの詳細は以下、なんだかんだラッパー使った方が楽
https://github.com/googleapis/google-api-python-client

dotenv は APIキーをコードにベタ書きしたくないので使う
ここ とか参照

↓こんな感じにやれば、あとは、youtube.hogehoge(params)的なふうにAPIが叩ける

from apiclient import discovery
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv('API_KEY')
youtube = discovery.build('youtube', 'v3', developerKey = API_KEY)

まずは, 推しの投稿動画のIDを全部収集

実は、アップロード済み動画というのは、1つのプレイリストとして扱われている。
アップロード済み動画のプレイリストIDで、PlaylistItems を叩くとよい。

で、そのプレイリストのIDはというと、Channels を叩く。
Channnels を叩くためのIDは、チャンネルのURLからぶっこ抜く

例えば、下記太字の部分
https:// www.youtube.com/channel/UCI0vsOl2fo-h44RfA6ATUjw

プレイリストIDを取得するには、以下のように叩く

response = youtube.channels().list(
    part="contentDetails",
    id="UCI0vsOl2fo-h44RfA6ATUjw",
    fields="items/contentDetails/relatedPlaylists/uploads"
).execute()

part は欲しい物を書くパラメータ, 今回は contentDetails を指定
さらに、fields を指定することで、返ってくるパラメータを細かく決められるので、
ここに必要最小限欲しい物を書くことで、APIの使用量を抑えられる
この辺は、公式リファレンスの下の方で、試し打ちできるので、そちらで試したのちに実装すると良い

で、帰ってきたのはこちら

{'items': [{'contentDetails': {'relatedPlaylists': {'likes': '', 'uploads': 'UUI0vsOl2fo-h44RfA6ATUjw'}}}]}

これで、アップロード済み動画のプレイリストIDがUUI0vsOl2fo-h44RfA6ATUjwであることがわかった。

ちょっと待って。

チャンネルID: UCI0vsOl2fo-h44RfA6ATUjw
プレイリストID: UUI0vsOl2fo-h44RfA6ATUjw

プレイリストIDって、チャンネルIDの先頭のUCUUにしただけじゃん……

てことで、貴重なAPIを叩くんじゃなくて、先頭のUCUUに置き換えるか

各動画のコメ欄を取得

プレイリストIDが分かったので、動画情報を一気にとっていく
↓みたいな関数をぶん回せばいい、VideoInfo は実装上扱いやすくするための dataclass

def get_video_info_in_playlist(playlist_id: str) -> list[VideoInfo]:
    # 参考: https://zenn.dev/yorifuji/articles/youtube-data-api-python
    video_info_list = []

    request = youtube.playlistItems().list(
        part="snippet",
        maxResults=100,
        playlistId=playlist_id,
        fields="nextPageToken,items/snippet(title,description,resourceId/videoId)"
    )

    while request:
        response = request.execute()
        video_info_list.extend(list(map(VideoInfo.from_response_snippet, response["items"])))
        request = youtube.playlistItems().list_next(request, response)

    return video_info_list

動画 IDが取れたので、コメントを一気にとっていく
コメントは, CommentThreads を叩いて取得する
普通のコメントと、リプライコメントが別になっているので、partで、snippet,repliesと両方指定しておく

今回は、コメントの中身だけが大事なので、↓のように、普通のコメントもリプライも CommentInfo クラスに押し込んでいく

def get_comments(video_id: str) -> list[CommentInfo]:
    comment_list = []

    comment_field = "snippet(videoId,textDisplay,textOriginal)"
    top_comment_f = f"items/snippet/topLevelComment/{comment_field}"
    replies_f = f"items/replies/comments/{comment_field}"

    request = youtube.commentThreads().list(
        part="snippet,replies",
        maxResults=100,
        videoId=video_id,
        fields=f"nextPageToken,{top_comment_f},{replies_f}"
    )

    while request:
        response = request.execute()
        c_mat = list(map(CommentInfo.response_item_to_comments, response["items"]))
        comment_list.extend(sum(c_mat, []))
        request = youtube.commentThreads().list_next(request, response)

    return comment_list

取得したデータは適当にjsonで保存。後でこねくり回す。

タイムスタンプの抽出

肝心のコメントデータが手に入ったので、データをそのままctrl+fしてもお目当てのタイムスタンプを見つけることができる。
ただ、さすがに、生データを検索するのは使い勝手が悪いので、タイムスタンプに相当する部分だけ抜き出していく。

タイムスタンプコメント

コメントの生データ例は以下の通り

生データ例

{
"text_display": "☆タイムスタンプ☆\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h00m00s">0:00:00</a> 配信開始\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h01m39s">0:01:39</a> こんりーな\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h04m05s">0:04:05</a> 名前読み\r
\r
◇セットリスト\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h06m01s">0:06:01</a> メーベル\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h09m11s">0:09:11</a> 幽霊東京\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h12m37s">0:12:37</a> 春を告げる\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h15m52s">0:15:52</a> 夜のピエロ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h19m13s">0:19:13</a> 夜に駆ける\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h23m20s">0:23:20</a> マーシャル・マキシマイザー\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h26m50s">0:26:50</a> 激動\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h31m44s">0:31:44</a> CORE PRIDE\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h36m05s">0:36:05</a> レディメイド\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h40m00s">0:40:00</a> サラマンダー\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h42m40s">0:42:40</a> 踊\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h46m13s">0:46:13</a> Mela!\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h50m11s">0:50:11</a> 正しくなれない\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h54m39s">0:54:39</a> 秒針を噛む\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h58m33s">0:58:33</a> 君の知らない物語\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h04m13s">1:04:13</a> からくりピエロ \r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h08m59s">1:08:59</a> Calc\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h13m00s">1:13:00</a> p.h.\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h15m45s">1:15:45</a> ボトム\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h19m17s">1:19:17</a> エバ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h23m05s">1:23:05</a> ポッカデラベリタ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h26m27s">1:26:27</a> フィクサー\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h30m28s">1:30:28</a> ビースト・ダンス\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h33m47s">1:33:47</a> 惑星ループ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h37m02s">1:37:02</a> 太陽系ディスコ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h40m23s">1:40:23</a> ダーリンダンス\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h43m45s">1:43:45</a> ベノム\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h47m02s">1:47:02</a> ルマ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h50m07s">1:50:07</a> カブトムシ\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h55m20s">1:55:20</a> カワキヲアメク\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=1h59m34s">1:59:34</a> フリージア\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=2h06m23s">2:06:23</a> you\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=2h12m20s">2:12:20</a> お富さん\r
\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=2h21m09s">2:21:09</a> スパチャお礼\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=2h21m48s">2:21:48</a> 名前読み\r
<a href="https://www.youtube.com/watch?v=wfkmjpymbmE&t=2h23m07s">2:23:07</a> 海産!",
"text_original": "☆タイムスタンプ☆\r\n0:00:00 配信開始\r\n0:01:39 こんりーな\r\n0:04:05 名前読み\r\n\r\n◇セットリスト\r\n0:06:01 メーベル\r\n0:09:11 幽霊東京\r\n0:12:37 春を告げる\r\n0:15:52 夜のピエロ\r\n0:19:13 夜に駆ける\r\n0:23:20 マーシャル・マキシマイザー\r\n0:26:50 激動\r\n0:31:44 CORE PRIDE\r\n0:36:05 レディメイド\r\n0:40:00 サラマンダー\r\n0:42:40 踊\r\n0:46:13 Mela!\r\n0:50:11 正しくなれない\r\n0:54:39 秒針を噛む\r\n0:58:33 君の知らない物語\r\n1:04:13 からくりピエロ \r\n1:08:59 Calc\r\n1:13:00 p.h.\r\n1:15:45 ボトム\r\n1:19:17 エバ\r\n1:23:05 ポッカデラベリタ\r\n1:26:27 フィクサー\r\n1:30:28 ビースト・ダンス\r\n1:33:47 惑星ループ\r\n1:37:02 太陽系ディスコ\r\n1:40:23 ダーリンダンス\r\n1:43:45 ベノム\r\n1:47:02 ルマ\r\n1:50:07 カブトムシ\r\n1:55:20 カワキヲアメク\r\n1:59:34 フリージア\r\n2:06:23 you\r\n2:12:20 お富さん\r\n\r\n2:21:09 スパチャお礼\r\n2:21:48 名前読み\r\n2:23:07 海産!"
}

ご覧の通り、text_displayの方には、欲しいリンクがそのまま a タグで埋め込まれているので、これを取得できればいい。
あと、ついでにタイムスタンプの横の曲名も

てことで、上記のようなコメントを以下の正規表現チェッカーにぶちこんで、望みのテキストが得られるまでコネコネする。
https://pythex.org/

結局、コメントを改行で区切ってから以下の正規表現を適用することで、リンク、00:00:00 を抽出した。
抽出したあと、リンク、00:00:00 を削除することで、曲名も抽出できた。

<a href=\\\"(https://www.youtube.com/watch\?v=[\w]+\&amp;t=[\w]+)\\\">([\d:]+)</a>

最終的に手に入ったデータは以下の通り。
これなら、ctrl+fでもなんとか運用できそう。

得られたデータ抜粋
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h23m20s",
    "timestamp": "0:23:20",
    "text": "マーシャル・マキシマイザー"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h26m50s",
    "timestamp": "0:26:50",
    "text": "激動"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h31m44s",
    "timestamp": "0:31:44",
    "text": "CORE PRIDE"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h36m05s",
    "timestamp": "0:36:05",
    "text": "レディメイド"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h40m00s",
    "timestamp": "0:40:00",
    "text": "サラマンダー"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h42m40s",
    "timestamp": "0:42:40",
    "text": "踊"
},
{
    "video_id": "wfkmjpymbmE",
    "link": "https://www.youtube.com/watch?v=wfkmjpymbmE&t=0h46m13s",
    "timestamp": "0:46:13",
    "text": "Mela!"
},

課題

  • 複数行にまたがるふざけたタイムスタンプ
    • 今回、1行に00:00 hogehoge 00:00だけ書かれている場合だけを対象にした
    • なので、1. 00:00\n hogeのようなものには対応できていない。
  • web アプリへの応用
    • 今回、Python で雑に書いたが、webアプリにできたらみんな使えていいと思う
    • その場合、APIの使用量を節約するために、
      • タイトルにが入ってる動画のコメントだけ収集
      • 一度コメントを取得できた動画に関してはスキップする
    • 等の工夫も必要だろう

まとめ

そんな感じで、推しの歌枠で聞いたあの曲どこで聞いたかなぁを解決してみました。
思ったよりもYouTube API簡単に叩けましたね。
みなさんも、Vtuber の推し活にYouTube APIを活用してみてください。ノシ

Discussion