Python 3 原始碼檔案預設為 UTF-8,而 str 型別會在記憶體中儲存 Unicode 文字,因此大多數日常的 Python 程式碼已經運行在 UTF-8 之上。當你需要在 Python 中轉換為 UTF-8時,兩個核心操作是 str.encode('utf-8')(從 Unicode 字串轉為位元組)以及 bytes.decode('utf-8')(反方向轉換);這涵蓋了字串字面值、網路傳輸內容,以及任何以二進位模式讀取的位元組。當磁碟上已存在的位元組並非由 UTF-8 產生時(例如 Windows-1252 的匯出檔、UTF-16 小端序的傾印檔,或帶有 UTF-8 位元組順序標記(BOM)儲存的檔案),問題就會變得複雜——因為用錯誤的方式解碼時,可能會以 UnicodeDecodeError 當機,或默默地把字元替換成替代字形 �。像 chardet 這類自動偵測程式庫會做出機率式的猜測,結果看起來似乎合理,卻仍可能損壞姓名、標點符號或貨幣符號。針對這些情況,明確指定來源編碼,再搭配像 UTF-8 轉換器 這類以檔案為基礎的轉換工具,就能在不寫程式的情況下產生可審核、無 BOM 的 UTF-8 檔案。

convert to utf 8 python
在 Python 中轉換為 UTF-8:字串、檔案與錯誤處理

Python 3 預設如何處理 UTF-8

自 2009 年的 PEP 3120 起,每個 Python 3 原始碼檔案都會以 UTF-8 進行解析,除非在編碼註解中宣告不同的編碼。執行階段則更進一步:str 物件儲存的是 Unicode 碼位(code point),內建的 open() 函式預設使用平台慣用的編碼(在現代 Linux 與 macOS 上即為 UTF-8),而 print() 則會透過標準輸出串流的編碼來寫入。這代表像 s = "café — naïve façade" 這樣的字串字面值已經包含了正確的字元;唯一的問題在於,當你把這些字串寫到某處時,那些碼位是如何被轉換成位元組的。

有兩個相關的重點經常讓新手感到困惑。第一,Python 原始碼檔案中的 "café" 是 str,而非位元組——在你呼叫 encode() 之前,它本身並沒有編碼。第二,open() 中的 encoding= 參數控制的是磁碟上的位元組如何被轉為 str(讀取時),或是反方向(寫入時),而不是 str 在內部如何儲存。這兩個事實說明了為什麼「轉換為 UTF-8」在 Python 中其實是兩件不同的工作:序列化你已有的 str,以及重新編碼別人用不同編碼產生的檔案。

將 Python 字串轉換為 UTF-8 位元組

針對第一項工作——將你已有的 Unicode 字串轉成 UTF-8 位元組——標準函式庫就足夠了:

  • "café".encode("utf-8") 會產生 b'caf\xc3\xa9',這是 é 對應的雙位元組 UTF-8 序列 c3 a9。
  • "你好".encode("utf-8") 因為每個中文字都在 BMP 之外,所以每個字元會產生三個位元組。
  • "🙂".encode("utf-8") 會產生四個位元組,因為 🙂 是 BMP 之外的補充字元。

如果你想讓 Python 原始碼檔案宣告不同的編碼,請在第一行或第二行加入編碼註解,例如 # -*- coding: latin-1 -*-。在今天這已經很少需要,因為 UTF-8 幾乎涵蓋了所有字元,但它能解釋為什麼較舊的教學文件會提到這個指令。在寫入磁碟時,建議優先使用明確的 open(path, "w", encoding="utf-8"),而非依賴平台預設值,特別是在 Windows 上,地區設定仍可能預設為舊式的代碼頁(code page)。

將位元組解碼回 Python 字串

反向操作在你以二進位模式讀取檔案或網路回應時特別重要。open(path, "rb") 會回傳位元組,你可以自行決定如何解讀它們:

  • 純 UTF-8 使用 data.decode("utf-8")。
  • 帶前置 BOM 的 UTF-8 使用 data.decode("utf-8-sig"),該 BOM 會被靜默地消耗掉。
  • UTF-16 檔案使用 data.decode("utf-16")、data.decode("utf-16-le") 或 data.decode("utf-16-be")。
  • Windows-1252 使用 data.decode("cp1252")。

請傳入 errors="strict"(即預設值),讓格式錯誤的位元組拋出 UnicodeDecodeError,而不是靜默地變成 �。瀏覽器解碼文字的方式(包括 0x80–0x9F 範圍內的可列印字元,例如歐元符號和花引號,而 ISO-8859-1 並未定義這些字元)定義於 WHATWG 編碼標準 之中;當 Windows 的匯出檔案聲稱自己是 ISO-8859-1 時,這也是「看起來對但其實不對」這類驚喜情形的常見來源。

為何 Python 的編碼自動偵測仍有所不足

在 Python 中將檔案轉換為 UTF-8 最常見的方式,最終看起來會像這樣:

  • raw = open(path, "rb").read()
  • guess = chardet.detect(raw)["encoding"]
  • text = raw.decode(guess)
  • open(path, "w", encoding="utf-8").write(text)

