Pythonのyoutube-transcript-apiの使用時に、プロキシサーバーを使用する


初心者です。間違い等ありましたら、ご指摘ください🙇

### 環境

- MacBook Air M2
- laravel 10.48.22
- python 3.12.4
- VSCode

SwiftUI✖️Laravel✖️Pythonを使用したアプリをHerokuへデプロイし、動作確認を行っていたところ、以下の問題に遭遇。

### 問題

Pythonライブラリ`youtube-transcript-api` を使用してYouTube動画の字幕を取得できるのだが、ローカル上では取得ができ、Heroku上では取得ができない…

### 結論（未解決）

プロキシサーバー（有料）を利用する。

### 原因

**YouTubeが、クラウドサービスのIPアドレスからのアクセスをブロックしているっぽい？**

以下の議論を参考にした。

https://github.com/jdepoix/youtube-transcript-api/issues/303

簡単に総括すると、

- クラウドサービス（例えば、Digital Ocean、AWS、Google Cloud、Herokuなど）を使うと、他のユーザーと同じIPアドレスのプールからIPが割り当てられるため、多くのユーザーが同じクラウドサービスからYouTubeにアクセスしようとすると、同じIPアドレスが大量のリクエストに使用される。
- その結果、YouTubeが「なんか同じIPアドレスからいっぱいリクエストが来るな。なんか怪しいな。このIPアドレスはブロックしておこう」という感じになっている？

- 議論の中で、`proxy server`を利用することで解決している複数のコメントが見られた。

### 方法・過程

詳細を見ていく。

まず、元々のソースコードから。

- **/htdocs/youtube-transcript-app/routes/api.php（字幕取得のAPIのURL）**
    
    ```php
    // ルーティング
    Route::get('/get_subtitles', 'App\Http\Controllers\SubtitleController@getSubtitles');
    ```
    
- **/htdocs/youtube-transcript-app/app/Http/Controllers/SubtitleController.php（字幕取得pythonスクリプトの実行）**
    
    ```php
    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（字幕取得処理）**
    
    ```python
    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))
    
    ```
    

以下を実行すると、

```python
http://localhost:8000/api/get_subtitles?video_id=NaqB5zYGEsw&t=4s
```

以下のような字幕情報が得られ、期待するレスポンスが得られた。

```json
        {
            "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へデプロイしたものを実行したところ…

```json
"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 を構築する

https://github.com/jdepoix/youtube-transcript-api/issues/303

先ほども言及した、こちらの議論を参考に、プロキシサーバーを使用してみる。

- **プロキシサーバーとは？**
    
    以下のサイトが参考になりました。
    
    [](https://wa3.i-3-i.info/word1752.html)
    

今回の用途としては、以下のような感じになるのでしょうか。

![ProxyServer.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3879232/9ca9b6c7-52ed-406b-fc9e-9f741cc38b1c.png)


**Smart Proxy** の **Residentail Proxy** でプロキシサーバーを構築。（有料）

[Log in | Smartproxy](https://dashboard.smartproxy.com/residential-proxies)

構築が完了したら、Smart Proxy で自動生成されたユーザー名とパスワードを用いて、プロキシサーバーにアクセスする。

```python
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]:[ポート番号]"

```

終わり。