2026年8月31日 星期一

Python一下:從水井三寶到多語導覽——ASCII、Unicode與中文字串

《Python一下:從風土資料到智慧生活》第 14 篇

從水井三寶到多語導覽——ASCII、Unicode與中文字串

讓白馬、烏龜、姻緣花與台語地方故事,跨過裝置、檔案和網頁仍能正確顯示。

CH9 進階字串Unicode多語導覽
學習目標
完成本篇後,你能分辨字元、碼位、字串與位元組;理解 ASCII、Unicode 與 UTF-8 的關係;使用 ord()chr()encode()decode();處理中文字串、正規化與常見亂碼。

一、電腦怎麼認識「姻緣花」?

人看到的是文字,電腦儲存的是數字與位元組。為了讓不同裝置對同一個字有共同理解,需要三個層次:

層次意義例子
字元 character人理解的文字單位「花」
Unicode 碼位字元的通用編號U+82B1
編碼 encoding把碼位轉成實際位元組的規則UTF-8
地方語言提醒:台語文字可能使用漢字、臺羅、聲調符號或混合形式。程式能保存字形,卻不能替社區決定哪種寫法最能表達語意;公開導覽前仍須由講述者與地方語言使用者確認。

二、ASCII:早期的基本字元表

ASCII 主要涵蓋英文字母、數字、標點與控制字元,無法直接表示「水井村」等中文字。

print(ord("A"))
print(chr(65))
print(ord("1"))

text = "USR"
for character in text:
    print(character, ord(character))

ord() 取得單一字元的 Unicode 碼位整數;chr() 則把整數轉回字元。ASCII 字元同時也是 Unicode 的一部分。

三、Unicode 不等於 UTF-8

Unicode 是字元及碼位的共同標準;UTF-8、UTF-16 則是把碼位轉成位元組的編碼方式。Python 3 的 str 表示 Unicode 文字,bytes 表示已編碼的位元組。

name = "姻緣花"

print(type(name))
for character in name:
    print(character, f"U+{ord(character):04X}")

四、encode 與 decode:文字和位元組之間的橋

story = "水井三寶:白馬、烏龜、姻緣花"

utf8_data = story.encode("utf-8")
restored_story = utf8_data.decode("utf-8")

print(utf8_data)
print(restored_story)
print(story == restored_story)

記住方向:文字 encode 成 bytes;bytes decode 回文字。兩端必須對同一批位元組使用相符編碼。

五、字數與位元組數為何不同?

texts = ["ABC", "水井村", "水井村🐢"]

for text in texts:
    encoded = text.encode("utf-8")
    print(
        text,
        "字串長度:", len(text),
        "UTF-8 位元組數:", len(encoded),
    )

len(str) 計算 Python 字串中的碼位數量;len(bytes) 計算位元組。資料庫欄位、網路封包或設備緩衝區若限制位元組,不能只用中文字數估算。

六、中文字串也能切片與搜尋

guide = "白馬、烏龜、姻緣花"

items = guide.split("、")
print(items)
print("烏龜" in guide)
print(guide.replace("、", "|"))

for number, item in enumerate(items, start=1):
    print(f"{number}. {item}")

Python 的字串操作對中文同樣適用。但使用者看到的一個圖形,可能由多個 Unicode 碼位組成,因此「畫面上的字數」不一定永遠等於 len()

七、看起來一樣,電腦卻判斷不同

帶重音字母可以是一個預組字元,也可以由基本字母加組合符號構成。兩者畫面相似,但直接比較可能不同。

import unicodedata

text_a = "Tâi-gí"
text_b = "Ta\u0302i-gi\u0301"

print(text_a, text_b)
print(text_a == text_b)

normalized_a = unicodedata.normalize("NFC", text_a)
normalized_b = unicodedata.normalize("NFC", text_b)
print(normalized_a == normalized_b)

搜尋、比對與去除重複資料前,可統一使用 NFC。正規化改變的是字元表示方式,不會自動修正拼字或翻譯。

八、清理訪談轉文字資料

語音轉文字結果常混入全形空白、換行與前後空白。先做最小幅度清理,並保留原始訪談版本。

import unicodedata


def clean_transcript(text):
    text = unicodedata.normalize("NFC", text)
    text = text.replace("\u3000", " ")
    lines = [line.strip() for line in text.splitlines()]
    return "\n".join(line for line in lines if line)


