錄入端加上 Unicode NFC 正規化:相容表意文字折疊為統一表意文字 - #1280
Merged
sudoghut merged 1 commit intoAug 25, 2026
Merged
Conversation
CJK 相容表意文字(U+F900–U+FAFF 與補充區)與統一表意文字在資料庫層是 不同位元組,唯一鍵擋不住、精確比對找不到、搜尋互不可見。生產庫實測 ALTNAME_DATA.c_alt_name_chn 107 列、OFFICE_CODES.c_office_chn 23 列、 BIOG_MAIN.c_name_chn 17 列、TEXT_CODES.c_title_chn 16 列含相容碼位; c_personid=551931「李晄」的李是 U+F9E1、其他人的李是 U+674E,用「李」 搜尋找不到這個人。 - 新增 App\Support\UnicodeNfc:NFC 正規化(含純 ASCII 快速路徑; malformed UTF-8 保留原值,不讓寫入路徑拿到 false) - 掛在 CharVariantMapService::replaceUsing()——replaceLenient/ replaceStrict/replaceFor/replaceRow 四個入口的共同匯流點(含 7 個 直接呼叫 strict/lenient 的姓名路徑),一處掛上即全覆蓋 - 順序:必須早於對照表查詢(表的鍵全是統一表意文字,相容碼位對不上), 也必須早於 empty($map) 早退(NFC 與對照表無關,缺表時照樣要做) - 不記進 replaced、不產生 notices:折疊前後字形一模一樣,列進通知只是 雜訊。連帶修掉 replaceRow() 以 replaced===[] 當寫回閘門的缺陷—— 只有 NFC 改動的值會被整個丟棄(測試先抓到) - 作用域沿用 VariantReplaceScope:排除欄同樣不做 NFC(代碼鍵/join 鍵 單邊正規化會打斷關聯,理由同 D3)。受影響的 4 個欄位全在範圍內 與異體字落地替換是兩件不同性質的事:NFC 折疊的兩個碼位在 Unicode 定義上 就是同一個字(canonical equivalence),不涉編輯判斷;而愼→慎 那類異體字 Unicode 刻意不折疊。實測 char_variant_map 的 7 筆種子變體字全部 NFC 不變, 兩套機制作用域不重疊。 不需 ext-intl(Normalizer 由既有相依 symfony/polyfill-intl-normalizer 提供)。 用 NFC 而非 NFKC——後者會抹掉全形字母、羅馬數字等有意義的區別,有測試釘住。 效能:一般中文 2.4µs/次、純 ASCII 0.065µs/次,批次 1000 列×20 欄約 48ms。 已知取捨:少數相容碼位帶來源編碼的讀音資訊(U+F9E1 李 來自 KS X 1001 的 「이」),NFC 會抹掉該區別。對 CBDB 不構成損失——讀音存在獨立拼音欄、 不靠碼位承載,且這些字出現在漢人姓名與官名裡,來源是輸入法/舊編碼意外。 範圍:本次只做寫入端。既有 163 列的回填與搜尋端 NFC 是獨立工作。 測試:tests/Unit/UnicodeNfcTest.php(9 tests,含「不得碰異體字」與 「不得退化成 NFKC」兩道界線)+ ApiV2MutateVariantReplacementTest 端到端。 已執行全套 phpunit(2987 tests;4 個 SecurityAuditLogTest 失敗為本機 環境既有、在純上游基底同樣失敗)、php-cs-fixer(0 fixes)。
This comment was marked as off-topic.
This comment was marked as off-topic.
Contributor
Author
|
我們之前處理過的 cbdb-project/cbdb_sqlite#19 就屬於此類問題,本 PR 可將未來同類問題一次性解決。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
問題
CJK 相容表意文字(U+F900–U+FAFF 與補充區 U+2F800–U+2FA1D)與統一表意文字在資料庫層是不同位元組:唯一鍵擋不住、精確比對找不到、搜尋互不可見。而系統目前沒有任何 Unicode 正規化(grep 過
app/、config/,只有無關的BracketNormalizer括號風格)。生產庫實測:
ALTNAME_DATA.c_alt_name_chnOFFICE_CODES.c_office_chnBIOG_MAIN.c_name_chnTEXT_CODES.c_title_chn最具體的一例(
utf32為實際碼位):用「李」(U+674E)搜尋,找不到 c_personid=551931。
與異體字落地替換的關係:兩件不同性質的事
char_variant_map)kSemanticVariant/kZVariantUnicode 的穩定性政策是永不給統一表意文字 canonical decomposition,所以 NFC 不會碰 愼/峯/靑 那類異體字——實測
char_variant_map的 7 筆種子變體字全部 NFC 不變。反過來相容表意文字一定折疊。兩套機制作用域不重疊。做法
app/Support/UnicodeNfc:NFC 正規化,含純 ASCII 快速路徑;malformed UTF-8 時保留原值(這是寫入路徑,不能讓欄位變成false)。CharVariantMapService::replaceUsing()——replaceLenient/replaceStrict/replaceFor/replaceRow四個公開入口的共同匯流點(含 7 個直接呼叫 strict/lenient 的姓名路徑),一處掛上即全覆蓋。empty($map)早退——NFC 與對照表無關,缺表/空表時照樣要做。replaced、不產生notices:折疊前後字形一模一樣,列進通知只是雜訊。順帶修掉一個整合缺陷
replaceRow()原本以$result['replaced'] === []當「要不要把值寫回」的閘門。NFC 刻意不記進replaced,於是只有 NFC 改動的值會被整個丟棄。改為以「文字是否改變」判斷;replaced仍只累積異體字替換。這是新增的測試先抓到的。已檢查全部 10 個replaceRow呼叫端,沒有人依賴舊的隱含契約。作用域
沿用
VariantReplaceScope——被排除的欄位(對照表自身、代碼鍵/join 鍵、拼音字典鍵等)同樣不做 NFC。理由與計畫文件 D3 相同:單邊正規化會打斷關聯。實測前述 4 個受影響欄位全都在範圍內,所以這個保守作用域不影響修復效果。同時它自動繼承了
BiogMainRepository的$variantFields語義——v2 路徑只把「本次實際變更的欄」送進替換範圍,所以 NFC 也只作用在使用者真的動過的欄位上,不構成對未觸碰欄位的回溯校正(D6)。相依與效能
ext-intl:Normalizer由既有相依symfony/polyfill-intl-normalizer提供(隨 Laravel 進來);日後若裝了 ext-intl 會自動讓位給原生實作。已知取捨
少數相容碼位在來源編碼裡帶有讀音資訊(U+F9E1 李 來自 KS X 1001 的「이」讀音,與 U+674E 的「리」相對),NFC 會抹掉該區別。判斷對 CBDB 不構成損失:讀音存在獨立的拼音欄(
c_name/c_surname/c_alt_name_pinyin),不靠碼位承載;且庫中這些字出現在漢人姓名與官名裡,來源是輸入法/舊編碼轉換的意外,不是刻意的讀音標記。這也是 W3C Character Model 對網路上文本的建議儲存形式。若認為需要保留該區別,這個判斷可以改。範圍
本次只做寫入端(錄入的相容字存成統一字)。以下是獨立工作、未包含:
測試
tests/Unit/UnicodeNfcTest.php(9 tests):折疊行為、四個入口都生效、NFC 早於對照表、缺表時仍生效、作用域排除,以及兩道界線測試——不得碰異體字(7 個種子字全驗)、不得退化成 NFKC。ApiV2MutateVariantReplacementTest:端到端一條,用生產庫實際存在的 U+F9E1/U+FA1D 走/api/v2/mutate。phpunit2987 tests / 15770 assertions;4 個SecurityAuditLogTest失敗為本機 Docker 取不到 request IP 的既有問題,已在不含本 PR 的基底上重跑確認同樣失敗。php-cs-fixer0 fixes。文檔
AGENTS.md§1.3 補上「NFC 是另一回事且已自動生效,不要在呼叫端自己做、不要改用 NFKC」;API.md三處(notices 涵蓋範圍、寫入端改寫說明、回應欄位表);CHANGELOG.md。