用 YouTube Data API v3 生成一份频道报表:给一个频道的 @handle,取出最近 N 个视频的标题、发布日期、时长、播放、点赞、评论数和点赞率,导出成 Excel 能直接打开的 CSV。全部是只读调用,用 API key 就够了,不需要 OAuth。
先看清成品
本机用
HttpMockSequence加 3 个按官方响应结构编写的 JSON 文件跑了 2 个测试,全部通过(不联网,用的是客户端库自带的 discovery 文档)。还用一个假的 API key 请求了真实接口,得到400 API key not valid(见调试示例)。没有用真实 key 取过数据,报表里的数字都是测试数据。
注意:YouTube Data API 本身免费,但有每日配额,各方法消耗多少以官方配额页为准,本文不列数字。只上传你有权发布的视频,遵守 YouTube 社区准则和 API 服务条款;不要共享频道账号或 OAuth 凭据,在 YouTube 无法访问的地区也不要用技术手段绕过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
channel_report.py |
handle → 上传列表 → 视频 ID → 统计数据 |
| CSV | utf-8-sig 编码,Excel 打开中文不乱码 |
| 时长 | ISO 8601 格式(如 PT3M12S)转成秒数 |
| 隐藏点赞的视频 | 点赞数和点赞率留空,不填 0 |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Google Cloud 项目 | 启用 YouTube Data API v3 | API 和服务 → 凭据 → 创建 API key |
| API key 限制 | 建议只允许调用 YouTube Data API | 防止 key 泄露后被滥用 |
| Python | 3.9 以上(本机 3.13) | pip install -r requirements.txt |
文件结构
29-youtube-A-src/
├── channel_report.py
├── test_report.py
├── fixtures/ # channels / playlist / videos 三个模拟响应
└── requirements.txt
第 1 步:调用顺序
| 调用 | 用途 | 关键参数 |
|---|---|---|
channels.list |
由 handle 找到频道和“上传”播放列表 | forHandle,part=contentDetails,snippet |
playlistItems.list |
列出上传列表里的视频 ID | 每页最多 50 条,用 pageToken 翻页 |
videos.list |
批量查询统计和时长 | id 一次最多 50 个,part=snippet,statistics,contentDetails |
用 search.list 也能列出视频,但配额消耗比 list 类方法高得多(具体数字见配额页),所以这里走上传列表。
第 2 步:脚本
channel_report.py:
"""频道最近 N 个视频的数据报表(YouTube Data API v3,只读,用 API key)。
用法:YT_API_KEY=... python channel_report.py @handle 20 report.csv"""
import csv, os, re, sys
from googleapiclient.discovery import build
def iso_duration_seconds(s):
m = re.fullmatch(r"P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", s or "")
if not m: return None
d, h, mi, se = (int(x or 0) for x in m.groups())
return d * 86400 + h * 3600 + mi * 60 + se
def uploads_playlist(yt, handle):
r = yt.channels().list(part="contentDetails,snippet", forHandle=handle).execute()
items = r.get("items", [])
if not items: raise SystemExit(f"找不到频道 {handle}")
return items[0]["snippet"]["title"], items[0]["contentDetails"]["relatedPlaylists"]["uploads"]
def recent_video_ids(yt, playlist_id, n):
ids, token = [], None
while len(ids) < n:
r = yt.playlistItems().list(part="contentDetails", playlistId=playlist_id,
maxResults=min(50, n - len(ids)), pageToken=token).execute()
ids += [it["contentDetails"]["videoId"] for it in r.get("items", [])]
token = r.get("nextPageToken")
if not token: break
return ids
def video_rows(yt, ids):
rows = []
for i in range(0, len(ids), 50): # videos.list 每次最多 50 个 ID
r = yt.videos().list(part="snippet,statistics,contentDetails", id=",".join(ids[i:i + 50])).execute()
for v in r.get("items", []):
st = v.get("statistics", {})
views = int(st.get("viewCount", 0))
likes = st.get("likeCount") # 作者隐藏点赞数时没有这个字段
rows.append({
"video_id": v["id"], "title": v["snippet"]["title"],
"published": v["snippet"]["publishedAt"][:10],
"seconds": iso_duration_seconds(v["contentDetails"]["duration"]),
"views": views, "likes": "" if likes is None else int(likes),
"comments": st.get("commentCount", ""),
"like_rate": "" if likes is None or views == 0 else f"{int(likes) / views:.2%}",
})
return rows
def main(handle, n, out, yt=None):
yt = yt or build("youtube", "v3", developerKey=os.environ["YT_API_KEY"])
title, pl = uploads_playlist(yt, handle)
rows = video_rows(yt, recent_video_ids(yt, pl, n))
with open(out, "w", newline="", encoding="utf-8-sig") as f: # utf-8-sig:Excel 打开中文不乱码
w = csv.DictWriter(f, list(rows[0]) if rows else ["video_id"]); w.writeheader(); w.writerows(rows)
print(f"{title}: {len(rows)} 个视频 → {out}")
return rows
if __name__ == "__main__":
main(sys.argv[1], int(sys.argv[2]), sys.argv[3])
第 3 步:离线测试
test_report.py:
from pathlib import Path
from googleapiclient.discovery import build
from googleapiclient.http import HttpMockSequence
import channel_report as cr
F = Path(__file__).with_name("fixtures")
def test_report(tmp_path):
http = HttpMockSequence([({"status": "200"}, (F / n).read_bytes()) for n in ("channels.json", "playlist.json", "videos.json")])
yt = build("youtube", "v3", http=http, developerKey="fake", static_discovery=True)
rows = cr.main("@demo", 10, tmp_path / "r.csv", yt=yt)
assert [r["seconds"] for r in rows] == [192, 3720]
assert rows[0]["like_rate"] == "5.00%"
assert rows[1]["likes"] == "" and rows[1]["like_rate"] == ""
assert (tmp_path / "r.csv").read_bytes().startswith(b"\xef\xbb\xbf")
def test_duration():
assert cr.iso_duration_seconds("PT45S") == 45
assert cr.iso_duration_seconds("P1DT2H") == 93600
assert cr.iso_duration_seconds("garbage") is None
fixtures/videos.json(第二个视频故意不带 likeCount):
{"items":[
{"id":"vid1","snippet":{"title":"三分钟学会手冲","publishedAt":"2026-09-30T10:00:00Z"},"contentDetails":{"duration":"PT3M12S"},"statistics":{"viewCount":"1200","likeCount":"60","commentCount":"8"}},
{"id":"vid2","snippet":{"title":"直播回放","publishedAt":"2026-09-01T12:00:00Z"},"contentDetails":{"duration":"PT1H2M"},"statistics":{"viewCount":"300","commentCount":"2"}}
]}
pip install -r requirements.txt
python -m pytest -q # 2 passed
检查结果:PT1H2M 转换成 3720 秒;没有点赞数的视频,点赞率一栏为空。
第 4 步:真实运行(本机没有用真实 key 运行)
export YT_API_KEY=你的key
python channel_report.py @你的频道handle 20 report.csv
预期输出 频道名: 20 个视频 → report.csv。
调试示例
用了错误的 key,报错信息里带着完整的 key
googleapiclient.errors.HttpError: <HttpError 400 when requesting
https://youtube.googleapis.com/youtube/v3/channels?part=contentDetails%2Csnippet&forHandle=%40YouTube&key=AIzaFAKEKEY000&alt=json
returned "API key not valid. Please pass a valid API key.">
这说明两件事:① key 不对,或者没有为这个项目启用 YouTube Data API v3;② 异常信息里包含了完整的请求地址,key 也在里面。如果把异常原样写进日志或者贴到群里,key 就泄露了。对外展示错误时,只显示 e.resp.status 和 e.reason。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
找不到频道 |
handle 拼错了,或者漏了 @ |
从频道主页地址里复制 |
403 quotaExceeded |
当天的配额用完了 | 第二天再试,或者在控制台申请提高配额 |
| 点赞列为空 | 作者隐藏了点赞数 | 正常现象,不要填 0 |
| Excel 打开是乱码 | 用了不带 BOM 的 UTF-8 | 脚本已经用 utf-8-sig |
模板规则
- 用 list 类方法加批量 ID,少用 search。
- 可能缺失的字段保持为空,不要当成 0。
- key 放在环境变量里,并在控制台限制它只能调用 YouTube Data API。
- 先用 fixture 做离线测试,再用真实 key 运行。
项目资源
频道报表的关键是选对调用路径:上传列表加 videos.list 批量查询,比搜索省很多配额。字段缺失时如实留空,报表才可信。