小红书爬虫实战:笔记、主页、搜索三合一接口与签名实现
简介面向小红书平台的爬虫项目适合有毕业设计、大作业或数据收集需求的开发者。项目完整实现了笔记详情、用户主页与关键词搜索三类采集场景覆盖从URL收集、请求网页到内容解析、数据存储的完整链路也包含遵守robots协议及应对反爬机制的常用策略。资源共1079个文件压缩包约2.7MB以JavaScript/TypeScript脚本为主781个js文件、68个ts文件同时包含Python脚本、JSON配置、Markdown文档等代码结构清晰便于阅读、调试与二次开发。目前已有874人学习下载常用于课程设计或课题研究。包内附有说明文档、环境配置示例、核心抓取模块及演示素材能帮助读者快速理解爬虫工程化实现也可作为学习Web爬虫开发、数据采集与存储的实战参考。1. 小红书爬虫的价值与难点笔记、主页、搜索为什么必须一起做小红书的信息密度比多数内容平台高笔记正文、封面图、用户主页、搜索推荐数据每一类都有独立的接口和独立的参数要求。做小红书爬虫项目的核心矛盾不在会不会 requests而在签名生成、会话保持和多接口联动。正文按笔记、主页、搜索三条链路拆开讲最后落到频率控制和数据落库整个工程在本地就能跑起来。小红书的 Web 端反爬围绕“签名 Cookie 行为检测”展开x-s 签名是绕不过去的坎。本文默认你具备基础 Python 爬虫能力能操作浏览器开发者工具抓包。如果这些还没掌握建议先补一下 requests 和 Network 面板的基本用法再回来。2. 小红书爬虫的技术底座接口路径、签名参数与会话维持2.1 Web 端接口布局feed、user_posted 与 search小红书 Web 端的数据请求集中在 /api/sns/web/v1/ 下抓包时能看到三个高频接口接口路径作用核心参数/api/sns/web/v1/feed笔记详情source_note_id/api/sns/web/v1/user_posted用户发布笔记列表user_id、cursor/api/sns/web/v1/search/notes搜索笔记keyword、page笔记详情返回标题、正文、图片列表、互动数据用户主页接口返回该作者发布的所有笔记用游标翻页搜索接口返回关键词命中结果的摘要列表。这里有一个关键认知搜索和主页拿到的都只是笔记 ID要拿完整正文最后都要回源到 feed 接口再补一次请求。所以工程里至少三个函数分别对应这三条链路一个公共的签名函数给三条链路共用。2.2 x-s 与 x-t签名参数的最小实现小程序端和 Web 端共用一套 x-s 签名机制。它在线上是混淆过的动态调试能看到本质是对请求路径和参数排序后做一次 MD5再拼上前缀。最小实现长这样import hashlib import time def generate_xs(path: str, params: dict) - str: query .join([f{k}{v} for k, v in params.items()]) raw f{path}?{query} return x3_ hashlib.md5(raw.encode()).hexdigest() def generate_xt() - str: return str(int(time.time() * 1000))网上流传的版本多数加了 salt 和二次混淆我这边习惯在本地用 Node 执行从 Web 端抽出来的原始签名函数Python 侧通过子进程或本地 HTTP 接口调用。圈里常说的“小红书逆向”主要逆的就是这段逻辑。参数顺序也有要求query 里各字段的排列顺序必须和浏览器请求保持一致否则同样的值会算出不同的摘要。2.3 Cookie 与会话参数表登录后抄下来的 Cookie 里a1 和 web_session 是核心字段。没有这两个字段签名再正确也会被拒。请求头还需要 x-b3-traceid、x-mini-gid 这类链路追踪字段它们由服务端下发也可以自己生成不影响签名结果。关键字段对照请求头来源缺失后果Cookie含 a1、web_session浏览器登录后复制返回 code 4000 会话失效x-s每次请求动态生成返回 code 461 签名错误x-t每次请求动态生成与 x-s 不匹配报错User-Agent浏览器复制命中 UA 风控懒得每次手抄 Cookie 的话把 Cookie 存成文本文件程序启动时读取并注入 headers这就算完成会话初始化了。3. 把小红书爬虫跑起来的前 10 分钟环境、依赖与登录态3.1 依赖安装与工程目录第一步先把目录和依赖建好命令直接抄mkdir xhs_crawler cd xhs_crawler python -m venv venv source venv/bin/activate pip install requests pandas richrequests 负责发送 HTTP 请求pandas 用来最后落数据表rich 是可选依赖主要用于在终端里格式化打印返回结果。venv 不是必须的但当你后续要装 Node 签名服务时虚拟环境能避免依赖冲突。3.2 手工获取登录 Cookie 的操作步骤按下面的顺序操作最快能在一分钟内拿到全部 Cookie打开 Chrome 无痕窗口访问 xiaohongshu.com。用手机 App 扫码登录。按 F12 切到开发者工具刷新页面。Network 面板里找到 feed 或 search 开头的 XHR 请求。在 Headers 面板里找到 Cookie 字段整段复制到 cookie.txt。注意复制的过程中不要切账号、不要退出登录否则 Cookie 会失效。判断 Cookie 是否还活着有个简单办法请求任意接口时返回 code 为 4000 的 JSON基本可以确定需要重新登录。3.3 最小可运行的请求骨架下面是工程里会反复用的基础请求函数假设第 2.2 节的签名函数已经保存在工程的 sign.py 中import requests from pathlib import Path def load_cookie() - str: return Path(cookie.txt).read_text().strip() HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Origin: https://www.xiaohongshu.com, Referer: https://www.xiaohongshu.com/, } def get_json(path: str, params: dict) - dict: headers { **HEADERS, Cookie: load_cookie(), x-s: generate_xs(path, params), x-t: generate_xt(), } url fhttps://www.xiaohongshu.com{path} resp requests.get(url, headersheaders, paramsparams, timeout10) return resp.json()get_json 是接下来三个功能模块的公共入口。它接收接口路径和参数字典内部完成签名生成、请求头注入和 GET 请求发送。x-s 的生成方式见第 2.2 节如果你已经在本地跑起了 Node 签名服务函数内部改成调用那台服务即可对上层调用方没有影响。4. 笔记、主页、搜索三合一小红书爬虫的核心实现4.1 笔记正文抓取feed 接口怎么用feed 接口的输入是笔记 IDsource_note_id输出是完整笔记内容def fetch_note_detail(note_id: str) - dict: path /api/sns/web/v1/feed params { source_note_id: note_id, image_formats: jpg, } data get_json(path, params) item data[data][items][0][note_card] return { note_id: note_id, title: item[display_title], desc: item[desc], image_urls: [img[url] for img in item[image_list]], likes: item[interact_info][liked_count], comments: item[interact_info][comment_count], }返回的 JSON 结构里 items 是数组通常只有一个元素。note_card 是笔记卡片的完整字段display_title 是标题desc 是正文image_list 是图片数组。interact_info 里是互动数据。小红书图片下载的原图逻辑也在这层处理图片地址里通常带处理参数拿原图的常见做法是把 URL 中!号之后的部分去掉直接请求剩余部分得到近似原图。4.2 用户主页笔记列表抓取游标翻页怎么写用户主页接口返回该作者最近发布的笔记。翻页用的不是页码而是游标这和小红书的服务端分页方式一致def fetch_user_notes(user_id: str, cursor: str ) - tuple: path /api/sns/web/v1/user_posted params { user_id: user_id, cursor: cursor, num_per_page: 30, } data get_json(path, params) note_ids [item[note_id] for item in data[data][notes]] next_cursor data[data][cursor] return note_ids, next_cursor def fetch_all_user_notes(user_id: str) - list: all_ids [] cursor while True: ids, cursor fetch_user_notes(user_id, cursor) all_ids.extend(ids) if not cursor: break time.sleep(random.uniform(0.5, 1.5)) return all_idscursor 的语义是服务端下发的下一页起始位置。第一次请求传空字符串服务端返回第一页和下一页的游标之后每次把上一次的游标原样传回去直到返回值里的 cursor 为空说明没有更多数据了。num_per_page 可以调到 30这个值在 Web 端是前端写死的浏览器里默认就是这个批量。中途如果某页返回异常不要直接 break先把当前 cursor 记到临时文件下次从这里继续。4.3 搜索关键词抓取search/notes 与 feed 的回源配合搜索接口一次返回一页笔记摘要page 从 1 开始计数def search_notes(keyword: str, page: int 1) - list: path /api/sns/web/v1/search/notes params { keyword: keyword, page: page, page_size: 20, search_id: default, } data get_json(path, params) if items not in data[data]: return [] return [item[id] for item in data[data][items]]注意 search 接口和 user_posted 接口的返回值结构不同搜索返回的每一项在 items 列表中key 是 id 而不是 note_id。如果需要拿完整正文、点赞数这类字段要先把搜索到的 note_id 收集起来再调 feed 接口逐条补充。很多爬虫工程只做了搜索不补全文最后入库的数据缺字段又得重跑一遍。4.4 把三条链串成一条主流程最后用几行代码把上面几个函数串成完整任务def main(): keyword input(请输入搜索关键词) note_ids search_notes(keyword, page1) for nid in note_ids[:20]: detail fetch_note_detail(nid) print(detail[title], detail[likes])如果想爬某个作者的整个主页换成 user_id 入口调用 fetch_all_user_notes 拿全量笔记 ID之后同样逐条调 feed 补详情。到这里标题里的三个功能已经全部打通搜索、主页、笔记详情共用同一个签名函数和同一个会话。5. 小红书爬虫的进阶验证签名调试、断点续抓与关键词策略5.1 签名错误461与会话失效4000怎么定位线上跑的时候最常见的两类报错是 code 461 和 code 4000。461 是签名错误优先检查请求 path 和 params 是否与浏览器完全一致包括参数顺序。推荐把签名前的原始字符串打印出来和浏览器里看到的 queries 逐一比对。4000 是会话失效多半是 Cookie 过期或者触发了行为风控重试两次仍返回 4000直接让脚本退出并提示重新扫码。def probe(data: dict) - str: if data.get(code) 0: return ok if data.get(code) 461: return sign error if data.get(code) 4000: return cookie invalid return fcode {data.get(code)}这里的 code 是响应 JSON 顶层的业务码不是 HTTP 状态码。签名错误时 HTTP 状态可能还是 200别只看状态码判断。5.2 断点续抓与本地去重抓长列表的时候不要等全部任务跑完再落盘。每拿到一条笔记就 append 进 CSV同时把 note_id 记到本地集合文件。中途断了以后重跑先加载历史 ID已经存在的直接跳过省流量也省时间import json from pathlib import Path SEEN_FILE seen_ids.json def load_seen() - set: if Path(SEEN_FILE).exists(): return set(json.loads(Path(SEEN_FILE).read_text())) return set() def save_seen(seen: set) - None: Path(SEEN_FILE).write_text(json.dumps(list(seen)))处理少量数据时直接读写 JSON 文件够用数据超过一万条就换 sqlite 或 Redis 做去重。要扩展到分布式抓取只需要把 note_id 队列从内存搬到 Redis 的 list每个 Worker 从里面 LPOP 即可。5.3 关键词拆解代替深翻页搜索接口翻页到后面返回结果变稀疏且被风控的概率上升。更实用的做法是做关键词词表把一个宽泛词拆成若干长尾词分别跑前几页KEYWORDS [法式连衣裙, 通勤连衣裙, 碎花连衣裙, 针织连衣裙] for kw in KEYWORDS: ids search_notes(kw, page1) for nid in ids[:20]: try: detail fetch_note_detail(nid) append_record(detail) except RuntimeError as e: print(f失败 {nid}: {e}) time.sleep(30)最后加一步验证随机抽 30 条抓取结果打开小红书网页端手动对比标题、正文和图片张数。抓取结果与页面展示存在不一致时优先检查接口 params 字段缺失和数据层级解析是否对上了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →