攝影或3C

Python unicodedata 小教室:把 `café` 變成 `cafe`,因為大家搜尋時只會打 `cafe` ; import unicodedata ; normalized = unicodedata.normalize(“NFKD”, text) ; “”.join(ch for ch in normalized if not unicodedata.combining(ch))

`import unicodedata` 主要就是拿來做「去掉重音符號」。

為什麼要這樣做?因為真實世界裡,人搜尋時常常會偷懶。

文件裡可能寫的是 `café`、`résumé`、`diagnóstico`,

但使用者實際打進搜尋框的往往是 `cafe`、`resume`、`diagnostico`。

所以如果我們想讓資料比較容易被搜到,

就常需要先把這些重音符號攤平,
讓查詢字形和資料字形更容易對上。

目前對應的程式是:

def strip_accents(text: str) -> str:
    normalized = unicodedata.normalize("NFKD", text)
    return "".join(ch for ch in normalized if not unicodedata.combining(ch))

## 1. 這段在做什麼

像西文裡的字:

– `diagnóstico`

– `información`

– `México`

裡面的 `ó / á / é` 這類字,其實可以拆成:

– 一個基底字母

– 加上一個重音符號

`unicodedata.normalize(“NFKD”, text)`
會先把字元分解成「基底字母 + 結合符號」。

這裡的 `NFKD` 比較適合整體看成
`Normalization Form Compatibility Decomposition`。

重點不是把 `N` 和 `F` 分開背,而是先把前半段一起理解成:

– `Normalization Form` = Unicode 的一種正規化形式

也就是說:

– `Normalization` 是大方向:
把不同寫法整理成比較一致的表示方式

– `Form` 是具體形式:Unicode 下面有好幾種正規化形式,
例如 `NFC / NFD / NFKC / NFKD`

– `K` 代表這是 compatibility 版本的 normalization;
它不是靠發音來縮寫,而是 Unicode 規格裡
約定用來標記 compatibility form 的代號

– 記憶口訣:`K = OK to flatten compatibility forms`

– `D` 代表 `Decomposition`,
表示它偏向把字元拆開,而不是重新合成

可以先把它白話記成:

– `NFKD` = 用 compatibility 規則做的「拆開型」正規化

一個最小對比例子是:

– `NFD`:會把 `é` 拆成 `e` + 重音符號

– `NFKD`:也會把 `é` 拆開,
而且還會比 `NFD` 更願意把一些相容字形攤平成一般形式

如果想看得更具體,可以直接跑這段 Python:

import unicodedata

samples = ["é", "", "", "", ""]
# "" 看起來像 f + i但其實是單一連字字元
# "" 看起來像 k + g但其實是單一 kg 符號字元
# 也就是說眼睛看起來像兩個字Unicode 內部未必真的是兩個字元

for text in samples:
    nfd = unicodedata.normalize("NFD", text)
    nfkd = unicodedata.normalize("NFKD", text)
    print("SRC ", repr(text), [hex(ord(ch)) for ch in text])
    print("NFD ", repr(nfd), [hex(ord(ch)) for ch in nfd])
    print("NFKD", repr(nfkd), [hex(ord(ch)) for ch in nfkd])
    print("---")

 你會看到重點差異像這樣:

這組例子剛好能看出差別:

– `é`:`NFD` 和 `NFKD` 都會拆成 `e` + 重音符號

– `A`:`NFD` 不動全形字母,`NFKD` 會攤平成一般半形 `A`

– `fi`:`NFD` 不動連字,`NFKD` 會攤平成 `fi`;
也就是把「看起來像一個連字字元」攤平成一般兩個字母

– `㎏`:`NFD` 不動單一的 kg 符號字元,`NFKD` 會攤平成一般 `kg`

– `①`:這類圈號數字也屬於相容字形,`NFD` 不動它,
`NFKD` 會把它攤平成一般 `1`

實務上你可以直接把它記成:
`NFKD` 的重點就是「把字元盡量拆開」。

對這份教學最重要的不是背縮寫,
而是知道它會把像 `ó` 這種字,拆成比較基礎的成分,

這樣後面才有辦法把重音符號單獨辨認出來並移除。

例如概念上:

– `ó` 會分解成 `o` + 重音符號

– `é` 會分解成 `e` + 重音符號

但要注意:很多介面在顯示這種結果時,
仍會把它們疊在一起畫出來,

所以你直接 `print()` 或直接看回傳值時,
畫面上常常還是像同一個 `ó`,不代表它沒有拆開。

真正看得出來的方法,是把結果攤開看:

import unicodedata

text = "ó"
normalized = unicodedata.normalize("NFKD", text)

print(normalized)
print(list(normalized))
print([hex(ord(ch)) for ch in normalized])
print([unicodedata.name(ch) for ch in normalized])

你會看到像這樣:

其中 `LATIN SMALL LETTER O`
就是普通小寫英文字母 `o` 的 Unicode 正式名稱。

這才表示它真的已經拆成兩個 code point:

– `o`

– `COMBINING ACUTE ACCENT`

接著:

unicodedata.combining(ch)

可以判斷某個字元是不是「結合符號」;
在這裡最常見的就是重音符號。

所以這行:

"".join(ch for ch in normalized if not unicodedata.combining(ch))

就是把那些重音符號濾掉,只留下基底字母。

## 2. 最小範例

import unicodedata


def strip_accents(text: str) -> str:
    normalized = unicodedata.normalize("NFKD", text)
    return "".join(ch for ch in normalized if not unicodedata.combining(ch))


print(strip_accents("diagnóstico"))
print(strip_accents("información"))
print(strip_accents("México"))

輸出:

## 3. 為什麼這支腳本要這樣做

在這份 synonyms pipeline 裡,西文輸出有一條規則是:

– `es_mx` 要全小寫

– `es_mx` 不用重音符號

所以程式裡會先做:

“`python

strip_accents(value.strip()).lower()

“`

這樣:

– `Diagnóstico` -> `diagnostico`

– `México` -> `mexico`

比較適合拿去做 BM25 檢索時的字形統一。

## 4. 常見問題

### Q1. 這是不是把中文也轉掉?

不會。

這段主要影響的是有重音的拉丁字母。
中文通常不會因為這段被改掉。

### Q2. `lower()``strip_accents()` 是一樣的事嗎?

不是。

– `lower()`:只管大小寫

– `strip_accents()`:只管把重音符號移除

補充:`strip_accents` 是這份教學裡自己定義的函式,

不是 Python 字串內建 method,

所以要寫成 `strip_accents(text).lower()`,

不是 `text.lower().strip_accents()`。

例如:

– `É` 先 `lower()` 會變成 `é`

– 再 `strip_accents()` 才會變成 `e`

### Q3. 為什麼不用 regex?

因為重音字元不只是表面上幾個固定字母。

`unicodedata.normalize(“NFKD”, …)` 是比較穩的 Unicode 正規化做法,

比手寫一串 `á -> a`、`é -> e` 的替換表更通用。

補充:`strip_accents()` 不會移除一般標點符號,

例如 apostrophe(撇號)、逗號、句點、連字號;
它處理的重點是 Unicode 的 combining marks。

## 5. 一句話版

`import unicodedata` 在這裡的用途,
就是把像 `diagnóstico` 這種有重音的字,

穩定轉成 `diagnostico`。

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