| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- import csv
- from collections import Counter, defaultdict
- from pathlib import Path
- from openpyxl import Workbook
- from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
- ROOT = Path(__file__).resolve().parents[1]
- OUTPUT_ROOT = ROOT / "outputs" / "overnight-quality-1780682025651"
- SOURCE = OUTPUT_ROOT / "manual-review-sample.csv"
- CSV_OUT = OUTPUT_ROOT / "software-client-list-dedup.csv"
- XLSX_OUT = OUTPUT_ROOT / "software-client-list-dedup.xlsx"
- SUMMARY_OUT = OUTPUT_ROOT / "software-client-list-dedup-summary.md"
- HEADERS = [
- "brief编号",
- "策略",
- "排名",
- "平台",
- "博主名称",
- "综合分",
- "brief匹配分",
- "参考风格分",
- "证据加分",
- "证据风险扣分",
- "推荐理由",
- "风险提示",
- "主页链接",
- "人工复核标签",
- ]
- STRATEGY_PRIORITY = {
- "result-first-broad": 50,
- "result-first": 40,
- "video-enhanced": 30,
- "result-first-risk": 20,
- "baseline-live": 10,
- }
- def to_num(row, key):
- try:
- return float(str(row.get(key, "")).strip() or 0)
- except ValueError:
- return 0.0
- def norm(text):
- return " ".join(str(text or "").strip().lower().split())
- def dedupe_key(row):
- link = norm(row.get("主页链接"))
- if link:
- return (norm(row.get("brief编号")), norm(row.get("平台")), link)
- return (norm(row.get("brief编号")), norm(row.get("平台")), norm(row.get("博主名称")))
- def quality_key(row):
- return (
- STRATEGY_PRIORITY.get(row.get("策略"), 0),
- to_num(row, "综合分"),
- to_num(row, "brief匹配分"),
- to_num(row, "参考风格分"),
- to_num(row, "证据加分"),
- -to_num(row, "证据风险扣分"),
- -to_num(row, "排名"),
- )
- def load_rows():
- with SOURCE.open("r", encoding="utf-8-sig", newline="") as handle:
- rows = list(csv.DictReader(handle))
- for row in rows:
- for header in HEADERS:
- row.setdefault(header, "")
- return rows
- def dedupe_rows(rows):
- kept = {}
- all_by_key = defaultdict(list)
- for row in rows:
- key = dedupe_key(row)
- all_by_key[key].append(row)
- if key not in kept or quality_key(row) > quality_key(kept[key]):
- kept[key] = dict(row)
- unique_rows = list(kept.values())
- unique_rows.sort(
- key=lambda row: (
- row.get("brief编号", ""),
- -to_num(row, "综合分"),
- -to_num(row, "brief匹配分"),
- -to_num(row, "参考风格分"),
- -to_num(row, "证据加分"),
- to_num(row, "证据风险扣分"),
- row.get("平台", ""),
- row.get("博主名称", ""),
- )
- )
- rank_by_brief = defaultdict(int)
- for row in unique_rows:
- brief = row.get("brief编号", "")
- rank_by_brief[brief] += 1
- row["排名"] = str(rank_by_brief[brief])
- row["人工复核标签"] = ""
- return unique_rows, all_by_key
- def write_csv(rows):
- with CSV_OUT.open("w", encoding="utf-8-sig", newline="") as handle:
- writer = csv.DictWriter(handle, fieldnames=HEADERS, extrasaction="ignore")
- writer.writeheader()
- writer.writerows(rows)
- def write_xlsx(rows, original_count, duplicate_key_count):
- wb = Workbook()
- ws = wb.active
- ws.title = "去重名单"
- ws.append(HEADERS)
- for row in rows:
- ws.append([row.get(header, "") for header in HEADERS])
- header_fill = PatternFill("solid", fgColor="1F4E78")
- header_font = Font(color="FFFFFF", bold=True)
- thin = Side(style="thin", color="D9E2F3")
- for cell in ws[1]:
- cell.fill = header_fill
- cell.font = header_font
- cell.alignment = Alignment(horizontal="center", vertical="center")
- cell.border = Border(bottom=thin)
- widths = {
- "A": 20,
- "B": 18,
- "C": 8,
- "D": 14,
- "E": 24,
- "F": 10,
- "G": 12,
- "H": 12,
- "I": 10,
- "J": 12,
- "K": 110,
- "L": 85,
- "M": 56,
- "N": 16,
- }
- for col, width in widths.items():
- ws.column_dimensions[col].width = width
- for row in ws.iter_rows(min_row=2):
- for cell in row:
- cell.alignment = Alignment(vertical="top", wrap_text=True)
- for idx in [3, 6, 7, 8, 9, 10]:
- row[idx - 1].alignment = Alignment(horizontal="center", vertical="top", wrap_text=True)
- ws.freeze_panes = "A2"
- ws.auto_filter.ref = ws.dimensions
- summary = wb.create_sheet("去重说明")
- summary_rows = [
- ["项目", "值"],
- ["来源文件", str(SOURCE)],
- ["原始行数", original_count],
- ["去重后行数", len(rows)],
- ["删除重复行数", original_count - len(rows)],
- ["存在重复的博主键数量", duplicate_key_count],
- ["去重口径", "同一 brief编号 + 平台 + 主页链接;没有主页链接时使用 brief编号 + 平台 + 博主名称"],
- ["保留规则", "优先 result-first-broad,其次 result-first、video-enhanced、result-first-risk、baseline-live;同优先级下保留综合分/brief匹配/参考风格/证据加分更高且风险扣分更低的一条"],
- ["排名规则", "去重后按每个 brief 重新从 1 排名"],
- ]
- for item in summary_rows:
- summary.append(item)
- summary.column_dimensions["A"].width = 22
- summary.column_dimensions["B"].width = 130
- for cell in summary[1]:
- cell.fill = header_fill
- cell.font = header_font
- for row in summary.iter_rows():
- for cell in row:
- cell.alignment = Alignment(vertical="top", wrap_text=True)
- wb.save(XLSX_OUT)
- def write_summary(rows, original_count, duplicate_key_count):
- brief_counts = Counter(row["brief编号"] for row in rows)
- strategy_counts = Counter(row["策略"] for row in rows)
- lines = [
- "# 软件端去重名单导出说明",
- "",
- f"- 来源:`{SOURCE}`",
- f"- 原始行数:{original_count}",
- f"- 去重后行数:{len(rows)}",
- f"- 删除重复行数:{original_count - len(rows)}",
- f"- 存在重复的博主键数量:{duplicate_key_count}",
- f"- CSV:`{CSV_OUT}`",
- f"- XLSX:`{XLSX_OUT}`",
- "",
- "## 各 brief 行数",
- "",
- ]
- for brief, count in sorted(brief_counts.items()):
- lines.append(f"- {brief}: {count}")
- lines.extend(["", "## 保留策略分布", ""])
- for strategy, count in sorted(strategy_counts.items()):
- lines.append(f"- {strategy}: {count}")
- lines.extend(
- [
- "",
- "## 去重与排序口径",
- "",
- "- 去重口径:同一 `brief编号 + 平台 + 主页链接` 视为同一博主;没有主页链接时用 `brief编号 + 平台 + 博主名称`。",
- "- 保留规则:优先发布策略 `result-first-broad`、`result-first`,再保留诊断策略;同优先级下看综合分、brief 匹配、参考风格、证据加分和风险扣分。",
- "- 排名规则:去重后按每个 brief 重新排序。",
- ]
- )
- SUMMARY_OUT.write_text("\n".join(lines), encoding="utf-8-sig")
- def main():
- rows = load_rows()
- unique_rows, all_by_key = dedupe_rows(rows)
- duplicate_key_count = sum(1 for values in all_by_key.values() if len(values) > 1)
- write_csv(unique_rows)
- write_xlsx(unique_rows, len(rows), duplicate_key_count)
- write_summary(unique_rows, len(rows), duplicate_key_count)
- print(f"rows_in={len(rows)}")
- print(f"rows_out={len(unique_rows)}")
- print(f"dropped={len(rows) - len(unique_rows)}")
- print(f"duplicate_keys={duplicate_key_count}")
- print(f"csv={CSV_OUT}")
- print(f"xlsx={XLSX_OUT}")
- print(f"summary={SUMMARY_OUT}")
- if __name__ == "__main__":
- main()
|