移植自 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 带网关原话 / 正常返回)。
7.5 KiB
title, tags, created, updated, nature, collected, collected_date
| title | tags | created | updated | nature | collected | collected_date | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 小红书养号页面交互技术 |
|
2026-05-29 | 2026-05-29 | method | true | 2026-06-09 |
核心选择器
首页/瀑布流
- 瀑布流容器:
#app .layout.limit - 笔记卡片:
section.note-item(DOM 分析确认) - 卡片封面链接:
a.cover.mask.ld - 卡片图片:
a.cover.mask.ld img
笔记详情页
- 详情容器:
.note-detail, [class*=note-detail], .note-page - 标题:
.title, h1, [class*=note-title] - 内容:
.content, [class*=content], .desc
弹窗/遮罩
- 登录弹窗:
[class*=login], [class*=modal], [role=dialog], .reds-alert - 通用遮罩:
[class*=mask], [class*=overlay], [class*=modal]
锚点验证
| 锚点类型 | 选择器 |
|---|---|
| 首页 | section.note-item |
| 笔记详情 | .note-detail, [class*=note-detail], .title, h1 |
| 视频页 | video |
| 评论打开 | .comment-section, [class*=comment-section] |
| 搜索页 | .search-result, [class*=search-result] |
JS 智能查找函数
find_refresh_button_js()
三策略查找瀑布流页面右下角 FAB 刷新按钮:
- 选择器匹配:
button[class*="refresh"]、[class*="fab"]、[class*="floating"]等 - SVG 扫描:查找包含旋转箭头路径的 SVG 元素(刷新图标 = arc + rotate 特征)
- fixed 定位过滤:右下角区域(x > viewport 60%, y > viewport 60%)的可点击元素
返回 {found, x, y, w, h, text, method}
find_qr_wall_back_button_js()
检测 QR 检测墙并定位返回首页按钮:
- QR 墙关键词检测:body 文本包含"扫码"/"二维码"/"非常用登录"/"验证"等
- 返回按钮匹配:优先级排序 — "返回首页"(10) > "返回"(20) > "首页"(30) > "确定/我知道了"(50)
- 候选过滤:宽度/高度 > 5px(排除不可见元素),文本长度 < 15 字符
返回 {found, x, y, w, h, text, priority}
dismiss_login_modal_js()
隐藏登录弹窗和固定定位遮罩层。设置 display:none 而非移除 DOM。
get_note_cards_js()
获取瀑布流所有笔记卡片的 index、href、title、rect 信息。
鼠标模拟技术
L 型鼠标路径(_l_shaped_click)
模拟真人鼠标移动轨迹:
起点(随机偏移) → 水平移动(5-10步) → 垂直移动(3-8步) → 微抖动(±2px) → 点击
- 每步延迟 10-30ms(随机),总耗时 100-400ms
- 起点偏移范围:x ±150px, y -100~+50px
- 限制在视口内(防止鼠标移出窗口)
- 微抖动模拟手指不稳
双击策略(聚焦→执行)
macOS 窗口激活限制下,点击需要先聚焦再执行:
- 第一次点击 → 聚焦窗口(可能不触发元素事件)
- 第二次点击 → 实际执行操作
误触作者主页(新标签页)检测
点击笔记卡片时可能点到作者头像/名称,导致新标签页打开作者主页。
检测方法(确定性锚点)
点击前: tabs_before = len(context.pages)
执行点击(Playwright click)
点击后: if len(context.pages) > tabs_before → 误触作者
- 新 tab URL 即为作者主页地址
- 关闭新 tab,回到原 tab,重新选卡片重试(最多 3 次)
- 3 次全部误触才返回 None(本轮失败,不导致全局退出)
代码位置
browse.click_note_card(page, max_retries=3):
- 使用
page.context.pages获取所有标签页 - 点击后比对 tab 数量变化
- 比截图 + AI 分析更可靠,是确定性锚点
page.evaluate() 超时保护
问题
Playwright 的 page.evaluate() 在页面状态异常时会无限挂起,无默认超时。卡死时不会抛出异常,整个 asyncio 循环停滞。
受影响的函数
所有涉及 page.evaluate() 的操作都可能卡死:
dismiss_login_modal()— 登录弹窗关闭get_note_cards()— 获取卡片列表click_note_card()— 内部调用 get_note_cardsbrowse_note_detail()— 视频检测click_refresh_button()— 刷新按钮查找click_qr_wall_back_button()— QR墙按钮查找
修复
全部加 asyncio.wait_for() 超时保护:
result = await asyncio.wait_for(
page.evaluate(some_js_function()),
timeout=10 # 秒
)
超时时间表
| 函数 | 超时 | 说明 |
|---|---|---|
| click_refresh_button | 10s | JS 三策略扫描,最复杂 |
| click_qr_wall_back_button | 10s | QR 墙检测 + 按钮定位 |
| get_note_cards | 10s | DOM 卡片遍历 |
| dismiss_login_modal | 8s | 简单 DOM 操作 |
| browse_note_detail (视频检测) | 8s | 简单 DOM 查询 |
搜索结果页刷新按钮
搜索页(URL含 /search)没有右下角 FAB 刷新按钮,click_refresh_button() 会在 10s 超时后安全返回 False,不会卡死。
常见问题与恢复策略
黑屏恢复
现象:瀑布流页面变黑,卡片不可见 根因:SPA 状态异常,非窗口移出屏幕 恢复链:
1. scroll_feed 回到顶部 → 检测卡片
2. click_bottom_nav_tab("发现") → 刷新页面状态
3. goto_home() → 直接导航兜底
QR 检测墙恢复
现象:中央弹出大弹窗,提示"扫码"/"非常用登录" 根因:XHS 非常用登录检测触发,非登录状态失效 恢复链:
1. click_qr_wall_back_button() → 点弹窗上的"返回首页"按钮
2. click_bottom_nav_tab("发现") → 底部导航恢复
3. go_back_to_home() → 直接导航兜底
重要区别:QR 墙 ≠ 未登录。QR 墙是频控触发,多刷会恢复。未登录二维码弹窗是另一种情况。
每轮必刷新策略(Step 0)
为彻底防止瀑布流黑屏/卡死,小红书养号每轮循环开头强制点击右下角刷新按钮:
每轮循环:
Step 0: click_refresh_button() ← 必执行(模拟鼠标点 FAB)
Step 1: scroll_feed_human()
Step 2: click_note_card()
...
搜索返回后: click_refresh_button() × 1 ← 回到首页后也刷新
- 找不到刷新按钮时不中断,仅跳过并记日志(搜索结果页无 FAB)
click_refresh_button()自带 10s 超时保护,不会卡死- 原锚点失败时的刷新逻辑保留作为兜底
首页锚点验证失败
1. click_refresh_button() → 点右下角物理刷新按钮(优先,模拟真人)
2. page.reload() → API 刷新(fallback)
3. init_anti_detection() → 重新初始化反检测
AI-layout 兼容(A/B 测试)
触发条件:指纹分辨率 + DPR(低分屏 1920x1080 DPR=1.0 易触发) 差异:无顶部搜索栏、无 header-container、首卡 y 偏移 272px vs 144px 兼容措施:
search():标准输入框 → ALT 输入框 → URL fallback(三重降级)click_search_result():标准选择器 → 通用 href 匹配- 启动时自动检测布局版本(standard/ai-layout/unknown)
指纹建议:screen ≥ 1920x1080, DPR ≥ 1.25
登录检测规则
- 顺序:页面级登录检测 → dismiss_login_modal(先检测再关闭,否则误判)
- 等待:首页加载后等 5 秒(给弹窗渲染时间)
- 关键词:"登录之后更精彩" / "扫码登录" / "手机号登录" / "请登录" / "立即登录"
- 尺寸区分:
getBoundingClientRect(width>200, height>100)区分导航栏 vs 模态弹窗
操作前状态检查清单
进入每轮养号前:
- 首页锚点验证(
section.note-item存在) - 登录弹窗关闭
- 反检测初始化
- Cookie/Session 检查(非必需,未登录也可浏览)