热点三榜数据接入定稿:全品类单文件(不传赛道一次调用)+ 三子目录落盘
- fetch_week_ranks.py 重写:28 赛道循环→全品类不传 type 每榜每期 1 次调用;hotContentRank source 修正为生产名「抖音每日点赞飙升榜」(GitHub 后缀版返回空);API 单次上限 TOP50(limit/page/offset 实测均无效)固化防再探测;成本铁律「批量付费调用前须先经用户确认」
- 落盘契约:抖音榜单/ 三子目录(账号周榜/视频热榜/点赞榜),hot/likes 全品类单文件/期 抖音周榜_{视频热榜|点赞榜}_{周一}.json;旧 28 赛道 56 文件归档 _archive_旧28赛道结构_20260903
- dsh-data.js:RANK_SUB 子目录映射 + rankFilePath + listRankFiles(子目录优先+根目录旧平铺回退) + collectCategories(week=文件名赛道、hot/likes=条目 category 并集)+ getRanking 赛道筛选(week 文件名级/hot likes 条目级)
- 前端三榜渲染(data-pages.js/app.js/style.css 承接前段)+ 文档同步(主 SKILL.md 落盘表/抓取器说明段)+ dsh 副本 diff=0 + 记忆日志
This commit is contained in:
1 parent
09dc431773
commit
faa0bd751f
10 files changed
+553
-116
No files matched your search
@@ -94,17 +94,27 @@ export REDFOX_API_KEY="ak_xxx..."
|
||||
| ①③ 用户环境 / 开发机(本机无 `.dsh`) | `C:\Users\maidou\Desktop\MCNSkill项目\三方数据\红狐数据\抖音榜单\` |
|
||||
| ② dsh 环境(本机有 `.dsh`) | `D:\dshworkspace\抖音榜单\`(兼容工作台 RANKING_DIR 现有读取) |
|
||||
|
||||
### 文件命名(按工作台榜单 tab 对齐,09-03 截图实证)
|
||||
### 目录结构(09-03 起分目录管理,按工作台 tab 对齐)
|
||||
|
||||
`{榜名}_{赛道}_{数据日期}.json`——所有榜单一律此格式,按榜名区分:
|
||||
`抖音榜单/` 下按三榜分三个子目录,避免长期平铺堆积;根目录旧平铺文件兼容(工作台读取端回退扫描),新产物一律入子目录:
|
||||
|
||||
| 工作台 tab | 类别 | 文件名(榜名部分) | 状态 |
|
||||
```
|
||||
抖音榜单/
|
||||
账号周榜/ 抖音周榜_{赛道}_{周一}.json ← 账号影响力周榜(每赛道一文件 × 多期)
|
||||
视频热榜/ 抖音周榜_视频热榜_{周一}.json ← 作品维周聚合 TOP50(全品类单文件/期)
|
||||
点赞榜/ 抖音周榜_点赞榜_{周一}.json ← 作品维七日点赞飙升 TOP50(全品类单文件/期)
|
||||
```
|
||||
|
||||
| 工作台 tab | 类别 | 落盘(子目录 + 文件名) | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1 账号周榜 | 账号影响力周榜 | `抖音周榜_{赛道}_{日期}.json` | ✅ 已实现(旧名保留,dsh 正则兼容) |
|
||||
| 2 视频热榜 | 作品维热榜 | `抖音视频热榜_{赛道}_{日期}.json` | ❌ 预留 |
|
||||
| 3 点赞榜 | 作品维点赞榜 | `抖音点赞榜_{赛道}_{日期}.json` | ❌ 预留 |
|
||||
| 4 涨粉榜 | 账号维涨粉榜 | `抖音涨粉榜_{赛道}_{日期}.json` | ✅ 已实现(2026-09-03 补技能 douyin-rise-ranking) |
|
||||
| 扩展 | 账号日榜/月榜、七日飙升榜、AI信息源等 | 同构命名追加 | ❌ 预留 |
|
||||
| 1 账号周榜 | 账号影响力周榜 | `账号周榜/抖音周榜_{赛道}_{周一}.json` | ✅ 已实现(douyin-top-account 抓取) |
|
||||
| 2 视频热榜 | 作品维周聚合作品榜 | `视频热榜/抖音周榜_视频热榜_{周一}.json` | ✅ 已实现(fetch_week_ranks.py,全品类 TOP50) |
|
||||
| 3 点赞榜 | 作品维七日点赞飙升 | `点赞榜/抖音周榜_点赞榜_{周一}.json` | ✅ 已实现(fetch_week_ranks.py,全品类 TOP50) |
|
||||
| 4 涨粉榜 | 账号维涨粉榜 | 平铺 `抖音涨粉榜_{赛道}_{日期}.json` | ✅ 已实现(douyin-rise-ranking 子技能命名,未接入工作台 tab) |
|
||||
|
||||
> **周粒度抓取器(工作台 视频热榜/点赞榜 数据源,09-03 重构定稿)**:`scripts/fetch_week_ranks.py` —— **不传赛道(type)= 全品类,每榜每期仅 1 次 API 调用**(禁止逐赛道/逐日循环)。hot = likesRank 传 周一~周日(服务端周聚合);likes = hotContentRank **source 必须为「抖音每日点赞飙升榜」生产名**(写「抖音七日点赞飙升榜-GitHub」返回空,09-03 实测修正),startTime 传周日(七日口径)。**★API 单次上限 TOP50**(limit=100 / page=2 / offset=50 均被忽略,09-03 实测),无 51-100 名获取途径,勿再探测。list 条目补 `rank`;顶层 `category:"全部"`(实际赛道在 list[].category,展示端按条目赛道筛选/下拉去重)。用法:`python scripts/fetch_week_ranks.py --board both [--date 2026-08-24] [--force]`。落盘目录解析同工作台(env `MCN_RANKING_DIR` → `.dsh` 部署 `D:\dshworkspace\抖音榜单` → 桌面 `三方数据\红狐数据\抖音榜单`)。**成本铁律(09-03 固化)**:涉及多赛道/多日/多窗口循环的付费调用前,必须先向用户确认调用次数与范围,禁止未经确认直接批量调用。
|
||||
|
||||
> 子技能 douyin-daily-hot(视频热榜)/ douyin-content-surge(点赞榜)日粒度归档沿用各自 SKILL.md 命名;**工作台周粒度文件一律 fetch_week_ranks.py 落盘对应子目录**,避免日/周口径混入。douyin-weekly-surge(官方七日口径)落盘 `抖音七日飙升榜_…` 命名保留,未接入工作台 tab。
|
||||
|
||||
### 查询幂等铁律(防重复花 API 钱)
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
工作台热点数据:视频热榜 / 点赞榜 周粒度抓取器(不传赛道=全品类,一次调用,禁止逐赛道/逐日循环)
|
||||
|
||||
- 视频热榜(board=hot) : likesRank 传 周一~周日 区间,服务端返回该周全品类聚合作品榜 TOP50(一次调用)
|
||||
- 点赞榜(board=likes) : hotContentRank 传 截至周日 startTime,返回七日窗口全品类作品榜 TOP50(一次调用)
|
||||
★ 2026-09-03 实测修正:hotContentRank 的 source 必须为「抖音每日点赞飙升榜」(生产名,无 -GitHub 后缀),
|
||||
写「抖音七日点赞飙升榜-GitHub」会返回空;不带 type 即全品类(50 条,条目 category 齐全)
|
||||
★ API 单次上限 TOP50:limit=100 / page=2 / offset=50 均被忽略(2026-09-03 实测),无 51-100 名途径,勿再探测
|
||||
|
||||
【成本约定 2026-09-03】本脚本每榜每周仅 1 次 API 调用;任何涉及按赛道/多日/多窗口循环的批量调用
|
||||
必须先向用户确认调用次数与范围,禁止未经确认直接执行付费调用(教训固化)。
|
||||
|
||||
落盘(RANKING_DIR 三子目录结构,与账号周榜分目录管理):
|
||||
视频热榜/抖音周榜_视频热榜_{周标号}.json (周标号 = 该周周一,工作台按文件期数切换)
|
||||
点赞榜/ 抖音周榜_点赞榜_{周标号}.json
|
||||
账号周榜(week)由 douyin-top-account 技能落盘到 账号周榜/ 子目录,本脚本不涉及。
|
||||
|
||||
用法:
|
||||
python fetch_week_ranks.py --board both [--date 2026-08-24] [--force]
|
||||
--board hot|likes|both 默认 both
|
||||
--date 周标号(周一日期),默认自动取上一个完整周的周一
|
||||
--force 覆盖已存在文件(默认命中已落盘文件跳过,免重复调用)
|
||||
|
||||
环境变量:
|
||||
REDFOX_API_KEY API 密钥(必填)
|
||||
MCN_RANKING_DIR 显式覆盖落盘目录(可选)
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, date, timedelta
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
LIKES_API = "https://redfox.hk/story/api/dy/search/likesRank"
|
||||
SURGE_API = "https://redfox.hk/story/api/dy/search/hotContentRank"
|
||||
|
||||
# 子目录(与 mcn-work-shop/dsh-data.js rankSubDir 对应)
|
||||
SUB_DIR = {"hot": "视频热榜", "likes": "点赞榜"}
|
||||
# 文件名前缀(与账号周榜 抖音周榜_{赛道}_{周一}.json 区分)
|
||||
FILE_PREFIX = {"hot": "抖音周榜_视频热榜", "likes": "抖音周榜_点赞榜"}
|
||||
# source:hot=likesRank 生产源;likes=hotContentRank 生产源(★无 -GitHub 后缀,2026-09-03 实测修正)
|
||||
SOURCE = {"hot": "抖音每日热门作品榜-GitHub", "likes": "抖音每日点赞飙升榜"}
|
||||
# 顶层 category 标记:全品类单文件(条目自带 category,展示端按条目赛道筛选)
|
||||
CATEGORY_ALL = "全部"
|
||||
|
||||
|
||||
def resolve_ranking_dir():
|
||||
"""与 mcn-work-shop/dsh-data.js resolveRankingDir 同款:env → .dsh 部署 → 桌面 抖音榜单"""
|
||||
env = os.environ.get("MCN_RANKING_DIR", "").strip()
|
||||
if env:
|
||||
return env
|
||||
here = os.path.abspath(__file__)
|
||||
if ".dsh" in here.replace("\\", "/"):
|
||||
return r"D:\dshworkspace\抖音榜单"
|
||||
home = os.path.expanduser("~")
|
||||
rel = os.path.join("MCNSkill项目", "三方数据", "红狐数据", "抖音榜单")
|
||||
candidates = [
|
||||
os.path.join(home, "Desktop", rel),
|
||||
os.path.join(home, "OneDrive", "Desktop", rel),
|
||||
os.path.join(home, "OneDrive", "桌面", rel),
|
||||
]
|
||||
for p in candidates:
|
||||
if os.path.isdir(p):
|
||||
return p
|
||||
return candidates[0]
|
||||
|
||||
|
||||
RANKING_DIR = resolve_ranking_dir()
|
||||
|
||||
|
||||
def get_api_key():
|
||||
key = os.environ.get("REDFOX_API_KEY", "").strip()
|
||||
if not key:
|
||||
print("❌ 未设置 REDFOX_API_KEY 环境变量")
|
||||
sys.exit(1)
|
||||
return key
|
||||
|
||||
|
||||
def call_api(url, body, retries=2):
|
||||
api_key = get_api_key()
|
||||
req = Request(url, data=json.dumps(body).encode("utf-8"), method="POST")
|
||||
req.add_header("X-API-KEY", api_key)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("Accept", "application/json")
|
||||
last = None
|
||||
for i in range(retries + 1):
|
||||
try:
|
||||
with urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
if data.get("code") == 2000:
|
||||
return data.get("data", [])
|
||||
print(f" ⚠ API code={data.get('code')} msg={data.get('msg', '')}")
|
||||
return []
|
||||
except HTTPError as e:
|
||||
last = f"HTTP {e.code}"
|
||||
if e.code in (429, 500, 502, 503):
|
||||
time.sleep(2 * (i + 1))
|
||||
continue
|
||||
break
|
||||
except URLError as e:
|
||||
last = f"net {e.reason}"
|
||||
time.sleep(2 * (i + 1))
|
||||
except Exception as e:
|
||||
last = str(e)[:120]
|
||||
break
|
||||
print(f" ❌ 调用失败: {last}")
|
||||
return []
|
||||
|
||||
|
||||
def as_list(data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict):
|
||||
for k in ("list", "items", "dailyRank", "weeklyRank"):
|
||||
v = data.get(k)
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return []
|
||||
|
||||
|
||||
def fetch_hot_week(monday, sunday):
|
||||
"""视频热榜周窗口(全品类,不传 type):likesRank 周一~周日,服务端周聚合一次调用"""
|
||||
body = {
|
||||
"source": SOURCE["hot"],
|
||||
"startTime": str(monday),
|
||||
"endTime": str(sunday),
|
||||
}
|
||||
return as_list(call_api(LIKES_API, body))
|
||||
|
||||
|
||||
def fetch_likes_week(sunday):
|
||||
"""点赞榜周窗口(全品类,不传 type):hotContentRank 截至周日,服务端七日窗口一次调用"""
|
||||
body = {
|
||||
"source": SOURCE["likes"],
|
||||
"startTime": str(sunday),
|
||||
}
|
||||
return as_list(call_api(SURGE_API, body))
|
||||
|
||||
|
||||
def save_board_file(board, monday, sunday, items):
|
||||
sub = os.path.join(RANKING_DIR, SUB_DIR[board])
|
||||
fname = f"{FILE_PREFIX[board]}_{monday}.json"
|
||||
fpath = os.path.join(sub, fname)
|
||||
doc = {
|
||||
"period": "week",
|
||||
"board": "video_hot" if board == "hot" else "like_surge",
|
||||
"date": str(monday), # 周标号(周一),与账号周榜一致,工作台按此切换
|
||||
"dateStart": str(monday),
|
||||
"dateEnd": str(sunday),
|
||||
"category": CATEGORY_ALL, # 全品类:条目自带 category,展示端按条目赛道筛选
|
||||
"total": len(items),
|
||||
"fetched_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"list": [{**it, "rank": i + 1} for i, it in enumerate(items)],
|
||||
}
|
||||
os.makedirs(sub, exist_ok=True)
|
||||
with open(fpath, "w", encoding="utf-8") as f:
|
||||
json.dump(doc, f, ensure_ascii=False, indent=2)
|
||||
return fpath, len(items)
|
||||
|
||||
|
||||
def last_full_week(today=None):
|
||||
"""上一个完整周的周一/周日"""
|
||||
today = today or date.today()
|
||||
this_monday = today - timedelta(days=today.weekday())
|
||||
last_monday = this_monday - timedelta(days=7)
|
||||
return last_monday, last_monday + timedelta(days=6)
|
||||
|
||||
|
||||
def _file_valid(fpath):
|
||||
"""文件存在且为合法非空榜单 JSON 才算命中(防中断/写坏残留被误判跳过)"""
|
||||
try:
|
||||
if os.path.getsize(fpath) < 200:
|
||||
return False
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
return isinstance(d, dict) and isinstance(d.get("list"), list) and len(d["list"]) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def run_board(board, monday, sunday, force):
|
||||
label = "视频热榜" if board == "hot" else "点赞榜"
|
||||
sub = os.path.join(RANKING_DIR, SUB_DIR[board])
|
||||
fname = f"{FILE_PREFIX[board]}_{monday}.json"
|
||||
fpath = os.path.join(sub, fname)
|
||||
if os.path.exists(fpath) and not force and _file_valid(fpath):
|
||||
print(f"=== {label} 周榜(全品类)已存在跳过: {os.path.join(SUB_DIR[board], fname)} ===")
|
||||
return
|
||||
print(f"=== {label} 周榜(全品类,不传赛道 1 次调用,周 {monday} ~ {sunday})===")
|
||||
items = fetch_hot_week(monday, sunday) if board == "hot" else fetch_likes_week(sunday)
|
||||
if not items:
|
||||
print(" ❌ 空数据(检查 source 是否为生产名 / 该周是否有数据)")
|
||||
return
|
||||
saved, n = save_board_file(board, monday, sunday, items)
|
||||
cats = sorted({(it.get("category") or "?").strip() for it in items})
|
||||
top = (items[0].get("title") or items[0].get("aweme_desc") or "-")[:20]
|
||||
print(f" ✅ {n} 条 → {os.path.join(SUB_DIR[board], fname)} top1: {top}")
|
||||
print(f" 赛道覆盖({len(cats)}): {'/'.join(cats)}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="视频热榜/点赞榜 周粒度全品类抓取(不传赛道,一次调用周窗口,禁止逐赛道/逐日循环)")
|
||||
ap.add_argument("--board", choices=["hot", "likes", "both"], default="both")
|
||||
ap.add_argument("--date", help="周标号(周一 YYYY-MM-DD),默认上一个完整周")
|
||||
ap.add_argument("--force", action="store_true", help="覆盖已存在文件")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.date:
|
||||
monday = datetime.strptime(args.date, "%Y-%m-%d").date()
|
||||
sunday = monday + timedelta(days=6)
|
||||
else:
|
||||
monday, sunday = last_full_week()
|
||||
print(f"落盘目录: {RANKING_DIR}({SUB_DIR['hot']}/{SUB_DIR['likes']} 子目录)")
|
||||
print(f"周窗口: {monday} ~ {sunday}(文件周标号 {monday})")
|
||||
if args.board in ("hot", "both"):
|
||||
run_board("hot", monday, sunday, args.force)
|
||||
if args.board in ("likes", "both"):
|
||||
run_board("likes", monday, sunday, args.force)
|
||||
print("\n全部完成 ✅(本次共 2 次 API 调用上限:hot 1 + likes 1)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -50,6 +50,6 @@ description: 抖音每日点赞飙升榜查询工具。日度收录全平台抖
|
||||
## 安全约束
|
||||
|
||||
- **API Key**:仅通过 `os.getenv("REDFOX_API_KEY")` 获取,禁止读取系统文件
|
||||
- **文件操作**:对话回复仅输出 Markdown 表格(不暴露原始 JSON);**榜单数据落盘到业务数据目录**(白名单例外,详则见主 [SKILL.md](../../SKILL.md)「榜单数据落盘规范」)—— `抖音点赞榜_{赛道}_{数据日期}.json`,目标 `桌面\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(①③环境)或 `D:\dshworkspace\抖音榜单\`(②dsh 环境)。**查询前先 `ls` 本地缓存命中免 API**(同榜名+赛道+数据日期全机器只调一次 API)
|
||||
- **文件操作**:对话回复仅输出 Markdown 表格(不暴露原始 JSON);**榜单数据落盘到业务数据目录**(白名单例外,详则见主 [SKILL.md](../../SKILL.md)「榜单数据落盘规范」)——日粒度归档命名 `抖音点赞榜_{赛道}_{数据日期}.json`,目标 `桌面\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(①③环境)或 `D:\dshworkspace\抖音榜单\`(②dsh 环境)。**查询前先 `ls` 本地缓存命中免 API**(同榜名+赛道+数据日期全机器只调一次 API)。工作台「点赞榜」周粒度文件(`抖音周榜_点赞榜_{赛道}_{周一}.json`)由主技能 `scripts/fetch_week_ranks.py` 统一落盘,本技能不做周粒度输出。
|
||||
- **网络请求**:脚本使用 `urllib.request`(标准库),禁止调用外部脚本或 subprocess
|
||||
- **输出内容**:仅输出 Markdown 表格格式榜单,不暴露原始 JSON
|
||||
@@ -10,7 +10,7 @@ description: 抖音每日最热作品榜,日度收录全平台抖音作品,
|
||||
1. **路径沙箱**:文件操作限定在技能根目录内
|
||||
2. **API Key**:仅 `os.getenv("REDFOX_API_KEY")`,禁止 .env/dotenv/配置文件读取
|
||||
3. **无系统命令**:不调用 subprocess/eval/exec/system()
|
||||
4. **输出限定**:仅 stdout(Markdown 表格对话回复);**榜单数据同时落盘到业务数据目录**(白名单例外,详则见主 [SKILL.md](../../SKILL.md) 「榜单数据落盘规范」)——命名:`抖音视频热榜_{赛道}_{数据日期}.json`,目标 `桌面\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(①③环境)或 `D:\dshworkspace\抖音榜单\`(②dsh 环境)。**查询前先 `ls` 本地缓存命中免 API**(同榜名+赛道+数据日期全机器只调一次 API)
|
||||
4. **输出限定**:仅 stdout(Markdown 表格对话回复);**榜单数据同时落盘到业务数据目录**(白名单例外,详则见主 [SKILL.md](../../SKILL.md) 「榜单数据落盘规范」)——日粒度归档命名:`抖音视频热榜_{赛道}_{数据日期}.json`,目标 `桌面\MCNSkill项目\三方数据\红狐数据\抖音榜单\`(①③环境)或 `D:\dshworkspace\抖音榜单\`(②dsh 环境)。**查询前先 `ls` 本地缓存命中免 API**(同榜名+赛道+数据日期全机器只调一次 API)。工作台「视频热榜」周粒度文件(`抖音周榜_视频热榜_{赛道}_{周一}.json`)由主技能 `scripts/fetch_week_ranks.py` 统一落盘,本技能不做周粒度输出。
|
||||
|
||||
## 执行流程
|
||||
|
||||
|
||||
Reference in new issue
Block a user