Files
contentm_agent/.workbuddy/skills/aigc-idea-impression/references/操作规范/获取抖音账号数据操作规范.md
T
WorkBuddy df56c2c137 初始化提交:contentm_agent 工作区全量快照
内容分四块:
1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。
2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。
3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。
4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。

.gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
2026-10-08 08:13:02 +08:00

15 KiB
Raw Blame History

获取抖音账号数据 操作规范(SOP)

端到端流程:从「账号昵称」到「归档 JSON」。基于 2026-08-30 实测(雨菲啊账号获取)沉淀。 工具:browser-harness(~/.local/bin/browser-harness.exe)。 通用浏览器准备步骤见 浏览器访问操作规范.md(本文件聚焦抖音账号获取全流程)。

〇、获取方式原则(红线,先读)

必须模拟真实用户操作——具体指模拟用户输入和鼠标点击:

能力 命令 说明
键盘输入 fill_input(selector, text) / type_text(text) 模拟键入(如搜索框输入关键词)
按键 press_key(key) 模拟回车/ESC 等按键
鼠标点击 click_at_xy(x, y) 模拟点击(如点搜索按钮、点用户卡片)
滚动 scroll(x, y, dy) 模拟滚轮滚动浏览
读取 js(...) / page_info() 仅用于读取数据 / 定位元素坐标,不代替用户行为

禁止:用 goto_url URL 直达代替搜索/点击;禁止运行内置脚本/第三方 API 代替浏览器行为。goto_url 仅用于打开站点首页。

  • 判定速查:能浏览器模拟 → 必须走浏览器(输入+点击+滚动);浏览器被验证码/登录墙拦截 → 才可降级第三方并注明
  • 行为节奏:等待渲染(6-7s)、按需滚动(2-3 次)、控制频率

🚨 风控规避红线(2026-08-30 实测教训,先读)

触发风控识别 → 立即停止操作,禁止继续。风控窗口期继续操作会加重风控(验证页升级、内容区封锁、预览节流)。

风控信号 表现 处理
预览节流 批量/连续 hover 卡片时长全部读不到 立即停止 hover 类操作
验证页 页面 title 变成 emoji(如 🐴)、正文只剩导航+页脚 立即停止,关闭相关页面标签
内容区封锁 主页/搜索页只有导航栏,账号信息与作品列表不渲染 立即停止,禁止刷新重试
CDP timed out browser-harness 连续超时 停止操作,doctor 确认 daemon 后再动

恢复流程:

  1. 冷却:停止一切抖音自动化操作,至少 15-30 分钟(浏览器页面保持闲置,不刷新不操作)
  2. 冷却后先手动正常访问一次(打开抖音首页浏览),确认无验证页再恢复自动化
  3. 恢复后严格按用户节奏:一次只 hover/点击一个元素,间隔自然(3-5s+),不批量扫描、不固定节奏

操作节奏铁律(防再触发):

  • 一次只处理 1 个目标(如只 hover 一个卡片),读完再处理下一个
  • 不做 for 循环连续 hover/点击(机器扫描特征)
  • 每次操作间有"浏览停顿",不机械匀速

一、触发场景

用户要求获取某抖音账号数据(粉丝/获赞/简介/抖音号等)时执行。如「获取 XXX 账号数据」「查一下 XXX 账号」。

二、环境准备(每次必做)

# 1. 清代理(Privoxy 10800 劫持 CDP)
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
export NO_PROXY="127.0.0.1,localhost,::1"

# 2. doctor 检查三要素
browser-harness doctor

# 3. CDP 授权:Chrome 弹「Allow remote debugging?」→ 用户点允许
#    (弹窗在后台时:chrome://inspect/#remote-debugging 勾选启用)

三、执行流程

步骤 1:打开抖音并搜索账号(模拟用户输入 + 点击)

BH="$HOME/.local/bin/browser-harness.exe"
"$BH" <<'PY'
import time
# ① 打开抖音首页(仅导航入口)
goto_url("https://www.douyin.com")
time.sleep(6)

