PythonでYouTube Liveのアーカイブからチャット(コメント)を取得する(改訂版)

watagassy.hatenablog.com

前回このような記事を書きまして、seleniumを用いたスクレイピングを行うといった形でプログラムを記載していましたが、selenium使わなくて良いんじゃね?ということが判明したので、こちらに改訂版のコードを書いておきます。

この変更に伴い、前回のプログラムの問題点であった「めちゃくちゃ遅い」という部分が解消されています。



from bs4 import BeautifulSoup
import json
import requests

target_url = youtube_url
dict_str = ""
next_url = ""
comment_data = []
session = requests.Session()
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'}

# まず動画ページにrequestsを実行しhtmlソースを手に入れてlive_chat_replayの先頭のurlを入手
html = requests.get(target_url)
soup = BeautifulSoup(html.text, "html.parser")

for iframe in soup.find_all("iframe"):
    if("live_chat_replay" in iframe["src"]):
        next_url= iframe["src"]


while(1):

    try:
   html = session.get(next_url, headers=headers)
        soup = BeautifulSoup(html.text,"lxml")


        # 次に飛ぶurlのデータがある部分をfind_allで探してsplitで整形
        for scrp in soup.find_all("script"):
            if "window[\"ytInitialData\"]" in scrp.text:
                dict_str = scrp.text.split(" = ")[1]

        # javascript表記なので更に整形. falseとtrueの表記を直す
        dict_str = dict_str.replace("false","False")
        dict_str = dict_str.replace("true","True")

        # 辞書形式と認識すると簡単にデータを取得できるが, 末尾に邪魔なのがあるので消しておく(「空白2つ + \n + ;」を消す)
        dict_str = dict_str.rstrip("  \n;")
        # 辞書形式に変換
        dics = eval(dict_str)

        # "https://www.youtube.com/live_chat_replay?continuation=" + continue_url が次のlive_chat_replayのurl
        continue_url = dics["continuationContents"]["liveChatContinuation"]["continuations"][0]["liveChatReplayContinuationData"]["continuation"]
        next_url = "https://www.youtube.com/live_chat_replay?continuation=" + continue_url
        # dics["continuationContents"]["liveChatContinuation"]["actions"]がコメントデータのリスト。先頭はノイズデータなので[1:]で保存
        for samp in dics["continuationContents"]["liveChatContinuation"]["actions"][1:]:
            comment_data.append(str(samp)+"\n")

    # next_urlが入手できなくなったら終わり
    except:
        break

# comment_data.txt にコメントデータを書き込む
with open("comment_data.txt", mode='w', encoding="utf-8") as f:
    f.writelines(comment_data)

基本的な構造はほぼ変わっていませんが、htmlの取得をdriver.get(next_url)からsession.get(next_url, headers=headers)に変更しています。




いろいろ試している間に、スーパーチャットなどが導入されている場合はコメントデータの構造が違うことなどが判明したので、それらに対応した追記もこちらでしていこうと思います。

PythonでYouTube Liveのアーカイブからチャット(コメント)を取得する

※こちらに修正して高速化した、コードの改訂版を載せています。基本的な構造は変わっていないので解説は本記事を参照してもらって大丈夫ですが、最終的なコードはリンク先を用いてください。
watagassy.hatenablog.com





YouTube Liveのチャット(コメント)は、ライブ中にリアルタイムで取得することはYouTube Data APIを用いることで可能なのですが、アーカイブから取得するという操作はAPIにはありません。


それでもチャットリプレイ機能で過去のコメントが表示されるので、クソ~~~どっかからどうにかして取得できるやろ~~~できんかな~~~と思っていたらこんな記事を見つけました。

summer-art.hatenablog.com

この記事の最後の方にこんなことが書かれていました。

放送終了後のチャットを取得できるかについて
まさにやりたいことはここから..
APIで取得できない.. リプレイの場合, webで確認してみると get_live_chat_replay?continuation=xxxxxxxxxxx.jsonで一定間隔でチャットを取得してることがわかった.
ここでcontinuationを知る方法について調査なう. videoIdから一番初めのcontinuationの値を知ることができたらあとは芋づる式に取得することができる. get_live_chat_replay?continuation=xxxxxxxxxxx.jsonには次に取得するcontinuationの値が含まれているから...


