1.2 中文與 Unicode 的處理#
概念說明#
中文處理的第一個挑戰是編碼選擇。台灣早期慣用 Big5 編碼,每個中文字佔 2 位元組;今日主流則是 UTF-8,常見中文字佔 3 位元組。若開啟檔案時用了錯的編碼,就會出現亂碼。Python 3 的 str 內部統一存 Unicode 碼點,問題多半出在「讀入」與「寫出」的邊界,因此養成明確指定 encoding='utf-8' 的習慣非常重要。
第二個挑戰是 Unicode 正規化(normalization)。同一個「看似相同」的字,可能由不同的碼點組成。最典型的是注音、聲調與組合字元:例如 é 可以是一個碼點 U+00E9(NFC 形式),也可以是 e 加上組合符號 U+0301(NFD 形式)。中文雖少用組合符號,但會遇到相容字元與異體字:如「裏/裡」、「為/爲」,以及全形與半形字元(A 與 A、: 與 :)。比較或搜尋文字前,常需先做正規化與全半形轉換,否則「明明一樣的字卻比對不到」。
第三個挑戰是繁簡轉換:繁體「頭髮」的「髮」簡體作「发」,但「發展」的「發」也作「发」;反向轉換時一個簡體字可能對應多個繁體字,必須靠詞彙層級的對照表,不能逐字硬轉。
演算法步驟:全半形正規化#
- 對字串套用
unicodedata.normalize('NFC', s),統一組合字元形式。 - 逐一檢查字元:若碼點落在全形英數區(
0xFF01~0xFF5E),減去0xFEE0轉成半形。 - 全形空白
U+3000轉成半形空格。 - 其餘字元(含中文字)保持不變,回傳結果。
Python 程式碼範例#
import unicodedata
def fullwidth_to_half(s: str) -> str:
out = []
for ch in unicodedata.normalize('NFC', s):
code = ord(ch)
if 0xFF01 <= code <= 0xFF5E:
out.append(chr(code - 0xFEE0))
elif ch == '\u3000': # 全形空白
out.append(' ')
else:
out.append(ch)
return ''.join(out)
print(fullwidth_to_half("Hello,世界!123")) # Hello,世界!123
# --- 組合字元與正規化 ---
s1 = 'é' # 單一碼點 U+00E9
s2 = 'é' # e + U+0301
print(s1 == s2) # False
print(unicodedata.normalize('NFC', s1) ==
unicodedata.normalize('NFC', s2)) # True
# --- 檔案讀寫:明確指定編碼 ---
with open('demo.txt', 'w', encoding='utf-8') as f:
f.write('中文測試\n')
with open('demo.txt', encoding='utf-8') as f:
print(f.read().strip())
# --- 簡單繁簡詞彙對照示範(實務請用 opencc 等函式庫)---
t2s = {'頭髮': '头发', '發展': '发展', '中文': '中文'}
def naive_t2s(s: str) -> str:
for t, sc in t2s.items():
s = s.replace(t, sc)
return s
print(naive_t2s('我的頭髮很長,國家發展很快'))
複雜度分析#
正規化與全半形轉換都需掃描字串一次,時間 O(n)、空間 O(n)。簡單的逐詞替換式繁簡轉換,若詞表有 m 個詞,每個詞做一次 replace 各需 O(n),總計 O(nm);若用 Aho-Corasick(見第3章)建立自動機,可一次掃描完成,降到 O(n + 比對命中數)。檔案 I/O 的編碼解碼為 O(n)。
練習#
- 寫一個函式
same_text(a, b),先做 NFC 正規化與全半形轉換再比較,回傳兩字串是否「視覺上相同」。 - 找出三個「一字對多字」的繁簡轉換例子(如「發/髮」皆為「发」),說明為什麼逐字轉換會出錯。
- 讀取一個以 Big5 編碼的檔案(可自行用
bytes.decode('big5')建立測試資料),轉成 UTF-8 存檔。