プロキシを使ったYouTube公開データスクレイピング完全ガイド

YouTube Data API v3のクオータ制限を超えて、InnerTubeエンドポイントとレジデンシャルプロキシで動画メタデータ・コメント・トレンドデータを大規模に抽出する実践的な手法を解説します。

プロキシを使ったYouTube公開データスクレイピング完全ガイド

なぜYouTube公開データのスクレイピングが必要なのか

メディアアナリティクスチームやクリエイターエコノミーの研究者にとって、YouTubeは世界最大の公開動画データベースです。毎日数百万本の動画がアップロードされ、視聴数、コメント、トレンドがリアルタイムに変動しています。しかし、YouTube Data API v3のクオータ制限は、大規模なデータ収集において深刻なボトルネックになります。

1日あたり10,000クオータユニットという制限の中で、search.list1回で100ユニットを消費すれば、1日の検索回数はわずか100回。コメントスレッドの大規模収集、早期トレンドの検出、広告モニタリングといったユースケースでは、APIだけでは到底カバーできません。そこで、YouTube residential proxiesを活用した公開データの直接抽出が現実的な選択肢となります。

重要な免責事項: この記事は公開データへの正当なアクセスのみを対象としています。各プラットフォームの利用規約(YouTube Terms of Service)、米国のCFAA、EUのGDPRなど適用される法令を遵守してください。ログインが必要なデータのスクレイピングや、著作権コンテンツの再配布は推奨しません。

YouTube Data API v3:いつ十分で、いつ不十分か

APIのクオータ体系とコスト

YouTube Data API v3はクオータベースの課金モデルを採用しています。各エンドポイントには異なるコストが設定されています:

エンドポイントクオータ消費1日あたり可能回数(10,000ユニット)
search.list100100回
videos.list110,000回
channels.list110,000回
commentThreads.list110,000回
playlistItems.list110,000回

一見するとvideos.listcommentThreads.listは1ユニットで済みますが、実際には検索→フィルタ→詳細取得のパイプラインが必要であり、search.listの100ユニット消費がすぐにクオータを圧迫します。

APIで十分なケース

  • 少数チャンネル(50以下)の定期メタデータ監視
  • 特定動画IDが既知のコメント取得
  • プレイリストのトラッキング
  • プロトタイプやPoC段階のデータ収集

スクレイピングが必要なケース

  • コメントスレッドの大規模収集:APIは1リクエストあたり最大100件のコメントしか返さず、深いスレッドの取得には多数のページネーションが必要
  • 早期トレンド検出:アップロード直後の動画をリアルタイムに発見するには、検索APIのクオータでは追いつかない
  • 広告モニタリング:APIは広告情報を一切提供しないため、ページスクレイピングが唯一の手段
  • 字幕・トランスクリプト:APIは字幕を提供しない(captions.listはダウンロードURLを返さない)
  • 推薦アルゴリズムの調査:関連動画やホームフィードの構造はAPIから取得不可

ログインなしでアクセス可能なデータ

YouTubeの大部分の公開データは、認証なしでブラウザから閲覧可能であり、適切な手法で抽出できます:

動画メタデータ

  • タイトル、説明文、タグ(一部)
  • 視聴数、高評価数、コメント数
  • アップロード日時、動画長
  • サムネイルURL、カテゴリ

チャンネルページ

  • チャンネル名、登録者数、説明文
  • 最新動画リスト、プレイリスト
  • チャンネルバナー、ロゴ

コメントスレッド(InnerTube経由)

  • トップレベルコメントと返信
  • 投稿者名、テキスト、タイムスタンプ、いいね数
  • ハートマーク、ピン留め状態

字幕トランスクリプト

  • 自動生成字幕と手動字幕のテキスト
  • タイムスタンプ付きセグメント

InnerTube API:YouTubeの内部エンドポイントを理解する