これをヒントにあれこれやっていたらアーカイブからチャットを取得できたので、今回それについて書いていこうと思います。

実装はPython3.6で行っています。


ちなみに先に言っておきますと、この方式で全コメント取得するのはめちゃくちゃ遅いです。ご了承ください。




まずはソースコードは以下です。

from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import requests

target_url = youtube_url
options = Options()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--disable-desktop-notifications')
options.add_argument("--disable-extensions")
options.add_argument('--blink-settings=imagesEnabled=false')
dict_str = ""
next_url = ""
comment_data = []

# まず動画ページにrequestsを実行しhtmlソースを手に入れてlive_chat_replayの先頭のurlを入手
html = requests.get(target_url)
soup = BeautifulSoup(html.text, "html.parser")

for iframe in soup.find_all("iframe"):
    if("live_chat_replay" in iframe["src"]):
        next_url= iframe["src"]


while(1):

    try:
        # chromedriverを開きnext_urlのソースを入手
        driver = webdriver.Chrome(chrome_options=options, executable_path=r"C:\chromedriver_win32\chromedriver.exe")
        driver.get(next_url)
        soup = BeautifulSoup(driver.page_source,"lxml")
        # 必ずquitすること。忘れるとgoogle chromeが開かれまくって大変なことになる
        driver.quit()

        # 次に飛ぶurlのデータがある部分をfind_allで探してsplitで整形
        for scrp in soup.find_all("script"):
            if "window[\"ytInitialData\"]" in scrp.text:
                dict_str = scrp.text.split(" = ")[1]

        # javascript表記なので更に整形. falseとtrueの表記を直す
        dict_str = dict_str.replace("false","False")
        dict_str = dict_str.replace("true","True")

        # 辞書形式と認識すると簡単にデータを取得できるが, 末尾に邪魔なのがあるので消しておく(「空白2つ + \n + ;」を消す)
        dict_str = dict_str.rstrip("  \n;")
        # 辞書形式に変換
        dics = eval(dict_str)

        # "https://www.youtube.com/live_chat_replay?continuation=" + continue_url が次のlive_chat_replayのurl
        continue_url = dics["continuationContents"]["liveChatContinuation"]["continuations"][0]["liveChatReplayContinuationData"]["continuation"]
        next_url = "https://www.youtube.com/live_chat_replay?continuation=" + continue_url
        # dics["continuationContents"]["liveChatContinuation"]["actions"]がコメントデータのリスト。先頭はノイズデータなので[1:]で保存
        for samp in dics["continuationContents"]["liveChatContinuation"]["actions"][1:]:
            comment_data.append(str(samp)+"\n")

    # next_urlが入手できなくなったら終わり
    except:
        break

# comment_data.txt にコメントデータを書き込む
with open("comment_data.txt", mode='w', encoding="utf-8") as f:
    f.writelines(comment_data)


youtube_url」にチャットを取得したい動画のURL
("https://www.youtube.com/watch?v=xxxxxxxx"の形式)を入れてこのプログラムを実行すると、comment_data.txtというファイルに1行ずつチャットに関するデータが保存されます。



先にこのプログラムが何をやっているか大雑把に説明すると、まずrequestsを用いて、youtube_urlの動画のhtmlからチャットのデータが存在するURL
("https://www.youtube.com/live_chat_replay?continuation=xxxxxxxx"という名前になっています)を探して飛びます。

そこでチャットがjavascriptで動的に生成されているので、seleniumを用いてスクレイピングします。

すると先頭40件くらいのチャットデータと次のチャットデータが存在するURLが取得できるので、そこに飛んで同様の操作をします。

これを繰り返してtxtファイルにチャットデータを保存する、というのが一連の流れです。


説明が必要そうな部分に分けて先頭から解説していきます。



from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import requests

target_url = youtube_url
options = Options()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--disable-desktop-notifications')
options.add_argument("--disable-extensions")
options.add_argument('--blink-settings=imagesEnabled=false')

