想定環境
- Windows:日本語版
- Python:3.6.5
- kivy:1.10.0
現象
Windowsでデフォルト設定から変更せず、kvファイルを書く・もらってくる・コピペするなどし、kvファイルに日本語が混ざった場合、実行すると以下のエラーが表示されます。
Traceback (most recent call last):
File "main.py", line 22, in <module>
mw.run()
File "E:\app\create\Python365\lib\site-packages\kivy\app.py", line 801, in run
self.load_kv(filename=self.kv_file)
File "E:\app\create\Python365\lib\site-packages\kivy\app.py", line 598, in load_kv
root = Builder.load_file(rfilename)
File "E:\app\create\Python365\lib\site-packages\kivy\lang\builder.py", line 290, in load_file
data = fd.read()
UnicodeDecodeError: 'cp932' codec can't decode byte 0x83 in position 38: illegal multibyte sequence
原因
Pythonのopen関数はテキストファイル読み込み処理時にエンコーディングの指定が無い場合、プラットフォーム依存のエンコーディングでデコードしようとし、WindowsではテキストファイルのデフォルトエンコーディングがShift_JIS(≒cp932)であるため、ascii範囲外のバイトコードが入ったutf-8で書かれたkvファイルはデコードエラー=読み込み失敗となります。
概要 で触れられているように、Python はバイナリとテキストの I/O を区別します。(mode 引数に 'b' を含めて) バイナリモードで開かれたファイルは、内容をいかなるデコーディングもせずに bytes オブジェクトとして返します。(デフォルトや、 mode 引数に 't' が含まれたときの) テキストモードでは、ファイルの内容は str として返され、バイト列はまず、プラットフォーム依存のエンコーディングか、encoding が指定された場合は指定されたエンコーディングを使ってデコードされます。
2. 組み込み関数 — Python 3.6.5 ドキュメント
対策
あまりやりたくなかった方法なのですが、どうやらこれしか手がなさそうなので仕方がありません。
kivyが正常に動作するなら、Pythonをインストールしたディレクトリの、Lib/site-packages/kivy
にkivyのコードが一式揃っています。
この中から Lib/site-packages/kivy/lang/builder.py
を変更します。
GitHubにIssuesとPull Requestが出ており、
Try first utf-8 encoding by default on reading .kv files. Fixes #5154 · jsbueno/kivy@ea255da
まだ採用されていないのでこれを取り込みます。
変更後ファイルはこちら
kivy/builder.py at ea255dac3f0848a1e3c2101d45cf7f770a4b8e25 · jsbueno/kivy
# 前略 kviy1.10.0では276行目あたり
def load_file(self, filename, **kwargs):
'''Insert a file into the language builder and return the root widget
(if defined) of the kv file.
:parameters:
`rulesonly`: bool, defaults to False
If True, the Builder will raise an exception if you have a root
widget inside the definition.
'''
filename = resource_find(filename) or filename
if __debug__:
trace('Lang: load file %s' % filename)
with open(filename, 'r') as fd:
kwargs['filename'] = filename
data = fd.read()
# remove bom ?
if PY2:
if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
raise ValueError('Unsupported UTF16 for kv files.')
if data.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)):
raise ValueError('Unsupported UTF32 for kv files.')
if data.startswith(codecs.BOM_UTF8):
data = data[len(codecs.BOM_UTF8):]
return self.load_string(data, **kwargs)
# 後略
ここを
# 前略
def load_file(self, filename, encoding=None, **kwargs):
'''Insert a file into the language builder and return the root widget
(if defined) of the kv file.
:parameters:
`rulesonly`: bool, defaults to False
If True, the Builder will raise an exception if you have a root
widget inside the definition.
`encoding`: File charcter encoding. Defaults to utf-8,
if not given, and utf-8 yields an encoding error, attempts
loading the file as the native system encoding as
given by `sys.getdefaultencoding()`
'''
filename = resource_find(filename) or filename
if __debug__:
trace('Lang: load file %s' % filename)
if encoding is None:
encoding_given = False
encoding = 'utf-8'
else:
encoding_given = True
kwargs['filename'] = filename
try:
with open(filename, 'r', encoding=encoding) as fd:
data = fd.read()
except UnicodeDecodeError:
if encoding_given:
# Don't try to guess encoding if it was passed explicitly
raise
Logger.warning(
'File "{0}" failed to open with "utf-8" codec. '
'Trying native system encoding now. Note that '
'this may cause text differences when the project '
'is run in other systems'.format(filename)
)
with open(filename, 'r') as fd:
data = fd.read()
# remove bom ?
if PY2:
if data.startswith((codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE)):
raise ValueError('Unsupported UTF16 for kv files.')
if data.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)):
raise ValueError('Unsupported UTF32 for kv files.')
if data.startswith(codecs.BOM_UTF8):
data = data[len(codecs.BOM_UTF8):]
return self.load_string(data, **kwargs) # インデント一つ減らす
# 後略
変更前は with open(filename, 'r') as fd:
の行にencodingの指定がなく、そのせいで次の行 data = fd.read()
でデコードエラーが発生して落ちていました。
変更後は load_file
メソッドで引数 encoding
を取れるようにし、指定があればその値を、指定がなければ 'utf-8' を指定して with open(filename, 'r', encoding=encoding) as fd:
するようになっています。
また、Pull Requestに含まれていないのですが、最後の行の return self.load_string(data, **kwargs)
の部分で、この行のインデントを一つ減らさないと正常に動作しませんのでご注意ください。
(2018/09/15追記:最後の行のインデント忘れて動かないやんやらかしたので追記)
ダメだった対策 1
個別のファイル読み込みで文字コード指定が出来ないなら、全体の文字コード指定を書き換えたらいけるんちゃうのと思って試してみました。
import sys, importlib
importlib.reload(sys)
sys.setdefaultencoding('utf8')
Python3ではこう書けばいいみたいな記事があったのですが、そもそもPython3では内部的にだいたい全部Unicodeで処理するようになっていて sys.setdefaultencoding
メソッドが無くなっており、文法エラーになります。
ダメだった対策 2
import locale
locale.setlocale(locale.LC_ALL, 'ja_JP.utf-8')
ロケールの設定なので文字コードの影響範囲もロケールの表示だけ。
感想
春前くらいにMicrosoftがWindowsのデフォルトエンコーディングをUTF-8にするとか何とかという噂を聞いた(正しくはTwitterで見かけた)ような気がしていたのでggってみたのですが、どうやら眉唾だったようです。
そうなればPythonもkivyも変更せずとも解消され、Python勢kivy勢だけでなく全方位の全プログラマが喜ぶところだと思うのですが、後方互換性を大事にするMicrosoftのことなので、まだ当分は変更しなさそうなのかなぁと思いました。
kivyに前述のプルリクエストが取り込まれるまでは、自前でkivyのコードを書き換えていくしかなさそうです。
参考資料
Python 3の各種エンコーディングについて - Qiita