YouTubeのフロントエンドは、InnerTubeと呼ばれる内部APIに依存しています。ブラウザでYouTubeを開くと、すべてのデータはInnerTubeのJSONエンドポイントから取得されます。これらのエンドポイントは公式にドキュメント化されていませんが、公開データにアクセスするための効率的なルートです。

主要なInnerTubeエンドポイント

エンドポイント用途キーパラメータ
/youtubei/v1/next動画ページ情報・コメント・関連動画videoId, continuation
/youtubei/v1/search検索結果query, params
/youtubei/v1/browseチャンネルページ・プレイリストbrowseId
/youtubei/v1/player動画プレイヤー情報(ストリームURL含む)videoId

InnerTubeリクエストの構造

InnerTubeへのリクエストには、特定のヘッダーとボディ構造が必要です:

{
  "context": {
    "client": {
      "clientName": "WEB",
      "clientVersion": "2.20240101.00.00",
      "hl": "ja",
      "gl": "JP"
    }
  },
  "videoId": "dQw4w9WgXcQ"
}

clientNameclientVersionは定期的に更新されるため、ブラウザのDevToolsで最新値を確認することが重要です。

continuationトークンによるページネーション

InnerTubeはページネーションにcontinuationトークンを使用します。最初のリクエストのレスポンスに含まれるcontinuationItemsからトークンを抽出し、次のリクエストのcontinuationパラメータに渡すことで、次のページのデータを取得できます:

# 最初のリクエスト → continuationトークンを取得
# /youtubei/v1/next に continuation トークンを送信
{
  "context": { ... },
  "continuation": "4QMf4...(長いトークン文字列)"
}

コメントスレッドでは、1リクエストあたり約20件のコメントが返されるため、大規模収集には数十回のcontinuationリクエストが必要です。

なぜレジデンシャルプロキシが必要なのか

Googleは、データセンターIPアドレスからの異常なリクエストを極めて高い精度で検出します。理由は明確です:

  • IPレピュテーション:AWS、GCP、AzureなどのデータセンターIPレンジは既知のリストとして管理されており、ボットトラフィックのフラグが立てられやすい
  • 行動パターン分析:同一IPからの大量リクエストは、人間のブラウジングパターンと明らかに異なる
  • ブラウザフィンガープリント:ヘッダーの順序、TLSフィンガープリント、JavaScript実行環境の不一致が検出信号となる

データセンタープロキシでYouTubeをスクレイピングすると、次のような問題が発生します:

  • HTTP 429(Too Many Requests)またはCAPTCHAチャレンジが頻発
  • 一時的なIPブロック(通常30分〜24時間)
  • リクエストは成功するが、データが制限される(コメントが一部しか返されない等)

レジデンシャルプロキシは、実際のISPに割り当てられたIPアドレスを使用するため、Googleのフィルターを通過する確率が大幅に向上します。各リクエストが異なる住宅IPから発信されることで、複数のユーザーが自然にアクセスしているように見えます。

Python実装:字幕取得+InnerTubeコメント収集+プロキシローテーション

例1:youtube-transcript-apiで字幕を取得

youtube-transcript-apiは、YouTubeの公開字幕を簡単に取得できるPythonライブラリです。プロキシを設定することで、大規模な字幕収集でもブロックを回避できます:

from youtube_transcript_api import YouTubeTranscriptApi

# ProxyHat レジデンシャルプロキシを設定
proxy_url = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"

proxies = {
    "http": proxy_url,
    "https": proxy_url,
}

video_id = "dQw4w9WgXcQ"

try:
    transcript_list = YouTubeTranscriptApi.get_transcript(
        video_id,
        languages=["ja", "en"],
        proxies=proxies
    )
    for snippet in transcript_list:
        print(f"[{snippet['start']:.1f}s] {snippet['text']}")
except Exception as e:
    print(f"字幕取得エラー: {e}")

例2:InnerTube APIでコメントスレッドを収集

InnerTubeエンドポイントを直接叩いて、コメントをページネーション付きで取得する実装です。プロキシローテーションを組み込むことで、大量リクエスト時のブロックを防止します:

import requests
import json
import time
import random

INNER_TUBE_URL = "https://www.youtube.com/youtubei/v1/next"
INNER_TUBE_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"

PROXY_USER = "user-country-US"
PROXY_PASS = "PASSWORD"
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080

def get_proxy_url(session_id=None):
    """ローテーション付きプロキシURLを生成"""
    if session_id:
        user = f"{PROXY_USER}-session-{session_id}"
    else:
        user = PROXY_USER
    return f"http://{user}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"

def build_context():
    return {
        "context": {
            "client": {
                "clientName": "WEB",
                "clientVersion": "2.20240601.00.00",
                "hl": "ja",
                "gl": "JP"
            }
        }
    }

def fetch_comments(video_id, max_pages=10):
    """InnerTube APIでコメントスレッドを取得"""
    comments = []
    continuation_token = None

    for page in range(max_pages):
        # 5リクエストごとにプロキシIPをローテーション
        session_id = f"yt-{page // 5}"
        proxy_url = get_proxy_url(session_id)
        proxies = {"http": proxy_url, "https": proxy_url}

        payload = build_context()
        if continuation_token:
            payload["continuation"] = continuation_token
        else:
            payload["videoId"] = video_id

        headers = {
            "Content-Type": "application/json",
            "User-Agent": (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/125.0.0.0 Safari/537.36"
            ),
            "Referer": f"https://www.youtube.com/watch?v={video_id}",
        }

        try:
            resp = requests.post(
                f"{INNER_TUBE_URL}?key={INNER_TUBE_KEY}",
                json=payload,
                headers=headers,
                proxies=proxies,
                timeout=15
            )
            resp.raise_for_status()
            data = resp.json()
        except requests.RequestException as e:
            print(f"リクエストエラー (page {page}): {e}")
            break

        # コメントを抽出
        new_comments = extract_comments(data)
        if not new_comments:
            break
        comments.extend(new_comments)
        print(f"ページ {page + 1}: {len(new_comments)}件のコメントを取得")

        # 次のcontinuationトークンを探す
        continuation_token = find_continuation(data)
        if not continuation_token:
            break

        # 人間らしいレート制限
        time.sleep(random.uniform(2.0, 5.0))

    return comments

def extract_comments(data):
    """レスポンスJSONからコメントテキストを抽出"""
    comments = []
    # レスポンス構造はネストが深いため、再帰的に検索
    def find_comment_renderers(obj):
        if isinstance(obj, dict):
            if "commentRenderer" in obj:
                renderer = obj["commentRenderer"]
                text = ""
                runs = renderer.get("contentText", {}).get("runs", [])
                for run in runs:
                    text += run.get("text", "")
                comments.append({
                    "author": renderer.get("authorText", {}).get("simpleText", ""),
                    "text": text,
                    "likes": renderer.get("voteCount", {}).get("simpleText", "0"),
                    "time": renderer.get("publishedTimeText", {}).get("runs", [{}])[0].get("text", "")
                })
            for v in obj.values():
                find_comment_renderers(v)
        elif isinstance(obj, list):
            for item in obj:
                find_comment_renderers(item)

    find_comment_renderers(data)
    return comments

def find_continuation(data):
    """レスポンスJSONからcontinuationトークンを抽出"""
    def search(obj):
        if isinstance(obj, dict):
            if "continuationItemRenderer" in obj:
                cont = obj["continuationItemRenderer"]
                endpoint = cont.get("continuationEndpoint", {})
                token = endpoint.get("continuationCommand", {}).get("token")
                if token:
                    return token
            for v in obj.values():
                result = search(v)
                if result:
                    return result
        elif isinstance(obj, list):
            for item in obj:
                result = search(item)
                if result:
                    return result
        return None

    return search(data)

# 実行例
if __name__ == "__main__":
    video_id = "dQw4w9WgXcQ"
    comments = fetch_comments(video_id, max_pages=20)
    print(f"\n合計 {len(comments)} 件のコメントを取得")
    for c in comments[:5]:
        print(f"  {c['author']}: {c['text'][:80]}...")