這條管線在檔案很長、明確為單一語言,且位元組統計明顯傾向某一編碼時運作良好。但在短檔案、語言混用文字、Windows-1252 與 ISO-8859-1 之間的歧異,以及同一段位元組範圍同時在多種舊編碼下有效、卻代表不同字元的檔案上則不可靠。以 Windows-1252 儲存為位元組 0x80 的歐元符號,在 ISO-8859-1 下解碼會變成控制字元;而智慧引號的位元組,在另一個不同的編碼標籤下可能根本無法解讀成任何有意義的字元。「自動」這一步其實只是猜測,而且這個猜測若搭配 errors="replace" 靜默通過 decode(),就可能在下游派送出損壞的文字,卻完全沒有錯誤可供調查。

無需撰寫 Python 即可將檔案轉換為 UTF-8

當你已經知道產生該檔案的編碼——因為來源應用程式、同事告知,或可靠的元資料指明——比較安全的做法是明確指定該選擇,並直接轉換位元組。UTF-8 轉換器正是依照此模式運作,整個過程都在瀏覽器中完成,因此原始位元組從不離開本機。

  1. 從產生該檔案的應用程式或可靠的元資料中辨識出來源編碼(UTF-8、UTF-16 小端序、UTF-16 大端序,或 Windows-1252)。
  2. 選擇該文字檔案(最大 10 MB),並從轉換器的清單中挑選對應的編碼。
  3. 執行轉換並檢查預覽結果,特別留意非 ASCII 字元,例如姓名、標點符號、貨幣符號與表情符號。
  4. 下載產生的檔案,該檔案會帶有 -utf8 後綴,並使用無 BOM 的純文字 UTF-8 媒體類型。
  5. 在下載的檔案正式取代任何原始檔案之前,請先在目標應用程式中進行測試,並保留原始檔案直到完整的工作流程驗證無誤。

UTF-8 模式會以致命錯誤處理(fatal error handling)來驗證來源:無效的後續位元組、被截斷的序列,以及被禁止的編碼都會導致明確的失敗,而非被替換成替代字元;前置的 UTF-8 BOM 則會在重新編碼之前,被符合標準的解碼器先行消耗掉。UTF-16LE 與 UTF-16BE 的選擇差異在於位元組順序——同一對位元組若顛倒端序,就會變成無意義的內容——而相符的 BOM 會被辨識並移除。Windows-1252 模式則使用瀏覽器中符合標準的解碼器,因此在重新編碼為 UTF-8 之前,0x80–0x9F 範圍內的可列印標點會被正確對應。

轉換器可接受的來源編碼

來源編碼常見來源可辨識的 BOM解碼器行為
UTF-8現代編輯器、Linux 預設值、多數網頁匯出EF BB BF(會被消耗)嚴格驗證,不靜默替換
UTF-16 小端序Windows 記事本的「Unicode」、部分 Java 與 .NET 工具FF FE(會被消耗)代理對(surrogate pair)會組合成單一碼位
UTF-16 大端序部分 Java 工具、網路通訊協定FE FF(會被消耗)代理對會組合成單一碼位
Windows-1252舊版 Windows 軟體、被誤標為 ISO-8859-1 的西歐匯出檔無0x80–0x9F 可列印標點在重新編碼前會先對應

輸出是由 UTF-8 編碼器產生,且不會附加 BOM,頁面會同時回報來源與輸出的位元組數量,因此可以看見預期的膨脹或壓縮情形。Windows-1252 的歐元符號位元組 0x80 會變成三個 UTF-8 位元組 E2 82 AC;UTF-16 檔案通常會縮小,因為 UTF-8 會將 ASCII 字元壓縮成單一位元組。位元組數量不同是預期中的現象,並不單獨代表資料遺失。

限制以及何時應改用串流工具

此轉換器的上限為 10 MB,因為瀏覽器的 File 與 Encoding 介面是基於記憶體內的緩衝區運作,且會在讀取前先檢查大小。針對資料庫傾印檔或數 GB 規模的記錄檔,建議改用值得信賴的串流工具,例如 Linux 上的 iconv,或 Windows 上的 Get-Content -Encoding | Out-File -Encoding utf8,這些工具皆接受明確的來源與目的編碼,並能避免一次將整個檔案載入記憶體。

預覽功能有助於在下載前就明顯抓出錯誤的選擇,但它可能無法顯示每一個控制字元或每一種 Unicode 正規化(normalisation)上的差異。請檢查具代表性的姓名、標點符號、貨幣符號與非 ASCII 文字行;若來源檔案內混合了多種編碼,單一解碼器無法可靠地修復,就必須先將檔案分割。

在目標應用程式中驗證輸出

乾淨的預覽雖屬必要,但並非充分條件。請在實際會使用該檔案的應用程式中開啟下載的 UTF-8 檔案——例如透過 open(path, encoding="utf-8") 讀取它的 Python 腳本、資料庫的 COPY 指令、JSON 解析器、CSV 匯入工具——並確認姓名、貨幣、標點與任何非 ASCII 文字行從頭到尾都正確無誤。請將頁面回報的位元組數與目的工具實際看到的數量相比對;若匯入工具回報的字元數少於預期,請重新檢視來源編碼的選擇,而非檢查輸出。只有在完整來回轉換(round-trip)皆驗證無誤後,才能正式汰除原始檔案。

延伸閱讀:C# 中的 UTF-8 解碼:位元組到字串,且不產生靜默錯誤。