
Douyin Top Account
- 229 installs
- 316 repo stars
- Updated August 4, 2026
- redfox-data/redfox-community
Use douyin-top-account for development tasks
About
douyin-top-account: A skill for development. This provides functionality for development workflows.
- douyin-top-account
Douyin Top Account by the numbers
- 229 all-time installs (skills.sh)
- +14 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,729 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 douyin-top-accountAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 229 |
|---|---|
| repo stars | ★ 316 |
| Last updated | August 4, 2026 |
| Repository | redfox-data/redfox-community ↗ |
What it does
Use douyin-top-account for development tasks
Files
抖音热门账号推荐
1. 简介
抖音每日最具影响力账号榜单追踪分析工具 -- 基于红狐数据 API,提供抖音全品类 / 分品类账号每日 / 每周 / 每月的表现榜单查询,支持排名查询、赛道筛选、HTML 可视化报告下载与定时订阅推送。
适用对象:新手内容创作者、品牌方 / 商务投放人员、MCN 机构 / 内容团队、行业分析师 / 咨询从业者。
---
2. 功能特性
核心功能:
- 排名查询 -- 查日榜、周榜、月榜 TOP50,支持全品类和分品类
- 赛道筛选 -- 按品类筛选(美食、旅行、科技、游戏等 27 个赛道)
- 报告下载 -- 生成 HTML 可视化报告,支持导出 PDF / 截图
- 定时订阅 -- 设置自动化定时推送,支持日 / 周 / 月频率
特色亮点:
- 日榜每日 17:30 更新 / 回溯 7 天,周榜每周一 17:30 更新 / 回溯 3 周,月榜每月 2 号 9 点更新 / 回溯 3 月
- 综合评分算法:根据总粉丝数、新增粉丝增量、新增点赞 / 分享 / 评论加权计算,满分 100
- 赛道模糊匹配:支持自然语言输入(如 "美妆" 自动映射到 "化妆美容")
- 数据一致性保障:对话展示的数据条数与 HTML 报告严格一致
---
3. 一键安装
鉴权
获取 API Key
请前往 红狐hub 获取API KEY
配置 API Key
方案1: 以OpenClaw为例,将REDFOX_API_KEY添加到~/.openclaw/openclaw.json中
{ "env": { "REDFOX_API_KEY": "ak_xxxx..." } }方案2: 终端配置:export REDFOX_API_KEY="ak_xxxx..."
export REDFOX_API_KEY="ak_xxxx..."依赖安装
无需安装第三方依赖,使用 Python 标准库即可运行。
---
4. 使用指南
基础使用
查询榜单数据(默认展示 TOP 20):
python scripts/fetch_rank.py --query "用户原始问题" --limit 20 --output /path/to/data.json脚本会自动解析:
- 时间:从用户文本识别日 / 周 / 月,未明确则默认日榜
- 日期:自动计算最新可用日期(日榜 17:30 更新,周榜周一 17:30 更新,月榜每月 2 号 9 点更新)
- 赛道:支持模糊匹配(如 "美妆" 映射为 "化妆美容")
生成 HTML 报告:
python scripts/generate_report.py --data /path/to/data.json --limit 20重要:HTML 报告展示的数据条数必须与对话中展示的数量保持一致。使用 deliver_attachments 交付 HTML 文件。查看全部 50 条数据:
python scripts/fetch_rank.py --query "用户原始问题" --limit 50 --output /path/to/data_full.json
python scripts/generate_report.py --data /path/to/data_full.json --limit 50高级使用
更新时间与回溯范围:
| 榜单类型 | 更新时间 | 回溯范围 |
|---|---|---|
| 日榜 | 每日17:30 | 过去7天 |
| 周榜 | 每周一17:30 | 过去3周 |
| 月榜 | 每月2号9点 | 过去3月 |
时间边界处理(必须严格遵守):
- 查询当日及未来日期:回复「非常抱歉🙏,我们最新的是昨日/上周/上月的数据,将为您提供最接近您需求的昨日/上周/上月热榜。」
- 查询时间早于回溯日期:回复「非常抱歉🙏,目前榜单最多支持回溯「过去7天/过去3周/过去3月」,我将为您查询最接近您需求的时间范围~」
订阅服务:
用户确认订阅后,使用 automation_update 工具创建自动化任务:
- 日榜:
FREQ=DAILY;BYHOUR=19;BYMINUTE=30 - 周榜:
FREQ=WEEKLY;BYDAY=MO;BYHOUR=19;BYMINUTE=30 - 月榜:
FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=16;BYMINUTE=0
automation prompt 示例:查询抖音最新日榜(全品类),生成报告并推送给用户
常用指令速查
| 用户意图 | 典型触发词 | 对应操作 |
|---|---|---|
| 排名查询 | 抖音日榜、周榜、月榜、TOP50、最夯账号、排行榜、最新推荐 | 调用脚本查询并格式化输出 |
| 领域查询 | 美食类/旅行类/游戏类/科技类...抖音排名 | 调用脚本并按赛道筛选 |
| 报告下载 | 下载报告、导出榜单、生成报告 | 运行 generate_report.py 生成 HTML |
输出格式
Markdown 表格模板:
📊 抖音{period_label} · {category_label}
数据日期:{date}
共 {total} 个账号上榜(展示 TOP {displayed} 条)
💡 榜单说明:{warning}{period_update_time},与实时数据存在差异
📐 综合评分:根据总粉丝数、新增粉丝增量、新增点赞/分享/评论加权计算,满分100
| 排名 | 账号名 | 综合评分 | 总粉丝数 | 新增粉丝 | 新增点赞 | 新增评论 | 新增分享 |
|:---:|--------|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
| 🥇 1 | [账号A](https://www.douyin.com/user/xxx) | 96 | 254.06w | 6919 | 24.64w | 6.68w | 13.39w |
| 🥈 2 | [账号B](https://www.douyin.com/user/xxx) | 89 | 113.55w | 1.91w | 7.90w | 4787 | 3.12w |
| 🥉 3 | [账号C](https://www.douyin.com/user/xxx) | 87 | 439.14w | 5859 | 16.51w | 993 | 687 |输出完榜单后追加:
⚡ 更多操作
⏺️ 本次榜单完整共 50 条数据,是否需要查看剩余 30 条?
📬 订阅服务
1️⃣ 是否需要订阅每日/周/月的抖音账号最新排名?
2️⃣ 是否需要订阅具体赛道的账号表现?我们支持:个人才艺、生活vlog、财富理财、二次元、居家装修、学习教育、小剧场、数码科技、旅行、美食、化妆美容、动物、亲子、汽车、情感、三农、健康医学、潮流风尚、舞蹈才艺、颜值造型、人文、音乐、影视、身体锻炼、体育、明星娱乐、游戏---
5. 使用场景
| 用户分层 | 核心痛点 | 使用方式 |
|---|---|---|
| 新手内容创作者 / 个体运营 | 不知道哪些账号做得好、无从借鉴、不知道自己水平在赛道内所处位置 | 查询 "美食类账号 TOP50",获取对标参考 |
| 品牌方 / 商务投放人员 | 找达人效率低、数据维度单一、不了解账号历史表现 | 查询 "上周抖音美妆 TOP50",获取投放候选 |
| MCN 机构 / 内容团队 | 竞品账号情报获取难、无法快速掌握市场动态 | 查询 "最近一周游戏账号涨粉最快",监控市场 |
| 行业分析师 / 咨询从业者 | 数据获取门槛高、缺乏可视化报告工具 | 生成 "3 月份抖音游戏类榜单分析报告" |
预期收益:快速获取赛道动态、识别优质账号、降低投放决策成本、提升内容运营效率。
---
6. 项目架构
目录结构
douyin-top-account/
├── SKILL.md # 本文件
├── scripts/
│ ├── fetch_rank.py # 查询抖音榜单数据
│ └── generate_report.py # 生成 HTML 可视化报告
└── references/
├── category_map.md # 赛道映射表与匹配规则
├── update_rules.md # 更新规则与日期计算逻辑
├── score_rules.md # 评分算法说明
└── api_docs.md # API 接口详细文档(技术参考)技术栈
| 技术 | 用途 |
|---|---|
| Python 标准库 | 数据查询与报告生成 |
| 红狐 API | 抖音榜单数据来源 |
| HTML / CSS | 可视化报告渲染 |
核心模块说明
- fetch_rank.py:接收用户查询参数,调用红狐 API 获取榜单数据,自动解析时间、日期和赛道,输出 JSON 数据文件。
- generate_report.py:读取 fetch_rank 输出的 JSON 数据,生成带排名表格、统计卡片、排名算法的 HTML 可视化报告;账号名均可点击跳转抖音主页。
资源索引
| 文件 | 用途 |
|---|---|
| scripts/fetch_rank.py | 查询抖音榜单数据 |
| scripts/generate_report.py | 生成 HTML 可视化报告 |
| references/category_map.md | 赛道映射表与匹配规则 |
| references/update_rules.md | 更新规则与日期计算逻辑 |
| references/score_rules.md | 评分算法说明 |
| references/api_docs.md | API 接口详细文档(技术参考) |
---
7. 常见问答
安装
Q: 需要安装什么依赖? A: 无需安装第三方依赖,使用 Python 标准库即可运行。
Q: 如何配置 API Key? A: 请前往 红狐hub 获取 API KEY,然后通过环境变量 REDFOX_API_KEY 配置。
使用
Q: 日榜数据什么时候更新? A: 每日 17:30 更新昨日数据。当前时间未到 17:30 时,脚本会自动取前天数据。
Q: 支持哪些赛道? A: 支持 27 个赛道,完整列表见 references/category_map.md。
Q: 数据回溯范围? A: 日榜可查过去 7 天,周榜可查过去 3 周,月榜可查过去 3 个月。
Q: 赛道识别失败怎么办? A: 无法识别时,请列出完整赛道列表供用户选择。
Q: 默认展示多少条数据? A: 日榜默认展示 TOP 20,完整 50 条通过报告或追问查看。
故障排除
Q: HTML 报告生成失败? A: 确保 workspace 有写权限,且 JSON 数据文件路径正确。
Q: 数据不一致? A: 对话展示多少条,HTML 报告就展示多少条,两者必须一致。如果用户要求查看全部 50 条,需要重新生成完整报告。
---
{
"category_map": {
"全部": "全部",
"个人才艺": "个人才艺",
"生活vlog": "生活vlog",
"生活VLOG": "生活vlog",
"财富理财": "财富理财",
"二次元": "二次元",
"居家装修": "居家装修",
"学习教育": "学习教育",
"小剧场": "小剧场",
"数码科技": "数码科技",
"旅行": "旅行",
"美食": "美食",
"化妆美容": "化妆美容",
"动物": "动物",
"亲子": "亲子",
"汽车": "汽车",
"情感": "情感",
"三农": "三农",
"健康医学": "健康医学",
"潮流风尚": "潮流风尚",
"舞蹈才艺": "舞蹈才艺",
"颜值造型": "颜值造型",
"人文": "人文",
"音乐": "音乐",
"影视": "影视",
"身体锻炼": "身体锻炼",
"体育": "体育",
"明星娱乐": "明星娱乐",
"游戏": "游戏",
"美妆护肤": "化妆美容",
"彩妆护肤": "化妆美容",
"美妆博主": "化妆美容",
"护肤美妆": "化妆美容",
"化妆品": "化妆美容",
"美妆": "化妆美容",
"彩妆": "化妆美容",
"护肤": "化妆美容",
"美容": "化妆美容",
"口红": "化妆美容",
"粉底": "化妆美容",
"眼妆": "化妆美容",
"美白": "化妆美容",
"防晒": "化妆美容",
"抗老": "化妆美容",
"素颜": "化妆美容",
"科技数码": "数码科技",
"数码测评": "数码科技",
"手机评测": "数码科技",
"电脑配件": "数码科技",
"人工智能": "数码科技",
"科技": "数码科技",
"数码": "数码科技",
"手机": "数码科技",
"电脑": "数码科技",
"平板": "数码科技",
"耳机": "数码科技",
"相机": "数码科技",
"智能硬件": "数码科技",
"智能家居": "数码科技",
"AI": "数码科技",
"ai": "数码科技",
"互联网": "数码科技",
"程序员": "数码科技",
"软件": "数码科技",
"硬件": "数码科技",
"极客": "数码科技",
"健康养生": "健康医学",
"中医养生": "健康医学",
"健康医疗": "健康医学",
"健康": "健康医学",
"养生": "健康医学",
"医疗": "健康医学",
"医生": "健康医学",
"医院": "健康医学",
"中医": "健康医学",
"减肥": "健康医学",
"营养": "健康医学",
"食疗": "健康医学",
"保健": "健康医学",
"心理": "健康医学",
"睡眠": "健康医学",
"美食探店": "美食",
"探店博主": "美食",
"吃播": "美食",
"烹饪": "美食",
"探店": "美食",
"下厨": "美食",
"厨艺": "美食",
"菜谱": "美食",
"甜品": "美食",
"烘焙": "美食",
"火锅": "美食",
"小吃": "美食",
"料理": "美食",
"食材": "美食",
"厨房": "美食",
"餐厅": "美食",
"网红餐": "美食",
"旅行vlog": "旅行",
"旅游博主": "旅行",
"旅游攻略": "旅行",
"旅游": "旅行",
"出行": "旅行",
"打卡": "旅行",
"景区": "旅行",
"景点": "旅行",
"民宿": "旅行",
"户外探险": "旅行",
"露营": "旅行",
"徒步": "旅行",
"背包客": "旅行",
"出国": "旅行",
"签证": "旅行",
"时尚穿搭": "潮流风尚",
"穿搭博主": "潮流风尚",
"街头时尚": "潮流风尚",
"穿搭": "潮流风尚",
"时尚": "潮流风尚",
"搭配": "潮流风尚",
"ootd": "潮流风尚",
"OOTD": "潮流风尚",
"潮牌": "潮流风尚",
"球鞋": "潮流风尚",
"sneaker": "潮流风尚",
"潮流": "潮流风尚",
"国潮": "潮流风尚",
"汉服": "潮流风尚",
"衣服": "潮流风尚",
"服装": "潮流风尚",
"品牌": "潮流风尚",
"奢侈品": "潮流风尚",
"包包": "潮流风尚",
"亲子育儿": "亲子",
"母婴育儿": "亲子",
"育儿经验": "亲子",
"育儿": "亲子",
"母婴": "亲子",
"宝宝": "亲子",
"婴儿": "亲子",
"儿童": "亲子",
"孕妈": "亲子",
"妈妈": "亲子",
"奶爸": "亲子",
"辅食": "亲子",
"胎教": "亲子",
"早教": "亲子",
"知识教育": "学习教育",
"在线教育": "学习教育",
"考研": "学习教育",
"高考": "学习教育",
"英语": "学习教育",
"数学": "学习教育",
"语文": "学习教育",
"教育": "学习教育",
"学习": "学习教育",
"读书": "学习教育",
"备考": "学习教育",
"刷题": "学习教育",
"知识": "学习教育",
"学科": "学习教育",
"老师": "学习教育",
"课程": "学习教育",
"讲解": "学习教育",
"考试": "学习教育",
"技能": "学习教育",
"宠物猫狗": "动物",
"猫咪": "动物",
"宠物": "动物",
"猫": "动物",
"狗": "动物",
"dog": "动物",
"cat": "动物",
"狗狗": "动物",
"萌宠": "动物",
"鸟": "动物",
"鱼": "动物",
"兔子": "动物",
"仓鼠": "动物",
"野生动物": "动物",
"健身运动": "身体锻炼",
"减脂塑形": "身体锻炼",
"健身打卡": "身体锻炼",
"健身": "身体锻炼",
"运动": "身体锻炼",
"瑜伽": "身体锻炼",
"锻炼": "身体锻炼",
"跑步": "身体锻炼",
"增肌": "身体锻炼",
"减脂": "身体锻炼",
"塑形": "身体锻炼",
"马拉松": "身体锻炼",
"游泳": "身体锻炼",
"拳击": "身体锻炼",
"格斗": "身体锻炼",
"器械": "身体锻炼",
"撸铁": "身体锻炼",
"健美": "身体锻炼",
"家居装修": "居家装修",
"室内设计": "居家装修",
"装修改造": "居家装修",
"房屋装修": "居家装修",
"装修": "居家装修",
"家居": "居家装修",
"家装": "居家装修",
"软装": "居家装修",
"硬装": "居家装修",
"整理收纳": "居家装修",
"收纳": "居家装修",
"设计师": "居家装修",
"北欧风": "居家装修",
"极简风": "居家装修",
"新中式": "居家装修",
"二手改造": "居家装修",
"影视解说": "影视",
"电影解说": "影视",
"剧集推荐": "影视",
"电影": "影视",
"剧集": "影视",
"电视剧": "影视",
"短剧": "影视",
"综艺": "影视",
"动画片": "影视",
"纪录片": "影视",
"电影推荐": "影视",
"追剧": "影视",
"影评": "影视",
"剧评": "影视",
"番剧": "影视",
"音乐创作": "音乐",
"原创音乐": "音乐",
"翻唱": "音乐",
"唱歌": "音乐",
"弹琴": "音乐",
"弹吉他": "音乐",
"吉他": "音乐",
"钢琴": "音乐",
"古琴": "音乐",
"编曲": "音乐",
"说唱": "音乐",
"rap": "音乐",
"RAP": "音乐",
"歌手": "音乐",
"乐器": "音乐",
"DJ": "音乐",
"dj": "音乐",
"电音": "音乐",
"民谣": "音乐",
"古风": "音乐",
"舞蹈教学": "舞蹈才艺",
"街舞": "舞蹈才艺",
"舞蹈": "舞蹈才艺",
"跳舞": "舞蹈才艺",
"古典舞": "舞蹈才艺",
"民族舞": "舞蹈才艺",
"爵士舞": "舞蹈才艺",
"芭蕾": "舞蹈才艺",
"breaking": "舞蹈才艺",
"hiphop": "舞蹈才艺",
"HIPHOP": "舞蹈才艺",
"编舞": "舞蹈才艺",
"明星八卦": "明星娱乐",
"娱乐八卦": "明星娱乐",
"明星": "明星娱乐",
"娱乐": "明星娱乐",
"偶像": "明星娱乐",
"idol": "明星娱乐",
"追星": "明星娱乐",
"八卦": "明星娱乐",
"艺人": "明星娱乐",
"网红": "明星娱乐",
"名人": "明星娱乐",
"游戏直播": "游戏",
"游戏解说": "游戏",
"手游攻略": "游戏",
"网游": "游戏",
"手游": "游戏",
"电竞": "游戏",
"主机游戏": "游戏",
"电子游戏": "游戏",
"主播": "游戏",
"LOL": "游戏",
"lol": "游戏",
"王者荣耀": "游戏",
"和平精英": "游戏",
"原神": "游戏",
"MC": "游戏",
"我的世界": "游戏",
"英雄联盟": "游戏",
"PUBG": "游戏",
"使命召唤": "游戏",
"战双": "游戏",
"鸣潮": "游戏",
"steam": "游戏",
"Steam": "游戏",
"投资理财": "财富理财",
"股票基金": "财富理财",
"理财": "财富理财",
"财经": "财富理财",
"股票": "财富理财",
"基金": "财富理财",
"炒股": "财富理财",
"期货": "财富理财",
"加密货币": "财富理财",
"比特币": "财富理财",
"投资": "财富理财",
"创业": "财富理财",
"副业": "财富理财",
"二次元动漫": "二次元",
"动漫": "二次元",
"漫画": "二次元",
"ACG": "二次元",
"acg": "二次元",
"anime": "二次元",
"cosplay": "二次元",
"cos": "二次元",
"COS": "二次元",
"Cosplay": "二次元",
"鬼灭": "二次元",
"海贼王": "二次元",
"火影": "二次元",
"进击的巨人": "二次元",
"漫威": "二次元",
"抱枕": "二次元",
"生活日常": "生活vlog",
"日常vlog": "生活vlog",
"vlog博主": "生活vlog",
"vlog": "生活vlog",
"VLOG": "生活vlog",
"Vlog": "生活vlog",
"日常": "生活vlog",
"生活记录": "生活vlog",
"生活分享": "生活vlog",
"城市生活": "生活vlog",
"人文历史": "人文",
"历史文化": "人文",
"历史": "人文",
"文化": "人文",
"文物": "人文",
"传统文化": "人文",
"国学": "人文",
"地理": "人文",
"人文地理": "人文",
"纪实": "人文",
"社会": "人文",
"汽车测评": "汽车",
"买车选车": "汽车",
"新能源车": "汽车",
"新能源": "汽车",
"驾车": "汽车",
"车评": "汽车",
"摩托车": "汽车",
"越野": "汽车",
"车": "汽车",
"三农生活": "三农",
"农村生活": "三农",
"乡村": "三农",
"农村": "三农",
"农业": "三农",
"种地": "三农",
"农民": "三农",
"土地": "三农",
"渔村": "三农",
"颜值博主": "颜值造型",
"发型": "颜值造型",
"造型": "颜值造型",
"颜值": "颜值造型",
"发色": "颜值造型",
"美发": "颜值造型",
"变美": "颜值造型",
"整形": "颜值造型",
"修容": "颜值造型",
"才艺展示": "个人才艺",
"才艺": "个人才艺",
"魔术": "个人才艺",
"杂技": "个人才艺",
"变装": "个人才艺",
"脱口秀": "个人才艺",
"相声": "个人才艺",
"小品": "个人才艺",
"情感博主": "情感",
"恋爱": "情感",
"婚姻": "情感",
"失恋": "情感",
"两性": "情感",
"情侣": "情感",
"家庭关系": "情感",
"相亲": "情感",
"单身": "情感",
"情景剧": "小剧场",
"微剧": "小剧场",
"短视频剧": "小剧场",
"剧情": "小剧场",
"搞笑视频": "小剧场",
"幽默": "小剧场",
"搞笑": "小剧场",
"喜剧": "小剧场",
"反转": "小剧场",
"体育赛事": "体育",
"篮球": "体育",
"足球": "体育",
"乒乓球": "体育",
"羽毛球": "体育",
"网球": "体育",
"排球": "体育",
"冰雪运动": "体育",
"滑雪": "体育",
"滑板": "体育",
"电竞体育": "体育",
"NBA": "体育",
"CBA": "体育",
"世界杯": "体育",
"奥运": "体育",
"综合": "全部",
"全品类": "全部",
"所有": "全部",
"全": "全部"
},
"exact_categories": [
"汽车",
"数码科技",
"生活vlog",
"颜值造型",
"小剧场",
"体育",
"学习教育",
"潮流风尚",
"人文",
"音乐",
"亲子",
"情感",
"旅行",
"个人才艺",
"舞蹈才艺",
"二次元",
"动物",
"健康医学",
"游戏",
"居家装修",
"化妆美容",
"明星娱乐",
"美食",
"影视",
"三农",
"身体锻炼",
"全部",
"财富理财"
],
"cat_display": {
"全部": "全品类",
"化妆美容": "美妆类",
"美食": "美食类",
"旅行": "旅行类",
"数码科技": "科技类",
"游戏": "游戏类",
"健康医学": "健康类",
"亲子": "亲子类",
"身体锻炼": "运动类",
"学习教育": "教育类",
"动物": "动物类",
"潮流风尚": "时尚类",
"居家装修": "家居类",
"影视": "影视类",
"音乐": "音乐类",
"舞蹈才艺": "舞蹈类",
"明星娱乐": "娱乐类",
"体育": "体育类",
"情感": "情感类",
"财富理财": "理财类",
"二次元": "二次元类",
"小剧场": "小剧场类",
"汽车": "汽车类",
"三农": "三农类",
"人文": "人文类",
"颜值造型": "颜值类",
"个人才艺": "才艺类",
"生活vlog": "生活类"
}
}{
"period_map": {
"day": "days",
"week": "weeks",
"month": "months"
},
"period_labels": {
"day": "日榜",
"week": "周榜",
"month": "月榜"
},
"period_keys": {
"days": "day",
"weeks": "week",
"months": "month"
},
"short_aliases": {
"day": "日",
"week": "周",
"month": "月"
},
"long_aliases": {
"day": [
"day",
"daily",
"日榜",
"日排名",
"日间",
"今日",
"今天",
"昨日"
],
"week": [
"week",
"weekly",
"周榜",
"周排名",
"周间",
"上周",
"本周",
"这周",
"本周期"
],
"month": [
"month",
"monthly",
"月榜",
"月排名",
"月间",
"上月",
"本月",
"这个月"
]
},
"update_rules": {
"day": {
"label": "日榜",
"update_time": "每日17:30",
"window_days": 9
},
"week": {
"label": "周榜",
"update_time": "每周一17:30",
"window_days": 21
},
"month": {
"label": "月榜",
"update_time": "每月2号9点",
"window_days": 90
}
}
}Douyin Top Account Rankings / douyin-top-account
---
Introduction
Quickly discover the most influential accounts on Douyin and stay on top of niche trends.
Core Value
Based on daily TOP 50 account data across all Douyin niches — follower growth, engagement, and growth trends — this skill helps you with topic decisions, benchmark accounts, competitor monitoring, and niche selection.
Who It's For
- 🎬 Content creators — see how top accounts perform in your niche and find benchmarks to learn from
- 🏢 Brands / ad buyers — efficiently screen quality creators and evaluate partnership value
- 📦 MCN agencies / content teams — track competitor dynamics and shape operations strategy
- 📊 Industry analysts — get data insights and generate analysis reports
---
Features
Core Capabilities
- Ranking queries: Daily, weekly, and monthly TOP 50 account rankings; TOP 20 shown by default
- Niche filtering: Query across all hot niches on Douyin or drill into any of 27 track categories with fuzzy keyword matching
- Composite score: Weighted ranking from total followers, new followers, and new likes/shares/comments — objective and credible, scored out of 100
- HTML report generation: Visual HTML reports with clickable account names linking to Douyin profiles; export as image or PDF
- Subscription push: Schedule daily, weekly, or monthly ranking updates, or subscribe by specific niche
---
API Key Acquisition & Security
- This skill requires the environment variable:
REDFOX_API_KEY. REDFOX_API_KEYis provided by RedFoxHub (https://redfox.hk).- Register at RedFoxHub to obtain your
REDFOX_API_KEY. - Configure
REDFOX_API_KEYas a device environment variable before using this skill. - Before providing your key, confirm its source, available scope, validity period, and whether reset/revocation is supported.
- Do not hard-code or expose the key in plaintext in code, prompts, logs, or output files.
---
Usage Guide
Describe what you need in plain language — no commands to memorize.
Quick phrase reference
| Intent | Example phrase | What you get |
|---|---|---|
| Daily ranking | "What's the latest Douyin daily account ranking?" | Latest all-niche daily TOP 20 |
| Weekly niche ranking | "How did food accounts rank on Douyin last week?" | Latest food-niche weekly TOP 20 |
| Monthly ranking | "Show me last month's gaming account ranking on Douyin" | Latest gaming-niche monthly TOP 20 |
| Full ranking | "I want to see all 50 entries" | Full TOP 50 with report |
| Download report | "Generate an HTML report for this ranking" | Visual HTML file delivered |
| Subscribe | "Send me daily food-niche Douyin ranking updates" | Scheduled push task created |
Sample output
📊 Douyin Daily Ranking · All niches
Data date: 2025-05-19 50 accounts on the list (showing TOP 20)
💡 Ranking note: Data updates daily at 17:30 with yesterday's figures; may differ from real-time data.
📐 Composite score: Weighted from total followers, new followers, and new likes/shares/comments; max 100:
| Rank | Account (clickable) | Score | Total followers | New followers | New likes | New comments | New shares |
|---|---|---|---|---|---|---|---|
| 🥇 1 | Account name | 96 | 2.54M | 6,919 | 246K | 67K | 134K |
| 🥈 2 | Account name | 89 | 1.13M | 19.1K | 79K | 4,787 | 31.2K |
| … | … | … | … | … | … | … | … |
⚡ More actions ⏺️ This ranking has 50 entries in total. View the remaining 30?
📬 Subscription 1️⃣ Subscribe to daily/weekly/monthly Douyin account ranking updates? 2️⃣ Subscribe to a specific niche?
---
Use Cases
| Scenario | Role | Example question | Benefit |
|---|---|---|---|
| Find niche benchmark accounts | Creator / solo operator | "Which food accounts are doing well? Show me the daily ranking" | Quickly spot category leaders and clarify direction |
| Evaluate creator partnerships | Brand / ad buyer | "Who stood out in last week's Douyin beauty TOP 50?" | Batch screening for faster partnership decisions |
| Monitor competitor niches | MCN / content team | "Which gaming accounts gained followers fastest this week?" | Track competitive landscape and respond quickly |
| Generate industry reports | Analyst / consultant | "Generate a March Douyin gaming ranking report for me" | One-click visual HTML for sharing and presentations |
| Track ranking changes regularly | Operations team | "Send me the latest travel weekly ranking every Monday" | Automated ranking intelligence via subscription |
---
Important data notes
Update schedule and lookback
| Ranking type | Update time | Lookback |
|---|---|---|
| Daily | Daily 17:30 — yesterday's data | Past 7 days |
| Weekly | Monday 17:30 — last week's data | Past 3 weeks |
| Monthly | 2nd of month 9:00 — last month's data | Past 3 months |
Supported niches (27)
Personal talent, lifestyle vlog, wealth & finance, anime/2D, home decor, education, short drama, tech, travel, food, beauty, pets, parenting, automotive, relationships, agriculture, health & medicine, fashion, dance, looks & styling, humanities, music, film & TV, fitness, sports, celebrity entertainment, gaming
Data freshness
Data is not real-time. Engagement figures reflect the collection moment and may differ from live platform data.
---
抖音热门账号推荐 / douyin-top-account
---
简介
帮你快速发现抖音平台最具影响力的账号,掌握赛道动态。
核心价值 基于每日抖音全赛道 TOP50 账号的涨粉、互动、增长趋势数据,帮你解决选题决策、对标账号、竞品监控、赛道选择四大核心问题。
适用对象
- 🎬 内容创作者 — 了解同赛道优秀账号表现,寻找对标和学习对象
- 🏢 品牌方/商务投放 — 高效筛选优质达人,评估合作价值
- 📦 MCN 机构/内容团队 — 掌握竞品动态,制定运营策略
- 📊 行业分析师 — 获取数据洞察,生成分析报告
---
功能特性
核心功能
- 排名查询:查询抖音账号日榜、周榜、月榜 TOP 50 账号,默认展示 TOP 20
- 赛道筛选:支持抖音全站热门、或者按 27 个赛道品类的精准查询,支持模糊词匹配
- 综合评分:根据总粉丝数、新增粉丝增量、新增点赞/分享/评论加权计算,多维指标排名客观可信,满分 100 分
- HTML 报告生成:生成带可视化布局的 HTML 文件,账号名可点击跳转抖音主页,支持到处图片或 PDF
- 订阅推送:支持设置日榜、周榜、月榜定时自动推送,也可按指定赛道订阅
---
密钥获取与安全说明
- 本技能需要使用环境变量:
REDFOX_API_KEY。 REDFOX_API_KEY由 红狐 hub (https://redfox.hk)提供。- 请前往 红狐 hub 注册账号,获取
REDFOX_API_KEY。 - 配置设备环境变量
REDFOX_API_KEY后使用本技能。 - 在提供密钥前,请先确认密钥来源、可用范围、有效期及是否支持重置/撤销。
- 禁止在代码、提示词、日志或输出文件中硬编码/明文暴露密钥。
---
使用指南
直接用自然语言描述你的需求,无需记忆任何命令格式。
常用说法速查
| 意图 | 示例话术 | 效果 |
|---|---|---|
| 查日榜 | "最新抖音账号日榜排名?" | 返回全品类最新日榜 TOP 20 |
| 查赛道周榜 | "上周抖音美食类账号排行怎么样?" | 返回美食赛道最新周榜 TOP 20 |
| 查月榜 | "帮我看看上个月游戏类抖音账号的榜单" | 返回游戏赛道最新月榜 TOP 20 |
| 查完整榜单 | "我想看完整的 50 条" | 展示完整 TOP 50 并生成报告 |
| 下载报告 | "帮我生成这次榜单的 HTML 报告" | 生成可视化 HTML 文件并交付 |
| 订阅推送 | "我想每天收到美食类抖音日榜更新" | 创建定时自动推送任务 |
输出示例
📊 抖音日榜 · 全品类
数据日期:2025-05-19 共 50 个账号上榜(展示 TOP 20 条)
💡 榜单说明:数据于每日 17:30 更新昨日数据,与实时数据存在差异
📐 综合评分:根据总粉丝数、新增粉丝增量、新增点赞/分享/评论加权计算,满分 100:
| 排名 | 账号名 | 综合评分 | 总粉丝数 | 新增粉丝 | 新增点赞 | 新增评论 | 新增分享 |
|---|---|---|---|---|---|---|---|
| 🥇 1 | 账号名(可点击) | 96 | 254w | 6919 | 24.6w | 6.7w | 13.4w |
| 🥈 2 | 账号名(可点击) | 89 | 113w | 1.9w | 7.9w | 4787 | 3.1w |
| … | … | … | … | … | … | … | … |
⚡ 更多操作 ⏺️ 本次榜单完整共 50 条数据,是否需要查看剩余 30 条?
📬 订阅服务 1️⃣ 是否需要订阅每日/周/月的抖音账号最新排名? 2️⃣ 是否需要订阅具体赛道的账号表现?
---
使用场景
| 场景 | 角色 | 示例问法 | 收益 |
|---|---|---|---|
| 寻找赛道标杆账号 | 内容创作者 / 个体运营 | "美食类账号哪些做得好?帮我看看最新日榜" | 快速找到行业头部,明确内容方向 |
| 评估达人合作候选人 | 品牌方 / 商务投放 | "上周抖音美妆类 TOP50 有哪些账号表现亮眼?" | 批量筛选,提升投放决策效率 |
| 监控竞品赛道动态 | MCN 机构 / 内容团队 | "最近一周游戏账号有哪些涨粉最快?" | 实时掌握竞争格局,快速响应市场变化 |
| 生成行业分析报告 | 行业分析师 / 咨询顾问 | "帮我生成 3 月份抖音游戏类榜单分析报告" | 一键生成可视化 HTML,方便汇报分享 |
| 定期追踪账号排名变化 | 运营团队 | "我想每周一收到旅行赛道的最新周榜" | 订阅推送,自动化榜单情报获取 |
---
重要数据说明
榜单更新时间与数据回溯
| 榜单类型 | 更新时间 | 可回溯范围 |
|---|---|---|
| 日榜 | 每日 17:30 更新昨日数据 | 过去 7 天 |
| 周榜 | 每周一 17:30 更新上周数据 | 过去 3 周 |
| 月榜 | 每月 2 号 9:00 更新上月数据 | 过去 3 月 |
支持赛道(27 个)
个人才艺、生活 vlog、财富理财、二次元、居家装修、学习教育、小剧场、数码科技、旅行、美食、化妆美容、动物、亲子、汽车、情感、三农、健康医学、潮流风尚、舞蹈才艺、颜值造型、人文、音乐、影视、身体锻炼、体育、明星娱乐、游戏
数据时效
数据非实时更新,账号互动数据为采集时刻的真实数据,与实时数据存在差异
---
抖音榜单 API 接口文档
接口基础信息
| 字段 | 值 |
|---|---|
| 接口地址 | https://redfox.hk/story/api/dyData/query |
| 请求方式 | POST |
| Content-Type | application/json |
| 认证方式 | X-API-KEY(从环境变量 REDFOX_API_KEY 获取,格式 ak_xxxxxxxx) |
---
请求参数
{
"dateType": "days", // 榜单周期:days=日榜 weeks=周榜 months=月榜(必填)
"rankDate": "2026-04-27", // 榜单开始日期(结束时间由服务器自行计算)(必填)
"type": "美食", // 赛道分类,默认"全部"(必填)
"source": "抖音每日最具影响力账号" // 固定值(必填)
}⚠️ 重要:source必须为"抖音每日最具影响力账号",使用其他值会导致接口返回空数据!
---
响应结构
{
"code": 2000,
"data": [
{
"accountLink": "https://www.douyin.com/user/...",
"accountName": "账号名称",
"accountRanking": 1,
"category": "美食",
"fansCount": "254.06w",
"fansGrowth": "6919",
"likedGrowth": "24.64w",
"commentsGrowth": "6.68w",
"collectedGrowth":"2.53w",
"sharedGrowth": "13.39w",
"rankDate": "2026-04-27",
"rankPeriod": "日榜"
}
]
}---
错误码
| code | 含义 |
|---|---|
| 2000 | 成功(注意:成功码是 2000 不是 0!) |
| 其他 | 错误,data 为空数组 |
---
字段映射(fetch_rank.py normalized JSON)
| 接口原始字段 | normalized 字段 | 含义 |
|---|---|---|
accountRanking | rank | 排名 |
accountName | accountName | 账号名 |
category | category | 赛道 |
fansCount | followers | 总粉丝数 |
fansGrowth | newFans | 新增粉丝 |
likedGrowth | newLikes | 新增点赞 |
commentsGrowth | newComments | 新增评论 |
sharedGrowth | newShares | 新增分享 |
accountLink | profileUrl | 主页链接 |
---
可用赛道列表
| 赛道名称 | 说明 |
|---|---|
| 全部 | 综合全部 |
| 个人才艺 | 个人才艺类 |
| 生活vlog | 生活Vlog类 |
| 财富理财 | 财富理财类 |
| 二次元 | 二次元类 |
| 居家装修 | 居家装修类 |
| 学习教育 | 学习教育类 |
| 小剧场 | 小剧场类 |
| 数码科技 | 数码科技类 |
| 旅行 | 旅行类 |
| 美食 | 美食类 |
| 化妆美容 | 化妆美容类 |
| 动物 | 动物类 |
| 亲子 | 亲子类 |
| 汽车 | 汽车类 |
| 情感 | 情感类 |
| 三农 | 三农类 |
| 健康医学 | 健康医学类 |
| 潮流风尚 | 潮流风尚类 |
| 舞蹈才艺 | 舞蹈才艺类 |
| 颜值造型 | 颜值造型类 |
| 人文 | 人文类 |
| 音乐 | 音乐类 |
| 影视 | 影视类 |
| 身体锻炼 | 身体锻炼类 |
| 体育 | 体育类 |
| 明星娱乐 | 明星娱乐类 |
| 游戏 | 游戏类 |
---
赛道模糊匹配规则
匹配策略:长词优先 + 全文扫描。用户任何口语化表达都会自动映射到最匹配的赛道,无法识别时降级为"全部"。
| 用户说(示例) | 映射到 |
|---|---|
| 美妆、彩妆、护肤、化妆品、口红、美白、美妆博主 | 化妆美容 |
| 科技、数码、手机、电脑、平板、AI、程序员、极客 | 数码科技 |
| 健康、养生、中医、减肥、营养、医生、心理 | 健康医学 |
| 美食、探店、烹饪、吃播、烘焙、菜谱、厨房 | 美食 |
| 旅行、旅游、出行、露营、徒步、打卡、景点 | 旅行 |
| 穿搭、时尚、搭配、OOTD、潮牌、球鞋、汉服、品牌 | 潮流风尚 |
| 亲子、育儿、母婴、宝宝、儿童、辅食、早教 | 亲子 |
| 教育、学习、读书、考研、高考、英语、老师、课程 | 学习教育 |
| 宠物、猫、狗、萌宠、猫咪、兔子、野生动物 | 动物 |
| 健身、运动、瑜伽、跑步、增肌、减脂、撸铁 | 身体锻炼 |
| 装修、家居、收纳、室内设计、软装、北欧风 | 居家装修 |
| 电影、剧集、综艺、电视剧、短剧、影评、追剧 | 影视 |
| 音乐、唱歌、吉他、钢琴、说唱、RAP、民谣 | 音乐 |
| 舞蹈、跳舞、街舞、古典舞、编舞 | 舞蹈才艺 |
| 明星、娱乐、偶像、追星、八卦、网红 | 明星娱乐 |
| 游戏、网游、手游、电竞、LOL、王者荣耀、原神 | 游戏 |
| 理财、财经、股票、基金、投资、创业、副业 | 财富理财 |
| 动漫、ACG、cosplay、cos、番剧 | 二次元 |
| vlog、日常、生活记录、日常vlog | 生活vlog |
| 人文、历史、文化、传统文化、国学 | 人文 |
| 汽车、车、车评、新能源、摩托车 | 汽车 |
| 农村、三农、乡村、农业、种地 | 三农 |
| 颜值、造型、发型、美发、变美 | 颜值造型 |
| 才艺、魔术、脱口秀、相声、变装 | 个人才艺 |
| 恋爱、婚姻、两性、情侣、相亲 | 情感 |
| 搞笑、剧情、小品、幽默、反转、情景剧 | 小剧场 |
| 篮球、足球、NBA、世界杯、滑雪、羽毛球 | 体育 |
| 综合、全品类、所有 | 全部 |
赛道映射表
支持的赛道列表
| 赛道名称 | 说明 |
|---|---|
| 全部 | 综合全部 |
| 个人才艺 | 个人才艺类 |
| 生活vlog | 生活Vlog类 |
| 财富理财 | 财富理财类 |
| 二次元 | 二次元类 |
| 居家装修 | 居家装修类 |
| 学习教育 | 学习教育类 |
| 小剧场 | 小剧场类 |
| 数码科技 | 数码科技类 |
| 旅行 | 旅行类 |
| 美食 | 美食类 |
| 化妆美容 | 化妆美容类 |
| 动物 | 动物类 |
| 亲子 | 亲子类 |
| 汽车 | 汽车类 |
| 情感 | 情感类 |
| 三农 | 三农类 |
| 健康医学 | 健康医学类 |
| 潮流风尚 | 潮流风尚类 |
| 舞蹈才艺 | 舞蹈才艺类 |
| 颜值造型 | 颜值造型类 |
| 人文 | 人文类 |
| 音乐 | 音乐类 |
| 影视 | 影视类 |
| 身体锻炼 | 身体锻炼类 |
| 体育 | 体育类 |
| 明星娱乐 | 明星娱乐类 |
| 游戏 | 游戏类 |
---
常用模糊匹配映射
| 用户说 | 映射到 |
|---|---|
| 游戏、网游、手游 | 游戏 |
| 美食、探店、烹饪 | 美食 |
| 旅行、旅游 | 旅行 |
| 科技、数码、手机 | 数码科技 |
| 美妆、彩妆、护肤 | 化妆美容 |
| 健身、运动、瑜伽 | 身体锻炼 |
| 亲子、育儿、母婴 | 亲子 |
| 穿搭、时尚 | 潮流风尚 |
| 音乐、唱歌、弹琴 | 音乐 |
| 影视、电影、剧集 | 影视 |
| 健康、养生 | 健康医学 |
| 萌宠、宠物 | 动物 |
| 理财、投资 | 财富理财 |
---
赛道识别处理
脚本 fetch_rank.py 使用 match_category() 函数进行匹配:
匹配策略(按优先级)
1. 精确匹配:大小写不敏感,完整词匹配 2. 模糊匹配:按关键词长度降序扫描,取最长命中词 3. 子串匹配:用户输入是某个key的子串时(如"美妆博主"→"美妆") 4. 兜底:返回"全部"
赛道识别失败时的处理
当 category_fuzzy=False 且无法识别时: 1. 告知用户"抱歉,无法识别该赛道类别" 2. 列出完整赛道列表 3. 询问用户指定某一赛道
完整赛道关键词(用于匹配)
全部、个人才艺、生活vlog、财富理财、二次元、居家装修、学习教育、小剧场、数码科技、旅行、美食、化妆美容、动物、亲子、汽车、情感、三农、健康医学、潮流风尚、舞蹈才艺、颜值造型、人文、音乐、影视、身体锻炼、体育、明星娱乐、游戏抖音账号综合评分规则
评分维度
综合评分基于以下维度计算:
| 维度 | 权重 | 计算方式 |
|---|---|---|
| 总粉丝数 | 20% | ln(x+1)/ln(max+1) × 100 |
| 新增粉丝 | 20% | ln(x+1)/ln(max+1) × 100 |
| 新增点赞 | 20% | ln(x+1)/ln(max+1) × 100 |
| 新增分享 | 20% | ln(x+1)/ln(max+1) × 100 |
| 新增评论 | 20% | ln(x+1)/ln(max+1) × 100 |
计算公式
对数归一化公式:
score = ln(x + 1) / ln(max + 1) × 100其中:
x为该账号的当前指标值max为本次榜单中该指标的最大值
示例
假设某榜单数据如下:
| 账号 | 新增点赞 | 得分计算 |
|---|---|---|
| A | 10万 | ln(100001)/ln(100001) × 100 = 100 |
| B | 5万 | ln(50001)/ln(100001) × 100 ≈ 85 |
| C | 1万 | ln(10001)/ln(100001) × 100 ≈ 67 |
最终得分
各维度加权求和后,保留整数作为综合评分。
更新规则与日期计算
榜单更新时间
| 榜单类型 | 更新时间 | 说明 |
|---|---|---|
| 日榜 | 每日 17:30 | 更新前一天数据 |
| 周榜 | 每周一 17:30 | 更新上周数据 |
| 月榜 | 每月2号 9点 | 更新上月数据 |
---
自动 offset 计算
根据当前时间是否已到更新时间,自动选择 offset:
| 榜单类型 | 当前时间 < 更新时间 | 当前时间 >= 更新时间 |
|---|---|---|
| 日榜 | offset=2(取前2天) | offset=1(取前一天) |
| 周榜 | offset=2(取前2周) | offset=1(取前1周) |
| 月榜 | offset=2(取前2月) | offset=1(取前1月) |
---
日期计算公式
日榜
target_date = date.today() - timedelta(days=offset)周榜
target_date = date.today() - timedelta(weeks=offset) 后的最近周一
target_sunday = target_monday + timedelta(days=6)月榜
target_date = date.today() - timedelta(days=30*offset) 后的每月1号
target_last_day = 下月1号 - 1天---
回溯范围校验
| 榜单类型 | 最大回溯 | 超出时提示语 |
|---|---|---|
| 日榜 | 过去7天 | "非常抱歉🙏,目前榜单最多支持回溯「过去7天」" |
| 周榜 | 过去3周 | "非常抱歉🙏,目前榜单最多支持回溯「过去3周」" |
| 月榜 | 过去3个月 | "非常抱歉🙏,目前榜单最多支持回溯「过去3月」" |
⚠️ 注意:自动计算的日期(如"最新")不受回溯窗口限制,脚本会自动调整 offset 取最近可用数据。
---
时间越界场景
当用户明确指定日期但超出范围时:
| 场景 | 提示语 |
|---|---|
| 查询当日或未来日期 | 非常抱歉🙏,我们最新的是昨日/上周/上月的数据,将为您提供最接近您需求的昨日/上周/上月AI热榜。 |
| 查询超出回溯范围的历史日期 | 非常抱歉🙏,目前榜单最多支持回溯「过去X天/周/月」,我将为您查询最接近您需求的时间范围~ |
---
周期关键词识别
自动从用户文本中识别日/周/月(无明确则默认日榜):
| 用户说 | 识别结果 |
|---|---|
| 含"周"、"周榜" | week |
| 含"月"、"月榜" | month |
| 其余默认 | day |
识别优先级
1. 优先识别周/月(长词优先) 2. 日/月/年中的"日"不参与识别(如"04月27日"中的"日") 3. 无明确时间词时默认日榜
---
近期日期关键词
| 关键词 | 含义 |
|---|---|
| 昨日、昨天 | offset=1 |
| 今日、今天 | offset=1 |
| 最新 | 根据更新时间自动计算 offset |
| 本周、这周 | 当前周 |
| 本月、这个月 | 当前月 |
#!/usr/bin/env python3
"""
抖音每日最具影响力账号榜单数据抓取脚本
用法:
python fetch_rank.py --period day --date 2026-04-27 --category 美食
python fetch_rank.py --query "最新抖音日榜"
python fetch_rank.py --query "美食类周榜"
python fetch_rank.py --query "2026年4月月榜"
"""
import argparse
import json
import os
import re
import sys
from datetime import date, datetime, timedelta
import urllib.request
import urllib.error
API_URL = "https://redfox.hk/story/api/dyData/query"
def _get_api_key() -> str:
"""
从环境变量 REDFOX_API_KEY 获取 API Key。
"""
api_key = os.environ.get("REDFOX_API_KEY", "").strip()
if api_key:
return api_key
print("[ERROR] 未设置 REDFOX_API_KEY 环境变量", file=sys.stderr)
print("[HINT] 请运行: export REDFOX_API_KEY=<你的apikey>", file=sys.stderr)
print("[HINT] 访问 https://redfox.hk/login 注册获取 API Key", file=sys.stderr)
sys.exit(4)
# ─────────────────────────────────────────────────────────
# 加载外部静态配置(assets/)
# ─────────────────────────────────────────────────────────
_ASSETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets")
def _load_json(filename: str) -> dict:
path = os.path.join(_ASSETS_DIR, filename)
with open(path, encoding="utf-8") as f:
return json.load(f)
_cat_cfg = _load_json("category_config.json")
_period_cfg = _load_json("period_config.json")
CATEGORY_MAP = _cat_cfg["category_map"]
EXACT_CATEGORIES = set(_cat_cfg["exact_categories"])
CAT_DISPLAY = _cat_cfg["cat_display"]
PERIOD_MAP = _period_cfg["period_map"]
PERIOD_LABELS = _period_cfg["period_labels"]
PERIOD_KEYS = _period_cfg["period_keys"]
SHORT_ALIASES = _period_cfg["short_aliases"]
LONG_ALIASES = _period_cfg["long_aliases"]
UPDATE_RULES = _period_cfg["update_rules"]
def _api_post(payload: dict, timeout: int = 15) -> dict:
"""
使用原生 urllib 发送 HTTPS POST 请求,携带 X-API-KEY 鉴权
"""
api_key = _get_api_key()
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"X-API-KEY": api_key,
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(API_URL, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f"[ERROR] HTTP {e.code}:{body[:200]}", file=sys.stderr)
sys.exit(2)
def _parse_period_keyword(text: str) -> str | None:
for p, aliases in LONG_ALIASES.items():
for a in aliases:
if a in text:
return p
for p, char in SHORT_ALIASES.items():
for i, ch in enumerate(text):
if ch == char:
before = text[i - 1] if i > 0 else " "
after = text[i + 1] if i < len(text) - 1 else " "
# 排除日期上下文中的月/周/日(如 "04月27日"、"2026年04月")
if before.isdigit() or after.isdigit():
continue
if not (("\u4e00" <= before <= "\u9fff") or ("\u4e00" <= after <= "\u9fff")):
return p
return None
UPDATE_RULES = {
"day": {"label": "日榜", "update_time": "每日17:30", "window_days": 7},
"week": {"label": "周榜", "update_time": "每周一17:30", "window_days": 21},
"month": {"label": "月榜", "update_time": "每月2号9点", "window_days": 70},
}
# 日期校验结果
DATE_OK = "ok"
DATE_FUTURE = "future" # 查询日期为当日或未来
DATE_TOO_EARLY = "too_early" # 超出回溯窗口
def _get_date_status(target_date: date, period: str) -> str:
"""
判断目标日期是否在可用范围内。
返回: "ok" / "future" / "too_early"
"""
today = date.today()
diff = (today - target_date).days
window = UPDATE_RULES[period]["window_days"]
if diff < 1:
return DATE_FUTURE
if diff > window:
return DATE_TOO_EARLY
return DATE_OK
def _latest_period_label(period: str) -> str:
"""返回用于提示语的最近一期中文标签,如「昨日」「上周」「上月」"""
return {"day": "昨日", "week": "上周", "month": "上月"}[period]
def _window_human(period: str) -> str:
"""返回用于提示语的回溯范围中文,如「过去7天」「过去3周」「过去3月」"""
w = UPDATE_RULES[period]["window_days"]
unit = {"day": "天", "week": "周", "month": "月"}[period]
return f"过去{w}{unit}"
def _get_date_range(period: str, offset: int = 1) -> tuple[str, str]:
"""
获取指定周期的日期区间。
period: day / week / month
offset: 往前回溯的期数,默认 1(上一期)
返回: (start_date, end_date) 格式为 "YYYY-MM-DD"
"""
today = date.today()
if period == "day":
target = today - timedelta(days=offset)
return str(target), str(target)
elif period == "week":
# 周榜:取 offset 期前的周一至周日
days_since_monday = today.weekday()
this_monday = today - timedelta(days=days_since_monday)
target_monday = this_monday - timedelta(weeks=offset)
target_sunday = target_monday + timedelta(days=6)
return str(target_monday), str(target_sunday)
elif period == "month":
# 月榜:取 offset 期前的月初至月末
month_offset = offset - 1
year = today.year
month = today.month - month_offset
while month <= 0:
month += 12
year -= 1
first_day = date(year, month, 1)
# 月末:下月1号减1天
if month == 12:
last_day = date(year + 1, 1, 1) - timedelta(days=1)
else:
last_day = date(year, month + 1, 1) - timedelta(days=1)
return str(first_day), str(last_day)
target = today - timedelta(days=offset)
return str(target), str(target)
def _get_latest_date(period: str, offset: int = 1) -> date:
"""获取指定周期的基础日期(日榜=目标日,周榜/月榜=起始日期)"""
start, end = _get_date_range(period, offset)
return date.fromisoformat(start)
def _is_data_updated(period: str) -> bool:
"""
判断当前时间是否已更新最新一期的数据。
日榜 17:30 更新,周榜周一 17:30 更新,月榜每月2号 9点 更新。
"""
now = datetime.now()
if period == "day":
return now.hour > 17 or (now.hour == 17 and now.minute >= 30)
elif period == "week":
if now.weekday() == 0:
return now.hour > 17 or (now.hour == 17 and now.minute >= 30)
return True
elif period == "month":
if now.day == 2:
return now.hour > 9 or (now.hour == 9 and now.minute >= 0)
elif now.day > 2:
return True
return False
return True
def _get_smart_offset(period: str) -> int:
"""根据当前是否已更新,返回合适的 offset(1 或 2)"""
return 1 if _is_data_updated(period) else 2
def _parse_date_from_text(text: str, explicit_period: str | None = None) -> tuple[date | None, str | None]:
"""
从文本中解析日期。
"""
today = date.today()
# YYYY-MM-DD / YYYY年MM月DD日
m = re.search(r"(\d{4})[年\-\/](\d{1,2})[月\-\/](\d{1,2})", text)
if m:
try:
return date(int(m.group(1)), int(m.group(2)), int(m.group(3))), None
except Exception:
pass
for kw, delta, require_period in [
("最新", None, True),
("今日", 1, False), ("今天", 1, False), ("昨日", 1, False),
("本周", 0, False), ("这周", 0, False), ("本月", 0, False), ("这个月", 0, False),
]:
if kw in text:
base_period = explicit_period or "day"
if delta is None:
if _is_data_updated(base_period):
delta = 1
else:
delta = 2
d = _get_latest_date(base_period, offset=delta)
return d, explicit_period
return None, None
def match_category(keyword: str) -> tuple[str, bool]:
"""
将用户输入的自然语言关键词映射到接口支持的赛道类型。
匹配策略(按优先级):
1. 精确匹配(大小写不敏感)
2. 全文扫描:按 key 长度降序扫描 CATEGORY_MAP,找到最长命中词
3. 兜底返回"全部"
返回: (category, is_certain)
- is_certain=True 表示命中了精确或完整关键词
- is_certain=False 表示模糊降级,提示用户结果可能不准
"""
kw = keyword.strip()
if not kw:
return "全部", False
# 1. 精确命中(大小写不敏感)
kw_lower = kw.lower()
for k, v in CATEGORY_MAP.items():
if k.lower() == kw_lower:
return v, True
# 2. 全文扫描(长词优先,避免"电影"被"电"误匹配)
sorted_keys = sorted(CATEGORY_MAP.keys(), key=len, reverse=True)
best_match = None
best_len = 0
for k in sorted_keys:
if k.lower() in kw_lower and len(k) > best_len:
best_match = CATEGORY_MAP[k]
best_len = len(k)
if best_match:
return best_match, True
# 3. 反向扫描:kw 是某个 key 的子串(例如用户输入"美妆博主",key="美妆")
for k in sorted_keys:
if kw_lower in k.lower() and len(kw_lower) >= 2:
return CATEGORY_MAP[k], True
# 4. 兜底
return "全部", False
# ─────────────────────────────────────────────────────────
# 综合评分计算
# ─────────────────────────────────────────────────────────
def _parse_num(val) -> float:
"""将字符串或数字转为 float,失败返回 0"""
if val is None or val == "-" or val == "":
return 0
if isinstance(val, (int, float)):
return float(val)
s = str(val).strip()
if not s:
return 0
try:
if "w" in s.lower() or "万" in s:
return float(s.lower().replace("w", "").replace("万", "")) * 10000
return float(s)
except Exception:
return 0
def calculate_scores_batch(items: list) -> list:
"""
批量计算评分并返回归一化后的分数
评分规则(满分100):使用对数归一化 ln(x+1)/ln(max+1)
- 总粉丝数:20%
- 新增粉丝:20%
- 新增点赞:20%
- 新增分享:20%
- 新增评论:20%
"""
import math
followers_list = [_parse_num(i.get("followers") or i.get("fansCount") or i.get("fans")) for i in items]
fans_list = [_parse_num(i.get("newFans") or i.get("fansGrowth")) for i in items]
likes_list = [_parse_num(i.get("newLikes") or i.get("likedGrowth")) for i in items]
shares_list = [_parse_num(i.get("newShares") or i.get("sharedGrowth")) for i in items]
comments_list = [_parse_num(i.get("newComments") or i.get("commentsGrowth")) for i in items]
max_followers = max(followers_list) if followers_list else 1
max_fans = max(fans_list) if fans_list else 1
max_likes = max(likes_list) if likes_list else 1
max_shares = max(shares_list) if shares_list else 1
max_comments = max(comments_list) if comments_list else 1
results = []
for item in items:
followers = _parse_num(item.get("followers") or item.get("fansCount") or item.get("fans"))
fans_growth = _parse_num(item.get("newFans") or item.get("fansGrowth"))
likes_growth = _parse_num(item.get("newLikes") or item.get("likedGrowth"))
shares_growth = _parse_num(item.get("newShares") or item.get("sharedGrowth"))
comments_growth = _parse_num(item.get("newComments") or item.get("commentsGrowth"))
def log_norm(val, max_val):
if val <= 0 or max_val <= 1:
return 0
return math.log(val + 1) / math.log(max_val + 1) * 100
norm_followers = log_norm(followers, max_followers)
norm_fans = log_norm(fans_growth, max_fans)
norm_likes = log_norm(likes_growth, max_likes)
norm_shares = log_norm(shares_growth, max_shares)
norm_comments = log_norm(comments_growth, max_comments)
score = (
norm_followers * 0.20 +
norm_fans * 0.20 +
norm_likes * 0.20 +
norm_shares * 0.20 +
norm_comments * 0.20
)
item["comprehensiveScore"] = round(score, 1)
return items
def fetch(period: str, rank_date_start: str, rank_date_end: str, category: str) -> list[dict]:
"""调用抖音数据接口"""
payload = {
"dateType": PERIOD_MAP[period],
"rankDate": rank_date_start, # 只传周期开始时间,服务器自行计算结束时间
"type": category if category else "全部",
"source": "抖音每日最具影响力账号-GitHub",
}
try:
result = _api_post(payload, timeout=15)
except urllib.error.URLError as e:
reason = str(e.reason)
if "timed out" in reason.lower() or "time" in reason.lower():
print("[ERROR] 请求超时,请检查网络后重试", file=sys.stderr)
else:
print(f"[ERROR] 无法连接到数据源:{e.reason}", file=sys.stderr)
print("[HINT] 请稍后重试,或联系管理员检查数据源 redfox.hk 的可用性", file=sys.stderr)
sys.exit(2)
except json.JSONDecodeError:
print("[ERROR] 返回数据格式异常(非 JSON)", file=sys.stderr)
sys.exit(2)
if result.get("code") != 2000:
msg = result.get("msg", result.get("message", "未知错误"))
print(f"[ERROR] 接口返回错误(code={result.get('code')}):{msg}", file=sys.stderr)
sys.exit(3)
return result.get("data", [])
def _fmt_num(val) -> str:
"""将数字格式化为易读形式"""
if val is None or val == "-":
return "-"
try:
n = int(val)
except (ValueError, TypeError):
return str(val)
if n == 0:
return "-"
if n >= 100_000_000:
return f"{n/100_000_000:.1f}亿"
if n >= 10_000:
return f"{n/10_000:.1f}w"
return str(n)
def _fmt_w(val) -> str:
"""将数字格式化为 w+ 格式,如 123456 → 1235w+,支持字符串输入"""
if val is None or val == "-" or val == "":
return "-"
# 如果是字符串,先尝试解析
s = str(val).strip()
if not s:
return "-"
# 处理带 w/W 的字符串,如 "1170.51w"
if 'w' in s.lower():
try:
n = float(s.lower().replace('w', '')) * 10000
w_part = int(n) // 10000
return f"{w_part}w+"
except (ValueError, TypeError):
return "-"
# 处理带 亿 的字符串
if '亿' in s:
try:
n = float(s.replace('亿', '')) * 100000000
yi_part = n / 100000000
return f"{yi_part:.1f}亿+"
except (ValueError, TypeError):
return "-"
try:
n = int(float(s))
except (ValueError, TypeError):
return "-"
if n == 0:
return "-"
if n >= 10_000:
return f"{n // 10000}w+"
return str(n)
def build_markdown_table(items: list, period_label: str, date_start: str, date_end: str,
cat_display: str, total: int, displayed: int | None = None,
fuzzy_flag: bool = False, status: str = "ok") -> str:
"""构建 Markdown 格式榜单输出
Args:
items: 要展示的数据列表
period_label: 周期标签(如"日榜")
date_start: 开始日期
date_end: 结束日期
cat_display: 赛道显示名
total: 数据总数
displayed: 实际展示的条数,默认为 len(items)
fuzzy_flag: 是否模糊匹配赛道
"""
# 从 UPDATE_RULES 中读取更新时间
label_to_key = {v["label"]: k for k, v in UPDATE_RULES.items()}
period_key = label_to_key.get(period_label, "day")
ut = UPDATE_RULES.get(period_key, {}).get("update_time", "")
# 日期显示:格式如 "2026年04月25日"
date_display = f"{date_start[:4]}年{date_start[5:7]}月{date_start[8:]}日"
# 实际展示条数
actual_displayed = displayed if displayed is not None else len(items)
remaining = total - actual_displayed if total > actual_displayed else 0
# 严格按照 SKILL.md 标准输出模版
# 全品类查询显示赛道列
is_all_category = cat_display == "全品类"
# 榜单说明:根据周期展示更新时间
if period_key == "day":
update_desc = "每日17:30更新"
window_human = "过去7天"
latest_desc = "昨日"
elif period_key == "week":
update_desc = "每周一17:30更新"
window_human = "过去3周"
latest_desc = "上周"
else:
update_desc = "每月2号9点更新"
window_human = "过去3月"
latest_desc = "上月"
# 超出范围时的提示
warning_msg = ""
if status == "future":
warning_msg = f"非常抱歉🙏,我们最新的是{latest_desc}的数据,将为您提供最接近您需求的{latest_desc}AI热榜。"
elif status == "too_early":
warning_msg = f"非常抱歉🙏,目前榜单最多支持回溯「{window_human}」,我将为您查询最接近您需求的时间范围~"
lines = [
f"📊 抖音{period_label} · {cat_display}",
"",
f"数据日期:{date_display}",
f"共 {total} 个账号上榜(展示 TOP {actual_displayed} 条)",
"",
]
# 将 warning_msg 合并到榜单说明中
if warning_msg:
bill_desc = f"💡 榜单说明:{warning_msg}{update_desc},与实时数据存在差异"
else:
bill_desc = f"💡 榜单说明:{update_desc},与实时数据存在差异"
lines.extend([
bill_desc,
"",
"📐 综合评分:根据总粉丝数、新增粉丝增量、新增点赞/分享/评论加权计算,满分100",
"",
])
if is_all_category:
lines.append("| 排名 | 账号名 | 赛道 | 综合评分 | 总粉丝数 | 新增粉丝 | 新增点赞 | 新增评论 | 新增分享 |")
lines.append("|:---:|--------|:---:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|")
else:
lines.append("| 排名 | 账号名 | 综合评分 | 总粉丝数 | 新增粉丝 | 新增点赞 | 新增评论 | 新增分享 |")
lines.append("|:---:|--------|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|")
for item in items:
account_name = item.get('accountName', '')
profile_url = item.get('accountLink') or item.get('profileUrl', '')
# 排名:1-3 使用 emoji
rank = item.get('accountRanking') or item.get('rank', '-')
if rank == 1:
rank_str = "🥇 1"
elif rank == 2:
rank_str = "🥈 2"
elif rank == 3:
rank_str = "🥉 3"
else:
rank_str = str(rank)
score = item.get('comprehensiveScore', '-')
# 粉丝数格式化为 w+
followers_raw = item.get('fansCount') or item.get('followers') or '-'
followers = _fmt_w(followers_raw) if followers_raw != '-' else '-'
new_fans = _fmt_w(item.get('fansGrowth') or item.get('newFans'))
new_likes = _fmt_w(item.get('likedGrowth') or item.get('newLikes'))
new_comments = _fmt_w(item.get('commentsGrowth') or item.get('newComments'))
new_shares = _fmt_w(item.get('sharedGrowth') or item.get('newShares'))
# 账号名转为可点击链接
if profile_url:
account_display = f"[{account_name}]({profile_url})"
else:
account_display = account_name
if is_all_category:
account_category = item.get('category', '-')
lines.append(
f"| {rank_str} | {account_display} | {account_category} | {score} | {followers} | "
f"{new_fans} | {new_likes} | {new_comments} | {new_shares} |"
)
else:
lines.append(
f"| {rank_str} | {account_display} | {score} | {followers} | "
f"{new_fans} | {new_likes} | {new_comments} | {new_shares} |"
)
# 追加更多操作和订阅服务
remaining = total - actual_displayed
cats = "个人才艺、生活vlog、财富理财、二次元、居家装修、学习教育、小剧场、数码科技、旅行、美食、化妆美容、动物、亲子、汽车、情感、三农、健康医学、潮流风尚、舞蹈才艺、颜值造型、人文、音乐、影视、身体锻炼、体育、明星娱乐、游戏"
lines.extend([
"",
"⚡ **更多操作**",
"",
f"⏺️ 本次榜单完整共 **{total}** 条数据,是否需要查看剩余 **{remaining}** 条?",
"",
"📬 订阅服务",
"",
f"1️⃣ 是否需要订阅每日/周/月的抖音账号最新排名?",
"",
f"2️⃣ 是否需要订阅具体赛道的账号表现?我们支持:{cats}",
"",
])
return "\n".join(lines)
def to_normalized_json(items: list, period: str, date_start: str, date_end: str,
category: str) -> dict:
"""转换为规范化 JSON 结构"""
def parse_inter(s) -> int:
if not s:
return 0
s = str(s).strip()
if "w" in s.lower():
try:
return int(float(s.lower().replace("w", "")) * 10000)
except Exception:
return 0
try:
return int(s)
except Exception:
return 0
# 批量计算评分并按评分降序排序
scored_items = calculate_scores_batch(items)
normalized_list = []
for item in scored_items:
score = item.get("comprehensiveScore", 0)
normalized_list.append({
"rank": item.get("accountRanking") or item.get("rank"),
"accountName": item.get("accountName", ""),
"category": item.get("category") or category or "全部",
"comprehensiveScore": score,
"followers": item.get("fansCount") or item.get("followers") or None,
"newNoteCount": item.get("newNoteCount") or 0,
"newFans": item.get("fansGrowth") or item.get("newFans") or None,
"newLikes": item.get("likedGrowth") or item.get("newLikes") or None,
"newComments": item.get("commentsGrowth") or item.get("newComments") or None,
"newShares": item.get("sharedGrowth") or item.get("newShares") or None,
"newInteraction": parse_inter(item.get("newInteractionCount", "0")),
"profileUrl": item.get("accountLink") or item.get("profileUrl", ""),
})
# 按综合评分降序排序
normalized_list.sort(key=lambda x: x.get("comprehensiveScore", 0), reverse=True)
# 更新排序后的排名
for i, item in enumerate(normalized_list):
item["rank"] = i + 1
date_display = date_start if date_start == date_end else f"{date_start}至{date_end}"
return {
"period": period,
"date": date_display,
"dateStart": date_start,
"dateEnd": date_end,
"category": category,
"total": len(normalized_list),
"list": normalized_list,
}
def parse_natural_query(query: str) -> dict:
text = query.strip()
result = {
"period": "day",
"date_start": str(_get_latest_date("day")),
"date_end": str(_get_latest_date("day")),
"category": "全部",
"category_fuzzy": False,
"warning": "",
"date_fallback": False,
}
# 1. 解析周期
period = _parse_period_keyword(text)
if period:
result["period"] = period
else:
period = "day"
result["period"] = period
# 2. 解析日期
found_date, _ = _parse_date_from_text(text, explicit_period=period)
smart_offset = _get_smart_offset(period)
if found_date:
start, end = _get_date_range(period, offset=smart_offset)
# 如果用户指定了具体日期,则使用该日期作为区间
result["date_start"] = str(found_date)
result["date_end"] = str(found_date)
result["date_explicit"] = True
else:
start, end = _get_date_range(period, offset=smart_offset)
result["date_start"] = start
result["date_end"] = end
result["date_explicit"] = False
# 3. 日期有效性校验
target_date = date.fromisoformat(result["date_start"])
status = _get_date_status(target_date, period)
period_label_latest = _latest_period_label(period)
window_human = _window_human(period)
if status == DATE_FUTURE:
# 当日或未来日期 → 切换至最近一期,并给出指定提示语
off = _get_smart_offset(period)
start, end = _get_date_range(period, offset=off)
result["warning"] = (
f"非常抱歉🙏,我们最新的是{period_label_latest}的数据,"
f"将为您提供最接近您需求的{period_label_latest}AI热榜。"
)
result["date_start"] = start
result["date_end"] = end
result["date_fallback"] = True
elif status == DATE_TOO_EARLY:
# 超出回溯范围 → 切换至窗口内最近一期
off = _get_smart_offset(period)
start, end = _get_date_range(period, offset=off)
result["warning"] = (
f"非常抱歉🙏,目前榜单最多支持回溯「{window_human}」,"
f"我将为您查询最接近您需求的时间范围~"
)
result["date_start"] = start
result["date_end"] = end
result["date_fallback"] = True
else:
# DATE_OK → 日期合法,无需处理;显式指定时也尊重用户选择
pass
# 4. 赛道匹配:直接对原始文本全文扫描,不做粗糙的正则剔除
# 先剔除时间/周期相关的噪声词,避免干扰
noise = r"(今日|今天|昨日|昨天|本周|这周|上周|本月|这个月|上个月|最新|最近|" \
r"抖音|榜单|排行榜|排名|榜|日榜|周榜|月榜|查询|给我|看看|想要|要看|帮我)"
kw = re.sub(noise, "", text)
matched_type, fuzzy = match_category(kw)
result["category"] = matched_type
result["category_fuzzy"] = fuzzy
return result
# ─────────────────────────────────────────────────────────
# CLI
# ─────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="抖音每日最具影响力账号榜单抓取")
parser.add_argument("--query", "-q", type=str, default="",
help="自然语言查询,如:最新美食周榜 / 2026年4月日榜")
parser.add_argument("--period", choices=["day", "week", "month"], default=None)
parser.add_argument("--date", default="", help="目标日期 YYYY-MM-DD")
parser.add_argument("--category", default="", help="赛道类型")
parser.add_argument("--limit", type=int, default=20)
parser.add_argument("--output", default="", help="JSON 输出文件路径")
parser.add_argument("--format", choices=["markdown", "json"], default="markdown")
args = parser.parse_args()
# 自然语言模式
if args.query:
parsed = parse_natural_query(args.query)
period = parsed["period"]
date_start = parsed["date_start"]
date_end = parsed["date_end"]
category = parsed["category"]
category_fuzzy = parsed["category_fuzzy"]
date_explicit = parsed.get("date_explicit", False)
if parsed["warning"]:
print(f"\n{parsed['warning']}\n", file=sys.stderr)
if parsed["date_fallback"]:
print(f"[WARN] 已回退至最近可用日期:{date_start} 至 {date_end}", file=sys.stderr)
print(f"[INFO] 解析结果 → 周期={period} 日期={date_start}至{date_end} 赛道={category}", file=sys.stderr)
else:
period = args.period or "day"
if args.date:
date_start = args.date
date_end = args.date
date_explicit = True
else:
date_start, date_end = _get_date_range(period, _get_smart_offset(period))
date_explicit = False
category = args.category if args.category else "全部"
category_fuzzy = False
# 日期有效性校验
target_date = date.fromisoformat(date_start)
status = _get_date_status(target_date, period)
period_label_latest = _latest_period_label(period)
window_human = _window_human(period)
if status == DATE_FUTURE:
off = _get_smart_offset(period)
start, end = _get_date_range(period, off)
print(
f"\n非常抱歉🙏,我们最新的是{period_label_latest}的数据,"
f"将为您提供最接近您需求的{period_label_latest}AI热榜。\n",
file=sys.stderr,
)
date_start = start
date_end = end
elif status == DATE_TOO_EARLY:
off = _get_smart_offset(period)
start, end = _get_date_range(period, off)
print(
f"\n非常抱歉🙏,目前榜单最多支持回溯「{window_human}」,"
f"我将为您查询最接近您需求的时间范围~\n",
file=sys.stderr,
)
date_start = start
date_end = end
limit = min(args.limit, 50)
items = fetch(period, date_start, date_end, category)
total = len(items)
period_label = PERIOD_LABELS.get(period, "日榜")
cat_display = CAT_DISPLAY.get(category, category + "类")
# 先在全部数据上计算评分并排序
normalized_all = to_normalized_json(items, period, date_start, date_end, category)
# 保存时只保存 limit 条数据,保持 JSON 与对话展示一致
normalized_output = normalized_all.copy()
normalized_output["list"] = normalized_all["list"][:limit]
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(normalized_output, f, ensure_ascii=False, indent=2)
print(f"[INFO] 已保存:{args.output}", file=sys.stderr)
if args.format == "json":
print(json.dumps(items, ensure_ascii=False, indent=2))
else:
# 使用前 limit 条已评分数据
display = normalized_all["list"][:limit]
print(build_markdown_table(display, period_label, date_start, date_end,
cat_display, total, limit, category_fuzzy, status))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
抖音榜单 HTML 报告生成脚本
用法:
python generate_report.py --data result.json --output report.html
python generate_report.py --data result.json # 自动命名输出文件
"""
import argparse
import json
import sys
from pathlib import Path
# ─────────────────────────────────────────────────────────
# 赛道识别:基于账号名推断细分赛道
# ─────────────────────────────────────────────────────────
CATEGORY_INFER_RULES = [
# 游戏类
{"keywords": ["王者荣耀", "英雄联盟", "原神", "崩坏", "蛋仔", "阴阳师", "明日方舟", "光遇", "鸣潮"], "category": "游戏"},
{"keywords": ["吃鸡", "和平精英", "PUBG", "LOL", "无畏契约", "CSGO", "DOTA", "Valorant"], "category": "游戏"},
{"keywords": ["迷你世界", "我的世界", "MC", "Minecraft"], "category": "游戏"},
# 明星/娱乐类
{"keywords": ["official", "工作室"], "category": "官方账号"},
{"keywords": ["爱奇艺", "优酷", "腾讯视频", "芒果TV", "Bilibili", "B站"], "category": "视频平台"},
{"keywords": ["微博", "新浪", "凤凰网", "澎湃"], "category": "新闻媒体"},
{"keywords": ["明星", "演员", "歌手", "偶像"], "category": "明星娱乐"},
# 美食类细分
{"keywords": ["美食", "吃货", "探店", "烹饪"], "category": "美食"},
{"keywords": ["烘焙", "蛋糕", "面包", "甜品"], "category": "烘焙甜点"},
# 旅行/户外类
{"keywords": ["旅行", "旅游", "自驾", "露营"], "category": "旅行"},
# 宠物类
{"keywords": ["宠物", "猫", "狗", "萌宠", "铲屎"], "category": "动物"},
# 时尚/穿搭类
{"keywords": ["穿搭", "OOTD", "衣橱"], "category": "潮流风尚"},
# 健身/运动类
{"keywords": ["健身", "瑜伽", "减脂", "增肌", "运动"], "category": "身体锻炼"},
# 学习/知识类
{"keywords": ["教育", "学习", "干货", "职场", "知识"], "category": "学习教育"},
# 科技/测评类
{"keywords": ["数码", "手机", "电脑", "测评", "评测", "科技"], "category": "数码科技"},
# 亲子/母婴类
{"keywords": ["母婴", "育儿", "宝宝", "辣妈", "萌娃", "亲子"], "category": "亲子"},
# 家居/装修类
{"keywords": ["家居", "装修", "软装", "设计", "收纳"], "category": "居家装修"},
# 情感/心理类
{"keywords": ["星座", "情感", "恋爱", "心理", "塔罗"], "category": "情感"},
# 音乐类
{"keywords": ["音乐", "唱歌", "弹琴", "乐器"], "category": "音乐"},
# 影视类
{"keywords": ["影视", "娱乐", "综艺", "剧集", "电影"], "category": "影视"},
# 舞蹈类
{"keywords": ["舞蹈", "跳舞", "舞"], "category": "舞蹈才艺"},
# 日常生活/Vlog
{"keywords": ["日记", "vlog", "plog", "日常", "记录", "生活"], "category": "生活vlog"},
# 二次元类
{"keywords": ["二次元", "动漫", "动画", "ACG"], "category": "二次元"},
# 健康医学类
{"keywords": ["健康", "养生", "医疗", "医学"], "category": "健康医学"},
]
def infer_category(account_name: str) -> str:
"""根据账号名推断赛道分类"""
combined = (account_name).lower()
# 精确账号名匹配
exact_account_map = {
"王者荣耀": "游戏",
"原神": "游戏",
"和平精英": "游戏",
"英雄联盟": "游戏",
"迷你世界": "游戏",
}
for exact_name, cat in exact_account_map.items():
if exact_name.lower() in combined:
return cat
# 规则遍历匹配
for rule in CATEGORY_INFER_RULES:
for kw in rule["keywords"]:
if kw.lower() in combined:
return rule["category"]
return "全部"
def parse_num(val) -> int:
"""解析数字字符串"""
if val is None or val == "-":
return 0
s = str(val).replace("w", "").replace("W", "").replace("+", "").strip()
try:
if "." in s:
return int(float(s) * 10000)
return int(float(s))
except (ValueError, TypeError):
return 0
def format_interaction(num: int) -> str:
if num >= 100_000:
return f"{num // 10_000}w+"
elif num >= 10_000:
return f"{num / 10_000:.1f}w+"
return str(num)
def format_followers(num: int) -> str:
if num >= 100_000_000:
return f"{num / 100_000_000:.1f}亿"
elif num >= 10_000:
return f"{num / 10_000:.1f}w"
return str(num)
PERIOD_UPDATE_RULES = {
"day": "每日17:30",
"week": "每周一17:30",
"month": "每月2号9点",
}
PERIOD_LABELS = {"day": "日榜", "week": "周榜", "month": "月榜"}
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>抖音{period_label} · {date}</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #f0f4ff 0%, #fafbff 100%);
min-height: 100vh;
padding: 24px;
color: #1a1a1a;
}}
.header {{
text-align: center;
margin-bottom: 32px;
}}
.header .logo {{
font-size: 48px;
margin-bottom: 8px;
}}
.header h1 {{
font-size: 28px;
font-weight: 700;
color: #000000;
margin-bottom: 6px;
}}
.header .meta {{
font-size: 13px;
color: #888;
}}
.notice {{
background: #fff8e1;
border-left: 4px solid #ffb300;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
font-size: 13px;
color: #5d4037;
}}
.stats-bar {{
display: flex;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}}
.stat-card {{
flex: 1;
min-width: 120px;
background: #fff;
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}}
.stat-card .num {{
font-size: 28px;
font-weight: 700;
color: #000000;
}}
.stat-card .label {{
font-size: 12px;
color: #888;
margin-top: 4px;
}}
.stat-card.hidden {{ display: none; }}
.more-hint {{
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border-left: 4px solid #ff9800;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
font-size: 13px;
color: #e65100;
display: none;
}}
.more-hint.show {{ display: block; }}
.table-wrap {{
background: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
margin-bottom: 24px;
}}
table {{
width: 100%;
border-collapse: collapse;
}}
thead th {{
background: linear-gradient(135deg, #000000, #000000);
color: #fff;
padding: 14px 16px;
text-align: left;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}}
tbody tr {{
border-bottom: 1px solid #f5f5f5;
transition: background 0.15s;
}}
tbody tr:hover {{ background: #f0f9ff; }}
tbody tr:last-child {{ border-bottom: none; }}
td {{
padding: 12px 16px;
font-size: 14px;
vertical-align: middle;
}}
.rank-badge {{
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
font-weight: 700;
font-size: 14px;
}}
.rank-1 {{ background: #FFD700; color: #5d4000; }}
.rank-2 {{ background: #C0C0C0; color: #3d3d3d; }}
.rank-3 {{ background: #CD7F32; color: #fff; }}
.rank-other {{ background: #f0f0f0; color: #555; }}
.account-name {{
font-weight: 600;
color: #333;
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}}
.account-name:hover {{
color: #000000;
text-decoration: underline;
}}
.score-bar-wrap {{
display: flex;
align-items: center;
gap: 8px;
}}
.score-bar {{
flex: 1;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
max-width: 100px;
}}
.score-fill {{
height: 100%;
background: linear-gradient(90deg, #000000, #000000);
border-radius: 4px;
}}
.score-num {{
font-weight: 600;
color: #000000;
min-width: 36px;
}}
.interaction {{
font-weight: 600;
color: #ff6b35;
}}
.export-bar {{
text-align: center;
margin-bottom: 16px;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}}
.btn {{
padding: 10px 24px;
border-radius: 24px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: transform 0.1s, box-shadow 0.1s;
}}
.btn:hover {{ transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }}
.btn-primary {{ background: linear-gradient(135deg, #000000, #000000); color: #fff; }}
.btn-secondary {{ background: #fff; color: #000000; border: 2px solid #000000; }}
.footer {{
text-align: center;
font-size: 12px;
color: #bbb;
padding: 16px;
}}
@media print {{
.export-bar {{ display: none; }}
body {{ background: #fff; padding: 0; }}
.table-wrap {{ box-shadow: none; }}
}}
</style>
</head>
<body>
<div class="header">
<div class="logo">📊</div>
<h1>抖音{period_label} · {category_label}</h1>
<div class="meta">数据日期:{date} | {display_hint}</div>
</div>
<div class="notice">
💡 <strong>榜单说明</strong>:{update_rule},与实时数据存在差异。<br>
📐 <strong>综合评分(满分100)</strong>:综合评分根据达人在抖音的 <strong>总粉丝数</strong>、周期内的 <strong>粉丝增量</strong>、<strong>点赞增量</strong>、<strong>分享增量</strong> 以及 <strong>评论增量</strong> 加权计算所得(满分100)。
</div>
<div class="stats-bar">
<div class="stat-card"><div class="num">{displayed_count}</div><div class="label">展示条数</div></div>
<div class="stat-card"><div class="num">{top_interaction}</div><div class="label">最高互动</div></div>
</div>
<div class="export-bar">
<button class="btn btn-primary" onclick="window.print()">🖨️ 打印 / 导出 PDF</button>
<button class="btn btn-secondary" id="downloadImgBtn" onclick="downloadAsImage()">📷 保存为图片</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script>
async function downloadAsImage() {{
const btn = document.getElementById('downloadImgBtn');
btn.textContent = '⏳ 生成中...';
btn.disabled = true;
try {{
const element = document.querySelector('.table-wrap');
const canvas = await html2canvas(element, {{
scale: 2,
backgroundColor: '#ffffff',
useCORS: true
}});
const link = document.createElement('a');
link.download = '抖音榜单_{date}.png';
link.href = canvas.toDataURL('image/png');
link.click();
btn.textContent = '✅ 下载成功';
setTimeout(() => {{
btn.textContent = '📷 保存为图片';
btn.disabled = false;
}}, 2000);
}} catch (e) {{
alert('生成图片失败,请尝试使用浏览器截图');
btn.textContent = '📷 保存为图片';
btn.disabled = false;
}}
}}
</script>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>排名</th>
<th>账号名</th>
{category_th}
<th>综合评分</th>
<th>总粉丝数</th>
<th>新增粉丝</th>
<th>新增点赞</th>
<th>新增评论</th>
<th>新增分享</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
<!-- 订阅服务模块(导出图片时排除) -->
<div id="subscription-section" style="margin-top: 40px; padding: 24px; background: #f8f9fa; border-radius: 12px;">
<h3 style="margin: 0 0 16px 0; font-size: 18px; color: #333;">📬 订阅服务</h3>
<div style="margin-bottom: 12px;">
<div style="font-weight: 500; color: #333;">1️⃣ 是否需要订阅每日/周/月的抖音账号最新排名?</div>
</div>
<div>
<div style="font-weight: 500; color: #333; margin-bottom: 8px;">2️⃣ 是否需要订阅具体赛道的账号表现?我们支持:</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">个人才艺</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">生活vlog</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">财富理财</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">二次元</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">居家装修</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">学习教育</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">小剧场</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">数码科技</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">旅行</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">美食</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">化妆美容</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">动物</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">亲子</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">汽车</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">情感</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">三农</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">健康医学</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">潮流风尚</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">舞蹈才艺</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">颜值造型</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">人文</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">音乐</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">影视</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">身体锻炼</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">体育</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">明星娱乐</span>
<span style="background: #e9ecef; padding: 4px 10px; border-radius: 16px; font-size: 13px;">游戏</span>
</div>
</div>
</div>
</body>
</html>"""
# 全品类行模板(带赛道列)
ROW_TEMPLATE_CAT = """ <tr>
<td><span class="rank-badge {rank_class}">{rank}</span></td>
<td><a href="{profile_url}" target="_blank" class="account-name" title="点击查看抖音主页">{account_name}</a></td>
<td class="category">{category}</td>
<td><span class="score">{score}</span></td>
<td>{followers}</td>
<td class="interaction">{new_fans}</td>
<td class="interaction">{new_likes}</td>
<td class="interaction">{new_comments}</td>
<td class="interaction">{new_shares}</td>
</tr>"""
# 非全品类行模板(无赛道列)
ROW_TEMPLATE = """ <tr>
<td><span class="rank-badge {rank_class}">{rank}</span></td>
<td><a href="{profile_url}" target="_blank" class="account-name" title="点击查看抖音主页">{account_name}</a></td>
<td><span class="score">{score}</span></td>
<td>{followers}</td>
<td class="interaction">{new_fans}</td>
<td class="interaction">{new_likes}</td>
<td class="interaction">{new_comments}</td>
<td class="interaction">{new_shares}</td>
</tr>"""
def _fmt_num(val) -> str:
"""格式化数字"""
if val is None or val == "-" or val == "":
return "-"
if isinstance(val, str):
return val if val.strip() else "-"
try:
n = int(val)
if n == 0:
return "-"
return format_followers(n)
except (TypeError, ValueError):
return str(val) if val else "-"
def generate_html(data: dict, output_path: str):
"""生成 HTML 报告"""
items = data.get("list", [])
period = data.get("period", "day")
date_start = data.get("dateStart", data.get("date", ""))
date_end = data.get("dateEnd", date_start)
category = data.get("category", "全部")
total = data.get("total", len(items))
# 实际展示的条数
displayed_count = len(items)
if date_start == date_end:
date_display = date_start
else:
date_display = f"{date_start} 至 {date_end}"
period_label = PERIOD_LABELS.get(period, "日榜")
CAT_DISPLAY = {
"全部": "全品类", "化妆美容": "美妆类", "美食": "美食类",
"旅行": "旅行类", "数码科技": "科技类", "游戏": "游戏类",
"健康医学": "健康类", "亲子": "亲子类", "身体锻炼": "运动类",
"学习教育": "教育类", "动物": "动物类", "潮流风尚": "时尚类",
"居家装修": "家居类", "影视": "影视类", "音乐": "音乐类",
"舞蹈才艺": "舞蹈类", "明星娱乐": "娱乐类", "体育": "体育类",
"情感": "情感类", "财富理财": "理财类", "二次元": "二次元类",
"小剧场": "小剧场类", "汽车": "汽车类", "三农": "三农类",
"人文": "人文类", "颜值造型": "颜值类", "个人才艺": "才艺类",
"生活vlog": "生活类",
}
cat_display = CAT_DISPLAY.get(category, category + "类")
is_all_category = (category == "全部")
# 表头:全品类时添加赛道列
if is_all_category:
category_th = '<th>赛道</th>'
else:
category_th = ''
# 榜单说明:根据周期展示更新时间
if period == "day":
update_rule = "每日17:30更新"
window_human = "过去7天"
latest_desc = "昨日"
elif period == "week":
update_rule = "每周一17:30更新"
window_human = "过去3周"
latest_desc = "上周"
else:
update_rule = "每月2号9点更新"
window_human = "过去3月"
latest_desc = "上月"
# 超出范围时的提示
warning_text = ""
if data.get("status") == "future":
warning_text = f'非常抱歉🙏,我们最新的是{latest_desc}的数据,将为您提供最接近您需求的{latest_desc}热榜。'
elif data.get("status") == "too_early":
warning_text = f'非常抱歉🙏,目前榜单最多支持回溯「{window_human}」,我将为您查询最接近您需求的时间范围~'
# 合并 warning 到榜单说明
if warning_text:
update_rule_full = f'{warning_text}{update_rule}'
else:
update_rule_full = update_rule
# 统计
max_interaction = 0
for item in items:
likes = parse_num(item.get('newLikes') or item.get('likedGrowth'))
comments = parse_num(item.get('newComments') or item.get('commentsGrowth'))
shares = parse_num(item.get('newShares') or item.get('sharedGrowth'))
interaction = likes + comments + shares
if interaction > max_interaction:
max_interaction = interaction
# 生成行
rows = []
for item in items:
rank = item.get('rank', '-')
rank_class = f"rank-{rank}" if rank in [1, 2, 3] else "rank-other"
account_name = item.get('accountName', '')
profile_url = item.get('profileUrl', '')
score = item.get('comprehensiveScore', '-')
if isinstance(score, (int, float)) and score > 0:
score = int(score)
followers = _fmt_num(item.get('followers') or item.get('fansCount'))
new_fans = _fmt_num(item.get('newFans') or item.get('fansGrowth'))
new_likes = _fmt_num(item.get('newLikes') or item.get('likedGrowth'))
new_comments = _fmt_num(item.get('newComments') or item.get('commentsGrowth'))
new_shares = _fmt_num(item.get('newShares') or item.get('sharedGrowth'))
# 全品类时使用带赛道列的模板
if is_all_category:
account_category = item.get('category', '-')
rows.append(ROW_TEMPLATE_CAT.format(
rank_class=rank_class,
rank=rank,
account_name=account_name,
profile_url=profile_url or '#',
category=account_category,
score=score,
followers=followers,
new_fans=new_fans,
new_likes=new_likes,
new_comments=new_comments,
new_shares=new_shares,
))
else:
rows.append(ROW_TEMPLATE.format(
rank_class=rank_class,
rank=rank,
account_name=account_name,
profile_url=profile_url or '#',
score=score,
followers=followers,
new_fans=new_fans,
new_likes=new_likes,
new_comments=new_comments,
new_shares=new_shares,
))
# 显示提示
if displayed_count >= total:
display_hint = f"共 {total} 个账号上榜"
total_hidden_class = ""
more_hint_class = ""
remaining_count = 0
else:
display_hint = f"共 {total} 个账号上榜(展示 TOP {displayed_count} 条)"
total_hidden_class = "hidden"
more_hint_class = "show"
remaining_count = total - displayed_count
html = HTML_TEMPLATE.format(
period_label=period_label,
category_label=cat_display,
date=date_display.replace(' ', '_'),
displayed_count=displayed_count,
total_count=total,
total_hidden_class=total_hidden_class,
remaining_count=remaining_count,
more_hint_class=more_hint_class,
display_hint=display_hint,
update_rule=update_rule,
top_interaction=format_interaction(max_interaction),
category_th=category_th,
rows="\n".join(rows),
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
print(f"[INFO] HTML 报告已生成:{output_path}")
def main():
parser = argparse.ArgumentParser(description="抖音榜单 HTML 报告生成")
parser.add_argument("--data", "-d", required=True, help="JSON 数据文件路径")
parser.add_argument("--output", "-o", default="", help="HTML 输出文件路径")
parser.add_argument("--limit", type=int, default=20, help="限制展示条数,0 表示全部展示")
args = parser.parse_args()
with open(args.data, "r", encoding="utf-8") as f:
data = json.load(f)
# 应用 limit 限制
if args.limit > 0 and "list" in data:
data["list"] = data["list"][:args.limit]
# 生成文件名:赛道+周期+日期+时间戳
if args.output:
output_path = Path(args.output)
else:
import time
date_start = data.get("dateStart", data.get("date", ""))
category = data.get("category", "全部")
period = data.get("period", "day")
period_name = {"day": "日榜", "week": "周榜", "month": "月榜"}.get(period, "榜")
date_str = date_start.replace("-", "")
timestamp = int(time.time())
filename = f"{category}{period_name}{date_str}_{timestamp}.html"
output_path = Path(filename)
generate_html(data, str(output_path))
# 自动打开 HTML 文件
import platform
import subprocess
abs_path = output_path.resolve()
try:
system = platform.system()
if system == "Darwin": # macOS
subprocess.run(["open", str(abs_path)], check=True)
elif system == "Windows":
subprocess.run(["start", "", str(abs_path)], shell=True, check=True)
else: # Linux
subprocess.run(["xdg-open", str(abs_path)], check=True)
print(f"\n✓ HTML 报告已自动打开: {abs_path}", file=sys.stderr)
except Exception as e:
print(f"\n✓ HTML 报告已生成: {abs_path}", file=sys.stderr)
if __name__ == "__main__":
main()