LoginSignup
4
2

More than 1 year has passed since last update.

Windowsでシステムロケールが英語のとき、`あ`を出力するpythonスクリプトの結果をファイルへリダイレクトすると、`UnicodeEncodeError`が発生する

Last updated at Posted at 2023-03-30

環境

  • Windows11 Pro(22H2)
  • コマンドプロンプト
  • Python3.11.2

概要

WindowsでPythonスクリプトを実行しています。
Pythonスクリプトでは、日本語を出力しています。

システムロケールが英語のとき、以下のコードを実行するとUnicodeEncodeErrorが発生しました。

> python -c "print('あ')"
あ

> python -c "print('あ')" > out.txt
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Users\WDAGUtilityAccount\AppData\Local\Programs\Python\Python311\Lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'charmap' codec can't encode character '\u3042' in position 0: character maps to <undefined>

その原因についてまとめた記事です。
※原因調査は同僚がやってくれました。私は同僚が調査してくれた内容を、自分の理解できる範囲でブログに書いただけです。

システムロケールについて

公式サイトには以下のように記載されています。

SystemLocale は、Unicode 対応でないプログラムで使用する既定の言語を指定します。
この設定は、Windows セットアップと Windows 展開サービスの両方で使用されます。
システム ロケールは、既定でシステムが使用するビットマップ フォントとコード ページ (ANSI または DOS) を指定します。 システム ロケール設定は、ANSI (Unicode 対応でない) アプリケーションのみに影響します。 Unicode 対応でないプログラムの言語は、システムごとの設定です。
ユーザーは、コントロール パネルの地域と言語の項目で [管理] タブを使用して、システム ロケールを変更できます。

システムロケールはsysteminfoコマンドで確認できます。

> systeminfo
...
システム ロケール:      ja;日本語
入力ロケール:           ja;日本語
...

システムロケールによって、コードページ(≒文字コード?)が決まります。
コードページはchcpコマンドで確認できます。
またPythonでは、locale.getencoding(Python3.11から利用可能)で現在のコードページを確認できます。

Get the current locale encoding:
On Android and VxWorks, return "utf-8".
On Unix, return the encoding of the current LC_CTYPE locale. Return "utf-8" if nl_langinfo(CODESET) returns an empty string: for example, if the current LC_CTYPE locale is not supported.
On Windows, return the ANSI code page.

システムロケールが日本語(ja;日本語)の場合

コードページはcp932

>chcp
現在のコード ページ: 932

>python -c "import locale; print(locale.getencoding())"
cp932

システムロケールが英語(en-us;English (United States))の場合

コードページはcp437

>chcp
Active code page: 437

>python -c "import locale; print(locale.getencoding())"
cp1252

※ Pythonのlocale.getencoding()cp1252を返しました。cp437とcp1252が同じなのかがどうか、私には分かりませんでした。。。

ファイルへリダイレクトしたときの動きを確認する

ファイルへリダイレクトしたときの動きを、システムロケールが日本語と英語の場合で確認しました。結果は以下の通りです。

システムロケール リダイレクトの元 結果
日本語 標準出力 OK
日本語 標準エラー出力 OK
英語 標準出力 UnicodeEncodeErrorが発生
英語 標準エラー出力 \u形式でファイルに出力される

システムロケールが日本語の場合

標準出力をファイルへリダイレクト

> python -c "print('あ')" > out.txt

> more out.txt
あ

標準エラー出力をファイルへリダイレクト

> python -c "import sys; print('あ', file=sys.stderr)" 2> out.txt

> more out.txt
あ

システムロケールが英語の場合

標準出力をファイルへリダイレクト

> python -c "print('あ')" > out.txt
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Users\WDAGUtilityAccount\AppData\Local\Programs\Python\Python311\Lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeEncodeError: 'charmap' codec can't encode character '\u3042' in position 0: character maps to <undefined>

標準エラー出力をファイルへリダイレクト

> python -c "import sys; print('あ', file=sys.stderr)" 2> out.txt

> more out.txt
\u3042

sys.stdout, sys.stderrの中身を確認する

以下のpythonファイルで、sys.stdout, sys.stderrの中身を確認しました。

sample.py
import sys

print("=== sys.stdout ===")
f = sys.stdout
print(f"{f.buffer.raw=}, {f.isatty()=}, {f.encoding=}, {f.errors=}")
print("=== sys.stderr ===")
f = sys.stderr
print(f"{f.buffer.raw=}, {f.isatty()=}, {f.encoding=}, {f.errors=}")
  • isatty() : ストリームが対話的か
  • errors : エンコード/デコードのエラーをどのように扱うか

システムロケールが日本語の場合

