fix: 修复 dedup 高重复率返回码被掩蔽的问题 (P1-1)

- run_dedup: 生产模式重复率仅作 WARNING 告警,不影响退出码(执行成功即 0)
- run_dedup: 新增 --strict 验收模式(重复率 > 5% 返回 1),保留 M3 验收门槛
- run_dedup: 新增统计快照 data/deduped/{date}/stats.json(原子写)
- run_dedup: 顺带修复空日场景 sources.json 写入 FileNotFoundError
- pipeline: 移除 dedup rc=1 特判,恢复'非 0 即失败'统一语义
- 新增 tests/test_run_dedup.py 7 个测试;全量 266 passed
This commit is contained in:
2026-08-22 19:50:14 +08:00
parent 85ba218f2f
commit 8fa27ad65b
4 changed files with 242 additions and 10 deletions
+45 -4
View File
@@ -22,7 +22,7 @@ import argparse
import json
import sys
from collections import Counter
from datetime import date
from datetime import date, datetime
from pathlib import Path
from loguru import logger
@@ -31,6 +31,9 @@ from pydantic import ValidationError
from dedup import Deduper
from extractor import Article
# 重复率告警阈值(超过则输出 WARNING 日志;--strict 验收模式下作为返回码门槛)
_DUP_RATE_THRESHOLD = 0.05
def _setup_logger(level: str) -> None:
logger.remove()
@@ -200,6 +203,11 @@ def main() -> int:
parser.add_argument("--simhash-threshold", type=int, default=3)
parser.add_argument("--window-days", type=int, default=30)
parser.add_argument("--reset", action="store_true", help="处理前清空指纹库")
parser.add_argument(
"--strict", action="store_true",
help="验收模式:重复率超过阈值(5%)时返回 1。"
"默认为生产模式,重复率仅作统计告警,不影响退出码",
)
parser.add_argument("--log-level", default="INFO")
args = parser.parse_args()
@@ -243,6 +251,7 @@ def main() -> int:
# {url_hash: [source_id, ...]},配合 uniques/{url_hash}.json 的 sources 字段
# 与指纹库 source_ids 列,提供「一条唯一新闻多个来源」的完整记录。
sources_path = out_root / args.date / "sources.json"
sources_path.parent.mkdir(parents=True, exist_ok=True)
sources_path.write_text(
json.dumps(
{k: v for k, v in sources_map.items() if v},
@@ -260,15 +269,47 @@ def main() -> int:
total = total_uniq + total_dup
rate = total_dup / max(total, 1)
logger.info(
log_fn = logger.warning if rate > _DUP_RATE_THRESHOLD else logger.info
log_fn(
"全部完成: 唯一 {} / 重复 {} (重复率 {:.1%}) layers={}",
total_uniq,
total_dup,
rate,
dict(total_layers),
)
# 验收门槛: ≤ 5%
return 0 if rate <= 0.05 or total == 0 else 1
# 统计快照: data/deduped/{date}/stats.json(原子写),供运维排查与监控
stats_payload = {
"date": args.date,
"generated_at": datetime.now().isoformat(),
"unique": total_uniq,
"duplicates": total_dup,
"total": total,
"dup_rate": round(rate, 4),
"dup_rate_threshold": _DUP_RATE_THRESHOLD,
"layers": dict(total_layers),
"fingerprint_total": deduper.stats().total,
}
stats_path = out_root / args.date / "stats.json"
stats_path.parent.mkdir(parents=True, exist_ok=True)
tmp = stats_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(stats_payload, ensure_ascii=False, indent=2),
encoding="utf-8")
tmp.replace(stats_path)
if rate > _DUP_RATE_THRESHOLD and total > 0:
logger.warning(
"重复率 {:.1%} 超过阈值 {:.0%}(唯一 {},重复 {})。"
"同日多次调度/跨源转载属常见现象;若为当日首次处理仍异常偏高,"
"请检查指纹库与抓取源(详见 {})",
rate, _DUP_RATE_THRESHOLD, total_uniq, total_dup, stats_path,
)
# 生产模式:重复率是统计指标,不影响退出码(执行成功即 0);
# 验收模式(--strict):保留 M3 验收门槛(重复率 ≤ 5%),超标返回 1。
if args.strict and total > 0 and rate > _DUP_RATE_THRESHOLD:
return 1
return 0
if __name__ == "__main__":