今回スクレイピングでデータを取得するので、BeautifulSoupとselenium、requestsを用います。導入していなければインストールしておいてください。
また今回seleniumスクレイピングする際にchromedriverを用いるので、こちらからダウンロードしておいてください。

chromedriver.chromium.org

その上でchromedriverを任意のファイルに置いて環境変数を設定してパスを通しておいてください。
Cドライブ直下に置いてパスを通せばそのまま上記のコードを動かすことができますが、別の場所においている場合は、パスをそちらに設定して以下のコードの「executable_path=」以降をchromedriverが置いている場所に書き換えてください。

        driver = webdriver.Chrome(chrome_options=options, executable_path=r"C:\chromedriver_win32\chromedriver.exe")

options.add_argumentとかいうのがいっぱいありますが、これはchromedriverでスクレイピングする際のオプションです。それぞれの内訳がどういうものかについてはこちらが詳しいので参照してください。
vaaaaaanquish.hatenablog.com





次です。

html = requests.get(target_url)
soup = BeautifulSoup(html.text, "html.parser")

for iframe in soup.find_all("iframe"):
    if("live_chat_replay" in iframe["src"]):
        next_url= iframe["src"]

次は動画のURLのhtmlを取得し、その中からfind_allで"iframe"タグを持つ部分を探します。
その中に
iframe["src"]="https://www.youtube.com/live_chat_replay?continuation=xxxxxxxx"であるデータが存在するので、それを探し出してnext_urlに代入します。





次からがいちばん重要な部分ですね。

        # chromedriverを開きnext_urlのソースを入手
        driver = webdriver.Chrome(chrome_options=options, executable_path=r"C:\chromedriver_win32\chromedriver.exe")
        driver.get(next_url)
        soup = BeautifulSoup(driver.page_source,"lxml")
        # 必ずquitすること。忘れるとgoogle chromeが開かれまくって大変なことになる
        driver.quit()

chromedriverを用いて、さきほど入手したnext_urlのhtmlを入手します。



# 次に飛ぶurlのデータがある部分をfind_allで探してsplitで整形
        for scrp in soup.find_all("script"):
            if "window[\"ytInitialData\"]" in scrp.text:
                dict_str = scrp.text.split(" = ")[1]

        # javascript表記なので更に整形. falseとtrueの表記を直す
        dict_str = dict_str.replace("false","False")
        dict_str = dict_str.replace("true","True")

        # 辞書形式と認識すると簡単にデータを取得できるが, 末尾に邪魔なのがあるので消しておく(「空白2つ + \n + ;」を消す)
        dict_str = dict_str.rstrip("  \n;")
        # 辞書形式に変換
        dics = eval(dict_str)


html中の"script"タグを持つ部分で、"window["ytInitialData"] = xxxxx"となっている部分のxxxxにチャットデータが存在するので、まず"window["ytInitialData"]"を探して、"="でsplitしxxxxの部分を取得します。

xxxxの部分はjson形式のデータになっていますが、javascriptのソースを取得しているのでそのままでは扱えません。ですのでいくつか修正を行ってevalで辞書形式にして扱いやすくします。

        # "https://www.youtube.com/live_chat_replay?continuation=" + continue_url が次のlive_chat_replayのurl
        continue_url = dics["continuationContents"]["liveChatContinuation"]["continuations"][0]["liveChatReplayContinuationData"]["continuation"]
        next_url = "https://www.youtube.com/live_chat_replay?continuation=" + continue_url
        # dics["continuationContents"]["liveChatContinuation"]["actions"]がコメントデータのリスト。先頭はノイズデータなので[1:]で保存
        for samp in dics["continuationContents"]["liveChatContinuation"]["actions"][1:]:
            comment_data.append(str(samp)+"\n")


dics["continuationContents"]["liveChatContinuation"]["continuations"][0]["liveChatReplayContinuationData"]["continuation"]が次に飛ぶURLのデータなので取得。

先頭に"https://www.youtube.com/live_chat_replay?continuation="をつけると次に飛ぶURLになるのでnext_urlに代入します。

