
Xiaohongshu Crawler
- 260 installs
- 316 repo stars
- Updated August 4, 2026
- redfox-data/redfox-community
Crawls popular Xiaohongshu posts by keyword with date-range and sort filters and returns the results as a structured table.
About
Scrapes trending Xiaohongshu note data by keyword with date and sort filtering. Creators use it to search RED for viral posts and popular content on a topic.
- Keyword search with date-range and sort-order filters
- Results shown as a structured table of notes
Xiaohongshu Crawler by the numbers
- 260 all-time installs (skills.sh)
- +19 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #882 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/redfox-data/redfox-community --skill xiaohongshu-crawlerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 260 |
|---|---|
| repo stars | ★ 316 |
| Last updated | August 4, 2026 |
| Repository | redfox-data/redfox-community ↗ |
What it does
Crawls popular Xiaohongshu posts by keyword with date-range and sort filters and returns the results as a structured table.
Files
小红书作品爬取
📝 简介
根据关键词爬取小红书热门作品,支持按日期范围筛选和多种排序方式(相关性、最新、最热),返回笔记标题、作者、互动数据等,结果以结构化表格展示。
✨ 功能特性
| 功能模块 | 能力描述 | 核心价值 |
|---|---|---|
| 作品爬取 | 关键词搜索小红书作品 | 精准发现高热度内容 |
| 日期筛选 | 支持按日期范围筛选 | 定位特定时间段的热门内容 |
| 多种排序 | 相关性/最新/最热三种排序 | 灵活满足不同筛选需求 |
| 互动数据 | 返回收藏、分享、评论、点赞等 | 全面评估作品热度 |
| 报告导出 | CSV(Excel兼容)+ HTML可视化报告 | 本地离线查看与分享 |
🔑 鉴权
- 获取 API Key:前往 红狐hub
- 配置方式1:写入
~/.openclaw/openclaw.json→{ "env": { "REDFOX_API_KEY": "ak_xxxx..." } } - 配置方式2:终端执行
export REDFOX_API_KEY="ak_xxxx..."
⚙️ 工作流程
Step 1: 🔍 用户意图理解
从用户输入中提取以下参数:
| 参数 | 提取规则 | 默认值 |
|---|---|---|
| keyword | 用户提到的搜索关键词 | 空字符串 "" |
| startDate | 起始日期,解析为 YYYY-MM-DD | 30天前(脚本自动计算) |
| endDate | 结束日期,解析为 YYYY-MM-DD | 今天(脚本自动计算) |
| sortType | 排序方式,见下表 | "_0" |
sortType 枚举:
| 值 | 含义 | 触发词 |
|---|---|---|
_0 | 相关性排序(默认) | 默认排序、综合排序、相关 |
_2 | 最新(按发布时间排序) | 最新、 newest、按时间 |
_4 | 最热(按互动数排序) | 最热、最火、hottest、按热度 |
时间解析规则:
| 用户输入 | 解析方式 | 示例 |
|---|---|---|
| 绝对日期 | 解析为 YYYY-MM-DD | 6月1号 → 2026-06-01 |
| 相对日期 | 基于当前日期计算 | 最近7天 → start=7天前, end=今天 |
| 日期范围 | 分别解析为 startDate/endDate | 5月30日到6月2日 |
| 未提供时间 | 不传 --start-date/--end-date,脚本默认查最近30天 | 不传参数 |
Step 1.5: 🧠 关键词类型判断(⚠️ 调用脚本前必须执行)
核心规则:泛化词必须先询问再查询,具体词直接查询。
关键词分类:
| 类型 | 特征 | 处理方式 |
|---|---|---|
| 空关键词 | 用户未提供关键词(如「最近热门笔记」) | 直接查询全站热门,keyword 传 "" |
| 细分词 | 含具体场景/属性/人群修饰(如「减脂餐」「小个子穿搭」「生酮饮食」) | 直接调用脚本 |
| 泛化词 | 纯大类词,无修饰成分(如「美食」「穿搭」「美妆」) | 必须先推荐细分词,等待用户回复后再查询 |
泛化词处理流程(⚠️ 禁止直接调用脚本!):
第一步:生成 10 个细分词推荐
生成原则:
- 词的大小适中,避免过细(查不到数据)或过泛
- 必须覆盖不同场景:趋势词、人群词、场景词、意图词各 2-3 个
输出示例:
我识别到「美食」是较大的分类,推荐以下细分方向:
家常菜、烘焙甜点、减脂餐、早餐、宵夜、地方菜系、懒人食谱、宝宝辅食、火锅、咖啡饮品
回复「拓展」将同时搜索这 10 个词,回复「不拓展」将继续搜索「美食」
第二步:等待用户回复(禁止在同一次对话中继续执行脚本)
- 用户回复「拓展」 → 调用脚本搜索 10 个细分词(逗号分隔)
- 用户回复「不拓展」 → 调用脚本搜索原关键词
Step 2: 📡 调用脚本
# 基础调用(仅关键词,日期默认最近30天,排序默认相关性)
python3 ~/.qoderwork/skills/xiaohongshu-crawler/scripts/crawl_xhs.py "<关键词>"
# 完整参数(仅传入非空的时间/排序参数)
python3 ~/.qoderwork/skills/xiaohongshu-crawler/scripts/crawl_xhs.py "<关键词>" --start-date <startDate> --end-date <endDate> --sort-type <_0|_2|_4>脚本返回 JSON,包含 articles 数组、total 总数,以及以下辅助字段:
| 字段 | 类型 | 说明 |
|---|---|---|
articles | 数组 | 作品数据(主要展示内容) |
relatedSearches | 数组 | 相关搜索词推荐 |
latestHotArticles | 数组 | 近期热门笔记推荐(辅助内容,默认展示 10 条) |
hotTopics | 数组 | 热门话题标签(仅供参考,不在对话中展示) |
每条作品字段(脚本内部字段 ↔ 接口原始字段):
| 脚本字段 | 接口原始字段 | 说明 |
|---|---|---|
title | title | 笔记标题 |
author | authorNickname | 作者昵称 |
collect_count | collectedCount | 收藏数 |
share_count | sharedCount | 分享数 |
comment_count | commentsCount | 评论数 |
like_count | likedCount | 点赞数 |
publish_time | createTime | 发布时间 |
work_url | shareInfoLink | 作品链接 |
cover | cover | 封面图 URL |
desc | desc | 笔记描述/话题标签 |
author_fans | authorFans | 作者粉丝数 |
interactive_count | interactiveCount | 互动总数 |
work_id | id | 作品 ID |
Step 3: 📊 结果展示
⚠️ 总数校验:展示的 N 必须取自 articles 数组长度,禁止人工计数。 ⚠️ 强制输出规则:直接读取脚本返回的 JSON 数据,按对应策略输出,禁止添加额外分析或建议。
情况 A:articles ≥ 20
A1. 输出查询范围:
📊 关键词「XXX」共爬取到 N 条小红书作品,以下是详细数据:
A2. 输出风控提示(紧跟 A1 之后、表格之前):
!!!受小红书风控规则限制,部分作品链接可能无法正常跳转,您可复制对应作品标题前往小红书搜索查看,感谢理解🙇♀️🙇♀️
A3. 展示前 20 条表格:
| # | 笔记标题 | 作者 | 收藏 | 分享 | 评论 | 点赞 | 发布时间 |
|---|---------|------|------|------|------|------|----------|
| 1 | [标题](work_url) | 作者名 | 1.2w | 3.5w | 8000 | 5.6w | 06-02 19:55 |A4. 提示剩余数据:
以上展示了前 20 条数据,还剩 M 条未展示。回复「查看全部」展开剩余数据。
A5. 推荐细分赛道(基于当前关键词生成 10 个相关细分方向词):
如需深入探索某个细分方向,可以从以下关键词中选择:
{细分词1}、{细分词2}、...、{细分词10}
回复具体关键词,我将为您查询。
情况 B:0 < articles < 20
B1. 输出查询范围 + 数据较少提示:
📊 关键词「XXX」共爬取到 N 条小红书作品。
💡 当前关键词在该时间段结果较少,您可以尝试更换更短的关键词或扩大时间范围。
B2. 输出风控提示:
!!!受小红书风控规则限制,部分作品链接可能无法正常跳转,您可复制对应作品标题前往小红书搜索查看,感谢理解🙇♀️🙇♀️
B3. 展示全部表格(同 A3 格式)。
B4. 推荐细分赛道(同 A5)。
情况 C:articles = 0
😔 抱歉,未找到与「XXX」相关的小红书作品,请尝试更换关键词或调整时间范围。
C1. 推荐搜索词(从 relatedSearches 字段提取,以加粗形式展示):
🔍 推荐搜索词:词1、词2、词3...
⚠️ 若 relatedSearches 为空则不展示此条。C2. 推荐热门笔记(从 latestHotArticles 字段取前 10 条,表格不含评分字段):
💡 我们为您推荐了近期的热门笔记供参考:
| # | 笔记标题 | 作者 | 收藏 | 分享 | 评论 | 点赞 | 发布时间 |
|---|---------|------|------|------|------|------|----------|
| 1 | [标题](work_url) | 作者名 | 1.2w | 3.5w | 8000 | 5.6w | 06-02 19:55 |⚠️ 若 latestHotArticles 为空则不展示此块。C3. 推荐热门赛道(从 hotTopics 字段提取;若为空则使用以下默认列表):
📈 您还可以尝试搜索以下热门赛道:
穿搭、美食、彩妆、影视、职场、萌宠、家居、旅行、运动、科技、互联网、医疗保健、教育、亲子育儿、生活
格式化规则(适用于所有情况):
- 数字 < 10000 直接展示;≥ 10000 用
x.xw格式 publish_time转为MM-DD HH:MM;无论是否指定时间范围均展示- 标题超 30 字截断加
...,使用[标题](work_url)链接格式 - 标题为空时显示“-”
Step 4: 📦 更多操作
展示完结果后,末尾追加以下内容:
⚡ 更多操作
• 是否需要下载 Excel 文件或 HTML 可视化报告?便于您在浏览器中打开查看
• 本次共 N 条作品,是否需要查看剩余 M 条?⚠️ 第二条仅在 articles > 20 时展示,N 为总数,M = N - 20。
4.1 下载报告
将爬取结果 JSON 写入临时文件,再调用报告生成脚本:
echo '<JSON数据>' > /tmp/xhs_crawl_data.json
python3 ~/.qoderwork/skills/xiaohongshu-crawler/scripts/generate_report.py "<关键词>" \
--input /tmp/xhs_crawl_data.json --format <csv|html|both>--format | 生成文件 | 告知用户示例 |
|---|---|---|
csv | CSV(Excel 兼容) | ✅ Excel(CSV)文件已生成:~/Downloads/XhsCrawl/小红书作品_关键词_时间戳.csv |
html | HTML 可视化报告 | ✅ HTML 可视化报告已生成:~/Downloads/XhsCrawl/小红书作品_关键词_时间戳.html |
both | CSV + HTML | 同时告知两个文件路径 |
4.2 用户回复「查看全部」
直接展示剩余数据(从第 21 条开始),使用与 A3 相同的表格格式,不再追加「更多操作」提示。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小红书作品爬取 · {{KEYWORD}} | {{DATE}}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #faf5f2; --bg-card: #ffffff; --bg-card-hover: #fff7f3;
--text: #1a1a1a; --text-secondary: #6b6b6b; --text-muted: #9e9e9e;
--accent: #ff2442; --accent-light: rgba(255,36,66,0.08);
--accent-border: rgba(255,36,66,0.2); --border: rgba(0,0,0,0.06);
--radius: 14px; --radius-sm: 10px;
--shadow: 0 2px 12px rgba(0,0,0,0.06); --shadow-hover: 0 4px 20px rgba(255,36,66,0.12);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
.header { text-align: center; padding: 2.5rem 1.5rem 1.2rem; background: linear-gradient(135deg, #ff2442 0%, #ff6b6b 100%); color: white; }
.header h1 { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.5px; }
.header .sub { font-size: 0.85rem; opacity: 0.85; margin-top: 0.3rem; }
.stats-bar { display: flex; justify-content: center; gap: 2.5rem; padding: 1.2rem; flex-wrap: wrap;
background: white; border-bottom: 1px solid var(--border); }
.stat-item { text-align: center; }
.stat-value { font-size: 1.4rem; font-weight: 700; color: var(--accent); }
.stat-label { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
.toolbar { position: sticky; top: 0; z-index: 100; background: rgba(250,245,242,0.92);
backdrop-filter: blur(12px); padding: 0.8rem 1.5rem; border-bottom: 1px solid var(--border); }
.toolbar-inner { max-width: 960px; margin: 0 auto; display: flex; gap: 0.7rem; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.sort-btns { display: flex; gap: 0.4rem; }
.sort-btn { padding: 0.55rem 1rem; border: 1.5px solid var(--border); border-radius: var(--radius);
background: white; font-size: 0.8rem; font-family: inherit; color: var(--text-secondary);
cursor: pointer; transition: all 0.15s; white-space: nowrap; }
.sort-btn:hover { border-color: var(--accent-border); color: var(--accent); }
.sort-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.container { max-width: 960px; margin: 0 auto; padding: 1.5rem; }
.results-info { font-size: 0.82rem; color: var(--text-muted); margin-bottom: 1rem; }
/* Table View */
.table-wrap { overflow-x: auto; border-radius: var(--radius); box-shadow: var(--shadow); }
table { width: 100%; border-collapse: collapse; background: white; font-size: 0.85rem; }
thead { background: #fafafa; position: sticky; top: 0; }
th { padding: 0.85rem 0.9rem; text-align: left; font-weight: 600; color: var(--text-secondary);
border-bottom: 2px solid var(--border); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.3px; white-space: nowrap; }
td { padding: 0.8rem 0.9rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
tr:hover td { background: var(--bg-card-hover); }
.title-link { color: var(--text); text-decoration: none; font-weight: 500; }
.title-link:hover { color: var(--accent); }
.cat-tag { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 0.72rem;
background: var(--accent-light); color: var(--accent); border: 1px solid var(--accent-border); }
/* Card View removed */
.empty { text-align: center; padding: 4rem 1rem; color: var(--text-muted); }
.title-fallback { color: var(--text-secondary); font-style: italic; }
.footer { text-align: center; padding: 2rem; font-size: 0.72rem; color: var(--text-muted); border-top: 1px solid var(--border); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.fade-in { animation: fadeIn 0.25s ease both; }
@media (max-width: 640px) {
.header h1 { font-size: 1.3rem; }
.stats-bar { gap: 1.5rem; }
.toolbar-inner { flex-direction: row; justify-content: flex-end; }
}
</style>
</head>
<body>
<div class="header">
<h1>📕 小红书作品爬取</h1>
<p class="sub">关键词「{{KEYWORD}}」</p>
</div>
<div class="stats-bar">
<div class="stat-item"><div class="stat-value" id="sTotal">{{TOTAL_COUNT}}</div><div class="stat-label">作品总数</div></div>
<div class="stat-item"><div class="stat-value" id="sShown">{{TOTAL_COUNT}}</div><div class="stat-label">当前展示</div></div>
<div class="stat-item"><div class="stat-value" id="sAvgLike">-</div><div class="stat-label">平均点赞</div></div>
</div>
<div class="toolbar">
<div class="toolbar-inner">
<div class="sort-btns">
<button class="sort-btn active" data-sort="like" onclick="sortBy('like',this)">👍 点赞</button>
<button class="sort-btn" data-sort="collect" onclick="sortBy('collect',this)">⭐ 收藏</button>
<button class="sort-btn" data-sort="share" onclick="sortBy('share',this)">📤 分享</button>
<button class="sort-btn" data-sort="comment" onclick="sortBy('comment',this)">💬 评论</button>
<button class="sort-btn" data-sort="time" onclick="sortBy('time',this)">🕐 最新</button>
</div>
</div>
</div>
<div class="container">
<div class="results-info" id="resultsInfo"></div>
<div id="tableView" class="table-wrap">
<table>
<thead><tr>
<th>#</th><th>笔记标题</th><th>作者</th>
<th>收藏</th><th>分享</th><th>评论</th><th>点赞</th><th>发布时间</th>
</tr></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div id="emptyView" class="empty" style="display:none">😔 没有匹配的作品,请尝试其他关键词</div>
</div>
<footer class="footer">Generated at {{TIMESTAMP}} · Powered by redfox.hk</footer>
<script>
const RAW = {{WORKS_DATA}};
let data = (RAW||[]).map((w,i) => ({...w, _idx: i+1}));
let filtered = [...data];
let currentSort = 'like';
const $ = id => document.getElementById(id);
function fmt(n) {
if (n == null) return '0';
n = Number(n);
if (isNaN(n)) return '0';
if (n >= 10000) return (n/10000).toFixed(1)+'w';
return n.toLocaleString();
}
function esc(s) { const d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
function fmtTime(t) { if(!t) return '-'; return t.replace(/^\d{4}-/,'').replace(/:\d{2}$/,'').replace(/-/g,'/'); }
function sortData(arr, by) {
const keyMap = {like:'like_count',collect:'collect_count',share:'share_count',comment:'comment_count',time:'publish_time'};
const k = keyMap[by]||'like_count';
return [...arr].sort((a,b) => {
if (by==='time') return (b[k]||'').localeCompare(a[k]||'');
return (Number(b[k])||0) - (Number(a[k])||0);
});
}
function renderTable() {
const tb = $('tableBody');
tb.innerHTML = '';
filtered.forEach((w,i) => {
const tr = document.createElement('tr');
tr.className = 'fade-in';
tr.style.animationDelay = Math.min(i*0.02,0.5)+'s';
const titleDisplay = w.title && w.title !== '-'
? (w.work_url ? `<a class="title-link" href="${esc(w.work_url)}" target="_blank">${esc(w.title)}</a>` : esc(w.title))
: `<span class="title-fallback">-</span>`;
tr.innerHTML = `<td>${i+1}</td>
<td>${titleDisplay}</td>
<td>${esc(w.author)||'-'}</td>
<td>${fmt(w.collect_count)}</td><td>${fmt(w.share_count)}</td>
<td>${fmt(w.comment_count)}</td><td>${fmt(w.like_count)}</td>
<td>${fmtTime(w.publish_time)}</td>`;
tb.appendChild(tr);
});
}
function render() {
const has = filtered.length > 0;
$('tableView').style.display = has ? 'block' : 'none';
$('emptyView').style.display = has ? 'none' : 'block';
$('sShown').textContent = filtered.length;
$('resultsInfo').textContent = `共 ${data.length} 条作品`;
if (has) renderTable();
}
function sortBy(by, btn) {
currentSort = by;
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
filtered = sortData(filtered, by);
render();
}
function doFilter() {
filtered = sortData([...data], currentSort);
render();
}
// 计算平均点赞
const likes = data.map(w => Number(w.like_count)||0);
const avg = likes.length ? Math.round(likes.reduce((a,b)=>a+b,0)/likes.length) : 0;
$('sAvgLike').textContent = fmt(avg);
// 初始渲染
filtered = sortData(filtered, 'like');
render();
</script>
</body>
</html>
Xiaohongshu Content Crawler / xiaohongshu-crawler
---
Introduction
Crawl trending Xiaohongshu (Little Red Book) posts by keyword, with date range filtering and multiple sorting options. Returns note titles, authors, engagement metrics, and more in a structured table format. Also supports CSV and HTML visual report export for offline viewing and sharing.
Core Value
- Precise content discovery: Triple filtering by keyword, date range, and sorting to quickly find high-engagement notes.
- Smart intent understanding: Automatically recommends sub-categories when broad terms are used; queries site-wide trending when no keyword is provided.
- Tiered display strategy: Shows top 20 with pagination when data is sufficient; provides expansion suggestions when results are few; recommends related searches and trending notes when no results found.
- One-click report export: Supports Excel (CSV) and HTML visual reports, viewable directly in your browser.
Ideal For
- 📝 Xiaohongshu Creators — Crawl similar trending posts by topic keyword for content inspiration.
- 🛍️ Brand / E-commerce Teams — Competitive content research to understand trending notes and engagement in specific categories.
- 🏢 MCN / Content Planners — Cross-category batch crawling to plan creator matrix content direction.
---
Features
Core Capabilities
- Keyword search: Supports exact keywords, multi-keyword combinations (comma-separated), and site-wide trending queries (empty keyword).
- Date range filtering: Supports absolute dates, relative dates (e.g., "last 7 days"), and date ranges. Defaults to the last 30 days.
- Multiple sorting options: Relevance (default), Latest (by publish time), and Hottest (by engagement) — switch flexibly as needed.
- Keyword generalization detection: When broad terms are entered (e.g., "food", "fashion"), recommends 10 sub-categories for your selection before searching.
- Tiered result display: ≥ 20 results shows top 20 + pagination prompt; 1-19 results shows all + expansion suggestions; 0 results shows related searches + trending notes + trending categories.
- Report export: One-click generation of CSV (Excel compatible) and HTML visual reports.
- Complete engagement data: Each post returns saves, shares, comments, likes, publish time, and more.
---
API Key Acquisition & Security
- This skill requires the environment variable:
REDFOX_API_KEY. REDFOX_API_KEYis provided by RedFoxHub (https://redfox.hk).- Visit RedFoxHub to register and obtain your
REDFOX_API_KEY. - Configure the
REDFOX_API_KEYenvironment variable on your device before using this skill. - Before providing your key, verify its source, scope, validity period, and whether it supports reset/revocation.
- Never hardcode or expose your key in code, prompts, logs, or output files.
---
Usage Guide
Simply describe what you want to search for in natural language — no need to memorize specific commands.
Common Phrases
| Intent | Example | Result |
|---|---|---|
| Search by keyword | "Crawl trending weight-loss meal posts from the past week" | Returns results sorted by relevance with keyword + last 7 days |
| Sort by popularity | "Show me the hottest fashion posts" | Sorted by engagement, highest interaction notes first |
| Broad term refinement | "Crawl food-related posts" | Recommends 10 sub-categories first, searches after your confirmation |
| Site-wide trending | "What's trending on Xiaohongshu lately" | Queries site-wide popular content with empty keyword |
| Specify date range | "Crawl skincare posts from May 20 to June 1" | Precise filtering by specified date range |
| Download reports | "Download Excel and HTML reports" | Generates CSV + HTML visual report files |
Output Example
After a query, you'll receive a structured table like this (illustrative):
| # | Note Title | Author | Saves | Shares | Comments | Likes | Publish Time |
|---|---|---|---|---|---|---|---|
| 1 | Weekly Meal Prep Recipes | Healthy Food Expert | 1.2w | 3500 | 800 | 5.6w | 06-02 19:55 |
| 2 | Office Worker Bento Collection | Foodie Worker | 8500 | 2100 | 320 | 3.2w | 06-01 12:30 |
---
Use Cases
| Scenario | Role | Example Prompt | Benefit |
|---|---|---|---|
| Topic research | Xiaohongshu creator | "Crawl trending petite fashion posts from the past week" | Understand content direction and engagement patterns in your niche |
| Competitive analysis | Brand operations | "Crawl sunscreen-related notes from the last 30 days, sorted by popularity" | Gain insights into category trends and user interests |
| Category exploration | MCN planner | "Show me what's trending in the food category recently" | Refine sub-categories first, then crawl for precise content opportunities |
| Data archiving | Content operations | "Download the HTML visual report after crawling" | Offline access to complete data for team sharing and review |
---
小红书作品爬取 / xiaohongshu-crawler
---
简介
根据关键词爬取小红书热门作品,支持按日期范围筛选和多种排序方式,返回笔记标题、作者、互动数据等,结果以结构化表格展示。同时支持 CSV 和 HTML 可视化报告导出,便于离线查看与分享。
核心价值
- 精准发现热门内容:通过关键词 + 日期 + 排序三重筛选,快速锁定高热度笔记。
- 智能意图理解:输入大类词时自动推荐细分方向,避免泛泛搜索;空关键词时查询全站热门。
- 分级展示策略:数据充足时展示前 20 条并提示翻页;数据较少时给出拓展建议;无结果时推荐相关搜索词和热门笔记。
- 报告一键导出:支持 Excel(CSV)和 HTML 可视化报告,本地浏览器即可打开查看。
适用对象
- 📝 小红书博主 — 按选题关键词爬取同类爆款,为创作提供数据参考。
- 🛍️ 品牌 / 电商运营 — 竞品内容调研,了解特定品类的热门笔记与互动表现。
- 🏢 MCN / 内容策划 — 跨赛道批量爬取,规划达人矩阵内容方向。
---
功能特性
核心功能
- 关键词搜索:支持精确关键词、多关键词组合(逗号分隔)、全站热门查询(空关键词)。
- 日期范围筛选:支持绝对日期、相对日期(如「最近 7 天」)、日期范围,默认查询最近 30 天。
- 多种排序方式:相关性(默认)、最新(按发布时间)、最热(按互动数)三种排序灵活切换。
- 关键词泛化识别:输入大类词(如「美食」「穿搭」)时,先推荐 10 个细分方向供你选择,确认后再搜索。
- 分级结果展示:数据 ≥ 20 条展示前 20 条 + 翻页提示;1-19 条展示全部 + 拓展建议;0 条推荐搜索词 + 热门笔记 + 热门赛道。
- 报告导出:支持 CSV(Excel 兼容)和 HTML 可视化报告一键生成。
- 互动数据完整:每条作品返回收藏、分享、评论、点赞、发布时间等完整数据。
---
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk) 提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
---
使用指南
直接用自然语言描述你想搜索的内容即可,无需记忆固定命令。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 按关键词搜索 | 「帮我爬取最近一周减脂餐相关的作品」 | 按关键词 + 近 7 天 + 相关性排序返回结果 |
| 按热度排序 | 「看看穿搭赛道最火的作品」 | 按最热排序,优先展示高互动笔记 |
| 大类先细分 | 「爬取美食相关作品」 | 先推荐 10 个细分方向,你确认后再搜索 |
| 查看全站热门 | 「最近小红书有什么热门作品」 | 空关键词查询全站热门内容 |
| 指定日期范围 | 「爬取 5 月 20 号到 6 月 1 号的护肤作品」 | 按指定日期范围精准筛选 |
| 下载报告 | 「下载 Excel 和 HTML 报告」 | 生成 CSV + HTML 可视化报告文件 |
输出示例
查询完成后,你将收到如下结构化表格(示意):
| # | 笔记标题 | 作者 | 收藏 | 分享 | 评论 | 点赞 | 发布时间 |
|---|---|---|---|---|---|---|---|
| 1 | 一周减脂餐食谱分享 | 健康饮食达人 | 1.2w | 3500 | 800 | 5.6w | 06-02 19:55 |
| 2 | 打工人减脂便当合集 | 上班族美食 | 8500 | 2100 | 320 | 3.2w | 06-01 12:30 |
---
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 选题调研 | 小红书博主 | 「帮我爬取近一周小个子穿搭的热门作品」 | 了解同类爆款的内容方向与互动表现 |
| 竞品分析 | 品牌运营 | 「爬取最近 30 天防晒霜相关笔记,按热度排序」 | 掌握品类热门内容与用户关注点 |
| 赛道探索 | MCN 策划 | 「看看美食赛道最近有什么热门作品」 | 先细分方向再爬取,精准定位内容机会 |
| 数据沉淀 | 内容运营 | 「爬取完帮我下载 HTML 可视化报告」 | 离线查看完整数据,便于团队分享与复盘 |
---
#!/usr/bin/env python3
"""
小红书作品爬取脚本
调用 Redfox API 爬取小红书热门作品数据
用法: python3 crawl_xhs.py "<关键词>" [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--sort-type _0|_2|_4]
"""
import sys
import os
import json
import argparse
from datetime import datetime, timedelta
try:
import requests
except ImportError:
print("[error] 缺少 requests 库,请执行: pip3 install requests", file=sys.stderr)
sys.exit(1)
API_URL = "https://redfox.hk/story/api/xhs/crawl/work"
def get_api_key() -> str:
"""从环境变量获取 API Key"""
val = os.environ.get("REDFOX_API_KEY")
if val:
return val
print("[error] 未找到环境变量 REDFOX_API_KEY,请确认已设置 API Key", file=sys.stderr)
sys.exit(1)
def format_works(works: list) -> list:
"""将原始数据转换为统一格式"""
items = []
for w in works:
title = (w.get("title") or "").strip() or "-"
items.append({
"title": title,
"author": (w.get("authorNickname") or "").strip(),
"collect_count": w.get("collectedCount", 0) or 0,
"share_count": w.get("sharedCount", 0) or 0,
"comment_count": w.get("commentsCount", 0) or 0,
"like_count": w.get("likedCount", 0) or 0,
"publish_time": w.get("createTime", ""),
"work_url": w.get("shareInfoLink", ""),
"cover": w.get("cover", ""),
"desc": w.get("desc", ""),
"author_fans": w.get("authorFans", 0) or 0,
"interactive_count": w.get("interactiveCount", 0) or 0,
"work_id": w.get("id", ""),
})
items.sort(key=lambda x: x["like_count"], reverse=True)
return items
def get_default_date_range() -> tuple:
"""返回默认日期范围:最近30天"""
today = datetime.now()
start = today - timedelta(days=30)
return start.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")
def crawl(keyword: str, start_date: str = "", end_date: str = "", sort_type: str = "_0") -> dict:
"""调用爬取接口,返回作品数据"""
if not start_date and not end_date:
start_date, end_date = get_default_date_range()
api_key = get_api_key()
payload = {
"keyword": keyword,
"startDate": start_date,
"endDate": end_date,
"source": "小红书作品爬取-GitHub",
"sortType": sort_type,
}
headers = {
"Content-Type": "application/json",
"X-API-Key": api_key,
"User-Agent": "QoderWork/1.0",
}
try:
resp = requests.post(API_URL, json=payload, headers=headers, timeout=15, verify=True)
resp.raise_for_status()
result = resp.json()
except requests.exceptions.HTTPError as e:
print(f"[error] HTTP {resp.status_code}: {resp.text}", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectionError as e:
print(f"[error] 网络请求失败: {e}", file=sys.stderr)
sys.exit(1)
except requests.exceptions.Timeout:
print("[error] 请求超时,请稍后重试", file=sys.stderr)
sys.exit(1)
except (json.JSONDecodeError, KeyError, TypeError) as e:
print(f"[error] 数据解析异常: {e}", file=sys.stderr)
sys.exit(1)
code = result.get("code")
if code != 2000:
print(f"[error] 接口返回错误: code={code}, msg={result.get('msg', '未知')}", file=sys.stderr)
sys.exit(1)
data = result.get("data") or {}
raw_list = (
data.get("works")
or data.get("list")
or data.get("articles")
or []
)
raw_hot = data.get("latestHotArticles") or []
return {
"articles": format_works(raw_list),
"total": len(raw_list),
"relatedSearches": data.get("relatedSearches") or [],
"latestHotArticles": format_works(raw_hot),
"hotTopics": data.get("hotTopics") or [],
}
def main():
parser = argparse.ArgumentParser(description="小红书作品爬取脚本")
parser.add_argument("keyword", help="搜索关键词")
parser.add_argument("--start-date", "-s", default="", help="起始日期,格式 YYYY-MM-DD(默认:30天前)")
parser.add_argument("--end-date", "-e", default="", help="结束日期,格式 YYYY-MM-DD(默认:今天)")
parser.add_argument("--sort-type", "-t", default="_0", choices=["_0", "_2", "_4"],
help="排序方式: _0=相关性(默认), _2=最新, _4=最热")
args = parser.parse_args()
keyword = args.keyword.strip()
result = crawl(keyword, start_date=args.start_date, end_date=args.end_date, sort_type=args.sort_type)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
小红书作品爬取 - 报告生成脚本
从 JSON 数据生成 CSV(Excel 兼容)和 HTML 可视化报告
用法: python3 generate_report.py "<关键词>" --input data.json [--output-dir ~/Downloads/XhsCrawl]
"""
import sys
import os
import json
import csv
import argparse
from datetime import datetime
from pathlib import Path
DEFAULT_OUTPUT_DIR = Path.home() / "Downloads" / "XhsCrawl"
TEMPLATE_PATH = Path(__file__).parent.parent / "assets" / "report_template.html"
# ─── CSV 导出 ────────────────────────────────────────────────────────────────────
def export_csv(works: list, keyword: str, output_dir: Path) -> Path:
output_dir.mkdir(parents=True, exist_ok=True)
date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_kw = "".join(c for c in keyword if c.isalnum() or c in " _-")[:20] or "小红书"
filename = f"小红书作品_{safe_kw}_{date_str}.csv"
filepath = output_dir / filename
fieldnames = ["序号", "笔记标题", "作者", "收藏数", "分享数", "评论数", "点赞数",
"互动总数", "作者粉丝数", "发布时间", "描述/话题", "封面图链接", "作品链接"]
with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for i, w in enumerate(works, 1):
writer.writerow({
"序号": i,
"笔记标题": w.get("title", ""),
"作者": w.get("author", ""),
"收藏数": w.get("collect_count", 0),
"分享数": w.get("share_count", 0),
"评论数": w.get("comment_count", 0),
"点赞数": w.get("like_count", 0),
"互动总数": w.get("interactive_count", 0),
"作者粉丝数": w.get("author_fans", 0),
"发布时间": w.get("publish_time", ""),
"描述/话题": w.get("desc", ""),
"封面图链接": w.get("cover", ""),
"作品链接": w.get("work_url", ""),
})
return filepath
# ─── HTML 报告生成 ───────────────────────────────────────────────────────────────
def generate_html(works: list, keyword: str, output_dir: Path) -> Path:
output_dir.mkdir(parents=True, exist_ok=True)
date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_kw = "".join(c for c in keyword if c.isalnum() or c in " _-")[:20] or "小红书"
html_filename = f"小红书作品_{safe_kw}_{date_str}.html"
html_path = output_dir / html_filename
if TEMPLATE_PATH.exists():
template = TEMPLATE_PATH.read_text(encoding="utf-8")
else:
print("[error] HTML 模板文件不存在,请确认 assets/report_template.html 已就位", file=sys.stderr)
sys.exit(1)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
works_json = json.dumps(works, ensure_ascii=False)
html = template
html = html.replace("{{KEYWORD}}", keyword)
html = html.replace("{{DATE}}", datetime.now().strftime("%Y-%m-%d"))
html = html.replace("{{TIMESTAMP}}", timestamp)
html = html.replace("{{TOTAL_COUNT}}", str(len(works)))
html = html.replace("{{WORKS_DATA}}", works_json)
html_path.write_text(html, encoding="utf-8")
return html_path
# ─── 主流程 ───────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="小红书作品报告生成(CSV + HTML)")
parser.add_argument("keyword", help="搜索关键词(用于文件命名)")
parser.add_argument("--input", "-i", required=True,
help="JSON 数据文件路径(crawl_xhs.py 的输出)")
parser.add_argument("--output-dir", "-o", default=str(DEFAULT_OUTPUT_DIR),
help=f"输出目录(默认:{DEFAULT_OUTPUT_DIR})")
parser.add_argument("--format", "-f", choices=["csv", "html", "both"], default="both",
help="输出格式(默认:both)")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"[error] 输入文件不存在: {input_path}", file=sys.stderr)
sys.exit(1)
try:
with open(input_path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"[error] 无法解析 JSON 文件: {e}", file=sys.stderr)
sys.exit(1)
works = data.get("articles") or data.get("works") or []
if not works:
print("[error] 数据为空,无法生成报告", file=sys.stderr)
sys.exit(1)
output_dir = Path(os.path.expanduser(args.output_dir))
keyword = args.keyword.strip()
generated = {}
if args.format in ("csv", "both"):
csv_path = export_csv(works, keyword, output_dir)
generated["csv"] = str(csv_path)
print(f"[csv] {csv_path}")
if args.format in ("html", "both"):
html_path = generate_html(works, keyword, output_dir)
generated["html"] = str(html_path)
print(f"[html] {html_path}")
print(json.dumps({"status": "ok", **generated, "total": len(works)}, ensure_ascii=False))
if __name__ == "__main__":
main()