Python
Spark

PySparkのソースコードにマルチバイト文字を使うとハマるかも

環境

  • Linux
  • Python 2.7.8 & IPython
  • Spark 2.1.0

Pythonのソースコードに日本語使うときって

例えばこんな感じで1行目に文字エンコーティング(文字コード)を宣言するんでしたよね。
(Shebangを1行目に書く場合は、2行目でエンコーディングを宣言)

sample_OK.py
# -*- coding: utf-8 -*-

# 文字列を代入する
x = "こんにちは"

(参考: https://qiita.com/ronin_gw/items/2c82b727461b18991eff)

この宣言をしないと、実行時にこんな感じで怒られてしまいます。

sample_NG.py
# -*- coding: utf-8 -*-

# 文字列を代入する
x = "こんにちは"
terminal
$ python sample_NG.py
  File "sample_NG.py", line 1
SyntaxError: Non-ASCII character '\xe6' in file sample_NG.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

ここまでは、普通のPythonの話です。

これをPySparkでやったところ

実行時エラーが起こった時に、やらかしたエラーの内容が出ずに上のようなエンコーディングエラーが出てしまいます。エンコーディング宣言したのに。
こんなエラーを見せられたところで、何が悪いか全くわかりません。

pyspark-app.py
# -*- coding: utf-8 -*-                                                                                                                        

import pyspark

def main():
    spark = pyspark.sql.SparkSession.builder.getOrCreate()

    # なんかやらかした
    x = float("abc")

    spark.stop()

if __name__ == "__main__":
    main()
terminal
$ spark-submit pyspark-app.py
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/zookeeper/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
(中略...)
Traceback (most recent call last):
  File "/usr/local/bin/ipython", line 11, in <module>
    sys.exit(start_ipython())
  File "/usr/local/lib/python2.7/site-packages/IPython/__init__.py", line 119, in start_ipython
    return launch_new_instance(argv=argv, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/traitlets/config/application.py", line 657, in launch_instance
    app.initialize(argv)
  File "<decorator-gen-109>", line 2, in initialize
  File "/usr/local/lib/python2.7/site-packages/traitlets/config/application.py", line 87, in catch_config_error
    return method(app, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/IPython/terminal/ipapp.py", line 315, in initialize
    self.init_code()
  File "/usr/local/lib/python2.7/site-packages/IPython/core/shellapp.py", line 273, in init_code
    self._run_cmd_line_code()
  File "/usr/local/lib/python2.7/site-packages/IPython/core/shellapp.py", line 396, in _run_cmd_line_code
    self.shell.showtraceback(tb_offset=4)
  File "/usr/local/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 1826, in showtraceback
    self._showtraceback(etype, value, stb)
  File "/usr/local/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 1844, in _showtraceback
    print(self.InteractiveTB.stb2text(stb))
UnicodeEncodeError: 'ascii' codec can't encode characters in position 507-514: ordinal not in range(128)

If you suspect this is an IPython bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@scipy.org
(以下略...)

原因

PYSPARK_DRIVER_PYTHONipythonを指定していると、IPythonがエンコーディング宣言を認識できずにエラーになるみたいです。
IPythonの方が対話的にいろいろ試したい時に便利ですし、エラー表示もわかりやすいのですが…。
以下のようにPYSPARK_DRIVER_PYTHONがただのpythonなら、普通にエラーの内容が見えます。

terminal
$ PYSPARK_DRIVER_PYTHON=python spark-submit pyspark-app.py
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/zookeeper/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
(中略...)
Traceback (most recent call last):
  File "/home/foo/pyspark-app.py", line 15, in <module>
    main()
  File "/home/foo/pyspark-app.py", line 10, in main
    x = float("abc")
ValueError: invalid literal for float(): abc
(以下略...)

なぜIPythonでだけハマるかは、あとでわかります。

対策

~/.local/lib/python-2.7/site-packages/usercustomize.pyというファイルを作成し

usercustomize.py
import sys
sys.setdefaultencoding('utf-8')

と書いて保存しましょう。
フォルダやファイルが存在しなければ、作成してください。
(参考: https://qiita.com/tukiyo3/items/06c0821e5002eb73d43f)

これにより、IPython経由で実行したときでもエラー箇所がわかるようになります。

terminal
ValueErrorTraceback (most recent call last)
/home/foo/pyspark-app.py in <module>()
     13 
     14 if __name__ == "__main__":
---> 15     main()

/home/foo/pyspark-app.py in main()
      8 
      9     # なんかやらかした
---> 10     x = float("abc")
     11 
     12     spark.stop()

ValueError: could not convert string to float: abc

別の方法として、環境変数PYTHONIOENCODINGを指定して実行してもOKです。

terminal
$ PYTHONIOENCODING=utf-8 spark-submit pyspark-app.py

(参考: http://methane.hatenablog.jp/entry/20120806/1344269400, https://qiita.com/FGtatsuro/items/cf178bc44ce7b068d233)

IPythonはエラー周辺のコードを表示してくれますが、これがマルチバイト文字を含んでいるので、エラー文字列出力処理でエラーになる、という厄介な状態になってしまっていたわけですね。

ついでに

PySparkはエンコーディング宣言を認識してくれないかもしれませんが、Emacsなどのエディタがエンコーディングの自動判別に使ってくれるので、個人的には常に書いておくことをお勧めしたいです。