← 返回 Skill 列表
extension
分类: 数据与分析需要 API Key

小红薯书关键词搜索笔记列表数据采集助手_kkAI

输入小红书搜索关键词(可加时间范围/笔记类型/排序筛选,支持多个关键词),返回该关键词下的笔记列表数据:笔记ID、用户名、小红书号、笔记类型、发布时间、标题、简介、评论数、收藏数、点赞数、分享数、图片组、视频链接; 使用方法: 关键词:自媒体运营; 排序:最热; 发布时间:一周内; 笔记类型:普通笔记; 页数:1; ;可选时间范围(一天内/一周内/半年内); 笔记类型(视频笔记/普通笔记); 排序(general 综合 / popularity_descending 最热 / time_descending 最新 / comment_descending 最多评论 / collect_descending 最多收藏,默认 general)

person作者: user_9d51f2abhubcommunity

小红书关键词搜索笔记列表数据采集助手_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,表示无需询问):

  1. has_more:本次采集成功、取到了笔记、未判定为末页,且至少有一个翻页凭据;
  2. 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 — 何时读取:需要核对接口原始字段名、查看错误码含义、调整解析/补救/翻页逻辑时