# ② 模拟用户输入:点击搜索框 → 键入关键词 → 回车
#    先定位搜索框坐标(js 辅助定位,不代替行为)
r = js("""
(() => {
    const els = Array.from(document.querySelectorAll('input'));
    const inp = els.find(e => (e.placeholder||'').includes('搜索'));
    if (!inp) return null;
    const b = inp.getBoundingClientRect();
    return {x: b.x + b.width/2, y: b.y + b.height/2};
})()
""")
pos = r.get("value", None) if isinstance(r, dict) else r
if pos:
    click_at_xy(pos["x"], pos["y"])          # 点击搜索框(模拟鼠标点击)
    time.sleep(1)
    type_text("{昵称}")                        # 键入账号名(模拟键盘输入)
    time.sleep(1)
    press_key("Enter")                         # 回车搜索
    time.sleep(6)
PY

备选:直接 goto_url("https://www.douyin.com/search/{昵称URL编码}?type=user") 定位到搜索页(导航式,非严格模拟用户,仅在交互定位困难时使用,且随后用点击进主页补齐)。

步骤 2:提取页面文本

"$BH" <<'PY'
r = js("document.body.innerText")
text = r.get("value", "") if isinstance(r, dict) else str(r)
idx = text.find("{昵称}")
seg = text[max(0,idx-100):idx+400]
print(seg)
PY

步骤 3:结构化解析(正则)

抖音号: 6301602506217
53.7万获赞 68.6万粉丝     ← 实际显示粘连:抖音号(13位) + 获赞 + 粉丝
简介:用户卡片下方文字段
import re
dyid = re.search(r"抖音号[::]\s*([0-9]+)", seg)
fans = re.search(r"([\d.]+万?)粉丝", seg)
likes = re.search(r"([\d.]+万?)获赞", seg)
# 简介 = 卡片区域内的独立文本行(抖音号行之后的非数字行)

⚠️ 实测坑:搜索结果卡片中「抖音号/获赞/粉丝」粘连显示(如 630160250621753.7万获赞68.6万粉丝)——抖音号为 13 位数字,先切出抖音号再解析获赞/粉丝。 ⚠️ 搜索卡片数据是近期数据(获赞可能仅为片段),必须进主页拿完整累计数据(见步骤 4)。

步骤 4:进入账号主页(模拟点击用户卡片)

搜索卡片仅公开片段,进入主页才能拿到完整数据(累计获赞/作品数/关注/IP属地/简介全文/置顶作品)。

# 模拟用户点击:定位目标用户卡片坐标 → 点击进入主页
"$BH" <<'PY'
import time
r = js("""
(() => {
    // 找搜索结果中"目标昵称"的用户卡片可点击区域
    const all = Array.from(document.querySelectorAll('a[href*="/user/MS4w"]'));
    const cards = all.filter(a => (a.innerText||'').includes('{昵称}') || true);
    const hrefs = [...new Set(all.map(a => a.getAttribute('href').split('?')[0]))];
    // 返回第一个真实用户卡片中心坐标
    const t = all[0]; if (!t) return null;
    const b = t.getBoundingClientRect();
    return {x: b.x + b.width/2, y: b.y + b.height/2, url: 'https:' + hrefs[0]};
})()
""")
info = r.get("value", None) if isinstance(r, dict) else r
if info:
    click_at_xy(info["x"], info["y"])   # 模拟鼠标点击卡片 → 进入主页
    time.sleep(7)
PY

# 提取主页文本(关注/粉丝/获赞/抖音号/IP属地/性别/简介/作品数/置顶作品)

⚠️ 实测坑 1:卡片 href 为协议相对 //www.douyin.com/user/MS4w...;若用导航兜底,用 "https:" + href,不要再拼域名。 ⚠️ 实测坑 2:搜索页需等待渲染(6-7s);当前不在搜索页需先回到搜索页。 ⚠️ 实测坑 3:点击式进入主页依赖卡片坐标,页面布局变化时坐标会偏移——点击后校验 page_info() 是否为主页(URL 含 /user/),失败则退回导航式兜底。

