ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

小红书爬虫实战:X-S签名与接口请求构造全解析

小红书爬虫实战:X-S签名与接口请求构造全解析 简介这是一份面向高校计算机相关专业如人工智能、通信工程、电子信息等学生的小红书数据爬虫课程实训资源覆盖笔记详情、用户主页、关键词搜索三类采集场景适合用于课程设计、毕业设计或爬虫方向入门进阶练习也适合教师作为教学案例参考。资源共包含4个文件一个Python爬虫主脚本负责请求构造、页面解析与数据提取逻辑完整两个CSV结果文件分别存储抓取到的笔记数据和用户信息便于直接查看、统计与二次分析一份Markdown详细说明文档系统梳理环境搭建、运行流程、关键代码解读与常见问题辅助快速上手。压缩包整体仅5KB体量轻巧、结构清晰适合快速部署。该资源已有63人学习使用代码经测试可稳定运行既可直接用于实训作业复现也可在此基础上扩展搜索策略、数据存储或可视化展示功能是一份理解小红书数据采集链路与爬虫工程细节的实用参考。1. 为什么小红书爬虫的难点不在解析而在请求签名很多 Python 课程实训卡在同一个位置requests 能打开首页却拿不到搜索接口的数据。小红书这类前端渲染的站点真正返回内容和用户数据的是 JSON 接口而这些接口的请求头里必须带一个X-S签名。你写爬虫作业写得再熟练签名算法不对得到的只会是空列表或者验证码页面。这份实训资源的核心价值就在这xhs.py里把搜索、笔记、主页三条链路串起来了且附带了两个带时间戳的 CSV 结果文件和一份详细说明文档能够作为课程设计、期末作业甚至毕业设计的完整起点。对已经工作的人与其从头抓包不如直接借这个项目拆一遍它的签名逻辑、解析策略和落盘方式省掉大量踩坑时间。2. 小红书笔记、主页、搜索接口的请求构造与 X-S 签名逻辑2.1 接口清单与请求参数项目里最核心的三个接口分别是搜索笔记、笔记详情和用户主页。常见实现会直接请求 Web 端的/api/sns/web/v1/系列路径而不是 App 端。选择 Web 接口的原因很简单抓包方便、Cookie 容易获取、返回结构相对稳定。App 端虽然有更多字段但签名和加密链路复杂得多对课程实训来说成本不值得。我在阅读xhs.py时会把接口和参数先整理成一张表这样后续看代码思路更清晰接口路径用途核心参数返回要点/api/sns/web/v1/search/notes按关键词搜索笔记keyword、page、page_sizedata.items中为笔记列表/api/sns/web/v1/note/feed获取单条笔记详情note_id标题、正文、点赞、评论数/api/sns/web/v1/user_posted获取用户主页发布的笔记user_id、cursor用户笔记列表和分页游标以搜索接口为例构造请求参数的代码往往长这样import time def build_search_payload(keyword: str, page: int, page_size: int 20) - dict: payload { keyword: keyword, page: page, page_size: page_size, search_id: v2_ str(int(time.time() * 1000)) } return payload这里有几个参数需要重点理解。keyword决定搜索主题page用于翻页page_size一般不超过 20调大反而容易被限制。search_id看起来像随机值实际上代表一次搜索行为的会话标识后端会用它做行为校验。若你在改写代码时把这个参数写死连续翻页到第 3 页后大概率触发风控返回空data或验证码。所以每次构造请求时我都建议动态生成search_id就像上面代码那样用毫秒时间戳拼一个唯一值。2.2 X-S 签名计算与失效边界X-S是整个项目最容易被忽视、又最容易出错的地方。在小红书的 Web 接口里请求头和请求体都会被纳入签名范围常见做法是把接口路径、查询参数以及一段固定字符串拼接后计算哈希。xhs.py中的处理逻辑经过简化后思路类似这样import hashlib def make_x_s(path: str, query: str) - str: sign_str path query xhs return hashlib.md5(sign_str.encode(utf-8)).hexdigest()上面只是演示拼接规律真实代码里的混淆表和拼接顺序更复杂还会加入时间戳或 Cookie 里的部分字段。在阅读这个项目时你不需要完全照搬它的混淆表但需要理解X-S的失效边界。最常见的失效场景有三个第一请求参数顺序变化导致后端计算出的签名不一致第二请求路径与签名时使用的路径不匹配比如访问/note/feed却用/note/detail参与签名第三时间窗口过期部分签名实现会包含当前时间爬虫挂太久后首次请求就会失败。判断签名有没有失效通常看响应里code是否为461或者返回的data是否为空列表。如果出现这两种情况不要急着改解析代码先重新生成签名。2.3 请求头、Cookie 与访问节奏接口签名之外请求头同样影响成功率。项目里的 Session 对象会统一维护 Cookie 和 User-Agent避免每次请求都重建状态。常见写法是把这些配置集中放在初始化阶段import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Referer: https://www.xiaohongshu.com/, })这份代码里最需要注意的是Referer。很多刚开始写爬虫的人只设置 User-Agent忽略 Referer结果搜索接口的响应能正常返回但笔记详情接口返回 403。原因是详情页会校验请求来源页面没有Referer直接访问会被当作非法请求。Cookie 的使用则要谨慎项目里读取账号登录后的 Cookie 能拿到更完整的数据比如评论内容和用户粉丝数没有登录态时只能拿到部分公开信息搜索接口甚至可能返回空数据。针对课程实训场景我一般建议先在浏览器里登录一次小红书再把 Cookie 复制到代码配置文件中这样能让整个采集流程更稳定又不需要处理扫码登录逻辑。访问节奏上代码里会在两次请求之间加入随机延迟避免同一个 Session 短时间连续请求造成风控。3. 从搜索到主页的爬虫链路xhs.py 的模块划分与数据落盘3.1 三个入口函数与调用方式项目的xhs.py在结构上把功能拆成三个入口搜索笔记、笔记详情、用户主页。这样做的好处是课程实训讲起来好分层期末答辩时也能说清楚“搜索 → 笔记 → 用户”的数据流转。类设计的常见形式是class XiaoHongShuCrawler: def __init__(self, session: requests.Session): self.session session def search_notes(self, keyword: str, max_page: int 5) - list: pass def fetch_note_detail(self, note_id: str) - dict: pass def fetch_user_profile(self, user_id: str) - dict: pass三个函数的职责边界非常明显search_notes负责从关键词拿到笔记 ID 列表fetch_note_detail负责取单条笔记的具体信息fetch_user_profile负责通过笔记作者 ID 深挖用户数据。入口函数拆分得越清楚后面做并发或者增量爬取时就越省事。实际调用顺序是先从搜索接口拿到笔记 ID 和作者 ID再依次调用详情和用户主页接口。这样设计下即使某个用户数据为空也不影响笔记数据的落盘。3.2 搜索笔记的解析与字段抽取搜索接口返回的 JSON 结构嵌套层级比较深如果不做容错随便一个字段缺失都会让整个程序崩溃。我在项目里看到的解析逻辑会把“字段存在性判断”和“数据提取”分开处理。下面的代码演示了如何从搜索响应中抽取笔记字段def parse_search_items(resp_json: dict) - list: rows [] items resp_json.get(data, {}).get(items, []) for item in items: if item.get(model_type) ! note: continue note item.get(model, {}) interact note.get(interact_info, {}) user note.get(user, {}) rows.append({ note_id: note.get(id, ), title: note.get(display_title, ).strip(), liked_count: interact.get(liked_count, 0), comment_count: interact.get(comment_count, 0), user_id: user.get(user_id, ), }) return rows这段代码的精髓在get()方法和默认值。接口返回的items中可能混入广告推荐位model_type不是note的内容会被直接跳过。取interact_info和user时用空字典兜底即便字段缺失也只是得到 0 或空字符串不会让程序中断。写完解析函数后需要把数据落盘。项目输出的 CSV 文件名带时间戳写入格式用的是utf-8-sigimport csv CSV_FIELDS [note_id, title, liked_count, comment_count, user_id] with open(xhs_notes.csv, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesCSV_FIELDS) writer.writerows(rows)utf-8-sig是为了在 Excel 里正常显示中文如果写成普通utf-8用 Office 打开时会出现中文乱码。这个细节在课程实训报告里可以作为“踩坑记录”写进去能体现你对数据落盘的考虑。3.3 用户主页深挖从笔记作者到用户数据“深度挖掘用户数据”这一块项目是通过fetch_user_profile实现的。拿到笔记中的user_id后再请求用户主页接口解析出粉丝数、笔记数、昵称、位置等信息。示例代码如下def parse_user_profile(resp_json: dict) - dict: basic resp_json.get(data, {}).get(basic_info, {}) return { user_id: basic.get(user_id, ), nickname: basic.get(nickname, ), followers: basic.get(followers, 0), following: basic.get(following, 0), location: basic.get(ip_location, ), }这里的ip_location字段通常是用户发布笔记时的 IP 属地不需要额外登录就能拿到。把笔记 CSV 和用户 CSV 通过user_id关联就能构成一张简单的用户维度表哪些用户发的内容点赞高、哪些用户粉丝多但互动少。对课程作业来说这已经足够展示“数据挖掘”的完整思路。需要注意的是用户主页接口的cursor分页参数它不像传统页数那样从 1 递增而是由上一页响应末尾返回。因此在循环采集时要判断has_more字段避免死循环或者遗漏第二页之后的笔记。4. 并发设计、代理切换与请求频率的平衡4.1 ThreadPoolExecutor 多线程采集如果只是单线程跑搜索一个关键词翻 5 页要等十几秒用户能接受。但要把笔记详情、用户主页也拉一遍几条笔记串行请求可能要一分钟这时候就需要并发。xhs.py里比较适合课程实训的并发方案是基于concurrent.futures的线程池。原因很直接项目用的是requests同步请求换asyncio要重写整个网络层而线程池只需要在调用处包一层from concurrent.futures import ThreadPoolExecutor, as_completed def batch_fetch(crawler: XiaoHongShuCrawler, note_ids: list[str], workers: int 6): results [] with ThreadPoolExecutor(max_workersworkers) as pool: futures { pool.submit(crawler.fetch_note_detail, note_id): note_id for note_id in note_ids } for future in as_completed(futures): results.append(future.result()) return results线程数定为多少才合适这是初学阶段最容易纠结的问题。并发设计并没有标准答案关键在于目标站的限流阈值。小红书 Web 端同一 Cookie 下短时间并发过高会直接触发验证码。我在课程作业中一般把workers控制在 6 到 8太大不仅不会提速反而会让整个 Session 被封。另一点需要注意的是future.result()的错误处理。当前代码里如果某个note_id请求失败异常会直接中断整个批次。更稳妥的做法是捕获异常并记录失败的note_id跑完后再单独重试。4.2 代理池切换与重试机制课程实训环境里经常出现请求一段时间后突然全部超时说明本机 IP 已被临时限制。此时要么停止等待要么用代理池切换出口 IP。项目的代理模块可以写成这样import random PROXY_POOL [ http://127.0.0.1:7890, http://user:pass192.168.1.10:8080, ] def get_random_proxy() - dict: proxy random.choice(PROXY_POOL) return {http: proxy, https: proxy}这里有个关键点http和https都需要设置同一个代理地址否则部分接口会走直连导致 IP 不一致反而增加风控概率。代理的选取要考虑目标接口的可用性公开免费代理只能用来临时演示稳定运行还是需要付费代理池或者自己维护代理 IP。在课程设计报告里你可以写清楚“代理池解决了 IP 维度风控并不能解决签名维度风控”这样显得思路专业。同时在请求外层加重试机制更容易出效果def request_with_retry(session: requests.Session, url: str, max_retries: int 3): for attempt in range(max_retries): try: response session.get(url, timeout10) if response.status_code 200: return response except requests.exceptions.ConnectionError: time.sleep(2 * (attempt 1)) raise RuntimeError(f请求失败: {url})重试间隔采用2 * (attempt 1)的线性退避第一次失败等 2 秒第二次失败等 4 秒避免立即重试造成二次封禁。注意这里不能捕获所有异常后直接continue如果响应返回的是 200 但 JSON 里code表示风控那也需要继续重试。简单的做法是检查response.json().get(code)是否等于 0不等于 0 就抛异常或重新生成签名。4.3 去重、限速与断点并发和代理解决了速度问题但重复数据问题同样需要处理。资源里已经包含xhs_notes_2022-01-20_19_49_51.csv和xhs_users_2022-01-20_15_43_58.csv可见采集时会按批次生成新文件。为了避免下次重复采集常见做法是在内存中维护一个已经处理过的note_id集合def deduplicate(note_ids: list[str], seen: set[str]) - list[str]: return [nid for nid in note_ids if nid not in seen]去重之后的限速同样不可省略。即使有代理池单 IP 每秒请求数依然有上限。我一般会在循环内部加入time.sleep(random.uniform(1, 3))让请求间隔不均匀分布。过于规律的 2 秒间隔反而容易被流量分析识别。断点续爬可以通过把seen集合持久化到本地文件来实现例如每次成功后追加写入processed_ids.txt下次启动时读取回来。这样即使跑到一半程序崩溃也不会从头再爬。5. 用 CSV 与说明文档还原数据链路验证结果和常见报错5.1 两个 CSV 的字段含义和文档价值项目附带的两份 CSV 是理解整套流程最直观的入口。xhs_notes_2022-01-20_19_49_51.csv存储笔记维度数据xhs_users_2022-01-20_15_43_58.csv存储用户维度数据。文件名中的日期时间代表采集批次而不是数据截止时间。在分析结果时要把两个文件通过user_id进行关联。字段对应关系如下CSV 文件字段含义xhs_notesnote_id笔记唯一 IDxhs_notestitle笔记标题xhs_notesliked_count点赞数xhs_notescomment_count评论数xhs_usersuser_id用户 IDxhs_usersfollowers粉丝数xhs_usersnotes发布笔记数详细说明.md文档里一般会记录运行环境和执行步骤包括 Python 版本、依赖库列表、Cookie 配置方式和输出文件说明。拿到项目后先打开文档确认依赖是requests还是额外安装了pandas再决定是否需要创建虚拟环境。如果文档里的运行命令和你当前系统不兼容优先看 CSV 文件是否完整以判断自己运行成功后应该产出什么样的数据格式。5.2 实训运行中常见的三类报错课程实训里最常出现的报错集中在签名、字段解析和编码三层。第一类是签名失效表现是搜索接口返回正常状态码但data为空或者直接出现461状态码。处理方式是重新登录获取新 Cookie并确认当前时间与系统时间误差不超过 1 分钟。第二类是KeyError或TypeError多出现在返回内容变成验证码页面时。此时打印响应内容会发现结构完全变了不再是正常 JSON。不要执着于修复解析代码而应该回退到签名和请求头检查。第三类是 CSV 中文乱码原因是没有用utf-8-sig写入或者用 Excel 打开了非 UTF-8 文件。上述问题都解决后可以用wc -l查看输出行数是否符合预期。5.3 一个实用技巧增量采集与结果合并如果直接用 pandas 读取旧 CSV然后去重新采集增量数据最方便的做法是把旧文件的note_id加载到set中新增数据只在不在集合中时才写入。下面的代码可以作为课程设计里的“项目改进”部分import pandas as pd CSV_PATH xhs_notes.csv def load_seen_ids(path: str CSV_PATH) - set[str]: try: df pd.read_csv(path, dtype{note_id: str}) return set(df[note_id].dropna()) except FileNotFoundError: return set() def save_new_rows(new_rows: list[dict], seen: set[str]) - int: added [row for row in new_rows if row[note_id] not in seen] if added: df_new pd.DataFrame(added) df_new.to_csv( CSV_PATH, modea, headernot pd.io.common.file_exists(CSV_PATH), indexFalse, encodingutf-8-sig ) seen.update(row[note_id] for row in added) return len(added)利用这个增量函数后续可以把fetch_note_detail的结果分批传进来。由于load_seen_ids只读取一次运行时内存占用很小适合给成百上千条笔记做去重。每次运行结束后可以再对 CSV 做一次drop_duplicates校验确保批量写入过程中没有重复追加。最后将这个增量逻辑配合ThreadPoolExecutor并把max_workers设置在 8 左右请求间隔改为random.uniform(1, 3)就能让整个xhs.py在较长时间内稳定运行不会因为重复数据或请求过快而中断。本文还有配套的精品资源点击获取
返回列表