"""译文文本清洗工具。 应用场景: - LLM 翻译时偶尔把 markdown 标记(加粗 `**`、`*`、`***`)原样带进译文里, 前端展示出来就成了 `**FBI**局长` 这种带星号的脏数据。 - enrichment 阶段也会把 `**` 带到 `body_zh_formatted` / `summary_zh`。 提供两个核心函数: - `clean_markdown_asterisks(text)`:清洗字符串里的 `*` / `**` / `***` 标记 - `clean_html_inner_text(html)`:BeautifulSoup 解析 HTML,只清洗文本节点, 保留标签结构和内联 style 另附 `wrap_html(text)`:把清洗后的 body_zh_text 包成简单 `

` HTML, 之前在 pipeline.py 是私有 `_wrap_html`,提到此处供复用。 设计原则: - 不引入额外依赖(只用 `re` + 项目已有的 `beautifulsoup4`) - 对 None / 空串安全返回 None / 空串 - 处理顺序从长到短,避免 `**` 被 `*` 先吃掉 - 反复循环直到稳定,应对 `**a****b**` 这种连续多对 - 兜底删除所有残留 `*`,保守但符合"去掉星号"的用户意图 """ from __future__ import annotations import re from bs4 import BeautifulSoup # === 核心清洗函数 === # ***text*** → text(粗+斜体) _ASTERISK_TRIPLE_RE = re.compile(r"\*\*\*([^*]+?)\*\*\*") # **text** → text(粗体) _ASTERISK_DOUBLE_RE = re.compile(r"\*\*([^*]+?)\*\*") # *text* → text(斜体,要求 text 不为空且不与 * 相邻,避免误伤 `*2*3*` 这种) # 第一个负向回看 (? str | None: """清洗字符串里的 markdown 星号标记残留。 处理顺序: 1. `***text***` -> `text` 2. `**text**` -> `text`(循环直到稳定,处理 `**a****b**` 这种连续) 3. `*text*` -> `text` 4. 兜底:残留的 `*` / `**` / `***` 一律删除(LLM 输出不严谨的脏数据) 对 None / 空串安全返回原值。 """ if not text: return text # 1+2) 先把 *** / ** 多轮替换,直到稳定(处理嵌套/连续多对) prev: str | None = None while prev != text: prev = text text = _ASTERISK_TRIPLE_RE.sub(r"\1", text) text = _ASTERISK_DOUBLE_RE.sub(r"\1", text) # 3) 单星号斜体 text = _ASTERISK_SINGLE_RE.sub(r"\1", text) # 4) 兜底:删掉所有零散 * text = _ANY_ASTERISK_RE.sub("", text) return text def clean_html_inner_text(html: str | None) -> str | None: """清洗 HTML 内的文本节点(保留标签结构和属性)。 用途:`body_zh_formatted` 这种由 LLM 排版产物 —— 不能整个重新生成(会丢 `diary-para` class 和内联 style), 只能用 BeautifulSoup 找到所有文本节点单独清洗。 对 None / 空串安全返回原值。 """ if not html: return html soup = BeautifulSoup(html, "html.parser") changed = False for node in list(soup.find_all(string=True)): # 跳过纯空白文本节点 original = str(node) if not original or not original.strip(): continue cleaned = clean_markdown_asterisks(original) if cleaned != original: node.replace_with(cleaned) changed = True return str(soup) if changed else html # === 公开版 wrap_html(原 pipeline._wrap_html,提到此处供脚本复用)=== def wrap_html(text: str | None) -> str | None: """把清洗后的译文纯文本包成简单的 `

` 段落 HTML。 内部会自动调用 `clean_markdown_asterisks` 清洗 `**` / `*` / `***`, 调用方无需"先清洗再 wrap"——这是幂等的,即使输入已清洗也是 no-op。 行为: - 按 `\n\n` 切段,空段过滤 - 每段包 `

...

` - 段落之间用 `\n` 拼接 对 None / 空串返回 None。 """ if not text: return None cleaned = clean_markdown_asterisks(text) if not cleaned: return None parts = [f"

{p.strip()}

" for p in cleaned.split("\n\n") if p.strip()] return "\n".join(parts) if parts else None