资讯动态

小红书数据采集Python算法还原:JS逆向签名参数x-s与x-s-common实战

发布时间:2026/10/21 6:32:07 来源:尧图企业网站定制
简介这是一份面向Python爬虫与逆向工程学习者的实战型资源聚焦小红书平台数据采集与算法还原。项目通过Python模拟x-s、x-common等接口协议结合JavaScript逆向思路破解前端加密逻辑并将GET、POST等请求类型与平台API抽象封装实现毫秒级抓取速度与开箱即用的调用体验。资源包共29个文件以11个py源码文件为核心辅以yml、yaml、ini、cfg等配置与持续集成文件以及md、rst文档和Makefile构建脚本整体约28KB结构紧凑、模块划分清晰。内容涵盖核心采集逻辑、异常处理、辅助工具与测试用例并配有README与变更日志说明使用方式。目前已有4451人学习下载适合希望深入理解社交媒体数据抓取、网络请求处理与逆向分析的中高级开发者参考借鉴。1. 从一次接口对不上的抓包说起这套小红书数据采集还原方案到底能干什么上周帮一个做内容分析的朋友看问题他用常规请求库去抓某内容平台的笔记列表返回的永远是加密串或者干脆 404。抓包一看请求头里躺着一个x-s、一个x-t还有一长串x-s-common全是动态生成的。这就是典型的 JavaScript 逆向场景——接口能看见参数算不出来数据就拿不到。这套「小红书数据采集 python 算法还原」资源核心解决的就是这件事把前端 JS 里生成签名参数的算法逻辑用 Python 复刻出来让采集脚本能稳定构造出合法请求。它适合两类人一是做数据采集、需要批量拿公开笔记信息的工程师二是正在学 JS 逆向、想找一个完整案例把「扣代码 → 补环境 → Python 还原」跑通的人。资源里给的是算法还原思路和可复现的代码骨架不是成品爬虫你得自己动手把逻辑接上。2. 签名参数是怎么来的x-s、x-t 与 x-s-common 的生成链路2.1 先搞清楚三个参数各自管什么逆向最忌讳一上来就抠代码得先知道每个参数在请求里扮演什么角色。这套资源里涉及的签名体系常见的是三个字段配合参数名作用是否随时间变化典型长度x-t毫秒级时间戳每次请求都变13 位数字x-s核心签名由路径、参数、时间戳等混合计算每次请求都变不定长十六进制串x-s-common设备与浏览器环境指纹的编码结果同环境内相对稳定较长 Base64 串x-t 最好理解就是发起请求那一刻的时间戳。x-s 是重点它通常把请求方法、接口路径、查询串、请求体、x-t 一起喂进一个哈希函数再拼上固定盐值做二次处理。x-s-common 则是把浏览器环境里能采集到的字段屏幕、时区、UA 相关特征等序列化后编码服务端拿它做风控判断。三者缺一不可少一个就会被判定为异常请求。2.2 定位加密入口的实操路径不要盲搜关键词效率太低。我一般按这个顺序走打开开发者工具的 Sources 面板勾选 XHR/fetch 断点触发一次笔记列表请求。请求被断下后在 Call Stack 里逐层往上翻找哪一帧的局部变量里出现了x-s字样。找到生成点后右键该函数「在函数定义处断点」重新触发确认入参和出参。把该函数及其依赖的工具函数整体抠出来放到本地 Node 环境跑。// 定位到签名函数后先在 Node 里验证它能否独立跑通 // 关键是把 window、document、navigator 这些浏览器对象补上 const crypto require(crypto); function sign(path, params, timestamp) { // 这里的拼接顺序必须和原 JS 完全一致错一个字符结果就全变 const raw ${path}?${params}t${timestamp}salt固定盐值; return crypto.createHash(md5).update(raw).digest(hex); } // 用抓包拿到的真实入参和出参做对照验证还原是否正确 console.log(sign(/api/sns/web/v1/feed, num20, 1700000000000));上面这段是骨架示意真实算法里的哈希方式、盐值、拼接顺序都要以你实际抠出来的代码为准。逻辑说明签名函数的本质是「确定性映射」——同样的输入必须得到同样的输出。所以验证还原是否成功的唯一标准就是拿真实请求的入参喂进去看输出能不能和抓到的 x-s 对上。参数说明path是接口路径params是查询串timestamp必须和请求头里的 x-t 完全一致salt是硬编码在 JS 里的固定串。对不上就逐字符比对拼接模板八成是分隔符或字段顺序的问题。2.3 补环境是绕不过去的一关抠出来的 JS 往往依赖一堆浏览器对象直接扔进 Node 会报window is not defined。常见做法是用jsdom起一个最小 DOM或者手写代理对象把用到的属性一个个补上。// 用 Proxy 兜底访问未定义的属性时返回一个可控的默认值 // 这样能快速让脚本跑起来再根据报错逐个补真实逻辑 const window new Proxy({}, { get(target, prop) { if (prop navigator) return { userAgent: Mozilla/5.0 ... }; if (prop location) return { href: https://example.com }; // 未识别的属性先返回空函数避免直接崩 return typeof target[prop] undefined ? function(){} : target[prop]; } }); global.window window;逻辑说明补环境的目标不是完美模拟浏览器而是让签名函数能顺利执行到返回结果那一步。参数说明userAgent、location.href这类字段如果参与了 x-s-common 的计算就必须填成和真实请求一致的值否则算出来的指纹对不上。踩坑提醒Proxy 兜底虽然方便但会把「本该报错的地方」静默吞掉导致你误以为补全了实际某个关键属性返回了空函数。建议先用兜底跑通再逐个把用到的属性替换成真实值。3. 把 JS 逻辑翻译成 Python三种还原路线怎么选3.1 路线对比纯 Python 重写、Node 子进程、PyExecJS还原算法有三条路各有适用场景别一上来就选最重的。路线做法优点缺点适用场景纯 Python 重写用 hashlib、hmac 等库复刻逻辑无外部依赖速度快遇到复杂混淆极难还原算法简单、以标准哈希为主Node 子进程Python 调 node 执行 JS 拿结果还原度高改动小每次请求起进程慢算法复杂、请求量不大PyExecJSPython 内嵌 JS 引擎执行调用方便环境兼容性玄学易崩快速验证阶段我的建议先用 PyExecJS 或 Node 子进程把 JS 跑通确认输入输出对得上再评估要不要花时间做纯 Python 重写。如果 QPS 要求不高Node 子进程方案最省心别为了性能硬啃混淆代码。3.2 Node 子进程方案的完整实现这是资源里最实用的部分Python 负责调度和采集JS 负责算签名两边通过标准输入输出通信。import subprocess import json def get_signature(path, params, timestamp): 调用 Node 脚本计算签名返回 x-s 和 x-s-common payload json.dumps({ path: path, params: params, timestamp: timestamp }) # 每次调用起一个 node 进程传入 JSON读回结果 result subprocess.run( [node, sign.js], inputpayload, capture_outputTrue, textTrue, timeout5 # 超时保护避免 JS 卡死拖垮采集 ) if result.returncode ! 0: raise RuntimeError(f签名失败: {result.stderr}) return json.loads(result.stdout) # 使用示例 sig get_signature(/api/sns/web/v1/feed, num20, 1700000000000) print(sig[x-s], sig[x-s-common])逻辑说明Python 把签名所需的三个入参序列化成 JSON通过 stdin 传给 NodeNode 侧sign.js读取 stdin、调用抠出来的签名函数、把结果以 JSON 打到 stdout。参数说明timeout5是必须的JS 里如果有死循环或环境缺失导致的阻塞没有超时会把整个采集流程挂死。capture_outputTrue同时捕获 stdout 和 stderr方便定位 Node 侧报错。踩坑提醒Node 进程启动有开销单次约几十毫秒如果每秒要发几十个请求这个方案会成为瓶颈此时要么做进程池要么老老实实重写成 Python。3.3 对应的 sign.js 怎么写// sign.js从 stdin 读入参数算出签名后从 stdout 输出 const crypto require(crypto); // 把抠出来的签名逻辑封装成函数 function generateSign(path, params, timestamp) { const raw ${path}?${params}t${timestamp}salt固定盐值; const xs crypto.createHash(md5).update(raw).digest(hex); // x-s-common 通常是环境指纹的编码这里用占位逻辑示意 const common Buffer.from(JSON.stringify({ t: timestamp })).toString(base64); return { x-s: xs, x-s-common: common }; } let input ; process.stdin.on(data, chunk input chunk); process.stdin.on(end, () { const { path, params, timestamp } JSON.parse(input); const result generateSign(path, params, timestamp); process.stdout.write(JSON.stringify(result)); });逻辑说明Node 侧用事件方式读取完整 stdin解析后调用签名函数结果序列化输出。参数说明salt和哈希方式必须和原 JS 一致这是还原正确性的命门。x-s-common的真实生成逻辑通常比这里复杂得多涉及环境字段的排序和编码需要你根据抠出来的代码补全。踩坑提醒stdout 只能输出签名结果本身任何console.log调试语句都会污染输出导致 Python 侧 JSON 解析失败调试信息一律走 stderr。4. 采集链路的工程化请求构造、频率控制与数据落库4.1 请求头必须和签名时的假设一致签名算对了请求头填错照样失败。这是最常见的翻车点。import requests def build_headers(sign_result, timestamp): 构造完整请求头字段值必须和签名时的假设一致 return { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., x-t: str(timestamp), x-s: sign_result[x-s], x-s-common: sign_result[x-s-common], Content-Type: application/json;charsetUTF-8, Referer: https://www.example.com/, } def fetch_feed(keyword, page): timestamp int(time.time() * 1000) params fkeyword{keyword}page{page}num20 sign_result get_signature(/api/sns/web/v1/search/notes, params, timestamp) headers build_headers(sign_result, timestamp) resp requests.get( https://www.example.com/api/sns/web/v1/search/notes, paramsparams, headersheaders, timeout10 ) return resp.json()逻辑说明时间戳在签名和请求头里必须是同一个值所以先算 timestamp再拿它去签名最后填进 header。参数说明User-Agent如果参与了 x-s-common 的计算就必须和补环境时用的完全一致否则指纹校验不过。Referer有些接口会校验缺了会返回风控页。踩坑提醒params在签名时和实际请求时的字符串必须逐字符相同包括参数顺序和编码方式用requests的params字典会自动重排建议直接拼成字符串。4.2 频率控制与重试策略采集不是发得越快越好触发风控后整个 IP 段都可能被限。import time import random def safe_fetch(keyword, max_page10): 带随机间隔和退避重试的采集循环 results [] for page in range(1, max_page 1): try: data fetch_feed(keyword, page) results.append(data) # 随机间隔 1.5 到 3.5 秒模拟人工浏览节奏 time.sleep(random.uniform(1.5, 3.5)) except Exception as e: print(f第 {page} 页失败: {e}) # 失败后指数退避给服务端喘息时间 time.sleep(5 * page) return results逻辑说明随机间隔比固定间隔更难被识别为机器行为指数退避在连续失败时逐步拉长等待。参数说明uniform(1.5, 3.5)的下限别低于 1 秒上限根据实际风控强度调整。5 * page是简单的线性退避页数越大等得越久。踩坑提醒如果连续多页都返回空数据或风控页别硬刚停下来检查签名是否失效或环境指纹是否被标记继续发只会加重封禁。4.3 数据落库的字段设计拿到 JSON 后别急着全量存先想清楚要哪些字段。import sqlite3 def init_db(pathnotes.db): conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS notes ( note_id TEXT PRIMARY KEY, title TEXT, author TEXT, liked_count INTEGER, collected_at INTEGER ) ) return conn def save_notes(conn, items): 用 INSERT OR IGNORE 做去重note_id 作为主键 for item in items: conn.execute( INSERT OR IGNORE INTO notes VALUES (?, ?, ?, ?, ?), (item[id], item[title], item[author], item.get(liked_count, 0), int(time.time())) ) conn.commit()逻辑说明用note_id做主键配合INSERT OR IGNORE重复采集同一篇笔记时自动跳过避免数据膨胀。参数说明collected_at记录采集时间方便后续做增量更新。踩坑提醒字段类型要提前定好liked_count这类数值字段如果接口返回的是字符串比如「1.2万」直接入库会类型混乱入库前先做清洗转换。5. 避坑与排查签名还原路上最容易翻车的五个点5.1 现象签名本地算出来和抓包不一致原因拼接模板里某个分隔符、字段顺序或大小写和原 JS 不同。JS 里字符串拼接极其敏感和?的位置、参数是否排序、时间戳是字符串还是数字都会影响结果。解决把原 JS 的拼接语句逐字符抄下来用同一组入参在 Node 里跑和抓包结果做 diff。定位到具体差异字符后再同步修改 Python 侧。别靠猜靠比对。5.2 现象Node 脚本单独跑正常Python 调用就报错原因多半是 stdin 读取不完整或编码问题。Python 的subprocess默认用系统编码中文参数传过去可能乱码。解决subprocess.run里显式指定encodingutf-8Node 侧process.stdin.setEncoding(utf8)。另外确认input传的是完整 JSON 字符串末尾不需要额外换行。5.3 现象请求返回 200 但数据是空的或提示风控原因x-s-common 的环境指纹和真实浏览器差异太大或者请求频率触发了限流。解决先降低频率把间隔拉到 3 秒以上观察是否恢复。如果仍然风控检查补环境时用的 UA、屏幕参数等是否和签名时一致。指纹类参数一旦被标记换个环境重新采集往往比硬调参数更快。5.4 现象PyExecJS 报「Could not find an available JavaScript runtime」原因PyExecJS 依赖系统里已安装的 JS 引擎Node、PyV8 等环境没配好就会找不到。解决确认 Node 已加入 PATH或者改用subprocess直接调 Node绕开 PyExecJS 的运行时查找逻辑。这也是我一般更推荐子进程方案的原因依赖更少出问题更好定位。5.5 现象采集跑一段时间后签名突然全部失效原因平台更新了前端 JS签名算法变了。这是逆向类项目的常态没有一劳永逸。解决保留一套「签名验证」脚本定期用真实请求校验本地算法是否还对得上。一旦发现对不上重新走一遍定位加密入口的流程。把签名逻辑单独抽成模块更新时只改这一个文件别散落在采集代码各处。6. 进阶把签名模块做成可替换的插件以及我的验证习惯写到这儿真正决定这套东西能不能长期用的不是某一次还原成功而是签名模块的可维护性。我的做法是把签名逻辑抽成一个独立接口采集主流程只依赖接口不关心底层是 Node 子进程还是纯 Python。class SignProvider: 签名提供者的抽象接口换算法时只改实现类 def sign(self, path, params, timestamp): raise NotImplementedError class NodeSignProvider(SignProvider): def sign(self, path, params, timestamp): return get_signature(path, params, timestamp) class PurePythonSignProvider(SignProvider): def sign(self, path, params, timestamp): # 纯 Python 重写版本性能更好但维护成本高 raw f{path}?{params}t{timestamp}salt固定盐值 xs hashlib.md5(raw.encode()).hexdigest() return {x-s: xs, x-s-common: ...}逻辑说明采集代码只调provider.sign(...)哪天 Node 方案扛不住性能了换成纯 Python 实现主流程一行不用动。参数说明三个入参保持统一任何实现类都必须接受同样的输入、返回同样结构的字典。踩坑提醒抽象层别做太早先把一种方案跑通再抽否则容易过度设计接口定错了返工更麻烦。验证方法上我习惯每次改完签名逻辑都拿三组真实抓包数据做回归一组正常请求、一组带中文参数的、一组参数顺序打乱的。三组都能对上才认为还原是可靠的。这个习惯是被坑出来的——曾经有一次只验证了最简单的请求上线后遇到带中文关键词的查询全部签名错误排查了半天才发现是编码环节漏了一步。从那以后我每次动签名相关代码都强制走一遍这三组回归不管改动多小。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