
小红书的内容质量一直在线图文笔记的排版、封面、标题设计都很有参考价值。不少做运营、做竞品分析的朋友甚至单纯想备份自己收藏内容的人都动过能不能用Python把小红书图文批量扒下来的念头。网上相关的碎片教程不少但要么只给一段跑不通的代码要么直接甩给你一个封装好的GUI工具出了问题根本不知道怎么办。这篇文章我打算换一个思路。不给你直接能跑的黑盒脚本而是带着你把整条链路的原理彻底吃透从分析小红书网页端的请求结构到找到图文信息的真实来源再到用Python解析封面、标题、文案最后落盘保存。整个过程中你会搞清楚数据到底藏在哪为什么有些教程里的代码跑两天就失效签名参数到底是个什么东西以后再遇到任何反爬严格的站点你都知道该从哪里下手。这套实战思路适合的人群很明确刚学完Python基础语法想拿真实项目练手的人做内容运营需要批量分析竞品笔记的人以及纯粹对爬虫技术感兴趣、想搞明白幕后机制的开发者。如果你已经是资深爬虫工程师这篇文章的代码部分对你来说偏基础但其中的分析思路和调试方法或许还能带来一点新启发。1. 爬虫落地前必须先建立的三个认知先别急着写代码。我见过太多人一上来就抱着requests.get(url)往小红书主页上怼然后被重定向、验证码、风控轮流教做人最后得出小红书爬不了的结论。这不是技术问题是认知问题。第一个认知小红书网页端是典型的JavaScript渲染应用。你在浏览器里看到的封面图、标题、点赞数绝大部分不是服务器直接返回的HTML里自带的而是页面加载后通过异步请求从接口拿数据再用前端框架渲染出来的。所以如果你直接请求笔记详情页的URL得到的HTML里可能只有一堆空的div标签和一段初始化脚本正文内容根本不在里面。第二个认知真正有价值的数据都在XHR接口里。浏览器开发者工具F12里的Network面板才是爬虫工程师的主战场。小红书网页端的数据接口有固定的请求格式返回的是结构化的JSON里面包含了笔记的标题、描述、图片链接、作者信息、点赞收藏数等等。你要做的不是解析HTML而是找到那个返回JSON的接口然后解析JSON。第三个认知接口不是裸奔的它有一层签名校验。小红书对Web端接口做了签名保护请求头里需要带上x-s、x-t这类签名参数而且签名算法会不定期更新。这就是为什么很多教程里的代码用着用着就突然失效了——不是你的代码写错了是对方的签名算法更新了你手里的钥匙失效了。这三个认知建立起来之后你再看网上那些爬虫失效的帖子基本一眼就能判断出问题出在哪一环。接下来我们就按这个思路一步步把整个流程走通。2. 准备工作环境搭建与目标站点结构分析2.1 Python环境与依赖库选择我默认你已经装好了Python 3.8以上的版本。没装的话去官网下载安装包安装时记得勾选Add Python to PATH这个细节能帮你省掉后面很多终端命令找不到的麻烦。需要安装的第三方库只有三个pip install requests pip install beautifulsoup4 pip install lxmlrequests负责发HTTP请求beautifulsoup4负责解析HTML虽然我们主要解析JSON但偶尔需要处理HTML片段lxml是BeautifulSoup的解析引擎比Python内置的html.parser快不少。有朋友会问为什么不装scrapy我的回答是不要为了用框架而用框架。scrapy是重型爬虫框架适合大规模分布式采集。你现在是零基础学原理用requests手动控制每一个请求才能真正理解HTTP交互的每一个细节。等你把这条路走通了再去学scrapy那就是水到渠成的事情。2.2 用浏览器开发者工具分析小红书请求结构打开浏览器无痕窗口访问小红书网页版随便点开一篇图文笔记。然后按F12打开开发者工具切到Network网络面板刷新页面。你会看到网络面板里刷出来几十个请求。这时候别慌我们按类型筛选点Fetch/XHR这一栏——这一步能把所有异步接口请求过滤出来排除掉图片、CSS、JS文件等静态资源。在这个筛选结果里你会找到一个名字类似fe_api/burdock/web/v2/note的请求不同时期的接口路径可能略有变化但关键词一般包含note。点开这个请求预览它的响应内容你会发现里面就是这篇笔记的完整结构化数据包括笔记标题title字段笔记正文desc字段图片链接列表imageList字段里面是图片的原始URL作者信息user字段点赞、收藏、评论数interactInfo字段看到这里你就明白了解析小红书图文的核心就是把爬虫的注意力从页面HTML转移到这个JSON接口上。我们需要从请求头里复制几个关键信息Cookie你的登录凭证。不登录虽然也能访问部分公开页面但会被频繁要求验证码所以建议登录后复制完整的Cookie字符串。User-Agent标识你的客户端类型。不要用默认的python-requests伪装成浏览器的UA字符串能降低被识别为爬虫的概率。还需要一个关键参数——笔记ID。小红书笔记的URL格式一般是https://www.xiaohongshu.com/explore/{笔记ID}那串62位左右的字符串就是笔记ID。它是接口请求中最重要的参数我们在后面会频繁用到。2.3 请求头与Cookie的作用机制爬虫初学者往往不重视请求头认为能拿到数据就行。但如果你想长期稳定地采集数据请求头就是你的第一道护城河。User-Agent的作用是告诉服务器我是一个浏览器。服务器可以通过UA字符串判断客户端的类型python-requests这种UA在服务端的风控系统里基本等于直接亮明身份。我们复制浏览器里那一长串UA本质上就是借一件迷彩服。Cookie的作用是告诉服务器我是谁我登录过。小红书的很多接口对未登录用户只返回部分数据登录后才会返回完整内容。尤其是图片链接未登录状态下经常被替换成模糊图或占位图。带上有效的Cookie才能拿到高清原图。还有一个容易被忽略的请求头——Referer它告诉服务器我是从哪个页面跳转过来的。小红书的部分接口会校验这个字段如果为空或来源不对可能返回403。所以我们在代码里最好把Referer也设置成笔记详情页的URL。到这里准备工作就做完了。我们把浏览器开发者工具里看到的请求信息原封不动地搬到Python代码里让脚本模仿浏览器的行为去请求接口。3. 手写首个请求脚本从HTML到JSON的跨越3.1 构造请求头与基础请求函数先写一个最小可用的请求函数。这个函数的核心任务是给定笔记ID返回接口的JSON数据。import requests def fetch_note_json(note_id, cookie_str): url fhttps://www.xiaohongshu.com/fe_api/burdock/web/v2/note/{note_id} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Cookie: cookie_str, Referer: fhttps://www.xiaohongshu.com/explore/{note_id}, Accept: application/json, text/plain, */* } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp.json()这里有个细节URL里的接口路径可能因为你访问时的页面版本不同而有差异但核心结构都是/fe_api/burdock/web/v2/note/{note_id}。如果你的网络面板里看到的路径不带note_id而是通过请求参数传递的那就在params里传效果一样。3.2 解析封面图URL的两种模式小红书图文的封面图在JSON里有两种存在形式。第一种是单图模式data下的imageList数组里只有一个元素里面包含urlDefault字段这个字段的值就是封面图的高清地址。第二种是多图模式imageList里有多个元素第一张图通常就是封面。每张图片的urlDefault字段就是对应的原图地址。但这里有一个非常折磨人的坑返回的图片URL是经过安全处理的短链直接requests下载可能返回403。这是因为图片的访问还需要带上Referer头而且必须是https://www.xiaohongshu.com/这个来源。不加Referer的话CDN会认为你是盗链直接拒绝。正确的图片下载方式是这样的def download_image(img_url, save_path): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.xiaohongshu.com/ } resp requests.get(img_url, headersheaders, timeout15) if resp.status_code 200: with open(save_path, wb) as f: f.write(resp.content) print(f图片已保存: {save_path}) else: print(f图片下载失败: {resp.status_code})另外一个常见问题是图片URL里的!和?等符号可能被转义。有些教程会让你对URL做urllib.parse.unquote解码但实际上小红书返回的URL本身是完整的不需要额外解码直接请求即可。如果遇到下载失败先检查Referer再检查URL是否被控制台输出时截断。3.3 提取标题与文案JSON字段的层级要摸清标题和文案的位置在JSON里相对固定但结构嵌套比较深。以我实际遇到的数据结构为例{ data: { items: [ { id: note_id, type: normal, title: 笔记标题, desc: 笔记正文内容, user: { nickname: 作者昵称 }, imageList: [ { urlDefault: https://sns-img-qc.xhscdn.com/xxx } ] } ] } }注意不同的接口版本返回的层级不一样。有的版本直接在data下就有title字段有的版本需要先取data.items[0]。这就是为什么我强调不要死记硬背字段路径要学会动态调试。我用一个通用提取函数来规避这个问题def extract_note_info(json_data): data json_data.get(data, {}) items data.get(items, []) if not items: return None note items[0] return { title: note.get(title, ), desc: note.get(desc, ), images: [img.get(urlDefault, ) for img in note.get(imageList, [])], nickname: note.get(user, {}).get(nickname, ) }提取字段的时候顺便加了一层items判断避免因接口返回空列表而直接抛IndexError。这种防御式写法在爬虫代码里非常实用因为对方的接口随时可能调整结构你的代码不能因为一个字段缺失就崩溃。4. 核心实战完整爬取单篇小红书图文的落地代码前面几节把关键原理拆开了现在我们把它们组装成一个完整的、能落地的脚本。这个脚本做的事情很纯粹输入一个笔记链接自动提取笔记ID请求接口解析JSON下载所有图片保存标题和文案到文本文件。import re import os import json import requests from urllib.parse import urlparse # ---------- 配置区 ---------- COOKIE_STR 这里粘贴你浏览器里的完整Cookie UA_STR Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 SAVE_DIR xhs_notes # --------------------------- def extract_note_id(url): 从笔记URL中提取笔记ID # 支持的URL格式: # https://www.xiaohongshu.com/explore/{note_id} # https://www.xiaohongshu.com/discovery/item/{note_id} match re.search(r/(?:explore|discovery/item)/([0-9a-zA-Z]), url) if match: return match.group(1) raise ValueError(f无法从URL中提取笔记ID: {url}) def fetch_note_json(note_id): url fhttps://www.xiaohongshu.com/fe_api/burdock/web/v2/note/{note_id} headers { User-Agent: UA_STR, Cookie: COOKIE_STR, Referer: fhttps://www.xiaohongshu.com/explore/{note_id}, Accept: application/json, text/plain, */* } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp.json() def extract_note_info(json_data): data json_data.get(data, {}) # 兼容两种常见的数据层级 items data.get(items) or data.get(note) or [] if isinstance(items, dict): items [items] if not items: return None note items[0] return { title: note.get(title, ), desc: note.get(desc, ), images: [img.get(urlDefault, ) for img in note.get(imageList, [])], nickname: note.get(user, {}).get(nickname, ) } def download_image(img_url, save_path): headers { User-Agent: UA_STR, Referer: https://www.xiaohongshu.com/ } resp requests.get(img_url, headersheaders, timeout15) if resp.status_code 200: with open(save_path, wb) as f: f.write(resp.content) return True return False def save_note(note_id, note_info): # 创建以笔记ID命名的文件夹 note_dir os.path.join(SAVE_DIR, note_id) os.makedirs(note_dir, exist_okTrue) # 保存标题和文案到文本文件 text_content f标题: {note_info[title]}\n\n作者: {note_info[nickname]}\n\n正文:\n{note_info[desc]}\n with open(os.path.join(note_dir, note.txt), w, encodingutf-8) as f: f.write(text_content) # 下载所有图片 for idx, img_url in enumerate(note_info[images]): ext .jpg save_path os.path.join(note_dir, fimage_{idx 1}{ext}) if download_image(img_url, save_path): print(f第 {idx1} 张图片下载成功) else: print(f第 {idx1} 张图片下载失败: {img_url}) print(f笔记已保存到目录: {note_dir}) def main(): # 测试用的笔记链接替换成你自己的 note_url https://www.xiaohongshu.com/explore/你的笔记ID try: note_id extract_note_id(note_url) print(f笔记ID: {note_id}) json_data fetch_note_json(note_id) note_info extract_note_info(json_data) if note_info is None: print(未获取到笔记信息可能是笔记ID无效或Cookie已过期) return print(f标题: {note_info[title]}) print(f图片数量: {len(note_info[images])}) save_note(note_id, note_info) except Exception as e: print(f发生错误: {e}) if __name__ __main__: main()这个脚本的运行过程很简单把note_url替换成真实的笔记链接把COOKIE_STR替换成你浏览器里复制的Cookie然后运行python xhs_spider.py脚本会依次执行提取笔记ID → 请求接口 → 解析JSON → 创建文件夹 → 保存文本 → 下载图片。整个过程在终端里都有输出你可以清楚地看到每一步的执行状态。4.1 代码设计的几个关键细节为什么用re.search而不是urlparse提取笔记ID因为小红书的URL格式在不同入口下不一样/explore/和/discovery/item/两种路径都常见。用正则表达式统一匹配这两种模式比繁琐的路径判断更简洁可靠。为什么图片扩展名直接写死.jpg小红书笔记图片基本都是JPG格式个别动图是GIF。如果你发现下载的图片打不开先检查一下文件头是不是JFIF格式JPG的标准文件头。想更严谨的话可以用imghdr库自动检测图片真实格式再决定扩展名。为什么Cookie要放到配置区单独维护因为Cookie会过期。把Cookie放在脚本顶部过期后只需替换一处不用在代码里到处找。这是爬虫脚本的基本素养——把易变参数和核心逻辑分离。4.2 如何验证下载的数据完整性跑完脚本后别急着庆祝先做两件事验证数据完整性。第一打开note.txt检查标题和文案是否完整有没有乱码。如果出现乱码八成是编码问题确认你用utf-8编码写入的如果文案被截断说明接口只返回了摘要字段需要检查JSON里是否有desc的完整版本。第二打开下载的图片逐张确认能否正常预览。如果某张图片显示无法打开删掉重新下载大概率是网络超时导致的文件不完整。你可以在download_image函数里增加一个文件大小校验比如小于10KB的图片文件基本可以判定为下载异常直接重试。5. 反爬机制深度解析为什么你的代码会突然失效5.1 签名参数x-s与x-t的工作原理很多人在这一步卡住拿着上一节代码去跑第一次可能还能拿到数据第二次就被拦截返回403或者一个空的JSON结构。问题出在小红书接口的签名校验机制上。浏览器请求接口时会在请求头里自动带上两个特殊参数x-s和x-t。x-t是一个时间戳x-s是根据请求路径、请求体、时间戳等参数通过特定算法生成的签名。服务端收到请求后会校验这两个参数签名是否正确、时间戳是否在允许的误差范围内。校验不通过直接拒绝。这意味着你通过requests构造的请求如果没有这两个签名参数或者签名是过期算法生成的就会被服务端识别为无效请求。我在2.2节的代码能跑通是因为我手动分析请求头时把x-s和x-t也一并复制过来写死在代码里了。但问题在于签名是有时效性的x-t时间戳过期后x-s自然也就失效了。所以你会看到网上很多教程声称已破解签名算法但过不了几天就有人评论说又失效了。这不是教程作者骗你而是小红书的签名算法每隔一段时间就会更新一次旧算法生成的签名在新规则下就是垃圾数据。5.2 频率控制与IP风控除了签名校验小红书还有一套频率控制策略。同一个账号短时间内的请求次数、同一个IP段内不同账号的请求频率都会被记录和分析。触发风控后轻则要求输入验证码重则临时封禁账号的接口权限甚至限制登录。我实测下来比较稳妥的频率是单账号每5-10秒最多请求一次。别贪快你不是在做一个高并发的采集系统而是在做技术验证。5.3 合规边界什么能爬什么不能爬讲完了反爬我必须认真聊聊合规问题。爬虫这个技术本身是中性的但使用不当就会踩法律红线。根据我这些年的经验有两条清晰的边界第一公开信息的采集要控制频率和规模。如果你只是偶尔手动备份自己的收藏笔记或者分析几篇竞品笔记这种低频、小规模的数据获取属于合理使用范围。但如果你想批量采集全站数据比如几百万篇笔记打包卖给别人那就严重侵犯了平台的合法权益也可能涉及个人信息保护问题。第二绕过技术保护措施本身就有风险。签名参数x-s本质上就是平台设置的技术保护措施。破解它的目的如果是大规模采集那就是在主动规避对方的保护机制。这类行为在司法实践中有过不少判例平台起诉爬虫方的胜诉率相当高。所以我在这篇文章里的态度是把技术原理讲透让你理解爬虫和反爬的对抗逻辑但实际操作请局限在少量、公开、非商业的范围内。比如爬自己发布的笔记做数据备份或者爬几篇公开笔记用来学习解析技术。5.4 继续深入的方向签名算法与JS逆向如果你想在合规的前提下继续深造我建议把精力投入到JS逆向这个方向。原理很简单签名参数是前端JS代码生成的那浏览器在发起请求前一定执行了某个JS函数来算出x-s。打开开发者工具的Sources面板搜索x-s这个关键词你能找到生成签名的JS文件。阅读这个文件搞清楚加密逻辑然后用Python的execjs库去执行同一段JS代码就能动态生成有效的签名参数。但这条路非常深涉及JavaScript代码混淆、补环境、Hook等技术栈不是一篇文章能讲完的。我的建议是先把这篇文章的requests方案跑通对爬虫流程有完整的体感再决定要不要深入JS逆向。6. 高频踩坑实录这些问题你八成也会遇到6.1 请求状态码200但返回的数据是空的这是最坑的问题之一因为状态码200不代表你拿到了有效数据。小红书的接口在风控触发时有时不会返回403错误状态码而是返回200和一个空的JSON结构比如{code: -1, msg: 操作频繁, data: null}如果你只看状态码就会误以为请求成功了然后发现解析不到任何字段。我的排查习惯是先打印原始响应文本而不是直接.json()解析。把resp.text打出来看如果出现操作频繁或者验证码字样就是触发风控了需要降低请求频率或者更换Cookie。6.2 图片下载后无法打开这个问题通常有两个原因。第一是文件不完整网络超时导致下载到一半中断。第二是图片URL解析错误你拿到的可能是缩略图地址而不是原图地址。小红书接口里图片URL有好几个字段urlDefault是原图地址urlPre是处理过的地址thumbnail开头的是缩略图。一定要优先使用urlDefault。如果你拿到的图片分辨率很低很可能就是用了缩略图字段。6.3 笔记ID提取失败我见过有人直接从分享链接里复制笔记链接但小红书的分享链接是短链接格式比如http://xhslink.com/xxxxx这种链接里没有笔记ID需要先请求一次获取重定向后的真实URL再提取ID。处理方式很简单用requests的allow_redirects参数获取重定向后的最终URLresp requests.get(share_url, allow_redirectsTrue, timeout10) final_url resp.url然后用extract_note_id(final_url)提取ID即可。6.4 Cookie过期时间不固定小红书的Cookie有效期没有明确的官方说明。我实测下来少则几小时多则几天整体上没有规律。这跟你的登录环境、操作频率都有关系。应对策略是写一个简单的Cookie检查函数请求接口前先发一个探针请求如果返回的JSON里没有data字段就提示Cookie可能已过期。把这些检查逻辑嵌入到请求函数里就不需要每次手动调试了。7. 效率升级从单篇到批量采集的架构演进思路跑通单篇笔记的爬取后你会很自然地想到一个问题能不能给一串笔记ID列表批量爬取可以但批量采集的架构设计和单篇采集有本质区别核心要处理三件事。第一请求调度。不能写一个简单的for循环然后time.sleep(5)就完事。这样虽然能用但效率太低。更好的方式是设计一个请求队列控制并发数比如用ThreadPoolExecutor开3-5个线程每个线程独立处理一个笔记ID。不过要提醒你并发数越大触发风控的概率越高。我实测下来3个并发配合5秒间隔是比较安全的上限。第二失败重试机制。网络请求不可能100%成功。单个笔记请求失败后应该先记录失败原因然后把笔记ID放回重试队列。连续重试超过3次还是失败就把这个ID写到日志文件里最后统一处理。第三数据存储。笔记量少的时候直接保存成JSON文件或者CSV就行。但当你要采集几千篇笔记时文件系统的目录结构会变得混乱查询和去重都很麻烦。这时候就该引入数据库了——SQLite是零配置的选择MySQL适合更复杂的查询场景。我建议学一下SQLite几行代码就能建表、插入、查询个人项目完全够用。批量采集的逻辑并不复杂但工程化落地涉及的主题非常多。这篇文章先点到为止等你把单篇爬取的原理消化透彻再来谈架构升级会顺理成章得多。8. 写给自己学习爬虫最值得投入的三个方向最后聊点代码之外的东西。爬虫这个领域入门容易精通难。作为一个在这条路上摸爬滚打多年的人我梳理了三个阶段最值得投入精力的方向供你参考。第一个方向是请求与解析的深刻理解。不要满足于requests.get然后json.loads。遇到一个陌生网站你能不能迅速找到它的数据接口能不能从HTML、JS、CSS里定位到真正有价值的信息这需要大量的阅读网络请求、分析响应结构的练习没有捷径。第二个方向是反爬对抗的思想。市面上反爬手段五花八门签名校验、指纹识别、字体反爬、验证码、IP封禁……但底层逻辑都是让爬虫工程师付出远超数据价值的成本。你不需要每一种反爬手段都能破解但至少要能识别出来知道对面用的是哪一招。这决定了你是选择绕过、模拟、还是换一条数据获取路径。第三个方向是工程化能力。爬虫最终要落地产出数据就离不开采集调度、数据清洗、增量更新、异常处理这些工程问题。很多只写脚本的人一到数据量大了就崩就是缺了工程化这一环。根据自己的目标选择方向然后持续深耕。技术栈会过时但解决问题的思路和方法论永远是你的核心竞争力。如果你跟着这篇文章把流程跑通了恭喜你你已经掌握了爬虫技术的核心骨架。之后的每一段爬虫代码无论目标站点是什么都只是在用不同的方式回答同样一个问题数据藏在哪里怎么把它拿下来。带着这个能力去自由探索吧。