DEV Community

Cover image for Windows 系統上 Python 的文字輸出編碼
codemee
codemee

Posted on • Edited on

3 2

Windows 系統上 Python 的文字輸出編碼

與文字編碼有關的幾個函式

在 Python 中, 有幾個地方都與文字的編碼有關, 很容易搞混:

設定 說明
locale.getpreferredencoding() 這是根據使用者作業系統的地區設定而決定的編碼, 它會決定輸出入文字時預設採用的編碼, 包含終端機輸出入、檔案輸出入等等。
sys.getfilesystemencoding() 這是處理檔案路徑名稱時預設採用的文字編碼。
sys.getdefaultencoding() 處理字串時預設的文字編碼, 用在 str.encode()bytes.decode()bytearray.decode()

我們可以使用以下這個簡單的程式顯示以上各項設定:

# print_encoding.py
import sys
import locale

print('locale.getpreferredencoding():\t{}'.format(
  locale.getpreferredencoding())
)
print('sys.getfilesystemencoding():\t{}'.format(
  sys.getfilesystemencoding())
)
print('sys.getdefaultencoding():\t{}'.format(
  sys.getdefaultencoding())
)
print('sys.stduot.encoding:\t\t{}'.format(
  sys.stdout.encoding)
)
Enter fullscreen mode Exit fullscreen mode
  • Windows 執行結果:
  ❯ python .\print_encoding.py
  locale.getpreferredencoding():  cp950
  sys.getfilesystemencoding():    utf-8
  sys.getdefaultencoding():       utf-8
  sys.stduot.encoding:            utf-8
Enter fullscreen mode Exit fullscreen mode

可以看到在繁體中文 Windows 上, 除了終端機、檔案輸出入預設使用 Big5 外, 其餘都採用 UTF-8。

  • Linux 上結果如下:
  $ python3 print_encoding.py
  locale.getpreferredencoding():  UTF-8
  sys.getfilesystemencoding():    utf-8
  sys.getdefaultencoding():       utf-8
  sys.stduot.encoding:            UTF-8
Enter fullscreen mode Exit fullscreen mode

完全都採用 UTF-8。

使用 print() 輸出文字

在預設的情況下, print() 會依照平台的設定輸出符合編碼的文字, 因此可以正常顯示輸出的文字, 例如以下的程式不論是在哪一種環境下輸出都是正確的:

# test_print.py
print('測試')
Enter fullscreen mode Exit fullscreen mode
  • 在 Windows 的 PowerShell 下:
  ❯ python test_print.py
  測試
Enter fullscreen mode Exit fullscreen mode
  • 在 Windows 的命令提示字元 (cmd.exe) 下:
  >python test_print.py
  測試
Enter fullscreen mode Exit fullscreen mode
  • 在 Linux 的 zsh 下:
  $ python3 test_print.py
  測試
Enter fullscreen mode Exit fullscreen mode

轉向儲存到文字

如果你將輸出結果轉向儲存到文字, 就會開始不一樣了, 我們分別將上述執行結果利用 > 轉向到文字檔案, 然後看看個別檔案的大小 (我們分別以 cmd、ps、zsh 代表在 Windows 下的 cmd.exe、PowerShell 以及 Linxu 下的 zsh):

❯ ls out*

    Directory: D:\code\test_ampy

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---         2021/7/13 下午 01:59              6 out_cmd.txt
-a---         2021/7/13 下午 01:59              8 out_ps.txt
-a---         2021/7/13 下午 02:00              7 out_zsh.txt
Enter fullscreen mode Exit fullscreen mode

你會發現在這 3 個環境下轉存的檔案大小各不相同:

  • 在 cmd.exe 下中文 Windows 的預設編碼是 Big5, Big5 中單一中文字佔 2 個位元組, 所以檔案中儲存的是 2 個中文字外加結尾的 Windows 換行標示 \x0D\x0A 總共 6 個位元組, 使用 16 進位模式觀察就很清楚了:
  B4 FA B8 D5 0D 0A 
Enter fullscreen mode Exit fullscreen mode

其中 \xB4\xFA 是『』、\xB8\xD5 是『』。

個別字元的 Big5 編碼可在全字庫查詢。

  • 在 Linux 下預設的文字編碼是 UTF-8, 『測試』這 2 個中文字在 UTF--8 下各佔 3 個位元組, 而換行是 \x0A, 所以總共 7 個位元組, 16 進位的內容如下:
  E6 B8 AC E8 A9 A6 0A 
Enter fullscreen mode Exit fullscreen mode

其中 \xE6\xB8\xAC 是『』、\xE8\xA9\xA6 是『』。

