にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

Python 3でcp932コマンドプロンプトに非cp932文字を含む文字列をprintする

Windowsバッチ処理用にPythonスクリプトを組んでいる。ドラッグ&ドロップで処理できるように、まず.batファイルを作成しそこからPythonスクリプトにドロップされたファイル名が渡される様にしている。

ここで、厄介なのが、コマンドプロンプトエンコーディングがcp932(Shift_JISWindows拡張)だということである。このため、Python 3からcp932外の文字をprint関数で出力しようとした場合、UnicodeEncodeErrorを吐いて終了する。
これをどうにかしたい。

なお、Python2系と3系では文字列型が大きく変更されているため、Python 2系の方法を3系で使うことはできないし、逆もまた然りである。これは3系用のもの。

対策

環境


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

とか。