(Windows) Python3でのUnicodeEncodeErrorの原因と回避方法

# 背景

strがUnicodeになってしまったので、
従来のShift-JISに代表されるようなCP932はどうなっているのか？

Windowsで標準出力する際に、ascii変換エラーになってしまう場合。
あれは一体なんなのか、それを整理した。


# 環境

Windows
Python3 (Anaconda3)

# WindowsとPythonとエンコーディング

## Python文字列のエンコーティング

Python3では、文字列に関する型は2種類ある。
- str型(Unicode専用)
- byte型(任意のエンコーディング)

strは、UTF-8専用。それ以外のエンコーディング文字列は格納できない。
一方、byteは任意の円コーティング文字列を格納可能。もちろんUTF-8も可能。
strからbyteに変換するには encode()、逆は、decode() で変換可能。
どっちがどっちか分からなくなったら、dir(str)をすれば分かる。Python2の時みたいに2種類の関数は存在しない。

尚、Python2では、str型とunicode型がある。

```
Python3内部                  Windows標準出力(入力)
==========                  ===================

  UTF-8  ---------------------->  CP932
 (str型)   str.encode('CP932')   (byte型)
         <----------------------
           byte.decode('CP932')
```

## Windowsのエンコーディング

Windowsの標準出力では、CP932というエンコーディングが採用されている。このため、str文字列を標準出力したりファイルに書き出したりする場合は、標準ではCP932への変換が自動的に動作する。



# printできない原因は何か？

実は、Pythonは明示的に変換しなくても、標準出力等をする場合、自動的にシステムのエンコーディングに変換してから出力しようとする。

Windowsの場合は、CP932へ変換しようとするため、CP932に変換出来ない場合は、UnicodeEncodeError例外が発生する。

``` 
>>> s = '\xa0'
>>> print(s)

>>> s.encode('utf-8')
b'\xc2\xa0'
>>> s.encode('cp932')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'cp932' codec can't encode character '\xa0' in position 0: illegal multibyte sequence
```

# 悪さをしているコードを消しちゃえ

UnicodeEncodeErrorの原因は、CP932へ変換できないコードが含まれている為なので、その悪さをしているコードを消してしまえば解決するのではないか。

この場合は、\xa0が悪さしているので、replace関数で置換すると、例外エラーが出なくなる。

```
>>> s
'\xa0'
>>> s.encode('cp932')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'cp932' codec can't encode character '\xa0' in position 0: illegal multibyte sequence
>>> s2 =s.replace('\xa0', '')
>>> s2.encode('cp932')
b''
```

# 悪さをしているコードを無視しちゃえ

CP932へ変換出来ないコードを網羅的に対応するのは面倒だし、漏れやすい。そもそも、encode関数に変換出来ない場合は無視するオプションがあるのではないか、と思ってググってみると、ignoreオプションが有った。

【参考】バイト列への変換
https://docs.python.jp/3/howto/unicode.html
（ignore以外にも、replaceやnamereplaceなどが有ります）

ignoreオプションを利用して例外エラーを抑止した例。

```
>>> s.encode('cp932')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'cp932' codec can't encode character '\xa0' in position 0: illegal multibyte sequence
>>> s.encode('cp932', "ignore")
b''
```

# まとめ

\xa0等が含まれている文字列は、Python3ではUTF-8となって内部管理されているため、Python内では問題なく処理ができるが、Windows環境でCP932へ変換しなければならないケース、例えば、標準出力する際や、ファイル出力する際には、
Unicode --> CP932
への変換処理が走る。
その際に、UnicodeEncodeErrorになるので、一度ignoreオプション付きでencodeしてbyte型へ変換し、decodeでstrに戻しておけば、次からはUnicodeEncodeErrorを回避できる。また、ファイルへ書き出す際にはバイナリモードでないとbyte型を出力できないので、ファイルopen時にバイナリモード（'w'や'a'ではなく'wb'や'ab'）を指定する。
尚、codecsを使ったopenの場合は、open時にエンコーディングとignoreオプションを指定でき、str型のまま出力できる。


標準出力する例：

```
import codecs
s = '\xa0'
b = s.encode('cp932', "ignore")
s_after = b.decode('cp932')
print(s_after)
```

ファイル出力する例：

```
f = open('test', 'ab')
s = '\xa0'
b = s.encode('cp932', 'ignore')
f.write(b)
f.close()
```

codecsを利用したファイル出力する例：

```
import codecs
f = codecs.open('test', 'ab', 'cp932', 'ignore')
s = '\xa0'
f.write(s) # codecsを使うとstrのままwriteできる
f.close()
```

# 参考

Python3 Unicode HOWTO
https://docs.python.jp/3/howto/unicode.html

CP932とUTF-8
https://android.googlesource.com/toolchain/benchmark/+/master/python/src/Modules/cjkcodecs/README