
Wechat Top Account
- 235 installs
- 316 repo stars
- Updated August 4, 2026
- redfox-data/redfox-community
Use wechat-top-account for development tasks
About
wechat-top-account: A skill for development. This provides functionality for development workflows.
- wechat-top-account
Wechat Top Account by the numbers
- 235 all-time installs (skills.sh)
- +18 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,678 of 4,347 Backend & APIs 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 wechat-top-accountAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 235 |
|---|---|
| repo stars | ★ 316 |
| Last updated | August 4, 2026 |
| Repository | redfox-data/redfox-community ↗ |
What it does
Use wechat-top-account for development tasks
Files
公众号热门账号推荐
简介
公众号热门账号推荐是一款微信公众号综合实力排行查询工具,帮助用户快速获取同赛道头部账号的阅读与互动数据,支撑运营对标与竞品跟踪。
通过对话或命令行,你可以:
- 📊 查询 23 个垂直分类的 TOP50 榜单,支持日榜、周榜、月榜切换
- 📈 查看综合评分(满分 100)及发布数、阅读分布、点赞、在看、转发等指标
- 🔍 获取内容生态、热点影响、蓝海赛道三类数据解读
- 📄 导出 HTML 可视化报告(支持图片/PDF)
- 🔔 订阅日/周/月榜,按更新时间自动推送
适用于公众号运营者、MCN 机构、品牌营销人员及需跟踪公众号竞争格局的内容团队。
功能特性
🎯 核心功能
- 📊 多维排行查询:按日榜、周榜、月榜查询 23 个分类 TOP50,输入关键词自动匹配分类
- 📈 综合评分体系:基于阅读、点赞、在看、转发等指标计算综合评分,满分 100
- 🔍 深度分析:提供内容生态变化、热点影响力量化、蓝海赛道挖掘三种解读视角
- 📄 HTML 报告:生成可视化榜单页面,支持导出图片和 PDF
- 🔔 定时订阅:支持订阅日榜、周榜、月榜,按更新时间自动推送
✨ 特色亮点
- ⚡ 定时更新:日榜每日 17:30、周榜每周一 17:30、月榜每月 3 日 23:00 自动刷新
- 📅 有限回溯:日榜近 7 天、周榜近 3 周、月榜近 3 个月,超出范围自动调整并提示
- 🔗 数据一致:榜单表格与 HTML 报告共用同一 API,顺序与内容完全一致
- 🔒 API 接入:通过红狐Hub API Key 调用
鉴权
获取 API Key
1. 访问 红狐Hub 官网 了解服务详情 2. 前往 注册页面 注册账号 3. 新注册用户将获赠免费积分,可立即开始使用 API 服务 4. 注册登录后,在个人中心获取 API Key,格式为 ak_xxxxxxxx
配置 API Key
REDFOX_API_KEY 从环境变量获取,格式 ak_xxxxxxxx。脚本采用三级认证回退:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | 环境变量 REDFOX_API_KEY | 直接读取当前会话的环境变量 |
| 2 | Shell 配置文件 | 自动读取 ~/.zshrc / ~/.bashrc / ~/.bash_profile / PowerShell Profile 中的配置 |
| 3 | 用户输入 | 若以上均未检测到,提示用户手动配置 |
macOS / Linux:
echo 'export REDFOX_API_KEY=<你的apikey>' >> ~/.zshrc
source ~/.zshrcWindows(PowerShell):
[Environment]::SetEnvironmentVariable("REDFOX_API_KEY", "<你的apikey>", "User")或将以下内容写入 PowerShell Profile:
$env:REDFOX_API_KEY = "<你的apikey>"配置后需重启终端。验证:echo $REDFOX_API_KEY(macOS/Linux)或 $env:REDFOX_API_KEY(Windows PowerShell)。
一键安装
前置条件
- Python 3.8 及以上版本
requests库:pip install requests- 红狐Hub API Key,格式
ak_xxxxxxxx
安装方式
SkillHub
1. 前往 SkillHub 2. 搜索 wechat-top-account 或 公众号热门账号推荐 3. 安装说明
- 可通过「通过对话安装」方式安装
- 如未安装 SkillHub 商店,先安装商店再安装 skill
- 支持下载 Zip 包到本地,通过本地导入方式安装
ClawHub
1. 前往 ClawHub 2. 搜索 wechat-top-account 或 公众号热门账号推荐 3. 安装说明
- 支持使用 CLI 命令安装:
openclaw skills install wechat-top-account或clawhub install wechat-top-account - 支持下载 skill 包到本地并导入安装
- 安装成功后可在对应客户端直接使用
安装完成后,请先完成 鉴权 配置 API Key,重启终端即可在对话中发起查询。
方式二:命令行独立使用
无需安装技能包,配置好 API Key 后可直接运行 scripts/ 下脚本:
pip install requests
python scripts/gzh_growth_fetcher.py --list_categories环境变量配置
| 变量名 | 必填 | 说明 |
|---|---|---|
REDFOX_API_KEY | 是 | 红狐Hub API 访问密钥,格式 ak_xxxxxxxx |
详细获取与配置步骤见上方 鉴权 章节。
使用指南
基础使用
1. 发起榜单查询
在对话中输入查询需求,系统自动识别榜单类型和分类:
用户:查询科技类公众号日榜
>
助手:输出科技数码分类日榜 TOP50 表格(含综合评分、阅读与互动指标),并附功能询问文案。
2. 查看榜单结果
榜单数据直接输出,无需确认。每条结果包含排名、账号名称(可跳转)、综合评分、发布数/文章数、总阅读、头条阅读、最高阅读、总点赞、总在看、总转发。
用户:本周美食类公众号排名
>
助手:输出美食餐饮周榜 TOP50,标注数据时间周期与更新时间。
3. 选择扩展功能
榜单输出后,可按需选择后续服务:
用户:1
>
助手:基于 TOP50 数据输出内容生态变化、热点影响、蓝海赛道三类分析。
回复 1 获取分析、2 生成 HTML 报告、3 或「订阅日/周/月榜」开启定时推送。
高级使用
1. 指定分类与日期
通过 --category 或 --keyword 精确匹配分类,--rank_date 指定查询日期(超出范围自动调整):
# 首次查询时必须同时保存到临时文件(唯一一次API调用)
python scripts/gzh_growth_fetcher.py --rank_type week --category "美食餐饮" --top_n 50 > /tmp/gzh_result.json 2>/dev/null
python scripts/gzh_growth_fetcher.py --rank_type month --keyword "财经" --rank_date 2026-04-01 --top_n 50 > /tmp/gzh_result.json 2>/dev/null2. 生成 HTML 可视化报告
用户选择 HTML 功能后,使用已保存的数据生成可导出图片/PDF 的页面(不调用API):
python scripts/gen_gzh_html.py --input_json_file /tmp/gzh_result.json --top 503. 获取深度分析解读
用户确认后,基于当前 TOP50 全量数据输出三段式分析:内容生态变化、热点影响力量化、蓝海赛道挖掘。
4. 订阅定时推送
用户回复「订阅日榜」「订阅周榜」「订阅月榜」或「全部订阅」,系统将按对应更新时间推送最新榜单。
完整参数、输出模板与日期校验逻辑见 核心工作流程。
常用命令速查
| 命令 | 功能 |
|---|---|
python scripts/gzh_growth_fetcher.py --rank_type day --keyword "科技" --top_n 50 > /tmp/gzh_result.json 2>/dev/null | 科技类日榜 TOP50(首次查询,同时保存文件) |
python scripts/gzh_growth_fetcher.py --rank_type week --category "美食餐饮" --top_n 50 > /tmp/gzh_result.json 2>/dev/null | 美食餐饮周榜 TOP50(首次查询,同时保存文件) |
python scripts/gzh_growth_fetcher.py --rank_type month --keyword "财经" --top_n 50 > /tmp/gzh_result.json 2>/dev/null | 财经类月榜 TOP50(首次查询,同时保存文件) |
python scripts/gzh_growth_fetcher.py --list_categories | 列出全部可用分类 |
python scripts/gen_gzh_html.py --input_json_file /tmp/gzh_result.json --top 50 | 用预取数据生成 HTML(不调用API) |
python scripts/gzh_growth_fetcher.py --input_json_file /tmp/gzh_result.json | 用预取数据重新分析(不调用API) |
使用场景
场景一:日常运营参考
角色:公众号运营者
需求:了解所属赛道头部账号的内容产出与互动表现
使用方式:
1. 查询所属分类日榜或周榜 2. 对比发布频率、单篇阅读与互动率 3. 选择分析功能提取内容策略参考
预期收益:明确对标账号,优化选题与发布节奏
---
场景二:竞品追踪
角色:品牌营销经理
需求:跟踪竞品公众号排名与互动变化
使用方式:
1. 按周查询竞品所在分类榜单 2. 关注排名波动与阅读/转发变化 3. 订阅周榜定时接收更新
预期收益:及时掌握竞品内容发力方向,调整自身策略
---
场景三:赛道研判
角色:内容创业者
需求:评估候选赛道的竞争度与阅读需求
使用方式:
1. 查询多个候选分类的月榜数据 2. 选择蓝海赛道分析功能 3. 对比上榜账号数量与平均阅读/评分
预期收益:选择竞争度低、需求旺盛的方向切入
---
场景四:定期数据汇报
角色:MCN 机构运营人员
需求:为内部复盘或客户汇报提供可视化数据
使用方式:
1. 订阅日榜和周榜保持数据更新 2. 按月导出 HTML 报告(图片/PDF) 3. 结合分析功能撰写复盘结论
预期收益:提升汇报效率,数据口径统一可追溯
项目架构
目录结构
wechat-top-account/
├── SKILL.md # 产品说明文档
├── references/
│ └── core_workflow.md # 核心工作流程(模板、逻辑、步骤)
├── scripts/
│ ├── gzh_growth_fetcher.py # 榜单数据抓取
│ └── gen_gzh_html.py # HTML 可视化报告生成
└── assets/ # 静态资源技术栈
| 项目 | 说明 |
|---|---|
| 运行环境 | Python 3.8+ |
| 核心依赖 | requests |
| 数据来源 | 红狐Hub API |
| 部署方式 | SkillHub / ClawHub / 命令行脚本 |
核心模块
| 模块 | 职责 |
|---|---|
gzh_growth_fetcher.py | 通过 红狐Hub API 获取榜单数据,支持关键词匹配分类、日期校验与三级认证回退 |
gen_gzh_html.py | 将榜单数据渲染为可导出图片和 PDF 的 HTML 页面 |
core_workflow.md | 输出格式模板、Checklist、日期计算逻辑、完整操作步骤 |
数据更新规则
| 榜单 | 更新时间 | 数据范围 |
|---|---|---|
| 日榜 | 每日 17:30 | 昨日数据,近 7 天 |
| 周榜 | 每周一 17:30 | 上周数据,近 3 周 |
| 月榜 | 每月 3 号 23:00 | 上月数据,近 3 个月 |
常见问答
安装相关问题
Q1:未检测到 API Key 怎么办?
按 鉴权 章节步骤注册 红狐Hub 并配置 REDFOX_API_KEY 环境变量。新注册用户有免费积分。
Q2:如何验证配置已生效?
执行 echo $REDFOX_API_KEY,确认输出格式为 ak_xxxxxxxx。若未生效,检查终端是否已重启。
Q3:支持哪些部署方式?
支持 SkillHub、ClawHub 平台安装,也可直接运行 scripts/ 下 Python 脚本独立调用。
---
使用相关问题
Q4:支持哪些分类?
共 23 个分类:总排名、乐活生活、人文资讯、企业品牌、体育娱乐、健康养生、创投商业、学术研究、情感心理、房产楼市、搞笑幽默、教育考试、文摘精选、旅游出行、时尚潮流、民生资讯、汽车交通、知识百科、科技数码、美容美体、美食餐饮、职场发展、财富理财。
Q5:数据可回溯多久?
日榜近 7 天、周榜近 3 周、月榜近 3 个月。超出范围时系统自动调整至最近可用数据。
Q6:榜单与 HTML 报告数据是否一致?
两者共用同一 API 接口,数据完全一致。
Q7:数据多久更新一次?
日榜每日 17:30、周榜每周一 17:30、月榜每月 3 日 23:00 更新。
---
故障排除
Q8:查询返回空数据?
确认分类名称准确(可用 --list_categories 列出全部分类),检查查询日期在有效范围内。
Q9:HTML 生成失败?
确认已安装 requests 库且 API Key 有效,查看脚本错误输出定位原因。
---
安全与许可
Q10:API Key 如何保管?
REDFOX_API_KEY 仅通过环境变量读取,请勿写入代码仓库或公开分享。Key 泄露可能导致积分被消耗。
Q11:数据来源与使用范围?
榜单数据由 红狐Hub 平台提供,仅供运营分析、竞品跟踪等合法商业用途,请遵守平台服务条款。
核心工作流程
输出格式模板、数据校验规则、日期计算逻辑、操作步骤及使用示例详见 references/core_workflow.md。
执行任务时须严格遵循 core_workflow.md 中的:
- 格式优先级声明与分阶段输出规则
- 输出前必检 Checklist
- 榜单标准模板与按需输出模板
- 榜单类型判断与分类匹配规则
- 日榜 / 周榜 / 月榜日期校验逻辑
- 数据获取与格式化输出流程
- API 调用次数上限(同一轮查询仅允许 1 次 API 调用)
注意事项
- 热门账号推荐榜直接输出,无需确认;分析、HTML、订阅须用户选择后方可生成
- 榜单表格与 HTML 报告须使用同一数据源,禁止修改接口返回顺序
- 同一轮查询(榜单+分析+HTML)全过程只允许调用 1 次 API 接口,首次获取数据时必须同时保存到临时文件,后续操作一律使用
--input_json_file读取,禁止重复调用 API - 未收到明确查询指令时,禁止自动调用脚本或输出榜单内容
- 禁止折叠输出
- 脚本输出的
formatted_markdown字段已包含完整的标准模板格式,直接原样输出即可,禁止自行拼装或修改格式
WeChat Top Accounts / wechat-top-account
---
Introduction
Quickly access the WeChat Official Account comprehensive ranking TOP 50, helping operators and brands understand the reading and engagement data of top accounts in their niche — supporting operational benchmarking and competitor tracking.
Core Value
- Covers TOP 50 rankings across 23 vertical categories, with flexible switching between daily, weekly, and monthly charts
- A comprehensive scoring system based on multiple metrics including reads, likes, in-views, and shares, with a maximum score of 100
- Automatically outputs three types of in-depth analysis: content ecosystem shifts, hot topic impact, and blue-ocean niche discovery
- One-click generation of a visual report page with image and PDF export support
- Subscribe to daily, weekly, or monthly rankings for automated delivery on each update
Who It's For
- 📱 WeChat Official Account Operators — Understand top account performance in your niche to optimize topic selection and publishing cadence
- 🏢 MCN Operations Teams — Monitor the competitive landscape across managed accounts and generate regular data reports
- 🏷️ Brand Marketing Managers — Track competitor account rankings and engagement shifts to adjust content strategy
- 🚀 Content Entrepreneurs — Evaluate competition levels and reading demand across candidate niches to identify blue-ocean opportunities
---
Features
Core Functions
- Multi-Dimensional Ranking Query: Query TOP 50 across 23 categories by daily, weekly, or monthly chart — keywords automatically match the right category
- Comprehensive Scoring System: A composite score (max 100) based on post count, reads, likes, in-views, shares, and more
- In-Depth Analysis: Three analytical perspectives: content ecosystem changes, hot topic impact quantification, and blue-ocean niche discovery
- Visual Report Export: One-click generation of a visual ranking page with image and PDF export support
- Scheduled Subscription Push: Subscribe to daily, weekly, or monthly rankings for automatic delivery on each update cycle
Highlights
- Scheduled Updates: Daily chart refreshes at 17:30, weekly chart every Monday at 17:30, monthly chart on the 3rd at 23:00
- Controlled Lookback: Daily charts span the past 7 days, weekly the past 3 weeks, monthly the past 3 months — out-of-range requests auto-adjust with a prompt
- Data Consistency: The ranking table and visual report share the same data source, with identical order and content
- On-Demand Expansion: The ranking table outputs directly without confirmation; analysis, reports, and subscriptions are user-initiated
---
API Key Acquisition & Security
- This skill requires the environment variable:
REDFOX_API_KEY. REDFOX_API_KEYis provided by RedFoxHub (https://redfox.hk).- Register an account at RedFoxHub to obtain your
REDFOX_API_KEY. - Configure the device environment variable
REDFOX_API_KEYbefore using this skill. - Before providing a key, confirm its source, scope, expiration, and whether it supports reset/revocation.
- Never hardcode or expose the key in plaintext in code, prompts, logs, or output files.
---
Usage Guide
Just describe your need in plain language — no commands to memorize.
Quick Reference
| Intent | Example Phrase | Result |
|---|---|---|
| Query daily chart by category | "Show the tech category daily ranking" | Outputs the Tech & Digital daily TOP 50 with composite scores and engagement metrics |
| Query weekly chart by category | "This week's food category ranking" | Outputs the Food & Dining weekly TOP 50 with time range noted |
| Query monthly chart by category | "Finance category monthly ranking" | Outputs the Finance monthly TOP 50 |
| Get in-depth analysis | (After chart output) reply "1" | Outputs three analysis types: content ecosystem, hot topic impact, and blue-ocean niches |
| Generate visual report | (After chart output) reply "2" | Generates a visual page exportable as image or PDF |
| Subscribe to updates | "Subscribe to weekly ranking" | Sets up automated weekly delivery of the latest chart |
| Subscribe to all | "Subscribe to all" | Enables daily, weekly, and monthly chart subscriptions at once |
Sample Output
After querying "Tech category daily ranking", you will receive:
1. Ranking Table: Rank, account name (clickable), composite score, post count, total reads, headline reads, peak reads, total likes, total in-views, total shares 2. Extension Prompt: Reply "1" for in-depth analysis, "2" for a visual report, "3" or say "Subscribe to daily/weekly/monthly" to enable scheduled delivery
---
Use Cases
| Scenario | Role | Example Query | Benefit |
|---|---|---|---|
| Daily operations reference | Account Operator | "Show tech category daily ranking" | Compare posting frequency and engagement rates to benchmark and optimize topic cadence |
| Competitor tracking | Brand Marketing Manager | "This week's FMCG category ranking" | Track competitor ranking shifts and content direction to adjust your own strategy |
| Niche evaluation | Content Entrepreneur | "Compare food and travel monthly rankings" | Assess competition levels across candidate niches and pick a blue-ocean direction |
| Regular data reporting | MCN Operations | "Subscribe to monthly ranking" | Export monthly visual reports with consistent and traceable data |
---
Key Data Notes
| Chart | Update Time | Data Range |
|---|---|---|
| Daily | Every day at 17:30 | Yesterday's data, past 7 days |
| Weekly | Every Monday at 17:30 | Last week's data, past 3 weeks |
| Monthly | 3rd of each month at 23:00 | Last month's data, past 3 months |
Covers 23 vertical categories, including Overall, Tech & Digital, Food & Dining, Finance, Education, Fashion, Health & Wellness, Emotional & Psychology, Career Development, and more. Just enter a keyword to automatically match the right category.
公众号热门账号推荐 / wechat-top-account
---
简介
快速获取微信公众号综合实力排行榜 TOP50,帮助运营者和品牌方掌握同赛道头部账号的阅读与互动数据,支撑运营对标与竞品跟踪。
核心价值
- 覆盖 23 个垂直分类的 TOP50 榜单,支持日榜、周榜、月榜自由切换
- 综合评分体系基于阅读、点赞、在看、转发等多项指标,满分 100 分
- 自动输出内容生态变化、热点影响、蓝海赛道三类深度解读
- 一键生成可视化页面报告,支持导出图片和 PDF
- 支持订阅日榜、周榜、月榜,按更新时间自动推送
适用对象
- 📱 公众号运营者 — 了解赛道头部账号表现,优化选题与发布节奏
- 🏢 MCN 机构运营 — 统一监控旗下账号竞争格局,生成定期数据汇报
- 🏷️ 品牌营销经理 — 跟踪竞品公众号排名与互动变化,调整内容策略
- 🚀 内容创业者 — 评估候选赛道竞争度与阅读需求,选择蓝海方向
---
功能特性
核心功能
- 多维排行查询:按日榜、周榜、月榜查询 23 个分类的 TOP50,输入关键词自动匹配分类
- 综合评分体系:基于发布数、阅读量、点赞、在看、转发等多维度综合评分,满分 100
- 深度分析解读:提供内容生态变化、热点影响力量化、蓝海赛道挖掘三种分析视角
- 可视化报告导出:一键生成榜单可视化页面,支持导出图片和 PDF
- 定时订阅推送:支持订阅日榜、周榜、月榜,按更新时间自动推送最新数据
特色亮点
- 定时更新:日榜每日 17:30、周榜每周一 17:30、月榜每月 3 日 23:00 自动刷新
- 有限回溯:日榜近 7 天、周榜近 3 周、月榜近 3 个月,超出范围自动调整并提示
- 数据一致:榜单表格与可视化报告共用同一数据来源,顺序与内容完全一致
- 按需扩展:榜单直接输出无需确认,分析、报告、订阅由用户自主选择
---
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
---
使用指南
直接用自然语言描述需求,无需记忆命令。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 查询分类日榜 | 查询科技类公众号日榜 | 输出科技数码分类日榜 TOP50,含综合评分与互动指标 |
| 查询分类周榜 | 本周美食类公众号排名 | 输出美食餐饮周榜 TOP50,标注数据时间周期 |
| 查询分类月榜 | 财经类公众号月榜 | 输出财经类月榜 TOP50 |
| 获取深度分析 | (榜单输出后回复)1 | 基于 TOP50 输出内容生态、热点影响、蓝海赛道三类分析 |
| 生成可视化报告 | (榜单输出后回复)2 | 生成可导出图片和 PDF 的榜单页面 |
| 订阅推送 | 订阅周榜 | 设置每周定时推送最新榜单 |
| 全部订阅 | 全部订阅 | 同时订阅日榜、周榜、月榜 |
输出示例
查询「科技类公众号日榜」后,你将收到:
1. 榜单表格:排名、账号名称(可点击跳转)、综合评分、发布数、总阅读、头条阅读、最高阅读、总点赞、总在看、总转发 2. 扩展功能提示:回复 1 获取深度分析、回复 2 生成可视化报告、回复 3 或说「订阅日/周/月榜」开启定时推送
---
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 日常运营参考 | 公众号运营者 | 查询科技类公众号日榜 | 对比发布频率与互动率,明确对标账号,优化选题节奏 |
| 竞品追踪 | 品牌营销经理 | 本周快消类公众号排名 | 跟踪竞品排名波动与内容发力方向,调整自身策略 |
| 赛道研判 | 内容创业者 | 对比美食和旅游类月榜 | 评估候选赛道竞争度,选择蓝海方向切入 |
| 定期数据汇报 | MCN 机构运营 | 订阅月榜 | 按月导出可视化报告,数据口径统一可追溯 |
---
重要数据说明
| 榜单 | 更新时间 | 数据范围 |
|---|---|---|
| 日榜 | 每日 17:30 | 昨日数据,近 7 天 |
| 周榜 | 每周一 17:30 | 上周数据,近 3 周 |
| 月榜 | 每月 3 日 23:00 | 上月数据,近 3 个月 |
共覆盖 23 个垂直分类,包括总排名、科技数码、美食餐饮、财经、教育考试、时尚潮流、健康养生、情感心理、职场发展等。输入关键词即可自动匹配对应分类。
公众号热门账号推荐 · 核心工作流程
本文档与根目录 SKILL.md 配套使用:SKILL.md 提供产品说明、安装配置与使用场景;本文档涵盖输出格式、日期逻辑、完整脚本操作步骤与示例。执行任务时须完整遵循本文档规范。
格式优先级声明(最高优先级)
本 Skill 的输出格式优先级高于智能体默认输出行为。无论智能体默认风格如何,须严格按照本文档规定的格式模板重组输出。
分阶段输出规则:热门账号推荐榜直接输出;分析、HTML 页面、订阅服务需先输出功能询问文案,用户确认后再生成。
执行环境(强制)
本 Skill 禁止在子任务或子 Agent 中执行,仅限主 Agent 任务。若检测到子任务环境,立即停止并返回:公众号热门账号推荐查询请在主对话中直接提问。
---
API 调用次数上限(强制,违反即为严重错误)
同一轮查询(榜单 + 分析 + HTML)全过程只允许调用 1 次 API 接口,禁止重复调用。
执行规则: 1. 首次获取数据时,必须同时将输出保存到临时文件(步骤A) 2. 后续所有操作(提取 formatted_markdown、生成 HTML、深度分析)必须使用 --input_json_file 读取已保存的文件,禁止再次运行脚本调用 API 3. 若因执行方式导致临时文件未成功保存,仅允许补存操作(用已输出的 JSON 手动写入文件),不得重新调用 API
错误示例:查询榜单调用1次 → 保存文件又调用1次 → 提取 markdown 再调用1次 = 3次调用(严重违规)
正确示例:查询榜单并保存文件调用1次 → 提取 markdown 从文件读取(0次)→ 生成 HTML 从文件读取(0次)= 总计1次调用
---
输出前必检Checklist(每次输出前逐项打勾,全部通过才能输出)
[ ] 标题格式是否使用二级标题:## 公众号热门账号推荐榜{日榜/周榜/月榜} - {分类名} (更新时间:{时间})
[ ] 数据说明是否在标题后、表格前(斜体)
[ ] 表格列顺序是否严格:排名→账号名称→综合评分(满分100)→发布数/文章数→总阅读数→头条阅读数→最高阅读数→总点赞→总在看→总转发
[ ] 排名1-3是否用了🥇🥈🥉,其余用数字
[ ] 账号名称是否加粗+跳转链接(https://open.weixin.qq.com/qr/code?username={accountId})
[ ] 指定字段是否都加粗(综合评分、总阅读、头条阅读、最高阅读、总点赞、总转发)
[ ] 数字>=1万是否用x.x万格式,>=1亿用x.x亿格式
[ ] 榜单表格是否输出了全部数据(TOP50)
[ ] 榜单表格后是否输出了功能询问文案
[ ] 分析/HTML/订阅是否在用户确认后才输出(禁止未经询问直接输出)
[ ] 本轮查询是否仅调用了1次API(首次获取数据时已保存临时文件,后续操作均使用--input_json_file)输出标准模板(严格套用,禁止自由发挥)
输出顺序:热门账号推荐榜(直接输出)→ 功能询问文案(直接输出)→ 用户选择后按需输出分析/HTML/订阅
热门账号推荐榜(直接输出)
公众号热门账号推荐榜{日榜/周榜/月榜} - {分类名} (更新时间:{更新时间})
公众号综合实力{日榜/周榜/月榜},基于阅读、点赞、转发、在看等多维数据综合排名 数据统计时间周期:{时间范围}
时间周期格式:日榜="2026-05-05";周榜="2026-05-04至2026-05-10";月榜="2026-04-01至2026-04-30"
| 排名 | 账号名称 | 综合评分(满分100) | 发布数/文章数 | 总阅读数 | 头条阅读数 | 最高阅读数 | 总点赞 | 总在看 | 总转发 |
|---|---|---|---|---|---|---|---|---|---|
| 🥇 | [公众号名](https://open.weixin.qq.com/qr/code?username={accountId}) | 94.7 | 21/73 | 226.9万 | 151.4万 | 10.0万 | 1.5万 | 5729 | 9.3万 |
| 🥈 | [公众号名](https://open.weixin.qq.com/qr/code?username={accountId}) | 93.8 | 21/46 | 179.7万 | 153.2万 | 10.0万 | 1.9万 | 6064 | 5.9万 |
| 🥉 | [公众号名](https://open.weixin.qq.com/qr/code?username={accountId}) | 93.2 | 7/51 | 209.3万 | 25.5万 | 10.0万 | 2736 | 821 | 5295 |
| 4 | 公众号名 | 92.5 | 7/43 | 155.8万 | 65.7万 | 10.0万 | 1.3万 | 1941 | 2.0万 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 50 | 公众号名 | 80.2 | 3/15 | 28.6万 | 9.1万 | 5.2万 | 892 | 413 | 3210 |
功能询问文案(榜单表格后直接输出,一字不改)
技能还支持以下功能:
1️⃣ 支持对以上数据进行分析。其中包含内容生态变化分析、热点影响力量化、蓝海赛道挖掘三种分析角度,方便您了解公众号阅读数据或获取运营参考时使用。
2️⃣ 可生成公众号风格的html文件包。支持导出图片、导出PDF,方便阅读、保存和分享。
3️⃣ 支持订阅每日/周/月的公众号账号最新排名,订阅后将定时推送给您(日榜将在每日17点30更新/周榜将在每周一17:30准时更新/月榜将在每月3号23:00准时推送)。直接回复"订阅日/周/月榜"即可订阅相关榜单,或者"全部订阅"将会为您推送全部类型的榜单。
回复数字即可~
---
按需输出模板(用户确认后使用)
热门账号推荐榜分析(用户选择1️⃣后输出)
公众号热门账号推荐榜{日榜/周榜/月榜}分析
以下内容分析基于获取的全部数据(TOP50)进行~
内容生态变化分析
- 平均发布数趋势:统计TOP50账号的平均发布数(发布天数/总天数),如近半年平均发布数从XX降至XX,说明用户从「追内容数量」转向「追内容质量」
- 平均单篇阅读趋势:统计TOP50账号的平均单篇阅读(总阅读/发布篇数),如从XX升至XX,验证内容质量提升带来的阅读效率增长
- 平均互动率趋势:统计TOP50的平均互动率(点赞+在看+转发/总阅读),如转发率从XX%持续降至XX%,说明公众号公域传播能力减弱,私域留存价值提升;如点赞率上升,说明读者深度阅读意愿增强
热点影响力量化
- 特殊事件节点前后对比:如618大促期间消费测评类账号排名集体上升XX位、政策出台后财经解读类账号入榜数增加XX个
- 热点对内容流量的拉动效果:量化特殊事件期间相关分类账号的平均阅读增幅(如+XX%)、综合评分提升幅度(如+XX分)
- 行业流量周期规律:总结热点事件的流量拉动持续周期(如促销类约X天、政策类约X天),以及非热点期间的流量回落规律
蓝海赛道挖掘
- 细分赛道竞争度统计:统计TOP50里各细分赛道的上榜账号数量,如老年教育、宠物医疗这类细分赛道上榜账号仅X个
- 蓝海判断标准:上榜账号少(<=X个)但对应账号综合评分高(>=XX)、平均阅读高(>=XX万),说明需求旺盛但供给不足
- 切入建议:列出具体蓝海赛道及其核心指标(上榜数/平均评分/平均阅读),给出内容定位和运营策略建议
HTML页面(用户选择2️⃣后生成)
由 python scripts/gen_gzh_html.py 生成,直接输出页面内容,禁止只输出文件路径。
订阅响应(用户选择3️⃣或回复订阅指令时)
用户回复"订阅日榜" → 回复:"您已成功订阅日榜,将在每日17:30准时推送🌟~" 用户回复"订阅周榜" → 回复:"您已成功订阅周榜,将在每周一17:30准时推送🌟~" 用户回复"订阅月榜" → 回复:"您已成功订阅月榜,将在每月3号23:00准时推送🌟~" 用户回复"全部订阅" → 回复:"您已成功订阅全部榜单,将在每日17:30推送日榜、每周一17:30推送周榜、每月3号23:00推送月榜🌟~"
---
数据接口更新规则
- 日榜:每日17:30更新昨日数据
- 周榜:每周一17:30更新上周数据
- 月榜:每月3号23:00更新上月数据
- 数据范围:日榜/周榜/月榜的公众号热门账号推荐榜排名TOP50
- 分类覆盖:23个垂直领域分类
- 日期查询范围:日榜近7天、周榜近3周、月榜近3个月
数据一致性要求
- 热门账号推荐榜表格与HTML页面数据必须完全一致(相同顺序、相同内容)
- 两个脚本使用相同的API接口,禁止修改接口返回的顺序
操作步骤
第一步:确定榜单类型
根据用户输入判断榜单类型:
- 用户提到"日榜"、"今日"、"当天"、"昨日"等 → 日榜(day)
- 用户提到"周榜"、"本周"、"这周"、"上周"等 → 周榜(week)
- 用户提到"月榜"、"本月"、"这月"、"上月"等 → 月榜(month)
- 未指定 → 默认日榜(day)
第二步:分类匹配
根据用户输入自动匹配合适的分类:
可用分类列表(共23个): 总排名、乐活生活、人文资讯、企业品牌、体育娱乐、健康养生、创投商业、学术研究、情感心理、房产楼市、搞笑幽默、教育考试、文摘精选、旅游出行、时尚潮流、民生资讯、汽车交通、知识百科、科技数码、美容美体、美食餐饮、职场发展、财富理财
匹配规则:
- 用户输入具体产品词(如"手机")→ 匹配对应分类("科技数码")
- 用户输入领域词(如"穿搭"、"美食")→ 直接匹配分类
- 未指定分类 → 使用"人文资讯"
关键词-分类映射示例:
手机/电脑/AI → 科技数码
穿搭/时尚/潮流 → 时尚潮流
美食/做饭/探店 → 美食餐饮
职场/求职/跳槽 → 职场发展
股票/基金/理财 → 财富理财
健身/运动/减肥 → 体育娱乐
护肤/化妆/美妆 → 美容美体第三步:日期判断与校验
日期查询范围限制:
- 日榜:支持查询最近7天数据
- 周榜:支持查询最近3周数据
- 月榜:支持查询最近3个月数据
日榜日期逻辑:
- 更新时间:每日17:30更新昨日数据
- 自动计算:
- 当前时间 >= 17:30 → 查询昨日数据
- 当前时间 < 17:30 → 查询前天数据
- 用户输入"今日"、"今天":
- 若17:30前 → 展示提醒"日榜数据暂未更新,将为您查询最接近您需求日期的榜单数据⭐~"并推荐查询昨日更新的最新榜单
- 用户输入"昨日"、"昨天" → 查询前天的具体日期
- 用户输入具体日期 → 仅支持最近7天,超出7天展示提醒:"非常抱歉🙏,目前公众号榜单最多支持回溯「近7天的日榜/近3周的周榜/近3个月的月榜」,我将为您查询最接近您需求时间的{日榜/周榜/月榜}数据⭐~"并自动查询最近可用的数据
周榜日期逻辑:
- 更新时间:每周一17:30更新上周数据
- 自动计算:
- 当前时间 >= 周一17:30 → 查询本周一日期
- 当前时间 < 周一17:30 → 查询上周一日期
- 用户输入具体日期 → 仅支持最近3周,超出范围同上提醒
月榜日期逻辑:
- 更新时间:每月3号23:00更新上月数据
- 自动计算:
- 当前已过本月3号23:00 → 查询上月1号日期(上月数据已更新)
- 当前未过本月3号23:00 → 查询上上月1号日期
- 用户输入"当月"、"本月" → 根据更新时间判断,若未到更新时间则展示提醒
- 用户输入"上月"、"上个月" → 查询相应月份
- 用户输入具体月份 → 仅支持最近3个月,超出范围展示提醒:"非常抱歉🙏,目前公众号榜单最多支持回溯「近7天的日榜/近3周的周榜/近3个月的月榜」,我将为您查询最接近您需求时间的月榜数据⭐~"并自动查询最近可用数据
第四步:获取热门账号推荐榜数据
调用脚本获取榜单数据(默认TOP50),首次查询必须同时将输出保存到临时文件(这是唯一一次API调用):
查询命令:
# 首次查询:必须同时保存到临时文件(唯一一次API调用)
python scripts/gzh_growth_fetcher.py --rank_type day --keyword "科技" --top_n 50 > /tmp/gzh_result.json 2>/dev/null
python scripts/gzh_growth_fetcher.py --rank_type week --category "科技数码" --top_n 50 > /tmp/gzh_result.json 2>/dev/null
python scripts/gzh_growth_fetcher.py --rank_type month --keyword "美食" --top_n 50 > /tmp/gzh_result.json 2>/dev/null参数说明:
--rank_type:榜单类型 day/week/month(默认day)--category:分类名称(可选)--keyword:用户输入的关键词,用于自动匹配分类--rank_date:查询日期(可选,自动判断;超出范围自动调整)--top_n:返回条数(默认50)--start:起始排名(默认1)--list_categories:列出所有可用分类--input_json_file:预取数据JSON文件路径(指定后不再调用API,复用已获取的数据)
数据字段说明(API返回字段):
accountName:公众号名称accountId:公众号ID(用于构建搜索链接)accountAvatar:公众号头像URLrankPosition:排名位置category:所属分类publishCount:发布数/文章数(如"3/9",表示9天内发布了3天)totalReadCount:总阅读数headlineReadCount:头条阅读数maxReadCount:最高阅读数totalLikeCount:总点赞数totalInSeeCount:总在看数totalForwardCount:总转发数averageReadCount:平均阅读数(API返回,但展示表格中不显示)comprehensiveScore:综合评分(满分100,接口返回)
日期校验输出:
- 脚本返回的
reminder字段包含日期提醒信息,必须输出给用户 - 超出查询范围时脚本自动调整日期,需展示提醒消息
第五步:按标准模板输出
重要:脚本输出的 `formatted_markdown` 字段已包含完整的标准模板格式(标题+数据说明+reminder+表格+功能询问文案),直接原样输出该字段即可,禁止自行拼装或修改格式。
1. 直接原样输出 formatted_markdown 字段内容(已包含榜单表格+功能询问文案) 2. 等待用户回复,按需输出:
- 用户选择分析 → 输出热门账号推荐榜分析(三段式)
- 用户选择HTML → 调用脚本生成HTML页面并输出
- 用户选择订阅 → 按订阅响应模板回复
- 用户可多选,按用户提到的顺序依次输出
数据复用规则(强制):同一轮查询全过程只允许调用 1 次 API。当用户先查看榜单再请求HTML/分析时,必须使用 --input_json_file 传入已获取的数据,禁止再次调用API。步骤如下:
# 步骤A:首次获取数据时,必须同时将输出保存到临时文件(唯一一次API调用)
python scripts/gzh_growth_fetcher.py --rank_type day --keyword "科技" --top_n 50 > /tmp/gzh_result.json 2>/dev/null
# 步骤B:用预取数据生成 HTML(不调用API)
python scripts/gen_gzh_html.py --input_json_file /tmp/gzh_result.json --top 50
# 步骤C:用预取数据重新分析(不调用API)
python scripts/gzh_growth_fetcher.py --input_json_file /tmp/gzh_result.jsonHTML生成命令(必须复用数据,禁止直接调用API):
python scripts/gen_gzh_html.py --input_json_file {预取数据文件路径} --top 50HTML文件命名:公众号热门账号推荐榜【日/周/月】_【分类名】_【8位时间戳】.html
输出后自检:对照"输出前必检Checklist"逐项确认。
使用示例
示例1:日榜关键词查询
- 场景/输入:"帮我查一下科技类的公众号排行"
- 预期产出:匹配科技数码分类,日榜,输出热门账号推荐榜表格 + 功能询问文案,等待用户选择
示例2:周榜查询 + 用户选择分析
- 场景/输入:"这周公众号热门账号推荐榜怎么样" → 用户回复"1"
- 预期产出:使用周榜(week),输出热门账号推荐榜 + 询问文案 → 用户选1后输出三段式分析
示例3:月榜 + 订阅
- 场景/输入:"本月美食类公众号排行" → 用户回复"订阅月榜"
- 预期产出:使用月榜(month),输出热门账号推荐榜 + 询问文案 → 用户订阅月榜后回复"月榜每月3号23:00更新,将在对应时间为您推送~"
资源索引
- 脚本:见 scripts/gzh_growth_fetcher.py
- 用途:获取公众号热门账号推荐榜单数据
- 参数:--rank_type day/week/month / --category 分类名 / --keyword 关键词 / --rank_date 日期 / --top_n 条数 / --start 起始排名 / --input_json_file 预取数据文件路径
- 脚本:见 scripts/gen_gzh_html.py
- 用途:生成可视化HTML页面
- 参数:--rank_type day/week/month / --category 分类名 / --keyword 关键词 / --rank_date 日期 / --top 条数 / --output 输出路径 / --input_json_file 预取数据文件路径
注意事项
- 格式优先级(最高):本Skill的格式要求 > 智能体默认输出,必须100%按标准模板输出
- 执行环境(强制):本Skill只能在主Agent任务中执行,禁止在子任务/子Agent中执行
- 热门账号推荐榜:直接输出,无需询问
- 分析/HTML/订阅:必须先输出功能询问文案,用户确认后才可输出,禁止未经询问直接输出
- 禁止折叠输出
- HTML页面:用户选择后生成,直接输出页面内容,禁止只输出文件路径
- 数据一致性:热门账号推荐榜表格与HTML页面必须使用相同的数据源,禁止修改接口返回的顺序
- 数据复用(强制):同一轮查询(榜单+分析+HTML)全过程只允许调用 1 次 API 接口。首次获取数据时必须同时保存到临时文件,后续操作一律使用
--input_json_file读取,禁止重复调用 API - 更新时间:日榜/周榜每日17:30更新,月榜每月3号23:00更新
- 日期查询范围:日榜近7天、周榜近3周、月榜近3个月,超出范围自动调整并展示提醒
- 禁止自动查询:未订阅且未收到用户明确查询输入时,禁止自动调用脚本或输出任何热榜信息
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""公众号热门账号推荐HTML生成器
从API获取公众号热门账号推荐榜单数据,生成可独立打开的HTML页面。
用法:python gen_gzh_html.py [--rank_type day|week|month] [--rank_date YYYY-MM-DD] [--category 分类名] [--top N] [--output PATH]
输出:gzh_growth.html(与脚本同目录)
样式特性:
- 公众号风格(绿色主题 #07c160)
- 卡片式布局
- 公众号头像+名称+数据指标
- TOP3 奖牌徽章 + 左边框高亮
- 导出 PDF/图片功能
- 页面最大宽度 750px
"""
import json
import sys
import os
import pathlib
from datetime import datetime, timedelta
from urllib.parse import quote
import requests
# ===== 常量 =====
API_URL = "https://redfox.hk/story/api/cozeSkill/getGzhCozeSkillDataIndex"
SOURCE = "公众号综合实力账号榜-GitHub"
CATEGORIES = [
"总排名", "乐活生活", "人文资讯", "企业品牌", "体育娱乐", "健康养生",
"创投商业", "学术研究", "情感心理", "房产楼市", "搞笑幽默",
"教育考试", "文摘精选", "旅游出行", "时尚潮流", "民生资讯",
"汽车交通", "知识百科", "科技数码", "美容美体", "美食餐饮",
"职场发展", "财富理财"
]
CATEGORY_KEYWORDS = {
"总排名": ["总排名", "综合", "全部", "热门", "推荐", "随便", "总榜", "整体"],
"乐活生活": ["乐活", "生活", "日常", "生活方式", "生活日常", "好物推荐"],
"人文资讯": ["人文", "资讯", "文化", "历史", "哲学", "人文社科"],
"企业品牌": ["企业", "品牌", "公司", "商业品牌", "品牌营销"],
"体育娱乐": ["体育", "娱乐", "运动", "健身", "篮球", "足球", "综艺", "明星"],
"健康养生": ["健康", "养生", "保健", "中医", "调理", "减肥", "瘦身"],
"创投商业": ["创投", "商业", "投资", "创业", "融资", "商业模式"],
"学术研究": ["学术", "研究", "论文", "科研", "学报"],
"情感心理": ["情感", "心理", "恋爱", "婚姻", "情绪", "心理咨询"],
"房产楼市": ["房产", "楼市", "买房", "房价", "地产", "租房", "装修"],
"搞笑幽默": ["搞笑", "幽默", "段子", "吐槽", "沙雕", "表情包"],
"教育考试": ["教育", "考试", "培训", "考研", "考公", "留学", "英语", "学习"],
"文摘精选": ["文摘", "精选", "美文", "散文", "故事", "文章精选"],
"旅游出行": ["旅游", "出行", "旅行", "攻略", "景点", "酒店", "度假"],
"时尚潮流": ["时尚", "潮流", "穿搭", "服饰", "OOTD", "ootd", "搭配"],
"民生资讯": ["民生", "社会", "热点", "时事", "政策", "民生新闻"],
"汽车交通": ["汽车", "交通", "车", "新能源", "电动车", "买车"],
"知识百科": ["知识", "百科", "科普", "常识", "冷知识"],
"科技数码": ["科技", "数码", "手机", "电脑", "智能", "AI", "互联网", "软件", "硬件"],
"美容美体": ["美容", "美体", "护肤", "化妆", "美妆", "彩妆"],
"美食餐饮": ["美食", "餐饮", "做饭", "烹饪", "餐厅", "探店", "食谱"],
"职场发展": ["职场", "工作", "求职", "面试", "跳槽", "升职", "加薪", "简历"],
"财富理财": ["财富", "理财", "财经", "投资", "基金", "股票", "保险", "财务", "赚钱"],
}
# 榜单更新时间规则
DAY_UPDATE_HOUR = 17
DAY_UPDATE_MINUTE = 30
MONTH_UPDATE_DAY = 3
MONTH_UPDATE_HOUR = 23
MONTH_UPDATE_MINUTE = 0
DAY_MAX_DAYS_BACK = 7
WEEK_MAX_WEEKS_BACK = 3
MONTH_MAX_MONTHS_BACK = 3
# ===== 日期计算 =====
def _is_after_day_update(now=None):
"""判断当前时间是否已过日榜/周榜更新时间(17:30)"""
if now is None:
now = datetime.now()
cutoff = now.replace(hour=DAY_UPDATE_HOUR, minute=DAY_UPDATE_MINUTE, second=0, microsecond=0)
return now >= cutoff
def _is_after_month_update(now=None):
"""判断当前时间是否已过月榜更新时间(当月3号23:00)"""
if now is None:
now = datetime.now()
try:
cutoff = now.replace(day=MONTH_UPDATE_DAY, hour=MONTH_UPDATE_HOUR,
minute=MONTH_UPDATE_MINUTE, second=0, microsecond=0)
except ValueError:
cutoff = now.replace(day=MONTH_UPDATE_DAY, hour=MONTH_UPDATE_HOUR,
minute=MONTH_UPDATE_MINUTE, second=0, microsecond=0)
return now >= cutoff
def get_latest_query_date(rank_type="day"):
"""获取最新可查询的榜单日期"""
now = datetime.now()
if rank_type == "day":
if _is_after_day_update(now):
target = now - timedelta(days=1)
else:
target = now - timedelta(days=2)
return target.strftime("%Y-%m-%d")
elif rank_type == "week":
weekday = now.weekday()
this_monday = now - timedelta(days=weekday)
if _is_after_day_update(now):
return this_monday.strftime("%Y-%m-%d")
else:
last_monday = this_monday - timedelta(weeks=1)
return last_monday.strftime("%Y-%m-%d")
elif rank_type == "month":
# 月榜: 每月3号23:00更新上月数据
# rankDate传上月1号 = 上月数据
# 3号23:00后 → 上月1号(=上月数据已更新); 3号23:00前 → 上上月1号
if _is_after_month_update(now):
if now.month == 1:
return datetime(now.year - 1, 12, 1).strftime("%Y-%m-%d")
else:
return datetime(now.year, now.month - 1, 1).strftime("%Y-%m-%d")
else:
if now.month == 1:
return datetime(now.year - 1, 11, 1).strftime("%Y-%m-%d")
elif now.month == 2:
return datetime(now.year - 1, 12, 1).strftime("%Y-%m-%d")
else:
return datetime(now.year, now.month - 2, 1).strftime("%Y-%m-%d")
return now.strftime("%Y-%m-%d")
def get_earliest_query_date(rank_type="day"):
"""获取可查询的最早日期"""
now = datetime.now()
if rank_type == "day":
earliest = now - timedelta(days=DAY_MAX_DAYS_BACK)
return earliest.strftime("%Y-%m-%d")
elif rank_type == "week":
weekday = now.weekday()
this_monday = now - timedelta(days=weekday)
earliest_monday = this_monday - timedelta(weeks=WEEK_MAX_WEEKS_BACK)
return earliest_monday.strftime("%Y-%m-%d")
elif rank_type == "month":
m = now.month
y = now.year
m -= MONTH_MAX_MONTHS_BACK
while m <= 0:
m += 12
y -= 1
return datetime(y, m, 1).strftime("%Y-%m-%d")
return now.strftime("%Y-%m-%d")
def validate_and_adjust_date(rank_type, user_date_str):
"""验证用户指定日期是否在可查询范围"""
user_date = datetime.strptime(user_date_str, "%Y-%m-%d")
latest = datetime.strptime(get_latest_query_date(rank_type), "%Y-%m-%d")
earliest = datetime.strptime(get_earliest_query_date(rank_type), "%Y-%m-%d")
if earliest <= user_date <= latest:
return {
"original_date": user_date_str,
"adjusted_date": user_date_str,
"is_adjusted": False,
"reminder": ""
}
if user_date > latest:
adjusted = latest.strftime("%Y-%m-%d")
else:
adjusted = earliest.strftime("%Y-%m-%d")
rank_label = {"day": "日榜", "week": "周榜", "month": "月榜"}.get(rank_type, "日榜")
reminder = (
'非常抱歉🙏,目前公众号榜单最多支持回溯「近7天的日榜/近3周的周榜/近3个月的月榜」,'
'我将为您查询最接近您需求时间的{}数据⭐~'
).format(rank_label)
return {
"original_date": user_date_str,
"adjusted_date": adjusted,
"is_adjusted": True,
"reminder": reminder
}
def get_query_date(rank_type="day", user_date=None):
"""根据榜单类型和当前时间确定查询日期
Args:
rank_type: day/week/month
user_date: 用户指定日期
Returns:
(查询日期字符串, 是否自动推断, 提醒消息)
"""
if user_date:
validation = validate_and_adjust_date(rank_type, user_date)
return validation["adjusted_date"], False, validation["reminder"]
rank_date = get_latest_query_date(rank_type)
# 检查今日日榜是否已更新
reminder = ""
if rank_type == "day" and not _is_after_day_update():
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
reminder = (
'日榜数据暂未更新,将为您查询最接近您需求日期的榜单数据⭐~\n'
f'推荐查询昨日更新的最新榜单({yesterday}日榜)'
)
now = datetime.now()
if rank_type == "day":
if _is_after_day_update(now):
pass # 已过17:30,查询昨日数据
else:
pass # 未过17:30,查询前天数据
elif rank_type == "week":
if _is_after_day_update(now):
pass
else:
pass
elif rank_type == "month":
if _is_after_month_update(now):
pass
else:
pass
return rank_date, True, reminder
# ===== 分类匹配 =====
def match_category(user_input):
"""根据用户输入匹配分类"""
if not user_input:
return "人文资讯"
if user_input in CATEGORIES:
return user_input
user_lower = user_input.lower().strip()
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
if keyword.lower() in user_lower:
return category
return "人文资讯"
# ===== 认证 =====
def _get_redfox_api_key():
"""三级认证回退获取 红狐Hub API Key
优先级:
1. 环境变量 REDFOX_API_KEY
2. Shell 配置文件(.zshrc / .bashrc / PowerShell Profile)
3. 返回 None,由上层提示用户配置
Returns:
str or None: API Key
"""
# 第一级:环境变量
api_key = os.getenv("REDFOX_API_KEY")
if api_key and api_key.startswith("ak_"):
return api_key
# 第二级:Shell 配置文件
home = pathlib.Path.home()
config_files = []
if sys.platform == "win32":
# PowerShell Profile
ps_dir = home / "Documents" / "WindowsPowerShell"
if ps_dir.exists():
for f in ps_dir.glob("Microsoft.PowerShell_profile.ps1"):
config_files.append(f)
ps7_dir = home / "Documents" / "PowerShell"
if ps7_dir.exists():
for f in ps7_dir.glob("Microsoft.PowerShell_profile.ps1"):
config_files.append(f)
# Git Bash / MSYS2
for name in [".bashrc", ".bash_profile", ".profile"]:
p = home / name
if p.exists():
config_files.append(p)
else:
for name in [".zshrc", ".bashrc", ".bash_profile", ".profile"]:
p = home / name
if p.exists():
config_files.append(p)
for config in config_files:
try:
content = config.read_text(encoding="utf-8", errors="ignore")
for line in content.splitlines():
line = line.strip()
if line.startswith("export REDFOX_API_KEY=") or line.startswith("set REDFOX_API_KEY="):
parts = line.split("=", 1)
if len(parts) == 2:
key = parts[1].strip().strip('"').strip("'")
if key and key.startswith("ak_"):
return key
# 匹配 PowerShell: $env:REDFOX_API_KEY = "xxx"
if "$env:REDFOX_API_KEY" in line:
parts = line.split("=", 1)
if len(parts) == 2:
key = parts[1].strip().strip('"').strip("'")
if key and key.startswith("ak_"):
return key
except Exception:
continue
return None
# ===== HTTP请求 =====
def fetch_gzh_growth(rank_type="day", rank_date=None, category="人文资讯"):
"""获取公众号热门账号推荐榜单数据"""
if not rank_date:
rank_date = get_latest_query_date(rank_type)
params = {
"rankType": rank_type,
"rankDate": rank_date,
"category": category,
"source": SOURCE,
}
credential = _get_redfox_api_key()
if not credential:
print(
"未检测到 红狐Hub API Key。请按以下步骤配置:\n"
"1. 访问 https://redfox.hk/ 了解服务详情\n"
"2. 前往 https://redfox.hk/login 注册账号(新用户获赠免费积分)\n"
"3. 注册登录后在个人中心获取 API Key(格式 ak_xxxxxxxx)\n"
"4. 设置环境变量:\n"
" macOS/Linux: export REDFOX_API_KEY=<你的apikey>\n"
" Windows PowerShell: $env:REDFOX_API_KEY = \"<你的apikey>\"\n"
" 或告知我帮你自动配置到 Shell 配置文件中",
file=sys.stderr
)
return {
"fetch_time": _get_update_time_label(rank_type, rank_date),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_list": [],
"error": "MISSING_API_KEY"
}
headers = {
"Content-Type": "application/json",
"X-API-KEY": credential,
}
try:
response = requests.get(API_URL, params=params, headers=headers, timeout=30)
if response.status_code != 200:
return {
"fetch_time": _get_update_time_label(rank_type, rank_date),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_list": []
}
api_response = response.json()
except Exception:
return {
"fetch_time": _get_update_time_label(rank_type, rank_date),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_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": _get_update_time_label(rank_type, rank_date),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_list": []
}
# 保持接口原始顺序,禁止重新排序
# 综合评分直接采用接口返回的comprehensiveScore字段
account_list = []
for idx, item in enumerate(data):
account_list.append({
"index": idx + 1,
"rankPosition": item.get("rankPosition", idx + 1),
"accountName": item.get("accountName", ""),
"accountId": item.get("accountId", ""),
"accountAvatar": item.get("accountAvatar", ""),
"category": item.get("category", ""),
"compositeScore": round(float(item.get("comprehensiveScore", 0)), 1) if item.get("comprehensiveScore") else 0,
"totalReadCount": item.get("totalReadCount", 0),
"headlineReadCount": item.get("headlineReadCount", 0),
"maxReadCount": item.get("maxReadCount", 0),
"totalLikeCount": item.get("totalLikeCount", 0),
"totalForwardCount": item.get("totalForwardCount", 0),
"totalInSeeCount": item.get("totalInSeeCount", 0),
"publishCount": item.get("publishCount", "-"),
})
return {
"fetch_time": _get_update_time_label(rank_type, rank_date),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_list": account_list
}
def _get_data_time_range(rank_type, rank_date):
"""根据榜单类型和查询日期生成数据统计时间周期描述"""
import calendar
date_obj = datetime.strptime(rank_date, "%Y-%m-%d")
if rank_type == "day":
return f"{rank_date}"
elif rank_type == "week":
end_date = date_obj + timedelta(days=6)
return f"{date_obj.strftime('%Y-%m-%d')}至{end_date.strftime('%Y-%m-%d')}"
elif rank_type == "month":
last_day = calendar.monthrange(date_obj.year, date_obj.month)[1]
return f"{date_obj.strftime('%Y-%m')}-01至{date_obj.strftime('%Y-%m')}-{last_day:02d}"
return rank_date
def _get_update_time_label(rank_type, rank_date):
"""获取榜单更新时间标签"""
if rank_type == "day":
d = datetime.strptime(rank_date, "%Y-%m-%d")
update_time = d + timedelta(days=1)
return f"{update_time.strftime('%Y-%m-%d')} 17:30"
elif rank_type == "week":
d = datetime.strptime(rank_date, "%Y-%m-%d")
next_monday = d + timedelta(weeks=1)
return f"{next_monday.strftime('%Y-%m-%d')} 17:30"
elif rank_type == "month":
d = datetime.strptime(rank_date, "%Y-%m-%d")
if d.month == 12:
update_day = datetime(d.year + 1, 1, MONTH_UPDATE_DAY)
else:
update_day = datetime(d.year, d.month + 1, MONTH_UPDATE_DAY)
return f"{update_day.strftime('%Y-%m-%d')} 23:00"
return rank_date
# ===== 数据转换 =====
def raw_data_to_account_list(raw_data):
"""将 gzh_growth_fetcher 的 raw_data 转换为 generate_html 所需的 account_list 格式
Args:
raw_data: gzh_growth_fetcher 输出 JSON 中的 raw_data 数组
Returns:
list: generate_html 所需的 account_list 格式
"""
account_list = []
for idx, item in enumerate(raw_data):
account_list.append({
"index": idx + 1,
"rankPosition": item.get("rankPosition", idx + 1),
"accountName": item.get("accountName", ""),
"accountId": item.get("accountId", ""),
"accountAvatar": item.get("accountAvatar", ""),
"category": item.get("category", ""),
"compositeScore": item.get("compositeScore", 0),
"totalReadCount": item.get("totalReadCount", 0),
"headlineReadCount": item.get("headlineReadCount", 0),
"maxReadCount": item.get("maxReadCount", 0),
"totalLikeCount": item.get("totalLikeCount", 0),
"totalForwardCount": item.get("totalForwardCount", 0),
"totalInSeeCount": item.get("totalInSeeCount", 0),
"publishCount": item.get("publishCount", "-"),
})
return account_list
# ===== HTML生成 =====
def generate_html(result, top_n=50):
"""生成HTML页面 - 公众号热门账号推荐榜
Args:
result: 榜单数据结果
top_n: 显示条数,默认50
"""
account_list = result["account_list"]
fetch_time = result["fetch_time"]
rank_date = result.get("rank_date", "")
category = result.get("category", "人文资讯")
rank_type = result.get("rank_type", "day")
rank_type_label = {"day": "日榜", "week": "周榜", "month": "月榜"}.get(rank_type, "日榜")
time_range = result.get("time_range", _get_data_time_range(rank_type, rank_date))
top_n = min(top_n, len(account_list), 100)
account_list = account_list[:top_n]
js_data = json.dumps(account_list, ensure_ascii=False, indent=2)
page_title = f"公众号综合实力{rank_type_label} - {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: #07c160;
border: 1.5px solid #07c160;
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: #f0faf4; }}
.pdf-content {{ background: transparent; }}
.header-wrap {{
background: linear-gradient(135deg, #07c160 0%, #2dc84d 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; }}
.account-list {{
display: flex;
flex-direction: column;
gap: 10px;
}}
.account-card {{
background: #fff;
border-radius: 12px;
padding: 14px;
transition: all 0.2s ease;
border: 1px solid #f0f0f0;
}}
.account-card:hover {{
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}}
.account-card.top1 {{ border-left: 3px solid #07c160; }}
.account-card.top2 {{ border-left: 3px solid #2dc84d; }}
.account-card.top3 {{ border-left: 3px solid #7ed89a; }}
.row1 {{
display: flex;
align-items: center;
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;
}}
.account-avatar {{
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px;
flex-shrink: 0;
}}
.account-info {{
flex: 1;
min-width: 0;
}}
.account-name {{
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
}}
.account-name a {{
color: #333;
text-decoration: none;
}}
.account-name a:hover {{
color: #07c160;
}}
.account-id {{
font-size: 12px;
color: #999;
margin-top: 2px;
}}
.publish-tag {{
display: inline-block;
background: #f0faf4;
color: #07c160;
border-radius: 4px;
padding: 1px 6px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}}
.row2 {{
display: flex;
flex-wrap: wrap;
gap: 14px;
padding-left: 48px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}}
.data-item {{
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}}
.data-label {{ color: #999; }}
.data-value {{ color: #333; font-weight: 600; }}
.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; }}
.account-card {{ padding: 12px; }}
.account-name {{ font-size: 15px; }}
.rank-num {{ min-width: 32px; height: 32px; font-size: 16px; }}
.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>公众号综合实力{rank_type_label}<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;">*公众号综合实力{rank_type_label},基于阅读、点赞、转发、在看等多维数据综合排名*</div><div style="font-size:12px;color:rgba(255,255,255,0.75);">*数据统计时间周期:{time_range}*</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="maxRead">--</div>
<div class="stat-label">最高总阅读</div>
</div>
<div class="stat-item">
<div class="stat-num" id="avgRead">--</div>
<div class="stat-label">平均总阅读</div>
</div>
</div>
</div>
<div class="account-list" id="accountList"></div>
<div class="footer-note">数据来源:公众号平台 · {category} · 公众号综合实力{rank_type_label}</div>
</div>
</div>
<script>
(function() {{
function bindCardClick() {{
var cards = document.querySelectorAll('.account-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 maxRead = 0, sumRead = 0;
function fmtNum(val) {{
var n = parseInt(val) || 0;
if (n >= 100000000) return (n / 100000000).toFixed(1) + '亿';
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
return n.toString();
}}
for (var i = 0; i < RAW.length; i++) {{
var totalRead = parseInt(RAW[i].totalReadCount) || 0;
if (totalRead > maxRead) maxRead = totalRead;
sumRead += totalRead;
}}
var avgRead = totalCount > 0 ? sumRead / totalCount : 0;
document.getElementById('totalCount').textContent = totalCount;
document.getElementById('maxRead').textContent = fmtNum(maxRead);
document.getElementById('avgRead').textContent = fmtNum(Math.round(avgRead));
var html = '';
for (var i = 0; i < RAW.length && i < {top_n}; i++) {{
var d = RAW[i];
var rank = i + 1;
var cardCls = 'account-card';
if (rank === 1) cardCls += ' top1';
else if (rank === 2) cardCls += ' top2';
else if (rank === 3) cardCls += ' top3';
var searchUrl = 'https://open.weixin.qq.com/qr/code?username=' + encodeURIComponent(d.accountId || d.accountName);
var avatar = d.accountAvatar || '';
var accountName = d.accountName || '--';
var accountId = d.accountId || '';
var totalReadCount = fmtNum(d.totalReadCount);
var headlineReadCount = fmtNum(d.headlineReadCount);
var maxReadCount = fmtNum(d.maxReadCount);
var totalLikeCount = fmtNum(d.totalLikeCount);
var totalForwardCount = fmtNum(d.totalForwardCount);
var totalInSeeCount = fmtNum(d.totalInSeeCount);
var publishCount = d.publishCount || '-';
var compositeScore = d.compositeScore || 0;
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="' + searchUrl + '">'
+ '<div class="row1">'
+ rankHtml
+ (avatar ? '<img class="account-avatar export-hide" src="' + avatar + '" referrerPolicy="no-referrer" onerror="this.style.display=\\'none\\'">' : '')
+ '<div class="account-info">'
+ '<div class="account-name"><a href="' + searchUrl + '" target="_blank" onclick="event.stopPropagation()">' + accountName + '</a>'
+ '<span class="publish-tag">发布' + publishCount + '</span>'
+ '</div>'
+ '<div class="account-id">ID: ' + accountId + ' | 综合评分: <b style="color:#07c160">' + compositeScore + '</b></div>'
+ '</div>'
+ '</div>'
+ '<div class="row2">'
+ '<div class="data-item"><span class="data-label">总阅读</span><span class="data-value">' + totalReadCount + '</span></div>'
+ '<div class="data-item"><span class="data-label">头条阅读</span><span class="data-value">' + headlineReadCount + '</span></div>'
+ '<div class="data-item"><span class="data-label">最高阅读</span><span class="data-value">' + maxReadCount + '</span></div>'
+ '<div class="data-item"><span class="data-label">点赞</span><span class="data-value">' + totalLikeCount + '</span></div>'
+ '<div class="data-item"><span class="data-label">在看</span><span class="data-value">' + totalInSeeCount + '</span></div>'
+ '<div class="data-item"><span class="data-label">转发</span><span class="data-value">' + totalForwardCount + '</span></div>'
+ '</div>'
+ '</div>';
}}
document.getElementById('accountList').innerHTML = html;
}})();
function _hideAvatarsForExport() {{
var imgs = document.querySelectorAll('img.export-hide');
imgs.forEach(function(img) {{
img.setAttribute('data-original-src', img.src);
img.style.display = 'none';
}});
}}
function _restoreAvatarsAfterExport() {{
var imgs = document.querySelectorAll('img.export-hide');
imgs.forEach(function(img) {{
var origSrc = img.getAttribute('data-original-src');
if (origSrc) {{
img.src = origSrc;
img.removeAttribute('data-original-src');
}}
img.style.display = '';
}});
}}
function exportImage() {{
var btn = document.querySelector('.btn-export-img');
btn.textContent = '生成中...';
btn.style.pointerEvents = 'none';
var target = document.getElementById('pdfContent');
_hideAvatarsForExport();
html2canvas(target, {{
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#f5f5f5',
logging: false,
windowWidth: target.scrollWidth,
windowHeight: target.scrollHeight
}}).then(function(canvas) {{
_restoreAvatarsAfterExport();
var link = document.createElement('a');
link.download = 'gzh_power_account_' + new Date().toISOString().slice(0,10) + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
btn.textContent = '导出图片';
btn.style.pointerEvents = '';
}}).catch(function(err) {{
_restoreAvatarsAfterExport();
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');
_hideAvatarsForExport();
html2canvas(target, {{
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#f5f5f5',
logging: false,
windowWidth: target.scrollWidth,
windowHeight: target.scrollHeight
}}).then(function(canvas) {{
_restoreAvatarsAfterExport();
var imgData = canvas.toDataURL('image/png', 1.0);
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;
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('gzh_power_account_' + dateStr + '.pdf');
btn.textContent = '导出 PDF';
btn.style.pointerEvents = '';
}}).catch(function(err) {{
_restoreAvatarsAfterExport();
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_type', type=str, default='day', choices=['day', 'week', 'month'], help='榜单类型')
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=50, help='显示条数,默认50')
parser.add_argument('--input_json_file', type=str, default=None,
help='预取数据JSON文件路径(来自gzh_growth_fetcher输出),指定后不再调用API')
args = parser.parse_args()
if args.input_json_file:
# 使用预取数据,不调用 API
if not os.path.isfile(args.input_json_file):
print(f"错误: 文件不存在: {args.input_json_file}", file=sys.stderr)
sys.exit(1)
try:
with open(args.input_json_file, 'r', encoding='utf-8') as f:
fetched = json.load(f)
except Exception as e:
print(f"错误: 读取文件失败: {e}", file=sys.stderr)
sys.exit(1)
raw_data = fetched.get("raw_data", [])
if not raw_data:
print("错误: 输入文件中无 raw_data 数据", file=sys.stderr)
sys.exit(1)
account_list = raw_data_to_account_list(raw_data)
rank_type = fetched.get("rank_type", "day")
rank_date = fetched.get("rank_date", "")
category = fetched.get("category", "人文资讯")
rank_type_label = {"day": "日榜", "week": "周榜", "month": "月榜"}.get(rank_type, "日榜")
if args.keyword or args.category:
print("注意: 已指定 --input_json_file,忽略 --keyword/--category 参数", file=sys.stderr)
result = {
"fetch_time": fetched.get("update_time", ""),
"rank_type": rank_type,
"rank_date": rank_date,
"category": category,
"time_range": _get_data_time_range(rank_type, rank_date),
"account_list": account_list
}
print(f"使用预取数据生成HTML(不调用API)...", file=sys.stderr)
print(f"榜单类型: {rank_type_label}", file=sys.stderr)
print(f"查询日期: {rank_date}", file=sys.stderr)
print(f"分类:{category}", file=sys.stderr)
else:
# 原有逻辑:调用 API 获取数据
# 处理分类
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, reminder = get_query_date(args.rank_type, args.rank_date)
rank_type_label = {"day": "日榜", "week": "周榜", "month": "月榜"}.get(args.rank_type, "日榜")
print(f"正在获取公众号综合实力{rank_type_label}数据...", file=sys.stderr)
print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", file=sys.stderr)
if reminder:
print(f"提醒: {reminder}", file=sys.stderr)
print(f"榜单类型: {rank_type_label}", file=sys.stderr)
print(f"查询日期: {rank_date}", file=sys.stderr)
print(f"分类:{category}", file=sys.stderr)
result = fetch_gzh_growth(rank_type=args.rank_type, rank_date=rank_date, category=category)
html = generate_html(result, top_n=args.top)
if args.output:
output_path = args.output
else:
rank_short = {"day": "日", "week": "周", "month": "月"}.get(result.get("rank_type", args.rank_type), "日")
timestamp_str = datetime.now().strftime("%Y%m%d")[-8:] + f"{datetime.now().strftime('%H%M%S')}"
filename = f"公众号综合实力{rank_short}_{result.get('category', category)}_{timestamp_str}.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['account_list'])} 条{rank_type_label}数据,展示TOP{args.top}", file=sys.stderr)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""公众号热门账号推荐榜数据获取脚本
通过HTTP请求获取公众号热门账号推荐榜单数据,支持日榜/周榜/月榜,22个分类查询。
用法:
python scripts/gzh_growth_fetcher.py --rank_type day --keyword 科技 --top_n 50
python scripts/gzh_growth_fetcher.py --rank_type week --category 科技数码
python scripts/gzh_growth_fetcher.py --rank_type month --category 总排名 --top_n 50
python scripts/gzh_growth_fetcher.py --list_categories
"""
import argparse
import json
import os
import sys
import pathlib
from datetime import datetime, timedelta
from urllib.parse import quote
import requests
# ===== 常量 =====
API_URL = "https://redfox.hk/story/api/cozeSkill/getGzhCozeSkillDataIndex"
SOURCE = "公众号综合实力账号榜-GitHub"
CATEGORIES = [
"乐活生活", "人文资讯", "企业品牌", "体育娱乐", "健康养生",
"创投商业", "学术研究", "情感心理", "房产楼市", "搞笑幽默",
"教育考试", "文摘精选", "旅游出行", "时尚潮流", "民生资讯",
"汽车交通", "知识百科", "科技数码", "美容美体", "美食餐饮",
"职场发展", "财富理财", "总排名"
]
RANK_TYPE_MAP = {
"day": "日榜",
"week": "周榜",
"month": "月榜"
}
# 榜单更新时间规则
# 日榜:每日17:30更新昨日数据
# 周榜:每周一17:30更新上周数据
# 月榜:每月3号23:00更新上月数据
DAY_UPDATE_HOUR = 17
DAY_UPDATE_MINUTE = 30
MONTH_UPDATE_DAY = 3
MONTH_UPDATE_HOUR = 23
MONTH_UPDATE_MINUTE = 0
DATA_DESCRIPTION_TEMPLATE = "公众号综合实力{rank_label},基于阅读、点赞、转发、在看等多维数据综合排名\n数据统计时间周期:{time_range}"
def get_data_time_range(rank_type, rank_date):
"""根据榜单类型和查询日期生成数据统计时间周期描述"""
from datetime import datetime, timedelta
import calendar
date_obj = datetime.strptime(rank_date, "%Y-%m-%d")
if rank_type == "day":
return f"{rank_date}"
elif rank_type == "week":
end_date = date_obj + timedelta(days=6)
return f"{date_obj.strftime('%Y-%m-%d')}至{end_date.strftime('%Y-%m-%d')}"
elif rank_type == "month":
last_day = calendar.monthrange(date_obj.year, date_obj.month)[1]
return f"{date_obj.strftime('%Y-%m')}-01至{date_obj.strftime('%Y-%m')}-{last_day:02d}"
return rank_date
# 日期查询范围限制
DAY_MAX_DAYS_BACK = 7
WEEK_MAX_WEEKS_BACK = 3
MONTH_MAX_MONTHS_BACK = 3
# ===== 日期计算 =====
def _is_after_day_update(now=None):
"""判断当前时间是否已过日榜/周榜更新时间(17:30)"""
if now is None:
now = datetime.now()
cutoff = now.replace(hour=DAY_UPDATE_HOUR, minute=DAY_UPDATE_MINUTE, second=0, microsecond=0)
return now >= cutoff
def _is_after_month_update(now=None):
"""判断当前时间是否已过月榜更新时间(当月3号23:00)"""
if now is None:
now = datetime.now()
# 本月3号23:00
try:
cutoff = now.replace(day=MONTH_UPDATE_DAY, hour=MONTH_UPDATE_HOUR,
minute=MONTH_UPDATE_MINUTE, second=0, microsecond=0)
except ValueError:
# 2月没有30号等情况不会出现(3号一定存在)
cutoff = now.replace(day=MONTH_UPDATE_DAY, hour=MONTH_UPDATE_HOUR,
minute=MONTH_UPDATE_MINUTE, second=0, microsecond=0)
return now >= cutoff
def get_latest_query_date(rank_type="day"):
"""获取最新可查询的榜单日期
Args:
rank_type: day/week/month
Returns:
str: 查询日期字符串 (yyyy-MM-dd)
"""
now = datetime.now()
if rank_type == "day":
# 日榜: 每日17:30更新昨日数据
# 17:30后 → 查昨天; 17:30前 → 查前天
if _is_after_day_update(now):
target = now - timedelta(days=1)
else:
target = now - timedelta(days=2)
return target.strftime("%Y-%m-%d")
elif rank_type == "week":
# 周榜: 每周一17:30更新上周数据
# 周一17:30后 → 查本周一; 周一17:30前 → 查上周一
weekday = now.weekday() # 0=Monday
this_monday = now - timedelta(days=weekday)
if _is_after_day_update(now):
# 17:30后 → 本周一
return this_monday.strftime("%Y-%m-%d")
else:
# 17:30前 → 上周一
last_monday = this_monday - timedelta(weeks=1)
return last_monday.strftime("%Y-%m-%d")
elif rank_type == "month":
# 月榜: 每月3号23:00更新上月数据
# rankDate传上月1号 = 上月数据
# 3号23:00后 → 上月1号(=上月数据已更新); 3号23:00前 → 上上月1号(=上上月数据)
if _is_after_month_update(now):
# 已过3号23:00 → 上月1号
if now.month == 1:
return datetime(now.year - 1, 12, 1).strftime("%Y-%m-%d")
else:
return datetime(now.year, now.month - 1, 1).strftime("%Y-%m-%d")
else:
# 未过3号23:00 → 上上月1号
if now.month == 1:
return datetime(now.year - 1, 11, 1).strftime("%Y-%m-%d")
elif now.month == 2:
return datetime(now.year - 1, 12, 1).strftime("%Y-%m-%d")
else:
return datetime(now.year, now.month - 2, 1).strftime("%Y-%m-%d")
return now.strftime("%Y-%m-%d")
def get_earliest_query_date(rank_type="day"):
"""获取可查询的最早日期(限制范围)
Args:
rank_type: day/week/month
Returns:
str: 最早可查询日期字符串 (yyyy-MM-dd)
"""
now = datetime.now()
if rank_type == "day":
# 最近7天
earliest = now - timedelta(days=DAY_MAX_DAYS_BACK)
return earliest.strftime("%Y-%m-%d")
elif rank_type == "week":
# 最近3周
weekday = now.weekday()
this_monday = now - timedelta(days=weekday)
earliest_monday = this_monday - timedelta(weeks=WEEK_MAX_WEEKS_BACK)
return earliest_monday.strftime("%Y-%m-%d")
elif rank_type == "month":
# 最近3个月
m = now.month
y = now.year
m -= MONTH_MAX_MONTHS_BACK
while m <= 0:
m += 12
y -= 1
return datetime(y, m, 1).strftime("%Y-%m-%d")
return now.strftime("%Y-%m-%d")
def validate_and_adjust_date(rank_type, user_date_str):
"""验证用户指定的日期是否在可查询范围内,超出范围则自动调整
Args:
rank_type: day/week/month
user_date_str: 用户指定的日期 (yyyy-MM-dd)
Returns:
dict: {
"original_date": 原始日期,
"adjusted_date": 调整后的日期(可能不变),
"is_adjusted": 是否被调整,
"reminder": 提醒消息(无调整时为空)
}
"""
user_date = datetime.strptime(user_date_str, "%Y-%m-%d")
latest = datetime.strptime(get_latest_query_date(rank_type), "%Y-%m-%d")
earliest = datetime.strptime(get_earliest_query_date(rank_type), "%Y-%m-%d")
if earliest <= user_date <= latest:
return {
"original_date": user_date_str,
"adjusted_date": user_date_str,
"is_adjusted": False,
"reminder": ""
}
# 超出范围,自动调整到最近可查询日期
if user_date > latest:
adjusted = latest.strftime("%Y-%m-%d")
else:
adjusted = earliest.strftime("%Y-%m-%d")
reminder = (
'非常抱歉🙏,目前公众号榜单最多支持回溯「近7天的日榜/近3周的周榜/近3个月的月榜」,'
'我将为您查询最接近您需求时间的{}数据⭐~'
).format(RANK_TYPE_MAP.get(rank_type, "日榜"))
return {
"original_date": user_date_str,
"adjusted_date": adjusted,
"is_adjusted": True,
"reminder": reminder
}
def get_today_query_reminder(rank_type="day"):
"""当用户查询今日日榜但数据未更新时,生成提醒
Args:
rank_type: day/week/month
Returns:
str: 提醒消息,不需要提醒时返回空字符串
"""
if rank_type == "day" and not _is_after_day_update():
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
return (
'日榜数据暂未更新,将为您查询最接近您需求日期的榜单数据⭐~\n'
f'推荐查询昨日更新的最新榜单({yesterday}日榜)'
)
return ""
# ===== 认证 =====
def _get_redfox_api_key():
"""三级认证回退获取 红狐Hub API Key
优先级:
1. 环境变量 REDFOX_API_KEY
2. Shell 配置文件(.zshrc / .bashrc / PowerShell Profile)
3. 返回 None,由上层提示用户配置
Returns:
str or None: API Key
"""
# 第一级:环境变量
api_key = os.getenv("REDFOX_API_KEY")
if api_key and api_key.startswith("ak_"):
return api_key
# 第二级:Shell 配置文件
home = pathlib.Path.home()
config_files = []
if sys.platform == "win32":
# PowerShell Profile
ps_dir = home / "Documents" / "WindowsPowerShell"
if ps_dir.exists():
for f in ps_dir.glob("Microsoft.PowerShell_profile.ps1"):
config_files.append(f)
ps7_dir = home / "Documents" / "PowerShell"
if ps7_dir.exists():
for f in ps7_dir.glob("Microsoft.PowerShell_profile.ps1"):
config_files.append(f)
# Git Bash / MSYS2
for name in [".bashrc", ".bash_profile", ".profile"]:
p = home / name
if p.exists():
config_files.append(p)
else:
for name in [".zshrc", ".bashrc", ".bash_profile", ".profile"]:
p = home / name
if p.exists():
config_files.append(p)
for config in config_files:
try:
content = config.read_text(encoding="utf-8", errors="ignore")
for line in content.splitlines():
line = line.strip()
# 匹配 export REDFOX_API_KEY=xxx 或 set REDFOX_API_KEY=xxx
if line.startswith("export REDFOX_API_KEY=") or line.startswith("set REDFOX_API_KEY="):
parts = line.split("=", 1)
if len(parts) == 2:
key = parts[1].strip().strip('"').strip("'")
if key and key.startswith("ak_"):
return key
# 匹配 PowerShell: $env:REDFOX_API_KEY = "xxx"
if "$env:REDFOX_API_KEY" in line:
parts = line.split("=", 1)
if len(parts) == 2:
key = parts[1].strip().strip('"').strip("'")
if key and key.startswith("ak_"):
return key
except Exception:
continue
return None
def _get_api_headers():
"""获取API请求头,含X-API-KEY凭证
三级回退:环境变量 → Shell配置 → 报错提示
"""
api_key = _get_redfox_api_key()
if not api_key:
raise ValueError(
"未检测到 红狐Hub API Key。请按以下步骤配置:\n"
"1. 访问 https://redfox.hk/ 了解服务详情\n"
"2. 前往 https://redfox.hk/login 注册账号(新用户获赠免费积分)\n"
"3. 注册登录后在个人中心获取 API Key(格式 ak_xxxxxxxx)\n"
"4. 设置环境变量:\n"
" macOS/Linux: export REDFOX_API_KEY=<你的apikey>\n"
" Windows PowerShell: $env:REDFOX_API_KEY = \"<你的apikey>\"\n"
" 或告知我帮你自动配置到 Shell 配置文件中"
)
return {
"Content-Type": "application/json",
"X-API-KEY": api_key,
}
# ===== API调用 =====
def fetch_ranking_data(rank_type="day", rank_date=None, category="人文资讯"):
"""获取公众号热门账号推荐榜数据
Args:
rank_type: day/week/month
rank_date: 查询日期 (yyyy-MM-dd),None则自动计算
category: 分类名称
Returns:
dict: API返回的完整数据
"""
if rank_date is None:
rank_date = get_latest_query_date(rank_type)
params = {
"rankType": rank_type,
"rankDate": rank_date,
"category": category,
"source": SOURCE,
}
headers = _get_api_headers()
try:
response = requests.get(API_URL, params=params, headers=headers, timeout=30)
if response.status_code >= 400:
raise ValueError(f"HTTP请求失败: {response.status_code}, {response.text}")
result = response.json()
except requests.exceptions.RequestException as e:
raise ValueError(f"请求失败: {str(e)}")
if result.get("code") != 2000:
raise ValueError(f"API返回错误: code={result.get('code')}, msg={result.get('msg', '未知错误')}")
return result
# ===== 分类匹配 =====
CATEGORY_KEYWORDS = {
"总排名": ["总排名", "综合", "全部", "热门", "推荐", "随便", "总榜", "整体"],
"乐活生活": ["乐活", "生活", "日常", "生活方式", "生活日常", "好物推荐"],
"人文资讯": ["人文", "资讯", "文化", "历史", "哲学", "人文社科"],
"企业品牌": ["企业", "品牌", "公司", "商业品牌", "品牌营销"],
"体育娱乐": ["体育", "娱乐", "运动", "健身", "篮球", "足球", "综艺", "明星"],
"健康养生": ["健康", "养生", "保健", "中医", "调理", "减肥", "瘦身"],
"创投商业": ["创投", "商业", "投资", "创业", "融资", "商业模式"],
"学术研究": ["学术", "研究", "论文", "科研", "学报"],
"情感心理": ["情感", "心理", "恋爱", "婚姻", "情绪", "心理咨询"],
"房产楼市": ["房产", "楼市", "买房", "房价", "地产", "租房", "装修"],
"搞笑幽默": ["搞笑", "幽默", "段子", "吐槽", "沙雕", "表情包"],
"教育考试": ["教育", "考试", "培训", "考研", "考公", "留学", "英语", "学习"],
"文摘精选": ["文摘", "精选", "美文", "散文", "故事", "文章精选"],
"旅游出行": ["旅游", "出行", "旅行", "攻略", "景点", "酒店", "度假"],
"时尚潮流": ["时尚", "潮流", "穿搭", "服饰", "OOTD", "ootd", "搭配"],
"民生资讯": ["民生", "社会", "热点", "时事", "政策", "民生新闻"],
"汽车交通": ["汽车", "交通", "车", "新能源", "电动车", "买车"],
"知识百科": ["知识", "百科", "科普", "常识", "冷知识"],
"科技数码": ["科技", "数码", "手机", "电脑", "智能", "AI", "互联网", "软件", "硬件"],
"美容美体": ["美容", "美体", "护肤", "化妆", "美妆", "彩妆"],
"美食餐饮": ["美食", "餐饮", "做饭", "烹饪", "餐厅", "探店", "食谱"],
"职场发展": ["职场", "工作", "求职", "面试", "跳槽", "升职", "加薪", "简历"],
"财富理财": ["财富", "理财", "财经", "基金", "股票", "保险", "财务", "赚钱"],
}
def match_category(keyword):
"""根据关键词匹配分类
Args:
keyword: 搜索关键词
Returns:
str or None: 匹配到的分类名,多个匹配时返回列表
"""
# 精确匹配分类名
if keyword in CATEGORIES:
return keyword
# 子串匹配分类名
matches = [c for c in CATEGORIES if keyword in c]
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
return matches
# 关键词映射匹配
keyword_lower = keyword.lower().strip()
for category, keywords in CATEGORY_KEYWORDS.items():
for kw in keywords:
if kw.lower() in keyword_lower or keyword_lower in kw.lower():
return category
return None
# ===== 数据格式化 =====
def format_number(num):
"""格式化数字:超过1万显示x.x万,超过1亿显示x.x亿"""
try:
num = int(num)
except (ValueError, TypeError):
return str(num)
if num >= 100000000:
return f"{num / 100000000:.1f}亿"
if num >= 10000:
return f"{num / 10000:.1f}万"
return str(num)
def get_comprehensive_score(item):
"""获取综合评分(满分100),直接采用接口返回的comprehensiveScore字段
Args:
item: 单条数据
Returns:
float: 综合评分(0-100)
"""
try:
return round(float(item.get("comprehensiveScore", 0)), 1)
except (ValueError, TypeError):
return 0.0
def format_ranking_table(data_list, top_n=50, start=1):
"""格式化榜单为Markdown表格(严格遵循core_workflow.md标准模板)
表格字段: 排名 | 账号名称 | 综合评分(满分100) | 发布数/文章数 | 总阅读数 | 头条阅读数 | 最高阅读数 | 总点赞 | 总在看 | 总转发
Args:
data_list: API返回的data数组
top_n: 显示前N条
start: 起始排名(1-based),用于"查看更多"时从TOP21开始
Returns:
str: Markdown表格字符串
"""
# start为1时从第1条开始,start为21时从第21条开始
items = data_list[start-1:start-1+top_n]
header = "| 排名 | 账号名称 | 综合评分(满分100) | 发布数/文章数 | 总阅读数 | 头条阅读数 | 最高阅读数 | 总点赞 | 总在看 | 总转发 |"
separator = "|:---:|:---|---:|:---:|---:|---:|---:|---:|:---:|---:|"
rows = []
for idx, item in enumerate(items):
# 使用实际索引位置判断奖牌,避免API返回的rankPosition重复
display_rank = start + idx
name = item.get("accountName", "-")
account_id = item.get("accountId", "")
name_link = f"[{name}](https://open.weixin.qq.com/qr/code?username={quote(account_id)})"
score = get_comprehensive_score(item)
publish = item.get("publishCount", "-")
total_read = format_number(item.get("totalReadCount", 0))
headline_read = format_number(item.get("headlineReadCount", 0))
max_read = format_number(item.get("maxReadCount", 0))
like_count = format_number(item.get("totalLikeCount", 0))
in_see_count = format_number(item.get("totalInSeeCount", 0))
forward_count = format_number(item.get("totalForwardCount", 0))
rank_medals = {1: "🥇", 2: "🥈", 3: "🥉"}
if display_rank in rank_medals:
# TOP3: 账号名称加粗,综合评分/总阅读/头条阅读/最高阅读/总点赞/总转发加粗
row = f"| {rank_medals[display_rank]} | **{name_link}** | **{score}** | {publish} | **{total_read}** | **{headline_read}** | **{max_read}** | **{like_count}** | **{in_see_count}** | **{forward_count}** |"
else:
# 非TOP3: 账号名称不加粗,但综合评分/总阅读/头条阅读/最高阅读/总点赞/总转发仍需加粗
row = f"| {display_rank} | {name_link} | **{score}** | {publish} | **{total_read}** | **{headline_read}** | **{max_read}** | **{like_count}** | **{in_see_count}** | **{forward_count}** |"
rows.append(row)
return "\n".join([header, separator] + rows)
# 功能询问文案模板(一字不改,遵循core_workflow.md)
FEATURE_PROMPT = """技能还支持以下功能:
1️⃣ 支持对以上数据进行分析。其中包含**内容生态变化分析**、**热点影响力量化**、**蓝海赛道挖掘**三种分析角度,方便您了解公众号阅读数据或获取运营参考时使用。
2️⃣ 可生成公众号风格的html文件包。支持**导出图片**、**导出PDF**,方便阅读、保存和分享。
3️⃣ 支持订阅每日/周/月的公众号账号最新排名,订阅后将定时推送给您(日榜将在每日17点30更新/周榜将在每周一17:30准时更新/月榜将在每月3号23:00准时推送)。直接回复"订阅日/周/月榜"即可订阅相关榜单,或者"全部订阅"将会为您推送全部类型的榜单。
回复数字即可~"""
def format_full_output(rank_label, category, update_time, data_description,
time_range, reminder, data_list, top_n=50, start=1):
"""格式化完整的榜单输出(严格遵循core_workflow.md标准模板)
输出顺序:标题 → 数据说明 → reminder → 表格 → 功能询问文案
Args:
rank_label: 榜单标签(日榜/周榜/月榜)
category: 分类名
update_time: 更新时间
data_description: 数据说明文本
time_range: 数据统计时间周期
reminder: 日期提醒(可为空)
data_list: API返回的data数组
top_n: 显示前N条
start: 起始排名
Returns:
str: 完整的Markdown输出
"""
parts = []
# 标题(二级标题)
parts.append(f"## 公众号热门账号推荐榜{rank_label} - {category} (更新时间:{update_time})")
parts.append("")
# 数据说明(斜体,在标题后、表格前)
parts.append(f"*公众号综合实力{rank_label},基于阅读、点赞、转发、在看等多维数据综合排名*")
parts.append(f"*数据统计时间周期:{time_range}*")
parts.append("")
# reminder(如有)
if reminder:
parts.append(reminder)
parts.append("")
# 榜单表格
parts.append(format_ranking_table(data_list, top_n, start))
parts.append("")
# 功能询问文案(一字不改)
parts.append(FEATURE_PROMPT)
return "\n".join(parts)
def format_analysis(data_list, rank_type="day", top_n=3):
"""格式化综合实力分析
Args:
data_list: API返回的data数组
rank_type: day/week/month
top_n: 分析前N个公众号
Returns:
str: Markdown分析文本
"""
rank_label = RANK_TYPE_MAP.get(rank_type, "日榜")
items = data_list[:top_n]
if not items:
return "暂无数据可供分析。"
lines = [f"**{rank_label}综合实力分析**\n"]
for i, item in enumerate(items):
name = item.get("accountName", "未知")
score = get_comprehensive_score(item)
total_read = item.get("totalReadCount", 0)
headline_read = item.get("headlineReadCount", 0)
max_read = item.get("maxReadCount", 0)
like_count = item.get("totalLikeCount", 0)
forward_count = item.get("totalForwardCount", 0)
in_see_count = item.get("totalInSeeCount", 0)
publish = item.get("publishCount", "-")
lines.append(f"**第{i+1}名:{name}(综合评分:{score})**")
lines.append(f"- 风格:总阅读{format_number(total_read)},头条阅读{format_number(headline_read)},最高阅读{format_number(max_read)}")
lines.append(f"- 特征:发布{publish},获赞{format_number(like_count)}次,在看{format_number(in_see_count)}次,转发{format_number(forward_count)}次")
# 分析建议
try:
total_int = int(total_read) if total_read else 0
except (ValueError, TypeError):
total_int = 0
try:
like_int = int(like_count) if like_count else 0
except (ValueError, TypeError):
like_int = 0
interaction_rate = f"{like_int / total_int * 100:.2f}%" if total_int and total_int > 0 else "N/A"
lines.append(f"- 建议:互动率{interaction_rate},{'内容传播力强,可作为竞品研究标杆' if score >= 80 else '内容稳定产出,关注互动提升空间'}")
lines.append("")
return "\n".join(lines)
def get_update_time_label(rank_type, rank_date):
"""获取榜单更新时间标签
Args:
rank_type: day/week/month
rank_date: 查询日期
Returns:
str: 更新时间标签
"""
if rank_type == "day":
# 日榜:查询日期的次日17:30
d = datetime.strptime(rank_date, "%Y-%m-%d")
update_time = d + timedelta(days=1)
return f"{update_time.strftime('%Y-%m-%d')} 17:30"
elif rank_type == "week":
# 周榜:查询周一的下一个周一17:30
d = datetime.strptime(rank_date, "%Y-%m-%d")
next_monday = d + timedelta(weeks=1)
return f"{next_monday.strftime('%Y-%m-%d')} 17:30"
elif rank_type == "month":
# 月榜:查询月份的次月3号23:00
d = datetime.strptime(rank_date, "%Y-%m-%d")
if d.month == 12:
update_day = datetime(d.year + 1, 1, MONTH_UPDATE_DAY)
else:
update_day = datetime(d.year, d.month + 1, MONTH_UPDATE_DAY)
return f"{update_day.strftime('%Y-%m-%d')} 23:00"
return rank_date
# ===== 主函数 =====
def main():
parser = argparse.ArgumentParser(description="公众号热门账号推荐榜数据获取工具")
parser.add_argument("--rank_type", choices=["day", "week", "month"], default="day",
help="榜单类型: day=日榜, week=周榜, month=月榜 (默认: day)")
parser.add_argument("--rank_date", type=str, default=None,
help="查询日期(yyyy-MM-dd),不指定则自动计算")
parser.add_argument("--category", type=str, default=None,
help="分类名称,如'科技数码'")
parser.add_argument("--keyword", type=str, default=None,
help="分类关键词,模糊匹配分类名")
parser.add_argument("--top_n", type=int, default=50,
help="显示前N条 (默认: 50)")
parser.add_argument("--start", type=int, default=1,
help="起始排名 (默认: 1,用于查看更多时从TOP21开始)")
parser.add_argument("--list_categories", action="store_true",
help="列出所有可用分类")
parser.add_argument("--raw", action="store_true",
help="输出原始JSON数据")
parser.add_argument("--input_json_file", type=str, default=None,
help="预取数据JSON文件路径(来自本脚本之前的输出),指定后不再调用API")
args = parser.parse_args()
# 使用预取数据模式
if args.input_json_file:
if not os.path.isfile(args.input_json_file):
print(f"错误: 文件不存在: {args.input_json_file}", file=sys.stderr)
sys.exit(1)
try:
with open(args.input_json_file, 'r', encoding='utf-8') as f:
fetched = json.load(f)
except Exception as e:
print(f"错误: 读取文件失败: {e}", file=sys.stderr)
sys.exit(1)
raw_data = fetched.get("raw_data", [])
if not raw_data:
print("错误: 输入文件中无 raw_data 数据", file=sys.stderr)
sys.exit(1)
rank_type = fetched.get("rank_type", "day")
rank_date = fetched.get("rank_date", "")
category = fetched.get("category", "人文资讯")
rank_label = fetched.get("rank_label", RANK_TYPE_MAP.get(rank_type, "日榜"))
update_time = fetched.get("update_time", "")
reminder = fetched.get("reminder", "")
print(f"使用预取数据重新处理(不调用API)...", file=sys.stderr)
print(f"榜单类型: {rank_label}", file=sys.stderr)
print(f"查询日期: {rank_date}", file=sys.stderr)
print(f"分类: {category}", file=sys.stderr)
# 从预取数据重新格式化
data_list = raw_data
top_n = min(args.top_n, len(data_list))
time_range = get_data_time_range(rank_type, rank_date)
output = {
"status": "success",
"rank_type": rank_type,
"rank_label": rank_label,
"rank_date": rank_date,
"category": category,
"data_description": fetched.get("data_description", ""),
"total_count": fetched.get("total_count", len(data_list)),
"top_n": top_n,
"update_time": update_time,
"reminder": reminder,
"ranking_table": format_ranking_table(data_list, args.top_n, args.start),
"analysis": format_analysis(data_list, rank_type),
"raw_data": data_list,
"formatted_markdown": format_full_output(
rank_label, category, update_time,
fetched.get("data_description", ""), time_range,
reminder, data_list, args.top_n, args.start
),
}
# 输出JSON到stdout,确保Windows下GBK终端也能正确输出
json_str = json.dumps(output, ensure_ascii=False)
try:
sys.stdout.write(json_str + "\n")
sys.stdout.flush()
except UnicodeEncodeError:
# Windows GBK终端无法编码emoji等字符时,回退到UTF-8编码
sys.stdout.buffer.write(json_str.encode('utf-8', errors='replace') + b"\n")
sys.stdout.buffer.flush()
return
# 列出分类
if args.list_categories:
print("可用分类列表:", file=sys.stderr)
for i, cat in enumerate(CATEGORIES, 1):
print(f" {i}. {cat}", file=sys.stderr)
return
# 确定分类
category = "人文资讯"
if args.category:
category = args.category
elif args.keyword:
matched = match_category(args.keyword)
if matched is None:
print(f"未找到匹配'{args.keyword}'的分类,可用分类:{', '.join(CATEGORIES)}", file=sys.stderr)
return
elif isinstance(matched, list):
print(f"关键词'{args.keyword}'匹配到多个分类:{', '.join(matched)},请指定更精确的分类名", file=sys.stderr)
return
else:
category = matched
print(f"根据关键词【{args.keyword}】匹配到分类:【{category}】", file=sys.stderr)
# 确定查询日期
rank_label = RANK_TYPE_MAP.get(args.rank_type, "日榜")
print(f"正在获取公众号综合实力{rank_label}数据...", file=sys.stderr)
print(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", file=sys.stderr)
reminder = ""
if args.rank_date:
# 用户指定了日期,验证范围
validation = validate_and_adjust_date(args.rank_type, args.rank_date)
rank_date = validation["adjusted_date"]
if validation["is_adjusted"]:
reminder = validation["reminder"]
print(f"用户指定日期: {args.rank_date} → 调整为: {rank_date}", file=sys.stderr)
print(f"提醒: {reminder}", file=sys.stderr)
else:
# 自动计算最新可查询日期
rank_date = get_latest_query_date(args.rank_type)
# 检查今日日榜是否已更新
today_reminder = get_today_query_reminder(args.rank_type)
if today_reminder:
reminder = today_reminder
print(f"提醒: {reminder}", file=sys.stderr)
print(f"榜单类型: {rank_label}", file=sys.stderr)
print(f"查询日期: {rank_date}", file=sys.stderr)
print(f"分类: {category}", file=sys.stderr)
print("-" * 60, file=sys.stderr)
try:
result = fetch_ranking_data(
rank_type=args.rank_type,
rank_date=rank_date,
category=category,
)
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
return
data_list = result.get("data", [])
if not data_list:
print("未获取到数据", file=sys.stderr)
return
# 原始输出
if args.raw:
print(json.dumps(result, ensure_ascii=False, indent=2))
return
# 格式化输出
# 综合评分直接采用接口返回的comprehensiveScore字段
scored_data = []
for item in data_list[:args.top_n]:
item_copy = dict(item)
item_copy["compositeScore"] = get_comprehensive_score(item)
scored_data.append(item_copy)
update_time = get_update_time_label(args.rank_type, rank_date)
time_range = get_data_time_range(args.rank_type, rank_date)
output = {
"status": "success",
"rank_type": args.rank_type,
"rank_label": rank_label,
"rank_date": rank_date,
"category": category,
"data_description": DATA_DESCRIPTION_TEMPLATE.format(rank_label=rank_label, time_range=time_range),
"total_count": len(data_list),
"top_n": min(args.top_n, len(data_list)),
"update_time": update_time,
"reminder": reminder,
"ranking_table": format_ranking_table(data_list, args.top_n, args.start),
"analysis": format_analysis(data_list, args.rank_type),
"raw_data": scored_data,
"formatted_markdown": format_full_output(
rank_label, category, update_time,
DATA_DESCRIPTION_TEMPLATE.format(rank_label=rank_label, time_range=time_range),
time_range, reminder, data_list, args.top_n, args.start
),
}
# 输出JSON到stdout,确保Windows下GBK终端也能正确输出
json_str = json.dumps(output, ensure_ascii=False)
try:
sys.stdout.write(json_str + "\n")
sys.stdout.flush()
except UnicodeEncodeError:
# Windows GBK终端无法编码emoji等字符时,回退到ASCII编码
sys.stdout.buffer.write(json_str.encode('utf-8', errors='replace') + b"\n")
sys.stdout.buffer.flush()
if __name__ == "__main__":
main()