feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
This commit is contained in:
2026-10-10 17:12:25 +08:00
parent 21ff01b894
commit 2112c1a870
1601 changed files with 367425 additions and 0 deletions
@@ -0,0 +1,285 @@
# 抖音 Web 端选择器手册
> 扫描时间:2026-04-27 | 浏览器窗口:702×681 | CDP 端口:9222
> 连接方式:Patchright `connect_over_cdp` | 代理需绕过:`no_proxy=localhost,127.0.0.1`
---
## 键盘快捷键
| 键 | 功能 | 备注 |
|----|------|------|
| **Z** | 点赞 | 需英文输入法 |
| **X** | 打开/关闭评论 | |
| **F** | 关注作者 / 进直播间 | |
| **B** | 弹幕开关 | |
| **空格** | 播放/暂停 | |
| **↑** | 上一个视频 | |
| **↓** | 下一个视频 | |
| **Enter** | 搜索确认 / 评论发送 | |
---
## 全局元素
| 元素 | 选择器 | 备注 |
|------|--------|------|
| 搜索框 | `[data-e2e="searchbar-input"]` | type=input |
| 搜索按钮 | `[data-e2e="searchbar-button"]` | |
| 右上角头像 | `[data-e2e="live-avatar"]` | hover 弹出菜单 |
| 左侧导航 | `[data-e2e="douyin-navigation"]` | |
### 左侧导航栏
| 项目 | 大致位置(y) |
|------|-----------|
| 精选 | 64 |
| 推荐 | 124 |
| 搜索 | 184 |
| 关注 | 244 |
| 朋友 | 304 |
| 我的 | 364 |
| 直播 | 441 |
| 放映厅 | 501 |
| 短剧 | 561 |
---
## 视频播放页(推荐/关注)
### 互动按钮
| 操作 | 键盘 | 选择器 | 状态判断 |
|------|------|--------|---------|
| 点赞 | **Z** | `[data-e2e="video-player-digg"]` | `data-e2e-state`: `video-player-no-digged`(未赞) / `video-player-digged`(已赞) |
| 评论(打开) | **X** | `[data-e2e="feed-comment-icon"]` | |
| 收藏 | - | `[data-e2e="video-player-collect"]` | |
| 分享 | - | `[data-e2e="video-player-share"]` | |
| 关注/进直播 | **F** | `[data-e2e="feed-follow-icon"]` | |
| 更多 | - | `[data-e2e="video-play-more"]` | |
### 翻页
| 操作 | 键盘 | 选择器 |
|------|------|--------|
| 上翻视频 | **↑** | `[data-e2e="video-switch-prev-arrow"]` |
| 下翻视频 | **↓** | `[data-e2e="video-switch-next-arrow"]` |
### 播放控制
| 操作 | 键盘 | 备注 |
|------|------|------|
| 播放/暂停 | **空格** | |
| 弹幕开关 | **B** | |
---
## 评论区
> 按 X 或点击评论图标后,右侧弹出评论面板,视频从 570px 缩窄到 234px
| 元素 | 选择器 | 备注 |
|------|--------|------|
| 评论列表 | `[data-e2e="comment-list"]` | |
| 评论输入框 | `.public-DraftEditor-content` | Draft.js 富文本编辑器,contentEditable |
| 输入容器(聚焦态) | `.comment-input-container-focus` | 激活后添加此 class |
| 发送按钮 | `.commentInput-right-ct .WFB7wUOX` | 向上箭头图标 |
| 表情按钮 | `.commentInput-right-ct .BVMl8WNl` 第2个 | |
| 图片上传 | `.commentInput-right-ct .BVMl8WNl` 第1个 | |
| @提及 | `.commentInput-right-ct .BVMl8WNl` 第3个 | |
| 评论更多 | `[data-e2e="video-comment-more"]` | |
| 弹幕输入框 | `input[placeholder="发一条弹幕吧"]` | 非评论框,注意区分 |
### 评论操作流程
```
1. 按 X 打开评论面板
2. 点击 .public-DraftEditor-content 激活输入
3. 输入文字(type_into Draft.js 编辑器)
4. Enter 发送 / 点击发送按钮
5. 可能触发验证码弹窗
```
---
## 搜索结果页
**URL 格式**:`https://www.douyin.com/root/search/{关键词}?type=general`
### Tab 分类栏
| Tab | 选择器 | 备注 |
|-----|--------|------|
| 综合(激活) | `.NiqGqBbw` | 不同 class 表示激活态 |
| 视频/用户/直播 | `.t3OsOj2N` | |
### 子分类筛选
| 选择器 | 示例 |
|--------|------|
| `.neT9xRMd.vZeqhI2r` | 直播间、胃肠镜、中医院… |
| 激活态额外 class | `.Bp4xnVjA` |
### 显示模式
| 模式 | 选择器 |
|------|--------|
| 多列 | `.cSG1ckL8` |
| 单列 | `.cSG1ckL8.ltQEgur_` |
### 结果卡片
| 元素 | 选择器 | 备注 |
|------|--------|------|
| 卡片容器 | `.search-result-card` | 整卡可点击(cursor:pointer),无 `<a>` 标签 |
| 视频封面 | `.videoImage` | 219×292 |
| 标题 | `.wSEoiOKC` | |
| 作者 | `.YebDknhI` | @username |
| 点赞数 | `.LUd60iMQ` | |
| 布局 | 3列网格 | x=31, x=276, x=521 |
### 点击第 N 个搜索结果
```python
cards = page.query_selector_all('.search-result-card')
cards[0].click() # 第1个
```
---
## 关注页
**URL**:`https://www.douyin.com/follow`
### 左侧关注列表
| 元素 | 选择器 | 备注 |
|------|--------|------|
| 列表头 | `.mZ1rdREf` | "列表综合排序" |
| 我的关注 | `.IMOte1q1.mZ1rdREf` | "我的关注(N)" |
| 关注用户项 | `.ILK2RAD5` | 点击切换到该用户视频 |
### 右侧视频流
与推荐页结构相同,互动按钮选择器通用。
---
## 个人主页
**URL**:`https://www.douyin.com/user/self?showTab=post`
### 用户信息
| 元素 | data-e2e |
|------|----------|
| 用户详情 | `user-detail` |
| 用户信息 | `user-info` |
| 关注数 | `user-info-follow` |
| 粉丝数 | `user-info-fans` |
| 获赞数 | `user-info-like` |
### Tab 栏
| Tab | 位置(y) |
|-----|---------|
| 作品 | 221 |
| 推荐 | 221 |
| 喜欢 | 221 |
| 收藏 | 221 |
| 观看历史 | 221 |
| 稍后再看 | 221 |
| 我的预约 | 221 |
| AI 笔记 | 221 |
### 作品列表
| 元素 | data-e2e |
|------|----------|
| 作品列表 | `user-post-list` |
| 滚动列表 | `scroll-list` |
---
## 头像悬停菜单
**触发**:hover `[data-e2e="live-avatar"]`
**面板**:`.userMenuPanelShadowAnimation`(x≈360, 宽328)
| 菜单项 | 选择器 | 链接 |
|--------|--------|------|
| 用户名 | `.uz1VJwFY.e6huIECy` | → 个人主页 |
| 我的喜欢 | `.uz1VJwFY.espXX7re` 第1个 | → 喜欢 |
| 我的收藏 | `.uz1VJwFY.espXX7re` 第2个 | → 收藏 |
| 观看历史 | `.uz1VJwFY.espXX7re` 第3个 | → 历史 |
| 稍后再看 | `.uz1VJwFY.espXX7re` 第4个 | → 稍后再看 |
| 我的作品 | `.uz1VJwFY.espXX7re` 第5个 | → 作品 |
| 我的订单 | `.uz1VJwFY.VSHeqy7Q` | 无链接 |
---
## 验证码弹窗
> 评论后可能触发短信验证码
| 元素 | 选择器 | 备注 |
|------|--------|------|
| 遮罩 | `.second-verify-mask` | 全屏遮罩 |
| 面板 | `.second-verify-panel` | |
| 标题 | `.uc-ui-verify-new_header-title` | "接收短信验证码" |
| 关闭 | `.uc-ui-verify-new_header-close` | |
| 提示 | `.uc-ui-verify_sms-verify_content_desc` | 手机号脱敏 |
| 输入框 | `.uc-ui-verify_sms-verify_input` | |
| 获取验证码 | 面板内按钮 | |
| 验证按钮 | `.uc-ui-verify_sms-verify_button.primary` | 非 disabled 时可点 |
| 取消按钮 | `.uc-ui-verify_sms-verify_button.second` | |
---
## 技术备忘
### CDP 连接
```python
import os
os.environ['no_proxy'] = 'localhost,127.0.0.1'
os.environ['NO_PROXY'] = 'localhost,127.0.0.1'
from patchright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp('http://localhost:9222')
context = browser.contexts[0]
page = context.pages[0]
```
### Draft.js 输入
评论框是 Draft.js 富文本编辑器,不能用普通 `fill()`,需用 `type_into()` 或 `press_sequentially()`:
```python
editor = page.locator('.public-DraftEditor-content')
await editor.click()
await editor.press_sequentially('评论内容')
```
### 起始页固定
每次操作从固定 URL 开始:`https://www.douyin.com/?recommend=1`
### SVG className
SVG 元素的 `className` 是 `SVGAnimatedString` 对象,非字符串。JS 中需用 `el.className.baseVal`。
### 点赞状态判断
```python
digg = page.locator('[data-e2e="video-player-digg"]')
state = await digg.get_attribute('data-e2e-state')
# "video-player-no-digged" = 未点赞
# "video-player-digged" = 已点赞
```
### 搜索卡片无 `<a>` 标签
搜索结果卡片通过 JS 事件跳转,需用 `card.click()` 而非导航链接。