YouTubeチャンネルの動画一覧を分析したいとき、まずはYouTube Data APIで動画ID、タイトル、投稿日、再生数などを取得します。
ただ、取得したデータをそのまま集計しようとすると、少し困ることがあります。通常動画なのか、ライブ配信アーカイブなのか、Shortsなのか。あるいは、ゲーム、雑談、音楽、解説など、どのような内容の動画なのか。こうした分析で見たい分類は、APIのレスポンスに完成した形で入っているわけではありません。
この記事では、YouTube Data APIで取得した動画一覧に、分析用の補助ラベルを付けてCSV化する流れを整理します。Vtuberや配信者チャンネルの活動傾向を見る場合にも使えますが、内容としてはYouTubeチャンネル全般に使える形を目指します。
この記事でやること
- YouTube APIでチャンネルの動画一覧を取得する
videos.listで再生数、動画時間、公式カテゴリなどを追加取得する- 通常動画、ライブ配信アーカイブ、Shortsなどの動画種別を付与する
- 公式カテゴリとは別に、分析用の独自カテゴリを付与する
- 後続の集計や可視化で使いやすいCSVとして保存する
この記事では、動画種別や独自カテゴリを「正解データ」として扱うのではなく、分析しやすくするための補助ラベルとして扱います。分類の精度を上げたい場合は、最終的に目視確認や手動修正を組み合わせる前提です。
前提記事
この記事は、YouTube Data APIの基本的な使い方をある程度理解している前提で進めます。APIキーの発行やチャンネル動画一覧の取得から確認したい場合は、先に以下の記事を見ると流れを追いやすいです。
- YouTube Data API v3入門|APIキー発行からPythonで動画情報を取得するまで
- YouTube Data APIでチャンネルの動画一覧を取得してCSV保存する方法|Python
- APIキーを外部ファイルで安全に管理する方法|.env・config.iniの使い分け
- YouTubeの情報を取得する方法まとめ|公式APIと非公式ライブラリの使い分け
YouTube APIで取得できる情報
YouTube Data APIのvideos.listでは、動画IDを指定して、タイトル、説明文、タグ、公式カテゴリID、動画時間、再生数、コメント数、ライブ配信関連の情報などを取得できます。
| 項目 | API上の主な場所 | 分析での使い方 |
|---|---|---|
| タイトル | snippet.title | キーワード分類、動画内容の確認 |
| 説明文 | snippet.description | ハッシュタグ、企画名、リンクの確認 |
| タグ | snippet.tags | 内容分類の補助 |
| 公式カテゴリID | snippet.categoryId | 大まかな公式分類 |
| ライブ状態 | snippet.liveBroadcastContent | ライブ中・予定配信の確認 |
| 動画時間 | contentDetails.duration | Shorts候補、長時間配信の判定 |
| 再生数 | statistics.viewCount | 人気動画や再生傾向の確認 |
| コメント数 | statistics.commentCount | 反応量の参考 |
| ライブ配信詳細 | liveStreamingDetails | ライブ配信アーカイブ候補の判定 |
一方で、分析でよく使いたくなる「通常動画」「ライブ配信アーカイブ」「Shorts」といった動画種別は、そのままの列として用意されているわけではありません。そのため、複数の項目を組み合わせて自分で補助ラベルを作ります。
動画種別は公式の正解ラベルではない
動画種別を付けるときに注意したいのは、API上に「これはShorts」「これは配信アーカイブ」といった分析用の完全な正解ラベルがあるわけではないことです。
ライブ配信についてはliveBroadcastContentやliveStreamingDetailsが参考になります。ただし、公開済みの配信アーカイブでは状態がnoneになっていることもあるため、ライブ配信詳細の有無や動画時間なども見ながら判断します。
Shortsについても、#shortsがタイトルや説明文に入っている場合は判定しやすいですが、すべてのShortsに必ず付いているとは限りません。また、60秒以内の通常動画もあるため、動画時間だけで断定するのも危険です。
この記事では、以下のような考え方で動画種別を付けます。
| 動画種別 | 判定に使う情報 | 注意点 |
|---|---|---|
live_archive | liveStreamingDetails、長い動画時間、ライブ関連情報 | 公開済みアーカイブではliveBroadcastContentだけでは判定しきれない |
shorts | #shorts、短い動画時間 | 短い通常動画と混ざる可能性がある |
normal_video | 上記に該当しない公開済み動画 | プレミア公開や編集動画も含まれることがある |
upcoming_or_live | liveBroadcastContentがliveまたはupcoming | 分析時点によって状態が変わる |
unknown | 情報が不足しているもの | 手動確認の候補にする |
公式カテゴリは粒度が粗い
YouTube APIにはcategoryIdがあります。これはYouTube側の公式カテゴリで、videoCategories.listを使うとカテゴリ名も取得できます。
公式カテゴリは、動画内容をかなり大きな単位で分けるための分類です。たとえば、以下のようなカテゴリがあります。
| 公式カテゴリ例 | 含まれやすい動画の例 | 分析時に困りやすい点 |
|---|---|---|
| Gaming | ゲーム実況、ゲーム配信アーカイブ、ゲーム紹介、攻略動画 | ゲーム名、配信形式、雑談多めのゲーム配信などは分からない |
| Music | 歌ってみた、MV、歌枠、演奏動画 | オリジナル曲、カバー、歌枠アーカイブなどの違いは分からない |
| Entertainment | 企画動画、バラエティ寄りの動画、配信者系コンテンツ | 企画、雑談、コラボ、切り抜きなどの細かい違いは分からない |
| People & Blogs | 日常系、雑談、個人発信、Vlog系の動画 | 雑談、近況報告、作業配信などの目的までは分からない |
| Education | 解説、講座、学習系コンテンツ | プログラミング、語学、資格、考察などのテーマは別途見たい |
| Science & Technology | 技術解説、ガジェット、サイエンス系動画 | AI、Python、PC、アプリ紹介などの細分類はできない |
ただし、チャンネル分析で見たい分類とは粒度が合わないことがあります。たとえば配信者やVtuberの活動傾向を見たい場合、「ゲーム」「雑談」「歌」「ASMR」「企画」「コラボ」のような分類が欲しくなります。しかし公式カテゴリだけでは、そこまで細かく分けられません。
たとえば同じGamingカテゴリでも、ゲーム実況の編集動画、長時間のライブ配信アーカイブ、参加型配信、ゲーム内イベントの視点配信では、分析で見たい意味がかなり違います。公式カテゴリは残しつつ、分析目的に合わせた独自カテゴリを追加する理由はここにあります。
そのため、この記事では公式カテゴリは公式データとして残しつつ、タイトル・説明文・タグを使って分析用の独自カテゴリも付与します。
完成イメージ
最終的には、以下のような列を持つCSVを作ります。
video_id
url
channel_id
channel_title
title
description
published_at
published_date
published_month
duration
duration_sec
view_count
like_count
comment_count
official_category_id
official_category_name
live_broadcast_content
has_live_streaming_details
video_type
content_category
days_since_published
views_per_dayofficial_category_idやdurationはAPI由来の情報です。一方で、video_typeやcontent_categoryは、分析しやすくするためにこちらで加工して追加する列です。
事前準備
必要なライブラリをインストールします。
pip install requests python-dotenv pandas isodateAPIキーは.envに保存しておきます。
YOUTUBE_API_KEY=ここにAPIキーを書くAPIキーをコードに直接書かない理由や、.envの使い方は以下の記事で整理しています。
APIキーを外部ファイルで安全に管理する方法|.env・config.iniの使い分け
チャンネルのuploads playlistを取得する
チャンネルに投稿された動画一覧を取得する場合、まずchannels.listでuploads playlist IDを取得します。
import os
from pathlib import Path
from datetime import datetime, timezone
import isodate
import pandas as pd
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 request_youtube_api(url: str, params: dict) -> dict:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
return response.json()
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,
}
data = request_youtube_api(url, params)
if not data.get("items"):
raise RuntimeError("チャンネルが見つかりませんでした")
return data["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]動画ID一覧を取得する
playlistItems.listで、uploads playlistに含まれる動画IDを取得します。1回のリクエストで取得できる件数には上限があるため、nextPageTokenを使ってページングします。
def get_upload_video_ids(playlist_id: str, max_pages: int | None = None) -> list[str]:
url = "https://www.googleapis.com/youtube/v3/playlistItems"
video_ids = []
page_token = None
page_count = 0
while True:
params = {
"part": "contentDetails",
"playlistId": playlist_id,
"maxResults": 50,
"key": API_KEY,
}
if page_token:
params["pageToken"] = page_token
data = request_youtube_api(url, params)
for item in data.get("items", []):
video_id = item.get("contentDetails", {}).get("videoId")
if video_id:
video_ids.append(video_id)
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 video_ids最初から全件取得すると確認に時間がかかることがあります。動作確認ではmax_pages=1にして、問題なければNoneに変えると扱いやすいです。
動画詳細を取得する
動画ID一覧を取得したら、videos.listで動画詳細を取得します。ここではsnippet、contentDetails、statistics、liveStreamingDetailsを指定します。
def chunked(values: list[str], size: int) -> list[list[str]]:
return [values[i:i + size] for i in range(0, len(values), size)]
def get_video_details(video_ids: list[str]) -> list[dict]:
url = "https://www.googleapis.com/youtube/v3/videos"
videos = []
for chunk in chunked(video_ids, 50):
params = {
"part": "snippet,contentDetails,statistics,liveStreamingDetails",
"id": ",".join(chunk),
"key": API_KEY,
}
data = request_youtube_api(url, params)
videos.extend(data.get("items", []))
return videos動画時間を秒に変換する
YouTube APIの動画時間は、PT1H23M45SのようなISO 8601 duration形式で返ってきます。集計しやすいように秒数へ変換します。
def parse_duration_seconds(duration: str | None) -> int | None:
if not duration:
return None
try:
parsed = isodate.parse_duration(duration)
return int(parsed.total_seconds())
except Exception:
return None動画種別を付与する
ここでは、ライブ関連情報、動画時間、#shortsの有無を使って、分析用のvideo_typeを付与します。
def contains_shorts_marker(title: str, description: str, tags: list[str]) -> bool:
text = " ".join([title, description, " ".join(tags)]).lower()
return "#shorts" in text or "#short" in text
def classify_video_type(
live_broadcast_content: str | None,
live_streaming_details: dict,
duration_sec: int | None,
title: str,
description: str,
tags: list[str],
) -> str:
if live_broadcast_content in {"live", "upcoming"}:
return "upcoming_or_live"
if live_streaming_details:
return "live_archive"
if contains_shorts_marker(title, description, tags):
return "shorts"
if duration_sec is not None and duration_sec <= 60:
return "shorts_candidate"
if duration_sec is None:
return "unknown"
return "normal_video"shorts_candidateは、動画時間だけでShorts候補としたものです。実際にShortsかどうかを厳密に確認したい場合は、URLや画面表示、手動確認を組み合わせるのが安全です。
分析用カテゴリを付与する
次に、タイトル・説明文・タグから、分析用のcontent_categoryを付与します。ここではシンプルなキーワードルールで分類します。
CATEGORY_KEYWORDS = {
"game": [
"ゲーム", "実況", "gta", "minecraft", "マイクラ", "apex", "valorant",
"原神", "崩壊", "モンハン", "steam",
],
"talk": [
"雑談", "朝活", "おはよう", "作業", "近況", "振り返り",
],
"music": [
"歌", "歌枠", "歌ってみた", "cover", "music", "karaoke",
],
"asmr": [
"asmr", "睡眠", "耳かき", "囁き",
],
"collaboration": [
"コラボ", "collab", "with",
],
"announcement": [
"告知", "お知らせ", "発表", "重大発表",
],
}
def classify_content_category(title: str, description: str, tags: list[str]) -> str:
text = " ".join([title, description, " ".join(tags)]).lower()
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
if keyword.lower() in text:
return category
return "other"この分類は、あくまで最初の整理用です。たとえばゲーム配信の中にも雑談が多い枠はありますし、歌枠の告知動画など複数の意味を持つ動画もあります。厳密に分類したい場合は、複数ラベルにする、手動確認列を追加する、チャンネルごとにキーワードを調整する、といった対応が必要です。
動画データを1行に整える
APIレスポンスを、CSVに保存しやすい1行の辞書へ変換します。
def safe_int(value) -> int | None:
if value is None:
return None
try:
return int(value)
except ValueError:
return None
def normalize_video_item(item: dict, category_map: dict[str, str]) -> dict:
snippet = item.get("snippet", {})
content_details = item.get("contentDetails", {})
statistics = item.get("statistics", {})
live_details = item.get("liveStreamingDetails", {})
title = snippet.get("title", "")
description = snippet.get("description", "")
tags = snippet.get("tags", [])
duration = content_details.get("duration")
duration_sec = parse_duration_seconds(duration)
live_broadcast_content = snippet.get("liveBroadcastContent")
category_id = snippet.get("categoryId")
published_at = snippet.get("publishedAt", "")
video_type = classify_video_type(
live_broadcast_content=live_broadcast_content,
live_streaming_details=live_details,
duration_sec=duration_sec,
title=title,
description=description,
tags=tags,
)
content_category = classify_content_category(title, description, tags)
published_dt = pd.to_datetime(published_at, errors="coerce", utc=True)
today = pd.Timestamp.now(tz="UTC")
days_since_published = None
if pd.notna(published_dt):
days_since_published = max((today - published_dt).days, 1)
view_count = safe_int(statistics.get("viewCount"))
views_per_day = None
if view_count is not None and days_since_published:
views_per_day = round(view_count / days_since_published, 2)
return {
"video_id": item.get("id"),
"url": f"https://www.youtube.com/watch?v={item.get('id')}",
"channel_id": snippet.get("channelId"),
"channel_title": snippet.get("channelTitle"),
"title": title,
"description": description,
"published_at": published_at,
"published_date": published_dt.date().isoformat() if pd.notna(published_dt) else "",
"published_month": published_dt.strftime("%Y-%m") if pd.notna(published_dt) else "",
"duration": duration,
"duration_sec": duration_sec,
"view_count": view_count,
"like_count": safe_int(statistics.get("likeCount")),
"comment_count": safe_int(statistics.get("commentCount")),
"official_category_id": category_id,
"official_category_name": category_map.get(category_id, ""),
"live_broadcast_content": live_broadcast_content,
"has_live_streaming_details": bool(live_details),
"video_type": video_type,
"content_category": content_category,
"days_since_published": days_since_published,
"views_per_day": views_per_day,
"tags": "|".join(tags),
}公式カテゴリ名を取得する
categoryIdだけだと読みにくいため、videoCategories.listでカテゴリ名も取得しておきます。
def get_video_category_map(region_code: str = "JP") -> dict[str, str]:
url = "https://www.googleapis.com/youtube/v3/videoCategories"
params = {
"part": "snippet",
"regionCode": region_code,
"key": API_KEY,
}
data = request_youtube_api(url, params)
return {
item["id"]: item["snippet"]["title"]
for item in data.get("items", [])
}公式カテゴリは、YouTube側が持っている大分類として残します。独自カテゴリとは目的が違うため、どちらか一方に寄せるより、両方の列を残しておく方が後で見返しやすいです。
CSVに保存する
ここまでの処理をまとめて、分析用CSVを作ります。
def build_analysis_dataset(channel_id: str, output_csv: Path, max_pages: int | None = 1) -> pd.DataFrame:
uploads_playlist_id = get_uploads_playlist_id(channel_id)
video_ids = get_upload_video_ids(uploads_playlist_id, max_pages=max_pages)
if not video_ids:
raise RuntimeError("動画IDを取得できませんでした")
videos = get_video_details(video_ids)
category_map = get_video_category_map(region_code="JP")
rows = [
normalize_video_item(item, category_map)
for item in videos
]
df = pd.DataFrame(rows)
output_csv.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(output_csv, index=False, encoding="utf-8-sig")
return df実行例です。まずはmax_pages=1で50件程度を取得し、列の中身を確認します。
CHANNEL_ID = "UCxxxxxxxxxxxxxxxxxxxxxx"
df = build_analysis_dataset(
channel_id=CHANNEL_ID,
output_csv=Path("outputs/youtube/channel_video_analysis_dataset.csv"),
max_pages=1,
)
print(df[[
"title",
"published_date",
"duration_sec",
"official_category_name",
"video_type",
"content_category",
"view_count",
"views_per_day",
]].head())問題なければ、max_pages=Noneにして全件取得します。
df = build_analysis_dataset(
channel_id=CHANNEL_ID,
output_csv=Path("outputs/youtube/channel_video_analysis_dataset.csv"),
max_pages=None,
)簡単に集計して確認する
CSVを作ったら、まずは動画種別や独自カテゴリの件数を確認します。
print(df["video_type"].value_counts(dropna=False))
print(df["content_category"].value_counts(dropna=False))月別の投稿数も見てみます。
monthly_counts = (
df.groupby(["published_month", "video_type"])
.size()
.reset_index(name="video_count")
)
print(monthly_counts.tail(20))この段階で、分類が明らかにずれている動画があれば、キーワードルールを調整します。分類は一度で完成させるというより、実際のチャンネルに合わせて少しずつ育てるものとして考えると扱いやすいです。
このデータでできる分析
- 月別の投稿本数を見る
- 通常動画、ライブ配信アーカイブ、Shortsの比率を見る
- ゲーム、雑談、音楽などのカテゴリ別に投稿傾向を見る
- 再生数の多い動画を動画種別ごとに確認する
- 投稿日からの日数を考慮した
views_per_dayで新旧動画を比較する - Vtuberや配信者チャンネルの活動スタイルを整理する
たとえばVtuberチャンネルを見る場合、ライブ配信中心なのか、Shortsも活用しているのか、ゲーム配信が多いのか、歌や雑談の比率が高いのか、といった入口を作れます。数字だけで魅力を判断するのではなく、チャンネルを知るための地図を作るイメージです。
注意点
分類は完全ではない
動画種別や独自カテゴリは、APIから返ってくる公式の正解ラベルではありません。タイトルや説明文に依存するため、表記ゆれやチャンネルごとの文化に影響されます。
Shorts判定は特に揺れやすい
#shortsが付いていないShortsや、60秒以内の通常動画もあります。この記事の判定は、分析のための近似的な分類として使います。
公式カテゴリと独自カテゴリを混同しない
公式カテゴリはYouTube側の大きな分類です。独自カテゴリは、自分が分析したい粒度に合わせて後から付ける分類です。記事やレポートで使うときは、どちらの分類なのかを分けて説明した方が誤解が少なくなります。
公開データの範囲で扱う
この記事で扱うのは、YouTube APIから取得できる公開メタデータです。非公開動画、削除済み動画、取得できない統計情報などは分析対象に含められません。
APIクォータに注意する
YouTube Data APIにはクォータがあります。投稿動画一覧を作るだけならuploads playlistを使う方が扱いやすく、検索目的でない場合にsearch.listを多用する必要はあまりありません。取得したCSVを保存しておき、同じデータを何度も取り直さないようにすると安心です。
公式ドキュメント
- Videos - YouTube Data API
- PlaylistItems: list - YouTube Data API
- VideoCategories: list - YouTube Data API
- Quota Calculator - YouTube Data API
まとめ
YouTube APIで取得した動画一覧は、そのままでも基本的な集計に使えます。ただ、チャンネルの活動傾向を見る場合は、通常動画、ライブ配信アーカイブ、Shortsといった動画種別や、ゲーム、雑談、音楽などの独自カテゴリを付けておくと見通しがよくなります。
一方で、これらの分類は公式の正解データではありません。APIで取得できる情報の制約を理解したうえで、分析用の補助ラベルとして使うのが大切です。
この形でCSVを整えておくと、投稿頻度、配信形式の比率、人気動画の傾向、カテゴリ別の活動スタイルなどを確認しやすくなります。後続の記事では、このデータを使った可視化や、YouTubeチャンネルの活動傾向の見方も整理していきたいと思います。