初心者です。間違い等ありましたら、ご指摘ください🙇
環境
- MacBook Air M2
- laravel 10.48.22
- python 3.12.4
- VSCode
SwiftUI✖️Laravel✖️Pythonを使用したアプリをHerokuへデプロイし、動作確認を行っていたところ、以下の問題に遭遇。
問題
Pythonライブラリyoutube-transcript-api
を使用してYouTube動画の字幕を取得できるのだが、ローカル上では取得ができ、Heroku上では取得ができない…
結論(未解決)
プロキシサーバー(有料)を利用する。
原因
YouTubeが、クラウドサービスのIPアドレスからのアクセスをブロックしているっぽい?
以下の議論を参考にした。
簡単に総括すると、
-
クラウドサービス(例えば、Digital Ocean、AWS、Google Cloud、Herokuなど)を使うと、他のユーザーと同じIPアドレスのプールからIPが割り当てられるため、多くのユーザーが同じクラウドサービスからYouTubeにアクセスしようとすると、同じIPアドレスが大量のリクエストに使用される。
-
その結果、YouTubeが「なんか同じIPアドレスからいっぱいリクエストが来るな。なんか怪しいな。このIPアドレスはブロックしておこう」という感じになっている?
-
議論の中で、
proxy server
を利用することで解決している複数のコメントが見られた。
方法・過程
詳細を見ていく。
まず、元々のソースコードから。
-
/htdocs/youtube-transcript-app/routes/api.php(字幕取得のAPIのURL)
// ルーティング Route::get('/get_subtitles', 'App\Http\Controllers\SubtitleController@getSubtitles');
-
/htdocs/youtube-transcript-app/app/Http/Controllers/SubtitleController.php(字幕取得pythonスクリプトの実行)
class SubtitleController extends Controller { // 指定の動画の字幕取得 public function getSubtitles(Request $request) { // 字幕取得したいYouTube動画のIDを取得 $video_id = $request->query('video_id'); // 実際の字幕取得処理を行うPythonスクリプトのパス $scriptPath = base_path('myenv/scripts/get_transcript.py'); // Processコンポーネントは、外部プログラムやコマンドをPHPスクリプト内で実行するためのもの $process = new Process(['python3', $scriptPath, $video_id]); // 実行 $process->run(); // 以下はエラーハンドリングのため、今回はあまり関係ない try { // プロセスの成功を確認 if (!$process->isSuccessful()) { throw new ProcessFailedException($process); // プロセス失敗時に例外をスロー } // 成功した場合の処理 $subtitles = $process->getOutput(); // フォーマット化したjsonレスポンスを返却 return response()->json(json_decode($subtitles, true)); } catch (ProcessFailedException $e) { // 失敗の例外処理 Log::error('Process Failed Exception: ' . $e->getMessage()); return response()->json([ 'detail' => $e->getMessage(), ]); // throw new VideoSubtitleException(message: '字幕の取得に失敗しました。\nこの動画には字幕が含まれていない可能性があります。', detail: $e->getMessage()); } catch (Exception $e) { // その他のエラー // throw $e; return response()->json([ 'detail' => $e->getMessage(), ]); } } }
-
/htdocs/youtube-transcript-app/myenv/scripts/get_transcript.py(字幕取得処理)
import sys import json from youtube_transcript_api import YouTubeTranscriptApi def get_transcript(video_id): # 字幕取得 transcript_list = YouTubeTranscriptApi.list_transcripts(video_id) # 英語字幕を探して抽出 transcript = transcript_list.find_transcript(['en']) transcript_data = transcript.fetch() 〜〜〜〜〜 # その他の任意の処理 〜〜〜〜〜 # 取得した字幕情報をlaravel側に返す return 〜〜〜 if __name__ == "__main__": video_id = sys.argv[1] transcripts = get_transcript(video_id) print(json.dumps(transcripts, ensure_ascii=False))
以下を実行すると、
http://localhost:8000/api/get_subtitles?video_id=NaqB5zYGEsw&t=4s
以下のような字幕情報が得られ、期待するレスポンスが得られた。
{
"text": "everyone is watching this show it is thank I mean it's a breakout hit and",
"start": 0.16,
"duration": 12.56
},
{
"text": "I I know sometimes when you're shooting as an actor you're kind of like in a",
"start": 7.24,
"duration": 6.718999999999999
},
{
"text": "vacuum and you don't understand that what's happening in the world when did",
"start": 10.28,
"duration": 8.68
},
.......
しかし、Herokuへデプロイしたものを実行したところ…
"The command \"'python3' '/Applications/MAMP/htdocs/youtube-transcript-app/myenv/scripts/get_transcript.py' 'NaqB5zYGEs'\" failed.\n\nExit Code: 1(General error)\n\nWorking directory: /Applications/MAMP/htdocs/youtube-transcript-app/public\n\nOutput:\n================\n\n\nError Output:\n================\nTraceback (most recent call last):\n File \"/Applications/MAMP/htdocs/youtube-transcript-app/myenv/scripts/get_transcript.py\", line 85, in <module>\n transcripts = get_transcript(video_id)\n ^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Applications/MAMP/htdocs/youtube-transcript-app/myenv/scripts/get_transcript.py\", line 40, in get_transcript\n transcript_list = YouTubeTranscriptApi.list_transcripts(video_id, proxies=proxies)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Applications/MAMP/htdocs/youtube-transcript-app/myenv/lib/python3.12/site-packages/youtube_transcript_api/_api.py\", line 71, in list_transcripts\n return TranscriptListFetcher(http_client).fetch(video_id)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Applications/MAMP/htdocs/youtube-transcript-app/myenv/lib/python3.12/site-packages/youtube_transcript_api/_transcripts.py\", line 48, in fetch\n self._extract_captions_json(self._fetch_video_html(video_id), video_id),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/Applications/MAMP/htdocs/youtube-transcript-app/myenv/lib/python3.12/site-packages/youtube_transcript_api/_transcripts.py\", line 62, in _extract_captions_json\n raise TranscriptsDisabled(video_id)\nyoutube_transcript_api._errors.TranscriptsDisabled: \nCould not retrieve a transcript for the video https://www.youtube.com/watch?v=NaqB5zYGEs! This is most likely caused by:\n\nSubtitles are disabled for this video\n\nIf you are sure that the described cause is not responsible for this error and that a transcript should be retrievable, please create an issue at https://github.com/jdepoix/youtube-transcript-api/issues. Please add which version of youtube_transcript_api you are using and provide the information needed to replicate the error. Also make sure that there are no open issues which already describe your problem!\n"
「字幕が取得できない」という旨のエラーが発生。
video_id
の値も変えたりしてみたが、変わらず。しかし、ローカル上では動作する…
Proxy Server を構築する
先ほども言及した、こちらの議論を参考に、プロキシサーバーを使用してみる。
今回の用途としては、以下のような感じになるのでしょうか。
Smart Proxy の Residentail Proxy でプロキシサーバーを構築。(有料)
構築が完了したら、Smart Proxy で自動生成されたユーザー名とパスワードを用いて、プロキシサーバーにアクセスする。
import sys
import json
from youtube_transcript_api import YouTubeTranscriptApi
def get_transcript(video_id):
######################## 追記
username = 'xxxxxxxxxxx' # SmartProxyにて、自動生成されたユーザー名
password = 'yyyyyyyyyyy' # SmartProxyにて、自動生成されたパスワード
proxy = f"http://{username}:{password}@[プロキシサーバーのURL]:[ポート番号]"
# 字幕取得
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id, proxies = {
'http': proxy,
'https': proxy
})
#########################
# 英語字幕を探して抽出
transcript = transcript_list.find_transcript(['en'])
transcript_data = transcript.fetch()
〜〜〜〜〜
# その他の任意の処理
〜〜〜〜〜
# 取得した字幕情報をlaravel側に返す
return 〜〜〜
if __name__ == "__main__":
video_id = sys.argv[1]
transcripts = get_transcript(video_id)
print(json.dumps(transcripts, ensure_ascii=False))
※ 個人情報のため、外部に公開する前に、username
, password
, proxy
は.envファイルに環境変数として定義。今回は、Herokuをクラウドサービスとして利用しているため、以下をターミナルで実行し、Heroku上に環境変数として定義。
heroku config:set PROXY_SERVER_URL="http://{username}:{password}@[プロキシサーバーのURL]:[ポート番号]"
終わり。