單一中文字的 UFT-8 編碼可用Unihan Database 查詢;中文字串的 UFT-8 編碼則可使用 UTF-8 encoder/decoder 網頁查詢。

  • PowerShell 比較特別, > 其實是 Out-File 內建指令, 如果沒有特別使用 -encoding 指定文字編碼, 預設會將文字轉成 UTF-8 編碼 後存檔, 只是 Windows 下換行是 \x0D\x0A, 16 進位的內容如下:
  E6 B8 AC E8 A9 A6 0D 0A  
Enter fullscreen mode Exit fullscreen mode

強制輸出 UTF-8 編碼的結果

如果你想強制程式輸出 UTF-8 編碼的文字, 可以有幾種作法。

使用 -X utf8 選項讓 Python 強制採用 UTF-8 編碼

執行 Python 環境時可以加上額外的 -X utf8 選項, 這會讓 Python 在輸出入時都採用 UTF-8 作為預設的文字編碼:

  • 在剛剛輸出 Big5 的 cmd.exe 下使用此選項:
  >python -X utf8 test_print.py
  測試
Enter fullscreen mode Exit fullscreen mode

看起來好像一樣, 但其實轉存到檔案就會發現不一樣了:

  >type out_cmd.txt
  測試

  >python -X utf8 test_print.py > out_cmd.txt

  >type out_cmd.txt
  皜祈岫
Enter fullscreen mode Exit fullscreen mode

原本用 type 指令可以顯示正確的檔案內容, 但加上 -X utf8 選項重新轉存檔案後用 type 看到的內容變得莫名其妙, 我們以 16 進位模式看一下實際檔案內容:

  E6 B8 AC E8 A9 A6 0D 0A
Enter fullscreen mode Exit fullscreen mode

原來檔案內容是用 UTF-8 編碼的『測試』加換行, 可是 cmd.exe 下的 type 指令把檔案內容用 Big5 編碼來解譯, 所以把 \xE6\xB8 當一個字, 變成『』;\xAC\xE8 當一個字, 變成『』;\xA9\xA6 也當一個字, 變成『』。

如果我們把字碼頁切換到代表 UTF-8 編碼的 65001, 再重新使用 type 指令檢視檔案內容:

  >chcp 65001
  Active code page: 65001

  D:\code\test_ampy>type out_cmd.txt
  測試

Enter fullscreen mode Exit fullscreen mode

就可以看到用 UTF-8 正確解譯檔案內容的結果了。為了後續實驗的正確性, 請記得將字元碼換回代表 Big5 的 950:

  >chcp 950
  Active code page: 950
Enter fullscreen mode Exit fullscreen mode
  • 在 PowerShell 下則為有類似的結果:
  ❯ python -X utf8 print.py > out_ps.txt
  ❯ type out_ps.txt
  皜祈岫
Enter fullscreen mode Exit fullscreen mode

看起來好像跟剛剛 cmd.exe 下的結果一樣, 可是如果觀察一下檔案大小:

  ❯ ls out_ps.txt

      Directory: D:\code\test_ampy

  Mode                 LastWriteTime         Length Name
  ----                 -------------         ------ ----
  -a---         2021/8/10 上午 09:33             11 out_ps.txt
Enter fullscreen mode Exit fullscreen mode

竟然是 11 個位元組, 單看文字看不出所以然, 用 16 進位模式看一下:

  E7 9A 9C E7 A5 88 E5 B2 AB 0D 0A
Enter fullscreen mode Exit fullscreen mode

這其實真的是 UTF-8 編碼的文字, 其中 \xE7\x9A\x9C 是『』、\xE7\xA5\x88 是『』、\xE5\xB2\xAB 是『』。但是我們明明輸出的是 UTF-8 編碼的『測試』, 為什麼會變成是 UTF-8 編碼的『皜祈岫』呢?

這是因為前面提過, PowerShell 的轉向存檔其實是 out-file 這個內部指令, 它會依據 \[Console\]::OutputEncoding 的設定來解讀輸入的文字:

  ❯ [console]::OutputEncoding

  EncodingName      : Chinese Traditional (Big5)
  WebName           : big5
  HeaderName        : big5
  BodyName          : big5
  Preamble          :
  WindowsCodePage   :
  IsBrowserDisplay  :
  IsBrowserSave     :
  IsMailNewsDisplay :
  IsMailNewsSave    :
  IsSingleByte      : False
  EncoderFallback   : System.Text.InternalEncoderBestFitFallback
  DecoderFallback   : System.Text.InternalDecoderBestFitFallback
  IsReadOnly        : False
  CodePage          : 950
Enter fullscreen mode Exit fullscreen mode

由於預設是 Big5 編碼, 因此原本 UTF-8 編碼輸出的『測試』就被兩個位元組一對當成 Big5 編碼解譯, 變成『皜祈岫』, 然後再將這 3 個字用預設的 UTF-8 編碼寫入檔案, 最後就變成我們看到的樣子了。

如果修改設定, 就可以讓 Out-File 正確解譯輸入的文字:

  ❯ [Console]::OutputEncoding = [text.encoding]::UTF8
  ❯ python -X utf8 print.py > out_ps.txt
  ❯ type out_ps.txt
  測試
  ❯ ls .\out_ps.txt

      Directory: D:\code\test_ampy

  Mode                 LastWriteTime         Length Name
  ----                 -------------         ------ ----
  -a---         2021/8/10 上午 09:40              8 out_ps.txt
Enter fullscreen mode Exit fullscreen mode

測試完請記得改回預設值, 才能讓後續的實驗正確:

  ❯ [Console]::OutputEncoding = [text.encoding]::GetEncoding('big5')
Enter fullscreen mode Exit fullscreen mode
  • 在 Linux 下因為是全 UTF-8 環境, 所以有沒有加 -X utf8 選項都一樣。

使用 sys.stdout.buffer 輸出個別位元組

使用 -X utf8 選項會讓所有的文字輸出入都採用 UTF-8, 如果只是希望某次輸出文字時強制輸出 UTF-8 編碼, 可以使用底層的 sys.stdout.buffer, 例如:

# test_buf_write.py
import sys

sys.stdout.buffer.write('測試\n'.encode('UTF-8'))
Enter fullscreen mode Exit fullscreen mode

我們先將字串轉成以 UTF-8 編碼的位元組串, 然後再利用 write() 一個個位元組輸出, 執行結果如下:

❯ python test_buf_write.py
測試
Enter fullscreen mode Exit fullscreen mode

看起來很正常, 不過魔鬼藏在細節中, 如果我們一樣將輸出結果轉向到檔案中, 再觀察一下個別檔案的長度:

❯ ls out*

    Directory: D:\code\test_ampy

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---         2021/7/13 下午 02:37              7 out_cmd.txt
-a---         2021/7/13 下午 02:37             11 out_ps.txt
-a---         2021/7/13 下午 02:37              7 out_zsh.txt
Enter fullscreen mode Exit fullscreen mode
  • 在 cmd.exe 和 zsh 下檔案都是 UTF-8 編碼的『測試』加上用 \x0A 表示的換行, 所以總共是 7 位元組。

  • 在 PowerShell 下的檔案卻是奇怪的 11 個位元組?如果使用 Get-Content 指令看看檔案內容:

  ❯ Get-Content out_ps.txt
  皜祈岫
Enter fullscreen mode Exit fullscreen mode

這就跟剛剛使用 -X utf8 選項時一樣, PowerShell 的 out-file 把原本 UTF-8 編碼輸出的『測試』兩兩一對當成 Big5 編碼解譯, 變成『皜祈岫』, 然後再將這 3 個字用預設的 UTF-8 編碼寫入檔案。另外, 換行的 \0x0A 也是因為 Out-File 的關係, 幫我們轉成 Windows 系統的 \0x0D\0x0A 了

除了修改 [Consoel]::OutputEncoding 設定外, 你也可以做個實驗, 幫 out-file 加上選項, 讓它存檔時不要使用 UTF-8, 改用 Big5, 這會讓它以為輸入以及輸出的編碼都是 Big5, 因而原封不動將輸入的內容轉存到檔案中:

  ❯ python test_buf_write.py | out-file -Encoding Big5 out_ps.txt
  D:\code\test_ampy
  ❯ Get-Content .\out_ps.txt
  測試
Enter fullscreen mode Exit fullscreen mode

Windows 下 Python 對終端機的特別處理

你可能會想說 Windows 終端機預設使用的是 Big5 編碼, 那如果使用 sys.stdout.buffer 直接送出 Big5 編碼後的位元組資料, 是不是就剛剛好呢?我們把剛剛使用過的 test_buf_write.py 修改成這樣, 讓我們可以從指令行透過參數指定編碼:

import sys
enc = 'UTF-8'
if len(sys.argv) > 1:
    enc = sys.argv[1]

sys.stdout.buffer.write('測試\n'.encode(enc))
Enter fullscreen mode Exit fullscreen mode

Python 可用的編碼可參考這裡

若不加參數, 預設使用 UTF-8, 以下是指定 big5 的結果:

