【问题标题】:json.dump() uses ASCII codec encoding (instead of requested UTF-8) when redirecting stdout to a filejson.dump() 在将标准输出重定向到文件时使用 ASCII 编解码器编码(而不是请求的 UTF-8)
【发布时间】:2020-06-09 14:39:33
【问题描述】:

这个小小的python程序:

#!/usr/bin/env python
# -*- coding: utf8 -*-

import json
import sys

x = { "name":u"This doesn't work β" }

json.dump(x, sys.stdout, ensure_ascii=False, encoding="utf8")
print

在终端运行时生成此输出:

$ ./tester.py
{"name": "This doesn't work β"}

这正是我所期望的。但是,如果我将标准输出重定向到一个文件,它会失败:

$ ./tester.py > output.json
Traceback (most recent call last):
  File "./tester.py", line 9, in <module>
json.dump(x, sys.stdout, ensure_ascii=False, encoding="utf8")
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 190, in dump
fp.write(chunk)
UnicodeEncodeError: 'ascii' codec can't encode character u'\u03b2' in position 19: ordinal not in range(128)

但是,直接打印(不带 json.dump)可以重定向到文件:

 print u"This does work β".encode('utf-8')

如果stdout不是终端,就好像json包忽略了encoding选项。

如何让 json 包做我想做的事?

【问题讨论】:

标签: python json unicode utf-8


【解决方案1】:

JSON 是一种文本序列化格式(顺便提一下推荐的二进制编码),而不是二进制序列化格式。json 模块本身只关心它想要的编码程度知道 Python 2 可怕的 str 类型应该代表什么(它是 ASCII 字节吗?UTF-8 字节?latin-1 字节?)。

如前所述,由于 Python 2 文本处理很糟糕,json 模块很乐意返回 str(当 ensure_ascii 为真时,或者在其他情况下星号对齐,并且确信您已经告诉它str 与您预期的encoding 兼容,并且没有任何输入实际上是unicode) 或unicode(大多数时候ensure_ascii 为假)。

与 Python 2 的其余部分一样,sys.stdout 有点虚伪。即使您的语言环境设置将其设置为encoding='ascii',当您向其写入str 时,它也会忽略它(sys.stdout.write('\xe9') 应该会失败,但它会将str 视为预编码的原始二进制数据并且不费心去验证它是否匹配预期的编码。但是unicode进来时,它没有那个选项;unicode文本(不是UTF-8文本,而不是 ASCII 文本等),来自独角兽和彩虹的理想文本世界,而那个世界不是用俗气的字节表示的。

所以sys.stdout 必须对结果进行编码,并且它使用区域设置确定的编码(sys.stdout.encoding 会告诉你它是什么)。当它是 ASCII 并且它接收到无法编码为 ASCII 的东西时,它会爆炸(应该如此)。

关键是,json 模块总是返回 textunicodestr,它确信在空洞的 Python 2 世界中实际上是文本),并且有时你很幸运,文本的格式恰好可以绕过sys.stdout 中的检查。

但你不应该依赖它。如果您的输出必须采用特定编码,使用该编码。最简单的方法是不使用sys.stdout(通过print 显式或隐式地)并将数据写入文件打开with io.open(Python 3 的open 的反向端口,可以正确处理编码),明确指定encoding='utf-8'。如果您必须使用sys.stdout,并且坚持忽略语言环境编码,则可以重新包装它,例如:

with io.open(sys.stdout.fileno(), encoding='utf-8', closefd=False) as encodedout:
    json.dump(x, encodedout, ensure_ascii=False, encoding="utf-8")

它将stdout 文件描述符临时包装在一个现代的类文件对象中(使用closefd 以避免在底层描述符关闭时关闭它)。

TL;DR:切换到 Python 3。Python 2 在非 ASCII 文本方面糟糕,而且它的模块通常更糟糕(json绝对应该返回一个一致的类型,或者对于ensure_ascii 的每个设置至少只返回一个类型,而不是根据输入和encoding 动态选择;它甚至不是最糟糕的,csv 模块绝对糟糕) .此外,它的生命周期已结束,并且从现在开始不会为任何东西打补丁,因此继续使用它会使您容易受到从今年年初到时间结束。除其他外,Python 3 专门将 str 用于文本(它具有 Py2 的 unicode 类型的完整 Unicode 支持),而现代 Python 3(3.7+)将 coerce ASCII locales to UTF-8(因为基本上所有系统实际上都可以处理后者) ,这应该可以解决您的所有问题。非 ASCII 文本的行为与 ASCII 文本相同,并且像您这样坚持它们是 ASCII 的怪异语言环境(因此不会处理非 ASCII 输出)将被“固定”以按照您的意愿工作,而无需手动编码和解码、重新包装文件句柄等。

【讨论】:

    【解决方案2】:

    将所有 cmets 和答案合并为一个最终答案:

    注意:此答案适用于 Python 2.7。 Python 3 可能会有所不同。

    json 规范说 json 文件是 utf-8 编码的。但是,Python json 包不喜欢冒险,因此在输出中直接写入 ascii 并转义 unicode 字符。

    您可以将 ensure_ascii 标志设置为 False,在这种情况下,json 包将生成 unicode 输出而不是 str。在这种情况下,编码 unicode 输出是您的问题。

    没有办法让 json 包在输出时生成 utf-8 或任何其他编码。它可以是 ascii 或 unicode;任君挑选。

    encoding 论点是一个红鲱鱼。该选项告诉 json 包 input 字符串是如何编码的。

    这就是最终对我有用的方法:

    ofile = codecs.getwriter('utf-8')(sys.stdout)
    json.dump(x, ofile, ensure_ascii=False)
    

    tl;dr:真正的谜团是为什么在让标准输出进入终端时它没有呕吐。事实证明,stdout.write() 正在检测何时输出到终端并根据 $LANG 环境变量进行编码。当输出到文件时,unicode 被编码为 ascii,遇到不可编码的字符时会报错。

    【讨论】:

      【解决方案3】:

      Python 使用了一个环境变量,它可以覆盖终端的编码或重定向,所以这应该可以在不将 stdout 包装在脚本中的情况下工作。

      $ export PYTHONIOENCODING=utf8
      $ ./tester.py > output.json
      

      【讨论】:

      • 请注意,只需调整语言环境变量(最简单的LANG)即可达到相同的效果(以及其他效果,这可能对整个系统更好,而不仅仅是使用 Python使用不同的编码)。虽然该问题的评论线程已移至聊天,但其中一部分解释说选择 Python 2.7 是为了使其可以在任何 Mac 上运行,而无需依赖用户以任何方式更改其环境。所以环境/语言环境调整只有在它们被合并到脚本本身时才有效。
      • @ShadowRanger 使用 shell 进行重定向,使用 shell 变量来控制重定向。
      • 很高兴知道。但是,我不想告诉用户设置环境变量只是为了运行我的软件。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多