agentsclimarketplace

Xhs media

Skill samlaying/browser-harness/.claude/skills/xhs-media

小红书全方位媒体下载:原视频 mp4(无水印签名源)+ 全分辨率原图 + 头像。 基于 browser-harness (CDP),点击卡片打开笔记后读取 window.__INITIAL_STATE__.note.noteDetailMap 拿到 DOM 里没有的 masterUrl(视频)和原图地址,当篇立即下载到独立文件夹,并生成媒体汇总 Excel。 与 xhs-crawler 互补:xhs-crawler 管文本/评论,xhs-media 管原视频/原图/头像。 触发:下载小红书视频、小红书原图、小红书头像、小红书全方位采集、批量下载小红书笔记媒体。From its SKILL.md

Install
npx -y skills add samlaying/browser-harness --skill xhs-media

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.3 KB, as published. Nobody here has run it

小红书媒体下载 Skill(xhs-media)

搜索 → 点击笔记 → 读 __INITIAL_STATE__ 拿原视频/原图/头像 → 当篇立即下载 → 汇总 Excel。

和 xhs-crawler 的分工:xhs-crawler 抓文本+评论(DOM);xhs-media 抓原视频+原图+头像(__INITIAL_STATE__)。要同时拿评论和媒体,两个 skill 配合用——xhs-media 下媒体,xhs-crawler 下评论,最后合并。

为什么需要这个 skill

xhs-crawler 只能从 DOM 抓压缩缩略图,且下不了视频(<video> 是 blob/MSE)。__INITIAL_STATE__ 里有服务端注入的签名 mp4 直链和全分辨率原图——这个 skill 专门读它。

前提

  1. browser-harness 已安装(uv tool install -e ~/browser-harness),Chrome 已连接且已登录小红书
  2. requests、openpyxl 已装(pip install requests openpyxl)
  3. 可选后处理:GROQ_API_KEY(视频转写)、OCR_SPACE_API_KEY(图片 OCR)

数据来源(两条路径)

路径触发做法
PRIMARY默认浮窗打开后,浏览器里 window.__INITIAL_STATE__.note.noteDetailMap 已含当前笔记全数据(含 masterUrl)。js() 在浏览器内只抠媒体字段、return 小对象。详见 references/initial-state.md
FALLBACKPRIMARY 字段空时用 xsecToken + 浏览器 cookies(cdp("Network.getAllCookies"))服务端 GET 笔记页 HTML,正则解析 __INITIAL_STATE__。两条路径共用 normalize_note() 映射

批量流程

Step 1:打开搜索页(同 xhs-crawler)

https://www.xiaohongshu.com/search_result?keyword={编码关键词}&source=web_explore_feed

脚本自动 window.scrollBy(0,1200) ×6 加载更多卡片。

Step 2:收集卡片(5列瀑布流排序,复用 xhs-crawler)

收集 .note-item 内 a[href*="/explore/"],去重,按 y 分组(差距<100px 同排)、排内按 x 升序——肉眼阅读顺序。算法见 xhs-crawler 的 waterfall-layout.md。

Step 2.5:建本次目录

mkdir -p xhs_media_data/$(date +%Y%m%d)_{关键词简写}

所有下载落这个目录。每篇一个子文件夹。

Step 3:单会话循环(关键!爬取与下载解耦)

XHS_KEYWORD="关键词" XHS_LIMIT=10 XHS_WORKERS=3 \
XHS_RUN_DIR="xhs_media_data/20260618_关键词" \
browser-harness < scripts/xhs_media_batch.py

