feat(scripts): 添加批量生成 YouTube Studio 内容管理器 URL 的脚本
This commit is contained in:
63
docs/install-skills.md
Normal file
63
docs/install-skills.md
Normal 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 分析 CSV(zip) |
|
||||
|
||||
## 安装(傻瓜式)
|
||||
|
||||
双击运行:
|
||||
|
||||
```
|
||||
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 编写)。
|
||||
231
docs/yt-studio-export-principle.md
Normal file
231
docs/yt-studio-export-principle.md
Normal 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/>前端生成 exportQuery:joinRequest 节点 + 日期范围] --> 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 承载 base64(PK 魔数开头)] --> 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`*
|
||||
Reference in New Issue
Block a user