❯ python test_buf_write.py big5
����
Enter fullscreen mode Exit fullscreen mode

咦?Windows 終端機預設使用 Big5 編碼, 為什麼直接送出 Big5 編碼不行呢?如果將程式輸出結果轉存到檔案呢?

❯ python test_buf_write.py big5 > big5.txt
D:\code\test_ampy
❯ type big5.txt
測試
Enter fullscreen mode Exit fullscreen mode

轉存到檔案是正確的, 那為什麼輸出到螢幕上是錯的呢?這就是 Python 在 Windows 上實作時的特別處理。

Windows 專用的 _io._WindowsConsoleIO 類別

sys.stdout.buffer 實際上是依靠底層的 sys.stdout.buffer.raw 跟終端機溝通, 這個 raw 在 Window 與 Linux 上是不同類別的物件:

>>> import sys
>>> sys.platform
'win32'
>>> type(sys.stdout.buffer.raw)
<class '_io._WindowsConsoleIO'>
>>>
Enter fullscreen mode Exit fullscreen mode

但若是 Linux 下:

>>> import sys
>>> sys.platform
'linux'
>>> type(sys.stdout.buffer.raw)
<class '_io.FileIO'>
>>>
Enter fullscreen mode Exit fullscreen mode

Windows 上專用的這個 _io._WindowsConsoleIO 類別, 在實作上使用 Win32 API 中的 MultiByteToWideChar() 函式 轉換要送給終端機的文字, 這個函式只能接受 UTF-8 編碼的文字, 如果送非 UTF-8 編碼的文字, 就會轉成 '\uFFFD', 代表不合法的文字, 會顯示為�。轉換好的文字會再透過 WriteConsoleW() 送給終端機顯示。

因此, 當我們直接透過 sys.stdout.buffer 送出 Big5 編碼的文字時, 因為不符合 UTF-8 的編碼, 所以 2 個中文字共 4 個位元組就被個別當成 4 個不合法的字元, 送到終端機上就顯示成 ���� 了。

Python 在 Windows 上終端機特別處理的相關細節可參考官網上的說明

轉存到檔案時的不同處理

你可能會想到, 剛剛轉存到檔案時不是正常嗎?這是因為 Python 會依據實際輸出目的地是終端機還是檔案, 讓 sys.stdout.buffer.raw 採用不同的類別, 我們以底下的程式觀察:

#print_raw_type.py
import sys
print(type(sys.stdout.buffer.raw))
Enter fullscreen mode Exit fullscreen mode

直接執行的結果如同前面所提到, 是 _io._WinodwsConsoleIO 類別的物件:

❯ python .\print_raw_type.py
<class '_io._WindowsConsoleIO'>
Enter fullscreen mode Exit fullscreen mode

但若是轉向將輸出存檔, 就變成跟在 Linux 下一樣是 _io.FileIO 類別的物件了:

❯ python .\print_raw_type.py > type.txt
❯ type type.txt
<class '_io.FileIO'>
Enter fullscreen mode Exit fullscreen mode

這時即使輸出以 Big5 編碼過的文字, 也不會因為 MultiByteToWideChar() 函式的限制而變成不合法的文字, 送什麼就是什麼。

不要啟用 Windows 上對終端機的特別處理

我們可以透過一個環境變數 PYTHONLEGACYWINDOWSSTDIO 來讓 Python 不要啟用特別的處理, 只要設定此環境變數為任意字串即可。以下以 cmd.exe 為例:

>set PYTHONLEGACYWINDOWSSTDIO=NO

>python test_buf_write.py big5
測試

>python test_buf_write.py
皜祈岫
Enter fullscreen mode Exit fullscreen mode

你可以看到, 設定環境變數後, 送出 Big5 編碼的文字可以正常顯示, 但是送出 UTF-8 編碼的文字反而會被當成 Big5 解譯成 3 個字了。

>set PYTHONLEGACYWINDOWSSTDIO=

>python test_buf_write.py big5
����
Enter fullscreen mode Exit fullscreen mode

一旦移除該環境變數, 就又改回特別處理, Big5 送出編碼的文字就無法正常顯示了。

在 PowerShell 上也可以進行相同的實驗:

❯ Set-Item Env:\PYTHONLEGACYWINDOWSSTDIO "NO"
❯ python .\test_buf_write.py big5
測試
❯ python .\test_buf_write.py
皜祈岫
❯ Remove-Item Env:\PYTHONLEGACYWINDOWSSTDIO
❯ python .\test_buf_write.py big5
����
Enter fullscreen mode Exit fullscreen mode

讀寫檔案

