# TikTok 视频数据抓取 —— 纯接口(API)版 目标:`https://www.tiktok.com/t/ZTy8r7T2a/` → `@marcgagilang/video/7552202593888603410` 抓取项:点赞 / 评论数 / 分享 / 收藏 / 播放 + 全量评论内容 > ✅ 不使用无障碍权限。全程 HTTP 请求,无 `auto`/`UiSelector`/点击/滑动。 --- ## 一、云机实测结论(2026-09-23,云机 106.119.179.124:23098,Android 14) ### 1.1 网络环境(关键前提) | 现象 | 结论 | |------|------| | `adb shell` 里 `curl https://www.tiktok.com` | ❌ `Recv failure: Connection reset by peer` | | 有 `tun0` 网卡(172.19.0.1/30) | 云机跑 **NekoBox**(包 `moe.nb4a`)VPN | | `127.0.0.1:2080` 在监听 | NekoBox 的 mixed 代理(HTTP + SOCKS5) | | `curl -x http://127.0.0.1:2080 https://www.tiktok.com` | ✅ **200** | **原因**:adb shell 的 curl 走 `eth0`(被过滤,TLS 握手被 reset),App 走 `tun0`。 **对脚本的影响**:Auto.js 走系统 VPN,一般**直连即可**;若连不上,在 CONFIG 里填 `proxyHost: "127.0.0.1"` + `proxyPort: 2080`。 ### 1.2 计数指标(公开 SSR,无需签名、无需登录) | 页面 | 结果 | |------|------| | `https://m.tiktok.com/v/.html` | ✅ **首选**。SSR 完整,含收藏数 | | `https://www.tiktok.com/embed/v2/` | ✅ 备用(无收藏数) | | `https://www.tiktok.com/video/` | ❌ 常返回 SPA 空壳,无 SSR 数据 | 实测值:**点赞 2,400,000 / 评论 18,900 / 分享 1,200,000 / 收藏 246,728 / 播放 14,800,000** > 注意:`itemStruct` 之后**第一个** `stats` 块是话题的 `{"videoCount":...}`, > 脚本会遍历所有 `"stats":{...}` 并取第一个含 `diggCount`/`playCount` 的(实测需跳过 5 个)。 ### 1.3 评论接口(官方全部被拦 → 调用代码已于 2026-09-25 移除) | 接口 | 结果 | |------|------| | `www.tiktok.com/api/comment/list/` | ❌ HTTP 200 但 `Content-Length: 0`(缺 A-Bogus/X-Bogus) | | 同上 + msToken 二次握手 | ❌ 仍为空(服务端下发了 `x-ms-token` 也没用) | | `api16/api19/.../comment/list/` (`app_name=musically`) | ❌ **403**(Argus 风控) | | 同上 (`app_name=tiktok_web`) | ❌ `2146 Please upgrade your TikTok app` | > `2146` 与 cookie / UA / `version_code`(试过 180800 / 270000 / 618002 / 2024505030 等) > / 设备参数**全部无关**,是死路。登录态 Cookie **对评论接口没有任何帮助**。 **可用方案**:`www.tikwm.com` 镜像接口(已实测) ``` 顶级评论 https://www.tikwm.com/api/comment/list?url=<视频URL>&count=50&cursor=N 子回复 https://www.tikwm.com/api/comment/reply?video_id=&comment_id=&count=50&cursor=N → {code:0, data:{comments:[...], total:18752, cursor:50, hasMore:true}} ``` ### 1.4 tikwm 的 6 个坑(2026-09-24/25 复测,★ 决定成败) | # | 坑 | 实测证据 | 后果 | |---|----|---------|------| | 1 | **`hasMore` 会偶发「假 false」** | `cursor=400` 返回 `hasMore:false`(仅 41 条),但 `cursor=450/500` 依然 `true` 且有数据 | 拿它当终止条件 → **第 9 页就收工**,只拿到 426 条 | | 2 | **只按「并发」限流,不按「时间」限流** | 串行 0.2s 间隔连打 20 次 → 20/20 全成功;并发 3 → 2/3;并发 5 → 3/5 | 必须**串行**;间隔只是「礼貌」,不是硬阈值(见 1.5) | | 3 | **`total` 字段不可信** | `cursor≥15000` 时 `total` 退化成 cursor 本身(15000/18000/18700…) | 不能拿 total 算进度或判结束 | | 4 | **`count` 上限 50** | 传 100 / 200 也只返回 **49 条** | 必须老老实实翻 200+ 页 | | 5 | **翻页深度上限 ≈ `cursor` 10369** | 超过后**真的**没有数据(不是假 false) | 拿不到声明的 1.8 万,实得 ≈ 1 万(52%) | | 6 | **限速维度必须用「请求数/小时」** | 评论量差异达 **20 倍**:500 条=10 请求,9,931 条=199 请求 | 按「视频个数」设固定间隔 → 速率失控(见 1.5) | **真实可抓范围**(实测): | 项 | 值 | |---|---| | 顶级评论 | `cursor` 从 0 翻到 **≈10369** 后真的没数据(约 212 页) | | 单页 | 46~50 条(`count=50`) | | 子回复 | 需单独接口;`reply_total` 声明值可信(声明 367 → 实测抓到 366) | | 声明总量 | `total=18752`(官方显示 18900),但**回复总量远超此数** | > ⚠️ `reply_total` 分布极不均匀:仅 `cursor=0` 这一页 49 条顶级评论, > `reply_total` 合计就 **3447**(单条最高 1446)。但全量统计后**实际总量只有 5952 条** > (1060 条评论带回复),缺口不大。 **回复接口 `comment/reply` 比列表接口更不稳**(实测): | 现象 | 证据 | |------|------| | 空页**随机**出现 | `cursor=50` 空,`cursor=100` 却正常 | | 返回条数会**超过**声明 | 声明 81 → 实得 **278**;声明 20 → 实得 23 | | 也可能只拿到一部分 | 声明 21 → 实得 **1**(cursor 50/100 都空,tikwm 侧就没有) | 所以回复的终止规则是「**已达标 + 末页未满**」而不是拿 `declared` 硬卡: ``` ① all.length >= declaredTotal 且 本页 rawN < count → 收工(末页) ② 连续 replyEmptyStreakLimit(=5) 页空 → 收工 ③ cursor > declaredTotal + 2*count → 收工(hardCap) ``` (踩过:用 `emptyStreak>=2` 会让「声明 61 实得 4」这种被误判成结束。) --- ### 1.5 限流模型与推荐抓取节奏(2026-09-24 标定) **先给结论:tikwm 只卡并发,不卡频率。** 用云机 curl 在同一出口 IP 上做了两组标定: **组 1 —— 串行下的频率:没有阈值** | 请求间隔 | 次数 | 成功 | `code=-1` | |---|---|---|---| | 0.2s(网络极限,≈24 req/min) | 20 | **20** | 0 | | 1.0s | 20 | **20** | 0 | | 3.0s | 15 | **15** | 0 | **组 2 —— 并发:阈值 ≈ 2** | 并发数 | 成功 | `code=-1` | |---|---|---| | 2 | 2/2 | 0 | | 3 | 2/3 | 1 | | 5 | 3/5 | 2 | 且**触发后无需冷却**——限流一结束,下一次请求立即恢复(恢复校验全通过)。 #### 所以「多久抓一个视频」的真实答案 **① 硬约束(有实测支撑):必须串行。** 这是唯一会被服务端拒绝的行为。 本脚本天然串行,所以你不必把"间隔"当成某个必须达到的阈值。 **② 建议节奏(主动收敛,非实测阈值)。** 之所以要主动收敛,是因为: - tikwm 是**免费第三方镜像**,靠广告养活,容量有限,持续满速打是透支共享资源; - 请求出口是 **NekoBox 的代理节点 IP**,若该节点被 Cloudflare/tikwm 标记, **你整个 VPN 都会受影响**,代价远大于抓慢一点。 > 2026-09-25 起**不再需要人工分档**:旧的「视频间隔 90s」被证明是有缺陷的—— > 评论量差异达 20 倍,固定间隔会让轻视频空等、重视频歇不够。现改为自动两级限速。 #### 现在的做法:按请求数驱动的两级限速 | 级别 | 机制 | 作用 | |---|---|---| | **① 令牌桶** `targetReqPerHour: 1200` | 每次请求前预占一个「令牌」,**跨视频累计、不因换视频重置** | 小时级**硬上界**(1200 → 每请求至少 3s) | | **② 视频间动态冷却** | `videoCooldownMinMs(15s) + 上一视频请求数 × videoCooldownPerReqMs(250ms)` | 重视频自动多歇,轻视频不空等 | | ③ 批次长休(沿用) | 每 5 个视频休 10 min(±20%) | 打断长时间匀速 | 端到端实测(`_probe/test_pacing.js` 模拟「轻 10 请求 + 重 199 请求 + 轻 10 请求」): ``` 219 请求 / 726s → 1086 请求/小时 (目标上界 1200) ``` **成本对照**(已含限速后的单视频耗时): | 评论量 | 请求数 | 动态冷却 | 令牌桶耗时 | 单视频合计 | |---|---|---|---|---| | 500 | 11 | 18s | 33s | ≈ 51s | | 2,000 | 41 | 25s | 123s | ≈ 2.5 min | | 5,000 | 101 | 40s | 303s | ≈ 5.7 min | | 9,931(本视频) | 200 | 65s | 600s | ≈ 11 min | > ⚠️ 加了主动限速后,本视频从原来的 ≈6 min 变成 ≈11 min。 > 这是「把速率压进安全区」的代价,不是性能 bug。 **仍然别按"抓几个"规划,按"这个视频要发多少次请求"规划。** 建议单 IP 控制在 **≤ 1000~1500 请求/小时**,默认配置(1200)已落在区间内。 #### 脚本内置的节流保护 | 机制 | 行为 | |---|---| | 串行 | 天然单线程,不存在并发风险 | | **令牌桶 `targetReqPerHour`** | 全局速率上界,跨视频累计(**主要限速手段**) | | 页间最小间隔 `requestDelay: 400` | 含 ±40% 抖动;通常小于令牌桶间隔,只在目标设得很高时主导 | | 自适应退避 | 命中 `code=-1` → 页间隔与令牌桶**同步** ×2(分别 8s / 30s 封顶) | | 快速恢复 | 连续 20 页干净通过 → 两者**同步一步回到基准**(不做缓慢衰减) | | 视频间动态冷却 | `15s + 请求数 × 250ms`,±50% 抖动 | | 批次长休 | 每 5 个视频长休 10 min,±20% 抖动 | | 断点续跑 `skipExisting` | 已有 `_stats.json` 的视频自动跳过,**跳过的不触发冷却** | | 失败也冷却 | 抓取失败的视频同样进入冷却(失败多因风控,连续快跑只会加剧) | | 结果自检 | 结束打印「共发请求 N 次,命中限流 M 次」+「实际速率 ≈ X 请求/小时」 | > 以上逻辑已用「加载生产文件本体 + 注入假 sleep 与可控时钟」的方式单测覆盖(**40 项断言全绿**)。 **怎么看收尾那两行日志**: ``` [pace] 期间共发请求 1987 次,命中限流 3 次 [pace] 实际速率 ≈ 1102 请求/小时(目标上界 1200) ``` - `命中限流 M`:偶发 1~5 次属正常(假 hasMore / 空页重试引起);**持续 > 0** 或出现 CF 403 页面(返回 HTML 而非 JSON)→ **换节点**,而不是继续降速。 - `实际速率`:长跑(几十个视频以上)会稳定落在目标以内;视频数很少时会偏高, 因为固定冷却占比低 —— 长跑自然收敛。 #### 顺带一个好消息:抓评论不牵连 TikTok 账号 评论全程走 tikwm,**不携带任何登录态**(`sessionid` 对评论接口无效,见 1.3)。 所以批量抓取的主要风险是「**IP 被镜像站/代理节点标记**」, 而**不是**「TikTok 账号被封」——脚本也不会拿 Cookie 去请求评论接口。 --- ### 1.6 登录态是否必需?换 IP 的边界在哪(2026-09-25 定论) **结论:不登录 TikTok 账号,本方案可完整跑通。** 上一轮的成功抓取 (9,931 顶级评论 + 3,316 回复)本身就是**无 Cookie** 跑出来的,已逐行核对脚本: | 链路 | 函数 | 请求头 | 用 Cookie? | |---|---|---|---| | 计数 | `fetchStats()` | `User-Agent` + `Accept` | **否** | | 评论 | `getCommentsViaMirror()` | `User-Agent` + `Accept` | **否** | | ~~官方评论~~ | ~~`getCommentsViaOfficial()`~~ | 曾含 `Cookie` | 接口已被风控拦死,**函数已于 2026-09-25 删除** | → 结论落地:`cookie` / `cookieFile` / `tryOfficial` 三项配置已从脚本移除, 主脚本现在**完全没有**携带登录态的代码路径。 **登录态唯一还能帮上忙的三个场景**(都不是"防封"): 1. 地区 / 年龄限制内容 —— 未登录时 `itemStruct` 可能整个缺失; 2. 私有、好友可见内容; 3. 想拿 TikTok 自己那份"完整 total"时(SSR 计数可能有延迟/取整)。 其中 1、3 用**目标地区的出口 IP** 同样能解决 → **短效 IP 确实可以替代登录态。** #### 换 IP 治的是哪一层(关键) 两条风控对象不同,混在一起看就会误判: | 层 | 谁在判定 | 换你的 IP 是否有效 | |---|---|---| | m.tiktok → TikTok Cloudflare | 你的出口 IP | **有效** | | tikwm 入口限流(并发 ≈ 2) | 你的出口 IP | **有效**(换 IP = 变成新客户端,额度重置) | | tikwm 服务端 → TikTok | **tikwm 自己的 IP** | **无效** | **三个反直觉点:** 1. **换频率 ≠ 提速。** 标定结论是"只卡并发、不卡频率"。只要串行跑, 30 分钟一换和 4 小时一换差别不大。换 IP 买的是「**被标后的恢复速度**」, 而不是「跑得更快」。 2. **换 IP 救不了 tikwm 上游被限。** 若 tikwm 自身取数被风控,表现为 **所有视频同时失败**(而非偶发 `code=-1`)——此时换节点无用,应等其恢复或换镜像。 3. **代理质量 > 更换频率。** 30 分钟一换的机房段 IP,比一个稳定的住宅 IP 更容易被 CF 直接判 bot;换太勤反而像扫描器。优先住宅 / 移动 IP。 #### 换 IP 的触发信号(按需换,而非纯定时) | 信号 | 判定 | |---|---| | `[pace] 命中限流 M` 持续 > 0 | 偶发 1~2 次属正常抖动(假 hasMore / 空页重试),**持续**才是信号 | | 连续出现 `code=-1` | 换节点 | | 返回 HTML 而非 JSON(CF 403 挑战页) | 换节点 | | 所有请求成功率骤降 | 换节点 | 命中任一条 → 立刻换节点;无异常时按期轮换(20~30 min)作兜底即可。 **换 IP 也治不了的天花板:** tikwm 顶级评论 cursor 上限 **≈ 10369**(见 1.5 与第六节), 这是镜像的数据完整性上限,与账号、IP 均无关。 > **合规提示**:用 IP 轮换替代登录态,不等于免除平台 ToS 约束。控制量级、 > 避免对单一镜像站高频施压。 --- ## 二、依赖与运行 1. Auto.js Pro 8;**只需悬浮窗权限**(看日志),无障碍、root 都不需要。 2. 手机网络必须能访问 TikTok(走 VPN)。 3. 运行: ```bash adb push tiktok_stats.js /sdcard/Download/tiktok_stats.js # Auto.js Pro 导入 → 运行 ``` 输出目录 `/sdcard/Download/tiktok_data/`: - `tiktok__stats.json` — 5 项计数 + 元信息 - `tiktok__comments.json` — 评论(结构化) - `tiktok__comments.txt` — 评论(可读文本) --- ## 三、关键修改点(CONFIG) | 配置项 | 说明 | |--------|------| | `shareUrl` | 目标短链 | | `awemeId` | 已知视频 ID 直填(当前 `7552202593888603410`),留空则解析短链 | | `maxComments` | `0`=顶级评论全量;`>0`=只抓前 N 条(调试建议先设 200) | | `commentPageSize` | 50(tikwm 上限,填大也没用) | | `useMirror` | ★ 评论来源:tikwm 镜像(`false` 则只抓计数、不抓评论) | | `proxyHost` / `proxyPort` | 一般留空;Auto.js 连不上时填 `127.0.0.1` / `2080` | | **`targetReqPerHour`** | ★ **小时级请求上界**(默认 1200 → 每请求至少 3s)。0=不限速 | | `requestDelay` / `maxRetries` | 页间最小间隔(400ms)与重试次数;**必须串行** | | `jitterRatio` | 页间间隔抖动比例(默认 0.4) | | `throttleBackoffFactor` | 命中限流时的退避倍数(2 倍,页间隔与令牌桶**同步**放大) | | `throttleMaxDelay` / `throttleMaxInterval` | 页间隔上限 8s / 令牌桶上限 30s | | `throttleRecoverPages` | 连续 N 页干净通过后**同步**回到基准(默认 20) | | `emptyStreakLimit` | 连续 N 页无数据才判定结束(默认 3,配合假 hasMore 容错) | | `maxOffset` | `0`=翻到无数据为止;设值则到该 cursor 停(实测上限 ≈10400) | | **`batchUrls`** | **批量模式**:填多个短链/ID 则按顺序批量抓(留空=只抓 `shareUrl`) | | **`videoCooldownMinMs`** | ★ 视频间冷却**底**(默认 15s) | | **`videoCooldownPerReqMs`** | ★ 每请求追加冷却(默认 250ms)→ 重视频自动多歇 | | `videoCooldownJitter` | 视频间冷却抖动(±50%) | | `batchCooldownEvery` / `batchLongCooldownMs` | 每 5 个视频长休 10 分钟 | | `skipExisting` | 批量模式下已有结果的视频跳过(**断点续跑**,仅批量生效) | | `fetchReplies` | **是否抓子回复**(默认 `false`,见 1.4 的量级说明) | | `replyTopN` | 抓回复最多的前 N 条评论的回复(默认 30) | | `replyMinTotal` | 只抓 `reply_total >=` 该值的评论(默认 2) | | `outputDir` | 输出目录(自动尝试多个候选并校验可写) | > **关于登录态**:本脚本**不提供** `cookie` / `cookieFile` 配置项,也刻意不带登录态。 > 计数与评论两条链路都不读 Cookie(见 1.6),且「登录态 + 轮换 IP」会造成同账号 > 跨地区跳变,是比匿名请求更强的风控特征。原先的 `cookie` / `cookieFile` / > `tryOfficial` 三项已于 2026-09-25 移除。 ### 批量抓取示例 ```js // 只改这一处即可批量:填链接或裸 ID 都行 batchUrls: [ "https://www.tiktok.com/t/ZTy8r7T2a/", "7552202593888603410", "https://www.tiktok.com/@someone/video/7512345678901234567" ], targetReqPerHour: 1200, // ★ 小时级上界(每请求至少 3s)—— 主要限速手段 videoCooldownMinMs: 15000, // 视频间冷却底 15s videoCooldownPerReqMs: 250, // + 每请求 250ms(重视频自动多歇) batchCooldownEvery: 5, // 每 5 个 batchLongCooldownMs: 600000, // 长休 10 分钟 skipExisting: true // 中断后直接重跑,只补没抓的 ``` **大批量长跑建议** - **想更保守**:只调 `targetReqPerHour: 600`(每请求 6s),其余不用动。 - **想更快**:先确认出口节点是**住宅 / 移动 IP** 再提高 —— 机房段 IP 提上去极易撞 CF 挑战。参考上限 **≤ 1500 请求/小时**。 - **无人值守**:直接跑即可。失败会自动重试并进入冷却;中断后原样重跑即可续抓。 - **换节点时机**:看收尾的 `[pace]` 行。`命中限流` 偶发属正常;**持续 > 0** 或出现 CF 403 页面就换节点,别靠降速硬扛。 --- ## 四、已修复的问题 1. **保存失败 ENOENT**:`files.ensureDir(dir)` 只创建传入路径的**父目录**,不创建 `dir` 自身。 已改为 `new java.io.File(dir).mkdirs()` 递归创建 + `createNewFile()` 可写校验, 失败自动降级到 `files.getSdcardPath()` / `files.cwd()` 等备选目录。 2. **Rhino `invalid return`(0.004s 闪退)**:Rhino 禁止顶层 `return`。 所有逻辑已收进 `main()`。 3. **收藏数时有时无**(246727 → 0):改为从 `m.tiktok.com` 的 SSR `stats` 块解析,稳定拿到。 4. **评论一直为 0**:官方接口全部被签名风控拦截,改用实测可用的镜像接口。 5. **提取器在云机上误命中 `/data/system/graphicsstats/...`**:`find /data -maxdepth 4` 兜底时把 graphicsstats 的日期目录当成了数据目录。已改为先 `pm dump` 拿权威 `dataDir`,再用 `/proc/1/root` 前缀探测可读性,不再靠 `find` 猜路径。 6. **提取器 grep 命中 0 项**:见 5.1「坑 2」,Cookie 存储是 Java 序列化 hex / SQLite, 不是明文,已改为结构化解析。 7. **评论只抓到 426 条(而不是 1 万+)**:tikwm 的 `hasMore` 会偶发**假 false**, 旧逻辑在 `cursor=400` 那页直接 `break`,累计正好 426。 已改为:**`hasMore` 只作提示不作终止**;空页**重试 3 次**;连续 3 页无数据才结束。 验证方式:录制 12 页真实响应(含出问题的那页)回放生产代码 → **426 → 559 条**,且正确打印「⚠ 第9页 hasMore=false,但实际返回 41 条 → 判定为假 false,继续翻页」。 8. **`reply_total` 误以为是总量**:tikwm 的 `total` 是「顶级评论 + 回复」的混合口径, 且回复量极大;已把子回复拆成独立可选阶段(`fetchReplies`),默认只抓 Top N。 9. **没有批量能力**:新增 `batchUrls` 批量模式 + 视频间冷却 + 批次长休 + `skipExisting` 断点续跑(**故意做在同一份脚本里**,避免复制出第二份代码后两边漂移)。 10. **退避后恢复过慢**(单测发现):原设计从 8000ms 回到 400ms 需 100 次成功, 相当于一次瞬时限流就把整段视频拖慢 20 倍。已改为「连续 20 页干净通过 → 一步回到基准」。 11. **被跳过的视频仍空等冷却**:批量续跑时会让 90% 的时间浪费在空等上。 已改为仅当本次视频真的发过请求才进入冷却。 12. **登录态零贡献却仍被保留、并诱导使用**(2026-09-25):逐行核对发现计数与评论 两条链路**都不读 Cookie**,唯一读 Cookie 的官方评论链路早已被风控拦死。 已删除 `buildCookie()` / `getCommentsViaOfficial()` 两个死函数, 以及 `cookie` / `cookieFile` / `tryOfficial` 三项配置 —— 顺带每个视频少打 1 次注定失败的请求。 13. **「固定 90s 视频间冷却」不适配大批量**:评论量差异达 20 倍(10 请求 vs 199 请求), 固定间隔让轻视频空等 5 倍时间、重视频又歇不够。已改为 **令牌桶(跨视频累计)+ 动态冷却(底 15s + 请求数 × 250ms)** 两级限速。 14. **失败视频不进入冷却**:原逻辑只在成功时冷却;但失败多因风控/网络, 且失败前通常已发过计数请求。已改为**失败同样进入冷却**,避免连续快跑放大风控。 15. **退避只作用于页间隔 → 在令牌桶面前形同虚设**(设计期规避 + 单测防回退): 引入令牌桶后,若 `code=-1` 退避只放大页间隔(400ms→8000ms), 仍会被令牌桶的 3s/请求淹没,退避等于没生效。 已改为**页间隔与令牌桶同步放大、同步回落**,并新增断言「令牌桶确实被退避放大」。 --- ## 五、登录态提取(root + shell) > ⚠️ **2026-09-25 提示:日常抓取不需要这一节。** > 已核实登录态对计数与评论**零贡献**(见 1.6),主脚本也不读 Cookie。 > 这里保留完整提取链路,仅用于:① 确实遇到地区/年龄限制内容需要验证; > ② 调试其他依赖登录态的接口。**不要**把提取到的 Cookie 塞进批量抓取流程。 ### 5.1 手机侧:`tiktok_extract_cookie.js`(Auto.js 版) ```bash adb push tiktok_extract_cookie.js /sdcard/Download/ # Auto.js Pro 导入运行 ``` **云机上踩的两个坑(已在脚本内解决,勿回退)** **坑 1 —— mount namespace 隔离** 云机会把 `/data/data` 用 **tmpfs 重新挂载成空目录**,只 bind 进 Auto.js 自己和 GMS: ``` tmpfs on /data/data type tmpfs (mode=751) ← 空壳 /dev/loop2 on /data/data/org.autojs.autojspro /dev/loop2 on /data/data/com.google.android.gms ``` 所以 `test -d /data/data/com.zhiliaoapp.musically` 返回 **NO**、`find /data` 只能扫到 `/data/system/graphicsstats/...` —— 这是**挂载视图**问题,不是权限问题,加 root 也没用。 解决:用 **`/proc/1/root` 前缀穿透**到 init 的挂载视图。`/proc//root` 是 magic symlink,会跟随目标进程的 mount ns,**不需要 CAP_SYS_ADMIN**(`nsenter` 需要,故仅作兜底): ``` /proc/1/root/data/user/0/com.zhiliaoapp.musically/shared_prefs/ttnetCookieStore.xml ``` 脚本按 `[/proc/1/root 前缀] → [nsenter] → [直接路径]` 依次探测并打印结果。 另外:`files.*` / `java.io.*` 在 **Auto.js 进程 uid** 下运行(非 root),读不到 `/proc/1/root` 下的文件 → 必须先用 root shell `cp` 到 `/sdcard`,再用 `files.read()`。 **坑 2 —— Cookie 存储不是明文** | 存储 | 形态 | grep 结果 | |------|------|-----------| | `shared_prefs/ttnetCookieStore.xml` | 值是 **Java 序列化 hex**(`ACED0005…`) | ❌ 永远 0 命中 | | `app_webview/Default/Cookies` | SQLite,name 与 value **分列** | ❌ 匹配不到 `name=value` | 所以必须结构化解析: - **XML**:`ACED0005…` 字节流结构 `[TC_STRING 名称][TC_STRING 值][TC_STRING 域名][TC_STRING 路径]`, 取第 2 个 `TC_STRING(0x74)` 即值。实测 170 条目 → 24 个 Cookie 名。 - **SQLite**:用 Android 内置 `android.database.sqlite.SQLiteDatabase` 直接查。 ### 5.2 PC 侧:`tiktok_extract_cookie_pc.js`(备用通道) ```bash node --experimental-sqlite tiktok_extract_cookie_pc.js ``` 同步用 `adb pull` 取两个存储,解析逻辑同上,并做双源交叉校验。 > 备注:这台云机的 `su` **不支持 `-c`**,必须用 `su 0 sh -c '...'`。 > Git Bash 下需 `export MSYS_NO_PATHCONV=1`,否则 `/sdcard/...` 会被转成 Windows 路径。 ### 5.3 实际能提取到的东西 24 个 Cookie 名(2018 字符):`sessionid` `sessionid_ss` `sid_tt` `uid_tt` `uid_tt_ss` `sid_guard` `msToken` `odin_tt` `passport_csrf_token` `passport_csrf_token_default` `multi_sids` `tt-target-idc` `store-idc` `cmpl_token` `d_ticket` `install_id` `ttreq` `store-country-code` `store-country-sign` `tt_session_tlb_tag` `user_oec_info` `BUYER_TOKEN` 等。 > `ttwid` **不在** App 的存储里(App 不持久化它),只能向 `www.tiktok.com` 现捞。 > 但当前主脚本**不需要**它 —— 不携带任何 Cookie 也能正常抓取(见 1.6)。 --- ## 六、取数链路 **计数**:`m.tiktok.com/v/.html` → `__UNIVERSAL_DATA_FOR_REHYDRATION__` → 定位 `itemStruct` → 遍历 `"stats":{...}` 取含 `diggCount` 的块。 失败降级 `embed/v2/` → 全局正则。 **全程不携带 Cookie**(未登录 SSR 同样完整)。 **评论**: 1. `www.tikwm.com/api/comment/list`(唯一来源,实测可用) - 从 `cursor=0` 串行翻页,单页 50,`hasMore` 假 false 忽略,空页重试, 连续 3 页无数据结束 → 实得顶级评论 **≈10369 条** 2. `www.tikwm.com/api/comment/reply`(子回复,可选) - 参数 `video_id` + `comment_id`(不需要 url) 3. ~~`www.tiktok.com/api/comment/list`~~(官方兜底 —— 已被 A-Bogus 签名风控拦死, 调用代码于 2026-09-25 移除,仅保留此说明) **输出**: - `tiktok__stats.json` — 5 项计数 + `topLevelComments` / `repliesFetched` - `tiktok__comments.json` — 结构化(回复挂在父评论的 `replies` 数组) - `tiktok__comments.txt` — 可读文本(回复以 `└─` 缩进)