feat(scripts): 添加批量生成 YouTube Studio 内容管理器 URL 的脚本

This commit is contained in:
2026-08-21 18:21:03 +08:00
commit c1b39e9466
29 changed files with 4020 additions and 0 deletions

63
docs/install-skills.md Normal file
View File

@@ -0,0 +1,63 @@
# 技能安装说明Windows
把本项目 `skills\` 下的技能一键安装到 Trae CN 的用户级技能目录 `%USERPROFILE%\.trae-cn\skills\`,安装后在任意项目的对话中都可触发。
## 技能清单
| 技能 | 用途 |
|---|---|
| `uv-env-setup` | 用 uv 准备并维护本项目 Python 运行环境(`uv sync` / `uv run` |
| `yt-studio-url-builder` | 根据需求清单CSV/Excel批量生成 YouTube Studio 内容管理器 explore URL |
| `youtube-studio-csv-download` | 复用已登录浏览器会话,脚本化下载 YouTube Studio 分析 CSVzip |
## 安装(傻瓜式)
双击运行:
```
scripts\install-skills.bat
```
看到 `全部安装成功` 及逐项 `[OK]` 即完成,按任意键关闭窗口。
命令行等价方式PowerShell / cmd 均可):
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-skills.ps1
```
## 脚本做了什么
1. 扫描项目 `skills\` 下每个包含 `SKILL.md` 的子目录(即可安装技能)。
2. 逐个复制到 `%USERPROFILE%\.trae-cn\skills\`
3. 目标已存在同名技能时,先把旧版移动到 `%USERPROFILE%\.trae-cn\skills-backup\<技能名>-<时间戳>\` 再装新版——不直接覆盖,旧版不丢。
4. 逐个校验并打印 `[OK]` / `[FAIL]`;全部成功时退出码为 0。
安全性:脚本只处理本项目 `skills\` 中出现的技能,不会删除或改动 `.trae-cn\skills\` 下的其他技能;不修改任何 Trae CN 配置文件。
## 验证
1. 重启 Trae CN或新建会话
2. 对话中直接提及技能名或相关意图,例如:
- 「帮我准备环境」→ `uv-env-setup`
- 「根据这份清单批量生成 Studio URL」→ `yt-studio-url-builder`
- 「用我的 Chrome 会话下载这份分析 CSV」→ `youtube-studio-csv-download`
## 更新与卸载
- **更新**:项目技能有改动后,重新双击 `install-skills.bat`(旧版自动备份)。
- **回滚**:把 `%USERPROFILE%\.trae-cn\skills-backup\<技能名>-<时间戳>\` 整个文件夹复制回 `%USERPROFILE%\.trae-cn\skills\<技能名>\`
- **卸载**:删除 `%USERPROFILE%\.trae-cn\skills\` 下对应技能文件夹。
- **清理备份**:确认新版可用后,删除 `%USERPROFILE%\.trae-cn\skills-backup\` 整个文件夹。
## 常见问题
- **双击 bat 闪退**:在 PowerShell 中手动运行 `scripts\install-skills.bat` 查看报错;最常见原因是 `install-skills.ps1` 没有和 bat 放在同一目录。
- **PowerShell 执行策略受限**bat 已带 `-ExecutionPolicy Bypass`;若组策略仍拦截,以管理员身份运行。
- **技能装了但不触发**:需重启 Trae CN 或新建会话后生效;确认对话窗口正常加载。
- **中文输出乱码**`install-skills.ps1` 必须保持 UTF-8 with BOM 编码(重新编辑保存时注意)。
## 注意
- 运行时依赖不随技能安装:两个脚本型技能的 Python 依赖pandas、openpyxl、playwright由项目根 `pyproject.toml` 统一管理,首次使用按 `uv-env-setup` 技能执行 `uv sync` 即可。
- 安装脚本仅支持 Windows路径与命令均按 Windows 编写)。

View File

@@ -0,0 +1,231 @@
# YouTube Studio 高级分析「导出当前视图」原理
> 技术原理说明 · 实测分析
>
> 从点击「导出当前视图 → 逗号分隔值 (.csv)」到 ZIP 落盘,拆解一次完整的请求-响应机制:后端并不生成可下载的 URL而是把打包好的 ZIP 以 base64 内联在 API 响应里,由前端解码成 Blob 后触发浏览器下载。
>
> 抓包日期2026-08-21 · 方法:浏览器抓包 + 脚本复现
## 目录
1. [概述与核心结论](#01-概述与核心结论)
2. [用户操作入口](#02-用户操作入口)
3. [网络请求机制](#03-网络请求机制)
4. [exportQuery 请求体结构](#04-exportquery-请求体结构)
5. [服务端响应与编码](#05-服务端响应与编码)
6. [ZIP 内容结构](#06-zip-内容结构)
7. [客户端解码与下载](#07-客户端解码与下载)
8. [文件命名规则](#08-文件命名规则)
9. [落盘与重名去重](#09-落盘与重名去重)
10. [拦截与自动化可行性](#10-拦截与自动化可行性)
11. [端到端流程总览](#11-端到端流程总览)
---
## 01 概述与核心结论
「高级分析」是 YouTube Studio内容管理器面向版权所有者与频道的一份数据分析视图。页面上方有一个「导出当前视图」按钮下拉后选择 `逗号分隔值 (.csv)`,即可把当前可见的表格导出为一份本地文件 —— 用户拿到手的并非多个散落的 CSV而是一个 ZIP 压缩包。
这份导出的关键在于:**整个文件并没有走传统的「服务端生成下载链接 → 浏览器 GET 下载」路径**。在 2026-08-21 的抓包中,页面向后端发了一次 `POST` 请求,响应体里直接内联了一段 base64 字符串,其解码结果是完整的 ZIP 字节流。前端拿到这段字符串后自行解码、构造 `Blob`,再唤起浏览器的下载动作。整个过程没有出现任何指向下载文件的 GET 请求或页面跳转。
> **一句话结论**
>
> 导出 = **前端发起 POST** → **后端打包 ZIP 并 base64 内联返回** → **前端解码为 Blob 触发下载**。因为没有独立下载 URL反而使「拦截响应、自行解码落盘」成为完全可行的自动化方式。
---
## 02 用户操作入口
操作发生在内容管理器的「高级分析 / Explore」页面URL 以 `studio.youtube.com/owner/…/analytics` 形态出现)。页面通过「维度」按钮切换统计口径(如**内容**、**频道**等),再通过时间选择器确定日期范围。
页面的「导出当前视图」下拉提供多种格式,本主题只涉及 `逗号分隔值 (.csv)`。点击该选项后,前端读取当前维度和日期范围,把它转成一份结构化的命名查询,随后发起网络请求。用户视角只有一次点击,但背后是下述完整链路。
---
## 03 网络请求机制
导出动作返回的是一个内嵌压缩包的 JSON API 调用,而不是文件下载请求。抓包得到的确切端点如下:
```
POST https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json
```
这是 YouTube 内部 `youtubei` 风格 v1 接口的一个方法,`yta_web` 对应 YouTube Analytics 的 Web 端命名空间,`csv_export` 即「导出 CSV实为 ZIP」的动作。`?alt=json` 表示要求返回 JSON 而非其他编码。请求体承载一份 `exportQuery`,描述「要导出哪些统计维度和哪段日期」。
---
## 04 exportQuery 请求体结构
请求体POST body的根字段是 `exportQuery`。它内部通过一个 `joinRequest` 描述导出内容,核心是 `nodes` 数组 —— 每个节点对应导出一张表格。实测一次导出包含三张表:**表格数据**、**图表数据**、**总计**。每个节点内部的 `value.query` 声明了维度类型和日期范围。
```json
{
"exportQuery": {
"joinRequest": {
"nodes": [
{ "value": { "query": {
"dimensions": [{ "type": "VIDEO" }], // 表格数据
"timeRange": { "dateIdRange": {
"inclusiveStart": 20260723,
"exclusiveEnd": 20260820
} }
} } },
{ "value": { "query": { /* 图表数据 */ } } },
{ "value": { "query": { /* 总计 */ } } }
]
}
}
}
```
其中两处日期字段最值得留意:`inclusiveStart` 表述导出区间的起始日(含),`exclusiveEnd` 表述结束日(不含,即「到这一天为止」)。两者都是 `YYYYMMDD` 格式的整数,例如 `20260723` 表示 2026-07-23。它们同时决定了导出内容和最终文件名。
> **关键词义**
>
> `exclusiveEnd` 是「开区间上界」:导出数据覆盖到 exclusiveEnd 的前一天。文件名里二者的下划线形式正是从这两个字段直接格式化而来。
---
## 05 服务端响应与编码
后端收到 `exportQuery` 后,把请求的三张表各自输出成 CSV再打包成一个 ZIP然后把 ZIP 的**二进制字节流做 base64 编码**,塞进 JSON 响应的一个重要字段里返回:
```json
{
"responseContext": { /* 服务元信息 */ },
"zippedData": "UEsDBBQAAAAA..." // base64 编码的 ZIP 字节流
}
```
`zippedData` 是这条响应的精华。它开头的 `UEsDBBQ` 是 base64 对 ZIP 文件头魔数 `PK\x03\x04`(即 ASCII 的 `PK` 两个字节)的编码 —— 这是识别「这是一个 ZIP」的最直接证据。JSON 只能承载文本,二进制 ZIP 无法原样嵌入,因此后端先 base64 编码,让整个压缩包变成一段纯文本字符串随响应返回。
这里没有生成任何 `/download/…` 之类的文件地址,也没有 `Content-Disposition: attachment` 的响应头来驱动浏览器另存 —— 下载的编排完全交还给了前端。
---
## 06 ZIP 内容结构
`zippedData` 做一次 base64 解码,得到的是一个标准 ZIP 压缩包,内含三个 CSV 文件,正好与请求体 `joinRequest.nodes` 的三个节点一一对应:
| ZIP 内文件名 | 对应请求节点 | 说明 |
| --- | --- | --- |
| `表格数据.csv` | nodes[0] | 当前维度下的逐条明细(如每条内容/频道的指标) |
| `图表数据.csv` | nodes[1] | 供图表渲染的时间序列 / 汇总数据 |
| `总计.csv` | nodes[2] | 全表合计行的汇总值 |
三张表的拆分说明导出本质上是一次「多表联查」:前端页面上同时呈现的明细表、图表和合计,被后端一次性打包进同一个压缩包交付,而不是三次独立下载。
---
## 07 客户端解码与下载
前端收到 JSON 后做的是标准的「base64 → 二进制 → Blob → 触发下载」四步。由于抓包时没有观察到任何指向下载文件的 GET 或跳转,可以判定这是**客户端 Blob 下载**而非服务端重定向下载。下面这段是这一机制的通用实现示意(并非从 YouTube 混淆后的前端源码中直接提取):
```javascript
// base64 → 字节数组
const bytes = atob(json.zippedData);
const buf = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) buf[i] = bytes.charCodeAt(i);
// 字节 → Blob → 对象 URL
const blob = new Blob([buf], { type: "application/zip" });
const url = URL.createObjectURL(blob);
// 挂到 <a> 上并触发点击,交由浏览器下载管理器接管
const a = document.createElement("a");
a.href = url;
a.download = "内容 2026-07-23_2026-08-20 WL Media.zip";
a.click();
URL.revokeObjectURL(url);
```
`a.download` 里填写的文件名,就是用户在下载管理器里看到的名字。而真正的写盘动作,由浏览器自身的下载管理器执行。
---
## 08 文件命名规则
与实测下载目录 `D:\Downloads` 中的产物一致,最终文件名遵循固定模板:
```
<维度标签> <inclusiveStart>_<exclusiveEnd> <账号名>.zip
```
其中 `inclusiveStart``exclusiveEnd` 取自请求体,由 `YYYYMMDD` 整数格式化为 `YYYY-MM-DD`。实测的一个完整例子是:
```
内容 2026-07-23_2026-08-20 WL Media.zip
```
维度标签来自「维度」选择器:类型 `VIDEO` 对应「内容」、`USER` 对应「频道」。账号名取自右上角账号选择器按钮的文本(示例为 `WL Media`)。
| 构成部分 | 来源 | 示例值 |
| --- | --- | --- |
| 维度标签 | 页面「维度」按钮VIDEO→内容 / USER→频道 | 内容 |
| 起始日 | `inclusiveStart` 格式化为 YYYY-MM-DD | 2026-07-23 |
| 结束日 | `exclusiveEnd` 格式化为 YYYY-MM-DD | 2026-08-20 |
| 账号名 | 右上角账号按钮文本 | WL Media |
---
## 09 落盘与重名去重
因为下载由浏览器下载管理器接管,落盘位置默认是浏览器的下载目录(本机为 `D:\Downloads`且无需「另存为」确认。Chromium 内核对重名文件会自动追加序号后缀,这正是「需求文件.zip → 需求文件 (1).zip → 需求文件 (2).zip」这一规则的由来。
去重逻辑是:若目标名已存在,则在扩展名前追加 ` (n)``n` 从 1 开始递增,每次取「最小不冲突的序号」—— 即便 `(1)` 已被占用,也会继续向后找 `(2)`,而不是覆盖或失败。这一规则在后续的自动化脚本中被原样复现,从而做到完全不依赖浏览器也能自管文件名。
> **去重规则**
>
> 同名文件依次保存为 `需求文件.zip`、`需求文件 (1).zip`、`需求文件 (2).zip`……若某个 `(n)` 已存在则跳到下一个可用 n。
---
## 10 拦截与自动化可行性
正因为导出所需的全部数据ZIP 字节流)都内联在一次 API 响应里,且不依赖额外的文件下载地址,**在响应层拦截即可完整掌控落盘行为** —— 保存目录、文件名、重名去重全由脚本决定,绕开浏览器的下载管理器和确认弹窗。
实现上有两条路径,落地为一套脚本与一个可复用 skill
- **响应拦截 + 自行解码**:命中 `csv_export` 响应后读取 `zippedData`base64 解码、反推文件名、去重后写盘,完全可控。
- **依赖 Chromium 下载偏好**:开启自动下载与内置去重,让浏览器原样落盘,仅省去人工点击。
无论哪条路径,前提都是复用**已登录 YouTube Studio** 的浏览器会话:要么以已登录的用户数据目录启动浏览器,要么通过调试端口附加到已打开的浏览器。否则页面会跳转到 Google 登录页,导出无法触发。
---
## 11 端到端流程总览
```mermaid
flowchart TD
A[1. 操作入口<br/>点击「导出当前视图 → 逗号分隔值 (.csv)」] --> B
B[2. 组装请求<br/>前端生成 exportQueryjoinRequest 节点 + 日期范围] --> C
C[3. 发送请求<br/>POST /youtubei/v1/yta_web/csv_export?alt=json] --> D
D[4. 服务端打包<br/>三张 CSV 压缩为 ZIP再 base64 编码] --> E
E[5. 内联返回<br/>响应体 zippedData 承载 base64PK 魔数开头)] --> F
F[6. 前端解码<br/>base64 → Blob触发浏览器下载] --> G
G[7. 落盘去重<br/>下载管理器写入默认目录,重名自动加 n]
style A fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
style B fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
style C fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
style D fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
style E fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
style F fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
style G fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
```
图 1 · 从点击导出到 ZIP 落盘的端到端流程(红色 = 客户端/前端,蓝色 = YouTube 后端)
**请求 / 响应结构映射**
| 请求 · exportQuery | 响应 · zippedData |
| --- | --- |
| `joinRequest.nodes`3 张表) | `responseContext`(元信息) |
| `dimensions` → 维度类型 | `zippedData` = base64(ZIP) |
| `dateIdRange.inclusiveStart` | 开头 `UEsDBBQ` = PK 魔数 |
| `dateIdRange.exclusiveEnd` | 解码得 表格/图表/总计.csv |
| 日期均为 YYYYMMDD 整数 | — |
---
*分析基于 2026-08-21 的浏览器抓包与脚本复现实证。配套脚本:`youtube-studio-csv-download`*