ARTICLE DETAIL

资讯详情

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

某手关键词搜索视频列表采集实战:签名逆向与风控兼容策略

某手关键词搜索视频列表采集实战:签名逆向与风控兼容策略 说实话我第一次把“某手关键词搜索视频列表”这几个字组合成一个需求时心里是有点犯嘀咕的。网上能查到的教程十个里有八个停在“打开Fiddler观察几个HTTP请求然后把URL复制下来丢到Postman里跑一下”的阶段好像只要发一个GET请求就能把视频列表全拿到手一样。等你把关键词、页码、排序一填真正跑起来就会撞上三堵墙签名校验第一道搜索结果不精准第二道请求频率稍微上去就被风控盯上第三道。这篇文章我想把这几年做接口采集实战攒下的经验拆开讲围绕某手搜索视频列表这条主线把签名分析、精准搜索参数调整、以及风控兼容的工程化处理一次性说清楚。适合已经会抓包、看得懂JSON、对逆向有一定概念的开发者如果你只是想找一个现成能用的采集脚本本文给的是框架思路和排查路径不建议直接拿去怼生产环境。1. 搭建“搜索链路”分析环境从抓包到定位真实视频列表接口1.1 为什么客户端请求和网页请求必须分开抓很多人在第一步就走偏了直接拿浏览器开发者工具抓网页版搜索接口抓到什么就分析什么。网页版接口确实简单参数透明、签名规则相对好找但它和手机App返回的数据结构、字段完整度、视频列表排序逻辑经常不一致。我自己实测下来同一批关键词网页版接口返回的视频列表会比App端少一部分内容排序权重也略有差异。核心原因是某手对Web端和App端走的是两套召回策略Web端偏向收录率高的头部内容App端则更贴近个人兴趣分发。所以如果你的业务目标是“尽可能还原用户在App里看到的结果”就必须把App端的请求抓下来分析。我常用的组合是Windows/macOS 上跑一个Charles或mitmproxy做中间人代理安卓模拟器里装对应版本的某手App配置代理指向电脑手机端安装并信任抓包工具的CA证书保证HTTPS流量可解。信任证书这一步没什么技术门槛但经常有人卡住。注意现在App很多都会做证书校验你直接抓大概率只能看到CONNECT请求看不到具体报文。遇到这种情况优先做两件事一是检查App是否用了系统证书存储之外的私有证书二是考虑用Frida或者LSPosed这类Hook框架去绕过证书绑定。这个属于逆向基本功我后面专门说。1.2 从一次完整搜索动作中锁定核心请求与关键Header环境就绪后在App里手动操作一次搜索输入关键词后滑动列表加载两三页然后回到抓包工具里筛选。你会在流量列表里看到一堆域名真正干活的请求往往长这样POST /rest/o/search/video?keywordxxxpage1count20路径里的字段可能有变化不同版本会加版本号、频道ID之类的参数但核心路径一定能看出“search”相关字样。这时候不要急着复制URL你要做的是把那次搜索请求的Header完整抄下来。值得记录的关键Header包括User-AgentApp版本、系统版本、机型都会影响返回内容Cookie里面有匿名身份标识后面风控部分会细讲did或类似设备ID字段一组名称带sig、token、verify字样的加密签名段Content-Type和实际Body的编码方式很多教程让你复制URL直接用忽略Header这是个大坑。某手的服务端校验强度不低至少会校验User-Agent和签名参数是否匹配你把Postman默认的UA一换签名直接失效。先别急着关抓包工具把下面几项信息也一并存下来请求体的原始字节流有些版本用Protobuf或MessagePack压缩光看文本是乱码请求时间戳和App本地时间戳的差值搜索结果为空、断网、切换关键词等边界场景下的请求差异这些数据后面分析签名和风控都用得上。我一般在这一步就把所有信息归档成一份Markdown笔记防止分析到一半回去翻抓包记录时忘了上下文。1.3 返回结构拆解视频列表里到底哪些字段能信某手搜索接口的返回结构通常是一个大的JSON最外层可能有result、feeds、searchResult这类字段但真实内容往往嵌套在多层对象里。我拿一个实际返回结构做过简化核心信息大概是这样的{ status: 200, content: { feeds: [ { photoId: 123456789, type: 1, authorName: 某创作者, caption: 视频标题内容, timestamp: 1735689600, likeCount: 1234, playCount: 5678, duration: 45, coverUrl: http://... } ], pcursor: xxx, hasMore: true } }有几个字段需要特别说明。photoId是视频的唯一ID去重和增量更新全靠它务必保存为字符串类型不要用整数解析因为超过JavaScript安全整数范围会丢精度。timestamp是发布时间但不同端可能返回毫秒或秒解析前先看长度是10位还是13位。playCount这类计数指标在不同接口里有延迟搜索接口里的数值通常是缓存结果不要把它当成实时的精确播放量。嵌套解析时还有一个坑同一条视频可能出现在多个关键词的搜索结果里也会在接口中返回不同形式的字段结构。比如有的接口会把作者信息单独放到author子对象里有的版本则直接平铺在顶层。最稳妥的办法是写一层适配器把不同版本返回的字段映射成统一结构而不是直接把某次抓包看到的JSON结构焊死在代码里。2. 签名机制逆向从“看不懂的字符串”到“能复现的签名算法”2.1 某手签名的整体思路参数排序、盐值拼接、摘要计算三步走某手接口的签名机制听起来很神秘拆开来看核心思路其实不复杂服务端和客户端约定一个算法客户端把请求参数和某些额外信息按固定规则拼接用摘要算法生成一个定长字符串放在请求头或请求体里。服务端收到请求后用相同的规则重新计算签名一致就放行不一致就拒绝。常见的拼接模式是选取请求中参与签名的参数通常是设备ID、时间戳、随机数、请求路径、部分业务字段对参数名按ASCII码升序排序然后按keyvalue的方式拼接成字符串在字符串头部或尾部加上固定盐值一个写死在App里的字符串用MD5、SHA-1、SHA-256或HMAC算法计算摘要最后可能再做一次Base64编码或十六进制转换。这个模式不是某手独有的很多国内大厂的接口都走类似路子。但你直接复制网上任何一篇“MD5加盐”教程去套十有八九失败因为细节决定了成败排序用的是全参还是部分参数、加盐加在开头还是结尾、是否要剔除空值、摘要之前要不要做URL编码差一个字符结果就完全不同。2.2 从JS脚本到原生SO库我定位签名函数的三步路径定位签名函数的路径取决于你抓的是Web端还是App端。Web端相对简单直接在浏览器开发者工具里搜索关键词比如sig、sign、token、Security这些字段名。实际逆向时我更推荐用断点法在Network面板里找到搜索接口右键选择“XHR/fetch断点”然后重新触发搜索。请求发出去之前代码会停在XHR调用的地方这时候往前回溯调用栈就能找到拼接参数和计算签名的函数位置。App端就麻烦不少。如果App是Flutter或React Native写的逻辑可能在Dart或JavaScript层还有机会通过Hook运行时函数来定位如果是原生实现签名函数通常编译进SO文件常见文件名是libcore.so、libkwsg.so、libcms.so之类的。定位思路有两条用Frida去Hook常见哈希函数比如MD5_Init、SHA256_Init、HMAC看哪个函数被调用了、入参是什么从抓包参数名反向搜索AndroidManifest或Java层代码找到生成签名的入口再往下追踪JNI调用。我实际用下来最顺手的流程是先用Frida脚本Hook住java.security.MessageDigest和javax.crypto.Mac这两个Java层类。某手App虽然有大量逻辑在SO里但不少版本还是会借助Java层的加密API做摘要计算。只要Hook到入参就能看到它到底拼了哪些字符串。如果So层自己实现了哈希算法你的Frida脚本就得转到Native层去Hook动态符号表里的导出函数这一步对不太熟悉Native逆向的开发者来说耗时最多。2.3 复现签名逻辑时最容易踩的三个坑当你定位到签名函数后要在外部代码里复现它真正的噩梦才刚刚开始。我把自己踩过的坑归纳成三类第一类是字符编码陷阱。签名拼接前参数值要做URL编码但有些版本的App用的是application/x-www-form-urlencoded的RFC标准有些自定义了一套规则比如空格编码成%20还是。中文关键词在签名前到底按UTF-8处理后直接拼接还是先做URI编码再拼接这是最容易翻车的地方。我的经验是抓包拿到一整条签名请求后把请求体复制出来尝试几种编码组合逐一比对最后的摘要结果哪个能对上就用哪个。第二类是时间戳自校验。很多版本签名会带上cTime字段发了什么时间戳签名就基于什么时间戳计算。你在外部复现时如果取的是本地当前时间但服务器判断时间差超过阈值就算签名格式正确也会被判无效。所以外部请求里必须显式生成一个时间戳既放进签名计算也放进请求体保持同一来源不要用两个时间来源。第三类是参数顺序和排除规则。并不是所有请求参数都参与签名有些字段是业务侧自定义的服务端不会校验但参与签名的字段一旦多了自己不认识的业务参数就会被拒。具体哪些参与、哪些不参与我的办法是逐一做“参数删除实验”删一个字段看签名是否还生效通过二分法缩小校验范围。这个过程枯燥但对理解边界很重要。3. 精准搜索策略从“搜得出来”到“搜得准、搜得全”3.1 排序规则与游标翻页别再用pagesize的思维写死逻辑某手搜索列表的翻页和传统分页接口有个很大区别它经常不是靠page1count20往下翻而是通过一个游标字段常见名字是pcursor来翻。意思是请求第一页后服务端把当前页面快照对应的位置标记返回给你第二次请求时把这个标记原样传回服务端就知道该从哪个位置继续给数据。实际表现为第一次请求pcursor返回体里带着pcursorxxx和hasMoretrue第二次请求把pcursorxxx带上再拿下一批。如果你机械地让page2、page3递增可能反复拿到同一批数据也可能直接越权被限制。另外有些接口还会限制最大翻页深度比如超过50页就强制你重新初始化游标这时候要在业务层做好“起点重置”策略。排序规则方面搜索接口通常有几种典型模式综合排序默认结合热度、相关性、时效性加权按发布时间排序简单的倒序排列按热度排序播放、点赞、评论的复合指标同一个关键词不同排序模式下返回的结果集合可能完全不同而且它们的时间戳字段含义也有差异。做监控类业务时我建议至少跑“综合”和“最新”两种排序再在应用层合并去重才能保证覆盖率。3.2 筛选维度时间范围、作品类型、地域字段等可控参数很多人以为搜索接口只能传一个keyword和一个page其实某手搜索请求里还有不少筛选项只是版本不同参数名各异。我常用到的几个维度用途典型字段示例说明发布时间startTime/endTime通常接受Unix时间戳不是字符串日期作品类型contentType/feedType可区分视频、图文、直播回放等时长范围durationRange某些版本支持按短视频/中长视频分段地域信息lat/lon/cityCode城市参数会影响本地化内容的召回排序方式sortType/order和排序维度对应不同字段名要试地域字段对搜索结果的影响特别容易被忽略。我试过一个冷门关键词手机位置在北京时能搜到一批本地探店视频把经纬度改到成都后结果列表里出现了完全不同的一批内容。如果你的数据需求是全国级别的建议不要固定一个城市的参数而是设计一套城市列表轮询策略否则搜索结果天然有地域偏差。3.3 搜索任务的工程化配置关键词库、去重、增量更新把搜索接口调通只是第一步真正有价值的是把它转成一条能长期稳定运行的数据管道。我通常会把任务拆成四层第一层关键词调度器。管理一个关键词队列支持配置优先级、调度频率、运行时间段。比如每天凌晨跑全量关键词白天只跑高优先级关键词避免白天高并发触发风控。第二层请求与签名层。统一封装请求逻辑接收参数、生成签名、发送请求、解析响应。这一层必须支持代理策略切换和签名缓存避免每次请求都重复计算签名导致性能浪费。第三层去重与存储层。以photoId为主键做唯一约束维护一个“最近已处理ID集合”。增量策略上我建议用Redis的Set存储过去7天的视频ID搜索到的新视频先判断是否在Set里在的话跳过不在的话入库并更新Set。第四层异常与告警层。每次请求记录响应耗时、状态码、返回体摘要出现连续失败或风控标志时自动触发降级。下面是我写的一个通用请求函数骨架实际使用时按官方返回结构调整字段名即可import time import hashlib import requests class KsSearchClient: def __init__(self, base_params, salt, timeout10): self.base_params base_params self.salt salt self.timeout timeout def _sign(self, params: dict) - str: # 参与签名的字段按key排序拼成字符串加盐做摘要 sorted_keys sorted(params.keys()) raw .join(f{k}{params[k]} for k in sorted_keys) raw self.salt # 根据实际逆向结果选择MD5或SHA256 return hashlib.md5(raw.encode(utf-8)).hexdigest() def search_videos(self, keyword: str, pcursor: str , extra: dict None): params dict(self.base_params) params[keyword] keyword params[pcursor] pcursor params[cTime] int(time.time()) if extra: params.update(extra) params[sig] self._sign(params) resp requests.post( https://api.example.com/rest/o/search/video, dataparams, headers{User-Agent: self.base_params.get(ua, )}, timeoutself.timeout, ) return resp.json()这段代码是简化的示例真实的签名参数集合和盐值需要根据逆向结果填充。写这段是为了说明架构思路签名逻辑、请求逻辑、解析逻辑一定要分层这样后面替换算法时不会牵一发动全身。4. 风控兼容怎么做不硬刚靠“拟人化工程”降低撞墙概率4.1 风控在防什么从频率、设备指纹、行为序列三个角度理解很多人一看到“风控”两个字就头大总想着怎么绕过结果越绕越严。我的观点是先把风控当成一个常规系统来理解再设计对应的工程策略。某手这种体量的平台风控一般会从三个维度做检测频率维度同一设备、同一IP在单位时间内的请求次数、失败率、速率波动设备维度设备ID、UA、系统参数是否真实、是否大量复用行为维度操作间隔是否均匀得像定时器、访问路径是否符合人类习惯、是否频繁切换搜索关键词等。这三个维度组合在一起形成一套风险评分。评分低就正常放行评分中等可能弹验证码评分高就直接拒绝或静默返回异常数据。理解了这套逻辑你就明白为什么网上那些“把并发开到50、sleep固定2秒”的建议完全是在自爆了。4.2 设备身份与Cookie生命周期的处理某手搜索接口虽然不强制登录但设备身份和Cookie几乎是必查项。正常App首次启动后会通过注册接口向服务端申请一个匿名设备ID后续请求都带着这个身份。你在外部模拟请求时如果直接忽略设备ID服务端会把你识别成一个“无身份客户端”很多功能直接不可用。工程化的做法是在模拟器里正常安装App并打开获取设备注册后的匿名ID把这个ID和对应的Cookie捆成一整套“身份档案”持久化存储请求时复用这一整套档案不要只取其中一个字段定期用App手动划一划、搜一搜保持这个身份活跃度。我还发现长时间不使用后再拿一个老设备ID去请求风控概率反而升高可能是服务端认为“设备休眠了很久突然活跃”异常。所以设备身份池要做好轮换常备几个不同“生命周期”阶段的身份档案新老交替使用。4.3 请求调度并发控制、随机延时、任务时间窗请求调度是整个风控兼容方案中最值得花时间的部分。我的经验值可以给一个参考单设备身份 单出口IP跑搜索接口控制在一分钟不超过10次请求单次任务连续运行不超过30分钟。这个数字没有统一标准不同版本、不同网络环境差异很大但起步宁慢勿快。随机延时不能是固定sleep更不能是固定范围内的均匀随机因为均匀随机一样能被聚类识别。可以参考人际操作习惯先快速看几个视频、停顿几秒、再翻页搜索。我通常是请求之间随机sleep 2到5秒每完成10次请求后追加一次10到30秒的长停顿模拟人在中途停下来看视频的行为。任务时间窗也很重要。某个关键词如果设置成全天每分钟跑一次那是典型的机器行为。我建议把批量任务集中在凌晨低峰时段执行白天最多做少量增量查询。关键词的调度频率要结合业务紧急度分级不要一刀切设置成同样的间隔。4.4 风控信号的识别与降级预案最后一部分也是很多人最缺的就是怎么判断自己被风控了。某手很少直接给你返回403或封禁提示它更擅长“静默降权”给你返回HTTP 200和看似正常的JSON但里面的搜索结果被替换成热门视频或者列表明显变短。因此必须有自检机制。我常用的风控信号识别规则有这些连续3次以上请求返回相同的一批photoId且与关键词无相关性返回体中出现了验证码相关的字段或特殊的status码接口响应时间突然变短明显没走真实搜索链路返回结果里出现引导登录、打开通知权限等非搜索内容。一旦检测到风险信号最忌讳的就是继续重试因为风控系统对“撞墙后持续重试”非常敏感。我的降级预案分三级一级降级停止当前任务切换设备身份降低频率到原间隔的3倍以上二级降级暂停半小时切换到备用网络出口只保留主动刷新任务三级降级停止所有采集行为改为人工打开App手动搜索验证24小时后再恢复。这套逻辑不一定能保证永不触发风控但至少能避免从小问题升级成封号级别的风险。5. 实战中的暗坑与排错记录几个让我调了一整夜的细节5.1 时间戳偏差导致的“签名一直无效”问题有个项目上线后的第一个晚上签名逻辑明明在测试环境跑得好好的一上生产就频繁报“签名错误”。排查到最后才发现生产服务器的系统时间比标准时间慢了40多秒而签名的cTime用的是服务器本地时间。服务端校验签名时拿请求里的cTime和自身时间做差值超过阈值直接拒绝。解决方案很简单所有请求时间戳统一从一台NTP时间源获取不要在业务代码里直接读time.time()。如果公司内网环境访问不了外网NTP至少要在部署脚本里做一次系统时间校准和定期同步。这类问题一旦发生表面看是签名算法问题实际是基础设施问题最容易让人钻牛角尖。5.2 搜索结果为空不一定是没数据可能是地域参数被锁有一次我用某个城市参数跑测试所有关键词都返回空列表。我第一反应是关键词太冷门换了一批热词还是空。最后对比发现是城市参数和请求里另一个字段冲突了。某手搜索接口在识别到设备地域信息和业务参数不一致时会选择“不给结果”而不是报错这是一个保护策略。排查思路是遇到空结果时先把城市参数、经纬度参数全部置空试一次再逐步添加。如果置空后能返回数据基本可以断定是地域相关参数问题。另外还要注意不同版本可能对城市参数的字段名有改动不要因为一次验证通过就以为是万能参数。5.3 不同端返回结构与字段语义差异我在1.1里提过Web端和App端的返回结构有差异实际操作中这个差异会带来很隐蔽的bug。比如同样表示发布时间Web端返回的是秒级时间戳App端可能返回毫秒级字符串同样表示播放量Web端命名为viewCountApp端则可能嵌套在stat对象里。为了避免这类问题我建议在解析层专门写一个跨端适配模块把所有接口映射成统一的数据模型。同时做数据对比时不要想当然地认为两个端返回同一个photoId的内容完全一致至少要验证标题、作者、点赞数这几个核心字段再决定能不能合并。5.4 特殊符号编码引起的“结果缺失”中文关键词里经常包含#、空格、这类特殊字符。URL编码规则不对时服务端接口可能不会报错但会把关键词截断导致返回的数据严重不完整。比如搜索“美食#探店”如果#没有被编码服务端可能只按“美食”去搜索结果看似正常实则有偏差。解决方案是在拼接请求参数时统一使用urllib.parse.urlencode这类标准库做编码并在签名时保证编码后的字符串参与计算。还要注意编码前后的顺序先编码还是先拼签名不同接口逻辑不同一定要在逆向阶段验证清楚。写在最后的几点实操心得回头再看这套方案最核心的体会是接口逆向不是一次性的“破解动作”而是一个持续维护的工程系统。签名算法会变、参数名会变、风控策略也会变所以一开始就要把代码结构、配置管理、监控告警都做好别指望写一次脚本就能一劳永逸。从投入产出比角度讲我的建议是控制好第一轮精力的投放。先把一个小关键词池完整跑通用一周时间观察风控信号稳定之后再逐步扩容。这个过程中记录每一次参数变更、每一个异常码的含义日积月累会形成一本非常值钱的笔记。最后再分享一个小技巧某手搜索接口的很多算法细节不同版本之间存在差异但构造请求的基础逻辑是相通的。如果你能把这套分析流程跑通一遍以后再去研究其他平台的搜索接口时会发现自己已经有了一套可靠的排查方法论这比任何一个具体的签名算法都更值钱。但请始终记住采集数据的目的是做分析研究要遵守平台规则不要超出正常使用边界去大量抓取他人隐私信息也不要对你的目标平台造成不必要的访问压力。
返回列表