背景
エックスサーバーで PHPからPythonスクリプトを呼び出す際に、日本語パラメータを渡すとPythonでUnicodeEncodeErrorが発生していた。
$parameters = ['あ'];
$command = array_merge(['python3', $filePath], $parameters);
$process = new Process($command);
$process->run();
Python
encoded = sys.argv[1].encode('utf-8')
print(encoded)
実行後
UnicodeEncodeError: 'utf-8' codec can't encode characters in position 0-2: surrogates not allowed
エラーを無視してエンコード
encoded = sys.argv[1].encode('utf-8', errors='surrogatepass')
print(encoded)
print("あ".encode('utf-8'))
b'\xed\xb3\xa3\xed\xb2\x81\xed\xb2\x82'
b'\xe3\x81\x82'
同じ文字列なのに結果が違っている。
昔のPythonでは日本語でトラブルが度々あったが、最近では見ない。
ChatGPTやsurrogatepass エラーで調べてもあまり出てこない。うーむ。
原因
Pythonでは、コマンドライン引数や標準入力のデコードに使用するエンコーディングがシステムロケール(LANG
環境変数)によって決定されます。
1. ロケールの影響
LANG
環境変数が未設定、または不適切に設定されている場合、Pythonは標準エンコーディングをascii
またはC
ロケールとして扱う。
日本語を含む非ASCII文字は、この設定ではサポートされず、エンコードエラーやサロゲートペアに関する問題が発生するらしい。
2. Symfony Processによる環境引き継ぎ
PHPのProcessクラスを用いてPythonスクリプトを呼び出す際、環境変数が正しく継承されない場合があります。
このため、PHP側で適切なロケールを明示的に設定しないと、Python実行環境が正しいエンコーディングを使用しないらしい。
3. サロゲートペアの問題
errors='surrogatepass'
を指定すると、UTF-16で使用されるサロゲートペアがUTF-8エンコードに影響を及ぼします。
UTF-8において通常無効とされるコードポイントを許可してしまうため、異常なバイト列が生成されてしまうのだとか。
確認
ロケール・エンコーディングをPythonから確認
(PHPのProcessから実行)
import locale
print(locale.getpreferredencoding()) # 使用されているエンコーディングを確認
ANSI_X3.4-1968
ならば環境変数を入れよう
Symfony Processを用いる際に、LANG
環境変数を明示的に設定します。
$process = new Process($command);
$process->setEnv(['LANG' => 'en_US.UTF-8']);
$process->run();
正しいロケールで動作しているかを確認
import locale
print(locale.getpreferredencoding()) # 使用されているエンコーディングを確認
UTF-8
問題なさそうなので、入力してみる
encoded = sys.argv[1].encode('utf-8')
print(encoded)
print("あ".encode('utf-8'))
b'\xe3\x81\x82'
b'\xe3\x81\x82'
問題なくUTF-8で入力されている。
(おまけ)Pythonファイルで指定してみる
Pythonスクリプト内でロケールを強制的に設定する方法はダメだった。
import os
import locale
# LANG環境変数を明示的に設定
os.environ['LANG'] = 'en_US.UTF-8'
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
encoded = sys.argv[1].encode('utf-8', errors='surrogatepass')
print(encoded)
UTF-8
b'\xed\xb3\xa3\xed\xb2\x81\xed\xb2\x82'
ロケール設定はUTF-8
となるが、入力処理で躓いている。