raw_text = " 阿嬤講:姻緣花開矣。\n\n  這是庄頭的記持。 "
print(clean_transcript(raw_text))
口述歷史倫理:不要因為「清理文字」而改寫長輩原意。建議同時保存原始錄音、逐字稿、修訂稿、講述者確認狀態及公開授權範圍。

九、讀寫 UTF-8 文字檔

開啟文字檔時明確指定 encoding="utf-8",可降低不同作業系統間的差異。

from pathlib import Path

path = Path("shuijing_guide.txt")
story = "水井三寶:白馬、烏龜、姻緣花\n"

path.write_text(story, encoding="utf-8")
loaded_story = path.read_text(encoding="utf-8")

print(loaded_story)

若來源是 Windows 軟體匯出的 UTF-8 BOM 檔案,可視情況以 utf-8-sig 讀取;但應先確認來源格式,不要輪流猜編碼直到「看似正常」。

十、亂碼是怎麼發生的?

亂碼通常不是文字「壞掉」,而是寫入與讀取使用不同編碼。以下故意用錯誤方式示範:

original = "水井村"
utf8_bytes = original.encode("utf-8")

wrong_text = utf8_bytes.decode("latin-1")
print(wrong_text)

recovered = wrong_text.encode("latin-1").decode("utf-8")
print(recovered)

這個還原方法只適用於特定「UTF-8 位元組被誤當 Latin-1」的情況。真實資料若已被多次轉碼或遺失位元組,可能無法完全恢復,應優先找回原始檔。

十一、不要隨便忽略解碼錯誤

data = b"guide:\xff\xfe"

try:
    text = data.decode("utf-8")
except UnicodeDecodeError as error:
    print("無法以 UTF-8 解碼:", error)
    text = None

print(text)

errors="ignore" 會直接丟掉無法解碼的內容,可能讓地方名稱或訪談語句悄悄缺字。資料保存工作應記錄錯誤並回查來源;只有明確了解後果時才使用替代策略。

十二、完成多語導覽資料清理工具

import unicodedata


def normalize_text(value):
    if not isinstance(value, str):
        raise TypeError("導覽文字必須是字串")
    value = unicodedata.normalize("NFC", value)
    return " ".join(value.split())


def clean_guide_record(record):
    required = ("主題", "語言", "內容")
    missing = [key for key in required if key not in record]
    if missing:
        raise KeyError("、".join(missing))

    return {
        "主題": normalize_text(record["主題"]),
        "語言": normalize_text(record["語言"]),
        "內容": normalize_text(record["內容"]),
    }


records = [
    {"主題": "白馬", "語言": "華語", "內容": " 水井村地方故事 "},
    {"主題": "姻緣花", "語言": "台語", "內容": "庄頭的記持"},
]

for record in records:
    print(clean_guide_record(record))

十三、常見問題檢查表

現象可能原因建議
中文字變成問號或亂碼編碼不一致確認原始編碼及讀寫設定
相同臺羅詞搜尋不到組合字元表示不同比對前統一 NFC
上傳設備後文字被截斷把字數誤當位元組數以指定編碼後的 bytes 計算
檔案開頭多出奇怪符號UTF-8 BOM確認後使用 utf-8-sig
忽略錯誤後地方名稱缺字使用 errors="ignore"保留錯誤並回查原始資料

十四、USR 實作挑戰

挑戰 A|水井三寶碼位卡
列出「白馬、烏龜、姻緣花」每個字元的 Unicode 碼位與 UTF-8 位元組數,觀察標點與中文字的差異。
挑戰 B|台語逐字稿正規化
對已獲同意的教學逐字稿進行 NFC 與空白清理,逐行比較原始版和清理版,請講述者確認沒有改變原意。
挑戰 C|跨裝置測試
將 UTF-8 導覽檔分別在 Thonny、Colab、手機與網頁開啟,記錄顯示差異與解決方式。

十五、與生成式 AI 協作

請擔任 Python Unicode 與地方語言資料助教。
請檢查我的多語導覽文字處理程式:
1. 分辨 str 與 bytes 的使用是否正確;
2. 檔案讀寫必須明確指定 UTF-8;
3. 搜尋比對前使用 NFC 正規化;
4. 不可用 errors="ignore" 默默刪字;
5. 保留原始逐字稿,不可擅自改寫地方語意;
6. 說明如何測試中文、臺羅與表情符號。
請先指出風險,再提供修正版。
本篇小結
Unicode 為字元建立共同編號,UTF-8 則把這些編號轉成位元組。只要清楚區分 strbytes、統一編碼並保留原始資料,多語地方故事就能更可靠地跨平台流動。

沒有留言:

張貼留言