質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.95%

YouTube Liveのアーカイブからチャットを取得するコードを模写したが上手くいかない @Python

解決済

回答 1

投稿

  • 評価 1
  • クリップ 3
  • VIEW 678

mongaa

score 1

前提・実現したいこと

PythonでYoutube Liveのアーカイブからチャットを取得したいです。
こちらのサイトのコードをほぼそのまま使用しています。
https://github.com/geerlingguy/youtube_chat_crawler/blob/master/YoutubeChatReplayCrawler.py

コードは下記について変更しています。
→youtubeの動画IDを引数としてvideo_idに入れているのを、初めからtarget_urlに打ち込んでいます。したがって、引数の長さを判別する箇所もコメントアウトしています。

発生している問題・エラーメッセージ

pyファイルを実行し作成されるテキストファイル(comment_data.txt)を開いても中身が空っぽです。
cmd,Visual Studio Code共にエラーメッセージは発生していません。

指定した動画からコメントを取得する流れのどこかで想定通りに動いていないと思われますが、
解決策が分からなくて困っています。

該当のソースコード

#!/usr/bin/env python3

from bs4 import BeautifulSoup
import ast
import requests
import re
import sys

# Verify user supplied a YouTube URL.
#if len(sys.argv) == 1:
#    print("Please provide a YouTube URL (e.g. ./YoutubeChatReplayCrawler.py YOUTUBE_VIDEO_URL)")
#    sys.exit(0)

# Produce a valid filename (from Django text utils).
def get_valid_filename(s):
    s = str(s).strip().replace(' ', '_')
    return re.sub(r'(?u)[^-\w.]', '', s)


# Set up variables for requests.
#target_url = sys.argv[1]
target_url = "https://www.youtube.com/watch?v=mOBvvwCosE4" #←適当に選んだ動画のURL

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

# Get the video page.
html = session.get(target_url)
soup = BeautifulSoup(html.text, 'html.parser')

# Retrieve the title and sanitize so it is a valid filename.
title = soup.find_all('title')
title = title[0].text.replace(' - YouTube', '')
title = get_valid_filename(title)

# Regex match for emoji.
RE_EMOJI = re.compile('[\U00010000-\U0010ffff]', flags=re.UNICODE)

# Find any live_chat_replay elements, get URL for next live chat message.
for iframe in soup.find_all("iframe"):
    if("live_chat_replay" in iframe["src"]):
        next_url = iframe["src"]

if not next_url:
    print("Couldn't find live_chat_replay iframe. Maybe try running again?")
    sys.exit(0)

# TODO - We should fail fast if next_url is empty, otherwise you get error:
# Invalid URL '': No schema supplied. Perhaps you meant http://?

# TODO - This loop is fragile. It loops endlessly when some exceptions are hit.
while(1):

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

        # Loop through all script tags.
        for script in soup.find_all('script'):
            script_text = str(script)
            if 'ytInitialData' in script_text:
                dict_str = ''.join(script_text.split(" = ")[1:])

        # Capitalize booleans so JSON is valid Python dict.
        dict_str = dict_str.replace("false", "False")
        dict_str = dict_str.replace("true", "True")

        # Strip extra HTML from JSON.
        dict_str = re.sub(r'};.*\n.+<\/script>', '}', dict_str)

        # Correct some characters.
        dict_str = dict_str.rstrip("  \n;")

        # TODO: I don't seem to have any issues with emoji in the messages.
        # dict_str = RE_EMOJI.sub(r'', dict_str)

        # Evaluate the cleaned up JSON into a python dict.
        dics = ast.literal_eval(dict_str)

        # TODO: On the last pass this returns KeyError since there are no more
        # continuations or actions. Should probably just break in that case.
        continue_url = dics["continuationContents"]["liveChatContinuation"]["continuations"][0]["liveChatReplayContinuationData"]["continuation"]
        print('Found another live chat continuation:')
        print(continue_url)
        next_url = "https://www.youtube.com/live_chat_replay?continuation=" + continue_url

        # Extract the data for each live chat comment.
        for samp in dics["continuationContents"]["liveChatContinuation"]["actions"][1:]:
            comment_data.append(str(samp) + "\n")

    # next_urlが入手できなくなったら終わり
    except requests.ConnectionError:
        print("Connection Error")
        continue
    except requests.HTTPError:
        print("HTTPError")
        break
    except requests.Timeout:
        print("Timeout")
        continue
    except requests.exceptions.RequestException as e:
        print(e)
        break
    except KeyError as e:
        error = str(e)
        if 'liveChatReplayContinuationData' in error:
            print('Hit last live chat segment, finishing job.')
        else:
            print("KeyError")
            print(e)
        break
    except SyntaxError as e:
        print("SyntaxError")
        print(e)
        break
        # continue #TODO
    except KeyboardInterrupt:
        break
    except Exception:
        print("Unexpected error:" + str(sys.exc_info()[0]))