> python sample.py
=== sys.stdout ===
f.buffer.raw=<_io._WindowsConsoleIO mode='wb' closefd=False>, f.isatty()=True, f.encoding='utf-8', f.errors='surrogateescape'
=== sys.stderr ===
f.buffer.raw=<_io._WindowsConsoleIO mode='wb' closefd=False>, f.isatty()=True, f.encoding='utf-8', f.errors='backslashreplace'

> python sample.py > out.txt 2>&1
> more out.txt
=== sys.stdout ===
f.buffer.raw=<_io.FileIO name='<stdout>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='cp932', f.errors='surrogateescape'
=== sys.stderr ===
f.buffer.raw=<_io.FileIO name='<stderr>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='cp932', f.errors='backslashreplace'

システムロケールが英語の場合

> python sample.py
=== sys.stdout ===
f.buffer.raw=<_io._WindowsConsoleIO mode='wb' closefd=False>, f.isatty()=True, f.encoding='utf-8', f.errors='surrogateescape'
=== sys.stderr ===
f.buffer.raw=<_io._WindowsConsoleIO mode='wb' closefd=False>, f.isatty()=True, f.encoding='utf-8', f.errors='backslashreplace'

> python sample.py > out.txt 2>&1
> more out.txt
=== sys.stdout ===
f.buffer.raw=<_io.FileIO name='<stdout>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='cp1252', f.errors='surrogateescape'
=== sys.stderr ===
f.buffer.raw=<_io.FileIO name='<stderr>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='cp1252', f.errors='backslashreplace'

起きたことの整理

システムロケールが英語のとき、ファイルへリダイレクトすると期待する結果にならない理由

ファイルへリダイレクトしない場合は、システムロケールに関わらず標準出力/標準エラー出力のencodingutf-8です。
しかし、ファイルへリダイレクトするとencodingがシステムロケールによって決まるコードページになります。
という文字はcp1252で表現できないので、ファイルへリダイレクトすると期待する結果になりませんでした。

システムロケールが英語のとき、標準出力と標準エラー出力で結果が異なる理由

標準出力と標準エラー出力で、errorsの値が異なるためです。
標準エラー出力のとき、errorsbackslashreplaceです。backslashreplaceの動きは以下の通りです。

'backslashreplace' を指定すると、不正な形式のデータをバックスラッシュ付きのエスケープシーケンスに置換します。

一方、標準出力のときerrorssurrogateescapeです。surrogateescapeは表現できない文字をエンコードしようとするとUnicodeEncodeErrorが発生するようです(surrogateescapeの正しい動きを理解できませんでした。。。)

surrogateescapeの動きについては、以下の記事を参照ください。

標準出力と標準エラー出力でerrorsの値が異なる理由は、分かりませんでした。

対策

UTF-8 Modeを利用すれば、ファイルへリダイレクトしたときもencodingutf-8になり、期待する結果になります。

# UTF-8 Modeを有効にする
> set PYTHONUTF8=1

> python -c "print('あ')" > out.txt

>more out.txt
πüé

out.txtmoreコマンドでは化けて表示されますが、VSCodeなどのエディタで開ければと表示されます。

また、ファイルへリダイレクトする際もencodingutf-8のままです。

> python sample.py > out.txt 2>&1

>more out.txt
=== sys.stdout ===
f.buffer.raw=<_io.FileIO name='<stdout>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='utf-8', f.errors='surrogateescape'
=== sys.stderr ===
f.buffer.raw=<_io.FileIO name='<stderr>' mode='wb' closefd=False>, f.isatty()=False, f.encoding='utf-8', f.errors='backslashreplace'

まとめ

以下の条件を全て満たすとき、UnicodeEncodeErrorが発生しました。

  • Windowsでシステムロケールが英語
  • Pythonでという文字を標準出力へ出力する
  • Pythonコマンドの結果をファイルへリダイレクトする

原因は以下の通りです。

  • システムロケールを英語にすると、Pythonでコードページは437(cp1252)になる。
  • という文字はcp1252では表現できない。
  • sys.stdout.errorssurrogateescapesurrogateescapeは、表現できない文字をエンコードしようとするとUnicodeEncodeErrorが発生する。

補足

PowerShellで標準エラー出力をファイルへリダイレクトすると、NativeCommandErrorが出力される

以下のコマンドをPowerShellで実行すると、NativeCommandErrorが発生します。

PS > python -c "import sys; print('あ', file=sys.stderr)" 2> out.txt
PS > more out.txt
python : あ
発生場所 行:1 文字:1
+ python -c "import sys; print('あ', file=sys.stderr)" 2> out.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (あ:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

PowerShellによるエラーで、今回の問題とは関係ありません。

参考サイト

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2