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

# 環境
* 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 対応でないプログラムの言語は、システムごとの設定です。
>ユーザーは、コントロール パネルの地域と言語の項目で [管理] タブを使用して、システム ロケールを変更できます。

https://learn.microsoft.com/ja-jp/windows-hardware/customize/desktop/unattend/microsoft-windows-international-core-systemlocale




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

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

システムロケールによって、コードページ（≒文字コード？）が決まります。
コードページは`chcp`コマンドで確認できます。
またPythonでは、[locale.getencoding](https://docs.python.org/ja/3/library/locale.html#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](https://ja.wikipedia.org/wiki/Microsoft%E3%82%B3%E3%83%BC%E3%83%89%E3%83%9A%E3%83%BC%E3%82%B8932)


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

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


#### システムロケールが英語（en-us;English (United States)）の場合
コードページは[cp437](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%89%E3%83%9A%E3%83%BC%E3%82%B8437)


```
>chcp
Active code page: 437

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

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

http://zuga.net/articles/text-ascii-vs-cp-1252-vs-cp-437/


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

| システムロケール | リダイレクトの元 | 結果                    |
|----------|----------|-----------------------|
| 日本語      | 標準出力     | 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`](https://docs.python.org/ja/3/library/io.html#io.TextIOWrapper) : エンコード/デコードのエラーをどのように扱うか




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

```
> 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'
```


# 起きたことの整理

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

## システムロケールが英語のとき、標準出力と標準エラー出力で結果が異なる理由
標準出力と標準エラー出力で、`errors`の値が異なるためです。
標準エラー出力のとき、`errors`は`backslashreplace`です。`backslashreplace`の動きは以下の通りです。

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

https://docs.python.org/ja/3.11/library/io.html#io.TextIOWrapper

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

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

https://docs.python.org/ja/3.11/howto/unicode.html#files-in-an-unknown-encoding

https://mizunashi-mana.github.io/blog/posts/2020/11/unicode-surrogate-codepoint/

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


# 対策
[UTF-8 Mode](https://docs.python.org/ja/3/using/windows.html#utf-8-mode)を利用すれば、ファイルへリダイレクトしたときも`encoding`が`utf-8`になり、期待する結果になります。


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

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

>more out.txt
πüé
```

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

また、ファイルへリダイレクトする際も`encoding`は`utf-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.errors`は`surrogateescape`。`surrogateescape`は、表現できない文字をエンコードしようとすると`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によるエラーで、今回の問題とは関係ありません。

https://fujii.github.io/2019/05/13/powershell-stderr-errorrecord/



# 参考サイト
https://scrapbox.io/unarist/Python%E3%81%A8Windows%E3%81%A8Unicode

https://flying-foozy.hatenablog.com/entry/20140205/1391620373

https://zrbabbler.hatenablog.com/entry/20180512/1526105583

https://mizunashi-mana.github.io/blog/posts/2020/11/unicode-surrogate-codepoint/

https://methane.hatenablog.jp/entry/2022/04/26/Python_3.15%E3%81%8B%E3%82%89%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%8CUTF-8%E3%81%AB%E3%81%AA%E3%82%8A