基于 Ninja Kiwi Open Data API 的 BTD6 数据抓取与整理工具。
本项目适合两类使用场景:
- 作为命令行工具,定时生成群公告用的 Markdown 或排行榜图片。
- 作为 Python 模块,被其他项目直接调用,拿到结构化结果后继续加工。
当前覆盖活动:Race、Boss、Odyssey、Daily。 说明:CT 相关链路已在本项目中移除。
- 获取 BTD6 官方 Open Data 原始数据。
- 输出活动简报(summary)。
- 输出最新一期详细信息(detail):race / boss / odyssey / daily。
- 输出排行榜(leaderboard):
- markdown 文本榜单
- image 图片榜单(适合社群转发)
- 支持翻译表(translate.md)做中文化映射。
- 支持 10 分钟 TTL 缓存,普通查询优先命中缓存,未命中或过期时按需回源。
- 支持一键全量更新(update)。
- 支持 collection event 轮换表与图片导出。
- 支持 collection event 缓存读取与按需回源。
说明:所有输出路径都相对于本项目目录解析,不受当前命令执行目录影响。
.
├── btd6_cli.py # CLI 入口
├── api_raw_fetcher.py # 原始 API 请求层
├── btd6_core/
│ ├── common.py # 通用工具:翻译、时间、格式化
│ ├── cache_store.py # 缓存索引与文件读写
│ ├── summary_service.py # 简报生成
│ ├── detail_service.py # 详情生成
│ ├── leaderboard_service.py # 排行榜生成
│ ├── image_renderer.py # PNG 排行榜渲染
│ └── update_service.py # 全量更新流程
├── translate.md # 翻译映射表
├── output/ # 所有输出根目录(运行时自动创建)
│ ├── cache_index.json # 缓存索引
│ ├── summary/ # 简报输出
│ ├── race/ boss/ odyssey/ daily/ # 详情与排行榜输出
│ └── collection_event/ # 收集活动缓存输出
├── assets/fonts/ # 图片渲染字体缓存
└── assets/InstaMonkeyIcon/ # collection event 图标资源
推荐 Python 3.10+。
本项目核心网络请求使用标准库 urllib,图片输出依赖 Pillow。
pip install pillow可通过环境变量设置:
export NK_API_KEY="your_api_key"也可以每次通过 --api-key 传入。
入口脚本:btd6_cli.py
python btd6_cli.py --help--api-key:Ninja Kiwi API Key,可选。--translate:翻译表路径,默认translate.md。--output:主输出文件路径,默认output/btd6_digest.md。--mode:模式,可选summary/detail/leaderboard/collection-event/update。--collection-event-output:collection-event 模式的 JSON 输出路径,默认output/collection_event_schedule.json。--collection-event-image-output:collection-event 模式的图片输出路径,默认output/collection_event_schedule.png。--only-upcoming:collection-event 模式仅输出当前和未来轮换。
生成当前活动简报(Race/Boss/Odyssey/Daily)。
python btd6_cli.py \
--mode summary \
--output output/summary.md生成指定活动类型的“最新一期详细信息”。
参数:
--detail-types:逗号分隔,支持race,boss,odyssey,daily。
示例:
python btd6_cli.py \
--mode detail \
--detail-types race,boss \
--output output/detail.md说明:
- 若只传一个类型,输出该类型详情。
- 若传多个类型,主输出会包含每个类型的文件路径和合并内容。
生成排行榜(当前实现固定从第一页起拉取,内部按类型取固定人数)。
参数:
--leaderboard-type:race/boss-standard/boss-elite--leaderboard-format:markdown/image
示例(文本排行榜):
python btd6_cli.py \
--mode leaderboard \
--leaderboard-type race \
--leaderboard-format markdown \
--output output/race_lb.md示例(图片排行榜):
python btd6_cli.py \
--mode leaderboard \
--leaderboard-type boss-elite \
--leaderboard-format image \
--output output/boss_elite_image_result.md说明:
--output保存的是执行摘要(来源、生成文件路径),真正榜单文件写入活动目录。- 图片榜单仅显示:排行、玩家、得分。
- 图片底部左侧显示数据来源,右侧显示数据获取时间(
YYYY-MM-DD HH:MM:SS)。
一键更新所有核心数据:
- 简报:summary
- 详情:race / boss / odyssey / daily
- 排行榜:race / boss-standard / boss-elite(markdown + image)
python btd6_cli.py \
--mode update \
--output output/update.md生成 collection event 的轮换 JSON 与图片。会优先读取缓存中的 JSON 和图片;如果缓存不存在或已过期,才会现场生成并写入缓存。图片图标默认读取 assets/InstaMonkeyIcon/,字体优先使用 assets/fonts/Gardenia-Bold.woff2,其次会按 assets/fonts/ 下的其他本地字体回退。
python btd6_cli.py \
--mode collection-event \
--collection-event-output output/collection_event_schedule.json \
--collection-event-image-output output/collection_event_schedule.png说明:
--only-upcoming仅保留当前和未来轮换。- 缓存 TTL 为 10 分钟,超过后会自动重新获取并覆盖缓存。
output/race/{event_id}_detail.mdoutput/boss/{event_id}_detail.mdoutput/odyssey/{event_id}_detail.mdoutput/daily/{event_id}_detail.md
output/summary/latest_summary.md
Markdown:
output/race/{event_id}_top100.mdoutput/boss/{event_id}_standard_top150.mdoutput/boss/{event_id}_elite_top150.md
Image:
output/race/{event_id}_top100.pngoutput/boss/{event_id}_standard_top150.pngoutput/boss/{event_id}_elite_top150.png
- Race:固定前 100 人(取前 2 页并截断)。
- Boss:固定前 150 人(尝试前 6 页并截断)。
output/collection_event/latest_collection_event.jsonoutput/collection_event/latest_collection_event.pngoutput/collection_event/latest_collection_event.upcoming.jsonoutput/collection_event/latest_collection_event.upcoming.png
说明:
collection-event模式优先读取对应缓存,缓存缺失或过期时会重新生成并写入。
缓存索引文件:output/cache_index.json
索引结构示例:
{
"items": {
"detail:race": {
"id": "race_event_id",
"path": "output/race/race_event_id_detail.md",
"updatedAt": "2026-03-24T12:34:56+08:00",
"updatedAtEpoch": 1774326896,
"ttlSeconds": 600
},
"leaderboard:boss-elite:image-fixed": {
"id": "boss_event_id",
"path": "output/boss/boss_event_id_elite_top150.png",
"updatedAt": "2026-03-24T12:35:10+08:00",
"updatedAtEpoch": 1774326910,
"ttlSeconds": 600
}
}
}读取逻辑:
summary/detail/leaderboard/collection-event模式优先读取缓存文件。- 缓存不存在或超过 10 分钟 TTL 时,会自动请求远程并覆盖缓存。
- 如果远程请求失败且本地仍有旧缓存,会自动回退到旧缓存继续服务。
update会强制回源并刷新全部缓存。
下面接口可直接在其他 Python 项目中导入调用。
文件:api_raw_fetcher.py
ApiClient(api_key: str | None = None, timeout: int = 45, retries: int = 2)ApiClient.get(path_or_url: str) -> dictfetch_raw_data(client: ApiClient) -> dict
说明:
get支持传完整 URL 或相对路径。- 内置重试与指数退避。
- 超时和 HTTP 错误会被格式化成更清晰的错误文本,例如
读取超时(45s)、HTTP 503 Service Unavailable。 - 统一期望 Open Data 返回格式:
{ success, error, body }。
示例:
from api_raw_fetcher import ApiClient, fetch_raw_data
client = ApiClient(api_key=None)
raw = fetch_raw_data(client)
print(raw.keys()) # dict_keys(['races', 'bosses', 'odyssey', 'daily'])文件:btd6_core/summary_service.py
build_report(client, trans) -> strresolve_summary(client, trans, refresh=False) -> tuple[path, content, cached]
说明:
refresh=False时优先读缓存;缓存不存在或过期时会主动请求远程。- 如果远程请求失败且本地仍有旧缓存,会自动回退到旧缓存。
示例:
from pathlib import Path
from api_raw_fetcher import ApiClient
from btd6_core.common import parse_translation_tables
from btd6_core.summary_service import build_report
client = ApiClient()
trans = parse_translation_tables(Path("translate.md"))
report = build_report(client, trans)文件:btd6_core/detail_service.py
resolve_detail(client, trans, detail_type, refresh=False) -> tuple[path, content, cached]
参数:
detail_type:race/boss/odyssey/daily
返回:
path:详情文件路径content:详情文本cached:是否来自缓存
说明:
refresh=False时优先读缓存;缓存不存在或过期时会主动请求远程。- 如果远程请求失败且本地仍有旧缓存,会自动回退到旧缓存。
文件:btd6_core/leaderboard_service.py
resolve_leaderboard(client, leaderboard_type, page, output_format="markdown", refresh=False) -> tuple[path, content, cached]
参数:
leaderboard_type:race/boss-standard/boss-elitepage:保留参数,当前内部统一按固定策略处理output_format:markdown/image
返回:
path:榜单文件路径(md 或 png)content:markdown 内容(image 时为空字符串)cached:是否来自缓存
说明:
refresh=False时优先读缓存;缓存不存在或过期时会主动请求远程。- 如果远程请求失败且本地仍有旧缓存,会自动回退到旧缓存。
文件:btd6_core/update_service.py
update_all_data(client, trans) -> str
用于批量刷新 summary、所有详情、排行榜和 collection event 缓存。 如果官方 API 刷新失败但本地仍有旧缓存,结果文本会明确标记为“已回退旧缓存”。
解析逻辑在 btd6_core/common.py 的 parse_translation_tables。
支持分类标题:
- 难度等级
- 英雄
- 猴塔
- 地图
- 地图类型
- 游戏模式
- BOSS气球
每个分类使用 Markdown 表格,至少两列(英文、中文)。
示例:
## 4. 地图
| 英文 | 中文 |
| --- | --- |
| MonkeyMeadow | 猴子草地 |
| TreeStump | 树桩 |渲染模块:btd6_core/image_renderer.py
- collection event 绘图优先使用本地
assets/fonts/*.woff2字体。 - 竞速图布局:4 列 × 25 行。
- Boss 图布局:3 列 × 50 行。
- 每行字段:排行、玩家、得分。
- 使用斑马纹提高可读性。
- 底部页脚:左侧
数据来源: btd6 open data,右侧数据获取时间: YYYY-MM-DD HH:MM:SS。
业务侧可以直接调用查询命令,缓存有效期为 10 分钟;如果希望提前预热,也可以单独跑一次 update:
python btd6_cli.py --mode update --output output/update.md
python btd6_cli.py --mode summary --output output/summary.md
python btd6_cli.py --mode leaderboard --leaderboard-type race --leaderboard-format image --output output/race_image.md你的系统只需读取 output/*.md 和 output/ 下活动目录生成的 *.png 即可。
from pathlib import Path
from api_raw_fetcher import ApiClient
from btd6_core.common import parse_translation_tables
from btd6_core.detail_service import resolve_detail
client = ApiClient()
trans = parse_translation_tables(Path("translate.md"))
path, content, cached = resolve_detail(client, trans, "boss", refresh=False)
payload = {
"path": str(path),
"cached": cached,
"preview": content[:200],
}- 网络波动:已内置重试与退避;若业务侧严格 SLA,建议外层再包一层任务重试。
- 缓存文件缺失或过期:查询命令会自动回源并刷新缓存。
- API 失败但存在旧缓存:查询命令会自动回退旧缓存,不会直接中断。
- API 失败:
ApiClient.get会抛出RuntimeError,上层应记录日志并告警。
- 数据来源:Ninja Kiwi Open Data API
- 本项目为数据整理与展示工具,不隶属于 Ninja Kiwi 官方。