# Write the comment data to a file named after the title of the video.
with open(title + ".json", mode='w', encoding="utf-8") as f:
    f.writelines(comment_data)

print('Comment data saved to ' + title + '.json')

試したこと

・実行時、すぐ"Couldn't find live_chat_replay iframe. Maybe try running again?"と表示されるので、while(1):のループに入る前を見ていけば良いと考えました。
・そこで、デバッグによって各変数の状態を確認したところ、next_url=""のままでした。よって、>>if("live_chat_replay" in iframe["src"]):は一度もTrueにならなかったと考えています。(Trueなら何かしら代入されているため)
・上記条件式で使用されるiframeの中にはいろんなタグが入っていました。"src"に関係ありそうな箇所(自信ないです)を載せておきます。
<iframe src="https://accounts.google.com/ServiceLogin?uilel=3&amp;hl=ja&amp;service=youtube&amp;passive=true&amp;continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Ffeature%3Dpassive%26next%3D%252Fsignin_passive%26hl%3Dja%26action_handle_signin%3Dtrue%26app%3Ddesktop" style="display: none"></iframe>

iframeを見ても何を確認してどうすれば良いかわからなくなり手詰まりの状態になっています。。

補足情報(FW/ツールのバージョンなど)

python          3.6.5
beautifulsoup4  4.9.1

使用PC:
Surface Pro 5
Windows 10

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    上記に当てはまらず、質問内容が明確になっていない質問には「」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

回答 1

checkベストアンサー

+2

推測なのですが、質問文の

next_url=""のままでした。よって、>>if("live_chat_replay" in iframe["src"]):は一度もTrueにならなかったと考えています。

や、6番目のコメントの

ブラウザの"検証"で確認してみると、下記のコードにチャットのツリーが繋がっていました。~

以下から推測するに、requestsで得られるHTMLと、実際にブラウザが最終的に獲得しているHTMLが異なるのではないでしょうか。

具体的には、ブラウザは、最初に獲得したソースからさらにjavascript等のスクリプトや他のサーバから追加のデータを取得し、レンダリングした上でページを表示している場合があります。
requestsライブラリはブラウザではないため、ブラウザの完全なレンダリング機能は持っていません。

pythonでブラウザのレンダリング機能を実現しようとするとselenium等のヘッドレスブラウザを使うことになります。

seleniumでも可能ですが、ドライバの準備等でハードルが高いため、もっと簡単に使える、requests_htmlというライブラリがあります。

質問文のコードを下記のように置き換えてみてはどうでしょうか。

実行前に

pip install requests_html


で requests_htmlをインストールしてください。

5行目以降。「#~コメント」にしているところを置き換えています。略となっているところはそのままでOKだと思います。

#!/usr/bin/env python3
(略)
import requests
import requests_html # <= 追加

(略)
# session = requests.Session() この行を↓に置き換え
session = requests_html.HTMLSession()

