效率工具

用 YouTube Data API v3 生成一份频道报表

用 YouTube Data API v3 生成一份频道报表:给一个频道的 @handle,取出最近 N 个视频的标题、发布日期、时长、播放、点赞、评论数和点赞率,导出成 Excel 能直接打开的 CSV。全部是只读调用,用 API key 就够了,不需要 OAuth。

BINBHO · 图文教程

用 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 运行。

项目资源

用途 地址
channels.list https://developers.google.com/youtube/v3/docs/channels/list
playlistItems.list https://developers.google.com/youtube/v3/docs/playlistItems/list
videos.list https://developers.google.com/youtube/v3/docs/videos/list
YouTube Data API 入门 https://developers.google.com/youtube/v3/getting-started
配额消耗 https://developers.google.com/youtube/v3/determine_quota_cost
API 服务条款 https://developers.google.com/youtube/terms/api-services-terms-of-service

频道报表的关键是选对调用路径:上传列表加 videos.list 批量查询,比搜索省很多配额。字段缺失时如实留空,报表才可信。