前面提過, locale.getpreferredencoding() 除了控制終端機的文字編碼外, 也控制檔案讀寫時的編碼, 在 Windows 上一樣預設是 Big5。

寫檔

為了能夠控制寫檔時採用的文字編碼, open() 現在多了一個 encoding 參數, 以底下的程式為例:

# test_file_write.py
import sys

if len(sys.argv) > 1:
    f = open('out_file.txt', 'w', encoding=sys.argv[1])
else:
    f = open('out_file.txt', 'w')
f.write('測試')
f.close()
Enter fullscreen mode Exit fullscreen mode

若沒有指定參數, 在建立檔案時就不加入 encoding 參數, 採用 locale.getpreferredencoding() 的設定, 例如:

❯ python .\test_file_write.py
D:\code\test_ampy
❯ Get-Content out_file.txt
���
D:\code\test_ampy
❯ Get-Content -Encoding big5 out_file.txt
測試
Enter fullscreen mode Exit fullscreen mode

由於預設是 Big5 編碼, 所以當我們在 PowerSehll 中用 Get-Content 讀取內容時, 會嘗試以 UTF-8 解譯錯檔案內容。但是若指定以 UTF-8 解譯, 就可以看到正確的檔案內容了。如果建檔的時候指定 encoding 參數, 就可以用特定的編碼存檔:

❯ python .\test_file_write.py utf8
❯ Get-Content out_file.txt
測試
Enter fullscreen mode Exit fullscreen mode

讀取檔案

讀檔時也是一樣, 以底下的程式為例:

# test_file_read.py
import sys

if len(sys.argv) > 1:
    f = open('out_file.txt', 'r', encoding=sys.argv[1])
else:
    f = open('out_file.txt', 'r')
print(f.readline())
f.close()
Enter fullscreen mode Exit fullscreen mode

先用之前的程式建立一個以 UTF-8 編碼的檔案:

❯ python .\test_file_write.py utf8
Enter fullscreen mode Exit fullscreen mode

如果以預設的 Big5 編碼讀檔, 就會解譯錯誤, 把 2 字共 6 個位元組的內容解譯成 3 個各 2 個位元組的 Big5 編碼文字:

❯ python .\test_file_read.py big5
皜祈岫
Enter fullscreen mode Exit fullscreen mode

但若是以 UTF-8 編碼讀檔, 就一切正常了:

❯ python .\test_file_read.py utf8
測試
Enter fullscreen mode Exit fullscreen mode

互動介面的歷史檔

如果你有安裝 pyreadline 模組, 在啟動 Python 互動介面時會改用 pyreadline 模組讀取操作過程記錄的歷史檔, 這個檔案位在使用者資料夾下的 .histoty_file, 不過它有個問題, pyreadline 預設會採用 sys.stdout.encoding(在 Windows 上預設是 UTF-8) 為文字編碼, 寫檔實是採用先編碼後再以二進位模式寫入, 但是讀檔時卻沒有指定編碼, 導致歷史檔中若含有中文, 就可能會遇到類似這樣的錯誤訊息

❯ python
Python 3.9.4 (tags/v3.9.4:1f2e308, Apr  6 2021, 13:40:21) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more informaTraceback (most recent call last):
  File "D:\Program Files\Python39\lib\site.py", line 449, in register_readline
main.py", line 165, in read_history_file
    self.mode._history.read_history_file(filename)
  File "D:\Program Files\Python39\lib\site-packages\pyreadline\lineeditor\history.py", line 82, in read_history_file
    for line in open(filename, 'r'):
UnicodeDecodeError: 'cp950' codec can't decode byte 0x93 in position 278: illegal multibyte sequence
Enter fullscreen mode Exit fullscreen mode

這是因為在我的歷史檔中有這樣一行:

ans = input("姓名:")
Enter fullscreen mode Exit fullscreen mode

其中『』的 UTF-8 編碼是 \0xE5\0xA7\0x93, 但因為中文 Windows 下預設讀檔是採用 Big5 編碼, 所以前面的 \0xE5\0xA7 被當成 1 個中文字, 而 \0x93 並不符合 Big5 編碼高位元組只能使用 0xA1~0xFE 的規範, 所以在解碼時就發生錯誤。

如果改用 -X utf8 強制使用 UTF-8 模式, 就可以正常讀取不會出錯:

❯ python -X utf8
Python 3.9.4 (tags/v3.9.4:1f2e308, Apr  6 2021, 13:40:21) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
Enter fullscreen mode Exit fullscreen mode

或者如果你其實不會用到 pyreadline, 也可以將之移除。

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more