# soup = session.get(target_url) この行を↓に置き換え
resp = session.get(target_url)

resp.html.render(sleep=3)  # <= 追加。レンダリングを完了させるため3秒待ちます。
# soup = BeautifulSoup(html.text, 'html.parser')  <= ここは不要なのでコメントアウトする。

(略)

# title = soup.find_all('title') この行を↓に置き換え
title = resp.html.find('title')

(略)

# for iframe in soup.find_all("iframe"):
#     if("live_chat_replay" in iframe["src"]):
#         next_url = iframe["src"]
# この3行を↓に置き換え
for iframe in resp.html.find("iframe"):
    if "live_chat_replay" in iframe.attrs["src"]:
        next_url = "".join(["https://www.youtube.com", iframe.attrs["src"]])

(略)


#         for samp in dics["continuationContents"]["liveChatContinuation"]["actions"][1:]:  
# これだと、チャットデータのブロックごとに最初の行が欠落するため、下記のようにする。
         for samp in dics["continuationContents"]["liveChatContinuation"]["actions"]:

以上になります。

投稿

編集

taizan205

score 639

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • mongaa

    mongaa

    2020/07/16 01:48

    回答ありがとうございます!!無事データ取得できました!

    seleniumは使ったことはあったのですが、requestsライブラリとの棲み分けがわかっておらず「requestsだけあればよくないか?」と思ってました・・・
    ですが、回答にあった"ヘッドレスブラウザ"という単語から調べてみてrequestsとselenium(requests_html)の違いを学ぶことができました!

    >>具体的には、ブラウザは、最初に獲得したソースからさらにjavascript等のスクリプトや他のサーバから追加のデータを取得し、レンダリングした上でページを表示している場合があります。
    >>requestsライブラリはブラウザではないため、ブラウザの完全なレンダリング機能は持っていません。
    こちらも知らなかったので、助かりました。今後は、ブラウザの"検証"コマンドとあわせて使うならrequestsではなくヘッドレスブラウザを使っていこうと思います。

    回答がひとつひとつ丁寧に書かれていたので順を追って理解できました。ありがとうございます。


    今回の疑問は解消できたのですが、ひとつ質問をしてもよろしいでしょうか?
    ヘッドレスブラウザではなく、requestsを使う場面ってどういう時なのでしょうか?
    「Webページの構成を知るにはブラウザの"検証"コマンドを使用することになる→ヘッドレスブラウザの方が確実」という流れになりrequestsを使用したい場面が思いつかないのですが・・・

  • taizan205

    taizan205

    2020/07/16 08:01 編集

    おっしゃる通りスクレイピングのような用途ではヘッドレスブラウザを使ったほうが確実な場面は多いと思います。

    requestsでできることはたいていヘッドレスブラウザでもできるはずです。
    回答したコードも元々あったrequestsは残していますが、すべてrequests_htmlに置き換えても動作すると思います。(一部コード修正は必要かもしれませんが)

    ただヘッドレスブラウザはrequestsに比べるとライブラリのサイズが大きく、メモリ消費量も重いです。
    たとえばyoutube api等のweb apiを通じてデータをやり取りする場合等、わざわざレンダリングを考慮する必要がない場合は、requestsや、python標準のhttpライブラリを使った方が軽いプログラムにできます。

  • mongaa

    mongaa

    2020/07/16 12:41

    >>ただヘッドレスブラウザはrequestsに比べるとライブラリのサイズが大きく、メモリ消費量も重いです。
    たとえばyoutube api等のweb apiを通じてデータをやり取りする場合等、わざわざレンダリングを考慮する必要がない場合は、requestsや、python標準のhttpライブラリを使った方が軽いプログラムにできます。
    →具体的な例でイメージできました!確かにAPIを叩く時はレンダリングを考慮しなくてよいですね!
     教えて頂きありがとうございました!

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.95%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る