File size: 2,789 Bytes
6cc3500 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | """引號正規化(繞過 zhtw-mcp 的 quote-pairing 缺陷)。
zhtw-mcp `convert` 的引號重指派會**刪除字元**(見
reports/upstream-issue-zhtw-mcp-quotes.md):
『稲亭物怪録』。 → 稲亭物怪録。 ← 正確的括號被刪
„Ich bin deutsche“。 → „Ich bin deutsche。← 德文引號被刪
實測這是 TWLAT 輸出中最大的單一錯誤來源(佔錯誤編輯 37.2%、
涉及 31/298 題)。因此本模組自行做**確定性**的引號轉換,
再把結果遮罩起來讓規則層碰不到。
規則(依《重訂標點符號手冊》):
最外層 「」,內層 『』,再內層回到 「」,以此類推。
只轉換與 CJK 相鄰的 CN 彎引號;英文縮寫(it's)、德文引號(„…“)不動。
"""
from __future__ import annotations
import regex
# 只認漢字/假名/諺文,**不含 CJK 標點**:否則 „Ich bin deutsche“。 的 “
# 會因為後面接了「。」而被誤判為中文語境。
CJK = regex.compile(r"[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]")
OPEN_CN = {"“": 0, "‘": 1} # “ ‘
CLOSE_CN = {"”": 0, "’": 1} # ” ’
PAIRS = [("「", "」"), ("『", "』")] # 「」 『』
ALL_QUOTES = "“”‘’「」『』"
def _cjk_near(text: str, i: int) -> bool:
"""該引號是否處於 CJK 語境(前後任一側 2 字元內有 CJK)。"""
for j in (i - 2, i - 1, i + 1, i + 2):
if 0 <= j < len(text) and CJK.match(text[j]):
return True
return False
def normalize(text: str) -> str:
"""把 CN 彎引號轉為臺灣規範引號,維持巢狀層級,且**不刪除任何字元**。"""
out = list(text)
depth = 0
for i, ch in enumerate(text):
if ch in OPEN_CN:
if not _cjk_near(text, i):
continue # 英文/德文語境 → 不動
out[i] = PAIRS[depth % 2][0]
depth += 1
elif ch in CLOSE_CN:
if not _cjk_near(text, i):
continue
depth = max(0, depth - 1)
out[i] = PAIRS[depth % 2][1]
return "".join(out)
def mask(text: str, base: int = 0xE800):
"""把所有引號字元換成 PUA 佔位符,讓規則層碰不到。回傳 (masked, saved)。"""
saved: list[str] = []
buf = []
for ch in text:
if ch in ALL_QUOTES and len(saved) < 0xF8FF - base:
buf.append(chr(base + len(saved)))
saved.append(ch)
else:
buf.append(ch)
return "".join(buf), saved
def unmask(text: str, saved: list[str], base: int = 0xE800) -> str:
if not saved:
return text
return "".join(saved[ord(c) - base] if base <= ord(c) < base + len(saved) else c
for c in text)
|