feat(monitor): 作品导出里,没有作品的博主也占一行
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

导出原先是从作品摊出来的,于是「这个号这段时间一条作品都没发」直接从表里消失 ——
而在表里,**消失和「一切正常」长得一模一样**。这类博主恰恰是名单上最该被看见的:
还在涨粉、只是没动静,或者号出了问题。

来源换成 博主 ∪ 作品(和作品栏同一份 list_creators),没有作品的那一行只填博主,
作品那几列留空 —— **空,不是 0**。

顺带把账号级指标加进作品导出(粉丝数 / 总获赞 / 主页作品数):没有作品的博主那一行
就只剩它们,不加的话那一行等于空行。「主页作品数」是平台上的总数,和下面几列(我们
监控到的作品)不是一回事,所以名字分得开。

排序改成按「这是谁」,同一个博主的行挨在一起 —— 表格是拿去筛和做透视的。
This commit is contained in:
2026-10-11 07:54:26 +08:00
parent 61808444ad
commit a42f8e8b2f
2 changed files with 140 additions and 1 deletions
+61 -1
View File
@@ -439,7 +439,10 @@ async def export_data(
if kind == "notes":
single_task = scoped[0] if scoped and len(scoped) == 1 else None
rows = await service.list_notes(session, single_task, False, 5000, platform)
rows = _notes_rows(
await service.list_creators(session, single_task, platform),
await service.list_notes(session, single_task, False, 5000, platform),
)
elif kind == "comments":
single_task = scoped[0] if scoped and len(scoped) == 1 else None
rows = await service.list_comments(session, single_task, note_id, 5000, platform)
@@ -482,6 +485,57 @@ async def export_data(
)
def _notes_rows(
creators: List[Dict[str, Any]], notes: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""博主 ∪ 作品,摊成一行行 —— **没有作品的博主也要占一行**,作品那几列留空。
导出是拿去比对名单的,而「这个号这段时间一条作品都没发」恰恰是名单上最该被看见的
一行:还在涨粉、只是没动静,或者号出了问题。按作品导的话他直接消失 —— 而在表里,
**消失和「一切正常」长得一模一样**。
和作品栏用的是同一个来源(`list_creators` 也是 账号快照 ∪ 作品),所以导出来的
名单和屏幕上看到的博主是同一批。
"""
works_by_creator: Dict[str, List[Dict[str, Any]]] = {}
for note in notes:
works_by_creator.setdefault(note["creator_hash"], []).append(note)
rows: List[Dict[str, Any]] = []
listed: set = set()
for creator in creators:
creator_hash = creator["creator_hash"]
listed.add(creator_hash)
works = works_by_creator.get(creator_hash, [])
if works:
# 作品行自己就带着博主那几列(list_notes 里已经拼好了),直接用。
rows.extend(works)
continue
# 一行只填博主:作品列全是空串(`_cell` 把缺失的键变成空)。
rows.append(
{
"creator_alias": creator["creator_alias"],
"creator_name": creator["creator_name"],
"creator_fans": creator["creator_fans"],
"creator_total_favorited": creator["creator_total_favorited"],
"creator_works": creator["creator_works"],
}
)
# 兜底:作品里的博主没出现在博主名单里就不该丢。正常情况下不会发生(名单是
# 账号快照 ∪ 作品),但「悄悄少几行」是这份导出最不该犯的错。
rows.extend(note for note in notes if note["creator_hash"] not in listed)
# 同一个博主的行挨在一起,按「这是谁」排 —— 表格是拿去筛和做透视的。
rows.sort(
key=lambda row: (
row.get("creator_alias") or row.get("creator_name") or "",
-(row.get("published_at") or 0),
)
)
return rows
def _export_columns(kind: str) -> List[tuple[str, str]]:
"""(key, header) pairs per export kind."""
if kind == "notes":
@@ -490,6 +544,12 @@ def _export_columns(kind: str) -> List[tuple[str, str]]:
# 备注优先 —— 昵称常常认不出是谁(见 notes 表那一层的说明)。
("creator_alias", "博主备注"),
("creator_name", "博主昵称"),
# 账号级指标 —— 没有作品的博主那一行**只剩它们**,没有的话那一行等于空行。
# 「主页作品数」是他在平台上的总数,和下面那几列(我们监控到的作品)不是
# 一回事,所以名字要分得开。
("creator_fans", "粉丝数"),
("creator_total_favorited", "总获赞"),
("creator_works", "主页作品数"),
("note_alias", "作品备注"),
("title", "标题"),
("note_id", "作品ID"),