この記事では、YouTube Data API v3を使って、指定したチャンネルの動画一覧を取得し、CSVに保存する方法をまとめます。
自分のデータに合う実行経路を選ぶ
以下はWindows PowerShellでの手順です。Python 3.10以上を用意し、必要なファイルを保存したフォルダーをエクスプローラーで開き、アドレス欄に powershell と入力してEnterを押します。Get-Location で作業フォルダーを確認してください。コード全文は後ろに掲載しています。選んだ経路だけを実行し、別の経路のコードを同じファイルへつなげないでください。
動画ID一覧の取得と、保存一覧への統計追加はいずれもYouTube API通信・APIキーが必要です。分析用CSVが完成済みなら 通信なしの集計・可視化へ進みます。キーなしの練習は 架空サンプルの可視化で可能です。
一覧をこれから取得する:完全版か条件記録付き版の一方
「実行コード全体」のPythonコード1個だけを youtube_channel_videos.py に保存します。その前の分割説明コードをさらに追加しません。同じフォルダーへAPIキーを入れた .env を置きます。以下は完全版を1回実行する経路で、main()内のchannel_idを実際の値にしてから実行します。
python -m pip install requests python-dotenv
python youtube_channel_videos.py取得範囲・時点も残すなら上の実行コマンドの代わりに、条件記録付きコードを run_channel_list_recorded.py に保存して次を1回実行します。youtube_channel_videos.pyは必要ですが、importではmain()は走りません。両方を続けて実行して二重取得しないでください。記録付き版のchannel_idとmax_pagesはそちらのファイル内で設定します。
python run_channel_list_recorded.py完全版はmain()の既存呼び出し、記録付き版はmax_pages変数が取得範囲を決めます。初回は1ページ。全ページを選ぶ場合だけ対応箇所をNoneへ変え、下に新たな取得呼び出しを追加しません。相対出力は作業フォルダー基準、完全版の.envはスクリプト横です。同じフォルダーから実行すれば両者が揃います。
一覧CSVがある:IDから統計を追加する(API通信あり)
統計追加コードだけを enrich_channel_statistics.py へ保存します。同じ作業フォルダーの outputs/youtube/channel_videos.csv を読み、IDで対応付けます。完全版をもう一度実行する必要はありません。.envとrequests・python-dotenvは必要です。
python -m pip install requests python-dotenv
python enrich_channel_statistics.py最初に確認するのは一覧の channel_videos.csv と、追加版の channel_videos_with_statistics.csv(いずれもoutputs/youtube内)です。追加版で detail_status=not_returned の行を確認すると、詳細が応答に含まれなかった動画が分かります。削除や非公開とは断定しません。returnedの統計欄の0と空欄も別です。対応する.metadata.jsonに取得時点・件数・not_returned_idsがあります。条件記録付き版はsavedとrows、統計版は保存件数の表示を確認します。
掲載コードと詳しい条件
チャンネルの動画一覧を取得する方法はいくつかありますが、この記事では channels.list でアップロード用再生リストIDを取得し、playlistItems.list で動画一覧をたどる方法を使います。特定チャンネルの投稿一覧を取得する目的に直接対応した経路です。
取得対象は、YouTube Data APIで取得できる公開動画のメタデータです。非公開動画や削除済み動画、権限のない情報は通常取得できません。実行時はYouTube API Servicesの利用規約やクォータ制限を確認し、必要以上に繰り返し取得しないようにします。
この記事でできること
- チャンネルIDからアップロード動画一覧を取得する
nextPageTokenを使って複数ページを取得する- 動画ID、タイトル、公開日、説明文などをCSVに保存する
- 必要に応じて再生数などの統計情報を追加する
search.listとの使い分けを理解する
全体の流れ
- APIキーを読み込む
channels.listでチャンネルのuploadsplaylist IDを取得するplaylistItems.listでアップロード動画一覧を取得する- ページングしながら全件を集める
- CSVに保存する
公式ドキュメントでも、チャンネルにアップロードされた動画を取得する例では、チャンネルの uploads playlistを取得してから playlistItems.list を呼び出す流れが紹介されています。
PlaylistItems: list - YouTube Data API
search.listを使わない理由
search.list でもチャンネルIDを指定して動画を検索できます。ただし、検索APIは検索結果として返るため、チャンネルの投稿一覧を安定して順番に集めたい用途では、uploads playlistを使うほうが素直です。
| 方法 | 向いている用途 | 補足 |
|---|---|---|
playlistItems.list | 特定チャンネルの投稿動画一覧 | uploads playlistをたどる。1回1ユニット(通常のクォータ枠) |
search.list | キーワードや条件で探す | 検索用途。1回1ユニット/専用枠は標準100回・日 |
API仕様確認:2026年9月11日(記事の更新日時とは別に、公式仕様を照合した日です)。Googleのクォータ表では、playlistItems.listもsearch.listも1回の呼び出しにつき1ユニットです。ただし、search.listはSearch Queries専用のクォータ枠で、標準上限は1日100回です。playlistItems.listなどの通常枠は、他の対象APIと合計で標準1日10,000ユニットです。追加ページの取得も1回ずつ数えます。1回あたりのコスト差ではなく、投稿一覧にはuploads playlist、キーワード・条件検索にはsearch.listと、目的に応じて使い分けます。実際の割当はGoogle Cloud Consoleで確認してください。2026年6月1日の更新履歴では、search.listとvideos.insertの専用枠への移行が案内されています。
Quota Calculator - YouTube Data API
事前準備
APIキーは .env に保存します。
YOUTUBE_API_KEY=ここにAPIキーを書く必要なライブラリをインストールします。
APIキーの管理方法は、次の記事で整理しています。
APIキーを外部ファイルで管理する方法|.envとconfig.iniの使い分け
チャンネルIDからuploads playlist IDを取得する
まず、channels.list を使って、チャンネルのアップロード動画が入っている再生リストIDを取得します。
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("YOUTUBE_API_KEY")
if not API_KEY:
raise RuntimeError("YOUTUBE_API_KEY が設定されていません")
def get_uploads_playlist_id(channel_id: str) -> str:
url = "https://www.googleapis.com/youtube/v3/channels"
params = {
"part": "contentDetails",
"id": channel_id,
"key": API_KEY,
}
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
status = getattr(exc.response, "status_code", None)
if status is not None:
raise RuntimeError(
f"HTTP {status}: APIキー・権限・quota等を確認してください。"
) from None
raise RuntimeError("通信に失敗しました。接続とタイムアウトを確認してください。") from None
data = response.json()
if not data["items"]:
raise RuntimeError("チャンネルが見つかりませんでした")
return data["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]HTTPエラー時はステータスと確認事項だけを表示し、通信エラー時もリクエストURLや例外全文は出力しません。APIキーを含む例外の連鎖表示も抑制しています。
uploads は、そのチャンネルに投稿された動画が入る再生リストです。以降は、この再生リストをページングしながら取得します。
動画一覧をページング取得する
playlistItems.list は、1回のリクエストで最大50件まで取得できます。さらに取得する場合は、レスポンスに含まれる nextPageToken を次のリクエストに渡します。
def get_playlist_items(playlist_id: str, max_pages: int | None = None) -> list[dict]:
url = "https://www.googleapis.com/youtube/v3/playlistItems"
items = []
page_token = None
page_count = 0
while True:
params = {
"part": "snippet,contentDetails",
"playlistId": playlist_id,
"maxResults": 50,
"key": API_KEY,
}
if page_token:
params["pageToken"] = page_token
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
status = getattr(exc.response, "status_code", None)
if status is not None:
raise RuntimeError(
f"HTTP {status}: APIキー・権限・quota等を確認してください。"
) from None
raise RuntimeError("通信に失敗しました。接続とタイムアウトを確認してください。") from None
data = response.json()
items.extend(data.get("items", []))
page_count += 1
if max_pages is not None and page_count >= max_pages:
break
page_token = data.get("nextPageToken")
if not page_token:
break
return itemsmax_pages を用意しておくと、動作確認時に最初の1ページだけ取得できます。いきなり全件取得するより、まず少量で確認するほうが安全です。
CSVに保存する
取得した動画一覧から、分析に使いやすい項目だけを取り出してCSVに保存します。
import csv
from pathlib import Path
def normalize_playlist_item(item: dict) -> dict:
snippet = item["snippet"]
content_details = item["contentDetails"]
return {
"video_id": content_details["videoId"],
"title": snippet.get("title", ""),
"published_at": content_details.get("videoPublishedAt", ""),
"channel_id": snippet.get("channelId", ""),
"channel_title": snippet.get("channelTitle", ""),
"position": snippet.get("position", ""),
"description": snippet.get("description", ""),
}
def save_csv(rows: list[dict], csv_path: Path) -> None:
csv_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"video_id",
"title",
"published_at",
"channel_id",
"channel_title",
"position",
"description",
]
with csv_path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)説明文は長くなることがあります。CSVで扱いにくい場合は、最初は説明文を外して保存しても問題ありません。
実行コード全体
ここまでをまとめたコードです。まずは max_pages=1 で動作確認し、問題なければ None に変更します。
import csv
from pathlib import Path
import os
import requests
from dotenv import load_dotenv
load_dotenv(Path(__file__).with_name(".env"))
API_KEY = os.getenv("YOUTUBE_API_KEY")
if not API_KEY:
raise RuntimeError("YOUTUBE_API_KEY が設定されていません")
def get_uploads_playlist_id(channel_id: str) -> str:
url = "https://www.googleapis.com/youtube/v3/channels"
params = {
"part": "contentDetails",
"id": channel_id,
"key": API_KEY,
}
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
status = getattr(exc.response, "status_code", None)
if status is not None:
raise RuntimeError(
f"HTTP {status}: APIキー・権限・quota等を確認してください。"
) from None
raise RuntimeError("通信に失敗しました。接続とタイムアウトを確認してください。") from None
data = response.json()
if not data["items"]:
raise RuntimeError("チャンネルが見つかりませんでした")
return data["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
def get_playlist_items(playlist_id: str, max_pages: int | None = None) -> list[dict]:
url = "https://www.googleapis.com/youtube/v3/playlistItems"
items = []
page_token = None
page_count = 0
while True:
params = {
"part": "snippet,contentDetails",
"playlistId": playlist_id,
"maxResults": 50,
"key": API_KEY,
}
if page_token:
params["pageToken"] = page_token
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
status = getattr(exc.response, "status_code", None)
if status is not None:
raise RuntimeError(
f"HTTP {status}: APIキー・権限・quota等を確認してください。"
) from None
raise RuntimeError("通信に失敗しました。接続とタイムアウトを確認してください。") from None
data = response.json()
items.extend(data.get("items", []))
page_count += 1
if max_pages is not None and page_count >= max_pages:
break
page_token = data.get("nextPageToken")
if not page_token:
break
return items
def normalize_playlist_item(item: dict) -> dict:
snippet = item["snippet"]
content_details = item["contentDetails"]
return {
"video_id": content_details["videoId"],
"title": snippet.get("title", ""),
"published_at": content_details.get("videoPublishedAt", ""),
"channel_id": snippet.get("channelId", ""),
"channel_title": snippet.get("channelTitle", ""),
"position": snippet.get("position", ""),
"description": snippet.get("description", ""),
}
def save_csv(rows: list[dict], csv_path: Path) -> None:
csv_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"video_id",
"title",
"published_at",
"channel_id",
"channel_title",
"position",
"description",
]
with csv_path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def main() -> None:
channel_id = "UCxxxxxxxxxxxxxxxxxxxxxx" # 対象チャンネルのIDへ変更
uploads_playlist_id = get_uploads_playlist_id(channel_id)
print("uploads playlist:", uploads_playlist_id)
items = get_playlist_items(uploads_playlist_id, max_pages=1)
rows = [normalize_playlist_item(item) for item in items]
csv_path = Path("outputs/youtube/channel_videos.csv")
save_csv(rows, csv_path)
print(f"saved: {csv_path}")
print(f"rows: {len(rows)}")
if __name__ == "__main__":
main()
Python 3.10以上で、上の完全版コードを youtube_channel_videos.py として保存します。同じフォルダーにAPIキーを設定した .env を置き、channel_id を対象のチャンネルIDに変更してください。Windowsのターミナルで保存先フォルダーに移動して実行します。
正常終了時の出力例(以下のID・動画名・件数は説明用の架空データです):
uploads playlist: UUexample
saved: outputs\youtube\channel_videos.csv
rows: 3CSVの列と内容の見本です。先頭は見出し、続く3行が動画データです。実際の保存内容はAPIの応答によって異なります。
video_id,title,published_at,channel_id,channel_title,position,description
example01,サンプル動画1,2026-01-03T09:00:00Z,UCexample,サンプルチャンネル,0,説明1
example02,サンプル動画2,2026-01-02T09:00:00Z,UCexample,サンプルチャンネル,1,説明2
example03,サンプル動画3,2026-01-01T09:00:00Z,UCexample,サンプルチャンネル,2,説明3全件取得する場合は、上の完全版コードの main() 内にある既存の get_playlist_items() 呼び出しを、max_pages=1 から max_pages=None に変更します。次の行は差し替え後の例です。呼び出しを追加するのではなく、既存の行を書き換えてください。
items = get_playlist_items(uploads_playlist_id, max_pages=None)一覧の取得範囲・取得時点も保存する場合
既存CSVがある方は取り直さず、次の統計追加へ進んでください。今後の取得で条件も残す場合のみ、上の完全版を youtube_channel_videos.py として保存し、次を同じフォルダーの run_channel_list_recorded.py に保存します。完全版の直接実行に代わる手順で、両方を続けて実行しません。channel_idを設定し、実行するとCSVと同名のmetadataを保存します。max_pages=1は最大1ページ、Noneはその時点でAPIからたどれる全ページです。
"""Alternative to running youtube_channel_videos.py directly; not an extra fetch step."""
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from youtube_channel_videos import get_uploads_playlist_id, get_playlist_items, normalize_playlist_item, save_csv
channel_id = "UCxxxxxxxxxxxxxxxxxxxxxx" # Change to the actual channel ID.
max_pages = 1 # None follows all API pages available at acquisition.
output = Path("outputs/youtube/channel_videos.csv")
sidecar = output.with_suffix(".metadata.json")
if output.exists() or sidecar.exists():
raise SystemExit("既存CSVを保持します。別フォルダーを指定してください。再取得は不要です。")
started = datetime.now(timezone.utc).isoformat()
playlist_id = get_uploads_playlist_id(channel_id)
items = get_playlist_items(playlist_id, max_pages=max_pages)
finished = datetime.now(timezone.utc).isoformat()
rows = [normalize_playlist_item(item) for item in items]
save_csv(rows, output)
metadata = {"csv_file": output.name, "csv_sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
"channel_ids": [channel_id], "list_started_at": started, "list_finished_at": finished, "saved_rows": len(rows),
"scope": {"source": "uploads playlist", "max_pages": max_pages,
"coverage": "API pagination exhausted" if max_pages is None else "at most specified pages"}}
sidecar.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"saved: {output.resolve()} / rows: {len(rows)}")
保存済みCSVへ統計情報を追加する
新しいPython実行から outputs/youtube/channel_videos.csv を読み、動画IDで統計を結合します。次を enrich_channel_statistics.py に保存し、同じフォルダーの.envへAPIキーを設定します。requestsとpython-dotenvを使い、ほかのPythonファイルの変数には依存しません。元一覧は維持し、channel_videos_with_statistics.csv とmetadataを別保存します。
"""Run from the project directory. Input CSV is never overwritten."""
import csv
import hashlib
import json
import os
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
import requests
from dotenv import load_dotenv
def utc_now():
return datetime.now(timezone.utc).isoformat()
def read_source(path):
with path.open(encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
columns = reader.fieldnames or []
if "video_id" not in columns:
raise ValueError("video_id列が必要です。動画一覧CSVを確認してください。")
rows = list(reader)
if any(not (r.get("video_id") or "").strip() for r in rows):
raise ValueError("空の動画IDがあります。元CSVを確認してください。")
for r in rows:
r["video_id"] = r["video_id"].strip()
meta_path = path.with_suffix(".metadata.json")
source_meta = {"scope": "UNKNOWN", "list_started_at": None, "list_finished_at": None}
digest = hashlib.sha256(path.read_bytes()).hexdigest()
if meta_path.exists():
source_meta = json.loads(meta_path.read_text(encoding="utf-8"))
if source_meta.get("csv_sha256") != digest:
raise ValueError("CSVとmetadataのhashが一致しません。同じ取得回の組を選んでください。")
return rows, columns, source_meta, digest
def get_video_statistics(video_ids, api_key):
result = {}
for start in range(0, len(video_ids), 50):
ids = video_ids[start:start + 50]
response = requests.get("https://www.googleapis.com/youtube/v3/videos",
params={"part": "statistics,contentDetails", "id": ",".join(ids), "key": api_key}, timeout=30)
response.raise_for_status()
for item in response.json().get("items", []):
key = item["id"]
if key not in ids or key in result:
raise ValueError("応答IDが要求IDと整合しません。保存を中止します。")
stats = item.get("statistics", {})
result[key] = {name: stats.get(api_name) for name, api_name in
[("view_count", "viewCount"), ("like_count", "likeCount"), ("comment_count", "commentCount")]}
result[key]["duration"] = item.get("contentDetails", {}).get("duration")
return result
def enrich(source, output, api_key):
if source.resolve() == output.resolve():
raise ValueError("元CSVと保存先は分けてください。")
meta_path = output.with_suffix(".metadata.json")
if output.exists() or meta_path.exists():
raise FileExistsError("保存先が存在します。別名を指定してください。")
rows, columns, source_meta, digest = read_source(source)
counts = Counter(r["video_id"] for r in rows)
ids = list(counts)
added = ["view_count", "like_count", "comment_count", "duration", "detail_status", "statistics_fetched_at"]
if set(added) & set(columns):
raise ValueError("統計追加前の一覧CSVを入力してください。")
started = utc_now()
# All requests must succeed before any result CSV is written.
stats = get_video_statistics(ids, api_key) if ids else {}
finished = utc_now()
for row in rows:
found = stats.get(row["video_id"])
row.update(found or {k: None for k in added[:4]})
row["detail_status"] = "returned" if found is not None else "not_returned"
row["statistics_fetched_at"] = finished
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("x", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=columns + added)
writer.writeheader()
writer.writerows(rows)
metadata = {"csv_file": output.name, "csv_sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
"source_file": str(source), "source_sha256": digest, "source_metadata": source_meta,
"scope": source_meta.get("scope", "UNKNOWN"),
"statistics_started_at": started, "statistics_finished_at": finished,
"input_rows": len(rows), "unique_ids": len(ids), "saved_rows": len(rows),
"duplicate_ids": {k: v for k, v in counts.items() if v > 1},
"not_returned_ids": [key for key in ids if key not in stats],
"note": "重複行は維持し、API要求のみIDを一意化。未応答の理由は未確認。"}
meta_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"saved: {output.resolve()} / rows: {len(rows)} / unique IDs: {len(ids)}")
return rows, metadata
if __name__ == "__main__":
load_dotenv(Path(__file__).with_name(".env"))
source = Path("outputs/youtube/channel_videos.csv")
output = Path("outputs/youtube/channel_videos_with_statistics.csv")
# Empty input needs no API key and makes no API request.
key = os.getenv("YOUTUBE_API_KEY", "")
try:
rows, _, _, _ = read_source(source)
if rows and not key:
raise ValueError(".envのYOUTUBE_API_KEYを確認してください。")
enrich(source, output, key)
except (requests.RequestException, ValueError, OSError) as exc:
# Request URLs can contain a key; do not print the exception text.
raise SystemExit(f"保存未完了: {type(exc).__name__}。入力列・保存先・認証・通信を確認してください。") from None
detail_status=returnedでview_count=0なら実測0、returnedで統計欄が空なら項目欠損、not_returnedなら動画詳細自体が応答に含まれなかった状態です。未応答の理由を削除済み・非公開とは断定しません。重複IDはAPI要求だけ一意化し、元CSVの行と順序は保持します。metadataのduplicate_idsとnot_returned_idsで確認できます。
0行でも見出し付きCSVとmetadataを保存し、APIは呼びません。API要求が1つでも失敗すると結果CSVの書き込み前に停止します。ディスク書き込み失敗時はmetadataがない不完全な出力を成功扱いしないでください。既存の出力には上書きしません。CSVをpandasや表計算で開き、行数・detail_status・空欄と0を確認します。元一覧のmetadataがない場合は取得時点・範囲を不明のまま記録し、統計取得開始・終了と混同しません。
取得後にできる分析
- 投稿日ごとの投稿本数を集計する
- タイトルに含まれる単語を集計する
- 再生数の多い動画と投稿時期を比較する
- 動画時間と再生数の関係を見る
- 特定シリーズや企画の投稿頻度を確認する
この段階では、まだ「動画一覧の取得」が目的です。分析記事に使う場合は、取得日、対象チャンネル、取得項目、欠損の有無をメモしておくと、後で読み返しやすくなります。
動画一覧を取得したあとに、通常動画・ライブ配信アーカイブ・Shortsなどの動画種別や、ゲーム・雑談・音楽などの独自カテゴリを付けたい場合は、YouTube APIで取得した動画一覧を分析用データセットに整える方法で整理しています。
よくあるつまずき
チャンネルIDがわからない
チャンネルURLがハンドル形式の場合は、channels.listのforHandleにハンドルを指定して、応答のチャンネルIDを確認できます。先頭の@は付けても省略しても構いません。取得したIDを、このページのプログラムで指定するチャンネルIDとして使います。ハンドルが分かっている場合は、キーワード検索で候補を探す必要はありません。
取得件数が想定より少ない
max_pages を指定している場合は、そのページ数で止まります。全件取得したい場合は max_pages=None にします。また、非公開動画や削除済み動画は通常の公開データとして取得できません。
クォータが不安
まず1ページだけ取得し、必要な項目が取れているか確認します。統計情報を追加する videos.list もコストは1ですが、不要な項目を何度も取り直さないように、CSVやJSONで途中保存しておくとよいです。
関連記事
- YouTube Data API v3入門|APIキーで動画情報を取得するPython手順
- APIキーを外部ファイルで管理する方法|.envとconfig.iniの使い分け
- YouTube APIで取得した動画一覧を分析用データセットに整える方法|動画種別・カテゴリ付与まで
- YouTubeの情報を取得する方法まとめ|公式API・字幕・チャット・文字起こしの使い分け
まとめ
チャンネルの動画一覧を取得する場合は、channels.list で uploads playlist IDを取得し、playlistItems.list でページングしながら動画一覧を取得する流れが扱いやすいです。
投稿動画一覧にはuploads playlist、キーワード・条件検索にはsearch.listを使い分けます。どちらも1回1ユニットですが、search.listは独立した標準1日100回の枠です。CSVを保存すれば、同じ一覧を取り直さずに投稿頻度やタイトル傾向を集計でき、必要な動画の統計情報を追加する手順にも進めます。