例3:Node.jsでInnerTube検索を実行

const https = require('https');

const INNER_TUBE_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
const PROXY_AUTH = 'user-country-US:PASSWORD';
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = 8080;

async function searchYouTube(query) {
  const payload = JSON.stringify({
    context: {
      client: {
        clientName: 'WEB',
        clientVersion: '2.20240601.00.00',
        hl: 'ja',
        gl: 'JP'
      }
    },
    query: query
  });

  // HTTPプロキシ経由でリクエスト(Node.jsのhttps-proxy-agentを使用推奨)
  const options = {
    hostname: PROXY_HOST,
    port: PROXY_PORT,
    path: `/youtubei/v1/search?key=${INNER_TUBE_KEY}`,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      'Proxy-Authorization': 'Basic ' + Buffer.from(PROXY_AUTH).toString('base64')
    }
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    });
    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}

// 実行例
(async () => {
  const results = await searchYouTube('AI ニュース 2024');
  console.log(JSON.stringify(results, null, 2).slice(0, 500));
})();

例4:curlで動画メタデータを取得

curl -x http://user-country-US:PASSWORD@gate.proxyhat.com:8080 \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' \
  -d '{"context":{"client":{"clientName":"WEB","clientVersion":"2.20240601.00.00","hl":"ja","gl":"JP"}},"videoId":"dQw4w9WgXcQ"}' \
  'https://www.youtube.com/youtubei/v1/next?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'

レート制限とローテーション戦略

YouTubeスクレイピングにおける最大の技術的課題は、Googleのレート制限とボット検出を回避しながら、十分なスループットを維持することです。以下のベストプラクティスを実装してください:

リクエスト間隔の設計

  • 基本間隔:2〜5秒のランダムな間隔を設定(人間のブラウジングパターンをシミュレート)
  • バースト防止:1IPあたり1分間に10リクエストを上限に設定
  • 429応答の処理:指数バックオフで再試行(初期5秒、最大300秒)

IPローテーションパターン

ProxyHatのレジデンシャルプロキシでは、ユーザー名にフラグを追加してIPローテーションを制御できます:

  • リクエストごとのローテーションuser-country-US:PASSWORD — 各リクエストで異なるIPを割り当て
  • スティッキーセッションuser-country-US-session-abc123:PASSWORD — 同一セッションIDで同じIPを維持(最大30分)

コメントのページネーションなど、一連のリクエストが同一IPから発信されるべき場合はスティッキーセッションを使用し、独立した動画メタデータの取得にはリクエストごとのローテーションを使用します。

フィンガープリントリスクの軽減

  • User-Agentの一貫性:プロキシIPのロケーションとUAの言語設定を一致させる(日本IPならjaのUA)
  • TLSフィンガープリントrequestsライブラリの代わりにcurl_cffitls_clientを使用すると、ChromeのTLSフィンガープリントを模倣できる
  • Accept-Languageヘッダー:IPの地理的位置と一致する言語を設定

プロキシタイプの比較:YouTubeスクレイピング向け

プロキシタイプ成功率レイテンシコスト適したユースケース
データセンター低(30〜50%)低(50〜150ms)少量のメタデータ取得のみ
レジデンシャル(ローテーション)高(90〜98%)中(200〜800ms)大規模コメント収集、検索スクレイピング
レジデンシャル(スティッキー)高(90〜98%)中(200〜800ms)セッション維持が必要なページネーション
モバイル最高(95〜99%)高(500〜1500ms)最も厳格なボット検出を回避する場合

ほとんどのYouTubeスクレイピングでは、レジデンシャルプロキシがコストと成功率の最適なバランスを提供します。詳細はProxyHatの料金プランを参照してください。

ジオターゲティングの活用

