Python还原小红书签名算法:从JS逆向到稳定采集
简介这是一份面向Python爬虫与逆向工程学习者的实战型资源聚焦小红书平台数据采集与算法还原适合具备一定Python基础、希望深入理解前端加密与接口模拟的中高级开发者。资源包共29个文件以11个py源码文件为核心辅以yml、txt、md、rst等配置与文档以及Makefile、bat等构建脚本整体约28KB结构紧凑、便于快速上手。项目围绕x-s、x-common等关键参数的Python还原展开封装了GET、POST等请求类型与小红书API接口并涉及JavaScript逆向解密思路帮助读者理解从请求构造到数据解析的完整链路。目录中core.py、help.py、exception.py等模块划分清晰tests与docs提供了测试用例与使用说明requirements.txt与setup.py则方便环境复现。目前已有4451人学习下载对于想研究社交媒体数据挖掘、提升爬虫性能与逆向分析能力的开发者而言是一份可直接参考的算法还原范例。1. 从一次翻车说起为什么我要把小红书采集的算法还原讲透去年帮一个做内容分析的朋友搭数据管道目标很明确按关键词抓小红书的笔记列表和详情做选题趋势。我一开始想得很简单requests 加个 UA 就完事结果第一轮跑下来返回的全是空壳页面字段一个都拿不到。换成 Selenium 渲染能出内容了但速度慢到离谱一百条笔记要跑好几分钟而且跑着跑着就弹验证。那次是真翻车血泪经验就是一句话小红书的接口不是靠请求头就能糊弄过去的它有一层前端 JavaScript 算出来的签名参数你不把这层算法还原出来采集就是玄学。这篇要讲的就是这件事——用 Python 把小红书 Web 端那套请求签名算法还原出来让采集从能跑变成稳定可控。适合两类人一类是刚接触 JavaScript 逆向、想找一个真实站点练手的新手照着思路能一步步复现另一类是已经在做采集、但被签名参数卡住的熟手能看到我判断参数来源、定位加密入口的底层逻辑。我不会给你一个复制粘贴就能用的脚本那种东西活不过一周我要给的是能自己迭代的方法。2. 先搞清楚要还原什么签名参数的定位与判断依据2.1 从请求里反推哪些参数是算出来的采集的第一步不是写代码是打开浏览器开发者工具把一次真实的笔记列表请求完整看一遍。你会看到 URL 上挂着一串 query 参数其中大部分是业务参数比如关键词、分页游标、页码这些是你自己能控制的。但总有那么一两个参数长得像一坨无规律的十六进制字符串长度固定每次请求都不一样而且你手动改一个字符服务端立刻返回签名错误。我一般会这么判断把同一个请求重放两次对比哪些参数变了。业务参数不变变的那个就是签名。小红书 Web 端的签名通常挂在请求头里名字类似x-s、x-t这种配合一个时间戳参数一起用。判断依据有三条一是它随请求变化二是它和请求体、时间戳存在关联三是删掉它服务端直接拒绝。三条都满足基本可以锁定它就是你要还原的目标。这里有个容易踩的坑很多人一上来就盯着 URL 参数找忽略了请求头。签名参数放请求头是更常见的做法因为放 URL 里容易被日志和代理记录下来。所以定位的时候请求头和 query 都要过一遍别漏。2.2 定位加密入口断点怎么下才不白费功夫锁定参数之后下一步是找到它在哪段 JS 里被生成。最笨也最有效的办法是全局搜索参数名比如搜x-s看它在哪些文件里出现。但小红书的前端代码是打包压缩过的变量名全是a、b、c直接搜参数名往往只能搜到赋值的地方搜不到计算逻辑。这时候要用 XHR/fetch 断点。在开发者工具的 Sources 面板里对发起请求的那个接口下断点然后触发一次请求代码会停在发请求的那一行。顺着调用栈往上翻一层层看参数是从哪个变量传进来的一直翻到它第一次被赋值的地方那里就是加密入口。这个过程需要耐心调用栈可能有七八层中间还夹着各种工具函数。我的经验是重点看那些参数名有意义的函数比如带sign、encrypt、token字样的压缩代码里函数名可能被改但字符串常量改不了搜字符串往往比搜函数名管用。提示断点调试时把 Pause on exceptions 打开有时候加密逻辑抛异常会暴露关键位置比一层层翻调用栈快。2.3 判断算法类型对称、非对称还是自定义混淆找到加密入口后要看它用的是什么算法。常见的就几类MD5、SHA 系列这类哈希AES、DES 这类对称加密RSA 这类非对称还有就是纯自定义的字符串拼接加混淆。判断方法很简单看代码里有没有明显的常量比如 MD5 的初始向量、AES 的 S 盒、RSA 的公钥字符串。如果看到一长串十六进制常量大概率是标准算法如果全是位运算和字符串拼接那就是自定义混淆。小红书这类站点的签名通常是哈希 自定义拼接的组合。它会把你请求里的关键字段按固定顺序拼成一个字符串加一个盐值再做一次哈希最后截取或编码。盐值往往硬编码在 JS 里是个固定字符串。你要做的就是把这个拼接顺序、盐值、哈希方式全部还原出来。这一步是整个逆向的核心也是最容易出错的地方——拼接顺序错一个字段结果就完全对不上。3. 用 Python 还原算法从 JS 逻辑到可运行代码3.1 把 JS 加密逻辑翻译成 Python假设你已经定位到加密函数逻辑大致是这样取请求方法、路径、时间戳、请求体按method path timestamp body salt的顺序拼接做一次哈希再和 timestamp 拼成最终签名。翻译成 Python 就是几行代码的事但细节决定成败。import hashlib import time def build_signature(method, path, body, salt): # 时间戳用毫秒很多站点签名里带时间戳做防重放 timestamp str(int(time.time() * 1000)) # 拼接顺序必须和 JS 里完全一致错一个字段结果就废 raw method path timestamp body salt # 哈希方式要和 JS 对齐注意 JS 里可能是 MD5 也可能是 SHA256 sign hashlib.md5(raw.encode(utf-8)).hexdigest() return sign, timestamp这段代码的关键不在语法在三个对齐点拼接顺序、时间戳精度、哈希方式。拼接顺序要严格照抄 JS包括字段之间有没有分隔符、大小写是否敏感。时间戳精度要对JS 里Date.now()是毫秒Python 的time.time()是秒必须乘 1000。哈希方式要对JS 里常见的CryptoJS.MD5对应 Python 的hashlib.md5但要注意 JS 的 MD5 输出默认是小写十六进制Python 的hexdigest()也是小写这点一致。参数说明method是大写请求方法path是不含域名的路径body是请求体的原始字符串注意是原始字符串不是字典序列化后的顺序要和实际发送的一致salt是从 JS 里抠出来的固定盐值。这四个参数任何一个不对签名都过不了。3.2 处理 JS 特有的编码和位运算Python 和 JavaScript 在字符串编码、位运算上有不少差异这是还原时最容易翻车的地方。JS 的字符串是 UTF-16Python 3 的字符串是 Unicode编码成 bytes 时默认 UTF-8大部分情况一致但遇到中文和特殊字符时可能出问题。如果签名里包含中文关键词要确认 JS 那边用的是encodeURIComponent还是别的编码方式Python 这边对应urllib.parse.quote。位运算的坑更大。JS 的位运算默认按 32 位有符号整数处理Python 的整数是任意精度直接照搬会出错。比如 JS 里a 2溢出后会截断Python 不会。遇到这种情况要在 Python 里手动做 32 位截断def to_int32(x): # 模拟 JS 的 32 位有符号整数截断 x x 0xFFFFFFFF if x 0x80000000: x - 0x100000000 return x这个函数在还原带位运算的混淆算法时几乎必用。判断依据是如果 JS 代码里出现了、、、、|、^这些运算符且操作数可能超过 32 位范围就要考虑截断问题。我一般会先按 Python 原生算一遍和 JS 结果对比对不上再逐个加截断这样定位最快。3.3 用 Python 调用 JS什么时候该偷懒有些站点的加密逻辑极其复杂几百行混淆代码硬翻译成 Python 要花好几天还容易出错。这时候可以考虑用 Python 直接调用 JS 引擎比如PyExecJS或者mini-racer把抠出来的 JS 代码丢进去执行拿返回值。import execjs # 把从站点抠出来的加密函数保存成 sign.js with open(sign.js, r, encodingutf-8) as f: js_code f.read() ctx execjs.compile(js_code) # 调用 JS 里的加密函数参数按 JS 函数签名传 sign ctx.call(getSign, method, path, timestamp, body)这种方式的优点是快抠出来就能用不用理解每一行逻辑。缺点是依赖 JS 运行环境部署时多一层依赖而且如果 JS 里有浏览器特有的对象比如window、document、navigator直接跑会报错需要补环境。补环境是个技术活要 mock 掉这些对象让 JS 以为自己跑在浏览器里。我的建议是算法简单就翻译成 Python一劳永逸算法复杂且短期要用就先调 JS 顶上后面再慢慢翻译。注意补环境时navigator.userAgent、window.location这些是最常被检测的mock 的值要和真实浏览器一致否则签名算出来也是错的。4. 采集链路的稳定性签名之外的工程问题4.1 请求频率与验证码的应对签名还原出来只是第一步能稳定跑起来是另一回事。小红书的服务端有风控请求频率过高会触发验证码返回一个需要滑动的页面。这时候光有签名没用得处理验证码。常见的思路是降低频率加随机延时模拟真人操作节奏。我一般会把请求间隔设在 2 到 5 秒之间随机单账号每小时请求数控制在合理范围。如果还是触发验证码就要考虑接入打码平台或者自己训练识别模型。打码平台是花钱买时间识别模型是花时间省钱看你的场景。对于采集量不大的场景降频加延时基本够用量大就得做账号池和 IP 池把请求分散开。这里不展开账号池的细节那是另一个话题但你要知道签名只是入场券风控才是持久战。4.2 数据解析与字段提取拿到响应之后是解析。小红书的接口返回是 JSON结构比较深笔记列表在data.items下面每条笔记有note_card字段里面是标题、作者、点赞数这些。解析的时候要注意字段可能缺失比如有的笔记没有标签有的没有地理位置直接按固定路径取会 KeyError。稳妥的做法是用.get()逐层取或者写个安全取值函数。def safe_get(data, path, defaultNone): # 按点分路径安全取值避免中间层缺失导致报错 keys path.split(.) for key in keys: if isinstance(data, dict): data data.get(key) elif isinstance(data, list) and key.isdigit(): idx int(key) data data[idx] if idx len(data) else None else: return default if data is None: return default return data title safe_get(item, note_card.display_title, )这个函数看着简单但在实际采集里能省掉大量异常处理。判断依据是只要接口返回结构不是 100% 稳定就该用安全取值。小红书的接口字段偶尔会调整写死路径的代码过段时间就崩用安全取值至少不会整个流程挂掉。4.3 增量采集与去重采集做久了数据会重复。同一篇笔记可能在不同关键词下都出现同一个作者的新笔记要增量抓。去重一般用笔记 ID存到本地数据库或者集合里抓之前先查一下有没有。增量的话记录上次采集的时间戳或者游标下次从那里继续。我一般会用 SQLite 存轻量不用额外部署。表结构就几个字段笔记 ID 做主键标题、作者、点赞数、采集时间。每次抓之前SELECT一下存在就跳过。这样跑久了也不会重复。游标的话小红书的列表接口一般返回cursor字段下一页请求带上它就行注意游标可能过期过期了要重新从第一页开始。5. 避坑指南还原过程中最常见的五个问题5.1 签名对不上但代码看起来一模一样现象Python 算出来的签名和浏览器里抓到的对不上逐行对比 JS 逻辑也没发现差异。原因大概率是编码或时间戳精度问题。JS 里字符串拼接时可能隐式调用了toString()或者时间戳用了秒而不是毫秒。解决把 JS 里的中间变量打印出来和 Python 的中间结果逐个对比定位到具体哪一步开始分叉。我一般会在 JS 里插console.log在 Python 里插print两边同步输出最快。5.2 本地能跑服务器上签名失效现象本地调试一切正常部署到服务器后签名全部报错。原因服务器时间不同步时间戳偏差太大服务端判定签名过期。解决服务器上装 NTP 同步时间或者签名时用服务端时间而不是本地时间。这个坑很隐蔽因为本地开发机时间通常是准的服务器不一定。5.3 请求头缺了某个字段导致被拒现象签名算对了但请求还是返回错误。原因除了签名服务端还校验其他请求头比如Referer、Origin、User-Agent缺一个都可能被拒。解决把浏览器里完整的请求头复制下来逐个比对缺什么补什么。特别注意Referer必须是站内页面不能是空的。5.4 JS 代码更新导致算法失效现象昨天还能跑今天突然全部签名错误。原因站点更新了前端代码加密逻辑变了盐值或者拼接顺序改了。解决重新走一遍定位流程对比新旧 JS 代码找到变化的地方。这种更新通常不会大改可能只是换个盐值或者加个字段改几行就能恢复。建议把 JS 代码版本管理起来方便对比。5.5 补环境不完整导致 JS 执行报错现象用execjs跑抠出来的 JS报window is not defined之类的错。原因JS 里引用了浏览器特有对象Node 环境里没有。解决在 JS 代码开头 mock 这些对象比如var window {}; var navigator {userAgent: ...};。mock 的值要尽量真实尤其是userAgent很多站点会校验它和签名是否匹配。6. 最后说一个技巧用对比法快速验证还原结果还原算法最怕的就是看起来对了但实际不对。我的习惯是每还原一步就做一次对比验证而不是全部写完再测。具体做法是在浏览器里触发一次真实请求把请求的输入参数method、path、timestamp、body和输出的签名都记下来然后在 Python 里用同样的输入跑一遍看输出是否一致。一致就继续下一步不一致就停下来查。这个对比法有个关键点输入要固定。时间戳是变的所以对比的时候要么把时间戳也当成输入传进去要么在 JS 和 Python 里都 mock 一个固定时间戳。我一般会在 JS 里临时把Date.now()替换成固定值Python 里也传同样的值这样两边输入完全一致输出不一致就一定是算法问题排除时间干扰。# 固定输入做对比验证 fixed_ts 1700000000000 fixed_body {keyword:test,page:1} js_sign 从浏览器抓到的签名 py_sign, _ build_signature(POST, /api/sns/web/v1/search/notes, fixed_body, SALT, fixed_ts) assert js_sign py_sign, f签名不一致: JS{js_sign}, PY{py_sign}这段断言代码我每次还原新算法都会写跑通了才继续。参数说明fixed_ts是固定的时间戳fixed_body是固定的请求体SALT是从 JS 里抠出来的盐值。断言失败时把两个值打出来逐字符对比通常能发现是大小写、编码还是顺序的问题。回过头看那次翻车教会我一件事采集这类事别想着走捷径签名算法该还原就得还原调 JS 只是权宜之计。从那以后我每次做新站点都强制先走一遍定位参数、下断点、翻译算法、固定输入对比这个流程一步都不省。这套流程跑熟了大部分站点的签名都能在半天内搞定。希望这份还原思路能帮到你少走点我当年走过的弯路。本文还有配套的精品资源点击获取