步骤 5:React Fiber 提取作品数据(⭐ 首选方法,mcn-dou-analysis 同款,避风控)

不依赖 hover/网络层/全局变量——抖音主页作品卡片的数据在 React 组件树(Fiber)的 awemeInfo 对象里,遍历 a[href*="/video/"] 元素向上查找 Fiber 即可一次性提取全部字段(含时长秒数),无 hover、不触发预览节流。2026-08-30 实测成功(王墨绫子 56 条,时长与 ffprobe 一致)。

// 提取所有卡片:id|标题|时长(毫秒)
(() => {
    const cards = Array.from(document.querySelectorAll('a[href*="/video/"]'));
    const seen = new Set(), rows = [];
    for (const a of cards) {
        const href = a.getAttribute('href')||'';
        const id = (href.match(/\/video\/(\d+)/)||[])[1];
        if (!id || seen.has(id)) continue;
        seen.add(id);
        let fiber = null;
        for (const k of Object.keys(a)) {
            if (k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')) { fiber = a[k]; break; }
        }
        if (!fiber) continue;
        let node = fiber, info = null, depth = 0;
        while (node && depth < 30) {
            const mp = node.memoizedProps;
            if (mp) {
                for (const key of ['awemeInfo','data','item','aweme']) {
                    const v = mp[key];
                    if (v && typeof v === 'object' && (v.aweme_id || v.video)) { info = v; break; }
                }
            }
            if (info) break;
            node = node.return; depth++;
        }
        if (info) {
            rows.push([id, (info.desc||'').split('\n')[0].slice(0,40), info.video ? info.video.duration : null].join('|'));
        }
    }
    return rows.join('\n');   // ⚠️ 返回值有长度限制,用 slice(START, START+8) 分页取
})()

要点:

  • duration 是毫秒(如 9400 = 9.4s,筛 ≥60s 用 duration >= 60000)
  • js 返回值有截断限制(约 160 字符),分页取:rows.slice(START, START+8).join('\n') 循环取
  • Fiber 提取不到的卡片(虚拟化差异)用 href 的 aweme_id 兜底
  • 滚动加载配合:先滚动 .route-sc/route-scroll-container 容器(非 window)触发懒加载,随机间隔 13s,23 次即可

步骤 6:作品卡片 hover 识别时长 + 捕获视频直链(备选,慎用)

主页作品卡片平时不显示时长(时长角标是 hover 才渲染的视觉层),但:

  1. hover 卡片 → 卡片显示预览进度 当前秒/总时长(如 00:02/00:09),即视频时长(总时长可靠)
  2. hover 同时触发预览视频加载 → 网络层直接捕获 douyinvod 直链 → curl 即可下载,无需进详情页、无需 yt-dlp/cookies
# ① 模拟鼠标悬停(hover)卡片 → 时长即现
"$BH" <<'PY'
import time
# 定位目标视频卡片坐标(js 辅助定位)
r = js("""
(() => {
    const a = document.querySelector('a[href*="{视频ID}"]');
    if (!a) return [0,0];
    const b = a.getBoundingClientRect();
    return [Math.round(b.x + b.width/2), Math.round(b.y + b.height/2)];
})()
""")
v = r.get("value", [0,0]) if isinstance(r, dict) else r
if v != [0,0]:
    # 模拟鼠标移动(hover)——CDP Input.mouseMoved 即用户悬停
    cdp("Input.dispatchMouseEvent", type="mouseMoved", x=v[0], y=v[1])
    time.sleep(2)
    r2 = js("(function(){var a=document.querySelector('a[href*=\"{视频ID}\"]'); return a?a.innerText.replace(/\\n+/g,'|'):'';})()")
    print(r2.get("value","") if isinstance(r2,dict) else r2)
    # 输出如:15.2万|00:02|/|00:09|不是一见钟情...  → 总时长 00:09
PY

# ② 捕获 hover 预览触发的视频直链(douyinvod CDN)
"$BH" <<'PY'
cdp("Network.enable")
cdp("Input.dispatchMouseEvent", type="mouseMoved", x={x}, y={y})  # 再次 hover
time.sleep(3)
evs = drain_events()
urls = []
for e in evs:
    try:
        if e.get("method") == "Network.requestWillBeSent":
            u = e.get("params",{}).get("request",{}).get("url","")
            if "douyinvod" in u: urls.append(u)
    except Exception: pass
uniq = list(dict.fromkeys(urls))
# 保存最后一个(通常为完整视频流)
open("video_url.txt","w").write(uniq[-1])
PY

# ③ curl 下载(带 Referer + UA)
curl -sL -o video.mp4 -H "Referer: https://www.douyin.com/" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36" \
  "$(cat video_url.txt)" --max-time 90

# ④ ffprobe 验证:时长 + 分辨率(判断真视频 vs 图集预览)
ffprobe -v error -show_entries format=duration -show_entries stream=codec_type,width,height -of default=noprint_wrappers=1 video.mp4

⚠️ 图集 vs 真视频判定(实测坑):

  • 列表层(hover 即判型,最快):hover 卡片显示预览进度 当前秒/总时长 → 真视频;hover 后无时长格式(只有赞+标题)→ 图集。实测:真视频《不是一见钟情》hover 显示 00:02/00:09;图集《另外,你能来总裁吗?》hover 后仅 3.8万|标题,无 MM:SS。
  • 下载层(ffprobe 三要素):图集预览:时长短(约 2-3s)、分辨率 720×960、无有效音频 → 内容为静态图轮播;真视频:分辨率 1080P/2K+ 竖屏(如 1440×2560)、有 aac 音频流、时长与 hover 显示一致
  • 判定顺序:先 hover 判型(列表层直接筛掉图集),下载后再用 ffprobe 三要素复核
  • hover 时长仅预览加载瞬间显示,预览播完/移开鼠标即消失——提取要在 hover 后 1-3 秒内完成

步骤 7:归档(作品数据,按账号名,无日期层)

# 账号数据:账号层
D:\MCNSkill项目\作品数据\{账号名称}\{昵称}_账号数据_{YYYY-MM-DD}.json
# 视频数据:账号层下按视频标题清洗后建文件夹(规则见 数据归档规范.md)
D:\MCNSkill项目\作品数据\{账号名称}\{视频标题清洗后}\video.mp4
{
  "date": "YYYY-MM-DD",
  "source": "browser-harness 抖音网页版",
  "account": {
    "name": "...", "douyinId": "...", "followers": "...", "totalLikes": "...",
    "bio": "...",
    "relatedAccounts": [...]   # 搜索发现的相关账号(如日常号)
  }
}

四、边界与限制

场景 处理
未登录态 搜索卡片仅公开信息;主页可获取完整累计数据(作品数/累计获赞/置顶作品)
搜索不到账号 换昵称变体(如主号/日常号)、或检查是否同名干扰
触发验证码/限流 停止操作,间隔后重试;不绕过
需完整作品列表 主页滚动加载 2~3 次收集,适度访问

五、输出模板(对用户汇报)

字段 值
昵称 ...
抖音号 ...
粉丝 ...
累计获赞 ...
简介 ...
关联账号 ...

六、经验沉淀关联

步骤 8:生成视频列表 Excel(账号层,参考 mcn-dou-analysis)

  • 账号层保存 {账号名称}_视频列表_{YYYY-MM-DD}.xlsx,13 列:序号 | 视频ID | 视频标题 | 视频地址 | 点赞数 | 点赞(显示) | 评论数 | 分享数 | 收藏数 | 播放量 | 视频时长(秒) | 发布时间 | 标签
  • 数据来源:Fiber 提取时长(id/标题/时长毫秒)+ 红狐 workList 补统计(likeCount/commentCount/shareCount/publishTime,按 url 中视频 ID 精确匹配)
  • 排序:按发布时间倒序;收藏数/播放量无数据时留空
  • 后续更新:重新 Fiber 提取 + 红狐 workList 合并,覆盖更新同名 xlsx