ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

抖音评论采集实战:浏览器自动化绕过签名,批量获取用户ID与评论数据

抖音评论采集实战:浏览器自动化绕过签名,批量获取用户ID与评论数据 做短视频数据分析的人大概率都遇到过这个需求想批量拉某个视频下的评论内容连同发评论的用户ID一起整理成表格用来做舆情分析、竞品观察或者评论区成分研究。手动去网页版翻评论翻几页就手酸而且抖音网页版的评论列表是动态加载的复制粘贴也拼不出完整数据。自己写爬虫吧又卡在签名参数上——抖音网页端接口的X-Bogus、msToken这些动态签名一直在变普通requests根本过不去更别提频繁请求还会触发滑块验证。这篇内容想聊的是我把抖音用户评论和ID采集这件事真正落地的一套组合方案不硬刚签名算法而是用浏览器自动化配合接口监听把评论内容和用户ID稳定地拿下来整个过程踩过的坑、绕过的弯一并说清楚。适合新媒体运营、舆情分析、独立开发者参考或者单纯想研究抖音评论区数据结构的技术爱好者。1. 先分清抖音号、UID、sec_uid和评论ID后面才不会抓瞎很多人在网上搜抖音用户ID采集其实连目标对象都没完全搞明白。抖音生态里跟ID沾边的东西太多了先把这个基础概念理清后面所有代码和流程才有意义。1.1 官方页面上能看到的是抖音号接口里给的是另一套你在抖音App里点进一个人的主页看到的那串英文加数字的自定义名称比如douyin123、zhangsan_life是抖音号。这是用户可以自定义的对外展示标识理论上可以改来改去所以它不适合做数据的稳定主键。真正稳定的是抖音内部的UID经常是十几位、二十几位的纯数字它一经分配基本不变类似于身份证号。但这里有个细节很多评论接口返回的用户信息里主键其实是sec_uid这是一串经过编码的长字符串用来构造用户主页链接。uid字段有些时候反而为空或者只返回short_id就是用户主页展示的那串数字短ID。1.2 一条评论背后到底返回多少字段无论通过什么方式采集最终拿到的评论数据对象长这样从抖音网页端评论接口里截取后简化{ cid: 7312345678901234567, text: 这个视频拍得真好, digg_count: 156, create_time: 1700000000, user: { uid: 4500000000000000, sec_uid: MS4wLjABAAAAGxxxxxxxx-xxxxxxxxxxxxxxxx, short_id: 1234567890, nickname: 示例用户, signature: 个人简介, follower_count: 1024 }, reply_comment: [] }这里的cid是评论IDtext是评论正文user对象里就是评论用户的各项ID信息。可以看到一条评论天然携带了用户UID、sec_uid、昵称、粉丝数等字段也就是说——你采评论的时候用户ID已经顺带拿到了不需要再单独跑一遍用户主页采集。很多人不知道这个点回来问我评论和ID是不是要分两个脚本其实一次请求就全有了。1.3 热词里说的抖音号转UID是怎么回事网上有不少人搜索抖音号转uid在线工具本质上是想在只知道抖音号自定义字符串的情况下查出这个人的数字UID和sec_uid。这个需求存在是因为部分第三方数据分析平台只认UID或者说后续要拼用户主页链接必须用sec_uid。实现原理并不复杂拿到用户主页链接通常是https://www.douyin.com/user/{sec_uid}访问这个页面时网页源码里会输出一份名为RENDER_DATA的JSON数据里面包含完整的用户信息UID、sec_uid、粉丝数、作品数都在里面。这就是那些在线转换工具的后台逻辑。但这里我要提醒一句第三方在线工具把你输入的用户主页链接拿去做什么你并不清楚。自己有能力的话用下面章节讲的浏览器方案顺手就把这个信息一起抓了不用依赖来路不明的网站。2. 三条采集路线怎么选我为什么最终锁定网页端浏览器自动化目标对象清楚了接下来是路线选择。抖音的采集路径网上说法很多但真正在实操层面靠谱的其实就三条。2.1 三条路线的真实对比方案原理优点缺点直接请求Web接口用requests调用www.douyin.com/aweme/v1/web/comment/list/速度快、数据干净需要处理X-Bogus、a_bogus、msToken动态签名维护成本高APP端抓包用抓包工具拦截抖音App的评论接口数据结构完整需要处理设备指纹、证书校验、App版本兼容门槛更高浏览器自动化Playwright/Selenium控制真实浏览器访问网页版监听网络响应无需逆向签名天然通过风控速度略慢依赖前端页面结构稳定三条路我都实际跑过。APP端抓包那条最麻烦的还不是抓包本身而是抖音App的接口有设备指纹校验你模拟的请求签名对不上服务端直接拒绝。Web端直接请求呢每次接口签名规则一更新你的脚本就废了而且新版网页端动态签名算法一直在升级手动逆向的性价比越来越低。所以我的结论很明确中小规模采集用Playwright控制真实浏览器监听页面加载过程中的评论接口响应拿JSON数据。这是目前综合成本最低、最抗接口变动的方案。2.2 动态签名不是无解但没必要硬碰硬有人会问网页端接口的X-Bogus到底能不能逆向能网上有很多开源项目在做但你要想清楚维护成本。签名算法迭代频率不低每次更新都要重新定位加密入口、补环境、调试你花一个周末逆向出来的东西下个月可能就失效了。浏览器自动化的思路是借力——真实浏览器在发起评论请求时签名参数都是前端JS自动生成的你不需要理解它怎么生成只要在浏览器发出请求后把响应内容截获下来就行。这相当于把逆向签名这个最难啃的骨头直接从流程里删掉了。2.3 这套方案的短板也提前说清楚没有任何方案是万能的。浏览器自动化的短板有两个一是性能一个浏览器实例同时打开的标签页有限如果你一天要采几千个视频单靠这条路效率不够二是页面结构变动会影响到滚动加载和响应监听的逻辑比如抖音改了评论区懒加载机制脚本就需要微调。如果只是像我一样做做数据分析、舆情监测一天采几十上百个视频的评论这套方案绰绰有余。真要跑大规模采集那是另一个量级的工程问题涉及分布式IP、设备指纹池属于另一个话题了。3. 从一条分享口令到完整评论数据集全流程实操理论部分到这里下面进入正题。这一节给你一条完整的实操链路跟着做就能跑通。3.1 第一步解析分享短链拿到视频aweme_id抖音分享口令通常长这样https://v.douyin.com/xxxxxxx/一个短链接。你需要先请求一下这个短链跟随重定向拿到真实的长链接。import requests def resolve_share_url(short_url: str) - str: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } resp requests.get(short_url, headersheaders, allow_redirectsTrue, timeout10) return resp.url long_url resolve_share_url(https://v.douyin.com/xxxxxxx/) print(long_url) # 典型输出https://www.douyin.com/video/7312345678901234567长链接里的最后那串数字就是aweme_id也就是视频作品ID。如果是图文笔记路径里可能是/note/提取方法一样。import re def extract_aweme_id(long_url: str) - str: match re.search(r/(?:video|note)/(\d), long_url) if not match: raise ValueError(无法从链接中提取视频ID) return match.group(1)这里有个常见问题短链接可能包含额外的跟踪参数重定向后的URL里也可能带query参数所以用正则匹配/video/或/note/后的数字最稳妥不要直接用split(/)[-1]容易被参数干扰。3.2 第二步监听评论列表接口而不是解析网页DOM拿到aweme_id之后用Playwright打开这个视频页面。关键点是你要在页面跳转之前就注册好网络响应监听器这样页面加载时如果触发了评论接口请求你就能第一时间拿到返回数据。from playwright.sync_api import sync_playwright COMMENTS [] def handle_response(response): if aweme/v1/web/comment/list in response.url and response.status 200: try: data response.json() comments data.get(comments, []) COMMENTS.extend(comments) print(f本批新增 {len(comments)} 条累计 {len(COMMENTS)} 条) has_more data.get(has_more, 0) print(f还有更多{has_more}) except Exception as exc: print(f解析评论接口失败{exc}) with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 有头模式更稳不容易被判机器人 context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, viewport{width: 1366, height: 900} ) page context.new_page() page.on(response, handle_response) video_url fhttps://www.douyin.com/video/{aweme_id} page.goto(video_url, wait_untildomcontentloaded) page.wait_for_timeout(3000) # 模拟滚动触发评论懒加载 for _ in range(8): page.mouse.wheel(0, 2500) page.wait_for_timeout(1200) browser.close()为什么不解析网页DOM因为评论区是动态渲染的你得不断滚动页面让新评论加载出来再从DOM里提取文本麻烦且容易受页面改版影响。而直接监听接口响应拿到的是结构化的JSONtext、create_time、user信息全都在干净利落。3.3 第三步把评论JSON清洗成目标数据表接口返回的comments列表里一个元素对应一条评论里面字段层级嵌套比较深需要拍平一下再入库。以下面的格式输出最终数据import json def flatten_comments(comments): rows [] for c in comments: user c.get(user, {}) rows.append({ comment_id: c.get(cid), video_id: c.get(aweme_id, ), text: c.get(text, ).strip(), digg_count: c.get(digg_count, 0), create_time: c.get(create_time), reply_total: c.get(reply_comment_total, 0), user_sec_uid: user.get(sec_uid), user_uid: user.get(uid), user_short_id: user.get(short_id), user_nickname: user.get(nickname), user_follower_count: user.get(follower_count, 0) }) return rows result flatten_comments(COMMENTS) print(json.dumps(result[:3], ensure_asciiFalse, indent2))这个阶段你就可以把评论ID、视频ID、用户sec_uid、UID这些核心字段一并导出成CSV或Excel了。注意一个细节评论的text字段里可能带\n换行符和多余空格建议做一次strip处理避免后面做数据分析时被脏数据干扰。3.4 第四步凭sec_uid反查用户主页信息如果采集评论时发现数据里缺用户粉丝数、作品数这些字段可以基于sec_uid构造用户主页链接再打开一次页面拿完整信息。构造方式非常简单https://www.douyin.com/user/{sec_uid}比如评论区抓到一个用户的sec_uid是MS4wLjABAAAAxxxx拼出来的主页链接就是https://www.douyin.com/user/MS4wLjABAAAAxxxx。用同样的浏览器自动化方案打开这个页面监听用户信息相关接口就能拿到粉丝数、关注数、获赞数、达人认证等更丰富的资料。这一步在竞品账号分析时特别有用同一个视频下的评论用户过滤出粉丝量大于某个阈值的达人用户就能快速找到潜在的合作资源。这时候建议用表格记录完整数据字段来源用途sec_uid评论接口用户主页唯一标识构造主页链接uid评论接口数字ID跨平台关联short_id评论接口展示用短IDnickname评论接口用户昵称follower_count用户主页接口判断用户影响力4. 采集脚本的地基分页游标、去重、限速与存储能跑通单次采集之后下一步是让它变得可靠、可复用。这一节涉及的工程化问题直接影响采集脚本能不能长期运行。4.1 评论列表的游标翻页逻辑评论接口返回的数据结构里有一个非常关键的字段cursor。这是下次请求要用的游标不是页码。第一次请求时cursor为0请求结束后服务端返回的JSON里有下一批数据的cursor值以及has_more字段表示是否还有更多评论。这个逻辑和传统的第1页、第2页不太一样。如果页面滚动得足够深评论接口可能被触发多次每次响应里的cursor都在变。你在监听器里拿到的每一次响应其实已经是服务端基于当时游标返回的下一批数据了。如果想要全量拉取某个视频的所有评论比如几万条的大爆款单纯靠滚动页面效率不够。更稳的做法是从监听到的响应里拿到最新cursor和has_more然后在浏览器上下文里主动用fetch继续请求剩余部分# 注意这个方法需要在Playwright的page.evaluate里执行复用浏览器的Cookie和签名 page.evaluate( async () { let cursor 0; let hasMore true; let allComments []; while (hasMore) { const url /aweme/v1/web/comment/list/?aweme_id${awemeId}cursor${cursor}count20device_platformwebappaid6383; const resp await fetch(url, {headers: {accept: application/json}}); const data await resp.json(); allComments allComments.concat(data.comments || []); hasMore data.has_more 1; cursor data.cursor; await new Promise(r setTimeout(r, 800)); } return allComments; } )这里借用了浏览器页面上下文里的fetch好处是签名参数由页面环境自动补全你不用手动处理X-Bogus。注意循环里加了800ms的延时给服务端一个喘息空间这是规避风控的基础操作。4.2 去重与增量更新的两种玩法评论区有个特性热门视频的评论量很大接口在游标翻页过程中可能返回部分重复数据尤其是翻页过程中有新增评论时。所以不管采集多少次最终入库前必须做去重。简单做法是拿comment_id做唯一键因为评论ID是全站唯一的。如果你用的是SQLite或MySQL直接建唯一索引CREATE TABLE comments ( comment_id TEXT PRIMARY KEY, video_id TEXT, text TEXT, digg_count INTEGER, create_time INTEGER, user_sec_uid TEXT, user_uid TEXT, user_nickname TEXT );之后每次采集用INSERT OR IGNORE天然去重。如果采集频率高比如每小时采一次监控评论区变化这个方案能保证新评论追加、老评论自动跳过。用JSONL文件存储的话去重思路也简单每次采集完读入已有文件里的comment_id集合再用集合过滤新数据最后追加写入。两种方式我都用过推荐SQLite查询和去重都省事。4.3 限速参数怎么定才不容易触发风控决定采集频率的核心是三组时间参数每次滚动间隔、每次请求间隔、同一IP下的并发数。根据实际测试页面内的滚动间隔尽量不要低于1秒也就是说每次滚动之后至少等1秒再滚下一次主动fetch分页时的请求间隔建议在700ms-1500ms之间随机不要用固定值同一视频下连续采集最多采到没有has_more为止然后休息15-30秒再切换下一个视频。之前看到有人图省事把间隔压到200ms结果最后一整批请求全部返回429状态码还触发了滑块验证。用随机延时看起来是小细节但对风控系统来说非人类的稳定频率本身就是最明显的机器特征。我习惯在脚本里加一点抖动import random import time sleep_time random.uniform(0.8, 1.5) # 每次请求之间随机休息0.8-1.5秒 time.sleep(sleep_time)4.4 数据存JSONL还是SQLite数据量小几千条评论存Excel就够数据量到了几十万条Excel打开都费劲。我给一个参考标准1万条以内CSV/Excel方便人工查看10万条以内JSONL方便程序化处理不丢格式10万条以上SQLite或MySQL必须有索引JSONL的每行是一条独立的JSON对象读取时逐行解析内存占用低。它最大的好处是不需要建表、改表结构采集脚本里加字段很方便适合数据格式还在快速迭代的阶段。等格式稳定了再一次性导入数据库。我自己就是在项目早期用JSONL后期入库SQLite做分析。5. 真实踩坑记录滑块、短链过期、乱码与429采集中间肯定会遇到各种意外。这里把我实际踩过的几个坑展开说说每个都给了对应的排查思路。5.1 踩坑一滚动速度太快直接触发滑块第一次写采集脚本时我把滚动间隔压缩到0.3秒想着快点采完。结果跑到第4个视频就被要求滑块验证浏览器弹出一个请拖动滑块完成拼图后续所有请求全部被拒。这个问题的本质是账号维度的风险评分被拉高了。虽然没有登录但抖音会给访问者生成一个设备标识同一个浏览器实例短时间内高强度访问多个视频页很容易被判定为异常。解决思路滚动间隔降到1.2秒以上单个浏览器实例不要连续访问超过10个视频多视频采集时每采完一个视频随机等待20-40秒再开始下一个只在页面确实需要滚动时才滚动不要无脑滚固定次数另外建议用有头模式headlessFalse无头浏览器的特征太明显。虽然会弹出浏览器窗口但稳定性高很多这一点在采集场景里比无头看起来更酷重要得多。5.2 踩坑二分享短链接当天有效次日就解析失败抖音的分享口令短链v.douyin.com/xxxxxx有有效期不是永久有效的。我遇到过前一天还能正常重定向的短链第二天直接404的情况。所以采集流程里解析完短链、拿到aweme_id之后要立刻把aweme_id落库保存后续任何环节重新采集都用这个数字ID拼直链不要再依赖短链接。如果短链确实已经失效还有一个兜底方案在抖音App里重新复制分享口令。但如果你已经保存了aweme_id根本不需要走这一步。5.3 踩坑三评论里的emoji和隐形字符让入库翻车抖音评论区的emoji出现频率极高尤其是颜文字和生僻Unicode字符。第一次往SQLite里写数据的时候直接用Python的sqlite3模块插入结果遇到包含特殊表情的评论直接抛OperationalError后来才发现是编码问题。解决方法是两层的表字段定义为TEXTPython侧连接SQLite时显式指定text_factorystr入库前对text做清洗保留emoji但去掉控制字符import re def clean_text(text: str) - str: # 去掉零宽字符和控制字符保留emoji text re.sub(r[\u0000-\u0008\u000b\u000c\u000e-\u001f], , text) return text.strip()如果text里带有特殊不可见字符比如iOS输入法生成的变体选择符后续做词频统计时会发现莫名其妙的分词错误这一步清洗能省掉后面许多麻烦。5.4 其他高频报错清单报错特征原因处置建议429 Too Many Requests请求频率过高触发限流增大请求间隔降低并发休息几分钟滑块验证弹出风险评分被拉高降低采集频率更换访问节奏接口返回verify字段需要验证码停用该IP和账号一段时间评论接口403Cookie失效或签名过期重新打开页面刷新Cookiecursor不增长评论区被折叠直接结束该视频采集不必死磕看到429别慌它只是服务端的限流机制在起作用不是封禁。停下来等几分钟调低频率再跑就行。真正要警惕的是那种接口直接返回空数据、status_code还是200的情况那通常意味着你的访问被静默降级了这时候要立刻停手检查账号状态。6. 这套技术能做什么、不能做什么边界必须拎清技术文章写到这还是要把边界说透。采集公开评论这件事本身不复杂但怎么用、用到什么程度是每个做数据工作的人都要想清楚的问题。6.1 能安全采集的是公开数据评论内容和用户公开主页信息本质上是用户主动公开发布的数据采集这类公开信息用于数据分析是目前行业内比较多见的场景。但有几条线绝对不能碰不采集用户私信、手机号、微信号等未公开信息不对用户进行批量关注、批量私信等骚扰性操作不把采集到的用户信息用于诈骗、非法营销等违法违规场景不绕过平台的技术限制去抓取需要特定权限才能看到的数据《个人信息保护法》对个人信息的处理有明确规定做采集之前最好想清楚自己的数据用途是否合法合规。6.2 关于匿名采集的一些实话热门搜索词里有抖音匿名采集很多人想不登录就去采集降低自己被关联的风险。真实情况是网页版抖音不登录也能看评论但能访问的深度和数量都有限制而且不登录状态下的接口风控判定更严格因为你没有账号行为历史数据可以背书。我的建议是正常登录一个日常使用的账号来访问网页版频率正常的话基本不会被骚扰。所谓匿名反而更容易触发验证。当然如果有人脑子里想的匿名采集是隐藏身份去搞监控之类的操作那种需求超出了这篇文章的讨论范围。6.3 数据使用阶段的合规底线采集完成之后数据存储和使用环节同样要注意存储时做必要的脱敏处理比如用户昵称在分析报告中尽量二次加工、不公开原始数据库、不做用户画像的非法商业化。合规的合理用途包括新媒体账号自身的评论数据分析、学术研究、市场调研、以及用户授权范围内的数据处理。数据是好东西但使用数据的姿势要正。我在给团队做内部工具时也会把合规要求直接写进代码仓库的README里让每个拿到脚本的人先看到使用边界。回到开头的话题。做评论采集这件事最容易走的弯路就是去逆向签名、硬碰风控。实际上用浏览器自动化将签名生成这个环节整个跳过才是中小企业或个人开发者值得投入的方向。我跑这套方案已经大半年了中间经历过多次抖音前端改版评论接口URL换过几次参数但因为监听的是浏览器真实网络请求脚本基本不受影响。最后提醒一句脚本能帮你把数据快速拿下来但运营判断和数据分析才是真正体现价值的部分别把精力全耗在采集上面。
返回列表