常见问题与术语表
这一页回答的是人们在部署前后真正会问的问题,然后把本套文档用到的每个术语解释 一遍。读完之后,你应该能判断这套软件是否适合你的场景,也能在读其它页面时不必 停下来查名词。
每个答案都出自代码或配置默认值。如果某个答案取决于项目控制不了的东西——平台 怎么做、你所在地的法律怎么规定——文中会直说,而不是猜。
合法性、责任与许可协议
Section titled “合法性、责任与许可协议”这样做合法吗?
Section titled “这样做合法吗?”没有人能替你所在的司法辖区回答这个问题,本页也不构成法律意见。能说准的只有: 这套软件做了什么,以及责任落在谁身上。
它是一个客户端。跑在你自己的机器上、用你自己的账号,像浏览器一样向抖音和
TikTok 发请求。它不对外发布任何东西,除非你主动开放,否则别人用不了;并且默认
所有接口都要求凭据(api.public_endpoints 默认为空)。没有任何数据被上传到别处。
也就是说,全部责任都在你这边。具体包括:
| 你需要承担的 | 实际含义 |
|---|---|
| 平台的服务条款 | 抖音和 TikTok 各有各的条款,自动化采集通常是违反条款的。本项目里没有任何人能替你豁免这一点。 |
| 当地法律 | 你所在地的数据保护法可能适用于你归档的内容:一条归档的作品里带着另一个人的昵称、头像、文字和图片。有些法域不论内容是否公开,都把这些当作个人数据。 |
| 二次分发 | 下载了视频不等于视频归你。归档和媒体卷是给你自己用的;把里面的东西发布出去是另一件事,后果也另算。 |
| 不要拿它对付具体的人 | 定时关注、定时打快照、反复重采某一个人,是这套软件给你的能力。那究竟是研究还是骚扰,由你决定,不由工具决定。 |
README 里用一段话说的是同一件事,这是有意为之。项目的立场是:自部署工具无法强制 执行上述任何一条,所以它不假装能——它做的是让采集行为可见:每次请求都有记录, 每次配置变更都有审计,而且没有任何采集会指向你没有点过名的作者或作品。会自行发起 的外连有四类:往身份池里铸造身份、探测你自己配置的代理、复查你已经归档的东西是否 还在,以及按周期重采你自己加进关注列表的条目。
可以商用吗?
Section titled “可以商用吗?”可以。许可协议是 Apache License 2.0, 允许商业使用、修改、再分发,也允许放进闭源产品。这项授权不可撤销。你不需要申请, 也不欠作者任何东西。
协议要求你做的:
| 义务 | 说明 |
|---|---|
| 保留协议与版权声明 | 分发副本时带上 LICENSE 文件,并保留已有的版权声明(§4a、§4c)。本仓库没有 NOTICE 文件,所以 NOTICE 条款(§4d)无内容可带。 |
| 声明你的改动 | 你修改过的文件必须显著标注被改动过(§4b)。 |
| 不含商标授权 | 协议授予的是著作权和专利权,不包括用项目名称或 Logo 为你的产品背书的权利(§6)。 |
| 专利授权是有条件的 | Apache-2.0 给了你专利许可(§3),如果你发起专利诉讼主张本软件侵权,该许可即终止(§3 末句)。 |
| 不提供任何担保,不承担责任 | 软件按“现状”提供(§7、§8)。它弄坏了你的部署、让你的出口地址被封、或者丢了数据,都由你自己承担。 |
有两件事这份协议做不到,而且在这里都很关键:
- 它不是平台给你的许可。 Apache-2.0 是作者就作者的代码给出的授权,它对抖音 和 TikTok 的服务条款只字未提,也无从提起。
- 它不是技术支持合同。 没有人欠你一个回复;下文“获取帮助、报告问题与赞助” 一节说明什么方式是有效的。
作者在商用许可之外附了一个请求——如果你靠它赚到了钱,请考虑也赞助一下,而不是 只拿。这段话写在 README 里,也显示在控制台的“关于”页上,并明确标注为请求、与协议 条款分开,因为把两者混在一起会既夸大协议、又弱化请求。你拒绝它不需要付出任何代价。
账号、Cookie 与封号风险
Section titled “账号、Cookie 与封号风险”需要用我自己账号的 Cookie 吗?
Section titled “需要用我自己账号的 Cookie 吗?”不需要,默认配置也从来不会向你要。
实例会自己维护一池游客身份:无头浏览器通过你配置的代理打开平台页面,让平台
给这个会话下发它自己的 Cookie,并记录浏览器实际呈现出来的指纹。ttwid 是这个版本
唯一“没有就不干活”的 Cookie,而平台会把它发给任何访客。整条路径里不涉及任何登录。
只有两种情况会用到你自己的 Cookie:
- 你没有启用
browserprofile。 没有浏览器容器就没有铸造,身份池只能靠你 粘贴进来的 Cookie 罐。这个罐子可以来自一个未登录的浏览器窗口,一样能用—— 它会带ttwid,通常还带s_v_web_id、odin_tt和msToken,这就是一个完全合格 的游客身份。在控制台的「身份池」页(/identities)导入,或者调用POST /api/v1/admin/identities/import。 - 你要的内容只有登录后才可见——你自己的私密作品、你在平台上关注了谁的名单, 或者任何平台设了门槛的数据。这时才需要导入那个账号的 Cookie 罐,并且每一次需要 它的请求都必须指定到那个身份上,因为用替代会话返回的答案根本不是你问的那个 问题的答案。
两条流程都见身份与代理。
我的账号会被封吗?
Section titled “我的账号会被封吗?”如果你没有导入账号,就没有账号可封。这正是游客池的意义:某个身份被拒了就冷却,
如果它一直失败,就不再计入身份池的可用数量(pool.max_fail_streak,默认 3),于是
填充器会铸一个新的来补,而不是把一个废掉的身份当成产能。退休那个废身份是手工动作
——没有任何机制会自动退休——但烧掉一个游客身份的代价,只是几分钟的铸造时间。
如果你确实导入了登录态的 Cookie 罐,那么暴露的就是那个账号,导入对话框里对此 说得很直白:
- 一套登录态 Cookie 等同于账号密码。 它加密存储(AES-256-GCM,用
DTK_SECRET_KEY派生),也不会有接口顺手把它返回出来——查看明文需要operator角色外加admin或identity:manage权限,并且会写一条审计记录——但你这个实例的 管理员是能用它的。 - 用一个专门为此准备的账号,绝不要用你的主号。
软件为保护任何身份(不论是否游客)所做的事:
| 机制 | 效果 |
|---|---|
| 每身份同时只有一个在途请求 | 一个身份不会同时开着两个请求,因为真实会话不会这么干。 |
| 每(身份,接口)令牌桶 | 一个身份没法把全部额度花在最敏感的那一个接口上。 |
| 风控响应触发冷却 | sched.cooldown_base_seconds(60 秒)按连续失败次数翻倍,再乘以该接口的风险权重,上限 sched.cooldown_max_seconds(21600 秒,即 6 小时)。 |
| 绝不重新组合各个部分 | Cookie 罐永远不会换到别的代理上,也永远不会配另一个 User-Agent。同一个出口地址上轮换 Cookie,比单纯的请求频率更刺眼。 |
| 熔断器 | 当一个接口在多个身份上同时失败时,对它的请求会停下来,而不是把剩下的池子也烧掉。 |
以上都不是保证。两个平台都不公布自己的规则,也都在改规则,任何声称能保证你账号 安全的工具都是在猜。诚实的结论是:游客身份便宜、本来就是拿来烧的;登录身份不是, 所以请用小号、给它单独配代理、并盯着「身份池」页上的冷却情况。
别人在我的实例上也有账号,他们能用我导入的登录身份吗?
Section titled “别人在我的实例上也有账号,他们能用我导入的登录身份吗?”只有你允许才行。身份池是实例级的,行记录没有归属人,所以在请求里指定身份
(?identity=<uuid>)需要 operator 角色并且具备 admin 或 identity:manage
权限。一把普通的只读 key 碰不到登录会话,而且被指定了身份的请求永远不会写进共享的
响应缓存——只属于某个会话的答案不能变成别的调用方读得到的东西。见
用户与 API 密钥。
代理、身份与吞吐量
Section titled “代理、身份与吞吐量”一定要用代理吗?
Section titled “一定要用代理吗?”不一定,但你多半还是会想用。
如果一条代理都没配,身份池填充器会用宿主机的直连出口铸造身份。这能用,也正是一个 没有代理的用户所要求的。但它同时意味着你所有的身份都挤在同一个地址后面——而这 恰恰是整套设计在别处竭力避免的那种“重新组合”。这是最容易让你的地址被注意到的配置, 要不要这么做由你决定。
配了代理之后,填充器会把每个新身份绑定到一个该平台还没有活跃身份在用的出口,并且 在所有代理都被占满时宁可等待也不叠加。所以代理数量实际上就是你身份池规模的 上限。
有两个后果值得在买代理之前知道:
- 代理挂掉会让它后面的身份进入冷却,而不是换到别的出口。Cookie 本身是好的, 坏的只是出口。一条永久失效的代理,最终意味着退休它名下的那些身份。
- 国家和时区不是装饰。铸造时它们会作为地理提示交给浏览器,让身份的语言和时钟与
它的流量看上去来自的地方对得上。一条德国代理配上
Asia/Shanghai的时钟,是白送 给对方的矛盾点。
我需要多少个身份?
Section titled “我需要多少个身份?”默认值是 pool.min_size 3、pool.target_size 8,按平台各算一份。可用数量掉到
最小值以下时填充器才开始补,补到目标值就停,所以健康的池子稳定在两者之间,而不会
因为差一个身份就每分钟被重新触发。
一个身份到底值多少,是由令牌桶决定的,而令牌桶是按(身份,接口)来算的:
| 接口 | 突发容量 | 补充速率 | 单身份持续速率 | 8 个身份持续速率 |
|---|---|---|---|---|
*.content_detail |
5 | 0.30/s | 3.3 秒一次 | 约 2.4/s |
*.author_profile |
4 | 0.20/s | 5 秒一次 | 约 1.6/s |
*.comments、*.comment_replies、*.mix_posts |
3 | 0.15/s | 6.7 秒一次 | 约 1.2/s |
*.author_posts、*.author_likes |
3 | 0.12/s | 8.3 秒一次 | 约 1/s |
tiktok.author_followers、tiktok.author_following |
3 | 0.12/s | 8.3 秒一次 | 约 1/s |
| 未登记的接口 | 3 | 0.15/s | 6.7 秒一次 | 约 1.2/s |
右边这一列是完全均摊情况下的天花板,不是承诺:身份在每次请求期间还持有独占锁, 所以上游一慢,这些数字就会往下掉。
请从你实际需要的量倒推。如果你一天只解析几十个链接,默认值已经很宽裕,可以把
pool.target_size 调小。如果你要把一个作者的整个作品列表走完,那么
author_posts 的“每身份每 8.3 秒一次”就是你真正的约束,而唯一提速的办法是增加
身份——也就是增加代理。调大令牌桶不是那个旋钮,它是在保护你已有的身份。
身份池为什么填得这么慢?
Section titled “身份池为什么填得这么慢?”这是设计使然。填充器每个 tick 最多铸造一个身份,一个 tick 是 60 秒,而且这一次
铸造是所有平台共用的,不是每个平台各有一次:每个 tick 只挑最紧缺的那一个池子。所以
一个从空池起步的部署,大约需要 16 个 tick、也就是一刻钟左右,两个池子才会都到达目标
值 8,还不算每次铸造本身花的时间。铸造期间它持有一把跨进程的 Redis 锁,这样
--scale worker=N 也不会把一次铸造变成 N 次;铸造持续失败时会指数退避(60 秒起,
翻倍到 3600 秒封顶)。
同一个部署在一秒内冒出五个新访客,这件事本身就比这些身份之后要做的任何事都更刺眼。 而且没有任何东西在等铸造——它完全不在请求路径上,所以填得慢不等于 API 慢,只是在 填满之前池子小一点。
有一个刻意为之的例外:如果现存的每一个身份都在失败,填充器会按住不动而不去 铸造。所有身份同时失败是平台级事件,往里补新身份,只是把新鲜身份喂给那个正在烧掉 其它身份的东西。
能抓什么,抓不到什么
Section titled “能抓什么,抓不到什么”支持 Bilibili、小红书、快手或微博吗?
Section titled “支持 Bilibili、小红书、快手或微博吗?”不支持。v5 支持两个平台:douyin 和 tiktok,这就是
src/dtk/core/types.py 里 Platform 枚举的全部内容。
V4 确实支持 Bilibili。v5 没有把它带过来,因为在 v5 里增加一个 平台远不止一个爬虫文件:它需要自己的签名实现、自己的参数构造、自己的解析器(输出 共用的归一化模型)、自己的接口策略(供令牌桶使用),以及自己在结果分类器里的规则。 一个只做了一半的平台——能抓到数据但失败分类是错的——会污染其它所有平台都依赖的 健康数据。
如果你想加一个,参与开发说明了这项工作的实际范围。
能下载无水印的视频吗?
Section titled “能下载无水印的视频吗?”平台提供了无水印流的时候可以。解析器会把无水印流排在媒体清单的第一位,下载器取
第一个流,所以默认路径拿到的就是干净的文件。抖音单独的“下载地址”带的是烧进画面的
水印;它被保留为一个额外的流并标记 watermark: true,而不是被丢掉,同时没有任何
逻辑按码率重排这个列表——那样做会悄悄挑中一个更大且带水印的文件。
准确地说:这里没有任何东西“去除”水印,它只是选取平台本来就提供的那条无水印流。 如果某条作品只有带水印的流,你拿到的就是带水印的。
它究竟能抓哪些东西?
Section titled “它究竟能抓哪些东西?”两个平台的链接、带文字的分享口令、短链,或者裸的作品 ID 都行;此外还有作者主页、 作者的作品与喜欢列表、评论及其回复、合集,TikTok 上还有粉丝与关注列表。调试台页面 和 REST API 暴露的是同一套能力。完整清单见调试台与工具 和 REST API 指南;为什么其中有些比另一些更费身份额度,见 核心概念。
浏览器里能打开的链接,为什么这里返回 NOT_FOUND?
Section titled “浏览器里能打开的链接,为什么这里返回 NOT_FOUND?”因为平台就是这么答的。抖音对不存在的作品返回 HTTP 200、空的负载和一个原因码;
TikTok 对已删除的作品返回两个不同的状态字段。两者都被归类为 business_error,不会
让身份付出任何代价——反过来把它们当成风控,正是过去一个打错的 ID 就能冷却一个好
身份、反复几次就能替所有人触发熔断的原因。
如果它在你浏览器里能打开而这里不行,常见原因是:内容只对登录账号可见(导入 Cookie 罐并为请求指定身份),或者它对你身份所在的出口做了地区限制。 故障排查讲了怎么把这两种情况区分开。
存储、磁盘与硬件
Section titled “存储、磁盘与硬件”视频会存在服务器上吗?
Section titled “视频会存在服务器上吗?”只有你打开了才会,而且要同时满足两个条件:downloader profile 在运行,并且
DTK_DOWNLOADER_URL 指向它。两者缺一,POST /api/v1/downloads 会返回 501 并附上
开启方法,其它一切照旧。
解析本身存的是元数据,不是字节。响应里带的是平台自己的 CDN 链接,服务端不做
中转——通过实例代理视频流量是明确排除在范围之外的。归档保存的是解析后的作品:
标题、作者、标签、计数,以及包含这些链接的媒体清单。这些链接是带签名且会过期的,
所以当已存镜像链接的年龄超过 media.mirror_max_age_seconds(600 秒)时,下载会
重新解析该作品。
下载器启用之后,文件按 <平台>/<作者>/<作品 ID>/ 的结构落在一个 Docker 卷上,
旁边有一个 meta.json;带 media:read 权限时,API 可以通过
GET /api/v1/downloads/{download_id}/files/{name} 把文件交给浏览器。
需要多少磁盘?
Section titled “需要多少磁盘?”| 项目 | 大小 | 说明 |
|---|---|---|
| 镜像(核心栈) | 约 4.5 GB | 其中 timescale/timescaledb-ha:pg17 约占 3.9 GB |
镜像(含 browser profile) |
约 6.3 GB | 浏览器镜像约 1.7 GB |
| 已存媒体 | media.max_bytes,默认 2 GiB |
超出后最旧的未置顶下载会被删掉文件,并由 media_evicted 告警说明删了什么。设为 0 关闭淘汰 |
| 单个媒体文件 | media.max_file_bytes,默认 512 MiB |
按实际写入的字节计算,而不是按服务端声称的大小 |
| Postgres 卷 | 随你保留的数据增长 | 见下文 |
数据库是会自己长大的那部分。有三样东西决定它长多快,还有一个看着像保留策略、 实际上什么都不决定:
retention.request_log_days(14)修剪“每请求一行”的日志。archive.store_raw(默认关闭)会额外保存各平台未经处理的原始负载。它是这个 实例可以选择存储的单项最大的东西,所以默认关闭。audit_log不被任何东西修剪,这是有意的。它会在实例的整个生命周期里缓慢增长。retention.content_days(0 = 永不删除)是为归档声明的,也会出现在/settings上,但目前没有任何代码读这个键,所以无论你填什么,归档都不会被自动清理。 默认的“永不删除”是刻意的——归档存在的意义正是活得比平台更久——一个连跑一年的 实例会留下它解析过的每一条作品。要删除归档作品,请在资料库里删。
除了媒体淘汰,没有任何机制会为了腾空间而删数据。取而代之的做法是“停手”:超过
capacity.warn_percent(80)会告警,超过 capacity.hard_stop_percent(92)后台采集
和新的下载任务暂停,而交互式读取照常。把磁盘满变成“我的 API 挂了”,比它想要避免的
那个故障更糟。见运维。
能跑在树莓派或 NAS 上吗?
Section titled “能跑在树莓派或 NAS 上吗?”核心栈大概可以,浏览器容器不行。
在 2 GB 内存里能跑得舒服的是这些——静息状态实测:postgres 112 MiB、api 104 MiB、
worker 71 MiB、redis 8 MiB。跑不动的是 browser-rpc:compose 文件给它的上限是
4 GiB 内存和 4 个 CPU,而实测预热后是 2.57 GiB 和 629% CPU,并且它的浏览器 profile
放在 tmpfs 上(/tmp 3 GB、/profiles 1 GB),那是内存不是磁盘。
所以在小机器上现实的部署方式是:只跑核心栈,不启用 browser profile,身份从你
笔记本的浏览器里手工导入。这是受支持的模式,不是坏掉的状态——不设置
DTK_BROWSER_RPC_URL 时,铸造任务会被整个跳过,而不是每分钟打一条错误日志。
关于架构:代码里没有任何 x86 特有的东西,唯一可能出问题的依赖——负责 TLS 模拟的
wreq——发布了 manylinux aarch64 的 wheel。镜像是在你自己的宿主机上构建的,所以
不存在多架构镜像仓库的问题,而所固定的 Postgres 镜像同时提供 arm64 和 amd64。
如果你改了这个镜像固定值,用同样的办法确认新的那个:
docker manifest inspect timescale/timescaledb-ha:pg17 | grep architecture另外把磁盘算进去:还没存任何数据就要约 4.5 GB 镜像,对一张 SD 卡来说不算少;而把 数据库放在 SD 卡上,出于另外的原因也是个坏主意。
网络、隐私与外发请求
Section titled “网络、隐私与外发请求”在中国大陆以外能用吗?抖音需要中国出口吗?
Section titled “在中国大陆以外能用吗?抖音需要中国出口吗?”这份代码里没有任何地区门槛。平台接受哪些出口是平台自己的决定,而且它们并不公开, 所以本项目也不会在这件事上给出任何方向的承诺。
软件给你的手段是:
- 身份按平台划分,每个身份绑定一个出口。没有任何东西阻止你把抖音身份放在一个地区 的出口后面、把 TikTok 身份放在另一个地区——对一台无法同等访问两边网络的主机来 说,这就是常规配置。
- 代理的国家和时区会作为提示交给铸造用的浏览器,让身份的区域设置与出口相符。
browser-rpc在创建上下文之前,还会通过该代理去探测DTK_BROWSER_GEO_PROBE_URL(默认https://ipinfo.io/json)来判断出口所在国家, 探测不到时退回DTK_BROWSER_DEFAULT_COUNTRY(US)。 - 代理探测器每 300 秒扫一遍所有代理,把延迟、出口地址、国家和时区写回记录。
与其泛泛地问,不如针对你这台机器回答:用控制台的「诊断」页(/diagnose)或者
dtk diagnose,它会走六步——组件、外网出口、代理、身份池、签名、冒烟抓取——并告诉
你是哪一步挂了。
它会“回传数据”吗?
Section titled “它会“回传数据”吗?”不会,而且每一处本来可能外连的地方都是显式选择加入的:
| 外发请求 | 默认 | 说明 |
|---|---|---|
| 更新检查 | system.check_updates 为 false |
配置说明写的是“这会发出外部请求,因此需要你主动开启”。控制台的检查是在你的浏览器里点击时向 GitHub releases API 发起的,永远不是服务端发的 |
| 赞助商 Logo | 由你自己的实例提供 | 本地的一份 16 KB 副本,这样打开控制台不会告诉第三方你的部署存在 |
| 地理探测 | 铸造时通过身份的代理访问 ipinfo.io |
把 DTK_BROWSER_GEO_PROBE_URL 设为空即可关闭 |
| 任务回调 | security.enable_task_webhook 为 false |
即使开启,回调地址也必须是 https,而且不能解析到私有地址或回环地址,因为调用方自带的回调 URL 是最典型的请求伪造入口 |
没有埋点、没有崩溃上报、没有授权校验。其余离开这台机器的流量都是发往平台的,走的
是你配置的身份,并且都记录在 request_log 里。
如果我把实例放到公网上,会暴露什么?
Section titled “如果我把实例放到公网上,会暴露什么?”默认情况下:一个只绑在回环地址上的对外端口,而且上面的每个接口都要求凭据。把它
放得更开是一个显式动作(DTK_BIND_HOST=0.0.0.0),而且容器本身不做 TLS 终止。动手
之前请先读安全——尤其是 api.public_endpoints、
security.cors_allow_origins 和 security.request_proxy 这三节,它们是把暴露面
放大得最多的三个设置。
v5 与 v4 的区别,以及有没有托管版
Section titled “v5 与 v4 的区别,以及有没有托管版”v4 和 v5 之间变了什么?
Section titled “v4 和 v5 之间变了什么?”v5 是从空分支起步的重写,与 V4 没有共用代码。直接克隆仓库拿到的就是它:
git clone https://github.com/Evil0ctal/Douyin_TikTok_Download_API.git| V4 | v5 | |
|---|---|---|
| 平台 | 抖音、TikTok、Bilibili | 抖音、TikTok |
| 凭据 | Cookie 手工粘进 crawlers/*/config.yaml,过期了再手工换 |
实例自己铸造并维护的一池身份,加密存储 |
| 失败时的表现 | Cookie 过期或算法变更后静默失败,直到用户来报错 | 每身份健康度、每接口熔断器、每请求一条结构化记录、告警 |
| 基础设施 | 不需要 | Postgres(带 TimescaleDB)和 Redis |
| Web 界面 | PyWebIO | React 控制台 |
| 其它入口 | REST | REST、MCP、CLI |
| 存储 | 无,结果是临时的 | 内容归档、计数快照、媒体下载、合集、关注列表 |
| 部署 | pip install -r requirements.txt、python3 start.py |
docker compose -p dtk -f docker/compose.yml up -d |
用维护者自己的说法,重写的理由是:V4 的问题从来不是功能少,而是接口会悄悄死掉 而没人知道。v5 把可观测和自愈排在功能前面,而那些明确不做的事同样是它的一部分—— 不做 AI 功能、不做任何形式的计费或多租户、不引入 Kafka/Elasticsearch/MinIO/k8s、 不通过服务器中转视频流量。
如果你要再分发,还有一个差别很重要:v5 的签名实现是为本项目、从平台自己的产物逆向 出来的,正是为了让整棵树在 Apache-2.0 下干净。V4 的 A-Bogus 模块是一份 GPL-3.0 代码 的移植。
能把 V4 的安装原地升级到 v5 吗?
Section titled “能把 V4 的安装原地升级到 v5 吗?”不能原地升级。两者没有共用的表结构、配置文件和代码——v5 把一切放在 Postgres 里,并
用 DTK_SECRET_KEY 加密凭据,而 V4 把 Cookie 放在 YAML 里。请把它当作一次全新部署:
把 v5 在旁边跑起来;如果想要旧的 Cookie 罐,就在控制台里把它们作为身份导入。
有托管版吗?
Section titled “有托管版吗?”本项目没有。没有托管的 v5,没有免费额度,也没有可以在这里购买的 API key——这套软件
就是为跑在你自己控制的机器上而设计的,而那也是你的 DTK_SECRET_KEY、你的身份和你的
代理唯一该待的地方。
如果你宁愿买数据也不想自己跑爬虫,本项目的赞助商 TikHub.io 在商业售卖托管的社交数据 API。那是一项付费的第三方服务,在这里作为赞助商披露、在 控制台里也如实标注;它不由本项目运营,本项目也不对它作任何担保。
获取帮助、报告问题与赞助
Section titled “获取帮助、报告问题与赞助”怎么获得支持?
Section titled “怎么获得支持?”两条路径,建议按这个顺序:
- GitHub issues—— 公开、自带历史记录,任何遇到过同样问题的人都能回答。
- 给作者发邮件——只会进到一个人的收件箱。地址在控制台的“关于”页和仓库里。 通过邮件寄来的问题,大多数在 issue 区会更快得到答案。
在此之前,请先按故障排查走一遍。“我部署好了但拿不到 数据”这类反馈里,很大一部分原因是代理、Cookie 罐、签名路径或网络——正好是诊断已经 替你看过的那四个地方。
报告问题时该附上什么?
Section titled “报告问题时该附上什么?”| 附上 | 从哪里拿 |
|---|---|
| 诊断报告 | 控制台的 /diagnose,或 dtk diagnose。它在生成时就做了脱敏:代理密码、Cookie 和 API key 根本不会进入报告,所以“把这个贴到 issue 里”才是安全的建议 |
request_id |
响应信封里的 meta.request_id。它和写进 request_log 的是同一个 id,这才让一份报告变成可以查的东西,而不只是一段描述 |
| 版本与 commit | 控制台的「系统信息」页(/system),或 dtk --version |
| 你问了什么、返回了什么 | 确切的 URL 或调用,以及完整的错误信封——尤其是 error.code,它是稳定的枚举值 |
| 浏览器与下载器 profile 是否在跑 | docker compose -p dtk -f docker/compose.yml ps |
绝对不要粘贴 Cookie 罐、API key、带凭据的代理 URL 或你的 .env。issue 是公开的,
而 Cookie 罐是凭据;登录态的那种,就是某个人的账号。
有两类情况特别值得报告,因为它们通常意味着平台变了、而不是你配错了:
UPSTREAM_CHANGED,以及 Logs 页上出现的 signature.refused 或 signature.rejected
规则名。
怎么赞助这个项目?
Section titled “怎么赞助这个项目?”可以通过 GitHub Sponsors。Solana、Tron (TRC20)、 Ethereum (ERC20)、BNB Smart Chain (BEP20) 和 Bitcoin 的加密货币地址列在 README 和 控制台的“关于”页上——请从那里复制,不要从别处,并且只能按地址所属的网络转账, 因为转错链的资产任何人都无法找回。
赞助项目(README 和控制台里的 Logo 位)与支持维护它的人是两回事;README 把 它们分成两节,正是为此。
下面是本套文档中用到的术语,用平白的话解释。凡是别处有精确定义的,链接页里有完整 说明。
身份与浏览器
Section titled “身份与浏览器”| 术语 | 含义 |
|---|---|
| 身份(identity) | 本系统调度的基本单位:一个 Cookie 罐、一份浏览器指纹、一个代理绑定和一段失败历史,在这行记录的整个生命周期里绑在一起。它不等于 Cookie——这四个部分永远不会被重新组合。 |
| Cookie 罐(jar) | 一个身份在平台上的会话 Cookie 集合,用 DTK_SECRET_KEY 加密存储。ttwid 是这个版本“没有就不干活”的唯一一个。 |
| 指纹(fingerprint) | 铸造用的浏览器对自己的描述:浏览器家族与主版本号、User-Agent、navigator.platform、屏幕几何、语言、时区、CPU 与内存提示。它决定了链路上使用的 TLS profile,也会被喂进签名。 |
| 铸造(minting) | 生成一个新身份:驱动真实的无头浏览器,通过这个身份将要绑定的代理访问平台,让 Cookie、指纹和出口三者自洽。由 browser-rpc 完成,每 60 秒的 tick 最多一个。 |
| 导入(importing) | 获得身份的另一条路:粘贴你自己浏览器里的 Cookie 罐。可自动识别四种格式——Cookie: 请求头、Cookie 编辑器扩展导出的 JSON 数组、Netscape cookies.txt,以及零散的 key=value 行。 |
| 出口(egress) | 请求实际发出的地址:一条代理,或者宿主机自己的连接。一个身份终生绑定一个出口。 |
| 游客身份 | 没有登录态的身份。便宜、可弃,也是身份池默认铸造的东西。 |
| 登录态 Cookie 罐 | 带真实会话(sessionid、sid_tt、sid_guard 等)的导入身份。等同于该账号的密码,请用小号。 |
| 身份状态 | minting、active、cooling、degraded、retired 之一。只有 active 参与正常轮换;retired 的 Cookie 罐已被清空,无法恢复。 |
| 冷却(cooldown) | 身份收到风控响应后的休息时间:sched.cooldown_base_seconds 按连续失败次数翻倍,再乘以该接口的风险权重,上限 sched.cooldown_max_seconds。 |
| 健康度(health score) | success_rate × (1 − risk_rate) × 0.5^consecutive_fails,取值在 [0, 1]。用相乘而不是加权求和,是为了让任何一项塌下来都能把总分拉下去。 |
| 指定身份(pin) | 指名某次请求必须走哪个身份。一次指定身份就是“只有一个成员的池”——任何拒绝都不会让它放宽,双向绕过响应缓存,并且只有一次传输尝试而不是三次。需要 operator 角色,外加 admin 或 identity:manage 权限(控制台会话只受角色限制)。 |
| 术语 | 含义 |
|---|---|
| 签名(signing) | 计算平台在肯回答之前所要求的额外查询参数和请求头。少了它们,请求会被拒绝,或者更糟——返回一个空 body。 |
| 原生签名器(native) | 用纯 Python 重写的平台算法(src/dtk/signing/native/)。每次签名耗时微秒级,不需要浏览器。这是默认值(signing.mode = native)。 |
浏览器签名器(rpc) |
在 browser-rpc 里运行站点自己的 JavaScript 来签名。耗时数百毫秒,但平台改算法时它会自动跟上。原生实现失效的那天就该切到这里。 |
a_bogus |
抖音 Web 当前使用的签名参数,由它的 bdms.js 产生。签名负载里包含浏览器的几何信息,所以它必须用与发送该请求相同的指纹来计算。 |
X-Bogus |
更早的抖音 / TikTok Web 签名。TikTok Web 至今仍接受它,部分抖音接口也接受。 |
X-Gnarly |
TikTok Web 的 webmssdk 追加到每次 API 调用上的四个参数之一——单次请求的封印。它是一个自带密钥的加密块。 |
X-Dynosaur |
与 X-Gnarly 配套的参数:TikTok 对发起调用的浏览器所做的环境报告。 |
x-secsdk-web-signature |
抖音在它做了签名保护的那部分接口上要求的请求头(14 条路径,抄自抖音自己的 Web SDK)。计算它需要该身份的 uifid Cookie。在这份路径清单之外,抖音自己的页面同样是不带签名发请求的。 |
msToken |
两个平台都会作为 Cookie 下发、并在查询串里回显的一个会话 token。TikTok 的 X-Dynosaur 和 X-Gnarly 是用身份自己的 msToken 算出来的,所以缺少它的 Cookie 罐无法走原生签名。 |
ttwid |
访客标识 Cookie。两个平台都必需,没有它的身份会被拒绝使用。 |
s_v_web_id / verifyFp |
同一个值的两处出现:抖音的 verifyFp 查询参数,就是算出这个签名的那个浏览器的 s_v_web_id Cookie。用一个会话签名、却带着另一个会话的 Cookie 发出去,这两者指向的是不同的访客。 |
| 签名保护路径 | 平台自己的 SDK 会签名的那些抖音接口。清单在 src/dtk/signing/protection.py;清单之外的路径,抖音自己的页面也是不签名就发。 |
| 术语 | 含义 |
|---|---|
| 调度器(scheduler) | 只回答一个问题的组件——此刻哪个身份可以发这个请求——或者给出一个既能告诉调用方、也会被记录下来的拒绝理由。 |
| 令牌桶(token bucket) | 按(身份,接口)计的额度:一个突发容量加一个补充速率。花掉一个令牌,请求才能发出去;只有 network_error 才退还令牌,因为根本没到达平台的请求没有花掉这个身份的额度。 |
| 在途锁(in-flight lock) | 每身份一把,跨所有接口,TTL 120 秒。一个身份一次只发一个请求,因为真实会话不会并发调 API。 |
| 量化 LRU | 轮换顺序:先按健康度层级(5 个粗分桶),再按最久未使用(时间戳按量化步长取整),最后是抖动。分桶正是为了避免最健康的那一个身份每次都赢。 |
| 熔断器(circuit breaker) | 按接口计。在 300 秒滚动窗口内,风控比例超过 sched.circuit_risk_threshold(0.6)、样本数不少于 sched.circuit_min_samples(20)、且失败横跨至少 sched.circuit_min_identities(3)个不同身份时触发。打开 300 秒,每 60 秒放一个探针。 |
| 风控(risk control) | 平台拒绝的是这个身份——验证码页、验证信封、429,或者一个 200 却把负载扣住不给。它会让身份的连续失败数加一、施加冷却,并计入熔断判断。它与业务错误的区分是刻意的:一条被删的视频绝不能算到一个好 Cookie 头上。 |
| 结果(outcome) | 对一次上游响应的分类:ok、business_error、risk_control 或 network_error。系统里每一个健康数字都挂在这一个判断上。 |
| 拒绝原因(reject reason) | 调度器拒发租约的理由,记录在 request_log 里:circuit_open、no_identity、no_token、all_inflight、pinned_unavailable、wait_timeout。还有一个 queue_full,它是 API 层在任务创建之前就做出的拒绝,只出现在错误信封的 details.reject_reason 里,永远不会写成 request_log 的行。 |
| 合并(coalescing) | 多个调用方同时问同一件事时,第一个任务在 Redis 里认领这个摘要(TTL 90 秒),其余的挂到同一个任务上,而不是各自排队。如果认领它的那个任务已经失败,这条认领会被丢弃而不是被加入。 |
| 响应缓存 | 按归一化后的业务参数做键,而不是按原始 URL,所以同一个链接的两种写法命中同一条缓存。命中不消耗身份额度,但仍然会被记录。?refresh=true 跳过读取但照样写入。 |
请求、任务与数据
Section titled “请求、任务与数据”| 术语 | 含义 |
|---|---|
| 信封(envelope) | 每个响应都带的四个顶层字段:success、data、error、meta。error.code 是可以用来分支判断的稳定枚举;旁边的 error.message 是本地化文本,永远不要去解析它。(这个词也指平台自己包在负载外面的那层 JSON,比如“信封完好但里面什么都没有”。) |
request_id |
meta.request_id 里的那个 id,同时也写进 request_log。引用它,才能把一份问题报告变成别人查得到的东西。 |
| 任务(task) | 一个排队的工作单元。数据接口默认是异步的:返回 202 和一个 task_id,你可以轮询 /api/v1/tasks/{id}、传 ?wait=N,或者用回调。状态流转是 queued → running → done | failed。 |
?wait=N |
请求服务端把连接挂住直到任务结束,上限是 api.max_wait_seconds(30)。没在时限内完成会返回 202 和 task id——那不是错误,也没有丢任何东西。 |
| 游标(cursor) | 指向下一页的不透明字符串。它底下的值在有些接口上是毫秒级发布时间戳,在另一些接口上是纯偏移量,两个平台都是如此;编码后的字符串把这个差别藏了起来,调用方不需要知道是哪一种。 |
raw |
平台未经处理的原始负载,挂在每个解析出的模型上,方便日后回算新字段。除非显式索取,否则不出现在 API 响应里;只有开启 archive.store_raw 时才会存进归档。 |
| 归档(archive) | 这个实例解析过的每条作品的记录,以结构化行存在 Postgres 里——不是文件。上游删掉的作品在这里仍然读得到,靠的就是它。 |
| 快照(snapshot) | 某一时刻对一条作品或一个作者计数值的一次观测(content_snapshots)。一系列快照才能把“120 万播放”变成一条增长曲线。 |
| 可用性(availability) | 归档对“它还在不在”的回答:live、deleted、private 或 unknown。unknown 是检查失败时留下的,刻意不写成 deleted。 |
| 资料库页(Library) | 归档之上的控制台页面。它从 Postgres 取数,所以浏览它不消耗身份,也不会发起抓取。 |
| 合集(collection) | 人工创建的一组归档作品——“要留着的”、“剪辑要用的”。它是资料库里唯一不是从作品本身派生出来的分组方式;没有任何逻辑会去推断成员关系。 |
| 关注列表(watchlist) | 按固定周期重新采集某个作者或某条作品的条目,把快照变成真正的时间序列。周期下限 watchlist.min_interval_seconds(900 秒),默认 watchlist.default_interval_seconds(6 小时)。 |
| 下载(download) | 把一条作品的媒体存到你磁盘上的请求,以及记录它的那一行。调用方从不提供媒体 URL——一次下载指向的是这个实例已经解析过的作品。 |
| 置顶下载(pin) | 把已存文件标记为不受淘汰影响。没有任何机制会自动设置它,而且它是唯一的豁免;一个没有豁免的按容量淘汰策略,迟早会删掉某人本来想留住的那个文件。 |
| 淘汰(eviction) | 媒体卷超过 media.max_bytes 后,删掉最旧的未置顶下载的文件。记录本身、它的摘要和文件清单会保留下来,并写上 files_removed_at,这样“采集过后来被清理了”和“从来没抓过”仍然分得清。 |
| 术语 | 含义 |
|---|---|
| 角色(role) | 控制台账号能做什么:demo < viewer < operator < admin,是阶梯而不是集合。demo 由演示模式生成,排在最底下。 |
| 权限范围(scope) | 一把 API key 能做什么:douyin:read、tiktok:read、archive:read、archive:export、media:read、media:write、identity:manage、admin。即使这把 key 属于管理员,它也仍然受自己的权限范围限制。 |
| API key | 脚本或 agent 在 X-API-Key 或 Authorization: Bearer 里发送的凭据。创建时只显示一次,库里只存前缀和 SHA-256 摘要。可以带自己的每分钟限流值,否则套用 api.default_rate_limit_per_min(120)。 |
| 限流(rate limit) | 防滥用手段,与计量和收费无关。它唯一的目的是不让一个跑飞的脚本把身份池抽干。 |
| 初始化令牌(setup token) | 全新实例在 API 日志里打印的一次性令牌,用来创建第一个管理员。它在 Redis 里存活 24 小时,能扛住 4 次失败尝试——第 5 次就会把它删掉;一旦有账号存在,/setup 就永久关闭。 |
| MCP | Model Context Protocol,AI 客户端(Claude Code、Claude Desktop、Codex CLI 等)发现并调用工具的协议。本实例在 API 进程内于 /mcp/ 提供服务,共 8 个只读工具,没有任何工具会碰到凭据。见 MCP 与 AI 客户端。 |
explain |
抓取时的一个选项,把请求原样交还给你——签名后的 URL、请求头和 Cookie 罐。它返回的是凭据,所以需要 operator 角色外加 admin 或 identity:manage 权限(控制台会话只受角色限制),并且会被审计。 |
| 诊断(Diagnose) | 控制台里的六步自检(组件、外网出口、代理、身份池、签名、冒烟抓取),命令行是 dtk diagnose。它的报告在生成时就已脱敏,所以贴到 issue 里是安全的。 |
| 术语 | 含义 |
|---|---|
aweme_id |
抖音的作品 ID,19 位数字。全程按字符串处理,因为它超出了 JavaScript 的安全整数范围,用整数会在界面上悄悄丢精度。抖音的评论回复接口把同一个值拼作 item_id;评论列表接口仍然叫它 aweme_id。 |
content_id |
本项目对作品 ID 的中立命名,两个平台通用。 |
sec_user_id |
抖音稳定的作者主键,也是归一化模型里作者的 uid。抖音自己那个会变的 uid 被刻意弃用,用户名同样不作为键。 |
secUid |
TikTok 的不透明作者键,它自己的接口会用它(有时与用户名二选一)。归一化模型把 TikTok 的数字 id 存为作者的 uid,并把 secUid 保留在 raw 里;从 URL 出发的主页查询用的是 @handle(uniqueId),因为那是 TikTok 用户详情接口无需先抓一次就能接受的输入。 |
unique_id |
就是 @ 用户名。从不作为键使用,因为用户可以改。 |
| 内容类型(content kind) | video、image_album 或 live。只有详情响应才能定下来是哪一种——URL 的形状只是提示,永远不作数。 |