質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.78%

pythonのjson.loadsで文字化けを防ぐ方法に関して

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 5,392

gyungyun545

score 84

python3.6にてcgiを作製しております。
クライアント(javascript)からデータを受け取った後までは、日本語がそのまま表示されるのですが、json.loads()にてjsonを読み込ませたところから文字コードが文字化けしてしまいます。
utf-8やshift-jisへの変換を試してみましたがすべて、変換できないといったエラーが返されてしまいます。
どうしても、方法がわからなくて困ってます。
ご教授いただければ幸いです。

※ 下記にソースを簡単に再現しました。

クライアント

<html lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  </head>
  <body>
    <form accept-charset="utf-8">
  :
  :
$.post("test.py", JSON.stringify({text:"ふぇふぇふぇ"}), (res) => {
  :
  :
});

サーバー(cgi)

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
import json
import cgi
import cgitb
import sys, io
cgitb.enable()

print('Content-type: text/html; charset=UTF-8\r\n')

#sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
data = sys.stdin.read()
print(data)
params = json.loads(data)
print(params)

exit()

出力結果

下記のようにdataまでは日本語ですが、json.loads後がバイトコードが表示されてしまいます。

{"text":"ふぇふぇふぇ"}
{'text':'\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87'}

試したこと

下記一文を入れてutf-8に変換しようとしたら、unicodeEncodeErrorが返されました。

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
UnicodeEncodeError: 'utf-8' codec can't encode characters in position 9-26: surrogates not allowed
      args =('utf-8', '{"text":"\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87"}', 9, 27, 'surrogates not allowed')
      encoding ='utf-8'
      end =27
      object ='{"text":"\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87"}'
      reason ='surrogates not allowed'
      start =9
      with_traceback =<built-in method with_traceback of UnicodeEncodeError object>

補足20190424 13:21

環境面の補足です。
python3のデフォルトのencodingですが、環境で実行した場合とcgiから実行した場合で値がことなりました。サーバーでインタプリタで実行した際は「UTF-8」でしたが、
cgiから実行した場合は、stdin/stdoutともに、「ANSI_X3.4-1968」でした。

$ locale
LANG=ja_JP.UTF-8
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_PAPER="ja_JP.UTF-8"
LC_NAME="ja_JP.UTF-8"
LC_ADDRESS="ja_JP.UTF-8"
LC_TELEPHONE="ja_JP.UTF-8"
LC_MEASUREMENT="ja_JP.UTF-8"
LC_IDENTIFICATION="ja_JP.UTF-8"
LC_ALL=

補足20190424 13:54

皆様のおかげでjsonファイルにunicodeを出力するところまで行けました。
unicodeであれば読み込むときに変換ができるので、なんとか行けそうです。
ただ、ファイルにコードではなくちゃんと日本語で表記されるようにするにはどうすればよいでしょうか?
dumpsでensure_ascii=Falseにしても、どうしてもunicodeで出力されてしまいます。

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
import json
import cgi
import cgitb
import sys, io
cgitb.enable()