YouTubeのコンテンツは視聴者の地理位置に大きく依存します。トレンド動画、推薦動画、広告はすべて国・地域ごとに異なります。ProxyHatのジオターゲティング機能を活用することで、特定市場のデータを正確に収集できます:

# 米国のトレンドを取得
proxy_us = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"

# ドイツのトレンドを取得
proxy_de = "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080"

# 都市レベルのターゲティング(ベルリン)
proxy_berlin = "http://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080"

これにより、各国のInnerTubeリクエストでglパラメータとIPの位置を一致させることができます。不一致があると、YouTubeが位置を上書きして期待した地域のデータが返されない場合があります。対応ロケーションの詳細はProxyHatのロケーション一覧を参照してください。

倫理的スクレイピングと公式APIの使い分け

倫理的ガイドライン

  • クリエイターの権利を尊重:動画コンテンツそのものやトランスクリプトを抽出しても、原著者の許可なく再配布・再パッケージングしない
  • robots.txtを確認:YouTubeのrobots.txtはInnerTubeパスをブロックしていますが、これは検索エンジン向けの指示であり、公開データへのアクセス自体を禁じるものではありません。ただし、アクセス頻度は常識の範囲内に留めるべきです
  • 個人データの取り扱い:コメントには個人が特定可能な情報が含まれる可能性がある。GDPRやCCPAの対象となる場合は、適切な匿名化と法的根拠が必要
  • サーバーへの配慮:過度なリクエストはYouTubeのインフラに負荷をかける。レート制限を遵守し、オフピーク時間を活用する

公式APIを使うべき場面

スクレイピングが常に最適解とは限りません。以下の場合は公式APIを使用してください:

  • 少量のデータ収集:1日あたりのリクエストがクオータ内で収まる場合
  • 本番環境での安定性が重要:InnerTubeエンドポイントは予告なく変更される可能性がある
  • チャンネルオーナーの同意がある:OAuth認証でアクセスできるデータは公式APIで取得
  • 法的コンプライアンスが最優先:規約違反のリスクを完全に排除したい場合

ハイブリッドアプローチの推奨

実務的には、公式APIとスクレイピングのハイブリッドが最も効果的です:

  1. 公式APIで基本メタデータとチャンネル情報を取得(コスト1ユニット)
  2. InnerTubeでコメントスレッドと関連動画を取得(APIクオータを消費しない)
  3. youtube-transcript-apiで字幕を取得(APIでは不可能)
  4. レジデンシャルプロキシでローテーションしながら大規模に実行

このアプローチにより、APIクオータを節約しつつ、APIでは取得不可能なデータにもアクセスできます。Webスクレイピングのベストプラクティスについては、ProxyHatのWebスクレイピングユースケースも参照してください。

Key Takeaways:重要なポイント

  • YouTube Data API v3の1日10,000クオータユニットは、大規模データ収集には不十分。特にsearch.listは1回100ユニットを消費
  • InnerTube API(/youtubei/v1/next, /youtubei/v1/search等)は、公開データにアクセスするための効率的なルート
  • continuationトークンによるページネーションで、コメントスレッドを段階的に取得可能
  • データセンターIPはGoogleにフラグ付けされやすいため、レジデンシャルプロキシが大規模スクレイピングには必須
  • リクエスト間隔2〜5秒、1IPあたり1分間10リクエスト上限で、人間らしいパターンを維持
  • スティッキーセッションはページネーションに、リクエストごとのローテーションは独立した取得に適している
  • クリエイターの権利を尊重し、トランスクリプトやスクレイピングしたコンテンツの再配布は避ける
  • 公式APIとスクレイピングのハイブリッドアプローチが、コスト効率とデータカバレッジの最適解

YouTube公開データの抽出は、メディアアナリティクスやクリエイターエコノミー研究において不可欠な手法です。ProxyHatのレジデンシャルプロキシと適切なローテーション戦略を組み合わせることで、大規模なデータ収集を安定的に実行できます。今すぐProxyHatを試して、YouTubeデータ抽出のスループットを向上させましょう。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る