この記事では、Pythonライブラリ youtube-transcript-api を使って、YouTube動画の字幕データを取得し、分析しやすい形に保存する方法をまとめます。
対象は、自分の動画、利用許諾のある動画、または権利関係を確認できる素材です。字幕本文は動画の内容そのものに近いため、取得したテキストをそのまま公開するのではなく、要約、分類、キーワード抽出などの分析前処理として扱う前提で進めます。
この記事でできること
youtube-transcript-apiで字幕を取得する- 取得できる字幕の言語や種類を確認する
- 字幕をCSVやテキストに保存する
- 手動字幕と自動生成字幕の違いを意識して扱う
- YouTube Data API、yt-dlp、Whisperとの使い分けを整理する
利用上の注意
youtube-transcript-api は、YouTube Data APIの公式機能ではありません。APIキーなしで字幕を取得できる便利なライブラリですが、YouTube側の仕様変更や制限の影響を受ける可能性があります。
- 自分の動画、許諾済み動画、または権利関係を確認できる動画を対象にしてください。
- 字幕全文をそのまま記事に転載する使い方は避けてください。
- 取得できる字幕がない動画もあります。
- 自動生成字幕には誤認識が含まれるため、重要な判断には元動画の確認が必要です。
- 大量取得や短時間の連続アクセスは避け、必要な範囲に絞って実行してください。
このサイトでは、字幕データは「動画内容を分析するための中間データ」として扱います。公開記事に使う場合は、全文引用ではなく、話題の分類や傾向の整理に寄せる方針です。
youtube-transcript-apiとは
youtube-transcript-api は、YouTube動画に付いている字幕・文字起こしデータをPythonから取得するためのライブラリです。公式READMEでは、手動字幕、自動生成字幕、翻訳字幕の取得に対応していることが説明されています。
ただし、これはYouTube Data API v3とは別のライブラリです。動画タイトル、再生数、チャンネル情報などのメタデータを安定して取得したい場合は、公式のYouTube Data APIを使うほうが向いています。
インストール
まず、Python環境にライブラリをインストールします。
pip install youtube-transcript-api仮想環境を使っている場合は、仮想環境を有効化した状態で実行してください。
動画IDを用意する
ライブラリに渡すのは、動画URL全体ではなく動画IDです。たとえば次のURLであれば、動画IDは dQw4w9WgXcQ の部分です。
https://www.youtube.com/watch?v=dQw4w9WgXcQURLから動画IDを取り出す簡単な関数を用意しておくと便利です。
11文字の動画ID、watch?v=、youtu.be/、/shorts/、/live/、/embed/ に対応します。後続例の video_id に extract_video_id("動画URL") の戻り値を渡してください。形式確認は動画や字幕の存在・取得成功を保証しません。
import re
from urllib.parse import parse_qs, urlparse
def extract_video_id(url_or_id: str) -> str:
value = url_or_id.strip()
if re.fullmatch(r"[A-Za-z0-9_-]{11}", value):
return value
parsed = urlparse(value)
if parsed.scheme not in {"https", "http"} or parsed.username or parsed.password:
raise ValueError("http(s)のYouTube URLまたは11文字の動画IDを指定してください")
host = (parsed.hostname or "").lower()
parts = parsed.path.strip("/").split("/")
video_id = None
if host in {"youtu.be", "www.youtu.be"} and len(parts) == 1:
video_id = parts[0]
elif host in {"youtube.com", "www.youtube.com", "m.youtube.com"}:
if parsed.path == "/watch":
ids = parse_qs(parsed.query).get("v", [])
if len(ids) == 1:
video_id = ids[0]
elif len(parts) == 2 and parts[0] in {"shorts", "live", "embed"}:
video_id = parts[1]
if not video_id or not re.fullmatch(r"[A-Za-z0-9_-]{11}", video_id):
raise ValueError("対応外のホスト・URL形式、または無効な動画IDです")
return video_id字幕を取得する基本コード
現在のREADMEでは、YouTubeTranscriptApi() のインスタンスを作って fetch() を呼び出す例が案内されています。
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ"
ytt_api = YouTubeTranscriptApi()
transcript = ytt_api.fetch(video_id, languages=["ja", "en"])
for snippet in transcript:
print(snippet.start, snippet.duration, snippet.text)languages=["ja", "en"] は、まず日本語字幕を探し、なければ英語字幕を探す指定です。対象動画に字幕がない場合や、指定した言語の字幕がない場合は取得できません。
CSVに保存する
分析前処理として使うなら、開始秒、継続時間、本文をCSVに保存しておくと扱いやすいです。
import csv
from pathlib import Path
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ"
output_dir = Path("outputs/subtitles")
output_dir.mkdir(parents=True, exist_ok=True)
ytt_api = YouTubeTranscriptApi()
transcript = ytt_api.fetch(video_id, languages=["ja", "en"])
csv_path = output_dir / f"{video_id}_subtitles.csv"
with csv_path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["start", "duration", "text"])
writer.writeheader()
for item in transcript:
writer.writerow({
"start": round(item.start, 2),
"duration": round(item.duration, 2),
"text": item.text,
})
print(f"saved: {csv_path}")CSVにしておくと、Google Sheetsやpandasで確認しやすくなります。字幕本文を公開するためではなく、後続の分類や要約に使う中間データとして保存します。
テキストとして保存する
要約やキーワード抽出に使うだけなら、字幕をつなげたテキストファイルも用意しておくと便利です。
from pathlib import Path
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ"
output_dir = Path("outputs/subtitles")
output_dir.mkdir(parents=True, exist_ok=True)
ytt_api = YouTubeTranscriptApi()
transcript = ytt_api.fetch(video_id, languages=["ja", "en"])
text = "\n".join(item.text for item in transcript)
txt_path = output_dir / f"{video_id}_subtitles.txt"
txt_path.write_text(text, encoding="utf-8")
print(f"saved: {txt_path}")後から公開記事に使う場合は、全文をそのまま載せるのではなく、話題の傾向や要点を自分の言葉で整理します。
利用可能な字幕を確認する
動画によって、手動字幕、自動生成字幕、翻訳可能な字幕の有無が異なります。先に利用可能な字幕を確認すると、取得できない理由を切り分けやすくなります。
from youtube_transcript_api import YouTubeTranscriptApi
video_id = "dQw4w9WgXcQ"
ytt_api = YouTubeTranscriptApi()
transcript_list = ytt_api.list(video_id)
for transcript in transcript_list:
print({
"language": transcript.language,
"language_code": transcript.language_code,
"is_generated": transcript.is_generated,
"is_translatable": transcript.is_translatable,
})is_generated が True の場合は、自動生成字幕です。自動生成字幕は便利ですが、固有名詞や専門用語、ゲーム名、アプリ名が崩れることがあります。
手動字幕と自動生成字幕の使い分け
| 種類 | 特徴 | 分析時の注意 |
|---|---|---|
| 手動字幕 | 投稿者や制作者が用意した字幕 | 比較的読みやすいが、完全な文字起こしとは限らない |
| 自動生成字幕 | YouTubeが音声から生成した字幕 | 誤認識が含まれやすいため、固有名詞や数字は確認する |
| 翻訳字幕 | 元字幕を別言語へ翻訳した字幕 | 分析する言語やニュアンスが変わる可能性がある |
分析目的であれば、手動字幕がある場合は手動字幕を優先し、自動生成字幕を使う場合は誤認識がある前提で扱うのがよいです。
取得できないときの主な理由
- 動画に字幕が付いていない
- 指定した言語の字幕が存在しない
- 年齢制限や地域制限などでアクセスできない
- YouTube側の仕様変更によりライブラリが一時的に動かない
- 短時間に多くのリクエストを送り、アクセスが制限されている
特に公開サービスやクラウド環境から大量に実行する場合、ブロックや制限に遭遇しやすくなります。必要な動画だけを対象にし、取得できない場合は無理に回避策へ寄せすぎないほうが運用しやすいです。
選んだ字幕をCSV・TXT・metadataへまとめて保存する
前の基本例とは別の実用例です。上の extract_video_id 関数を save_subtitles.py の先頭に保存し、その後に次のコードを続けてください。基本例のfetch処理を先に実行する必要はありません。字幕一覧を取得してから選択した本文を1回取得し、その同じ結果からCSVとTXTを作ります。通信全体が1回という意味ではありません。
import csv
import json
import tempfile
from pathlib import Path
from datetime import datetime, timezone
from youtube_transcript_api import (
YouTubeTranscriptApi, NoTranscriptFound, TranscriptsDisabled,
RequestBlocked, IpBlocked, VideoUnavailable,
)
def save_subtitles(video_id, languages=("ja",), output_root="outputs/subtitles", api=None):
api = api or YouTubeTranscriptApi()
available = api.list(video_id) # 字幕の一覧。本文取得とは別です。
for item in available:
print(item.language_code, "自動生成" if item.is_generated else "手動")
# 言語の優先順位を先に適用し、同言語では手動を優先します。
selected = None
for language in languages:
try:
selected = available.find_transcript([language])
break
except NoTranscriptFound:
continue
if selected is None:
raise NoTranscriptFound(video_id, list(languages), available)
fetched = selected.fetch() # 選択した字幕本文を1回取得し、両形式へ保存
rows = [{"start":x.start, "duration":x.duration, "text":x.text} for x in fetched]
if not rows or not any(r["text"].strip() for r in rows):
raise ValueError("字幕本文が空です。成功ファイルは作成しません")
fetched_at = datetime.now(timezone.utc).isoformat()
root = Path(output_root)
root.mkdir(parents=True, exist_ok=True)
# 毎回別フォルダー。旧成功結果を今回の失敗結果と混同しません。
run = Path(tempfile.mkdtemp(prefix=video_id + "_", dir=root))
metadata = {
"status":"WRITING", "video_id":video_id,
"language_code":fetched.language_code, "language":fetched.language,
"is_generated":fetched.is_generated, "translated":False,
"requested_languages":list(languages), "fetched_at_utc":fetched_at,
"rows":len(rows), "csv":"subtitles.csv", "txt":"subtitles.txt",
}
meta = run / "metadata.json"
def checkpoint():
meta.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
checkpoint()
try:
with (run / "subtitles.csv").open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["start", "duration", "text"])
writer.writeheader()
writer.writerows(rows)
(run / "subtitles.txt").write_text("\n".join(r["text"] for r in rows), encoding="utf-8")
metadata["status"] = "COMPLETE"
checkpoint() # この印がある場合のみ両ファイルの保存完了
except Exception:
metadata["status"] = "WRITE_FAILED"
checkpoint()
raise
print("保存完了:", run.resolve(), metadata["language_code"], metadata["is_generated"])
return run
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("video")
parser.add_argument("--english-fallback", action="store_true")
args = parser.parse_args()
# 上の extract_video_id 関数も、このファイルの先頭へ保存します。
try:
save_subtitles(extract_video_id(args.video), ("ja", "en") if args.english_fallback else ("ja",))
except TranscriptsDisabled:
raise SystemExit("字幕を利用できません(無効化・未提供)。成功出力なし")
except NoTranscriptFound:
raise SystemExit("希望言語の字幕がありません。英語許可とは別の条件です")
except (RequestBlocked, IpBlocked):
raise SystemExit("アクセスが制限されています。回避・連続再試行は行わず停止します")
except VideoUnavailable:
raise SystemExit("動画を利用できません。字幕なしとは区別してください")
except Exception as exc:
raise SystemExit("取得・保存未完了: " + type(exc).__name__)# 日本語のみ。なければ停止
python save_subtitles.py "許諾済み動画URL"
# 日本語がなければ英語も許可する場合だけ、こちらを使う
python save_subtitles.py "許諾済み動画URL" --english-fallback表示された新規フォルダーの subtitles.csv(start/duration/text)、subtitles.txt、metadata.json を確認します。metadataの COMPLETE は保存完了であり、字幕の正確さの保証ではありません。実際の言語、手動/自動、動画ID、UTC取得日時、件数を確認します。英語へ切り替わった結果を日本語字幕とは扱いません。この例は翻訳を要求しません。手動字幕も自動字幕もない場合、指定言語なし、アクセス制限、動画利用不可は別の終了案内です。保存途中の失敗はWRITING/WRITE_FAILEDで、成功出力として使いません。
他の方法との使い分け
| 方法 | 向いている用途 | 注意点 |
|---|---|---|
| YouTube Data API | 動画タイトル、チャンネル、再生数などのメタデータ取得 | APIキーだけで任意の公開動画の字幕本文を取得する用途とは異なります。公式のcaptions.downloadは、OAuth 2.0認証と対象動画の編集権限がある場合に字幕本文を取得できます。 |
| youtube-transcript-api | 字幕が付いている動画の字幕取得 | 非公式ライブラリなので仕様変更の影響を受ける |
| yt-dlp | 許諾済み素材の字幕・音声取得や前処理 | 利用範囲とサービス規約の確認が必要 |
| Whisper | 字幕がない許諾済み音声の文字起こし | 音声取得と文字起こし精度の確認が必要 |
字幕が用意されている動画なら youtube-transcript-api は軽く試せます。字幕がない許諾済み素材を扱う場合は、Whisperで文字起こしする流れを検討します。
分析に使うときの書き方
字幕データを記事に使う場合、字幕本文を長く引用するより、次のように処理内容と読み取り結果を分けて書くと自然です。
- 取得対象は許諾済み動画であること
- 字幕の種類が手動字幕か自動生成字幕か
- 分析前にどのような整形をしたか
- 頻出語、話題、時間帯ごとの変化など、集計結果として読める範囲
- 誤認識や字幕欠落の可能性
このあたりを明記しておくと、取得結果をそのまま正解として扱っていないことが読者にも伝わります。
関連記事
- YouTubeの情報を取得する方法まとめ|公式API・字幕・チャット・文字起こしの使い分け
- YouTube Data API v3入門|APIキーで動画情報を取得するPython手順
- yt-dlpで許諾済み動画の音声・字幕を分析用に取得する方法|Whisper文字起こし前処理
- Whisperで許諾済み音声を文字起こしして要約する方法|Pythonで分析前処理を整える
追加例の確認環境:Python 3.12、yt-dlp 2026.8.19、youtube-transcript-api 1.2.4、openai-whisper 20250625、FFmpeg 8.0。保存・言語選択は実ライブラリのTranscriptListと合成字幕で検証し、日本語・改行・引用符、英語切替、空字幕・アクセス制限時の分岐を確認しました。字幕本文の通信はモックであり、YouTubeからの実字幕取得は未検証です。
まとめ
youtube-transcript-api を使うと、字幕が用意されているYouTube動画から字幕データを取得できます。動画IDを指定し、言語を選び、CSVやテキストに保存しておくと、要約やキーワード抽出などの前処理に使いやすくなります。
一方で、公式APIではないこと、字幕本文はコンテンツそのものに近いこと、自動生成字幕には誤認識があることには注意が必要です。公開記事では全文転載ではなく、取得条件と限界を明記したうえで、傾向や話題の整理として活用するのがよいと思います。