尧图精选

抖音用户主页视频数据采集实战:接口签名、游标翻页与风控应对

🕒 发布时间:2026/10/2 14:36:20 📁 来源:尧图网络
做抖音用户主页的视频数据采集听起来好像挺简单但真正跑起来才会发现这里面的水远比想象中深。不是写个requests请求、解析一下JSON就能交差的。点赞、收藏、分享这些看似普通的“数字”背后牵扯到接口签名、游标翻页、动态字段、风控限流、数据一致性等等一系列问题。我把这段时间做抖音爬虫的完整过程、踩过的坑、还有最终的工程化方案整理出来希望对想接手这类活的朋友有点帮助。先说清楚这个项目是干什么的给定一个抖音用户的个人主页链接自动采集该用户发布的全部视频列表并抓取每个视频的点赞数、评论数、收藏数、分享数、播放量、发布时间、文案等公开信息。适合做竞品账号分析、达人投放筛选、内容监控这类场景。整个方案基于Python 3.9 requests 异步并发数据落地到MySQL代码结构上考虑了增量更新和断点续采。1. 项目背景与整体方案选型1.1 这个采集项目到底要解决什么问题市面上其实已经有不少现成的抖音数据工具但要么收费高要么字段不全要么数据更新不及时。自己动手写采集方案的核心诉求无非就三点可控、可扩展、省成本。可控是指请求频率、采集范围、数据字段都能自己说了算。可扩展是指今天采用户主页明天想采某个话题下的视频、某个音乐下的作品你不需要推翻重来而是把核心的请求、解析、入库逻辑抽象出来复用。省成本就更直接了免费接口能搞定的数据没必要去买昂贵的第三方数据服务。另外一个隐藏的需求点是数据质量。你要拿去分析的数据必须能回答清楚“这个点赞数是什么时候采的”“为什么昨天和今天的数据对不上”这类问题。所以这个项目里我把时间戳、数据快照、去重策略都当成了核心设计来考虑而不是顺手加一两个字段完事。1.2 技术栈与方案选型的四个考量项目最终选定的技术栈是Python 3.9、requests、asyncio aiohttp、MySQL数据库、Redis做简单的分布式锁。之所以这么选有四个实际考量第一Python在数据采集和分析生态上毫无悬念是最顺手的特别是配合pandas做后续的数据清洗和统计省掉大量搬砖时间。第二requests稳定可靠适合80%的常规请求场景。很多人一上来就追求大而全的框架其实没必要。只有碰到需要高并发、海量请求的时候才值得上aiohttp做异步IO。我在这个项目里的做法是同步requests负责核心采集逻辑因为要控制顺序和频率aiohttp负责批量更新场景下的高并发任务两者组合而不是二选一。第三存储选了MySQL而不是MongoDB原因是数据字段相对规整后期做报表、关联查询都方便。视频数据的关键字段加到索引里查询速度非常快。如果只是自己用SQLite也完全够但考虑到可能会多人一起看数据还是上了MySQL。第四Redis主要用来做增量采集时的任务锁防止同一个用户被多个采集进程同时抓取白白增加风控风险。这个其实到后期才加的前期单机单进程跑根本不需要但如果你要部署成集群任务这个锁就必须有。1.3 合规边界先讲清楚在讲任何技术细节之前有些话必须说在前面。本方案只针对公开的、用户本身已经公开分享的内容做采集不碰任何需要登录后才能看到的私有数据不采集用户的私信、联系人、位置等隐私信息也不对数据做商业化的转售。采集频率控制在合理范围不给目标服务器造成压力。如果你是做商业项目请务必先仔细阅读抖音的Robots协议和相关服务条款确认你的使用场景符合规定。技术本身是中性的但怎么用是每个人自己的选择。2. 抖音用户主页的数据来源与接口拆解2.1 两种主要的数据获取路径抖音用户主页的数据获取业内常用的思路基本有两条路解析页面注入数据和直接调用Web接口。第一种思路是从用户主页HTML源码中提取一个叫RENDER_DATA的script标签。这个标签里其实是一大段URL编码后的JSON里面就包含了用户基本信息和前几个视频的列表数据。好处是不用处理签名参数打开页面有什么拿什么坏处是只有首屏数据想拿完整列表还是得翻页所以它通常只是作为“初始数据”来使用。第二种思路是直接调用抖音Web端的作品列表接口通过sec_user_id和游标参数来翻页取全量数据。这条路能拿到的字段更完整、更结构化但通常需要携带签名参数这个后面单独讲。实际项目里我建议两条路都保留页面解析作为兜底和校验接口调用作为主力。万一接口改了签名逻辑导致拿不到数据至少页面解析还能顶上。2.2 从页面源码提取注入数据的思路用户主页URL大概是这样的格式https://www.douyin.com/user/MS4wLjABAAAAxxxxxx末尾那一长串就是sec_uid。我们写代码时可以用正则把它提取出来然后请求这个地址拿到HTML之后去匹配RENDER_DATA。打开浏览器开发者工具看一眼你会发现在script idRENDER_DATA typeapplication/json这个标签里的内容是经过了encodeURIComponent编码的必须先解码才能正常解析JSON。解码之后里面会有一个类似app、user、post这样的嵌套结构。我的经验是不同的网页版本结构可能略有差异所以解析代码里要做多级容错不能写死某个层级。这段数据里虽然已经有了视频列表但只是前几个而且字段名是压缩过的比如视频信息里直接嵌套了aweme_detail用起来不够直观。所以我的策略是把这个JSON里的核心字段先拉出来作为第一批数据随后立即进入接口轮询模式继续翻页补全。2.3 接口参数与响应字段全解读在Web端作品列表接口的URL可以简化成这样一个结构https://www.douyin.com/aweme/v1/web/aweme/post/?sec_user_idxxxmax_cursor0count18device_platformwebappaid6383其中sec_user_id就是主页URL里那一串用户标识max_cursor是游标第一次请求为0后续翻页使用上一次响应返回的max_cursor值count是每页数量一般建议18到20不要贪多接口往往会限制上限。device_platformwebapp和aid6383是Web端固定的通用参数。响应里的核心字段集中在aweme_list这个数组里。每个视频对象常用的公开字段包括字段含义备注aweme_id视频ID全局唯一适合做主键desc视频文案可能含有表情符号入库要注意编码create_time发布时间秒级时间戳展示时需转换时区author.nickname作者昵称公开展示的昵称statistics.digg_count点赞数核心指标之一statistics.comment_count评论数核心指标之一statistics.collect_count收藏数部分视频不返回注意空值statistics.share_count分享数部分视频不返回注意空值statistics.play_count播放量并非所有视频都返回该字段video.play_addr.url_list视频播放地址可用于本地备份场景注意版权这里有个很容易踩的坑收藏数collect_count并不是所有老视频都有某些视频接口返回的字段里直接没有这个key代码里如果直接data[statistics][collect_count]就会直接KeyError崩溃。正确的做法是用get方法加默认值同时统一做空值处理把它转成0而不是NULL。2.4 签名校验的原理与合规调试建议Web端这些接口直接拿requests去请求Unicode大概率会遇到签名校验问题。简单说服务端会校验请求头里某些动态生成参数是否正确如果缺失或者不对接口会返回异常或者干脆返回空列表。这是抖音前端自己的一套签名生成逻辑在页面加载时由JavaScript动态计算出来的。项目里处理签名参数的方式分为两步走。第一步打开浏览器开发者工具切到Network面板过滤XHR请求找到aweme/post这条接口在请求头里能看到完整的签名参数。第二步在实际开发中有两种做法一是在自己代码里用纯Python复刻JS的签名逻辑二是通过本地调试工具截取浏览器生成的参数。我的建议是如果只想采集几十个账号根本不需要自己去复刻签名直接用浏览器调试时抓到的固定参数就能跑通大部分场景。只有做大规模分布式采集才需要考虑动态生成签名。这里要特别提醒复刻签名逻辑属于对平台防护机制的逆向工作如果用于商业用途可能会带来法律风险请务必谨慎评估。调试时有一个很实用的技巧请求失败不要急着改代码先手动在浏览器里再访问一次目标用户主页看看接口是否正常返回。如果浏览器里正常说明平台没有限制你这种访问行为问题出在请求参数上如果浏览器里也不正常那大概率是账号或IP层面被限制了。3. 核心代码实现从请求到入库的完整流程3.1 请求构造与基础工具函数我习惯先把请求会话封成一个公共模块。最关键的是请求头的构造HTTP头部必须模拟真实浏览器的行为不能带着Python-requests的默认UA上去。我的经验是至少要把UA、Referer、Cookie这几项配齐。UA可以在浏览器里复制最近版本Chrome的UA字符串Referer设置为用户主页地址Cookie则需要登录后在浏览器里复制过来一般包含登录身份标识和偏好设置。下面是我项目里实际用到的请求工具函数为了演示方便做了简化但核心逻辑是完整的import requests import random import time def create_session(cookie_str: str) - requests.Session: session requests.Session() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: https://www.douyin.com/user/, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Cookie: cookie_str, } session.headers.update(headers) return session def random_sleep(min_sec: float 1.0, max_sec: float 3.0): 随机等待尽量模仿真实用户浏览节奏给服务器减轻压力 time.sleep(random.uniform(min_sec, max_sec))随机延时那段我特意写了注释。很多人觉得延时无所谓直接60秒间隔拉满结果还是被限制为什么因为你的访问模式太规律了。调成随机的、不规律的节奏配合合理的总体频率反而更稳妥。3.2 视频列表翻页与游标机制视频列表的翻页和普通的分页完全不一样它不是page1、page2这种方式而是游标cursor机制。你可以把游标理解成一个时间锚点接口返回的max_cursor表示“本次这批数据截至哪个位置”下次请求把这个值传回去服务端就从那个位置继续往更早的时间翻。第一次请求max_cursor0服务端返回最近的作品列表同时返回has_more字段值为1表示还有更多值为0表示翻到底了。核心代码如下def fetch_user_videos(session, sec_uid, max_cursor0, count18): api_url https://www.douyin.com/aweme/v1/web/aweme/post/ params { sec_user_id: sec_uid, max_cursor: max_cursor, count: count, device_platform: webapp, aid: 6383, } resp session.get(api_url, paramsparams, timeout15) resp.raise_for_status() data resp.json() return data翻页循环的终止条件要注意当接口返回的has_more不为1时就要停而不是靠列表长度判断。曾经有一次因为某个账号设置了仅半年可见has_more一直是1但列表始终是那几条差点造成死循环。后来我在循环里加了一个保护条件连续翻页超过限制次数比如20次或者游标没有变化就直接退出宁可少采几条也不能卡死整个任务。3.3 点赞、收藏、分享等数据字段的提取与映射接口返回的JSON字段名是短横线风格而且层级较深直接拿原始结构入库不是不行但后续写分析SQL会比较痛苦。我更倾向于做一次标准化映射把响应里的嵌套结构拍平成一张宽表。每个视频一行数据字段固定后面不管是写SQL还是做图表都轻松。以下是标准化的核心函数def parse_video_item(item: dict) - dict: stats item.get(statistics) or {} video_detail item.get(video) or {} play_addr_list (video_detail.get(play_addr) or {}).get(url_list) or [] return { aweme_id: item.get(aweme_id), desc: item.get(desc), create_time: item.get(create_time), digg_count: stats.get(digg_count, 0), comment_count: stats.get(comment_count, 0), collect_count: stats.get(collect_count, 0), share_count: stats.get(share_count, 0), play_count: stats.get(play_count, 0), video_url: play_addr_list[0] if play_addr_list else , }这里所有统计数据都用get加了默认值避免因为字段缺失直接崩溃。play_addr里存的是视频播放地址如果你要做视频的本地备份可以顺着这个地址下载。但是请注意下载的视频仅限个人备份、剪辑参考这类用途不要再分发出去版权问题不是开玩笑的。拍平后的数据直接存入MySQL更新策略是ON DUPLICATE KEY UPDATE。用aweme_id做唯一索引每次全量跑完已经存在的记录就更新最新统计数据不存在的记录就插入天然的幂等操作。3.4 并发采集与限速策略怎么设计讲到并发很多人第一反应是“开100个线程直接梭哈”这绝对是个灾难性的想法。我在项目里验证过抖音对并发请求非常敏感单个IP在短时间内高频请求触发验证的概率会指数级上升。那“并发设计到底哪个好”我给出的结论是小规模采集用单线程游标顺序翻页最好中等规模几百个账号用信号量控制并发把并发数压在3到5之间大规模分布式采集才需要上多节点任务队列那已经超出普通项目的范畴了。篇幅有限我挑中等规模并发方案说说核心思路import asyncio import aiohttp SEMAPHORE_LIMIT 3 async def fetch_one(session, params, semaphore): async with semaphore: async with session.get(api_url, paramsparams) as resp: return await resp.json() async def main(): semaphore asyncio.Semaphore(SEMAPHORE_LIMIT) connector aiohttp.TCPConnector(limitSEMAPHORE_LIMIT, limit_per_hostSEMAPHORE_LIMIT) async with aiohttp.ClientSession(connectorconnector) as session: tasks [fetch_one(session, params, semaphore) for params in params_list] results await asyncio.gather(*tasks, return_exceptionsTrue)这个方案的精髓在于Semaphore它像一道闸门不管外部塞进来多少个协程同一时刻只有3个请求在飞多出来的全部排队等待。配合TCPConnector的连接数限制双保险。实际跑下来3并发配合1到2秒随机延时一个账号几百个视频大概几分钟就能采完数据稳定性和速度都能兼顾。4. 实战中的常见问题与排查记录4.1 接口返回空数据或状态码异常我在项目调试阶段遇到过最头疼的情况接口返回的HTTP状态码是200JSON也能正常解析但aweme_list是空的status_code却是0。这种情况通常不是签名问题而是请求参数里的sec_user_id对应的用户设置了隐私保护或者该用户已经被重置为空号。排查时不要死磕代码先手动打开主页看一眼确认账号本身没问题再去查参数。还有一类情况是接口直接返回验证码页面或者状态码462、461这类异常值。这就不是业务逻辑能解决的了必须停下来检查访问频率。我的处理方式是把该账号标记为“待重试”塞回队列尾部等一段时间后换个时间段再跑而不是在同一时间点反复重试。4.2 数据不一致为什么接口里的点赞数和APP里不一样这个坑特别多人遇到拿接口采回来的点赞数和手机APP里刷到的点赞数对比发现对不上于是怀疑自己代码写错了。其实绝大多数情况下不是你代码的问题而是平台侧的原因。点赞数、评论数这类指标对用户端展示的是经过缓存、异步计算后的结果。同一个视频你上午看到的点赞数是1万下午接口返回的可能是9998或者10102这是平台根据清洗、去重、防刷策略动态调整的结果本身就是一个波动值。另外Web端和APP端的统计口径也可能有细微差异。正确处理方式是把采集数据当作快照来用。我在数据库表里设计了updated_at时间戳字段每次采集记录更新时间。做数据分析时只做趋势判断不做绝对值PK比如“上周这条视频点赞涨了5000”比“现在这条视频点赞是5000”更有分析价值。4.3 爬虫被风控拦截时怎么办风控拦截是有层次的。最轻的表现是部分请求返回空数据严重一点是请求直接302到验证码页面再严重就是接口彻底拒绝访问。遇到这种情况首先要停而不是继续硬冲。很多人的第一反应是换IP、换UA这治标不治本。正确的处理顺序是先降低频率把并发从3降到1把随机延时拉长到5秒以上然后错峰不要赶在热门时段去采集最后实在不行给任务加一个时间窗口比如每天只采一轮分几天把历史数据补齐。还有一个容易被忽视的点是请求顺序。如果你在全站乱跳着请求一会儿用户A、一会儿用户B、再跳回用户A这种毫无规律的跳转模式本身就很可疑。我的习惯是把同一用户的请求在短时间内聚拢按用户维度串行用户之间再切换整体节奏更接近真人浏览。4.4 数据入库与导出的编码问题抖音视频文案里大量的emoji表情、特殊符号、换行符是数据采集中最常见的坑。MySQL表如果用的是utf8字符集遇到emoji会直接报错因为emoji需要utf8mb4才能存储。解决办法是在建表时明确指定CHARSETutf8mb4同时连接串里也要加上charsetutf8mb4两边都对齐才会生效。还有一个我踩过坑的实际问题采集完数据导出Excel时粘贴到表格里经常出现错行、乱码。原因是文案里的换行符和逗号污染了CSV格式。如果你只是内部看数据最省心的方法是直接用pandas写成xlsx而不是CSV如果一定要粘贴到网页表格里最好在导出前把换行符替换成空格把逗号转义掉。这里我做了一个常见问题速查表基本覆盖了项目中遇到的典型问题现象可能原因解决办法接口返回正常但列表为空用户隐私设置/账号异常手动打开主页核实请求被302到验证页面单IP请求过密降低并发、增加随机延时、错峰执行MySQL入库报字符错误表字符集不是utf8mb4重建表指定utf8mb4KEY异常导致程序崩溃字段缺失统一用get加默认值点赞数和APP对不上缓存/统计口径差异当作快照处理关注趋势翻页一直不结束has_more和游标判断异常增加翻页次数保护和游标变化检查5. 数据拿到手之后清洗、存储与应用5.1 数据模型与存储设计我在项目里建了一张视频主表和一张采集日志表。视频主表存储拍平成宽表后的视频数据核心字段就是前面提到的那些外加created_at和updated_at两个时间戳。索引方面以aweme_id做唯一索引sec_uid和create_time各建一个普通索引这样按用户查、按时间范围筛选都很高效。采集日志表记录每次采集任务的执行情况哪个用户、从哪个游标开始、采了多少条新数据、是否完整结束。这张表的价值在于断点续采。如果任务跑了一半因为网络原因挂了重启后只要看日志表里该用户最后的进度直接从中断的游标继续不用从头开始省掉大量重复请求。这其实就是网上很多人说的“断点续采”的思路实际落地时没有想象中复杂核心就是把状态记录下来而不是每次都从0开始。5.2 数据质量校验与增量更新数据采完之后我习惯做三道质量检查。第一道检查是数量层面用户主页显示的视频数和库里该用户的总行数是否基本一致差太多说明有漏采。第二道是内容层面随机抽几条视频到APP里核对文案、点赞数是否在合理范围内。第三道是时间连续性按create_time排序看看相邻两条视频的时间间隔是否异常有异常跳跃说明中间可能有视频被漏掉了。增量更新的策略是每天定时跑一次采集任务对已有视频更新统计数据对新视频做插入对已删除的视频做标记。这样日积月累你就有了一个完整的、带历史趋势的数据资产。比如发现某个大号最近一个月点赞量一直下滑哪怕他没有发新视频仅靠历史视频的复盘分析就能看出很多信号。5.3 基于这些数据能做什么分析数据拿到手只是第一步真正的价值在后续分析。我目前已经接了几个实际场景一个是商品带货分析通过历史视频的点赞、收藏、分享数据反推哪类内容更容易激发用户互动为投放选品提供依据另一个是竞品账号监控每天对比竞品账号的粉丝增速和视频互动率提前发现内容的潜在爆款趋势。如果你有编程基础还可以把这些数据接到可视化看板上按时间维度画点赞趋势曲线按视频类型做互动率对比。采集是苦活累活但把数据用起来的那一刻你会觉得前面所有为字段缺失抓狂、为编码问题折腾的时间都值了。最后再分享一个小技巧。采集任务最好固定到本地定时任务里跑比如每天凌晨两点这时候值班的人少、接口压力小采集任务最稳定。跑之前检查一下前一天的任务日志确认没有异常积压再启动新任务这样一旦出问题排查范围会小很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →