做小红书数据采集这段时间,被问得最多的不是“能不能爬”,而是“为什么代码明明照着写,跑起来就是被限流、拿到的字段还缺斤少两”。说实话,小红书在一众内容平台里属于反爬做得比较用心的一家,核心接口全走签名校验,Web端和App端的风控策略还不一样,网上很多教程要么只讲个思路,要么直接甩一个封装好的库就完事,真正能落地跑通的并不多。这篇东西就是我最近一次完整实践的全记录,从短链解析到作品ID提取、从x-s签名处理到图文和视频的批量下载,再到封禁、限流、登录态失效这些高频问题的排查思路,全部按实操顺序写出来。适合刚接触爬虫、想拿小红书练手的朋友,也适合已经写过基础爬虫、但卡在签名或风控上的同学参考。
1. 先搞清楚你在对抗什么:小红书反爬的真实构成
很多人一上来就写代码,Requests直接怼详情页接口,结果返回一个-46或者-30就懵了。这里先说一句大实话:在小红书这个平台上,接口本身几乎不设防,真正拦住你的是签名和风控策略。参数拼对了、签名算对了、Cookie是新鲜的,请求就能通;少一个头、签名过期、请求频率稍微高一点,立刻就会触发拦截。所以第一步不是写代码,是先理解它的反爬体系由哪几层组成。
1.1 小红书的数据结构与爬虫视野里的“三座大山”
从小红书App或者Web端打开任意一篇笔记,你能看到的核心数据无非是这些:笔记标题、正文描述、作者昵称与ID、发布时间、点赞/收藏/评论数、图片列表、视频地址。这些字段对爬虫来说都是目标。但要想稳定拿到这些字段,必须先翻过三座大山:
第一座是短链解析。你在App里复制的分享链接长这样:http://xhslink.com/m/xxxxx,这是短链,不是真正的详情页地址。爬虫拿到短链后,要先去请求一次拿到跳转后的长链接,再从长链接的URL参数里解析出noteId(笔记ID)。这个ID是所有后续接口请求的“身份证”,没有它什么都干不了。
第二座是x-s签名。小红书Web端详情页接口https://www.xiaohongshu.com/explore/{noteId}并不是纯静态页面,关键数据是异步加载的,请求/api/sns/web/v1/feed这个接口时,Header里必须带上x-s这个动态签名。x-s是通过window._webmsxyw这个JS文件里的算法对请求参数和时间戳加密生成的,不带上它,接口直接返回{"success": false}。
第三座是风控策略。就算你拿到了x-s,如果短时间内请求次数太多,或者请求的Headers指纹看起来像机器(比如TLS指纹、Accept-Language异常、缺少Referer),平台就会返回滑块验证码或者直接封禁本机IP一段时间。
这三座大山缺一不可,任何一个没处理好,整个采集流程都会断在半路。后面每一章我都会针对其中一座山展开讲,这里先建立一个整体认知:小红书反爬不是单一验证,而是“短链+签名+频控+指纹”四层叠加的体系,写代码之前一定要有心理准备,不能拿普通文章站的经验来套。
1.2 接口选型:Web端优先还是App端优先
这是很多人纠结的点。App端接口/api/sns/v1/note/note-detail返回的数据最全,图片原图、视频无水印地址都能拿到,而且字段结构比Web端清晰得多。但App端的难点在于签名算法更复杂,需要逆向com.xingin.xhs这个客户端包,成本高、脆弱性强,App一升级算法就可能变。
Web端接口在字段完整度上略逊一筹,但胜在签名算法相对稳定、社区逆向资料多、用Playwright或者Pyppeteer模拟操作时不需要处理手机设备指纹。
我自己的建议是新手直接从Web端入手。原因很简单:Web端的x-s签名有成熟的JS文件可以分析,网上也有开源项目维护了最新算法,配合Playwright自动操作页面时甚至不需要自己算签名,直接等页面渲染完从网络请求里读结果就行。等把整个流程跑通、理解了数据结构和反爬逻辑之后,如果确实需要原图无水印,再回头看App端也不迟。
另外还要考虑一个场景:你是想写一次性脚本,还是想做一个长期跑的采集服务。一次性脚本用Playwright直连页面最稳,长期服务则需要在接口层做签名复用和IP池管理,这个差距在方案设计阶段就要想清楚,否则后期返工成本很高。
1.3 采集方案选型:三种路线怎么选
市面上常见的采集方案有三类,我按推荐程度排序:
方案A:Playwright + 浏览器模拟操作(推荐新手)。用Playwright启动一个真实Chromium,打开笔记页面,等待XHR请求返回数据后直接从response里取值。这种方式不用关心x-s怎么算,因为浏览器自己会算,你只是拦截一下数据而已。缺点是速度慢、并发低,但一个月跑几百几千条数据完全够用。
方案B:Requests + x-s签名算法(推荐进阶)。直接请求接口,需要在JS文件里提取x-s生成算法,或者用开源库生成签名。速度快、并发高,适合规模化采集。缺点是签名算法会变,要定期维护。我这次实战用的是这个方案,后面第2章会详细讲。
方案C:第三方采集API(推荐零基础)。网上有现成的小红书数据API服务,传个链接返回结构化JSON。省事是省事,但费用不低,而且数据格式不可控,有些服务稳定性也一般。
我的观点是:技术学习阶段一定要走方案A或B,哪怕慢一点,至少你能掌握整个数据流转链路。上来就买API,出了问题你连排查方向都没有。
2. 从分享链接到作品ID:短链解析与x-s签名实操
前面说了,拿到短链后第一步是解析出真实链接和noteId。这一步看起来简单,但实操里有几个坑,我这次就踩了。
2.1 短链解析的原理与代码实现
短链的本质是一个302重定向。用Requests请求短链URL时,只要不设置allow_redirects=False,Requests会自动跟随重定向,最终返回的response.url就是长链接。长链接的格式通常是这样的:
https://www.xiaohongshu.com/explore/64f3c1e2000000001f03c0d2?xsec_token=ABYz...其中/explore/后面的那串32位十六进制字符串就是noteId,也就是笔记ID。这里有个关键细节:长链接里还有xsec_token这个参数,它是一段临时校验凭证,有时效性,后面请求接口时也要带上,否则就算破解了x-s也会被拒。
实操代码很简单:
import requests def parse_share_url(short_url: str) -> dict: headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 " "Mobile/15E148 Safari/604.1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } resp = requests.get(short_url, headers=headers, allow_redirects=True, timeout=10) long_url = resp.url # 提取 noteId if "/explore/" in long_url: note_id = long_url.split("/explore/")[1].split("?")[0] elif "/discovery/item/" in long_url: note_id = long_url.split("/discovery/item/")[1].split("?")[0] else: note_id = "" # 提取 xsec_token from urllib.parse import urlparse, parse_qs query = parse_qs(urlparse(long_url).query) return { "note_id": note_id, "xsec_token": query.get("xsec_token", [""])[0], "long_url": long_url, }注意几点:第一,User-Agent尽量用手机端,因为App里复制的短链在PC端访问时部分老链接会跳到首页而不是笔记页;第二,短链请求不要加过多自定义Header,否则容易被WAF识别为异常请求;第三,解析出来的noteId一定要校验长度,正常是24位十六进制字符串,长度不对基本就是解析失败了。
2.2 详情页接口与x-s签名处理
拿到noteId和xsec_token之后,就可以请求详情页数据接口了。我用的是Web端接口:
GET https://www.xiaohongshu.com/api/sns/web/v1/feed params: source_note_id: {noteId} source: detail xsec_token: {xsec_token}这个接口返回的JSON结构体里,data.items[0].note_card就是笔记主体数据。但直接请求它,返回的几乎是必然的{"success": false, "code": 510},除非你在Header里带上合法的x-s。x-s的生成逻辑简单说就是:把请求路径、查询参数、时间戳、x-t放到一个数组里,通过某个加盐的MD5算法拼成签名。具体的算法细节每家实现都不太一样,我这边是直接从浏览器里提取的JS逆向结果。
说句实话,x-s这块逆向过程比较枯燥:打开Chrome开发者工具,全局搜索x-s,找到window._webmsxyw相关的JS文件,断点调试后把加密逻辑抠出来,用execjs在Python里调用。这中间会遇到各种JS环境缺失的问题,需要补window、document等对象,非常磨人。
如果你不想折腾逆向,有两个替代方案:
- 用Playwright打开一个空白页面,执行
page.evaluate调用页面里的签名函数,把参数传进去拿签名返回。这种方案不用抠JS,但对浏览器环境依赖较高,并发时性能一般。 - 直接找开源库,比如GitHub上维护得比较好的
ReaJason/xhs这类项目,它会同步最新的签名算法和接口封装。用这类库时注意看更新时间,超过三个月没更新的基本就不太可靠了。
我自己这次的实践是先用Playwright方案跑通,再用逆向JS方案做性能优化。两条路都验证过,才敢在博文里写出来。
2.3 登录态与Cookie的坑
web端详情接口还有一个隐性问题:未登录状态下,部分数据字段会被裁剪。比如作者IP归属地、部分评论数据,未登录的Cookie拿不到。另外高频请求时,未登录账号比登录账号更容易触发滑块验证。
所以实际操作中,我建议先在浏览器里手动登录一次小红书,再把Cookie导出到脚本里。注意web_session这个Cookie是核心,它过期之后所有接口都会返回-101。处理方式有两种:一种是写个定时任务定期用Playwright重新登录刷新Cookie,另一种是直接接打码平台过滑块。
常量级的小红书采集,Cookie能撑几个小时到一天不等;大并发场景下,单账号Cookie基本撑不过一个小时。这个后面第4章会展开讲。
3. 图文和视频数据怎么拿:字段解析与批量下载实战
接口通了之后,真正的体力活才开始。同一个接口对不同类型笔记返回的字段结构不一样,图文和视频要分开解析,而且图片有原图和缩略图之分、视频有水印和无水印之分,这里面的门道很多。
3.1 图文笔记的字段拆解与图片下载
详情接口返回的JSON里,图文笔记的核心数据在note_card下面:
{ "note_id": "64f3c1e2000000001f03c0d2", "title": "笔记标题", "desc": "笔记正文内容", "user": { "nickname": "作者昵称", "user_id": "作者ID" }, "image_list": [ { "url_default": "https://sns-webpic-qc.xhscdn.com/xxx", "url_pre": "https://sns-webpic-qc.xhscdn.com/xxx", "info_list": [] } ], "interact_info": { "liked_count": "1.2万", "collected_count": "3456", "comment_count": "89" }, "type": "normal" }这里最容易被忽视的是url_default和url_pre的区别。url_pre是压缩图,宽度960px左右,适合做数据分析;url_default是原图,文件大、下载慢,但画质最好。如果你是要做素材库,建议直接用url_default;如果只是批量采集做内容分析,用url_pre就够了,能省一半流量和存储。
还有一个细节:图片URL里的imageView2/2/w/960/format/jpg这类参数可以通过改URL动态调整尺寸。比如把w/960改成w/1920可以拿到更高分辨率的版本,把format/jpg改成format/png可以转格式。这个技巧在做图片数据集时很实用。
下载图片时一定要设置流式下载,用requests.get(url, stream=True)分块写入文件,否则大图一次性读进内存容易导致机器卡死。文件名建议用noteId加序号,比如64f3c1e2000000001f03c0d2_1.jpg,方便后续回溯。
3.2 视频笔记的地址提取与去水印处理
视频笔记在note_card里多了一个video字段:
"video": { "url": "https://sns-video-bd.xhscdn.com/xxx", "duration": 246000, "captions": [] }这个url拿下来是可以直接播放的,但注意它已经带了水印。所谓“去水印”,技术原理并不是真的对视频做图像处理,而是找到无水印的源视频地址。小红书的视频源地址通常隐藏在stream.hls或origin_video相关字段里。如果你是通过解析分享链接拿到的页面数据,可以去试一下把接口换成/api/sns/web/v1/feed时返回的video.url里加一个参数:
视频地址中如果包含 watermark=1,改成 watermark=0 大概率能拿到无水印源地址但这个不是100%有效,版本更新后参数名可能会变。更可靠的办法是抓包App端下载视频时的网络请求,里面有无水印地址的明文。这个方式需要配合抓包工具(如Charles或Frida),对于纯Python爬虫场景来说有点重,所以我一般建议:如果你只是做个人素材收集,直接用带水印的版本就行;如果确实需要无水印,优先考虑官方创作中心下载,或者用专业剪辑工具,不要花太多精力在解析上,性价比太低。
3.3 批量下载与数据存储设计
爬虫跑起来之后,数据量很快就上来了,这时候存储设计就很重要。我这次的方案是:
data/ raw/ 64f3c1e2000000001f03c0d2/ images/0.jpg images/1.jpg video.mp4 meta.json每篇笔记一个文件夹,meta.json存结构化信息,图片视频单独放。这样做的优点是后续回溯方便:拿到一条URL,能直接通过note_id找到对应的所有素材。缺点是小文件多,不适合大规模存储。如果你采集量达到十万级,建议直接上云存储(OSS/COS)或者SQLite,不要在本地堆文件夹。
meta.json的字段我一般这样设计:
{ "note_id": "", "type": "image|video", "title": "", "desc": "", "author": "", "user_id": "", "liked_count": 0, "collected_count": 0, "comment_count": 0, "image_count": 0, "download_time": "2025-01-01 00:00:00", "source_url": "" }数值字段不要存“1.2万”这种格式化后的字符串,要存原始数值,统计时才方便。如果你从接口拿到的就是格式化字符串,需要写个解析函数转换一下,比如“1.2万” -> 12000、“3456” -> 3456。
4. 踩坑实录:高频拦截、登录失效与请求异常排查
跑了几天之后,我把各种异常都见了一遍。这一章是最实在的部分,全是我自己踩出来的教训,做成速查表给各位参考。
4.1 高频拦截与IP限流的应对策略
现象:爬虫跑了十几分钟,突然返回{"success": false, "code": 461},或者请求直接被重置(Connection Reset)。
原因:触发频控了。小红书对单个IP的请求速率有限制,Web端接口稳定跑的话,单IP并发控制在2-3个以内,每秒请求数不要超过1次比较安全。新手最容易犯的错是盲目加并发,开10个线程去怼同一个接口,结果全被封。
应对策略有三档:
第一档,降低请求频率。最简单也最有效,每抓取一篇笔记后sleep(1-2秒)随机延时,这样一天跑几千篇基本没问题。
第二档,代理IP池。当数据量更大时,单IP扛不住,需要配置代理池。注意不要用免费代理,质量太差,反而容易被风控关联。国内代理服务商的隧道代理模式比较合适,按量付费,稳定很多。
第三档,分布式采集。多台机器、多个IP,配合消息队列做任务分发。这个一般团队级才会用到,个人玩家不用考虑。
还有一点很多人不知道:请求失败了不要立即重试。发现返回461或者510,应该立刻停止当前线程,等待5-10分钟再继续。立即重试只会让风控判定更严格。我遇到过最严重的封禁,是某次没控制好频率,IP被封了24小时,直接导致当天任务全停。
4.2 登录失效与Cookie过期排查
现象:请求一直返回-101,或者"success": false, "code": -100。
原因:web_session过期了,或者Cookie被风控标记失效。小红书的登录态不像普通网站能维持很久,尤其是频繁请求时,失效速度会加快。
排查思路:
- 先检查本地时间是否正确。x-s签名里带时间戳,本地时间偏差超过30秒,签名就会失效,返回
-101。 - 检查Cookie里的
web_session字段是否为空。为空就是没带上登录态。 - 打开浏览器手动访问一篇笔记,看能否正常加载。如果手动访问也跳验证码,说明账号本身被风控了,需要人工恢复。
我自己的做法是把Cookie存到本地文件,写一个前置检查函数,每次跑批量任务前先拿一个不重要的接口试一下,如果返回401/403就自动触发Playwright重新登录。这样能避免任务跑了一半才发现Cookie失效,白白浪费时间和流量。
4.3 常见错误码速查与处理思路
下面这个表是我踩坑过程中整理的,不完全覆盖所有情况,但覆盖了90%的日常使用场景:
| 错误码/现象 | 含义 | 处理办法 |
|---|---|---|
| -30 | 签名校验失败 | 重新生成x-s,检查x-t时间戳 |
| -46 | 参数缺失或非法 | 检查noteId、xsec_token是否为空 |
| -100 | 登录态失效 | 重新登录,刷新Cookie |
| -101 | 签名过期 | 检查系统时间,重新生成签名 |
| 461 | 请求频率过高 | 停10分钟,降低频率 |
| 510 | 反爬校验未通过 | 检查Header完整性,补Referer |
| 滑块验证 | 触发人机验证 | 停任务,换IP,或打码平台 |
| Connection Reset | IP被临时封禁 | 停一段时间,后续换代理 |
遇到错误时一定要能看懂错误码,否则就像无头苍蝇一样乱试。我的习惯是写一个统一的异常处理函数,把错误码打印出来的同时记录到日志文件,然后根据码值决定是重试、延时还是放弃。
4.4 一些容易被忽略的Headers细节
Header字段是很多教程的一笔带过之处,但恰恰是最坑的地方。有一次我排查了一下午,最后发现是Referer没带,导致接口一直返回510。
小红书Web端接口对Headers的要求可以整理成一个最小集:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Referer: https://www.xiaohongshu.com/explore/{noteId}?xsec_token=... Origin: https://www.xiaohongshu.com Accept: application/json, text/plain, */* Accept-Language: zh-CN,zh;q=0.9 Content-Type: application/json; charset=UTF-8有些教程会让你把所有浏览器Headers一股脑贴上,但这样反而容易触发风控,因为真实浏览器每次请求的Header顺序和字段会有细微差别,而爬虫如果每次都一摸一样,反而容易被识别。我的建议是只保留核心字段,把Accept-Language固定成zh-CN,zh;q=0.9,把Referer每次动态生成,这样指纹更自然。
TLS指纹是另一道坎。如果你用Python的Requests库请求HTTPS接口,TLS握手特征和真实浏览器差距很大,风控系统可以通过JA3指纹识别出你是Python脚本。这个问题在目标平台风控升级后越来越明显,简单方案是使用curl_cffi这个库,它模拟了浏览器的TLS指纹,实测能解决很多莫名其妙的拦截问题。
5. 做采集之前必须想清楚的合规边界
现在来聊一个很多人避而不谈、但实际做项目绕不开的话题。小红书爬虫在技术上可行,但合规性的边界要心里有数,否则真有可能给自己惹麻烦。
首先,爬取公开页面数据用于个人学习、学术研究,在多数情况下属于正当需求。但需要注意几点边界:
第一,不要绕过多因素认证或破坏技术保护措施。破解验证码、伪造签名本身就在灰产边缘,如果是为了商业目的,风险会成倍放大。我分享这些技术是为了帮助大家理解反爬原理,请用在正当场景。
第二,不要大规模爬取并商业化。把小红书上的用户笔记、图片、视频采集下来做自己的内容库,或者二次售卖,这是最危险的用法,可能涉及著作权和不正当竞争问题。
第三,遵守平台的Robots协议和服务条款。爬虫请求频率过高对平台服务器造成压力,本身就是不文明的行为。建议控制频率,做好限速,用最小代价获取所需数据。
我自己的原则是:只采集公开接口能拿到的数据和用户主动分享的内容,不碰用户隐私字段(如手机号、收货地址),不做批量注册和刷量操作,数据只用于个人研究和学习,采集的规模控制在合理范围内。这个边界看起来模糊,但实际上很好判断,核心是“给别人带来什么影响”——不打扰、不破坏、不滥用,就算是有职业操守的数据开发者。
如果本身是电商运营或者内容创作者,需要定期分析小红书数据,我更推荐走官方合作渠道,比如蒲公英平台、千帆数据后台,这些工具提供的数据更规范、更稳定,也避免了所有合规风险。爬虫更适合做一些官方工具覆盖不到的研究性、趣味性的小项目,比如分析某个话题的热门笔记、收集某个博主的作品集作为学习素材。
写在最后的一个小技巧
如果你之前一直卡在x-s签名这一步,我给你一个能快速上手的建议:不要先抠JS,先用Playwright跑通全流程,哪怕慢一点,重在把数据链路和字段结构搞清楚。签名这种事,等你确认自己的需求确实需要高性能时再碰,那时候你对整个系统的理解已经足以支撑你快速定位逆向要点。
最后再说一个不是人人都知道的小经验:小红书的Web端接口对x-s的校验并不是所有接口都同等严格,部分搜索接口对签名的容忍度会高一些,所以如果你只是做关键词搜索采集,成功率会比详情页采集高很多。善用这个特性,前期开发会顺畅不少。