最近VTuberにハマりまして今まで縁が無かったライブ配信を頻繁に見るようになりました。ライブ配信は動画のコメントとは別に視聴者から投稿されたチャットを読むことができるんですが、これが配信者との掛け合いに発展したりして結構面白いんですよね。
このチャットをデータとして取得できれば特定時間内の件数や感情分析などでライブ配信の盛り上がり度を算出したり、内容を要約すれば動画を見なくても内容の把握ができたりするかもしれないと思い付きました。
と言う事で、今回はこのチャットデータをpythonで抜き出す方法を3種類まとめてみました。それぞれ特徴がありましたがこの記事を読めばある程度網羅できると思います。
まとめるとこんな感じ
チャットデータの取得方法
今回ご紹介する下記3種類のチャット取得ツールは配信中かアーカイブかによって挙動が微妙に異なります、簡単にまとめるとこんな感じ。
ツール | 生配信 | アーカイブ |
yt-dlp | △(実行時点から投稿されたチャットのみ) | ◎(全部取れる) |
pytchat | △(〃) | ○(上位チャットのみ) |
Live Streaming API | △(〃) | × |
配信中のライブチャットに関してはいずれの方法でも実行時点以降のコメントしか拾う事ができません。なので、全チャットを取得するにはライブが配信開始になった瞬間にチャットを取得する処理を走らせるような工夫が必要になります。
一方、アーカイブ化されたライブ配信はyt-dlpかpychatを使えば簡単に取得できますんで、コピペで動く簡単なスクリプトを用意しました。
yt-dlpを使う
基本的には動画をダウンロードするためのCLIツールなんですが、チャットデータを取得するオプションもあったりします。チャットデータを単体で取得するというよりはライブ配信のアーカイブをダウンロードするついでに、チャットデータもセットで取得するみたいな使い方が多いのかなと思います。
yt-dlpの使い方は「pythonでYouTubeをダウンロードするyt-dlpのコピペコード14本まとめ」で解説していますが、チャットを取得するスクリプトはこんな感じ。
from yt_dlp import YoutubeDL
ydl_video_opts = {
'outtmpl' : '%(id)s'+'_.mp4',
'format' : 'best',
'writesubtitles' : True,
'skip_download' : True
}
with YoutubeDL(ydl_video_opts) as ydl:
result = ydl.download([
'https://www.youtube.com/watch?v=xIPQdO53PDA'
])
動画のダウンロードは行わずチャットのみ取得するオプションにしてみました。動画もセットでダウンロードしたい場合は「’skip_download’ : True」を消せばOKです。
今ハマっているvtuberの大代真白さんのアーカイブ配信でテストしてみます、時間は1:12:41ありますがvtuberの配信としては短めですね。
取得できるデータはこんな感じ。
{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "replayChatItemAction": {"actions": [{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "addChatItemAction": {"item": {"liveChatViewerEngagementMessageRenderer": {"id": "CiMKIVJFUExBWV9WRU0yMDIyLzEyLzA5LTE0OjUyOjEwLjUwMA%3D%3D", "timestampUsec": "1670626330500342", "icon": {"iconType": "YOUTUBE_ROUND"}, "message": {"runs": [{"text": "Live chat replay is on. Messages that appeared when the stream was live will show up here."}]}, "trackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o"}}}}], "videoOffsetTimeMsec": "0"}}
{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "replayChatItemAction": {"actions": [{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "addChatItemAction": {"item": {"liveChatPaidMessageRenderer": {"id": "ChwKGkNJZVV5NFBDNV9zQ0ZRc3NyUVlkQ1hzRkdn", "timestampUsec": "1670416416135913", "authorName": {"simpleText": "ジェットニードル"}, "authorPhoto": {"thumbnails": [{"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s32-c-k-c0x00ffffff-no-rj", "width": 32, "height": 32}, {"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s64-c-k-c0x00ffffff-no-rj", "width": 64, "height": 64}]}, "purchaseAmountText": {"simpleText": "¥5,000"}, "message": {"runs": [{"text": "いや、まだ早い!う〇こと判断するにはまだ早い!でっかい馬糞ウニかもしれない!!"}]}, "headerBackgroundColor": 4290910299, "headerTextColor": 4294967295, "bodyBackgroundColor": 4293467747, "bodyTextColor": 4294967295, "authorExternalChannelId": "UCUg0L9DgBqshuz4vu6MlEPw", "authorNameTextColor": 3019898879, "contextMenuEndpoint": {"clickTrackingParams": "CBIQ7rsEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "commandMetadata": {"webCommandMetadata": {"ignoreNavigation": true}}, "liveChatItemContextMenuEndpoint": {"params": "Q2g0S0hBb2FRMGxsVlhrMFVFTTFYM05EUmxGemMzSlJXV1JEV0hOR1IyY2FLU29uQ2hoVlEwWkhOblJsWVhCYVlVNDJTakZ2Vmxoc04wMVpVRUVTQzJvd09UaHZOekF5TkZOeklBRW9BVElhQ2hoVlExVm5NRXc1UkdkQ2NYTm9kWG8wZG5VMlRXeEZVSGM0QWtnQlVBOCUzRA=="}}, "timestampColor": 2164260863, "contextMenuAccessibility": {"accessibilityData": {"label": "Chat actions"}}, "timestampText": {"simpleText": "-26:49"}, "trackingParams": "CBIQ7rsEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "authorBadges": [{"liveChatAuthorBadgeRenderer": {"customThumbnail": {"thumbnails": [{"url": "https://yt3.ggpht.com/8e16_tQaCtP4Tppt3NwGDU5iHe3y8ZmCz2eBJCcrA_lUEXTZ--rh3WkrPWVkGSGUSjK7Z8nf=s16-c-k", "width": 16, "height": 16}, {"url": "https://yt3.ggpht.com/8e16_tQaCtP4Tppt3NwGDU5iHe3y8ZmCz2eBJCcrA_lUEXTZ--rh3WkrPWVkGSGUSjK7Z8nf=s32-c-k", "width": 32, "height": 32}]}, "tooltip": "Member (2 months)", "accessibility": {"accessibilityData": {"label": "Member (2 months)"}}}}], "textInputBackgroundColor": 805306368}}}}], "videoOffsetTimeMsec": "0"}}
{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "replayChatItemAction": {"actions": [{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "addLiveChatTickerItemAction": {"item": {"liveChatTickerPaidMessageItemRenderer": {"id": "ChwKGkNJZVV5NFBDNV9zQ0ZRc3NyUVlkQ1hzRkdn", "amount": {"simpleText": "¥5,000"}, "amountTextColor": 4294967295, "startBackgroundColor": 4293467747, "endBackgroundColor": 4290910299, "authorPhoto": {"thumbnails": [{"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s32-c-k-c0x00ffffff-no-rj", "width": 32, "height": 32}, {"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s64-c-k-c0x00ffffff-no-rj", "width": 64, "height": 64}], "accessibility": {"accessibilityData": {"label": "ジェットニードル"}}}, "durationSec": 1800, "showItemEndpoint": {"clickTrackingParams": "CA8QsMgEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "commandMetadata": {"webCommandMetadata": {"ignoreNavigation": true}}, "showLiveChatItemEndpoint": {"renderer": {"liveChatPaidMessageRenderer": {"id": "ChwKGkNJZVV5NFBDNV9zQ0ZRc3NyUVlkQ1hzRkdn", "timestampUsec": "1670416416135913", "authorName": {"simpleText": "ジェットニードル"}, "authorPhoto": {"thumbnails": [{"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s32-c-k-c0x00ffffff-no-rj", "width": 32, "height": 32}, {"url": "https://yt4.ggpht.com/ytc/AMLnZu_e4mh8PKUfyn75YNiP2DQ05xfc3N27r4G6kafz=s64-c-k-c0x00ffffff-no-rj", "width": 64, "height": 64}]}, "purchaseAmountText": {"simpleText": "¥5,000"}, "message": {"runs": [{"text": "いや、まだ早い!う〇こと判断するにはまだ早い!でっかい馬糞ウニかもしれない!!"}]}, "headerBackgroundColor": 4290910299, "headerTextColor": 4294967295, "bodyBackgroundColor": 4293467747, "bodyTextColor": 4294967295, "authorExternalChannelId": "UCUg0L9DgBqshuz4vu6MlEPw", "authorNameTextColor": 3019898879, "contextMenuEndpoint": {"clickTrackingParams": "CBEQ7rsEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "commandMetadata": {"webCommandMetadata": {"ignoreNavigation": true}}, "liveChatItemContextMenuEndpoint": {"params": "Q2g0S0hBb2FRMGxsVlhrMFVFTTFYM05EUmxGemMzSlJXV1JEV0hOR1IyY2FLU29uQ2hoVlEwWkhOblJsWVhCYVlVNDJTakZ2Vmxoc04wMVpVRUVTQzJvd09UaHZOekF5TkZOeklBRW9BVElhQ2hoVlExVm5NRXc1UkdkQ2NYTm9kWG8wZG5VMlRXeEZVSGM0QWtnQlVBOCUzRA=="}}, "timestampColor": 2164260863, "contextMenuAccessibility": {"accessibilityData": {"label": "Chat actions"}}, "timestampText": {"simpleText": "-26:49"}, "trackingParams": "CBEQ7rsEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "authorBadges": [{"liveChatAuthorBadgeRenderer": {"customThumbnail": {"thumbnails": [{"url": "https://yt3.ggpht.com/8e16_tQaCtP4Tppt3NwGDU5iHe3y8ZmCz2eBJCcrA_lUEXTZ--rh3WkrPWVkGSGUSjK7Z8nf=s16-c-k", "width": 16, "height": 16}, {"url": "https://yt3.ggpht.com/8e16_tQaCtP4Tppt3NwGDU5iHe3y8ZmCz2eBJCcrA_lUEXTZ--rh3WkrPWVkGSGUSjK7Z8nf=s32-c-k", "width": 32, "height": 32}]}, "tooltip": "Member (2 months)", "accessibility": {"accessibilityData": {"label": "Member (2 months)"}}}}], "textInputBackgroundColor": 805306368}}, "trackingParams": "CBAQjtEGIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o"}}, "authorExternalChannelId": "UCUg0L9DgBqshuz4vu6MlEPw", "fullDurationSec": 1800, "trackingParams": "CA8QsMgEIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o"}}, "durationSec": "1800"}}], "videoOffsetTimeMsec": "0"}}
{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "replayChatItemAction": {"actions": [{"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "addChatItemAction": {"item": {"liveChatTextMessageRenderer": {"message": {"runs": [{"text": "待機"}]}, "authorName": {"simpleText": "カルロス・フィットネス"}, "authorPhoto": {"thumbnails": [{"url": "https://yt4.ggpht.com/ytc/AMLnZu9M2P8p4FtMU8EgIidtz1yuxDHUsLHjx0D7I6lt=s32-c-k-c0x00ffffff-no-rj", "width": 32, "height": 32}, {"url": "https://yt4.ggpht.com/ytc/AMLnZu9M2P8p4FtMU8EgIidtz1yuxDHUsLHjx0D7I6lt=s64-c-k-c0x00ffffff-no-rj", "width": 64, "height": 64}]}, "contextMenuEndpoint": {"clickTrackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o", "commandMetadata": {"webCommandMetadata": {"ignoreNavigation": true}}, "liveChatItemContextMenuEndpoint": {"params": "Q2p3S09nb2FRMDlNZGw5MFlrZzFYM05EUmxacmJYSlJXV1JDUzI5QlEwRVNIRU5NZFZneFRWaElOVjl6UTBabGNrRk9RV05rZDJ0M1FVNW5MVEFhS1NvbkNoaFZRMFpITm5SbFlYQmFZVTQyU2pGdlZsaHNOMDFaVUVFU0Myb3dPVGh2TnpBeU5GTnpJQUVvQVRJYUNoaFZRMEpaVTFSRk9XSlFZMGRhVTNsaVpGVlpWVEowV25jNEFrZ0JVQUUlM0Q="}}, "id": "CjoKGkNPTHZfdGJINV9zQ0ZWa21yUVlkQktvQUNBEhxDTHVYMU1YSDVfc0NGZXJBTkFjZHdrd0FOZy0w", "timestampUsec": "1670417922215517", "authorExternalChannelId": "UCBYSTE9bPcGZSybdUYU2tZw", "contextMenuAccessibility": {"accessibilityData": {"label": "Chat actions"}}, "timestampText": {"simpleText": "-1:43"}, "trackingParams": "CAEQl98BIhMIo7nkh9Dt-wIVCLBYCh1Zwg8o"}}, "clientId": "CLuX1MXH5_sCFerANAcdwkwANg-0"}}], "videoOffsetTimeMsec": "0"}}
:
:
:
データ件数は9,732行で全部取得するのに1分40秒かかりました。
取得できるデータはjson形式ではありますが、データ内容を解説したマニュアルや便利な吸出しツールが見つからなかったので、チャットデータ内から必要な値を抜き出すには自前でコーディングする必要がありそうです。
ご覧の通り、生の詳細なデータが取れるので自前で複雑な物を実装したい場合にはyt-dlpが向いてるかも。
配信中のライブチャットを対象にした場合は配信が終了するまで処理が走りっぱなしになるので、コメントを全部取得したい場合はパソコンの準備を整えてから処理を実行した方がいいでしょうね。
pytchatを使う
yt-dlpに比べて扱いやすいデータが簡単に取得できるんですが一部のコメントが欠落する事がありました、恐らく配信者によって非表示設定にされたチャットコメントが取れない感じなのかなと推測してます。
2022.12.28追記:非表示コメントが取れない方と取れる方が両方いるっぽいです、未検証ですが全チャットか上位チャットかを指定する「topchat_only」パラメータもあるみたいなので、入用の方はgithubの記述をご確認下さい。
ちなみに配信者にチャットコメントをブロックされた視聴者はスパチャも贈れなくなるそうです、寂しい。僕はブロックされたこともスパチャを投げた事もないのでこれは未検証。
pytchatはスパチャの金額とかもサラッと取得できるので、拘り仕様のアプリを自作するなどの用途でなければ使いやすいと思います。
コピペコードはこんな感じ。
import pytchat
import time
# PytchatCoreオブジェクトの取得
livechat = pytchat.create(video_id = "msosBK_qWOs")
while livechat.is_alive():
# チャットデータの取得
chatdata = livechat.get()
for c in chatdata.items:
print(f"{c.datetime} {c.author.name} {c.message} {c.amountString}")
time.sleep(1)
yt-dlpと同じ条件でテストしてみます。
取得できるデータはこんな感じ。
2022-12-07 22:58:52 熟成タラコさん🤍 双子座の女
2022-12-07 22:58:55 山口二郎 そこそこじゃねえかよ
2022-12-07 22:58:56 神野みこと あと数時間w
2022-12-07 22:58:57 K ヒロ おお:
:
:
チャットコメントを全部取得するのに4分42秒かかりました、yt-dlpと比較しておおよそ3倍時間がかかる感じですね。データはやっぱり見やすくて良き。
Live Streaming APIを使う
Live Streaming APIのLiveChatMessagesリソースを使うと生配信中のチャットを取得できます。ただしアーカイブされたライブ配信のチャットは取得できず。公式が提供している唯一のチャット取得機能ではあるけど、クォータ数も消費するしAPIキー用意しなきゃいけないしイマイチ使いづらい。
流れ的には、
- YouTube Data APIのvideosメソッドでactiveLiveChatId取得
- activeLiveChatIdをキーにLiveChatMessagesを叩く
でチャットが取れる。配信が終わるとactiveLiveChatIdが消滅するのでAPI経由ではチャットを拾えなくなっちゃいます。
スクリプトはこんな感じで書いてみました。
import urllib.request
import json
#-------↓パラメータ入力↓-------
APIKEY = 'APIキー'
video_id = '動画ID' #ライブ配信中のみ
#-------↑パラメータ入力↑-------
#videoメソッドでactiveLiveChatId取得
param = {
'part':'liveStreamingDetails',
'id':video_id,
'key':APIKEY
}
target_url = 'https://www.googleapis.com/youtube/v3/videos?'+(urllib.parse.urlencode(param))
req = urllib.request.Request(target_url)
with urllib.request.urlopen(req) as json_ress:
ress = json.load(json_ress)
#LiveChatMessagesメソッドでチャットを取得
param = {
'part':'id,snippet,authorDetails',
'liveChatId':ress['items'][0]['liveStreamingDetails']['activeLiveChatId'],
'key':APIKEY
}
target_url = 'https://www.googleapis.com/youtube/v3/liveChat/messages?'+(urllib.parse.urlencode(param))
req = urllib.request.Request(target_url)
with urllib.request.urlopen(req) as json_ress:
ress = json.load(json_ress)
for res in ress['items']:
print(res['snippet']['publishedAt'] + ' ' + res['authorDetails']['displayName'] + ' ' + res['snippet']['displayMessage'])
試しに下記のvtuberさんのライブ配信のチャットを取得してみました。
2022-12-08T21:00:07.483689+00:00 久遠【クオン】 全然脱走しとるw
2022-12-08T21:00:15.143884+00:00 かにみそ王 っっw
2022-12-08T21:00:36.452518+00:00 久遠【クオン】 全員じゃなくて全然
2022-12-08T21:00:51.987888+00:00 久遠【クオン】 わーいじゆうだー!
2022-12-08T21:01:21.183153+00:00 久遠【クオン】 もちろん、おうちが一番だでな
2022-12-08T21:03:03.588353+00:00 久遠【クオン】 誰だったんだw
2022-12-08T21:05:44.367684+00:00 かにみそ王 よきよき
2022-12-08T21:06:33.173667+00:00 久遠【クオン】 ハロウィンか
2022-12-08T21:07:57.332975+00:00 久遠【クオン】 足りなさそうね
2022-12-08T21:08:18.266388+00:00 久遠【クオン】 生きるぞ!
:
:
:
良さそう!
APIでは1回に取得できるコメントの数が決まっているので、配信終了まで継続してコメントを取得し続けるには何かしらの方法で処理をループさせる必要があります。cronとかで時間を指定して自動実行とか、エラーを吐くまで無限ループさせるとかやり方は無数にあると思うので、好きな方法でやって下さい。
このAPIを使用するメリットは公式が公開しているツールなので仕様変更などで突如使えなくなる事は無さそう、とかですかね?yt-dlpやpytchatは有志によって支えられてるツールなので、youtubeに仕様変更があった際に使えなくなる可能性はあると思います。
yt-dlpとpytchatの比較
生配信しか対応できないLive Streaming APIは無かった事にしまして、今までの検証をまとめるとこんな感じ。
比較 | yt-dlp | pytchat |
速度 | ◎(1分40秒) | △(4分42秒) |
精度 | ◎(全件取得可能) | ○(非表示コメントのみ取得不可?) |
使いやすさ | ×(自前でコーディングが必要) | ◎(欲しい値が簡単に取れる) |
早くて精度も高いけど使いにくいyt-dlpと、時間が掛かって一部コメントも取れないけど使いやすいpytchat、という結果になりました。yt-dlpは玄人向け/pytchatは万人向けという印象ですね。
それぞれ個性がはっきり出たので用途に応じて好きな方を使えば良いと思います、ただ生配信のチャット取得に関しては配信が終わるまで両者処理が走りっぱなしになるので、速度に差はなくなると思います。
まとめ
今回はライブチャットの取得方法をまとめてみました。紹介した3種類のツールにはそれぞれ特徴がある事が分かったので、各環境ごとに最適な物を選んで使う感じでいいと思います。
個人開発プログラマーを応援するメンバーシップを始めました('ω')ノ
質問・要望・共同作業など、みんなのやりたい事をスマイルがお手伝いします。立ち上げたばかりでよく分かってないので、とりあえず何でもありやってみます。
コメント
> pytchatでは上位チャットしか取得できない様です
pytchatも全チャット取得に対応していますよ。と言うよりデフォルトで全チャットから取る仕様なので意図的にオプション設定しなければ全て取得になります。
ただ対応しているチャットアクションが限定されているのでyt-dlpよりは少なく取得されてるように見えるかもしれません。
topchat_only=False
https://github.com/taizan-hokuto/pytchat/wiki/PytchatCore_#%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E3%81%A7%E6%8C%87%E5%AE%9A%E5%8F%AF%E8%83%BD%E3%81%AA%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E4%B8%80%E8%A6%A7
管理人のスマイルです(‘ω’)ノ
> pytchatも全チャット取得に対応していますよ。
情報提供ありがとうございます、投稿した作業動画(https://www.youtube.com/watch?v=WebNlg6CpBs)に付いたコメントでyoutubeの仕様変更で全チャット取得できなくなって困ってた、という方がいましたし、僕自身も記事執筆時に検証した際、デフォのpytchatで非表示コメントが取得できなかったと記憶しています。今でも非表示コメント取れてますかね??
> 対応しているチャットアクションが限定されている
すいません、これは具体的にどういったチャットコメントの事を指していますか??勉強させて頂けると助かります。
よろしくお願いしますm(_ _)m