feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 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:
@@ -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()` 而非导航链接。
|
||||
Reference in New Issue
Block a user