
Xiaohongshu Dailytop
- 262 installs
- 316 repo stars
- Updated August 4, 2026
- redfox-data/redfox-community
Tracks the top-performing Xiaohongshu notes of the day by category to spot rising content directions and generate topic ideas.
About
Surfaces each day's highest-heat Xiaohongshu notes across categories like beauty, fashion and food. Creators use it to monitor real-time trends and extract reusable topic and format ideas.
- Category-filtered query of the day's hottest notes
- Distills reusable topics, phrasing and formats from top content
Xiaohongshu Dailytop by the numbers
- 262 all-time installs (skills.sh)
- +16 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #880 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-dailytopAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 262 |
|---|---|
| repo stars | ★ 316 |
| Last updated | August 4, 2026 |
| Repository | redfox-data/redfox-community ↗ |
What it does
Tracks the top-performing Xiaohongshu notes of the day by category to spot rising content directions and generate topic ideas.
Files
小红书每日爆款笔记
简介
一句话定位:按关键词或赛道查询小红书当日互动暴涨 TOP50 爆款笔记,自动拆解标题、内容与趋势规律,帮创作者和运营快速抓住流量风口、落地下一篇选题。
通过简单的关键词查询,你可以:
- 分类查询当日热门笔记 :按美妆、穿搭、美食等分类检索小红书当天热度最高的笔记,快速掌握各赛道头部内容。
- 单日爆款笔记分析:查看单日互动增长最快、曝光飙升的笔记详情,分析其标题、封面、内容结构等爆款特征。
- 各领域当日趋势追踪:监测不同领域的实时热度变化,识别正在起量的内容方向,预判下一波流量风口。
- 创作灵感生成:基于当日爆款内容,提炼可复用的选题、话术与呈现形式,直接用于你的下一篇笔记策划。
适用于品牌方、MCN机构、自媒体从业者、内容策划等需要监测小红书热门内容的场景。 ---
功能特性
🎯 核心功能
- 🔥 单日爆发榜查询:获取当日热门笔记TOP50,按分类查询,支持25个垂直领域精准匹配
- 📊 智能规律分析:生成热门内容分析、爆款标题分析、最高数据作品分析三维度报告
- 📈 多维度数据展示:完整呈现互动数、点赞、评论、收藏、分享及新增数据
- 🔔 自动订阅推送:支持每日19:30固定时间推送最新榜单到消息栏
✨ 特色亮点
- 🏷️ 全分类覆盖:综合全部、化妆美容、时尚穿搭、美味佳肴等 25 个主流分类
- 🔍 智能分类匹配:输入「睫毛膏」→ 化妆美容,无需手动选类(对用户不展示匹配结果)
- ⏰ 灵活日期适配:支持指定日期;未指定时按 19:00 更新规则自动判断查询日
- 📄 双格式输出:Markdown 表格 + 按需生成 HTML 页面,自用与汇报均可
---
一键安装
前置条件
- Python 3.8 及以上
requests:pip install requests- 红狐Hub 账号与 API Key(格式
ak_xxxxxxxx,新用户注册赠免费积分)
安装方式
SkillHub / ClawHub(推荐)
1. 前往 SkillHub 或 ClawHub 2. 搜索 xiaohongshu-dailytop 或 小红书每日爆款笔记 3. 按平台指引安装技能包 4. 在对话环境或本机配置 REDFOX_API_KEY(见下方)
WorkBuddy / 龙虾(本地技能)
1. 将本技能目录放入 WorkBuddy 的 skills 路径(如 ~/.workbuddy/skills/xiaohongshu-dailytop) 2. 在对话中通过技能名或描述触发(如「查小红书每日爆款笔记」「睫毛膏爆款」) 3. 配置 REDFOX_API_KEY 后重启终端,确保 Agent 与脚本均可读取
命令行独立使用
pip install requests
cd xiaohongshu-dailytop
# 步骤一:获取数据并写入缓存
python scripts/xhs_daily_fetcher.py --keyword "睫毛膏" --top_n 50 --output_json /tmp/xhs_daily_cache.json
# 步骤二:从缓存生成 HTML(跳过 API 调用)
python scripts/gen_xhs_html.py --keyword "睫毛膏" --top 20 --input_json /tmp/xhs_daily_cache.json环境变量配置
| 变量名 | 必填 | 说明 |
|---|---|---|
REDFOX_API_KEY | 是 | 红狐 API 密钥,格式 ak_xxxxxxxx |
获取 API Key:访问 红狐Hub官网 → 注册/登录 → 个人中心复制 Key。
macOS / Linux:
echo 'export REDFOX_API_KEY=<你的apikey>' >> ~/.zshrc
source ~/.zshrcWindows(PowerShell):
[Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "<你的apikey>", "User")验证:echo $REDFOX_API_KEY(macOS/Linux)或 echo %REDFOX_API_KEY%(Windows)。未生效请重启终端。
Windows Git Bash 注意事项:PowerShell 设置的用户级环境变量 Git Bash 可能无法读取,需在命令中显式注入:
export REDFOX_API_KEY="ak_xxx" && python scripts/xhs_daily_fetcher.py ...---
使用指南
基础使用
1. 发起查询
告诉助手关键词或领域(未指定分类则自动匹配,未指定日期则按 19:00 规则判断):
用户:帮我查一下睫毛膏相关的爆款笔记
>
助手:依次输出每日爆款笔记 TOP20、每日爆款笔记分析、功能服务询问
2. 查看更多
默认 TOP20;数据超过 20 条时可回复「查看更多」:
用户:查看更多
>
助手:展示 TOP21 至榜单末尾
3. 生成 HTML 文件包
在功能服务询问后,回复"2"或"生成":
用户:2
>
助手:生成小红书风格的 HTML 文件包,支持导出 PDF
4. 订阅推送
在功能服务询问后,回复"1"或"订阅":
用户:1
>
助手:已开通每日 19:30 推送小红书每日爆款笔记 TOP50
高级使用
1. 指定日期查询
用户:查 2026-04-20 美妆类每日爆款笔记
2. 泛查询热门内容
用户:最近小红书有什么热门内容
(未指定关键词时使用「综合全部」分类。)
3. 指定分类查询
用户:查时尚穿搭分类昨天的爆发榜
4. 取消订阅
用户:取消订阅
常用命令速查
| 命令 / 用户说法 | 功能 |
|---|---|
查[关键词]爆款 / 每日爆款笔记 | 按关键词查询当日爆发榜(默认 TOP20) |
查[日期] [分类/关键词]爆款 | 指定日期或分类查询 |
查看更多 | 展示 TOP21–末尾 |
1 / 订阅 | 开通每日 19:30 TOP50 推送 |
2 / 生成 | 生成小红书风格 HTML 文件包 |
python scripts/xhs_daily_fetcher.py --keyword "睫毛膏" --top_n 50 --output_json /tmp/cache.json | 命令行拉取榜单数据并写入缓存 |
python scripts/gen_xhs_html.py --keyword "睫毛膏" --top 20 --input_json /tmp/cache.json | 命令行从缓存生成 HTML(跳过 API) |
Agent 执行说明:完整输出顺序、表格格式、功能服务询问文案等硬性约束见 references/core_workflow.md。
---
使用场景
场景一:内容创作者找每日爆款灵感
角色:美妆 / 穿搭博主
需求:快速找到当日互动暴涨的对标笔记与标题套路
使用方式:
1. 输入赛道关键词(如「化妆美容」「穿搭」)查询每日爆款笔记 2. 阅读「爆款标题分析」「最高数据作品分析」提炼可复制结构 3. 导出 HTML 为 PDF 留存参考
预期收益:缩短选题时间,提高笔记点击率与互动率
---
场景二:品牌运营追踪当日热点
角色:快消品牌小红书运营
需求:掌握当日各分类爆发内容,指导投放与种草方向
使用方式:
1. 按品类关键词每日查询爆发榜 TOP20 2. 对比互动、收藏、分享新增数据筛选高转化笔记 3. 订阅每日 19:30 推送,持续跟踪赛道变化
预期收益:热点响应更及时,内容策略与平台节奏对齐
---
场景三:MCN 挖掘潜力笔记与账号
角色:MCN 内容星探
需求:发现低粉高互动的爆发笔记,评估签约价值
使用方式:
1. 查看榜单中作者粉丝数与互动新增对比 2. 结合「最高互动作品分析」判断账号成长潜力 3. 使用 HTML 内链接跳转原文与作者主页二次核实
预期收益:降低漏检成本,提高达人挖掘效率
---
场景四:竞品与赛道日报复盘
角色:增长 / 数据分析
需求:为团队提供可存档的每日爆款数据包
使用方式:
1. 指定日期拉取目标分类 TOP50(先 TOP20,再「查看更多」) 2. 将 HTML 导出 PDF 纳入日报 3. 基于「热门内容分析」归纳封面、选题、热点结合等共性
预期收益:数据口径统一,复盘与汇报材料一键生成
---
项目架构
目录结构
xiaohongshu-dailytop/
├── SKILL.md # 产品说明(本文档)
├── references/
│ └── core_workflow.md # Agent 执行流程、输出规范、分类与日期规则
└── scripts/
├── xhs_daily_fetcher.py # 调用 API 获取榜单数据
├── gen_xhs_html.py # 生成可视化 HTML 页面
└── xhs_weekly.html # HTML 模板资源技术栈
| 项目 | 说明 |
|---|---|
| 运行环境 | Python 3.8+ |
| 核心依赖 | requests |
| 数据来源 | 红狐 API getXhsCozeSkillDataOne |
| 鉴权 | 请求头 X-API-KEY ← 环境变量 REDFOX_API_KEY |
| 部署方式 | SkillHub / ClawHub / WorkBuddy 本地技能 / 命令行 |
核心模块
| 模块 | 职责 |
|---|---|
xhs_daily_fetcher.py | 关键词匹配分类、日期判断、调用 API、输出榜单数据;支持 --output_json 写入 JSON 缓存供 gen_html 复用 |
gen_xhs_html.py | 优先从 --input_json 缓存读取数据生成 HTML(跳过 API 调用),缓存无效时回退到独立 API;支持 --top 20/50 |
core_workflow.md | 四段强制输出、表格/HTML 格式、订阅文案、分类列表、接口调用上限与数据复用规则 |
---
常见问答
安装相关问题
Q1:提示未找到 `REDFOX_API_KEY` 怎么办?
按 一键安装 注册 红狐Hub官网 并配置环境变量;配置后重启终端。若用户不会配置,Agent 应协助写入 ~/.zshrc / ~/.bashrc 或 Windows 用户级环境变量。
Q2:如何验证 API Key 已生效?
执行 echo $REDFOX_API_KEY(macOS/Linux)或 echo %REDFOX_API_KEY%(Windows),确认以 ak_ 开头。
Q3:支持哪些安装/调用方式?
SkillHub、ClawHub、WorkBuddy 本地技能目录,或直接运行 scripts/ 下 Python 脚本。
---
使用相关问题
Q4:榜单数据每天何时更新?
每日 19:00 更新昨日数据。当前时间 ≥ 19:00 查昨日;< 19:00 查前天(未指定日期时)。
Q5:「每日爆款笔记」指标含义是什么?
笔记发布后第 3 天相对第 2 天的互动增量由高到低排序的 TOP50,不是累计总榜。
Q6:为什么只显示 20 条?
首次默认 TOP20;返回超过 20 条时回复「查看更多」可加载剩余,HTML 同步扩展为全量。
Q7:一次查询会输出哪些内容?
固定三部分:每日爆款笔记 → 每日爆款笔记分析(3 模块)→ 功能服务询问。HTML 文件包按需生成。顺序与格式以 core_workflow.md 为准。
---
安全与许可
Q8:API Key 如何保管?
仅通过环境变量读取,勿提交至公开仓库或聊天记录。
Q9:数据使用边界?
榜单以 API/脚本成功返回为准;跳转原文须遵守小红书平台规则与版权要求,不对未验证数据做夸大承诺。
接口调用优化
Q10:一次查询会调用几次 API?
正常有数据场景仅 1 次(fetcher 获取数据并写入缓存,gen_html 从缓存读取跳过 API)。默认日期无数据时脚本内置回退一天,最多 2 次。Agent 禁止在脚本之外自行构造 API 请求。
Q11:gen_html 能否跳过 API 直接使用缓存?
可以。使用 --input_json 参数指定 fetcher 输出的 JSON 缓存文件,gen_html 将完全跳过 API 调用,即使没有 API Key 也能正常生成 HTML。
---
核心工作流程
完整执行规范(触发前提、输出、表格/HTML 模板、功能服务询问原文、分类匹配、日期规则、脚本参数)必须先读取并遵循 references/core_workflow.md,并完整遵循其中的触发规则、输出规范、操作步骤与注意事项。脚本路径相对于技能目录:scripts/xhs_daily_fetcher.py、scripts/gen_xhs_html.py。
执行任务时须严格遵循,包括但不限于:
- 禁止在用户未明确查询或未订阅时自动拉榜
- 必须按顺序输出:爆发榜 → 分析 → 功能服务询问
- 禁止折叠输出;禁止输出 CSV/JSON 等文件
- 表格与 HTML 数据源、顺序必须一致
- HTML 按需生成,文件名格式:小红书每日爆款笔记_{分类}_唯一时间戳.html
注意事项
- 数据时效:每日 19:00 更新;查询日期规则见核心流程
- 输出完整性:三部分缺一不可;功能服务询问须在每日分析之后立即原样输出
- 数据真实性:以脚本/API 返回为准,禁止幻觉补齐排名或互动数
- 合规:遵守平台与版权规范;订阅推送时间为每日 19:30
Xiaohongshu Daily Viral Notes / xiaohongshu-dailytop
---
Introduction
Query the TOP50 Xiaohongshu notes with the highest same-day engagement growth by keyword or track, with automatic breakdown of titles, content patterns, and trends — helping creators and operators catch the traffic wave and plan the next post.
Core Value
- Tracks notes with the highest engagement increment today, not cumulative totals — capturing content that is actively exploding right now
- Three-dimension analysis (hot content, viral titles, top-data posts) delivered in one pass, ready to feed directly into topic planning
- Supports daily 19:30 subscription pushes to continuously track track trend changes
Who It's For
- ✍️ Beauty / fashion and other vertical creators — find daily benchmark notes and extract replicable title structures and content formulas
- 📢 FMCG brand operators — stay on top of today's breakout content to guide placements and seeding direction
- 🔍 MCN talent scouts — spot low-follower high-engagement breakout notes to assess account growth potential
---
Features
Core Capabilities
- Single-day breakout rankings: Fetch today's hot notes TOP50, supporting 25 vertical categories with precise matching
- Smart pattern analysis: Generate three-dimension reports — hot content analysis, viral title analysis, top-data post analysis
- Multi-dimension data display: Full presentation of total engagement, likes, comments, collects, shares, and new incremental data
- Auto subscription push: Subscribe to receive the latest rankings at a fixed time of 19:30 every day
Highlights
- Input "mascara" and it auto-matches "Beauty & Makeup" — no manual category selection needed
- First output shows TOP20; reply "load more" to get the rest, with HTML synced to full data
- Ranking table and HTML page share the same data source and order — fully consistent
---
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 using a key, confirm its source, scope, expiry, and whether it supports reset or revocation.
- Never hard-code or expose the key in plaintext within code, prompts, logs, or output files.
---
Usage Guide
Describe your needs in natural language — no commands to memorize.
Quick Reference
| Intent | Example phrase | Result |
|---|---|---|
| Query today's virals | Show me viral notes about mascara | Auto-matches category, outputs today's breakout TOP20 + three-dimension analysis + HTML + subscription option |
| Query general hot content | What's trending on Xiaohongshu lately? | Uses "All" category, full four-section output |
| Query specific date | Show beauty daily viral notes for 2026-04-20 | Queries single-day breakout rankings for the specified date and category |
| Load more | Reply "load more" | Shows TOP21 to end; HTML refreshes to full data |
| Subscribe to push | Reply 1 after output completes | Activates daily 19:30 push of Xiaohongshu daily viral notes TOP50 |
Output Example
Each query produces four fixed sections: daily viral notes rankings → three-dimension pattern analysis → HTML visual page → subscription service options.
---
Use Cases
| Scenario | Role | Example question | Benefit |
|---|---|---|---|
| Daily viral inspiration | Beauty / fashion creator | Show today's beauty viral notes | Extract today's viral title structures and content patterns for the next post |
| Real-time hot topic tracking | Brand Xiaohongshu operator | Subscribe to daily fashion breakout push | Continuously track track changes, respond to hot topics faster |
| Rising note discovery | MCN talent scout | What low-follower high-engagement food breakouts are there today? | Use new engagement data increments to discover growth-stage accounts |
| Daily data review | Growth / data analyst | Fetch today's full tech TOP50 and export HTML | Export PDF for daily reports with a unified, archivable data source |
小红书每日爆款笔记 / xiaohongshu-dailytop
---
简介
按关键词或赛道查询小红书当日互动暴涨 TOP50 爆款笔记,自动拆解标题、内容与趋势规律,帮创作者和运营快速抓住流量风口、落地下一篇选题。
核心价值
- 追踪当日互动增量最高的笔记,不是累计总榜,捕捉的是正在爆发的内容信号
- 三维度分析(热门内容、爆款标题、最高数据作品)一次性交付,直接用于选题策划
- 支持每日 19:30 订阅推送,持续跟踪赛道热点变化
适用对象
- ✍️ 美妆 / 穿搭等垂类博主 — 每日找对标笔记,提炼可复制的标题结构与内容套路
- 📢 快消品牌运营 — 掌握当日爆发内容,指导投放与种草方向
- 🔍 MCN 内容星探 — 发现低粉高互动爆发笔记,评估账号成长潜力
---
功能特性
核心功能
- 单日爆发榜查询:获取当日热门笔记 TOP50,支持 25 个垂直领域精准匹配
- 智能规律分析:生成热门内容分析、爆款标题分析、最高数据作品分析三维度报告
- 多维度数据展示:完整呈现互动数、点赞、评论、收藏、分享及新增数据
- 自动订阅推送:支持每日 19:30 固定时间推送最新榜单
特色亮点
- 输入「睫毛膏」自动匹配「化妆美容」分类,无需手动选类
- 首次输出 TOP20,回复「查看更多」加载剩余,HTML 同步扩展为全量
- 榜单表格与 HTML 页面数据来源、顺序完全一致
---
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
---
使用指南
直接用自然语言描述需求,无需记忆命令。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 查当日爆款 | 帮我查一下睫毛膏相关的爆款笔记 | 自动匹配分类,输出当日爆发榜 TOP20 + 三维度分析 + HTML + 订阅选项 |
| 查泛热门内容 | 最近小红书有什么热门内容 | 使用「综合全部」分类查询,完整四段输出 |
| 查指定日期 | 查 2026-04-20 美妆类每日爆款笔记 | 指定日期查询对应分类单日爆发榜 |
| 加载更多 | 回复「查看更多」 | 展示 TOP21 至末尾,HTML 同步刷新为全量数据 |
| 订阅推送 | 输出完成后回复 1 | 开通每日 19:30 推送小红书每日爆款笔记 TOP50 |
输出示例
每次查询固定四段输出:每日爆款笔记榜单 → 三维度规律分析 → HTML 可视化页面 → 订阅服务选项。
---
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 每日爆款灵感 | 美妆 / 穿搭博主 | 查一下今天化妆美容爆款笔记 | 提炼当日爆款标题结构与内容特征,直接用于下一篇笔记策划 |
| 当日热点追踪 | 品牌小红书运营 | 订阅穿搭类每日爆款推送 | 持续跟踪赛道变化,热点响应更及时 |
| 潜力笔记挖掘 | MCN 内容星探 | 今天美食类有哪些低粉高互动的爆发笔记? | 结合互动新增数据发现成长型账号 |
| 每日数据复盘 | 增长 / 数据分析 | 查今天科技类 TOP50 并导出 HTML | 导出 PDF 纳入日报,数据口径统一可存档 |
小红书每日爆款笔记 · 核心工作流程
Agent 执行本技能时须完整遵循本文档。用户向导读见 SKILL.md。
任务目标
- 本 Skill 用于:获取小红书平台爆款内容榜单数据
- 能力包含:
- 每日爆款笔记数据获取(按分类查询)
- 每日爆款笔记智能分析
- 冷门爆款笔记挖掘(关键词匹配分类)
- 订阅服务(每日定时推送)
- 触发条件:用户询问小红书热门内容、爆款笔记、各领域趋势分析
数据接口更新规则
- 更新时间:每天19:00更新昨天的榜单数据
- 数据范围:当日的热门笔记TOP50
- 分类覆盖:25个垂直领域分类
- 接口地址:
https://redfox.hk/story/api/cozeSkill/getXhsCozeSkillDataOne - 请求方式:GET(query 参数:
rankDate、source、category);禁止使用 POST(会返回系统内部错误) - 认证方式:请求头
X-API-KEY,从环境变量REDFOX_API_KEY获取;脚本通过requests.get发起请求
鉴权
获取 API Key
1. 访问 RedFox 官网 了解服务详情 2. 前往 注册页面 注册账号 3. 新注册用户将获赠免费积分,可立即开始使用 API 服务 4. 注册登录后,在个人中心获取 API Key,格式为 ak_xxxxxxxx
配置 API Key
REDFOX_API_KEY从环境变量获取,格式ak_xxxxxxxx- 若未设置,提示用户自行配置:
export REDFOX_API_KEY=<你的apikey>;若用户不会配置,Agent 应主动帮用户设置: - macOS/Linux:将
export REDFOX_API_KEY=<值>追加到~/.zshrc(zsh)或~/.bashrc(bash),然后source对应文件使其全局生效 - Windows:使用
[Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "<值>", "User")设置用户级永久环境变量(需重启终端生效) - 配置完成后应验证:
echo $REDFOX_API_KEY(macOS/Linux)或echo %REDFOX_API_KEY%(Windows),确保换一个 skill 也能读取到
输出规范(强制执行)
触发前提(强制,禁止违反):
- 禁止在用户未明确输入查询内容或未订阅时,自动查询和输出热榜信息
- 只有当用户主动输入关键词、分类名或明确表达查询意图时,才可调用脚本获取数据
- 仅订阅推送场景下,按订阅约定时间自动输出,其余场景一律等待用户主动触发
- 禁止主动向用户推送热榜数据、禁止在闲聊或无关对话中自动调用接口
数据一致性要求(重要):
- 每日爆款笔记表格与HTML页面数据必须完全一致(相同顺序、相同内容)
- 两个脚本使用相同的API接口,禁止修改接口返回的顺序
- 禁止胡乱渲染或使用不同数据源
输出内容必须严格包含以下三部分,禁止遗漏或胡乱输出:
- 每日爆款笔记(更新时间):必须直接输出,禁止折叠,禁止遗漏
- 每日爆款笔记分析:必须直接输出,禁止折叠,禁止遗漏
- 功能服务询问:必须在每日分析之后强制输出,禁止跳过,禁止遗漏,禁止等待用户回复
输出格式要求(强制执行):
- 禁止折叠输出:所有内容必须直接完整输出,禁止使用折叠、隐藏、展开等任何形式的折叠输出
- 禁止输出文件:禁止输出任何其他文件(如CSV、JSON、TXT等)
- 输出顺序:严格按照每日爆款笔记 → 每日分析 → 功能服务询问的顺序执行,禁止混乱输出
- HTML页面改为按需生成:不再强制自动生成HTML,改为在功能服务询问中让用户选择是否生成。用户回复"2"或"生成"后才执行 gen_html 脚本
第一部分:每日爆款笔记(必须包含榜单的数据更新时间)
输出要求:
- 必须直接完整输出表格内容
- 禁止折叠、隐藏或展开表格
- 禁止输出为文件格式
- 表格字段和数据必须加载完整,加载失败则再次尝试
- 标题格式:🔥 【{分类名称}】每日爆款笔记 (更新时间:{查询日期} 19:00)(标题整体加粗)
- 标题示例:🔥 【化妆美容】每日爆款笔记 (更新时间:2026-04-21 19:00)
- 表格输出格式(严格按此格式执行,使用标准 Markdown 表格格式):
| 排名 | 笔记信息 | 互动数 | 点赞 | 评论 | 收藏 | 分享 |
|---|---|---|---|---|---|---|
| 🥇 | [笔记标题链接]<br>作者名 粉丝数+ | 22w+<br>↑新增23w+ | 22w+<br>↑新增21w+ | 893<br>↑新增21w+ | 1w+<br>↑新增21w+ | 1w+<br>↑新增21w+ |
| 🥈 | [笔记标题链接]<br>作者名 粉丝数+ | 20w+<br>↑新增18w+ | 18w+<br>↑新增16w+ | 756<br>↑新增18w+ | 8k+<br>↑新增16w+ | 900+<br>↑新增16w+ |
| 🥉 | [笔记标题链接]<br>作者名 粉丝数+ | 18w+<br>↑新增15w+ | 15w+<br>↑新增13w+ | 621<br>↑新增15w+ | 7k+<br>↑新增13w+ | 800+<br>↑新增13w+ |
字段详细说明:
- 排名:1-3名显示🥇🥈🥉,其余显示数字
- 笔记信息:
- 第一行:笔记标题(可点击跳转)
- 第二行:作者名 粉丝数+(作者名可点击跳转到作者主页,链接使用API返回的userJumpUrl字段,字号与标题一样大)
- 互动数:
- 第一行:累计互动数(格式:数字w+ 或 纯数字)
- 第二行:↑新增数字w+("↑新增"字号小一号,正常文字颜色,不换行)
- 点赞:
- 第一行:累计点赞数(格式:数字w+ 或 纯数字)
- 第二行:↑新增数字w+("↑新增"字号小一号,正常文字颜色,不换行)
- 评论:
- 第一行:累计评论数(格式:数字w+ 或 纯数字)
- 第二行:↑新增数字w+("↑新增"字号小一号,正常文字颜色,不换行)
- 收藏:
- 第一行:累计收藏数(格式:数字w+ 或 纯数字)
- 第二行:↑新增数字w+("↑新增"字号小一号,正常文字颜色,不换行)
- 分享:
- 第一行:累计分享数(格式:数字w+ 或 纯数字)
- 第二行:↑新增数字w+("↑新增"字号小一号,正常文字颜色,不换行)
- 分享字段必须展示且不能出错
重要说明:
- 使用标准 Markdown 表格格式
- 单元格内使用
<br>换行显示累计数据和新增数据 - 累计数据:大于等于1万显示"数字w+",小于1万显示纯数字(不显示"累计:"文案)
- 新增数据格式:累计数据 <br> ↑新增数字w+ 或 ↑新增纯数字("↑新增"字号小一号,正常文字颜色)
- 每个数据单元格占两行:第一行累计数据,第二行"↑新增数值"(不换行)
- "↑新增"文案字号比累计数据小一号
- "↑新增数值"使用正常文字颜色显示,与累计数据在同一行
数据说明(必须在表格上方、标题之后输出):
- 在每日爆款笔记标题之后、表格之前输出:_每日爆款笔记指笔记发后第3天比第2天涨的数据由高到低Top50_
数据输出逻辑(必须严格执行):
- 首次输出:
- 每日爆款笔记表格:显示TOP1到TOP20
- HTML页面:显示TOP1到TOP20(与热榜表格保持一致)
- "查看更多"提示逻辑(必须严格执行):
- 如果返回数据 ≤ 20条:不展示"查看更多"提示
- 如果返回数据 > 20条:展示"回复"查看更多"可看剩余{数量}条!✨"(显示剩余数据条数)
- 如果返回数据 = 50条:展示"回复"查看更多"可看剩余30条!✨"(固定30条)
- 用户输入"查看更多"后:
- 每日爆款笔记表格:输出TOP21到实际数据末尾(延续之前的TOP20)
- HTML页面:输出TOP1到实际数据末尾(完整数据,覆盖之前的HTML文件)
第二部分:每日爆款笔记分析
输出要求:
- 必须直接完整输出3个模块内容(热门内容分析、爆款标题分析、最高数据作品分析)
- 禁止折叠、隐藏或展开任何模块
- 禁止输出为文件格式
- 所有分析内容都基于获取到的所有数据
- 表格字段和数据必须加载完整,加载失败则再次尝试
- 必须包含3个模块,禁止遗漏
- 标题格式:📊 每日爆款笔记分析(加粗,不带Top数量)
- 示例:📊 每日爆款笔记分析
以下内容分析基于获取的全部数据(共{数据条数}条)进行~
💡热门内容分析
| 内容分类 | 内容特征 | 实际效果 | 参考笔记 |
|---|---|---|---|
| 封面设计 | 采用高清摄影或设计软件精心制作的封面图,画面构图饱满、色彩搭配协调,同时加入醒目的文字标题或标签,突出核心卖点或吸引眼球的元素,通过强烈的视觉冲击力在用户快速浏览时瞬间抓住注意力 | 点击率提升45% | [笔记标题链接1]<br>[笔记标题链接2] |
| 内容创作 | 围绕用户痛点或情感共鸣点展开叙事,采用第一人称视角分享真实经历或心得,结合表情符号和分段排版增强可读性,适当设置悬念或反转,激发用户的情感共鸣和参与欲望 | 互动率提升50% | [笔记标题链接3]<br>[笔记标题链接4] |
| 内容价值 | 提供具体可操作的步骤、方法或技巧,配合清晰的图片展示和简明的文字说明,确保用户看完后能够直接复用或实践,内容具有较高的实用价值和参考意义,值得用户收藏备查 | 收藏率提升55% | [笔记标题链接5]<br>[笔记标题链接6] |
| 热点结合 | 紧跟当前热门话题、节日或社会热点,将自身内容与热点元素巧妙结合,使用相关话题标签增加曝光机会,借助热点的流量优势快速提升笔记的传播范围和影响力 | 曝光率提升40% | [笔记标题链接7]<br>[笔记标题链接8] |
📈爆款标题分析
| 标题类型 | 标题特征 | 标题模版 | 发布规律 | 参考标题 |
|---|---|---|---|---|
| 提问式 | 标题以疑问句开头,设置悬念或提出用户关心的问题,引发用户的好奇心和思考欲望,激发点击阅读或评论互动 | "你还在XX吗?" | 工作日早高峰效果最佳 | 这个方法你还在用吗? |
| 数字式 | 标题中包含具体的数字、数量或统计数据,利用人们对量化信息的偏好,增强内容的可信度和专业感,让用户觉得内容有价值 | "XX个XX技巧" | 午餐时间点击率高 | 5个快速提升XX的技巧 |
| 情绪词 | 使用具有强烈情感色彩的词汇或网络流行语,如"绝了"、"太XX了"等,直接触发用户的情绪反应,引发点赞、评论或分享行为 | "太XX了!忍不住" | 晚间黄金时段转化高 | 太好用了!忍不住分享给你 |
| 利益点 | 直接在标题中明确承诺用户可以获得的具体利益或效果,如"XX天就能XX"、"省XX元"等,降低用户决策成本,提高点击率和收藏率 | "XX天就能XX" | 深夜发布收藏率最高 | 7天就能看到明显效果 |
✨最高数据作品分析
| 维度 | 核心特征分析 | 参考笔记 |
|---|---|---|
| 最高点赞 | 风格:具体风格描述<br>特征:详细特征说明<br>建议:具体建议 | [笔记标题链接] |
| 最高收藏 | 风格:具体风格描述<br>特征:详细特征说明<br>建议:具体建议 | [笔记标题链接] |
| 最高评论 | 风格:具体风格描述<br>特征:详细特征说明<br>建议:具体建议 | [笔记标题链接] |
| 最高互动 | 风格:具体风格描述<br>特征:详细特征说明<br>建议:具体建议 | [笔记标题链接] |
第三部分:功能服务询问(每日分析之后输出,禁止跳过)
强制要求(必须严格遵守):
- 功能服务询问是输出内容的第3部分,必须在每日爆款笔记分析之后立即输出
- 禁止跳过,禁止遗漏,禁止等待用户回复
- 禁止折叠输出
功能服务询问内容(必须原样输出,禁止修改,禁止省略):
技能还支持以下功能:
1️⃣ 支持订阅小红书每日爆款笔记。订阅后将在每日19:30为您推送小红书每日爆款笔记TOP50。直接回复"订阅或 1"即可订阅相关内容。
2️⃣ 可生成小红书风格的html文件包。支持导出PDF,方便阅读、保存和分享。
回复对应的数字即可。功能响应逻辑:
- 用户回复"1"或"订阅" → 开通每日19:30推送小红书每日爆款笔记TOP50
- 用户回复"2"或"生成" → 执行HTML生成脚本,生成HTML文件包
HTML页面生成(用户回复"2"后执行)
执行命令:
- 首次输出:
python scripts/gen_xhs_html.py --keyword "{关键词}" --top 20 - 用户输入"查看更多"后:
python scripts/gen_xhs_html.py --keyword "{关键词}" --top 50
HTML文件命名规则:
- 文件名格式:
小红书每日爆款笔记_{分类}_唯一时间戳.html - 唯一时间戳格式:
YYYYMMDD_HHmmss(精确到秒,确保每次生成文件名唯一) - 示例:
小红书每日爆款笔记_日常生活_20260528_144500.html
文件特点:
- 可独立打开的HTML文件
- 支持直接点击链接跳转
- 支持导出图片和PDF功能(导出结果与页面显示完全一致,PDF和图片导出内容一定是正确的)
- HTML页面样式:页面宽度700px,前三名序号更大(36px),互动数据与头像边界对齐,新增数据显示为"↑新增数值"格式,整体字号增大1px
- 数据展示:包含排名、标题、作者、粉丝数、互动数、点赞、评论、收藏、分享(含新增数据,分享字段必须展示且不能出错)
数据输出逻辑详解
核心原则:
- 每日爆款笔记表格与HTML页面必须保持数据一致性
- 使用相同的API接口,禁止修改接口返回的顺序
阶段一:首次输出
1. 每日爆款笔记表格:
- 调用脚本获取数据(最多50条)
- 展示TOP1到TOP20(前20条)
- "查看更多"提示逻辑(根据实际数据量动态调整):
- 如果返回数据 ≤ 20条:不展示"查看更多"提示
- 如果返回数据 > 20条且 < 50条:展示"回复"查看更多"可看剩余{实际剩余数量}条!✨"
- 如果返回数据 = 50条:展示"回复"查看更多"可看剩余30条!✨"(固定30条)
2. 每日爆款笔记分析:
- 基于获取的全部数据进行分析,输出3个模块
3. 功能服务询问:
- 在每日分析后立即输出功能服务询问文案
- 等待用户回复数字选择功能
4. HTML页面(用户回复"2"后执行):
- 执行命令:
python scripts/gen_xhs_html.py --keyword "{关键词}" --top 20 - 生成HTML文件,包含TOP1到TOP20数据
- 与每日爆款笔记表格数据完全一致
- 文件名格式:
小红书每日爆款笔记_{分类}_YYYYMMDD_HHmmss.html
阶段二:用户输入"查看更多"
1. 每日爆款笔记表格:
- 继续展示TOP21到实际数据末尾(根据实际数据量)
- 延续首次输出的数据,不重复TOP1-20
2. HTML页面:
- 执行命令:
python scripts/gen_xhs_html.py --keyword "{关键词}" --top 50 - 文件名格式:
小红书每日爆款笔记_{分类}_YYYYMMDD_HHmmss.html
数据一致性保障
- 两个脚本使用相同的API接口
- 相同的API接口,禁止修改接口返回的顺序
- 相同的数据字段和格式
- 确保表格和HTML页面的排名、数据完全一致(禁止修改接口返回的顺序)
操作步骤
标准流程
第一步:分类匹配
根据用户输入自动匹配合适的分类:
可用分类列表(共25个): 综合全部、出行代步、休闲爱好、影视娱乐、数码科技、医疗保健、综合杂项、星座情感、时尚穿搭、婚庆婚礼、拍摄记录、学习教育、化妆美容、居家装修、旅行度假、亲子育儿、个人护理、美味佳肴、职业发展、宠物天地、潮流鞋包、日常生活、科学探索、新闻资讯、体育锻炼
匹配规则:
- 用户输入具体产品词(如"睫毛膏")→ 匹配对应分类("化妆美容")
- 用户输入领域词(如"穿搭"、"美食")→ 直接匹配分类
- 未指定分类 → 使用"综合全部"
关键词-分类映射示例:
睫毛膏/口红/粉底 → 化妆美容
穿搭/衣服/时尚 → 时尚穿搭
美食/做饭/探店 → 美味佳肴
猫/狗/宠物 → 宠物天地
游戏/王者/电竞 → 休闲娱乐第二步:查询冷门爆款笔记
- 用户输入关键词后,自动匹配对应分类(不告知用户匹配到的分类名称)
- 查询该分类下的冷门爆款笔记(最多50条)
- 根据实际数据量动态展示:
- 如果返回数据 ≤ 20条:默认展示全部数据,不展示"查看更多"提示
- 如果返回数据 > 20条:默认展示TOP20,展示"回复"查看更多"可看剩余{数量}条!✨"提示
- 如果返回数据 = 50条:默认展示TOP20,展示"回复"查看更多"可看剩余30条!✨"提示
第三步:日期判断
根据用户输入和当前时间确定查询日期:
判断规则:
1. 用户指定了日期 → 直接使用指定日期 2. 用户未指定日期:
- 当前时间 >= 19:00 → 查询昨日数据(当日19:00已更新)
- 当前时间 < 19:00 → 查询前天数据(等待当日19:00更新)
第四步:获取每日爆款笔记数据
API Key 前置检查(强制):
- 调用脚本前,必须先确认
REDFOX_API_KEY环境变量已正确配置 - Windows 环境:Git Bash 可能无法读取 PowerShell 设置的用户级环境变量,需在命令中显式
export - 验证方式:
export REDFOX_API_KEY="ak_xxx"后再执行脚本,或先执行echo $REDFOX_API_KEY确认非空 - 若 Key 缺失,直接提示用户配置并终止,禁止进入多轮调试
调用脚本获取榜单数据,必须携带 `--output_json` 参数写入缓存:
export REDFOX_API_KEY="ak_xxx" && python scripts/xhs_daily_fetcher.py --keyword "睫毛膏" --top_n 50 --output_json /tmp/xhs_daily_cache.json参数说明:
--category:分类名称(可选)--keyword:用户输入的关键词,用于自动匹配分类--rank_date:查询日期(可选,自动判断)--top_n:返回条数(默认50)--output_json:必须指定,数据获取成功后写入 JSON 缓存,供 gen_html 复用
无数据回退规则(强制):
- 当默认日期返回 0 条数据时,脚本内置自动回退一天,Agent 无需手动重试
- 最多尝试 2 个日期(默认日期 + 回退一天),若仍无数据则直接告知用户该分类暂无数据
- 禁止用循环方式批量探测日期
数据完整性要求:
- 确保表格所有字段和数据完整加载
- 如果数据加载失败或字段缺失,重新调用脚本再次尝试(最多重试 1 次)
- 直到所有字段(排名、笔记信息、互动数、点赞、评论、收藏、分享)都有完整数据
- 分享字段使用正确的API字段名:
AnaAddBean.useShareCount(累计分享数)、AnaAddBean.addShareCount(新增分享数) - 确保分享数值正确输出,避免使用错误的字段名导致数值不对
接口调用上限(强制):
- 正常流程最多调用 1 次接口(fetcher 一次获取 + 回退由脚本内置处理)
- HTML 生成禁止独立调用接口,必须从缓存读取
- 禁止 Agent 在脚本之外自行构造 API 请求排查问题
- 整个查询流程总接口调用次数上限:2 次(含脚本内置回退)
第五步:输出每日爆款笔记和每日分析
按照输出规范,依次输出:
1. 每日爆款笔记(含更新时间) 2. 每日爆款笔记分析(3个模块)
第六步:输出功能服务询问(3项输出中的第3项)
重要要求:每日分析输出完成后必须立即执行此步
输出功能服务询问文案(原样输出,禁止修改,禁止省略):
技能还支持以下功能:
1️⃣ 支持订阅小红书每日爆款笔记。订阅后将在每日19:30为您推送小红书每日爆款笔记TOP50。直接回复"订阅或 1"即可订阅相关内容。
2️⃣ 可生成小红书风格的html文件包。支持导出PDF,方便阅读、保存和分享。
回复对应的数字即可。响应逻辑:
- 用户回复"1"或"订阅" → 开通订阅,每日19:30推送
- 用户回复"2"或"生成" → 执行第七步生成HTML
第七步:生成HTML页面(用户回复"2"后执行)
仅在用户明确回复"2"或"生成"时才执行此步,不自动生成
生成可视化HTML页面。必须使用 `--input_json` 从缓存读取数据,禁止独立调用 API。
首次输出时:
export REDFOX_API_KEY="ak_xxx" && python scripts/gen_xhs_html.py --keyword "穿搭" --top 20 --input_json /tmp/xhs_daily_cache.json --rank_date "YYYY-MM-DD" --output "小红书每日爆款笔记_穿搭_20260528_144500.html"用户输入"查看更多"后:
export REDFOX_API_KEY="ak_xxx" && python scripts/gen_xhs_html.py --keyword "穿搭" --top 50 --input_json /tmp/xhs_daily_cache.json --rank_date "YYYY-MM-DD" --output "小红书每日爆款笔记_穿搭_20260528_144500.html"HTML文件命名规则(强制):
- 文件名格式:
小红书每日爆款笔记_{分类}_唯一时间戳.html - 唯一时间戳:
YYYYMMDD_HHmmss(当前生成时间,精确到秒) - 示例:
小红书每日爆款笔记_日常生活_20260528_144500.html
订阅服务说明
订阅内容
小红书每日爆款笔记推荐TOP50
- 每日19:30更新
- 推送当日的50篇热门内容
- 包含完整分析和HTML文件
订阅触发方式
用户回复"1"或"订阅"即可开通每日推送。推送时按标准输出格式(每日爆款笔记 + 每日分析 + 功能服务询问)执行。
使用示例
示例1:关键词查询
- 场景/输入:"帮我查一下睫毛膏相关的爆款笔记"
- 预期产出:匹配化妆美容分类,依次输出每日爆款笔记、每日分析、功能服务询问
示例2:泛查询热门内容
- 场景/输入:"最近小红书有什么热门内容"
- 预期产出:使用综合全部分类,依次输出每日爆款笔记、每日分析、功能服务询问
资源索引
- 脚本:见 scripts/xhs_daily_fetcher.py
- 用途:获取小红书爆款内容榜单数据
- 参数:--category 分类名 / --keyword 关键词 / --top_n 条数
- 脚本:见 scripts/gen_xhs_html.py
- 用途:生成可视化HTML页面
- 参数:--category 分类名 / --top 条数 / --output 输出路径
注意事项
- 数据每日19:00更新昨天的榜单
- 输出内容(强制,禁止遗漏,禁止胡乱输出):必须严格包含每日爆款笔记(更新时间)+ 每日爆款笔记分析 + 功能服务询问
- 🔥 每日爆款笔记(必须包含更新时间)
- 📊 每日爆款笔记分析(3个模块:热门内容分析、爆款标题分析、最高数据作品分析)
- 功能服务询问(强制输出,禁止遗漏,禁止跳过,禁止等待用户回复):每日分析后立即原样输出询问文案
- 禁止输出遗漏,禁止胡乱输出,必须严格按照此3项内容输出
- 输出格式要求(强制):
- 禁止折叠输出:所有内容必须直接完整输出,禁止使用折叠、隐藏、展开等任何形式的折叠输出
- 禁止输出文件:禁止输出任何文件(如CSV、JSON、TXT等)
- 输出顺序:严格按照每日爆款笔记 → 每日分析 → 功能服务询问的顺序执行,禁止混乱输出
- 输出顺序(严格按此顺序执行,禁止混乱输出):
1. 🔥 每日爆款笔记(含更新时间,展示互动数/点赞/评论/收藏及新增数据,不显示匹配到的分类名称) 2. 📊 每日爆款笔记分析(💡热门内容分析、📈爆款标题分析、✨最高数据作品分析) 3. 功能服务询问(强制输出,禁止遗漏,禁止跳过,禁止等待用户回复):每日分析后立即原样输出询问文案
- HTML页面生成规则(按需生成):
- HTML页面不再自动生成,改为用户回复"2"或"生成"后执行
- 文件名格式:
小红书每日爆款笔记_{分类}_YYYYMMDD_HHmmss.html - 生成后直接输出HTML页面,禁止只输出文件路径
- PDF和图片导出内容一定是正确的
- 功能服务询问输出要求(强制,禁止遗漏):
- 功能服务询问是输出内容之一,禁止遗漏
- 必须在每日分析完成后立即展示,不能等待用户回复
- 禁止跳过,禁止遗漏,禁止等待用户回复,禁止折叠
- 用户回复"1"或"订阅":开通订阅
- 用户回复"2"或"生成":生成HTML文件包
- 数据一致性要求:每日爆款笔记表格与HTML页面必须使用相同的数据源,禁止修改接口返回的顺序,确保完全一致
- 数据完整性要求(强制):
- 表格所有字段和数据必须完整加载
- 如果数据加载失败或字段缺失,必须重新调用脚本再次尝试
- 直到所有字段都有完整数据(每日爆款笔记7个字段、每日分析所有表格字段)
- 数据输出逻辑(必须严格执行):
- 首次输出:每日爆款笔记TOP1-20,HTML页面TOP1-20(保持一致)
- 查看更多:每日爆款笔记TOP21-实际数据末尾,HTML页面TOP1-实际数据末尾(完整数据)
- "查看更多"提示逻辑:
- 数据 ≤ 20条:不展示
- 数据 > 20条且 < 50条:展示"回复"查看更多"可看剩余{数量}条!✨"
- 数据 = 50条:展示"回复"查看更多"可看剩余30条!✨"
- 表格字段:排名、笔记信息、互动数(新增互动)、点赞(新增点赞)、评论(新增评论)、收藏(新增收藏)、分享(新增分享,必须展示且不能出错,避免胡乱输出)
- 每日分析包含:热门内容分析、爆款标题分析、最高数据作品分析
- 每日分析标题后显示"以下内容分析基于获取的全部数据(共{数据条数}条)进行~"
- 分析内容从创作者视角出发,提供可执行建议
- 禁止自动查询:未订阅且未收到用户明确查询输入时,禁止自动调用脚本或输出任何热榜信息
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
小红书每日爆款笔记HTML生成器
从API获取日榜数据,生成可独立打开的HTML页面。
用法:python gen_xhs_html.py [--rank_date YYYY-MM-DD] [--category 分类名] [--top N] [--output PATH]
输出:小红书每日爆款笔记_{分类}_{时间戳}.html
样式特性:
- 小红书风格(红色主题 #ff2442)
- 卡片式布局,多行展示
- 标题和作者在第一行,数据在第二行
- TOP3 奖牌徽章 + 左边框高亮
- 导出 PDF 功能
- 页面最大宽度 750px
"""
import json
import sys
import os
from datetime import datetime, timedelta
import requests
REDFOX_API_BASE = "https://redfox.hk/story/api/cozeSkill/getXhsCozeSkillDataOne"
REDFOX_API_SOURCE = "小红书单日数据爆款文章-GitHub"
def get_redfox_api_key():
"""
获取 RedFox API Key,三级回退策略:
1. 从环境变量 REDFOX_API_KEY 获取
2. 从 shell 配置文件读取
3. 提示用户配置
"""
# 第一级:从环境变量获取
api_key = os.environ.get("REDFOX_API_KEY")
if api_key:
return api_key
# 第二级:从 shell 配置文件读取
home = os.path.expanduser("~")
shell_files = []
for fname in [".zshrc", ".bashrc", ".bash_profile", ".profile"]:
fpath = os.path.join(home, fname)
if os.path.isfile(fpath):
shell_files.append(fpath)
for fpath in shell_files:
try:
with open(fpath, "r", encoding="utf-8") as f:
for line in f:
stripped = line.strip()
if stripped.startswith("export REDFOX_API_KEY="):
key_part = stripped.split("=", 1)[1].strip().strip('"').strip("'")
if key_part:
return key_part
except Exception:
continue
# 第三级:仍未获取到,提示用户配置
raise RuntimeError(
"未找到 REDFOX_API_KEY 配置。\n"
"请设置环境变量后重试:\n"
" macOS/Linux: export REDFOX_API_KEY=<你的apikey>\n"
" Windows PowerShell: [Environment]::SetEnvironmentVariable('REDFOX_API_KEY', '<值>', 'User')\n"
"\n"
"获取 API Key: https://redfox.hk/login"
)
def get_query_date(user_date: str = None) -> tuple:
"""
根据用户输入和当前时间确定查询日期
规则:
1. 用户指定了日期 → 直接使用
2. 未指定日期:
- 当前时间 >= 19:00 → 查询昨日数据(当日19:00已更新)
- 当前时间 < 19:00 → 查询前天数据(等待当日19:00更新)
Args:
user_date: 用户指定的日期(格式:yyyy-MM-dd)
Returns:
(查询日期, 是否为自动推断)
"""
if user_date:
return user_date, False
now = datetime.now()
cutoff_time = now.replace(hour=19, minute=0, second=0, microsecond=0)
if now >= cutoff_time:
# 超过19:00,查询昨日(当日已更新)
query_date = (now - timedelta(days=1)).strftime("%Y-%m-%d")
else:
# 未超过19:00,查询前天(等待当日更新)
query_date = (now - timedelta(days=2)).strftime("%Y-%m-%d")
return query_date, True
def clean_text(text: str) -> str:
"""
清理文本中的特殊字符,用于表格单元格显示
清理内容:
- 空格
- 换行符(\r, \n)
- URL链接
- Image标记
- 特殊字符(<>{}[]|\)
"""
import re
if not text:
return ""
text = str(text)
# 移除URL
text = re.sub(r'https?://\S+', '', text)
# 移除Image标记
text = re.sub(r'Image:\s*\[.*?\]', '', text)
# 移除换行符
text = re.sub(r'[\r\n]+', '', text)
# 移除空格
text = text.replace(' ', '')
# 移除特殊字符
text = re.sub(r'[<>{}[\]\\|]', '', text)
return text.strip()
# 分类关键词映射(包含产品词映射)
CATEGORY_KEYWORDS = {
"综合全部": ["综合", "全部", "热门", "推荐", "随便", "随便看看", "总榜", "整体"],
"出行代步": ["出行", "代步", "交通", "汽车", "打车", "地铁", "公交", "开车", "驾车", "出行方式", "通勤", "自驾", "新能源车", "电动车"],
"休闲爱好": ["休闲", "爱好", "兴趣", "娱乐", "休闲活动", "兴趣爱", "业余", "消遣", "手工", "DIY", "收藏"],
"影视娱乐": ["影视", "娱乐", "电影", "电视剧", "综艺", "明星", "追剧", "看剧", "演员", "导演", "剧集", "追星", "综艺"],
"数码科技": ["数码", "科技", "手机", "电脑", "数码产品", "科技产品", "智能", "电子", "硬件", "软件", "app", "APP", "iPhone", "安卓", "平板", "耳机", "键盘", "鼠标"],
"医疗保健": ["医疗", "保健", "健康", "医院", "医生", "看病", "养生", "保健", "体检", "治疗", "药品", "中医", "减肥", "瘦身"],
"综合杂项": ["杂项", "其他", "综合杂项", "杂货", "综合类"],
"星座情感": ["星座", "情感", "爱情", "恋爱", "感情", "星座运势", "情感咨询", "脱单", "表白", "分手", "复合", "塔罗", "占卜"],
"时尚穿搭": ["时尚", "穿搭", "衣服", "服装", "搭配", "穿衣", "时装", "潮流穿搭", "服饰", "OOTD", "ootd", "裙子", "裤子", "外套", "大衣", "西装", "毛衣", "T恤"],
"婚庆婚礼": ["婚庆", "婚礼", "结婚", "婚纱", "婚宴", "求婚", "订婚", "婚庆策划", "新娘", "新郎", "伴娘", "钻戒"],
"拍摄记录": ["拍摄", "记录", "摄影", "拍照", "照片", "视频", "vlog", "Vlog", "VLOG", "摄像", "短视频", "相机", "镜头"],
"学习教育": ["学习", "教育", "培训", "课程", "考试", "学校", "教育机构", "学习方法", "考研", "考公", "留学", "英语", "编程", "技能"],
"化妆美容": ["化妆", "美容", "美妆", "妆容", "护肤", "彩妆", "化妆品", "美容护肤", "化妆教程", "美颜", "睫毛膏", "口红", "粉底", "眉笔", "眼影", "腮红", "遮瑕", "定妆", "精华", "面霜", "水乳", "防晒", "面膜"],
"居家装修": ["居家", "装修", "家居", "家装", "房子装修", "室内设计", "软装", "硬装", "家居好物", "家具", "收纳", "整理"],
"旅行度假": ["旅行", "度假", "旅游", "出游", "旅行攻略", "景点", "旅游攻略", "自由行", "跟团游", "自驾游", "酒店", "民宿"],
"亲子育儿": ["亲子", "育儿", "宝宝", "儿童", "带娃", "育儿经", "亲子活动", "母婴", "幼儿", "小孩", "奶粉", "尿布", "玩具"],
"个人护理": ["个人护理", "护理", "护肤", "身体护理", "美容护理", "护理产品", "个人清洁", "洗发水", "沐浴露", "牙膏", "卫生巾"],
"美味佳肴": ["美味", "佳肴", "美食", "做饭", "烹饪", "菜谱", "美食推荐", "餐厅", "探店", "食谱", "好吃", "甜品", "烘焙", "奶茶", "咖啡", "零食"],
"职业发展": ["职业", "发展", "工作", "职场", "求职", "面试", "职业规划", "跳槽", "升职", "加薪", "简历", "副业", "创业"],
"宠物天地": ["宠物", "猫", "狗", "养猫", "养狗", "萌宠", "宠物猫", "宠物狗", "铲屎官", "喵星人", "汪星人", "猫粮", "狗粮"],
"潮流鞋包": ["潮流", "鞋包", "鞋子", "包包", "潮鞋", "名牌包", "运动鞋", "高跟鞋", "手提包", "球鞋", "帆布鞋", "靴子"],
"日常生活": ["日常", "生活", "日常记录", "生活日常", "vlog日常", "生活分享", "好物推荐"],
"科学探索": ["科学", "探索", "科普", "科学知识", "实验", "发现", "研究", "科技探索"],
"新闻资讯": ["新闻", "资讯", "热点", "时事", "新闻报道", "新闻资讯", "最新消息"],
"体育锻炼": ["体育", "锻炼", "运动", "健身", "减肥", "瘦身", "体育运动", "健身房", "瑜伽", "跑步", "游泳", "篮球", "足球"],
}
def match_category(user_input: str) -> str:
"""
根据用户输入匹配分类
Args:
user_input: 用户输入的关键词或描述
Returns:
匹配的分类名称,默认返回"综合全部"
"""
if not user_input:
return "综合全部"
user_input = user_input.lower().strip()
# 遍历分类关键词进行匹配
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
if keyword.lower() in user_input:
return category
# 如果没有匹配到,返回综合全部
return "综合全部"
def fetch_xhs_weekly(rank_date=None, category="综合全部"):
"""获取小红书平台日榜数据"""
if not rank_date:
rank_date = datetime.now().strftime("%Y-%m-%d")
api_key = get_redfox_api_key()
headers = {
"X-API-KEY": api_key,
}
params = {
"rankDate": rank_date,
"source": REDFOX_API_SOURCE,
"category": category,
}
try:
# 接口仅支持 GET(query 参数);POST 会返回系统错误
response = requests.get(REDFOX_API_BASE, params=params, headers=headers, timeout=30)
if response.status_code >= 400:
return {"fetch_time": rank_date + " 19:00", "query_type": "日榜", "category": category, "hot_list": []}
api_response = response.json()
except Exception:
return {"fetch_time": rank_date + " 19:00", "query_type": "日榜", "category": category, "hot_list": []}
# 提取数据
if isinstance(api_response, dict):
data = api_response.get("data", [])
elif isinstance(api_response, list):
data = api_response
else:
data = []
# 如果数据为空,返回空列表
if not data:
return {"fetch_time": rank_date + " 19:00", "query_type": "日榜", "category": category, "hot_list": []}
def parse_count(value) -> int:
"""解析计数值,支持 "2w+" 格式"""
if isinstance(value, int):
return value
if isinstance(value, str):
if 'w+' in value.lower():
num = float(value.lower().replace('w+', '').replace('w', ''))
return int(num * 10000)
try:
return int(value)
except:
return 0
return 0
# 保持接口原始顺序,禁止重新排序
# 格式化数据
hot_list = []
for idx, item in enumerate(data):
ana_add = item.get("anaAdd", {})
hot_list.append({
"index": idx + 1,
"userName": clean_text(item.get("userName", "")),
"userHeadUrl": item.get("userHeadUrl", ""),
"userJumpUrl": item.get("userJumpUrl", ""),
"fans": clean_text(str(item.get("fans", ""))),
"title": clean_text(item.get("title", "")),
"desc": clean_text(item.get("desc", "")),
"coverUrl": item.get("coverUrl", ""),
"photoJumpUrl": (item.get("photoJumpUrl", "") or "").replace(" ", "%20"),
"publicTime": item.get("publicTime", ""),
"interactiveCount": clean_text(str(ana_add.get("interactiveCount", "0") or "0")),
"addInteractiveount": clean_text(str(ana_add.get("addInteractiveount", "0") or "0")),
"collectedCount": clean_text(str(ana_add.get("collectedCount", "0") or "0")),
"addCollectedCunt": clean_text(str(ana_add.get("addCollectedCunt", "0") or "0")),
"useLikeCount": clean_text(str(ana_add.get("useLikeCount", "0") or "0")),
"addLikeCount": clean_text(str(ana_add.get("addLikeCount", "0") or "0")),
"useCommentCount": clean_text(str(ana_add.get("useCommentCount", "0") or "0")),
"addCommentCount": clean_text(str(ana_add.get("addCommentCount", "0") or "0")),
"useShareCount": clean_text(str(ana_add.get("useShareCount", "0") or "0")),
"addShareCount": clean_text(str(ana_add.get("addShareCount", "0") or "0"))
})
return {
"fetch_time": f"{rank_date} 19:00",
"query_type": "日榜",
"rank_date": rank_date,
"category": category,
"hot_list": hot_list
}
def generate_html(result, top_n=20):
"""生成HTML页面 - 小红书风格
Args:
result: 日榜数据结果
top_n: 显示条数,默认20,可设为50
重要:只传递实际需要展示的数据到HTML,确保统计数据与展示数据一致
"""
hot_list = result["hot_list"]
fetch_time = result["fetch_time"]
rank_date = result.get("rank_date", "")
category = result.get("category", "综合全部")
# 限制显示条数 - 必须先截取,确保统计数据准确
top_n = min(top_n, len(hot_list), 50) # 最大支持TOP50
hot_list = hot_list[:top_n]
js_data = json.dumps(hot_list, ensure_ascii=False, indent=2)
page_title = f"小红书每日爆款笔记 - {category}"
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{page_title}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.5;
}}
.page-wrap {{
max-width: 700px;
margin: 0 auto;
padding: 12px 12px 24px;
}}
.export-bar {{
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 8px 0 10px;
}}
.btn-export-pdf, .btn-export-img {{
background: #fff;
color: #ff2442;
border: 1.5px solid #ff2442;
border-radius: 20px;
padding: 6px 18px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}}
.btn-export-pdf:hover, .btn-export-img:hover {{ background: #fff5f6; }}
.pdf-content {{ background: transparent; }}
.header-wrap {{
background: linear-gradient(135deg, #ff2442 0%, #ff6b81 100%);
border-radius: 12px;
padding: 20px 16px;
margin-bottom: 12px;
color: #fff;
}}
.header-wrap h1 {{
font-size: 22px;
font-weight: 700;
margin-bottom: 4px;
}}
.header-meta {{
font-size: 13px;
opacity: 0.9;
}}
.category-badge {{
display: inline-block;
background: rgba(255,255,255,0.25);
border-radius: 10px;
padding: 2px 10px;
font-size: 13px;
margin-left: 8px;
}}
.stats-row {{
display: flex;
justify-content: space-around;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid rgba(255,255,255,0.2);
}}
.stat-item {{
text-align: center;
}}
.stat-num {{
font-size: 20px;
font-weight: 700;
}}
.stat-label {{
font-size: 12px;
opacity: 0.85;
margin-top: 2px;
}}
.note-list {{
display: flex;
flex-direction: column;
gap: 10px;
}}
.note-card {{
background: #fff;
border-radius: 12px;
padding: 14px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid #f0f0f0;
}}
.note-card:hover {{
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}}
.note-card.top1 {{ border-left: 3px solid #ff2442; }}
.note-card.top2 {{ border-left: 3px solid #ff6b81; }}
.note-card.top3 {{ border-left: 3px solid #ffb3c1; }}
.note-row1 {{
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}}
.rank-num {{
min-width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: #999;
margin-right: 12px;
flex-shrink: 0;
}}
.note-info {{
flex: 1;
min-width: 0;
}}
.note-title {{
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
}}
.note-title a {{
color: #333;
text-decoration: none;
}}
.note-title a:hover {{
color: #ff2442;
}}
.author-info {{
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #999;
}}
.author-avatar {{
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
}}
.author-name {{
color: #666;
}}
.fans-count {{
color: #999;
}}
.note-row2 {{
display: flex;
flex-wrap: wrap;
gap: 14px;
padding-left: 48px;
/* 48px = rank-num宽度36px + margin-right 12px,与头像边界对齐 */
}}
.data-item {{
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}}
.data-label {{
color: #999;
}}
.data-value {{
color: #333;
font-weight: 600;
}}
.data-add {{
color: #ff2442;
font-size: 12px;
margin-left: 4px;
}}
.footer-note {{
text-align: center;
font-size: 12px;
color: #bbb;
margin-top: 16px;
padding: 10px 0;
}}
@media (max-width: 600px) {{
.page-wrap {{ padding: 8px 8px 20px; }}
.header-wrap {{ padding: 16px 12px; }}
.header-wrap h1 {{ font-size: 20px; }}
.note-card {{ padding: 12px; }}
.note-title {{ font-size: 15px; }}
.rank-num {{ min-width: 32px; height: 32px; font-size: 16px; }}
.note-row2 {{ gap: 10px; padding-left: 44px; margin-top: 8px; }}
.data-item {{ font-size: 12px; }}
}}
</style>
</head>
<body>
<div class="page-wrap">
<div class="export-bar">
<button class="btn-export-img" onclick="exportImage()">导出图片</button>
<button class="btn-export-pdf" onclick="exportPdf()">导出 PDF</button>
</div>
<div class="pdf-content" id="pdfContent">
<div class="header-wrap">
<h1>📱 小红书每日爆款笔记<span class="category-badge">{category}</span></h1>
<div class="header-meta">更新时间:{fetch_time}</div>
<div style="font-size:12px;color:rgba(255,255,255,0.75);margin-top:4px;">*每日爆款笔记指笔记发后第3天比第2天涨的数据由高到低Top50*</div>
<div class="stats-row">
<div class="stat-item">
<div class="stat-num" id="totalCount">--</div>
<div class="stat-label">笔记总数</div>
</div>
<div class="stat-item">
<div class="stat-num" id="maxInteractive">--</div>
<div class="stat-label">最高互动</div>
</div>
<div class="stat-item">
<div class="stat-num" id="avgInteractive">--</div>
<div class="stat-label">平均互动</div>
</div>
</div>
</div>
<div class="note-list" id="noteList"></div>
<div class="footer-note">数据来源:小红书平台 · {category} · 当日热门笔记排行</div>
</div>
</div>
<script>
(function() {{
function bindCardClick() {{
var cards = document.querySelectorAll('.note-card[data-href]');
cards.forEach(function(card) {{
card.addEventListener('click', function(e) {{
if (e.target.closest('a, button')) return;
var url = this.getAttribute('data-href');
if (url && url !== '#') window.open(url, '_blank');
}});
}});
}}
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', bindCardClick);
}} else {{
bindCardClick();
}}
}})();
(function() {{
var RAW = {js_data};
var totalCount = RAW.length;
var maxInteractive = 0, sumInteractive = 0;
function parseInteractive(val) {{
if (typeof val === 'string') {{
if (val.indexOf('w') !== -1 || val.indexOf('W') !== -1) {{
return parseFloat(val) * 10000;
}} else if (val.indexOf('亿') !== -1) {{
return parseFloat(val) * 100000000;
}}
return parseFloat(val) || 0;
}}
return val || 0;
}}
function fmtNum(val) {{
if (typeof val === 'string') {{
if (val.indexOf('w') !== -1 || val.indexOf('W') !== -1 || val.indexOf('亿') !== -1) {{
return val;
}}
}}
var n = parseInt(val) || 0;
if (n >= 10000) {{
return (n / 10000).toFixed(1) + 'w';
}}
return n.toString();
}}
for (var i = 0; i < RAW.length; i++) {{
var interactive = parseInteractive(RAW[i].interactiveCount);
if (interactive > maxInteractive) maxInteractive = interactive;
sumInteractive += interactive;
}}
var avgInteractive = totalCount > 0 ? sumInteractive / totalCount : 0;
document.getElementById('totalCount').textContent = totalCount;
document.getElementById('maxInteractive').textContent = fmtNum(maxInteractive);
document.getElementById('avgInteractive').textContent = fmtNum(Math.round(avgInteractive));
var html = '';
for (var i = 0; i < RAW.length && i < {top_n}; i++) {{
var d = RAW[i];
var rank = i + 1;
var cardCls = 'note-card';
if (rank === 1) cardCls += ' top1';
else if (rank === 2) cardCls += ' top2';
else if (rank === 3) cardCls += ' top3';
var noteUrl = (d.photoJumpUrl || '#').replace(/ /g, '%20');
var avatar = d.userHeadUrl || '';
var userName = (d.userName || '--').replace(/ /g, '');
var userUrl = (d.userJumpUrl || '#').replace(/ /g, '%20');
var fans = (d.fans || '').replace(/ /g, '');
var title = (d.title || '--').replace(/ /g, '');
var interactive = (d.interactiveCount || '0').replace(/ /g, '');
var addInteractive = (d.addInteractiveount || '0').replace(/ /g, '');
var likeCount = (d.useLikeCount || '0').replace(/ /g, '');
var addLikeCount = (d.addLikeCount || '0').replace(/ /g, '');
var commentCount = (d.useCommentCount || '0').replace(/ /g, '');
var addCommentCount = (d.addCommentCount || '0').replace(/ /g, '');
var collected = (d.collectedCount || '0').replace(/ /g, '');
var addCollected = (d.addCollectedCunt || '0').replace(/ /g, '');
var shareCount = (d.useShareCount || '0').replace(/ /g, '');
var addShareCount = (d.addShareCount || '0').replace(/ /g, '');
var rankHtml = '';
if (rank === 1) {{
rankHtml = '<div class="rank-num">🥇</div>';
}} else if (rank === 2) {{
rankHtml = '<div class="rank-num">🥈</div>';
}} else if (rank === 3) {{
rankHtml = '<div class="rank-num">🥉</div>';
}} else {{
rankHtml = '<div class="rank-num">' + rank + '</div>';
}}
html += '<div class="' + cardCls + '" data-href="' + noteUrl + '">'
+ '<div class="note-row1">'
+ rankHtml
+ '<div class="note-info">'
+ '<div class="note-title"><a href="' + noteUrl + '" target="_blank" onclick="event.stopPropagation()">' + title + '</a></div>'
+ '<div class="author-info">'
+ (avatar ? '<img class="author-avatar" src="' + avatar + '" onerror="this.style.display=\\'none\\'">' : '')
+ '<span class="author-name">' + (userUrl && userUrl !== '#' ? '<a href="' + userUrl + '" target="_blank" onclick="event.stopPropagation()">' + userName + '</a>' : userName) + '</span>'
+ (fans ? '<span class="fans-count">·' + fans + '粉丝</span>' : '')
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div class="note-row2">'
+ '<div class="data-item"><span class="data-label">互动</span><span class="data-value">' + fmtNum(interactive) + '</span><span class="data-add">↑' + fmtNum(addInteractive) + '</span></div>'
+ '<div class="data-item"><span class="data-label">点赞</span><span class="data-value">' + fmtNum(likeCount) + '</span><span class="data-add">↑' + fmtNum(addLikeCount) + '</span></div>'
+ '<div class="data-item"><span class="data-label">评论</span><span class="data-value">' + fmtNum(commentCount) + '</span><span class="data-add">↑' + fmtNum(addCommentCount) + '</span></div>'
+ '<div class="data-item"><span class="data-label">收藏</span><span class="data-value">' + fmtNum(collected) + '</span><span class="data-add">↑' + fmtNum(addCollected) + '</span></div>'
+ '<div class="data-item"><span class="data-label">分享</span><span class="data-value">' + fmtNum(shareCount) + '</span><span class="data-add">↑' + fmtNum(addShareCount) + '</span></div>'
+ '</div>'
+ '</div>';
}}
document.getElementById('noteList').innerHTML = html;
}})();
function exportImage() {{
var btn = document.querySelector('.btn-export-img');
btn.textContent = '生成中...';
btn.style.pointerEvents = 'none';
var target = document.getElementById('pdfContent');
html2canvas(target, {{
scale: 2,
useCORS: true,
backgroundColor: '#f5f5f5',
logging: false,
windowWidth: target.scrollWidth,
windowHeight: target.scrollHeight
}}).then(function(canvas) {{
var link = document.createElement('a');
link.download = 'xhs_weekly_' + new Date().toISOString().slice(0,10) + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
btn.textContent = '导出图片';
btn.style.pointerEvents = '';
}}).catch(function(err) {{
alert('图片生成失败:' + err.message);
btn.textContent = '导出图片';
btn.style.pointerEvents = '';
}});
}}
function exportPdf() {{
var btn = document.querySelector('.btn-export-pdf');
btn.textContent = '生成中...';
btn.style.pointerEvents = 'none';
var target = document.getElementById('pdfContent');
html2canvas(target, {{
scale: 2,
useCORS: true,
backgroundColor: '#f5f5f5',
logging: false,
windowWidth: target.scrollWidth,
windowHeight: target.scrollHeight
}}).then(function(canvas) {{
var imgData = canvas.toDataURL('image/png', 1.0);
// 使用横向 A4 纸
var pdf = new jspdf.jsPDF('l', 'mm', 'a4');
var pdfW = pdf.internal.pageSize.getWidth();
var pdfH = pdf.internal.pageSize.getHeight();
var margin = 8;
var contentW = pdfW - margin * 2;
var contentH = pdfH - margin * 2;
// 计算图片尺寸以适应横向 A4
var imgW = contentW;
var imgH = (canvas.height * imgW) / canvas.width;
// 如果高度仍然超过页面高度,按高度缩放
if (imgH > contentH) {{
imgH = contentH;
imgW = (canvas.width * imgH) / canvas.height;
}}
// 居中显示
var imgX = (pdfW - imgW) / 2;
var imgY = (pdfH - imgH) / 2;
// 所有内容在一页中
pdf.addImage(imgData, 'PNG', imgX, imgY, imgW, imgH);
var now = new Date();
var dateStr = now.getFullYear() +
String(now.getMonth()+1).padStart(2,'0') +
String(now.getDate()).padStart(2,'0') +
'_' +
String(now.getHours()).padStart(2,'0') +
String(now.getMinutes()).padStart(2,'0');
pdf.save('xhs_weekly_' + dateStr + '.pdf');
btn.textContent = '导出 PDF';
btn.style.pointerEvents = '';
}}).catch(function(err) {{
alert('PDF 生成失败:' + err.message);
btn.textContent = '导出 PDF';
btn.style.pointerEvents = '';
}});
}}
</script>
</body>
</html>'''
# Replace js_data placeholder
html = html.replace("{js_data}", js_data)
return html
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='生成小红书每日爆款笔记HTML页面')
parser.add_argument('--rank_date', type=str, help='查询日期,格式 YYYY-MM-DD')
parser.add_argument('--category', type=str, default=None, help='分类名称')
parser.add_argument('--keyword', type=str, help='用户输入的关键词,用于自动匹配分类')
parser.add_argument('--output', type=str, help='输出文件路径')
parser.add_argument('--top', type=int, default=20, help='显示条数,默认20,可设为50')
parser.add_argument('--input_json', type=str, default=None, help='从 JSON 缓存文件读取数据,跳过 API 调用')
args = parser.parse_args()
# 优先从 JSON 缓存读取数据
if args.input_json and os.path.isfile(args.input_json):
try:
with open(args.input_json, 'r', encoding='utf-8') as f:
cache = json.load(f)
cached_articles = cache.get("articles", [])
cached_rank_date = cache.get("rank_date", "")
cached_category = cache.get("category", "")
if cached_articles and cached_rank_date:
print(f"从缓存读取数据: {args.input_json}", file=sys.stderr)
print(f"缓存日期: {cached_rank_date}, 分类: {cached_category}, 共 {len(cached_articles)} 条", file=sys.stderr)
# 使用缓存数据构造 result
result = {
"fetch_time": f"{cached_rank_date} 19:00",
"query_type": "日榜",
"rank_date": cached_rank_date,
"category": cached_category or "综合全部",
"hot_list": cached_articles
}
html = generate_html(result, top_n=args.top)
if args.output:
output_path = args.output
else:
# 新命名规则:小红书每日爆款笔记_{分类}_唯一时间戳.html
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_category = (cached_category or "综合全部").replace(" ", "_").replace("/", "_")
filename = f"小红书每日爆款笔记_{safe_category}_{timestamp}.html"
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html)
print(f"已生成:{output_path}", file=sys.stderr)
print(f"共 {len(result['hot_list'])} 条日榜数据,展示TOP{args.top}", file=sys.stderr)
sys.exit(0)
else:
print(f"缓存数据无效,回退到 API 调用", file=sys.stderr)
except Exception as e:
print(f"缓存读取失败: {e},回退到 API 调用", file=sys.stderr)
# 处理分类
if args.keyword and not args.category:
category = match_category(args.keyword)
print(f"根据关键词【{args.keyword}】匹配到分类:【{category}】", file=sys.stderr)
elif args.category:
category = args.category
else:
category = "综合全部"
# 处理日期
rank_date, is_auto = get_query_date(args.rank_date)
print(f"正在获取小红书平台日榜数据...", file=sys.stderr)
print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", file=sys.stderr)
if is_auto:
cutoff_time = datetime.now().replace(hour=19, minute=0, second=0, microsecond=0)
if datetime.now() >= cutoff_time:
print(f"当前已过19:00,查询昨日数据", file=sys.stderr)
else:
print(f"当前未过19:00,查询前天数据", file=sys.stderr)
print(f"查询日期: {rank_date}", file=sys.stderr)
print(f"分类:{category}", file=sys.stderr)
result = fetch_xhs_weekly(rank_date=rank_date, category=category)
html = generate_html(result, top_n=args.top)
if args.output:
output_path = args.output
else:
# 新命名规则:小红书每日爆款笔记_{分类}_唯一时间戳.html
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_category = category.replace(" ", "_").replace("/", "_")
filename = f"小红书每日爆款笔记_{safe_category}_{timestamp}.html"
output_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html)
print(f"已生成:{output_path}", file=sys.stderr)
print(f"共 {len(result['hot_list'])} 条日榜数据,展示TOP{args.top}", file=sys.stderr)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
小红书爆款内容榜单获取脚本
功能:
1. 调用API获取小红书爆款内容数据
2. 保持接口原始顺序,禁止重新排序
3. 输出TOP30榜单列表(纯文本格式)
4. 提供三维度内容分析(数据表现、内容结构、选题价值)
使用方法:
python fetch_explosive_articles.py --rank_date "2026-04-15" --category "时尚穿搭"
"""
import argparse
import json
import sys
import os
from datetime import datetime, timedelta
import requests
REDFOX_API_BASE = "https://redfox.hk/story/api/cozeSkill/getXhsCozeSkillDataOne"
REDFOX_API_SOURCE = "小红书单日数据爆款文章-GitHub"
def get_redfox_api_key():
"""
获取 RedFox API Key,三级回退策略:
1. 从环境变量 REDFOX_API_KEY 获取
2. 从 shell 配置文件读取
3. 提示用户配置
"""
# 第一级:从环境变量获取
api_key = os.environ.get("REDFOX_API_KEY")
if api_key:
return api_key
# 第二级:从 shell 配置文件读取
home = os.path.expanduser("~")
shell_files = []
for fname in [".zshrc", ".bashrc", ".bash_profile", ".profile"]:
fpath = os.path.join(home, fname)
if os.path.isfile(fpath):
shell_files.append(fpath)
for fpath in shell_files:
try:
with open(fpath, "r", encoding="utf-8") as f:
for line in f:
stripped = line.strip()
if stripped.startswith("export REDFOX_API_KEY="):
key_part = stripped.split("=", 1)[1].strip().strip('"').strip("'")
if key_part:
return key_part
except Exception:
continue
# 第三级:仍未获取到,提示用户配置
raise RuntimeError(
"未找到 REDFOX_API_KEY 配置。\n"
"请设置环境变量后重试:\n"
" macOS/Linux: export REDFOX_API_KEY=<你的apikey>\n"
" Windows PowerShell: [Environment]::SetEnvironmentVariable('REDFOX_API_KEY', '<值>', 'User')\n"
"\n"
"获取 API Key: https://redfox.hk/login"
)
def get_query_date(user_date: str = None) -> tuple:
"""
根据用户输入和当前时间确定查询日期
规则:
1. 用户指定了日期 → 直接使用
2. 未指定日期:
- 当前时间 >= 19:00 → 查询昨日数据(当日19:00已更新)
- 当前时间 < 19:00 → 查询前天数据(等待当日19:00更新)
Args:
user_date: 用户指定的日期(格式:yyyy-MM-dd)
Returns:
(查询日期, 是否为自动推断)
"""
if user_date:
return user_date, False
now = datetime.now()
cutoff_time = now.replace(hour=19, minute=0, second=0, microsecond=0)
if now >= cutoff_time:
# 超过19:00,查询昨日(当日已更新)
query_date = (now - timedelta(days=1)).strftime("%Y-%m-%d")
else:
# 未超过19:00,查询前天(等待当日更新)
query_date = (now - timedelta(days=2)).strftime("%Y-%m-%d")
return query_date, True
# 分类关键词映射(包含产品词映射)
CATEGORY_KEYWORDS = {
"综合全部": ["综合", "全部", "热门", "推荐", "随便", "随便看看", "总榜", "整体"],
"出行代步": ["出行", "代步", "交通", "汽车", "打车", "地铁", "公交", "开车", "驾车", "出行方式", "通勤", "自驾", "新能源车", "电动车"],
"休闲爱好": ["休闲", "爱好", "兴趣", "娱乐", "休闲活动", "兴趣爱", "业余", "消遣", "手工", "DIY", "收藏"],
"影视娱乐": ["影视", "娱乐", "电影", "电视剧", "综艺", "明星", "追剧", "看剧", "演员", "导演", "剧集", "追星", "综艺"],
"数码科技": ["数码", "科技", "手机", "电脑", "数码产品", "科技产品", "智能", "电子", "硬件", "软件", "app", "APP", "iPhone", "安卓", "平板", "耳机", "键盘", "鼠标"],
"医疗保健": ["医疗", "保健", "健康", "医院", "医生", "看病", "养生", "保健", "体检", "治疗", "药品", "中医", "减肥", "瘦身"],
"综合杂项": ["杂项", "其他", "综合杂项", "杂货", "综合类"],
"星座情感": ["星座", "情感", "爱情", "恋爱", "感情", "星座运势", "情感咨询", "脱单", "表白", "分手", "复合", "塔罗", "占卜"],
"时尚穿搭": ["时尚", "穿搭", "衣服", "服装", "搭配", "穿衣", "时装", "潮流穿搭", "服饰", "OOTD", "ootd", "裙子", "裤子", "外套", "大衣", "西装", "毛衣", "T恤"],
"婚庆婚礼": ["婚庆", "婚礼", "结婚", "婚纱", "婚宴", "求婚", "订婚", "婚庆策划", "新娘", "新郎", "伴娘", "钻戒"],
"拍摄记录": ["拍摄", "记录", "摄影", "拍照", "照片", "视频", "vlog", "Vlog", "VLOG", "摄像", "短视频", "相机", "镜头"],
"学习教育": ["学习", "教育", "培训", "课程", "考试", "学校", "教育机构", "学习方法", "考研", "考公", "留学", "英语", "编程", "技能"],
"化妆美容": ["化妆", "美容", "美妆", "妆容", "护肤", "彩妆", "化妆品", "美容护肤", "化妆教程", "美颜", "睫毛膏", "口红", "粉底", "眉笔", "眼影", "腮红", "遮瑕", "定妆", "精华", "面霜", "水乳", "防晒", "面膜"],
"居家装修": ["居家", "装修", "家居", "家装", "房子装修", "室内设计", "软装", "硬装", "家居好物", "家具", "收纳", "整理"],
"旅行度假": ["旅行", "度假", "旅游", "出游", "旅行攻略", "景点", "旅游攻略", "自由行", "跟团游", "自驾游", "酒店", "民宿"],
"亲子育儿": ["亲子", "育儿", "宝宝", "儿童", "带娃", "育儿经", "亲子活动", "母婴", "幼儿", "小孩", "奶粉", "尿布", "玩具"],
"个人护理": ["个人护理", "护理", "护肤", "身体护理", "美容护理", "护理产品", "个人清洁", "洗发水", "沐浴露", "牙膏", "卫生巾"],
"美味佳肴": ["美味", "佳肴", "美食", "做饭", "烹饪", "菜谱", "美食推荐", "餐厅", "探店", "食谱", "好吃", "甜品", "烘焙", "奶茶", "咖啡", "零食"],
"职业发展": ["职业", "发展", "工作", "职场", "求职", "面试", "职业规划", "跳槽", "升职", "加薪", "简历", "副业", "创业"],
"宠物天地": ["宠物", "猫", "狗", "养猫", "养狗", "萌宠", "宠物猫", "宠物狗", "铲屎官", "喵星人", "汪星人", "猫粮", "狗粮"],
"潮流鞋包": ["潮流", "鞋包", "鞋子", "包包", "潮鞋", "名牌包", "运动鞋", "高跟鞋", "手提包", "球鞋", "帆布鞋", "靴子"],
"日常生活": ["日常", "生活", "日常记录", "生活日常", "vlog日常", "生活分享", "好物推荐"],
"科学探索": ["科学", "探索", "科普", "科学知识", "实验", "发现", "研究", "科技探索"],
"新闻资讯": ["新闻", "资讯", "热点", "时事", "新闻报道", "新闻资讯", "最新消息"],
"体育锻炼": ["体育", "锻炼", "运动", "健身", "减肥", "瘦身", "体育运动", "健身房", "瑜伽", "跑步", "游泳", "篮球", "足球"],
}
def match_category(user_input: str) -> str:
"""
根据用户输入匹配分类
Args:
user_input: 用户输入的关键词或描述
Returns:
匹配的分类名称,默认返回"综合全部"
"""
if not user_input:
return "综合全部"
user_input = user_input.lower().strip()
# 遍历分类关键词进行匹配
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
if keyword.lower() in user_input:
return category
# 如果没有匹配到,返回综合全部
return "综合全部"
def get_all_categories():
"""获取所有分类列表"""
return list(CATEGORY_KEYWORDS.keys())
def fetch_explosive_articles(rank_date: str, category: str = "综合全部") -> dict:
"""
调用API获取小红书爆款内容数据
Args:
rank_date: 日期(格式:yyyy-MM-dd)
category: 分类名称(默认:综合全部)
Returns:
API返回的数据
"""
api_key = get_redfox_api_key()
headers = {
"X-API-KEY": api_key,
}
params = {
"rankDate": rank_date,
"source": REDFOX_API_SOURCE,
"category": category,
}
try:
# 接口仅支持 GET(query 参数);POST 会返回系统错误
response = requests.get(REDFOX_API_BASE, params=params, headers=headers, timeout=30)
if response.status_code >= 400:
raise Exception(f"HTTP请求失败: {response.status_code}, {response.text}")
data = response.json()
# 检查API返回的错误信息(成功码为2000)
if "code" in data and data["code"] != 2000:
error_msg = data.get("msg", data.get("message", "未知错误"))
raise Exception(f"API错误: {error_msg}")
return data
except requests.exceptions.RequestException as e:
raise Exception(f"请求失败: {str(e)}")
except json.JSONDecodeError as e:
raise Exception(f"JSON解析失败: {str(e)}")
def process_ranking_data(data: dict, top_n: int = 10) -> list:
"""
处理榜单数据,保持接口原始顺序
Args:
data: API返回的原始数据
top_n: 返回前N条数据(默认10条)
Returns:
榜单列表(保持接口原始顺序,禁止重新排序)
"""
# 提取笔记列表
articles = []
if isinstance(data, list):
articles = data
elif isinstance(data, dict):
articles = data.get("data", data.get("list", data.get("articles", [])))
if isinstance(articles, dict):
articles = articles.get("list", articles.get("records", []))
if not articles:
return []
# 保持接口原始顺序,禁止重新排序
return articles[:top_n]
def parse_count(value) -> int:
"""解析计数值,支持 "2w+" 格式"""
if isinstance(value, int):
return value
if isinstance(value, str):
if 'w+' in value.lower():
num = float(value.lower().replace('w+', '').replace('w', ''))
return int(num * 10000)
try:
return int(value)
except:
return 0
return 0
def clean_text(text: str) -> str:
"""
清理文本中的特殊字符,用于表格单元格显示
清理内容:
- 空格
- 换行符(\r, \n)
- URL链接
- Image标记
- 特殊字符(<>{}[]|\)
"""
import re
if not text:
return ""
text = str(text)
# 移除URL
text = re.sub(r'https?://\S+', '', text)
# 移除Image标记
text = re.sub(r'Image:\s*\[.*?\]', '', text)
# 移除换行符
text = re.sub(r'[\r\n]+', '', text)
# 移除空格
text = text.replace(' ', '')
# 移除特殊字符
text = re.sub(r'[<>{}[\]\\|]', '', text)
return text.strip()
def generate_core_content(title: str, desc: str, user_name: str) -> str:
"""
生成笔记核心内容摘要
规则:主要讲述什么人做了什么事,内容丰富且信息完整
Args:
title: 笔记标题
desc: 笔记描述
user_name: 作者名称
Returns:
核心内容摘要(30-60字)
"""
import re
# 清理desc中的URL和特殊字符
desc_clean = re.sub(r'https?://\S+', '', desc) # 移除URL
desc_clean = re.sub(r'Image:\s*\[.*?\]', '', desc_clean) # 移除Image标记
desc_clean = re.sub(r'#\S+', '', desc_clean) # 移除话题标签
desc_clean = re.sub(r'[\r\n]+', '', desc_clean) # 移除换行符
desc_clean = re.sub(r'[<>{}[\]\\]', '', desc_clean) # 移除特殊字符
text = f"{title} {desc_clean}".strip()
# 识别主体(谁)
subject = user_name if user_name else "博主"
# 识别行为(做什么)
action = ""
topic = ""
# 旅行/风景类
if any(kw in text for kw in ["旅行", "旅游", "打卡", "景点", "出游", "攻略"]):
action = "分享"
location_match = re.search(r"在([^\s,,。!?]{2,8})(旅行|旅游|打卡|游玩)", text)
if location_match:
topic = f"{location_match.group(1)}旅行攻略与体验"
else:
topic = "旅行见闻与实用攻略"
# 美食类
elif any(kw in text for kw in ["美食", "做饭", "食谱", "餐厅", "探店", "好吃", "烹饪", "料理"]):
action = "推荐"
topic = "美食制作方法与餐厅探店体验"
# 穿搭/时尚类
elif any(kw in text for kw in ["穿搭", "穿衣", "服装", "时尚", "OOTD", "ootd", "搭配"]):
action = "展示"
topic = "时尚穿搭技巧与单品推荐"
# 美妆/护肤类
elif any(kw in text for kw in ["化妆", "妆容", "美妆", "护肤", "彩妆", "口红"]):
action = "分享"
topic = "美妆教程与护肤心得"
# 宠物类
elif any(kw in text for kw in ["宠物", "猫", "狗", "萌宠", "铲屎官"]):
action = "记录"
topic = "宠物日常与养护经验"
# 游戏类
elif any(kw in text for kw in ["游戏", "王者", "电竞", "玩家"]):
action = "分享"
topic = "游戏攻略与精彩操作"
# 搞笑/娱乐类
elif any(kw in text for kw in ["搞笑", "有趣", "沙雕", "表情包", "梗"]):
action = "创作"
topic = "趣味内容与搞笑段子"
# 生活/日常类
elif any(kw in text for kw in ["日常", "生活", "vlog", "记录", "Vlog", "VLOG"]):
action = "记录"
topic = "日常生活分享与个人见闻"
# 情感类
elif any(kw in text for kw in ["情感", "爱情", "恋爱", "故事", "感悟"]):
action = "分享"
topic = "情感故事与人生感悟"
# 教程/干货类
elif any(kw in text for kw in ["教程", "方法", "技巧", "攻略", "干货"]):
action = "传授"
topic = "实用技巧与干货教程"
# 默认
else:
action = "分享"
if title and len(title) > 5:
topic = title[:25] + "..."
else:
topic = "精彩内容与个人体验"
# 组合生成摘要
summary = f"{subject}{action}{topic}"
# 控制长度在30-60字
if len(summary) < 30:
if desc_clean and len(desc_clean) > 10:
extra = desc_clean[:30].strip()
summary = f"{summary},{extra}..."
elif len(summary) > 60:
summary = summary[:57] + "..."
# 最终清理:移除所有换行符和特殊字符
summary = re.sub(r'[\r\n]+', '', summary)
summary = re.sub(r'[<>{}[\]\\|]', '', summary)
return summary
def format_ranking_list(articles: list, category: str = "综合全部") -> str:
"""
将榜单数据格式化为表格样式
字段:排名、笔记信息、互动数、点赞、评论、收藏、分享
Args:
articles: 笔记列表(保持接口原始顺序)
category: 当前分类
Returns:
表格格式的字符串
"""
if not articles:
return f"未获取到【{clean_text(category)}】分类的爆款内容数据"
output_lines = []
# 表头 - 加粗并添加图标,标题后添加数据说明
output_lines.append(f"**🔥 【{clean_text(category)}】每日爆款笔记**")
output_lines.append("*每日爆款笔记指笔记发后第3天比第2天涨的数据由高到低Top50*")
output_lines.append("| 排名 | 笔记信息 | 互动数 | 点赞 | 评论 | 收藏 | 分享 |")
output_lines.append("|:---:|:---|:---:|:---:|:---:|:---:|:---:|")
for idx, article in enumerate(articles, 1):
# 获取并清理所有字段
title = clean_text(article.get("title", "无标题")) or "无标题"
user_name = clean_text(article.get("userName", "未知用户")) or "未知用户"
photo_jump_url = article.get("photoJumpUrl", "#") or "#"
# URL中的空格编码为%20,其他特殊字符清理
photo_jump_url = photo_jump_url.replace(" ", "%20")
user_jump_url = (article.get("userJumpUrl", "#") or "#").replace(" ", "%20")
fans = clean_text(str(article.get("fans", "0"))) or "0"
# 从anaAdd对象获取互动数据并清理
ana_add = article.get("anaAdd", {})
interactive_count = clean_text(str(ana_add.get("interactiveCount", "0") or article.get("interactiveCount", "0"))) or "0"
add_interactive_count = clean_text(str(ana_add.get("addInteractiveount", "0") or article.get("addInteractiveount", "0"))) or "0"
like_count = clean_text(str(ana_add.get("useLikeCount", "0") or article.get("useLikeCount", "0"))) or "0"
add_like_count = clean_text(str(ana_add.get("addLikeCount", "0") or article.get("addLikeCount", "0"))) or "0"
comment_count = clean_text(str(ana_add.get("useCommentCount", "0") or article.get("useCommentCount", "0"))) or "0"
add_comment_count = clean_text(str(ana_add.get("addCommentCount", "0") or article.get("addCommentCount", "0"))) or "0"
collected_count = clean_text(str(ana_add.get("collectedCount", "0") or article.get("collectedCount", "0"))) or "0"
add_collected_count = clean_text(str(ana_add.get("addCollectedCunt", "0") or article.get("addCollectedCunt", "0"))) or "0"
share_count = clean_text(str(ana_add.get("useShareCount", "0") or article.get("useShareCount", "0"))) or "0"
add_share_count = clean_text(str(ana_add.get("addShareCount", "0") or article.get("addShareCount", "0"))) or "0"
# 排名(前三名带奖牌)
if idx == 1:
rank_display = "🥇"
elif idx == 2:
rank_display = "🥈"
elif idx == 3:
rank_display = "🥉"
else:
rank_display = str(idx)
# 笔记信息:标题链接 + 作者链接(粉丝数)
if photo_jump_url and photo_jump_url != "#":
if user_jump_url and user_jump_url != "#":
note_info = f"[{title}]({photo_jump_url})<br>[{user_name}]({user_jump_url})({fans}粉丝)"
else:
note_info = f"[{title}]({photo_jump_url})<br>{user_name}({fans}粉丝)"
else:
if user_jump_url and user_jump_url != "#":
note_info = f"{title}<br>[{user_name}]({user_jump_url})({fans}粉丝)"
else:
note_info = f"{title}<br>{user_name}({fans}粉丝)"
# 格式化数字
def format_num(val):
try:
num = parse_count(val)
if num >= 10000:
return f"{num // 10000}w+"
return str(val)
except:
return str(val)
# 格式化显示:累计数值<br>↑新增数值
interactive_display = f"{format_num(interactive_count)}<br>↑新增{format_num(add_interactive_count)}"
like_display = f"{format_num(like_count)}<br>↑新增{format_num(add_like_count)}"
comment_display = f"{format_num(comment_count)}<br>↑新增{format_num(add_comment_count)}"
collect_display = f"{format_num(collected_count)}<br>↑新增{format_num(add_collected_count)}"
share_display = f"{format_num(share_count)}<br>↑新增{format_num(add_share_count)}"
output_lines.append(f"| {rank_display} | {note_info} | {interactive_display} | {like_display} | {comment_display} | {collect_display} | {share_display} |")
return "\n".join(output_lines)
def analyze_content(article: dict) -> str:
"""
基于笔记的具体文本内容,生成专业且通俗的内容分析
分析规则:
1. 内容概述:简明扼要地概括笔记核心内容
2. 热点利用:分析笔记借用了哪些热点话题或趋势
3. 传播作用:阐述内容起到的作用
4. 达成效果:说明内容达到的效果
Args:
article: 笔记数据
Returns:
内容分析字符串(专业通俗风格)
"""
# 提取文本内容
title = article.get("title", "")
desc = article.get("desc", "")
# 合并所有文本用于分析
all_text = f"{title} {desc}"
# 动态提取关键信息
content_analysis = []
# 1. 内容概述 - 基于实际内容动态生成
topic_keywords = []
if "旅行" in all_text or "风景" in all_text or "自由" in all_text:
topic_keywords.append("旅行生活")
if "猫" in all_text or "狗" in all_text or "宠物" in all_text:
topic_keywords.append("萌宠日常")
if "游戏" in all_text or "王者" in all_text:
topic_keywords.append("游戏娱乐")
if "手工" in all_text or "DIY" in all_text:
topic_keywords.append("手工创意")
if "美食" in all_text or "做饭" in all_text:
topic_keywords.append("美食分享")
if "穿搭" in all_text or "时尚" in all_text:
topic_keywords.append("时尚穿搭")
if "上学" in all_text or "学校" in all_text:
topic_keywords.append("校园生活")
if "表情包" in all_text or "梗图" in all_text:
topic_keywords.append("趣味内容")
if "帅哥" in all_text or "美女" in all_text:
topic_keywords.append("颜值分享")
# 从desc中提取标签
if "#" in desc:
tags = [t.strip() for t in desc.split("#") if t.strip()]
if tags:
topic_keywords.extend(tags[:2])
# 动态生成内容概述
if topic_keywords:
topic_str = "、".join(topic_keywords[:3])
content_analysis.append(f"本笔记围绕{topic_str}等话题展开")
else:
if len(title) > 0:
key_part = title[:20] if len(title) > 20 else title
content_analysis.append(f"本笔记聚焦{key_part}等话题")
# 2. 热点利用 - 基于实际内容动态判断
hotspot_detected = []
if any(word in all_text for word in ["热门", "爆火", "流行"]):
hotspot_detected.append(("热门话题", "流量热点"))
if any(word in all_text for word in ["治愈", "温暖", "感动"]):
hotspot_detected.append(("治愈系", "情感热点"))
if any(word in all_text for word in ["搞笑", "有趣", "沙雕"]):
hotspot_detected.append(("搞笑内容", "娱乐热点"))
if any(word in all_text for word in ["日常", "生活", "vlog"]):
hotspot_detected.append(("生活分享", "日常热点"))
if hotspot_detected:
hotspots = [h[0] for h in hotspot_detected[:2]]
content_analysis.append(f"笔记借力{'与'.join(hotspots)}等话题吸引关注")
else:
content_analysis.append("笔记紧扣用户感兴趣的话题")
# 3. 传播作用
if any(word in all_text for word in ["分享", "推荐", "安利"]):
content_analysis.append("通过分享体验满足用户好奇心")
elif any(word in all_text for word in ["教程", "攻略", "方法"]):
content_analysis.append("通过提供实用价值满足用户需求")
else:
content_analysis.append("满足用户对相关内容的需求")
# 4. 达成效果
ana_add = article.get("anaAdd", {})
like_count = parse_count(ana_add.get("useLikeCount", 0) or article.get("useLikeCount", 0))
collected_count = parse_count(ana_add.get("collectedCount", 0) or article.get("collectedCount", 0))
if like_count > 10000 or collected_count > 1000:
content_analysis.append("获得高互动传播效果")
else:
content_analysis.append("获得良好传播效果")
# 组合分析内容
analysis = ",".join(content_analysis) + "。"
return analysis
def main():
parser = argparse.ArgumentParser(description="获取小红书爆款内容榜单")
parser.add_argument("--rank_date", required=False, default=None, help="查询日期(格式:yyyy-MM-dd,默认今天)")
parser.add_argument("--category", required=False, default=None, help="分类名称(如:时尚穿搭、美食佳肴等),不传则根据关键词匹配")
parser.add_argument("--keyword", required=False, default=None, help="用户输入的关键词,用于自动匹配分类")
parser.add_argument("--top_n", type=int, default=10, help="返回前N条数据(默认10)")
parser.add_argument("--list_categories", action="store_true", help="列出所有可用分类")
parser.add_argument("--output_json", required=False, default=None, help="将获取到的原始数据写入指定 JSON 缓存文件,供 gen_html 复用")
args = parser.parse_args()
# 列出所有分类
if args.list_categories:
print("可用分类列表:")
for cat in get_all_categories():
print(f" - {cat}")
return 0
try:
# 处理日期参数
rank_date, is_auto = get_query_date(args.rank_date)
# 处理分类参数
if args.category:
category = args.category
elif args.keyword:
category = match_category(args.keyword)
print(f"根据关键词【{args.keyword}】匹配到分类:【{category}】")
else:
category = "综合全部"
print(f"正在获取小红书爆款内容数据...")
print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if is_auto:
cutoff_time = datetime.now().replace(hour=19, minute=0, second=0, microsecond=0)
if datetime.now() >= cutoff_time:
print(f"当前已过19:00,查询昨日数据(当日已更新)")
else:
print(f"当前未过19:00,查询前天数据(等待当日19:00更新)")
print(f"查询日期: {rank_date}")
print(f"分类: {category}")
print("-" * 60)
# 获取数据
data = fetch_explosive_articles(rank_date, category)
# 处理数据(保持接口原始顺序)
articles = process_ranking_data(data, args.top_n)
# 无数据回退逻辑:仅回退一次
if len(articles) == 0 and is_auto:
prev_date = (datetime.strptime(rank_date, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
print(f"默认日期 {rank_date} 无数据,回退一天尝试 {prev_date}")
print("-" * 60)
data = fetch_explosive_articles(prev_date, category)
articles = process_ranking_data(data, args.top_n)
if articles:
rank_date = prev_date # 更新有效日期
# 写入 JSON 缓存文件(有数据时)
if args.output_json and len(articles) > 0:
raw_articles = process_ranking_data(data, 50) # 取全量50条写入缓存
cache_data = {
"rank_date": rank_date,
"category": category,
"articles": raw_articles
}
try:
os.makedirs(os.path.dirname(os.path.abspath(args.output_json)), exist_ok=True)
with open(args.output_json, "w", encoding="utf-8") as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
print(f"数据已缓存至: {args.output_json}", file=sys.stderr)
except Exception as e:
print(f"缓存写入失败: {e}", file=sys.stderr)
# 检查数据量是否足够
if len(articles) < 10:
print(f"提示:当前获取到 {len(articles)} 条数据,不足10条")
print("建议:可尝试查询其他日期或其他分类的数据")
print("-" * 60)
# 格式化输出
ranking_list = format_ranking_list(articles, category)
print(ranking_list)
# 返回统计信息
print("\n" + "=" * 60)
if articles:
print(f"共获取到 {len(articles)} 条【{category}】爆款内容数据")
else:
print(f"未获取到【{category}】分类的爆款内容数据")
print("建议:尝试查询其他日期或其他分类的数据")
return 0
except Exception as e:
print(f"错误: {str(e)}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())