sys.stdin  = io.TextIOWrapper(sys.stdin.buffer,  encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

print('Content-type: text/html; charset=UTF-8\r\n')

data = sys.stdin.buffer.read()
print(data)
params = json.loads(data)
print(params)
with open("sample", mode="w", encode="utf-8") as f:
    json.dumps(params, f, indent=2, ensure_ascii=False)

exit()
{"text": "\u3042"}
{"text": "あ"}


ファイルに{"text": "あ"}と出力したい。

{"text": "\u3042"}

補足20190424 14:11

解決しました。
ソースを何度も修正する際にミスが有ったようです。
具体的には、同じコードを複数書いてexit()で実行されないようにしていたのですが、
ensure_ascii=Falseを追加する場所を間違えてました。
ずっとこの課題に取り組み、疲れが溜まっていたのが原因です。
本当に解決してよかったです。しばらく休みます。

下に最終的にうまく行ったコードを載せます。

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
import json
import cgi
import cgitb
import sys, io
cgitb.enable()

sys.stdin  = io.TextIOWrapper(sys.stdin.buffer,  encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

print('Content-type: text/html; charset=UTF-8\r\n')

data = sys.stdin.buffer.read()
print(data)
params = json.loads(data.decode())
print(params)
with open("sample", mode="w", encoding="utf-8") as f:
    json.dumps(params, f, indent=2, ensure_ascii=False)

exit()
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+2

何が起こっている分からないので、何を確認するか、という話だけ。


print関数のデコードの影響を受けないように、print(data) のところを print(data.encode('unicode_escape')) として、文字列に実際にどんなコードポイントで格納されているかを確認する。

実行例

% echo -n 'ふ' | python -c 'import sys; print(sys.stdin.read().encode("unicode_escape"))'
b'\\u3075'


(注: 長さ6のバイト列です)


エンコード/デコードの影響を受けないように、

data = sys.stdin.read()
print(data)


data = sys.stdin.buffer.read()
print(data)


として、バイト列に実際にどんなデータが格納されているかを確認する。

実行例

% echo -n 'ふ' | python -c 'import sys; print(sys.stdin.buffer.read())'
b'\xe3\x81\xb5'


(注: 長さ3のバイト列です)


どんな結果になるでしょうか?

エンコード/デコード関連のトラブルだと、print(data)して{"text":"ふぇふぇふぇ"}が出たからといって安心できなくて、"の間に不可視文字があるんじゃないの? などを疑ったりしないといけないので、コードポイントやバイナリデータを見るようにしましょう。

% python -c 'print("ふ")'
ふ
% python -c 'print("\uFEFFふ")'
ふ
(↑ゼロ幅空白\uFEFFが一見して区別できない)

% python -c 'print("ふ".encode("unicode_escape"))'
b'\\u3075'
% python -c 'print("\uFEFFふ".encode("unicode_escape"))'
b'\\ufeff\\u3075'

KSwordOfHaste さんの回答の続きのような形になりますが、

% echo -n 'あ' | LANG=C python -c 'import sys; print(sys.stdin.read().encode("unicode_escape"))'
b'\\udce3\\udc81\\udc82'
% LANG=C python -c 'import sys; print(sys.stdin.encoding, sys.stdout.encoding)'
US-ASCII US-ASCII


LANG=C の設定下だとsys.stdinの方も US-ASCII encoding になる影響を受けるので、

sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8")
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")


とするのがいいようです。

% echo -n 'あ' | LANG=C python -c 'import io, sys; sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"); print(sys.stdin.read().encode("unicode_escape"))'
b'\\u3042'

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/24 11:49 編集

    ご教授いただきありがとうございます。
    わかりやすさのため、{text:"あ"}で試してみましたが下記のように出力されました。
    b'{"text":"\xe3\x81\x82"}'
    特に余計な文字コードが含まれているように見えません。
    これをdecode()すればよいのかと思ったのですが、エラー?になります。
    エラーの返却値は通常はhtml形式でエラー情報が含まれるのですが、中身が空っぽで理解できませんでした。
    ```python3
    data = sys.stdin.buffer.read()
    print(data)
    print(data.decode())
    ```
    <出力結果>
    b'{"text":"\xe3\x81\x82"}'<!--: spamContent-Type: text/html<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --><body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --> --></font> </font> </font> </script> </object> </blockquote> </pre></table> </table> </table> </table> </table> </font> </font> </font>

    キャンセル

+1

quiquiさんが本質的なことを既にコメントされているので蛇足かも知れませんが、再現できたっぽいのでコメントします。quiquiさん回答とダブってるところがありますがご容赦ください。

多分サーバー上でのロケール設定(LANGないしはLC_CTYPE)がCなどになっているのではないでしょうか?

cgi.py

#!/usr/bin/python3
#coding: utf-8
import sys
import json

print('stdin:', sys.stdin.encoding)
print('stdout:', sys.stdout.encoding)

data = sys.stdin.read()

s = ''.join(c for c in data if ord(c) > 0xff)  # 日本語文字のみ残す
c = ''.join(f' U+{ord(c):04x}' for c in s)     # U+xxxx形式に変換
# out = s.encode(sys.stdout.encoding)  # LC_CTYPE=Cのとき例外が起きたので断念
print('in python str: ', c)
# print('encoded for output: ', out)   # LC_CTYPE=Cのとき例外が起きたので断念
print(data)

prms = json.loads(data)
print(prms)


(quiquiさんの回答にある`data.encode('unicode_escape')を知らなかったためエレガントでない方法でコードポイントを表示しています・・・)

$ locale
LANG=ja_JP.UTF-8
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_ALL=
                     # 注: bashを動かしている端末もUTF8にしてます

$ cat in
{"text":"ふぇふぇふぇ"}

$ ./cgi.py < in
stdin: UTF-8
stdout: UTF-8
in python str:   U+3075 U+3047 U+3075 U+3047 U+3075 U+3047
{"text":"ふぇふぇふぇ"}

{'text': 'ふぇふぇふぇ'}

$ LC_CTYPE=C ./cgi.py < in
stdin: ANSI_X3.4-1968
stdout: ANSI_X3.4-1968
in python str:   U+dce3 U+dc81 U+dcb5 U+dce3 U+dc81 U+dc87 U+dce3 U+dc81 U+dcb5 U+dce3 U+dc81 U+dc87 U+dce3 U+dc81 U+dcb5 U+dce3 U+dc81 U+dc87
{"text":"ふぇふぇふぇ"}

{'text': '\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87\udce3\udc81\udcb5\udce3\udc81\udc87'}

$

LC_CTYPEがUTF-8となっている場合はstrの各文字は期待通りのコードポイント値('ふ'がU+3075、'ぇ'がU+3047)になってました。LC_CTYPEをCにするとこれがサロゲートペアにあたる範囲のコードポイントになってしまってます。要するにこれはPythonにおいてふぇふぇふぇを表す文字ではないです。

下記のようにdataまでは日本語ですが

そこの認識がちょっと不足していたのだと思います。上に示したように本当に期待通りの文字かどうかはそのまま表示しても確かなことはわからないと思います。Pythonのstrの各要素の文字のコードポイントを確認して初めて安心できると思います。(quiquiさん回答の通り)

自分の環境(Windows 10 cygwin64bit)ではLC_CTYPEをCにするとPythonインタープリタのI/OエンコーディングがANSI_X3.4-1968となっていました。検索してみつけたのはJavaの資料でしたが、それによればこれはUS_ASCIIとのことでした。普段そんなものを使わないので正確なエンコーディング規則がわかりませんでしたが、7bit(or 8bit?)の範囲で基本的なラテン文字しか含まないものだと思います。

PythonがそのエンコーディングでUTF-8の2バイト以上でエンコードされた文字(MSB=1になったバイト)を読むとどうやらサロゲートペア範囲のコードポイントとして読み込まれるようです。いずれにせよ化けてないように見えたのはこれを逆変換した際に元のUTF8のバイト列が再現されたため端末には期待通りに表示されてしまい、あたかも正しい日本語がPythonに読み込まれたかのように見えただけだと思います。

対処

PythonインタープリタのI/Oのエンコーディングをstdin, stdoutともにUTF-8にすれば期待通りになるのではないでしょうか?あるいはPythonを実行する環境のlocale設定を同じCロケールでもC.UTF-8にしておくとか。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/24 13:21

    ご回答有り難うございます。

    stdin, stdoutともにUTF-8にした場合、printでエラーになってしまいました。。。
    これは、utf-8以外でもエラーになるようです。
    ```python3
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
    ata = sys.stdin.read()
    print(data)
    params = json.loads(data)
    ```
    ```error
    UnicodeEncodeError: 'utf-8' codec can't encode characters in position 9-11: surrogates not allowed
    args =('utf-8', '{"text":"\udce3\udc81\udc82"}', 9, 12, 'surrogates not allowed')
    encoding ='utf-8'
    end =12
    object ='{"text":"\udce3\udc81\udc82"}'
    reason ='surrogates not allowed'
    start =9
    with_traceback =<built-in method with_traceback of UnicodeEncodeError object>
    ```
    ちなみに、printがない場合は、POSTエラーとなり、次の行のprintも表示されないので、おそらくjson.loads(data)の行でコケてるかと思います。
    POST https://xxxxxxxx/test.py 500 (Internal Server Error)"

    キャンセル

0

json.loads

無視される非推奨の引数 encoding を除いて、その他の引数は load() のものと同じ意味です。

とあるように.loadsではencoding指定しても無意味です。よって以下回答は取り消します。

当方の非CGI環境では現象再現できませんでしたが、params = json.loads(data,encoding='utf-8')のようにloads時に文字コードutf-8指定してみてはいかがでしょうか?

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/04/24 11:41

    json.loads の encoding引数は、無視される引数です。
    https://docs.python.org/ja/3.6/library/json.html#json.loads

    キャンセル

  • 2019/04/24 11:45

    ドキュメント確認しました。
    loadsではloadとは異なりencodingは無視されるのですね。
    ご指摘ありがとうございます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.78%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る