import argparse
|
import csv
|
import hashlib
|
import re
|
from collections import Counter
|
from datetime import datetime, timezone, timedelta
|
from pathlib import Path
|
|
|
CN_TZ = timezone(timedelta(hours=8))
|
|
|
TRIGGERS = [
|
("推荐关注", "broker_recommend_attention"),
|
("建议关注", "broker_suggest_attention"),
|
("相关标的", "broker_related_targets"),
|
]
|
|
|
def sha256_file(path: Path) -> str:
|
h = hashlib.sha256()
|
with path.open("rb") as f:
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
h.update(chunk)
|
return h.hexdigest()
|
|
|
def normalize(text: str) -> str:
|
return re.sub(r"\s+", "", text or "")
|
|
|
def classify(context: str):
|
normalized = normalize(context)
|
hits = [label for phrase, label in TRIGGERS if phrase in normalized]
|
return ";".join(hits)
|
|
|
def parse_table_row(line: str):
|
if not line.startswith("|") or line.startswith("|---"):
|
return None
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
if len(cells) < 4:
|
return None
|
return cells
|
|
|
def build(view_path: Path, ledger_path: Path, manifest_path: Path, summary_path: Path, run_id: str):
|
created_at = datetime.now(CN_TZ).isoformat(timespec="seconds")
|
lines = view_path.read_text(encoding="utf-8").splitlines()
|
in_company_table = False
|
ledger_rows = []
|
new_lines = []
|
replaced = 0
|
|
for line in lines:
|
if line.startswith("| 公司名片段 | 提及次数 | 样例定位 | 样例上下文 |"):
|
in_company_table = True
|
new_lines.append(line)
|
continue
|
if in_company_table and line.startswith("## ") and not line.startswith("## 公司提及频次"):
|
in_company_table = False
|
|
if in_company_table:
|
cells = parse_table_row(line)
|
if cells and cells[0] not in ("公司名片段", "---"):
|
phrase_type = classify(cells[3])
|
if phrase_type:
|
replaced += 1
|
row_id = f"YS-COMPANY-BROKER-PHRASE-016-{replaced:04d}"
|
display_context = (
|
"BROKER_ORIGINAL_VIEW_SEGMENTED:该样例包含券商原文关注或标的列举表达,"
|
"已从公司主读法分账;正式公司证据卡不得引用为分析员结论。"
|
f"原始定位:{cells[2]}"
|
)
|
ledger_rows.append(
|
{
|
"broker_phrase_split_id": row_id,
|
"case_id": "ANA-YS-INDUSTRY-001",
|
"batch_id": "BATCH-003",
|
"run_id": run_id,
|
"company_name_fragment": cells[0],
|
"mention_count": cells[1],
|
"sample_location": cells[2],
|
"phrase_type": phrase_type,
|
"original_context": cells[3],
|
"display_context": display_context,
|
"evidence_status": "BROKER_ORIGINAL_VIEW_SEGMENTED",
|
"review_status": "DRAFT_FOR_REVIEW",
|
"created_at": created_at,
|
}
|
)
|
cells[3] = display_context
|
new_lines.append("| " + " | ".join(cells) + " |")
|
continue
|
new_lines.append(line)
|
|
section = [
|
"",
|
"## 券商原文观点分账",
|
"",
|
f"PASS-016 已将公司提及频次表中 {replaced} 条含“推荐关注/建议关注/相关标的”等券商原文表达的样例上下文从主展示降噪,统一标记为 `BROKER_ORIGINAL_VIEW_SEGMENTED`。",
|
"",
|
"这些片段只保留为研报原文观点或公司提及线索,不得进入分析员结论字段、公司投资读法、交易指令或收益承诺。原始片段、定位和分账类型见:`ana-data/cases/有色案例/evidence/company_view_broker_phrase_split_pass016.csv`。",
|
]
|
if "## 券商原文观点分账" not in "\n".join(new_lines):
|
for i, line in enumerate(new_lines):
|
if line.startswith("## 后续公司证据卡字段"):
|
new_lines = new_lines[:i] + section + [""] + new_lines[i:]
|
break
|
|
for i, line in enumerate(new_lines):
|
if line.startswith("更新时间:"):
|
new_lines[i] = "更新时间:2026-06-26T07:43:00+08:00"
|
break
|
|
view_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
|
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
fieldnames = [
|
"broker_phrase_split_id",
|
"case_id",
|
"batch_id",
|
"run_id",
|
"company_name_fragment",
|
"mention_count",
|
"sample_location",
|
"phrase_type",
|
"original_context",
|
"display_context",
|
"evidence_status",
|
"review_status",
|
"created_at",
|
]
|
with ledger_path.open("w", encoding="utf-8-sig", newline="") as f:
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
writer.writeheader()
|
writer.writerows(ledger_rows)
|
|
digest = sha256_file(ledger_path)
|
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
manifest_rows = [
|
{
|
"case_id": "ANA-YS-INDUSTRY-001",
|
"batch_id": "BATCH-003",
|
"run_id": run_id,
|
"artifact_type": "company_view_broker_phrase_split",
|
"artifact_path": ledger_path.as_posix(),
|
"row_count": str(len(ledger_rows)),
|
"sha256": digest,
|
"review_status": "DRAFT_FOR_REVIEW",
|
"created_at": created_at,
|
}
|
]
|
with manifest_path.open("w", encoding="utf-8-sig", newline="") as f:
|
writer = csv.DictWriter(f, fieldnames=list(manifest_rows[0].keys()))
|
writer.writeheader()
|
writer.writerows(manifest_rows)
|
|
by_type = Counter()
|
for row in ledger_rows:
|
for item in row["phrase_type"].split(";"):
|
by_type[item] += 1
|
summary_lines = [
|
"# 公司视图券商原文观点分账 PASS-016 摘要",
|
"",
|
"状态:DRAFT_FOR_REVIEW",
|
f"生成时间:{created_at}",
|
"",
|
"## 输出",
|
"",
|
f"- 分账清单:`{ledger_path.as_posix()}`",
|
f"- manifest:`{manifest_path.as_posix()}`",
|
f"- 公司视图草稿:`{view_path.as_posix()}`",
|
f"- 分账记录数:{len(ledger_rows)}",
|
f"- sha256:`{digest}`",
|
"",
|
"## 表达类型",
|
"",
|
"| 类型 | 数量 |",
|
"|---|---:|",
|
]
|
for key, count in by_type.items():
|
summary_lines.append(f"| {key} | {count} |")
|
summary_lines += [
|
"",
|
"## 边界",
|
"",
|
"本轮只对公司视图展示层做券商原文观点分账和降噪,不改变底层原文证据,不输出正式公司结论、交易指令或收益承诺。正式公司证据卡仍需业务占比、资源/产能、利润弹性、触发/失效条件、风险和官方来源核验。",
|
"",
|
]
|
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
summary_path.write_text("\n".join(summary_lines), encoding="utf-8")
|
print(f"OK split={len(ledger_rows)} output={ledger_path} sha256={digest}")
|
|
|
def main():
|
parser = argparse.ArgumentParser()
|
parser.add_argument("--view", required=True)
|
parser.add_argument("--ledger", required=True)
|
parser.add_argument("--manifest", required=True)
|
parser.add_argument("--summary", required=True)
|
parser.add_argument("--run-id", required=True)
|
args = parser.parse_args()
|
build(Path(args.view), Path(args.ledger), Path(args.manifest), Path(args.summary), args.run_id)
|
|
|
if __name__ == "__main__":
|
main()
|