feat(monitor): 评论接口带上 a_bogus 签名;修好作品导出的空列
**评论能采了。** 之前 comment/list 一直回 200 + 空 body,被读成「这条没评论」—— 而它其实只是被网关挡了。缺的就是 a_bogus 签名,仓库里本来就有 (libs/douyin.js + execjs)。签上之后实测 200 / 9960 字节真评论。 只给评论接口签:作品、详情、博主资料三个不带签名也照常返回,而给它们加签名是 没验证过的改动。签名按需 import —— 那个模块 import 时就把 JS 喂给 execjs, 不该拖进监控层热路径。 **作品导出那几列一直是空的。** 列名写的是裸键 liked_count,而作品行的指标嵌在 metrics / deltas 里,row.get() 永远取到 None —— 导出来的表有「点赞/评论/收藏/ 分享」四列,每一格都没有数。原来的测试只断言了「作品ID」,所以没发现。 顺手补上:导出带上 博主备注/昵称、作品备注、发布时间,时间戳格式化成人能读的 形态(原来是一串 13 位毫秒,Excel 里没法看也没法排序)。
This commit is contained in:
+55
-10
@@ -18,7 +18,7 @@
|
||||
|
||||
"""HTTP API for scheduled monitoring tasks."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Response
|
||||
@@ -486,20 +486,29 @@ def _export_columns(kind: str) -> List[tuple[str, str]]:
|
||||
"""(key, header) pairs per export kind."""
|
||||
if kind == "notes":
|
||||
return [
|
||||
("note_id", "作品ID"),
|
||||
# 先放「这是谁」:导出来是拿去比对和汇报的,一行只有作品 ID 没法用。
|
||||
# 备注优先 —— 昵称常常认不出是谁(见 notes 表那一层的说明)。
|
||||
("creator_alias", "博主备注"),
|
||||
("creator_name", "博主昵称"),
|
||||
("note_alias", "作品备注"),
|
||||
("title", "标题"),
|
||||
("note_id", "作品ID"),
|
||||
("note_url", "链接"),
|
||||
("liked_count", "点赞"),
|
||||
("comment_count", "评论"),
|
||||
("collected_count", "收藏"),
|
||||
("share_count", "分享"),
|
||||
("liked_count_delta", "点赞增量"),
|
||||
("comment_count_delta", "评论增量"),
|
||||
("published_at", "发布时间"),
|
||||
# 指标嵌在 row["metrics"] 里,所以这里必须写成路径 —— 写成裸键名的话这几列
|
||||
# 全空(见 _lookup)。
|
||||
("metrics.liked_count", "点赞"),
|
||||
("metrics.comment_count", "评论"),
|
||||
("metrics.collected_count", "收藏"),
|
||||
("metrics.share_count", "分享"),
|
||||
("deltas.liked_count", "点赞增量"),
|
||||
("deltas.comment_count", "评论增量"),
|
||||
("first_seen_at", "首次发现"),
|
||||
("last_seen_at", "最近采集"),
|
||||
]
|
||||
if kind == "comments":
|
||||
return [
|
||||
("note_creator_name", "博主昵称"),
|
||||
("note_title", "所属作品"),
|
||||
("note_id", "作品ID"),
|
||||
("comment_id", "评论ID"),
|
||||
@@ -521,6 +530,42 @@ def _export_columns(kind: str) -> List[tuple[str, str]]:
|
||||
]
|
||||
|
||||
|
||||
# 表里存的是毫秒时间戳。直接倒进 CSV 就是一串 13 位数字 —— 打开 Excel 的人没法看,
|
||||
# 也没法排序。这几个键统一格式化成人能读的形态。
|
||||
_TIME_KEYS = {"published_at", "first_seen_at", "last_seen_at", "create_time"}
|
||||
|
||||
|
||||
def _fmt_time(value: Any) -> str:
|
||||
"""毫秒 → ``YYYY-MM-DD HH:MM``(服务器本地时区)。"""
|
||||
try:
|
||||
return datetime.fromtimestamp(int(value) / 1000).strftime("%Y-%m-%d %H:%M")
|
||||
except (TypeError, ValueError, OSError, OverflowError):
|
||||
return ""
|
||||
|
||||
|
||||
def _cell_for(row: Dict[str, Any], key: str) -> Any:
|
||||
"""一列的值:时间键格式化成人能读的,其余照原样(None 变空串)。"""
|
||||
value = _lookup(row, key)
|
||||
if key.rsplit(".", 1)[-1] in _TIME_KEYS:
|
||||
return _fmt_time(value)
|
||||
return _cell(value)
|
||||
|
||||
|
||||
def _lookup(row: Dict[str, Any], key: str) -> Any:
|
||||
"""取一列的值。键可以是 ``metrics.liked_count`` 这种路径。
|
||||
|
||||
作品行的指标是**嵌在** ``metrics`` / ``deltas`` 里的,而 ``_export_columns`` 里写的
|
||||
是 ``liked_count`` —— 照顶层键直接 ``row.get()`` 的话,点赞/评论/收藏/分享四列连带
|
||||
两个增量列**永远是空的**,导出来的表看着有这几列,其实一格都没有。
|
||||
"""
|
||||
value: Any = row
|
||||
for part in key.split("."):
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
value = value.get(part)
|
||||
return value
|
||||
|
||||
|
||||
def _cell(value: Any) -> Any:
|
||||
if value is None:
|
||||
return ""
|
||||
@@ -537,7 +582,7 @@ def _to_csv(rows: List[Dict[str, Any]], columns: List[tuple[str, str]]) -> bytes
|
||||
writer = csv.writer(buffer)
|
||||
writer.writerow([header for _, header in columns])
|
||||
for row in rows:
|
||||
writer.writerow([_cell(row.get(key)) for key, _ in columns])
|
||||
writer.writerow([_cell_for(row, key) for key, _ in columns])
|
||||
|
||||
# utf-8-sig: without the BOM Excel opens Chinese CSV as mojibake, which is
|
||||
# the single most common complaint about CSV exports here.
|
||||
@@ -554,7 +599,7 @@ def _to_xlsx(rows: List[Dict[str, Any]], columns: List[tuple[str, str]], sheet:
|
||||
worksheet.title = {"notes": "作品", "comments": "评论"}.get(sheet, "报表")
|
||||
worksheet.append([header for _, header in columns])
|
||||
for row in rows:
|
||||
worksheet.append([_cell(row.get(key)) for key, _ in columns])
|
||||
worksheet.append([_cell_for(row, key) for key, _ in columns])
|
||||
|
||||
output = io.BytesIO()
|
||||
workbook.save(output)
|
||||
|
||||
Reference in New Issue
Block a user