必须单个 browser-harness 会话完成(SPA 不重新导航,继承 xhs-crawler #16)。

异步架构(生产者-消费者):浏览器循环只做提取,下载丢给后台线程池——下载不再阻塞爬取。

浏览器循环(生产者,单CDP会话,串行)        下载线程池(N=XHS_WORKERS 个worker)
  关闭上一篇浮窗                            while task=queue.get():
  点击前验证 elementFromPoint                 video→masterUrl,失败换backupUrls
  等浮窗开 + noteId 进 __INITIAL_STATE__       →还失败用cookies重取新鲜URL(fetch_via_http)
  extract_media 读 __INITIAL_STATE__         images/avatar 并发下
  ① 写 {noteId}.json 到盘(崩溃检查点) ──┐    写文件夹+笔记信息.json
  ② 入队 DownloadTask(含cookies快照) ───┴─→ 结果按index排序入results
  立即下一篇(不等下载)                      
循环结束 → pool.join() 等队列排空 → build_excel(results)

每篇浏览器侧步骤:

  1. 关闭上一篇浮窗(.close-circle hover+click → wait_mask_gone)
  2. 点击前验证 elementFromPoint(防误触"发布",Retina 偏移,继承 #17)
  3. 等浮窗打开 + noteId 进入 __INITIAL_STATE__
  4. 媒体优先:extract_media 读 __INITIAL_STATE__ 拿 masterUrl/原图/头像
  5. ① 写 {noteId}.json 到盘(崩溃检查点,进程挂了也能续传)
  6. ② 入队下载(非阻塞,立即下一篇)——worker 只做 requests I/O,不碰 js/cdp,无 CDP 争用
  7. 笔记间 3~8s 随机停顿;每 5 篇 ensure_daemon+ensure_real_tab 重连(#9)

视频下载兜底链(worker 内):masterUrl → 失败换 backupUrls[] → 还失败用入队时快照的 cookies 调 fetch_via_http(noteId, xsecToken) 重取新鲜签名 URL 再下(cookies 会话期内有效)。

Step 4:汇总 Excel

循环结束自动生成 xhs_media_*.xlsx:一行一篇(标题/作者/类型/视频路径超链接/图片数/头像/点赞/收藏/评论/IP/首图缩略图)。也可单独跑:

python3 scripts/xhs_media_download.py xhs_media_data/20260618_关键词/

续传/恢复

进程崩了或中断?每篇 extract 后都写了 {noteId}.json 检查点,重跑下载器即可恢复——download_url 跳过已存在文件,只补缺失的:

XHS_WORKERS=4 python3 scripts/xhs_media_download.py xhs_media_data/20260618_关键词/

(视频签名 URL 若已过期,续传模式下没有浏览器 cookies 重取,会改用 backupUrls;实在下不到的需重跑 batch。)

输出结构

xhs_media_data/20260618_关键词/
├── 001_标题前缀_a1b2c3d4/
│   ├── 视频_a1b2c3d4.mp4          # 视频笔记
│   ├── 图片_01.jpg, 图片_02.jpg …  # 图文笔记的原图(全分辨率)
│   ├── 头像_昵称.jpg
│   └── 笔记信息.json               # 全部字段 + downloaded_files[]
├── 002_…/
├── {noteId}.json                   # 每篇原始媒体 JSON(供 Excel/合并用)
└── xhs_media_20260618_HHMMSS.xlsx  # 媒体汇总表

复用 xhs-crawler(不 import,逐字复制)

xhs_media_batch.py 顶部标 SYNC 的 helper(safe_js/safe_cdp/jitter/verify_click_target/click_card_with_verify/wait_mask_gone)逐字复制自 xhs-crawler/scripts/xhs_crawl.py。

为什么复制不 import:① import xhs_crawl 会触发它 module-level 的爬取(它无 __main__ 守卫);② harness exec 时 __name__!='__main__',守卫语义不同。详见 references/gotchas.md #10、#11。改这些函数要同步回 xhs_crawl.py。

关键坑点

完整列表见 references/gotchas.md,最关键的:

  • 签名 URL 过期:masterUrl 含 ?sign=&t=,当篇立即下,Referer=https://www.xiaohongshu.com/;失败换 backupUrls。
  • 取 video.media.stream.h264[0].masterUrl,不是 consumer.originVideoKey(实测不存在)。
  • js() 只 return 小对象:__INITIAL_STATE__ 整包很大,整包 return 爆 "chain too long"。
  • xsec_token 只能 click 卡片获得,不能直接 goto /explore/{id}(404)。
  • blob 视频只能从 __INITIAL_STATE__ 拿,不能从 <video> 抓。

可选后处理(非强制依赖)

下载完成后,可对媒体做文字提取。脚本复制自 redbook-download/,主下载路径不调用它们:

视频转写(Groq Whisper)

export GROQ_API_KEY='...'
python3 scripts/video_transcriber.py xhs_media_data/20260618_关键词/001_*/
# → 生成 视频_{noteId}.txt(>25MB 自动用 ffmpeg 切 5 分钟)

图片 OCR(OCR.space)

export OCR_SPACE_API_KEY='...'   # 不设则用免费 demo key
python3 scripts/ocr_processor.py xhs_media_data/20260618_关键词/001_*/
# → 生成 图片_NN.txt(跳过头像)

文件

文件用途
scripts/xhs_media_extract.py读 __INITIAL_STATE__ 取媒体字段(PRIMARY)+ HTTP fallback;cookies 拆分;可 pipe 单篇、可 import
scripts/xhs_media_batch.py单会话批量驱动(点击/验证/关闭复制自 xhs_crawl)+ 异步入队 + 重取兜底 worker
scripts/xhs_media_download.py下载器(视频/原图/头像)+ DownloadPool 线程池 + 汇总 Excel;纯 CLI,可续传
scripts/video_transcriber.py可选:Groq Whisper,mp4→txt
scripts/ocr_processor.py可选:OCR.space,jpg→txt
references/initial-state.md__INITIAL_STATE__ 字段地图(实测校验)
references/gotchas.md11 个已知坑点

已知限制

  • 视频是有水印的 h264 流源(originVideoKey 无水印源在实测里拿不到);如需无水印需另寻接口。
  • 评论区不在这个 skill 范围(用 xhs-crawler)。
  • 视频转写受 Groq 25MB/5min 限制(仅转写阶段,下载不受限)。
  • CDP 长 session 每 5 篇重连一次。

参考

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.