Windowsでバッチ処理用にPythonスクリプトを組んでいる。ドラッグ&ドロップで処理できるように、まず.batファイルを作成しそこからPythonスクリプトにドロップされたファイル名が渡される様にしている。
ここで、厄介なのが、コマンドプロンプトのエンコーディングがcp932(Shift_JISのWindows拡張)だということである。このため、Python 3からcp932外の文字をprint関数で出力しようとした場合、UnicodeEncodeErrorを吐いて終了する。
これをどうにかしたい。
なお、Python2系と3系では文字列型が大きく変更されているため、Python 2系の方法を3系で使うことはできないし、逆もまた然りである。これは3系用のもの。
対策
環境
- Windows 10 64bit
- Python 3.5.1
Python 3系にのみ対応する。
次のコードをプログラムの最初に書く。
import io, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=sys.stdout.encoding, errors='backslashreplace', line_buffering=sys.stdout.line_buffering)
io.TextIOWrapperはバイナリバッファをテキストIOストリームとして扱えるようにするものらしい。ここでは標準出力のバッファ(sys.stdout.buffer)を指定している。
encodingオプションに標準出力のエンコーディング(sys.stdout.encoding)を指定している。
今回大事なのはerrorsオプションで、encodingで指定されたエンコーディングに含まれない文字の扱いを指定する。ここが標準の'strict'だとエラーを吐いて停止する。'\u2049'の様に表示される'backslashreplace'か、全く無視する'ignore'、'?'などで置き換える'replace'、'⁉'の様になる'xmlcharrefreplace'、'\N{EXCLAMATION QUESTION MARK}'の様になる'namereplace'のいずれかを指定する。
line_bufferingはデフォルトでFalseだが、これをTrueにしないとプログラムの最後にまとめて出力されたりするので、もとの値を継承するようにした。
これはちょっとした確認用なので、表示できないデータが失われても問題ない場合だからこの方法でいいが、それで困る場合は素直にコマンドプロンプトのエンコーディングを変えた方がよさそう。
おまけ: cp932外文字判別
ある文字列が、cp932に含まれない文字を含むか判定するには、
def iscp932encodable(s): return s.encode('cp932', errors='ignore').decode('cp932') == s
とか。要するに一度cp932に変換してもとに戻してcp932に変換できない文字を消してしまって、それと元の文字列を比較しているというもの。
cp932に含まれない文字の集合を抽出するには、
def notcp932encodable(s): return set(s) - set(s.encode('cp932', errors='ignore').decode('cp932'))
とか。