小红书关键词搜索笔记列表数据采集助手_kkAI
Skill ID:
xhs-search-collector-kkai(ASCII 标识,便于工具链调用与路径处理)
显示名:小红书关键词搜索笔记列表数据采集助手_kkAI
1. 简介
输入一个小红书搜索关键词,调用站点动作 xhs-search 取回该关键词下的笔记列表,每条返回 12 项数据:
笔记ID · 用户名 · 笔记类型 · 发布时间 · 标题 · 简介 · 评论数 · 收藏数 · 点赞数 · 分享数 · 图片组 · 视频链接
适用于找选题、追热点、抓某话题下的爆款笔记、批量导出搜索结果、做竞品或内容分析等场景。
输入:关键词(必填)+ 可选筛选条件(时间范围 / 笔记类型 / 排序)。
试试这样使用
- 关键词:
AI生图 - 采集页数:
2 - 限定时间与类型:
AI生图+ 时间范围一周内+ 笔记类型视频笔记
# 采 2 页(用户示例)
python scripts/xhs_search_collect.py --keyword "AI生图" --pages 2
# 加筛选
python scripts/xhs_search_collect.py --keyword "AI生图" --note-time 一周内 --note-type 视频笔记 --pages 2
1.1 两条硬性约束(改动前必读)
约束一:每一页只请求一次;只有失败才补一次,且最多补一次。
每一页:
请求 1 次
若成功 -> 取回本页笔记 + 下一页凭据,结束该页(不补发)
若失败(网络/超时/业务失败)-> 等 2 秒后补 1 次,之后无论成败都不再发第 3 次
若失败且错误不可补发 -> 直接结束(重发也不会有不同结果,只会浪费额度)
不可补发的错误:unauthorized(apikey 失效 / 未带密钥)、insufficient_balance(余额不足)、
upstream_busy(上游临时故障/限流,服务端要求等 10 分钟)、
action_forbidden(动作名被站点网关拒绝,重发多少次都一样)
多个关键词(一次搜多个词)时追加一条:
某个关键词撞上上面那四类**账户/接口级**错误 -> 立刻中止整批,
剩余关键词进 skipped_keywords 字段、不再逐个去刷(它们的结局必然一样,纯属白花额度/白等)
由此得到的请求次数(--retry 默认 1,硬上限 1):
| 情况 | 请求次数 |
| --------------------- | ------------------------ |
| 默认只采 1 页(--pages 1) | 1 次 |
| 采 2 页(--pages 2) | 2 次 |
| 某页失败,补发一次后成功 | +1(该页 2 次) |
| 某页两次都失败(最坏) | 2 次(绝不第 3 次) |
| --retry 0 | 恒为 页数 次(完全不补) |
| 多个关键词各 1 页 | 关键词数 × 1 次 |
| 多关键词中途撞上账户级错误 | 只发到出错那个关键词为止,剩下的 0 次 |
永远不会出现第 3 次尝试;成功过的页不会被重跑;不做轮询、不做并发。
request_count / retry_count / max_requests_per_page 三个字段可直接核对「没有多余请求」。
约束二:等待时间为 0-600 秒,请耐心等待。
该接口是排队型服务,单次请求的返回耗时在 0-600 秒之间浮动,请求发出后长时间无响应属于正常现象。
默认等待上限 600 秒,可用 --timeout 调整(取值 0-600,0 表示不设超时、一直等到服务端返回)。
等待期间脚本每 15 秒向 stderr 打印一次心跳([等待中] …),不会因为"看起来卡住了"就中途重发。
1.2 接口实测要点(改脚本前必读)
| 要点 | 事实 | 脚本应对 |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 接口与参数 | POST https://u.ai-gc.art/mcp/rd.php?dr=xhs-search,body {"keyword", "noteTime", "noteType", "page", "sort", "search_id", "session_id"};请求头 Content-Type: application/json + Authorization: Bearer <apikey> | 参数名严格按站点文档;page 为页码(从 1 开始) |
| 动作名已确认有效 | 2026-10-02 实测返回业务态(code:402 / code:403 / code:200),从未出现 403 forbidden | 说明 xhs-search 已被网关接受;脚本按该动作名固定调用,无需试探 |
| 成功响应结构已实测 | 2026-10-02 关键词「AI知识卡片」取回 20 条。顶层键:code / tips / status_code / request_id / note_info / search_id / session_id / has_more / total_count | 笔记列表容器键 = note_info(已放到 NOTE_LIST_KEYS 首位);整树兜底保留 |
| 翻页凭据 | 站点返回 search_id + session_id 两项(形如 d3666d09f233451e@98816a3f22054399);⚠️ 不返回 cursor | 三种凭据仍都提取;cursor 为空时续采命令不带 --cursor |
| has_more 是字符串 | 实测 "1" / "0",不是布尔值;另有 total_count(存在但实测为 null,不可依赖)与 status_code(成功时 0,非 HTTP 码,不参与成败判定) | pick_has_more 兼容布尔 / 数字 / 字符串(true/1/yes ↔ false/0/no);total_count 空值安全,直接输出 null |
| 单页条数不固定 | 同一关键词两次请求分别返回 16 条 / 20 条 | 不按固定页大小预估额度,按响应实际条数统计 |
| 失败也是 HTTP 200 | 业务失败返回 HTTP 200,成败必须看 body 的 code / tips | 必须解析响应体,不能只看 HTTP 状态码 |
| 余额耗尽优先于一切 | {"code":402,"tips":"余额不足,请登录 u.ai-gc.art ,请充值后重试"}。⚠️ 欠费时连参数校验都不做:空 body 与错误参数名同样返回 402 | 单独识别为 insufficient_balance 且不补发;排查问题前先确认余额 |
| 403 + 裸 forbidden 与 apikey 无关 | dr 动作名不被站点网关接受时返回 HTTP 403 + 非 JSON 的裸文本 forbidden;实测有效密钥与无效密钥返回完全相同 | 单独识别为 action_forbidden,明确提示不要重新获取 apikey。注意与「200 + JSON code:403 缺少必要的keyword参数」(余额正常时的参数校验)区分 |
| 三种失败形态必须分清 | 403 + forbidden=动作名问题;200 + JSON code:403=apikey 无效或参数缺失;401 + missing api key=未带密钥 | 判定顺序:余额不足 → 动作被拒 → 鉴权失败 → 上游限流 → 其他业务失败 |
| 上游失败不补发 | 实测 {"code":404,"tips":"API 请求失败,请10分钟后,再重试"}(站点自身打不通小红书) | 归类 upstream_busy、retry_after_seconds=600、不补发,等 10 分钟后原命令重跑即可;已取到的结果无需重做 |
| 错误判定只看提示字段 | 搜索结果的标题/简介里出现「1分钟」「余额不足」「稍后再试」等字样不代表出错 | _error_blob() 只取 tips/msg/error 等提示字段,绝不把整包正文拼进判断串(同站点曾因此把整页成功数据误判为限流) |
| 响应体会夹带 PHP 告警 | 成功响应在 JSON 之后还会追加 <br /><b>Warning</b>: Cannot modify header information …(站点自身缺陷) | http_post 在 json.loads 失败时截取最外层花括号再解析,天然容错;不要因此判定响应非法 |
| create_time | 实测是 Unix 毫秒级时间戳(如 1783494711000) | 脚本归一化为秒后按 UTC+8 转可读时间,另存 create_time_ts(秒) |
| note_id | 实测键名就是 note_id(同时也兼容 id) | 主用 note_id,多拼写兼容 |
| red_id | 站点额外返回小红书号(如 talentzhao0421) | 输出为 red_id |
| note_img | 对象数组 [{"img_url": "..."}] | normalize_media 提取 img_url 等键,并兼容字符串数组 / JSON 字符串 / 逗号分隔 |
| ⚠️ video_url 会粘包 | 实测:视频笔记之后的多条笔记(含图文)都会带上同一个 video_url —— 站点沿用了上一条视频笔记的值 | 原值如实保留 + 每条笔记加 video_url_suspect 布尔标记;首次出现按该条自己的 type 定夺,重复出现一律判粘包;type 未知时不下结论。--human 对粘包笔记不直接展示该链接 |
| comment_count | 真实拼写就是 comment_count(历史文档里的 omment_count 缺了首字母) | 主用 comment_count,同时兼容 omment_count |
| 字段为空属正常 | 无简介的笔记 desc 为空、comment_count 为 0、无视频的笔记 video_url 为 null 等 | 不做告警、不影响 ok 判定;缺失字段统一为 null(数组类为 []) |
📌 仍待实测的最后 1 项:
noteTime/noteType站点实际接受中文还是英文取值。
文档写的是中文(一天内 / 视频笔记),但实测返回的type是英文normal/video,
所以筛选项也可能吃英文。脚本当前按文档发中文并同时兼容英文写法;
若实测发现站点用英文,只需把脚本顶部NOTE_TIME_ALIASES/NOTE_TYPE_ALIASES的
目标值改成英文(别名表的键不用动),再补一条回归用例。
1.3 分页机制(本技能的核心)
| 概念 | 说明 |
| --------------------------- | ----------------------------------------------------------------------------- |
| page | 页码,即「第几页」,从 1 开始计数(默认 1)。第 1 页传 1,第 N 页传 N。不是偏移量、不是记录数、不是采集页数 |
| --page(别名 --start-page) | 起始页码:从第几页开始(默认 1) |
| --pages | 采集页数:本次要采几页(默认 1,上限 50)。两者只差一个字母,别填反 |
| search_id / session_id | 翻页凭据。首页传空串;第 N 页取第 N-1 页响应里返回的值 |
| cursor | 站点响应里也可能给 cursor(文档把它列为返回字段)。脚本也会一并提取并带上,作为兜底 |
| 首页 | page=1,search_id="",session_id="" |
| 续采 | --page <下一页页码> --search-id <上页返回值> --session-id <上页返回值> |
页码与页数的组合关系:
--page 3 --pages 2= 请求第 3、4 页;
--pages 2(不带--page)= 请求第 1、2 页。
结果顶层同时给出start_page(起始页码)与end_page(实际取到的最后一个页码),
每条笔记的page字段就是它所在的页码,可与pages(页数)对照核对。
⚠️ 续采必须带
--page与翻页凭据:只传--keyword会从第 1 页重新开始,白花一次额度。
结果里的next_action.suggested_command已经把这些参数都拼好了,直接复制执行即可。
同理,--page > 1却不带凭据时脚本会在warnings里提醒——接口约定第 N 页须带第 N-1 页
返回的凭据,缺凭据多半取不到那一页,纯属白花额度。
五道翻页安全阀(任一命中立即停止,绝不反复请求同一页):
| # | 条件 | 说明 |
| - | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| 1 | 本页没有任何笔记 | 判为末页 |
| 2 | 响应里 has_more=false | 判为末页 |
| 3 | 本页返回的笔记与已采集的完全重复(没有任何新增) | 判为翻页失效(凭据没前进),避免继续白花额度 |
| 4 | 响应没有返回任何翻页凭据(search_id / session_id / cursor 全空) | 判为末页 |
| 5 | 凭据与「请求时使用的」完全相同(服务端回显) | 不立刻停——只要本页还有新内容就按 page 继续推进,并在 warnings 里说明;若同时也没有新内容,则由第 3 条拦住 |
此外还有页数硬上限 MAX_PAGES = 50(--pages 超出自动收敛到 50)。
页码自检(纯本地判断,不消耗额度,只写 warnings):
| 触发条件 | 告警含义 |
| ---------------------------------------------------------- | --------------------------------------------------------------------- |
| --page N(N>1)但 search_id / session_id / cursor 全为空 | 缺翻页凭据,很可能取不到第 N 页(白花 1 次额度);建议从第 1 页起采或直接用 next_action 的续采命令 |
| --page 超过 MAX_REASONABLE_PAGE = 50 | 多半是把「采集页数」误填进了 --page(--pages 与 --page 只差一个字母) |
| 一次传多个关键词,同时又给了翻页凭据 | 凭据只对应上一页所属的那个关键词,套用到其它关键词会取到无关结果;多关键词请先采第 1 页再按各自的 next_action 分别续采 |
1.4 筛选参数与你该知道的取舍
| 参数 | 默认 | 站点取值 | 脚本行为 |
| ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| noteTime | 空(不限) | 一天内 / 一周内 / 半年内 | 做别名归一化:1天/day/今天 → 一天内,7天/week → 一周内,半年 → 半年内,不限 → 空串;表里没有的写法原样透传 |
| noteType | 空(不限) | 视频笔记 / 普通笔记 | 同样归一化:视频/video → 视频笔记,图文/普通/normal → 普通笔记,不限 → 空串;未命中原样透传 |
| sort | general(综合) | general 综合(默认)/ popularity_descending 最热 / time_descending 最新 / comment_descending 最多评论 / collect_descending 最多收藏(英文枚举) | 中文/英文/缩写都能识别:最热/热门/hot → popularity_descending,最新/latest → time_descending,最多评论 → comment_descending,最多收藏 → collect_descending,综合/默认/default → general;未命中原样透传 |
⚠️
sort与noteTime/noteType的归一化方向相反:那两个的站点取值是中文,所以脚本把
各种写法归到中文上;sort的站点取值是英文枚举,所以脚本把中文说法最热/最新/最多评论/最多收藏
映射成英文再发出去(直接把中文丢给站点是无效的)。不传
--sort时脚本会显式带上general(而不是空串),这样请求体里的排序始终是明确的,
续采命令也会把排序一起带上,避免「第一次按最热采、续采却换回默认」这种口径漂移。
💡 为什么不做成"聪明"的自动匹配:归一化只覆盖「明显同义」的写法,未知取值原样透传
——这样站点日后新增或改用别的排序值时,用户直接传原值即可,不用改脚本。
1.5 多个关键词(一次搜多个词)
| 传法 | 写法 | |
| ----- | ---------------------------------------- | --------------------------------- |
| 一次传多个 | --keyword "AI生图\|AI绘画"(只用 \| / ; / 换行 分隔) |
| 重复传参 | --keyword "AI生图" --keyword "AI绘画" | |
| 从文件读 | --file keywords.txt(每行一个关键词,# 开头为注释) | |
🚨 空格与逗号刻意不做分隔符:搜索词经常自带空格(「AI 生图」)和逗号。
如果按空格/逗号拆,一个关键词会被切成两个,白花一倍额度还把结果搞乱。
所以--keyword "AI 生图"就是一个关键词;要搜多个词请用|或重复传参。
多关键词时的其它约定:
| 约定 | 说明 |
| ------------- | --------------------------------------------------------------- |
| 自动去重 | 重复关键词(大小写不敏感)只采一次,丢弃数记在顶层 duplicates_removed |
| 账户级错误立即中止 | 撞上「余额不足 / 密钥失效 / 上游限流 / 动作被拒」时立刻停止整批,剩余关键词记入 skipped_keywords |
| 不做顶层合并 | 只有单关键词时才把笔记字段平铺到顶层;多关键词时结果只在 results[i] 里 |
| 筛选条件共用 | noteTime / noteType / sort 对该次所有关键词统一生效 |
2. 功能特性
- 🔍 关键词搜索采集 — 输入关键词即可,无需先知道笔记 ID 或博主主页
- 📦 多关键词一次搜 —
--keyword "A|B"、重复传参、或--file;自动去重,且不会把带空格的关键词拆碎 - 🎛 三种筛选 — 时间范围(一天内/一周内/半年内)、笔记类型(视频笔记/普通笔记)、排序;中英文写法都能识别
- 📄 分页可控 —
page+search_id/session_id(并兼容cursor)翻页;五道安全阀 + 页数硬上限 - 🧠 默认只采 1 页 — 每页都要花额度,先取第一页给用户看;未到末页时给出
next_action与可直接执行的续采命令,由助手问用户后再继续 - 🛑 批量遇账户级错误立即中止 — 余额不足 / 密钥失效 / 上游限流 / 动作被拒时停批,剩余关键词记入
skipped_keywords - 1️⃣ 失败才补发一次 — 每页先请求 1 次,只有返回失败时才补 1 次,最多补一次;成功不补发、不轮询
- 🚫 不可补发错误不浪费额度 — apikey 失效、余额不足、上游临时故障/限流、动作名被网关拒绝这四类"重发也不会变"的错误直接返回
- 🕙 上游限流可识别 — 实测
404,API 请求失败,请10分钟后,再重试属上游临时故障,会返回upstream_busy与建议等待 600 秒,不做无意义的立即补发 - ⏳ 0-600 秒耐心等待 — 默认等待上限 600 秒(
--timeout可调,0为不设超时),等待期间每 15 秒打印心跳 - 🧩 容器键自适应 — 响应结构变化(
note_info→notes→search_result→data.list→ …)无需改代码,自动挑出最像笔记列表的那个数组 - 🔢 互动数据标准化 — 兼容
1.2万/5.6w/1,234等写法并统一转为整数 - 🔤 类型易读 — 额外输出
type_label(normal→ 图文笔记,video→ 视频笔记) - 🖼 图片组与视频链接 — 图文笔记返回全部图片 URL 与张数,视频笔记返回视频直链;字段为空属正常现象
- 🕐 时间戳自动转换 —
create_time按 UTC+8 转可读时间,同时保留create_time_ts - 🔐 apikey 不落脚本 — 密钥只从环境变量或本机密钥文件读取,绝不硬编码;日志中一律打码
- 🧯 错误可诊断 — 区分「apikey 失效」「余额不足」「上游故障」「动作被拒」,分别给出正确的处理指令
3. 鉴权(必读)
3.1 获取 API Key
请用户登录 https://u.ai-gc.art/ 获取 apikey。
3.2 配置 API Key
apikey 是密钥,不保存在脚本中。 三种配置方式,优先级从高到低:
| 优先级 | 方式 | 说明 |
| --- | ------------------------------------------ | -------------------------- |
| 1 | --apikey 参数 | 临时使用,会进入终端历史,仅调试用 |
| 2 | 环境变量 AI_GC_ART_API_KEY | 适合 CI / 常驻环境 |
| 3 | 本机密钥文件 ~/.workbuddy/ai-gc.art_apikey.txt | 推荐,用 --set-apikey 写入 |
密钥按「站点」存放,不按技能存放:文件名取站点名
ai-gc.art,因为u.ai-gc.art站点下的所有接口(含本技能)共用同一份 apikey。若本机已配置过该站点的其它技能,无需重复配置,直接复用即可。
兼容性:旧文件名~/.workbuddy/ai-gc.art_apikey(无扩展名版)、~/.workbuddy/xhs_apikey.txt(按技能命名版)仍可读取,首次读到时脚本会自动迁移到新路径;环境变量旧名XHS_API_KEY/XHS_APIKEY/XHS_KEY也仍然生效。
# 保存 apikey(一次配置长期有效)
python scripts/xhs_search_collect.py --set-apikey <用户提供的apikey>
# 校验配置是否生效(不发起采集请求)
python scripts/xhs_search_collect.py --check-key
3.3 缺少 apikey 时的处理
当 --check-key 返回 apikey_configured: false,必须先向用户索要 apikey,并按以下话术引导:
需要小红书数据采集服务的 apikey 才能继续。
请登录 https://u.ai-gc.art/ 获取 apikey 后发给我,我会保存到本机密钥文件(不会写进脚本)。
拿到用户提供的 apikey 后,执行 --set-apikey 保存,再继续采集。禁止自行编造、猜测或复用其他服务的密钥。
4. 使用指南
4.1 标准流程(五步)
Step 1 — 确认 apikey 已配置
python scripts/xhs_search_collect.py --check-key
若未配置,按 §3.3 向用户索要后 --set-apikey 保存。
Step 2 — 采集搜索结果的笔记列表
# 默认:只采第 1 页
python scripts/xhs_search_collect.py --keyword "AI生图"
# 用户示例:采集 2 页
python scripts/xhs_search_collect.py --keyword "AI生图" --pages 2
# 加时间范围与笔记类型筛选
python scripts/xhs_search_collect.py --keyword "AI生图" --note-time 一周内 --note-type 视频笔记 --pages 2
# 排序:最热 / 最新 / 最多评论 / 最多收藏(默认 general 综合,可写中文)
python scripts/xhs_search_collect.py --keyword "AI生图" --sort 最热 --pages 2
# 多个关键词(竖线分隔;注意空格与逗号不会被当分隔符)
python scripts/xhs_search_collect.py --keyword "AI生图|AI绘画" --pages 1
# 续采下一页(用上一次结果里 next_action 给出的命令,不要重复第 1 页)
python scripts/xhs_search_collect.py --keyword "AI生图" --page 2 \
--search-id "<上次返回的 search_id>" --session-id "<上次返回的 session_id>" --pages 1
# 自定义等待上限(0-600 秒,默认 600)
python scripts/xhs_search_collect.py --keyword "AI生图" --timeout 300
# 关闭补救(每页只发 1 次,失败就失败)
python scripts/xhs_search_collect.py --keyword "AI生图" --retry 0
# 导出 JSON 到文件
python scripts/xhs_search_collect.py --keyword "AI生图" --pages 2 --output notes.json
⏳ 执行时请留足等待时间:接口响应可能需要数十秒到 600 秒。
命令会一直阻塞等待,这是正常现象。等待期间 stderr 会出现[等待中] …心跳;
不要因为长时间无输出就中断重跑,那会重复消耗额度。
多关键词的总耗时 = 关键词数 × 页数 × 单页耗时,关键词多时不要设很小的--timeout。
💻 Windows 下中文可能显示成乱码:Windows PowerShell 5.1 捕获子进程输出时按本地代码页解码,
中文标题 / 简介可能变成乱码。这不是数据坏了——加--output <文件>让脚本自己以 UTF-8 落盘,
再用读取工具打开该文件,就能看到正确中文。
Step 3 — 按 §4.3 模板渲染结果给用户
Step 4 — 检查 next_action:不为 null 就必须先问用户(强制)
两种情况会出现 next_action(详见 §4.5):
| reason | 触发场景 | 助手应当对用户说的话(大意) |
| ------------- | ----------------------------------- | -------------------------------------- |
| has_more | 还没采到末页,后面还有内容 | 说明已采 N 页 / M 条,问「是否继续采集更多页?」 |
| fatal_error | 多关键词撞上余额不足 / 密钥失效 / 上游限流 / 动作被拒,已中止 | 说明失败原因 + 还有 N 个关键词没采,问「处理后再继续采这 N 个吗?」 |
用户答复后的动作:
| 用户答复 | 助手动作 |
| -------- | --------------------------------------------------------------------- |
| 继续 / 好 | 直接执行 next_action.suggested_command,回到 Step 3 渲染,并再次检查 next_action |
| 继续,但多采几页 | 把命令里的 --pages(或 suggested_pages)改大再执行(单次不超过 50 页) |
| 不用了 / 算了 | 立即结束任务,不再发任何请求 |
⚠️ 不要替用户做决定。每一页都要花额度,
next_action里已经给好可直接执行的命令,
用户点头再执行。用户说不用 → 立即收工,不要再发任何请求。
Step 5 — 检查 warnings / errors,必要时向用户说明
warnings里出现「完全重复」→ 说明翻页凭据已失效,脚本为避免白花额度提前停了;
如实告知用户,并把next_search_id/next_session_id/next_cursor一起带出,便于人工判断。warnings里出现「服务端回显」→ 正常现象(服务端把凭据原样回传),本页仍有新内容,已按page继续。warnings里出现「未提供翻页凭据」/「超出常规范围」/「翻页凭据(…)只对应上一页所属的那个关键词」
→ 页码或凭据填得不对(见 §1.3 页码自检)。先按提示纠正参数再重跑,不要照原命令重复执行,
否则会继续白花额度。多关键词的凭据问题尤其要拆成「先采第 1 页 → 再按各关键词续采」。errors里出现upstream_busy/insufficient_balance/unauthorized/action_forbidden
→ 按 §5 对应段落处理,不要盲目重试。
4.2 命令速查表
| 场景 | 命令 |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 保存 apikey | python scripts/xhs_search_collect.py --set-apikey <key> |
| 检查 apikey | python scripts/xhs_search_collect.py --check-key |
| 搜索(默认 1 页) | python scripts/xhs_search_collect.py --keyword "AI生图" |
| 采集 2 页 | python scripts/xhs_search_collect.py --keyword "AI生图" --pages 2 |
| 从第 3 页起采 2 页(页码 + 页数) | python scripts/xhs_search_collect.py --keyword "AI生图" --page 3 --pages 2 |
| 限定时间范围 | python scripts/xhs_search_collect.py --keyword "AI生图" --note-time 一周内 |
| 只看视频笔记 | python scripts/xhs_search_collect.py --keyword "AI生图" --note-type 视频笔记 |
| 按最热/最新/最多评论/最多收藏排序 | python scripts/xhs_search_collect.py --keyword "AI生图" --sort 最热(也可写 popularity_descending / time_descending / comment_descending / collect_descending) |
| 多个关键词 | python scripts/xhs_search_collect.py --keyword "AI生图\|AI绘画" |
| 从文件读关键词 | python scripts/xhs_search_collect.py --file keywords.txt |
| 续采下一页 | python scripts/xhs_search_collect.py --keyword "AI生图" --page 2 --search-id "<S>" --session-id "<E>" --pages 1 |
| 自定义等待上限 | python scripts/xhs_search_collect.py --keyword "AI生图" --timeout 300 |
| 不设超时(一直等) | python scripts/xhs_search_collect.py --keyword "AI生图" --timeout 0 |
| 关闭补救 | python scripts/xhs_search_collect.py --keyword "AI生图" --retry 0 |
| 导出 JSON | python scripts/xhs_search_collect.py --keyword "AI生图" --output out.json |
| 排查失败原因 | python scripts/xhs_search_collect.py --keyword "AI生图" --raw --human |
4.3 结果展示模板(强制)
stdout 为标准 JSON,面向用户时必须渲染成结构化卡片,不要直接贴 JSON。
先给一行汇总:
🔍 小红书关键词搜索:AI生图
📄 已采集 2 页 | 共 40 条笔记 | 时间范围:一周内 | 类型:视频笔记
(仅当 `total_count` 不为 `null` 时才补一句:该关键词共约 N 条 —— 实测站点常给 `null`,没有就不写)
再逐条渲染(条数多时先给表格,再按需展开):
📕 #1 ⭐ 点赞 1.2万 | 收藏 5.6k | 评论 320
标题:如何做一套风格统一的知识卡片?
👤 作者:**AI日常(小红书号:xxxx) | 🏷 图文笔记 | 🕐 2026-09-24 18:07:01
🆔 6ab4cc59000000001403a8**
📝 简介:#AI工具[话题]# #知识卡片[话题]#
🖼 图片(1 张):https://sns-i11.rednotecdn.com/...jpg
🎬 视频链接:https://sns-video.xhscdn.com/....mp4 ← 仅视频笔记才输出这一行
🔗 https://www.xiaohongshu.com/explore/6ab4cc59000000001403a8**
- 字段为
null时显示-,不要显示None/null。 - 图文笔记(
video_url为null)不输出「🎬 视频链接」这一行。 - ⚠️
video_url_suspect为true时不要展示该链接(站点粘包,链接不属于这条笔记,见 §1.2)。
若确实要提,就写明「站点返回的视频链接疑似沿用自其它笔记,不代表本条含视频」,不要当成事实展示。 - 条数多(> 10)时先给一行表格(序号 / 笔记ID / 标题 / 点赞 / 收藏 / 评论),再按需展开明细。
多关键词时,先给一行汇总,再按关键词分组:
🔍 小红书关键词搜索(批量)
共 {total_keywords} 个关键词,成功 {succeeded} 个,失败 {failed} 个
📄 共 {total_notes} 条笔记 | 共发 {total_requests} 次请求 | 去重丢弃 {duplicates_removed} 个关键词
追加行(按条件择一):
| 条件 | 追加内容 |
| ----------------------- | -------------------------------------------------------------- |
| skipped_count > 0 | ⛔ 因账户/接口级错误提前中止,有 {skipped_count} 个关键词未采集:{skipped_keywords} |
| next_action 不为 null | ➡️ 已备好后续命令(是否继续由用户决定) |
next_action 不为 null 时,渲染完结果后必须按 §4.1 Step 4 向用户提问,例如:
➡️ 已采集 2 页,共 40 条笔记,关键词「AI生图」还有更多内容。是否继续采集更多页?
(续采每页消耗 1 次额度;说"不用了"就到此为止)
提问后停下等用户答复,不要自己接着往下采。
4.4 输出 JSON 结构
单关键词采集时,顶层会直接合并该关键词的结果(下图是默认 1 页、首次就成功的典型返回):
{
"ok": true,
"total_keywords": 1,
"succeeded": 1,
"failed": 0,
"total_notes": 20,
"apikey_source": "密钥文件 C:\\Users\\...\\.workbuddy\\ai-gc.art_apikey.txt",
"timeout_seconds": 600,
"retry": 1,
"pages": 1,
"start_page": 1,
"end_page": 1,
"search_filters": { "noteTime": "一周内", "noteType": "视频笔记", "sort": "general" },
"warnings": [],
"max_requests_per_page": 2,
"max_requests_per_keyword": 2,
"duplicates_removed": 0,
"aborted": false,
"skipped_count": 0,
"skipped_keywords": [],
"total_requests": 1,
"total_retries": 0,
"next_action": null,
"keyword": "AI生图",
"notes": [ { "…一条笔记(见下表)…" } ],
"note_count": 20,
"pages_requested": 1,
"pages_fetched": 1,
"has_more": null,
"next_page": 2,
"next_search_id": "…",
"next_session_id": "…",
"next_cursor": null,
"exhausted": false,
"page_duplicates_removed": 0,
"request_count": 1,
"retry_count": 0,
"filters": { "note_time": "一周内", "note_type": "视频笔记", "sort": "general" },
"errors": [],
"warnings": [],
"results": [ { "…与上面合并到顶层的字段一致…" } ]
}
单条笔记对象的字段:
| 字段 | 含义 |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| note_id | 笔记 ID |
| userid | 作者 ID |
| red_id | 小红书号(站点实测返回,如 talentzhao0421) |
| nickname | 用户名 |
| type / type_label | 接口类型值 / 中文展示名(normal 图文笔记、video 视频笔记) |
| create_time / create_time_ts | 可读时间(UTC+8) / 归一化后的 Unix 秒级时间戳(站点给的是毫秒) |
| title / desc | 标题 / 简介 |
| comment_count / collect_count / digg_count / share_count | 评论数 / 收藏数 / 点赞数 / 分享数 |
| note_img / image_count | 图片 URL 列表 / 图片张数 |
| video_url | 站点原样返回的视频直链(不改数据);没有则 null |
| video_url_suspect | true = 该 video_url 判定为站点粘包(站点把视频笔记的链接沿用到后续笔记,含图文笔记),不代表本条含视频(见 §1.2) |
| note_url | 由笔记 ID 拼出的笔记链接 |
| share_url | 接口给了就原样输出,否则 null |
| page / index | 该笔记来自第几页 / 结果里的序号 |
顶层(多关键词时)与单关键词结果里的关键字段:
| 字段 | 含义 |
| ------------------------------------------------------------------ | -------------------------------------------------------------------- |
| ok | 本次运行是否全部成功 |
| total_keywords / succeeded / failed | 关键词总数 / 成功 / 未成功(含因中止跳过的) |
| total_notes / total_requests / total_retries | 全部关键词的笔记条数 / 请求次数合计 / 补发次数合计 |
| pages / start_page | 每个关键词采集的页数 / 起始页码 |
| search_filters | 本次实际使用的筛选条件(归一化后) |
| max_requests_per_page / max_requests_per_keyword | 单页 / 单关键词的请求次数上限(用于核对「没有多余请求」) |
| duplicates_removed | 因重复(同一关键词传多次)而未重复消耗额度的个数 |
| aborted / skipped_count / skipped_keywords[] | 是否因账户级错误提前中止 / 未采集的关键词个数与列表(可原样用于续采) |
| next_action | 需要向用户确认时的结构化信息(见 §4.5);无需询问时为 null |
| note_count / pages_fetched / pages_requested | 该关键词取到的笔记数 / 实际取回页数 / 请求页数 |
| total_count | 站点字段,本意是「该关键词总条数」;⚠️ 实测恒为 null(不可依赖),脚本原样输出且不参与任何判断 |
| has_more | 接口给出的「还有下一页」标记(站点实测是字符串 "1"/"0",脚本已归一化为布尔);接口没给时为 null |
| raw_responses[] | 仅 --raw 时出现:每页的完整原始响应({page, body},单页上限 20 万字符),便于核对容器键与字段结构 |
| next_page / next_search_id / next_session_id / next_cursor | 下一页的页码与翻页凭据(续采直接用) |
| exhausted | 是否已判定到末页 |
| page_duplicates_removed | 该关键词内部跨页重复而丢弃的笔记条数 |
| filters | 该关键词实际使用的筛选条件(与顶层 search_filters 一致) |
| request_count / retry_count | 该关键词实际发出的请求次数 / 其中属于补发的次数 |
| errors[] | 失败详情(error_code / error / action_required 等) |
| warnings[] | 非致命提示(如「本页与已采集的完全重复,已停止」) |
多关键词时不做顶层合并(
notes只存在于results[i]里),顶层只给汇总信息。
4.5 next_action 机制(脚本只给信息,交互留给助手)
| 字段 | 含义 |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| ask_user | 恒为 true,表示「应当向用户提问」 |
| reason | has_more(还有下一页) 或 fatal_error(账户级错误中止,剩余关键词待采) |
| error_code | 仅 fatal_error 时出现(insufficient_balance / unauthorized / upstream_busy / action_forbidden) |
| question | 可直接念给用户的问题,已带页数/条数(或关键词个数) |
| keyword / keywords[] | has_more 时是当前关键词;fatal_error 时是剩余关键词列表 |
| suggested_pages | 建议续采的页数(默认 1) |
| resume_page | 续采应从第几页开始 |
| resume_search_id / resume_session_id / resume_cursor | 续采要带的翻页凭据 |
| suggested_command | 可直接执行的续采命令(已带 --keyword / --page / 凭据 / --pages,写法与 §4.1 一致) |
| cost_hint / stop_hint | 给助手看的额度说明 / 「用户说不用就收工」提示 |
| per_keyword[] | 仅多关键词且多个关键词都还有更多内容时出现:每个关键词各自的续采命令 |
出现条件(必须满足,否则为 null,表示无需询问):
has_more:本次采集成功、取到了笔记、未判定为末页,且至少有一个翻页凭据;fatal_error:批量中确实有skipped_keywords(即真的中止了),且错误码属于那四类。
⚠️ 脚本内绝不
input():stdout 必须保持纯 JSON,且一旦阻塞等待 stdin,Agent 的工具调用会直接挂住。
需要交互时,永远由助手在对话里问,不要把交互塞进脚本。
用户说「不用了 / 够了」→ 立即结束任务,不再发任何请求。
5. 错误处理与退出码
| 退出码 | 含义 | 处理方式 |
| --- | ---------------------------- | -------------------------------------- |
| 0 | 全部成功 | 正常渲染 |
| 1 | 部分或全部采集失败(含批量中止跳过) | 读取 results[].errors[],按 error 提示用户 |
| 2 | 参数错误(未传关键词 / 文件不存在 / 关键词全为空) | 向用户确认关键词 |
| 3 | 缺少 apikey | 按 §3.3 向用户索要 |
余额不足(error_code: "insufficient_balance",如 余额不足,请登录 u.ai-gc.art ,请充值后重试):
- 告知用户账户额度已耗尽,需去 https://u.ai-gc.art/ 充值
- 明确区分:apikey 本身是有效的,不要引导用户重新获取 key,否则用户会白折腾
- 充值后可直接重试,无需重新配置
- ⚠️ 实测在本站点上,欠费时连参数校验失败的请求都会返回 402,所以「换个参数试试」在欠费期间毫无意义,先充值
鉴权类失败(error_code: "unauthorized",如 访问被拒绝: 无效的apikey,请登录 u.ai-gc.art 获取):
- 明确告知用户 apikey 无效或已过期,请重新登录 https://u.ai-gc.art/ 获取
- 拿到新 key 后重新
--set-apikey覆盖旧值,再重试 - 不要反复重试同一个失效的 key(脚本已识别为不可补发)
- 若响应是「未携带密钥」(HTTP 401
missing api key),说明本机还没有密钥,按 §3.3 向用户索要
动作被拒(error_code: "action_forbidden",表现为 HTTP 403 + 裸文本 forbidden):
- 这是站点网关不接受该接口动作名,与 apikey 无关(实测有效密钥与无效密钥返回完全相同)
- 明确告诉用户不要重新获取 apikey——那解决不了问题,只会白跑一趟
- 检查脚本内的
DR_SEARCH动作名(xhs-search)是否与站点实际开放的接口一致;若确认无误,
说明该动作尚未对当前账号开放,需联系站点支持
上游临时故障 / 限流(error_code: "upstream_busy",如 404,API 请求失败,请10分钟后,再重试,同时带 retry_after_seconds: 600):
- 明确告知用户:这是上游接口临时不可用,不是参数错误、不是 apikey 问题
- 不要改参数、不要重新获取 apikey、不要立刻重跑——服务端要求等 10 分钟,立即重发必然同样失败,只会白扣额度
- 正确做法:等约 10 分钟后原样重跑同一条命令(脚本已识别为不可补发)
- 若用户已有部分结果(多页/多关键词中途失败),已取到的数据无需重做
等待超时(error 含「等待超时:已等待 N 秒仍未返回」):
- 该错误属于可补发:脚本会自动补一次;若补发仍超时,把
--timeout调大(上限 600 秒)后由用户确认再重新发起,--timeout 0表示一直等待 - 向用户如实说明是"等待超时"而非"数据取不到"
其他常见失败:
invalid_keyword(退出码 2)→ 未传关键词,或传进来的都是空白empty_result→ 接口返回成功但响应里没有笔记列表(关键词无结果 / 筛选过窄 / 响应结构变更),会被当作失败并补发一次;仍失败时加--raw --human查看原始返回error_code: "api_error"(其他业务失败)→ 脚本已按策略补发一次;仍失败时加--raw看原始响应error_code: "missing_apikey"(ok:false,退出码 3)→ 本机配置问题:先跑--check-key确认,再按 §3.3 索要 apikey 并用--set-apikey重新落盘- 网络请求失败 → 脚本已自动补一次;仍失败则提示检查网络后由用户确认再重试
💡 额度提醒:
xhs-search每页都消耗额度。
默认只采 1 页 = 1 次;采 N 页 = N 次;多关键词 = 关键词数 × 页数 次。
想省额度:① 先按默认 1 页看效果,需要再续采;② 续采务必带上--page与翻页凭据,
否则会从第 1 页重来、白花一次;③ 重复关键词会自动去重,不用自己先去重;
④ 调试请用离线测试(见 §6),零消耗。
6. 项目架构
xhs-search-collector-kkai/
├── SKILL.md # Skill 定义与使用文档(本文件)
├── _icon.png # 技能图标(kkAI 小红书 logo,黄色底;2026-10-26 按用户指定图替换)
├── scripts/
│ └── xhs_search_collect.py # 核心采集脚本(搜索接口 + 分页 + 字段归一化 + CLI)
├── references/
│ └── api_reference.md # 接口明细、字段映射表、错误码说明、排查清单
└── tests/
└── test_xhs_search_collect.py # 离线回归测试(mock 响应,不消耗接口额度)
离线回归测试(修改脚本后建议运行,无需 apikey、不发起真实请求):
python tests/test_xhs_search_collect.py
覆盖 365 项断言 / 17 组用例:
- 字段归一化:
note_info容器、comment_count/video_url真实拼写与omment_count/vide_url兼容、
note_img对象数组 / JSON 字符串 / 逗号分隔 / 字符串数组、万/w/亿/k/千分位数字转换、
时间戳(含毫秒)转 UTC+8 可读时间并保留create_time_ts、空值转null/[] - 请求次数:默认 1 页 = 1 次、2 页 = 2 次、失败补发 = +1、两次都失败绝不发第 3 次、
--retry 0只发一轮、
成功过的页不被重跑 - 不可补发错误:
insufficient_balance/unauthorized/upstream_busy/action_forbidden/
401 missing api key时retry_count == 0且带action_required;
反向断言「服务器繁忙,请稍后重试」这类普通业务失败仍补发一次 - 403 归类别诊断:
403 + 裸 forbidden→action_forbidden(不误判为unauthorized);
200 + JSON code:403→unauthorized;403但响应体是 JSON 时不算动作被拒 - 业务正文不污染错误判定(真实事故回归):标题含「1分钟」、简介含「余额不足 / apikey 无效 / 稍后再试」
的成功响应仍判成功、条数正确、无error_code;反向断言真正的限流文案仍被识别、成功态绝不判为限流 - PHP 告警容错:JSON 之后夹带
<br /><b>Warning</b>: Cannot modify header information …仍能解析成功 - 分页五道安全阀:空页 /
has_more=false/ 无凭据 / 内容完全重复 / 凭据被回显但仍有新内容时继续推进;
凭据在页间正确传递、首页凭据为空串、跨页去重计数、data嵌套凭据可取值、
笔记实体内部的session_id不被当成翻页凭据、仅有cursor时也能翻页、
续采直接请求指定页码(不重复第 1 页) next_action:未到末页给出has_more且命令含--page/--search-id/--pages/ 关键词;
末页 / 失败 / 空结果 / 中途失败时必须为null;多关键词时带per_keyword- 多关键词:
|/;/ 换行分隔、空格与逗号不拆(「AI 生图」「猫,狗 训练」不被切碎)、
去重计数、撞账户级错误只发 1 次即中止且剩余进skipped_keywords、普通业务失败不中止批量 - 筛选归一化:
1天/day→一天内、7天/week→一周内、视频/video→视频笔记、
不限→ 空串、未知取值原样透传、参数正确透传到请求体 - 排序归一化(
sort,站点为英文枚举):5 个站点取值原样可用、最热/热门/hot→
popularity_descending、最新/latest→time_descending、最多评论/最多收藏→
comment_descending/collect_descending、综合/默认/default→general;
大小写与全角空格容错;不传 / 空串 / None →general;未知取值原样透传(保留原始大小写);
sort默认值贯穿到请求体,且续采命令保留排序(避免口径漂移) - 边界收敛:
clamp_timeout/clamp_retry/clamp_retry_delay/clamp_pages/clamp_start_page - 页码语义(
page= 页码,默认 1):默认首页page=1且凭据为空串;--page 3 --pages 2请求第 3、4 页
(页码而非偏移量)、凭据逐页传递、每条笔记带自己的页码、next_page为下一页页码;
--page N(N>1)缺凭据或页码超上限时给出告警(带凭据则不告警);--start-page别名生效;
顶层start_page/end_page/pages三者含义不混,单关键词平铺后仍不被覆盖 - CLI 端到端:退出码 0/1/2/3、
--human摘要走 stderr 且 stdout 仍为纯 JSON、--compact、--output、
--raw、--file批量、--set-apikey(写临时文件、不回显明文)、--check-key、
以及「apikey 明文不出现在 stdout / stderr」 - 静态安全自审:无
subprocess/os.system/eval(/exec(/base64/input();
出网域名仅u.ai-gc.art(www.xiaohongshu.com只用于拼接展示用链接,从不作为请求目标) - 真实响应结构回归(
test_real_response_structure,2026-10-02 实测夹具):note_info容器解析、
red_id输出、毫秒时间戳归一化为秒并按 UTC+8 渲染、note_img对象数组、
total_count透传到结果与next_action、has_more字符串"1"/"0"归一化、
站点不返回cursor时续采命令不带--cursor、
video_url粘包的三条分支(视频笔记可信 / 后续重复一律粘包 /type未知不误报)且原值不被改动、
--raw在成功时也留存完整原始响应
| 组件 | 技术 | 说明 |
| ---- | ---------------------------------------------- | --------------------------------------------------------- |
| 运行环境 | Python 3.8+ | 纯标准库,无第三方依赖 |
| 数据接口 | u.ai-gc.art/mcp/rd.php | 动作 dr=xhs-search(keyword + 可选筛选 + 翻页参数)—— 已实测动作名被站点接受 |
| 鉴权 | Authorization: Bearer <apikey> | apikey 来自环境变量或本机密钥文件 |
| 等待策略 | 单次请求最长 600 秒 | --timeout 可调(0-600,0 为不设超时);等待期间打印心跳,中途不重发 |
| 补救策略 | 每页最多补 1 次 | --retry 可调(0-1,硬上限 1);--retry-delay 默认 2 秒 |
| 分页策略 | page + search_id/session_id(兼容 cursor) | 默认只采 1 页;五道安全阀 + 页数上限 50;未到末页给 next_action |
| 多关键词 | 单次可传多个(\| / ; / 换行) | 空格与逗号不拆;自动去重;账户级错误立即中止整批 |
| 输出 | JSON(stdout) | 供 Agent 解析后渲染为 Markdown 卡片 |
资源索引
- 核心脚本:
scripts/xhs_search_collect.py— 参数:--keyword(可重复 / 可分隔多个)、--file、--pages、--page、--search-id、--session-id、--cursor、--note-time、--note-type、--sort、--apikey、--set-apikey、--check-key、--output、--compact、--human、--raw、--timeout、--retry、--retry-delay、--sleep - 参考文档:
references/api_reference.md— 何时读取:需要核对接口原始字段名、查看错误码含义、调整解析/补救/翻页逻辑时
Scan to join WeChat group