コメントデータはdics["continuationContents"]["liveChatContinuation"]["actions"]にリストで入っているので、forを用いて1行ずつ取得します。

ただし注意が必要なのは、dics["continuationContents"]["liveChatContinuation"]["actions"]の各要素はコメントだけでなく、コメントに関するコメント以外のデータも含まれています。

ですのでコメントのみを取得したい場合は、comment_data.append(str(samp)+"\n")を以下のように書き換えてください。

comment_data.append(str(samp["replayChatItemAction"]["actions"][0]["addChatItemAction"]["item"]["liveChatTextMessageRenderer"]["message"]["simpleText"])+"\n")

クソめんどくさいですね…。正直このあたりのデータ構造を確認するのが一番大変でした。


他に「チャットがリプレイ欄に表示された時間」を取得したいときは以下のようにしてください。

comment_data.append(str(samp["replayChatItemAction"]["actions"][0]["addChatItemAction"]["item"]["liveChatTextMessageRenderer"]["timestampText"]["simpleText"])+"\n")



最後はnext_urlを取得できなくなれば(実際はcontinue_urlを取得できなくなれば、ですが)全てのデータを取得し終えたということなのですが、処理を書くのがめんどくさいのでexceptで書いています。

    # next_urlが入手できなくなったら終わり
    except:
        break




以上がyoutube liveのアーカイブからチャットを取得する方法です。

一応この方式で遅くなる原因ですが、コメントを取得するために何度も別のURLを開き続けなければいけないので、その分だけchromedriverでchromeを開くことになるからです。

しかも一度に取得できるコメントの最大数が40件くらいなので、何千件とコメントがある動画で実行したときにはもうはちゃめちゃ遅いです。なにか解決方法ありませんかね?


まあ書いてみればなんのことはないという感じですが、プログラムを書く時間よりもhtmlファイルとずっとにらめっこする時間のほうが長くて文字にならない苦労がめちゃくちゃありました…。

とりあえず記事にすることができてホッと一息です。

何でも楽しむ全てが私~神楽すず的アイドル道~

神楽すずというVTuberがいます。電脳少女シロちゃんの後輩にあたるVTuber12人で構成されたグループ、「アイドル部」に所属するVTuberです。

 

私はVTuberの中でも特段シロちゃんとアイドル部を推している、つまり彼女たちのファンなのですが。こうしてファンをやっている立場から一番身近に感じるアイドル部のメンバーが、神楽すずちゃんなんですよね。

 

続きを読む

新しい扉の前に立つ北上双葉へ~もしくは彼女に歌う神楽すずへ~

アイドル部というvtuberのグループがあります。全員が女の子で2Dのvtuberです。筆者は幾人も存在するvtuberの中で、特に彼女たちを推しています。つまりファンです。

彼女たちは所属的に、シロちゃんの後輩にあたる存在なのですが、そのアイドル部に在籍する12人の子たちには1つの目標が設定されています。

曰く「youtubeのチャンネル登録者数が5万人を超えたら3D化する」と。

 

続きを読む

vtuber生放送接続数ランキング(2018-09-02,19:57-20:07)(テスト)

この投稿はvtuberの生放送の最大視聴者数のランキングを、自動で取得して投稿するプログラムのテストです。

続きを読む

仮想舟上饗宴歌 感想編

youtu.be

こちらの動画に感銘を受けて以下みたいな記事を書いたんですが。
watagassy.hatenablog.com
記事を書くために歌詞をジックリ見ていると感情という感情が湧いてきて仕方なかったので普通に感想も書くことにしました。
内容的には、順不同に歌詞の一節を取り出して恍惚するだけになると思います。

続きを読む

仮想舟上饗宴歌の解説

8/26、vtuber界にものすごい動画が上がりましたね。

 

youtu.be

これです。

 

いわゆるオールスター系の動画ですが、まずそこに出てくるvtuberの数がすごい。そしてそれら全ての文脈的つながりをオリジナルの歌詞で表現しているというのが更にすごい。

 

これに感銘を受けまして、僭越ながら解説記事を書きたいなと思った次第です。

 

続きを読む