如何从JSON获取字符串对象而不是Unicode

301

我正在使用Python 2解析从ASCII编码文本文件中获取的JSON。

当用jsonsimplejson加载这些文件时,所有字符串值都被转换为Unicode对象,而不是字符串对象。问题是,我必须使用一些只接受字符串对象的库来处理数据。我不能更改这些库也不能更新它们。

是否有可能获得字符串对象而不是Unicode对象?

示例

>>> import json
>>> original_list = ['a', 'b']
>>> json_list = json.dumps(original_list)
>>> json_list
'["a", "b"]'
>>> new_list = json.loads(json_list)
>>> new_list
[u'a', u'b']  # I want these to be of type `str`, not `unicode`

一个简单而干净的解决方案是使用最新版本的Python,即Python 3及以上版本


1
在Python3下没有问题,new_list中的项目类型是str - GoingMyWay
1
Python 3k不是“Python的最新版本”,它只是一个替代分支。 - user2589273
13
看到这样的评论在2017年12月是很奇怪的——Python 2已经被弃用,在2020年1月1日之后将不再进行维护,距离现在不到2年时间:https://pythonclock.org/。 - Zaar Hai
3
许多人被迫使用Python 2,因为许多应用程序嵌入了它们自己的Python版本以进行自动化和脚本编写,所以人们必须一直使用它,直到供应商进行更新(我在看你,Maya、Houdini、Nuke...)。 - Geordie
1
@Geordie,我当然知道并理解这一点。我的评论是关于术语的 - Python不是“替代分支”,而是对那些被困在其中的人来说不幸缺乏替代选择(双关语)。 - Zaar Hai
显示剩余2条评论
21个回答

188

虽然这里有一些好的答案,但我最终使用PyYAML来解析我的JSON文件,因为它将键和值作为str类型字符串而不是unicode类型。因为JSON是YAML的子集,所以它可以很好地工作:

>>> import json
>>> import yaml
>>> list_org = ['a', 'b']
>>> list_dump = json.dumps(list_org)
>>> list_dump
'["a", "b"]'
>>> json.loads(list_dump)
[u'a', u'b']
>>> yaml.safe_load(list_dump)
['a', 'b']

注意事项

需要注意的一些事情:

  • 我使用了字符串对象,因为我的所有条目都是ASCII编码的。如果我使用Unicode编码的条目,我会以Unicode对象的形式获得它们——没有转换!

  • 你应该(很可能总是)使用PyYAML的safe_load函数;如果你用它来加载JSON文件,你也不需要load函数的“额外能力”。

  • 如果你想要一个对1.2版本规范有更多支持(并且正确解析非常低的数字)的YAML解析器,请尝试Ruamel YAMLpip install ruamel.yamlimport ruamel.yaml as yaml就是我在测试中所需的全部。

转换

作为说明,这里没有任何转换!如果您不能确定只处理ASCII值(大多数情况下您不能确定),最好使用一个转换函数
我现在已经使用了来自Mark Amery的函数几次,它非常好用且易于使用。您也可以将类似的函数作为object_hook使用,因为它可能会在处理大文件时提高性能。请参见稍微复杂一些的来自Mirec Miskuf的答案。

9
如果你决定使用这个答案,请小心。它对于 Brutus 的情况完美适用,但这也是因为他知道他的数据只包含 ASCII 可编码字符。如果你没有这个保证,这个答案就不适用了。例如,在 Python shell 中执行 yaml.load(json.dumps([u'a', u'£', u'É'])),你会发现返回值是 ['a', u'\xa3', u'\xc9'](其中包含 unicode 字符串)。如果你不能确定你的数据只包含 ASCII 字符集中的字符,你应该使用另一种方法(我推荐我的答案)。 - Mark Amery
1
YAML 也使用 [u'a', u'b'],请小心。 - Carlos Calla
1
这很好,但它不能处理低数字..看这里:https://dev59.com/QF0a5IYBdhLWcg3wVHXU - Oren
@Oren:这不是YAML规范的错误,而是PyYAML解析器的错误。ruamel的YAML解析器可以正常工作。 - Brutus
我想要输出结果为 ["a", "b"] 而不是 ['a', 'b'] @Brutus - user60679
显示剩余2条评论

146

没有内置选项可以使json模块函数返回字节串而不是Unicode字符串。然而,这个简短而简单的递归函数将把任何解码的JSON对象从使用Unicode字符串转换为UTF-8编码的字节串:

def byteify(input):
    if isinstance(input, dict):
        return {byteify(key): byteify(value)
                for key, value in input.iteritems()}
    elif isinstance(input, list):
        return [byteify(element) for element in input]
    elif isinstance(input, unicode):
        return input.encode('utf-8')
    else:
        return input

只需在从 json.loadjson.loads 调用中获取的输出上调用此函数。

需要注意几点:

  • 为了支持 Python 2.6 或更早版本,请将 return {byteify(key): byteify(value) for key, value in input.iteritems()} 替换为 return dict([(byteify(key), byteify(value)) for key, value in input.iteritems()]),因为字典推导直到 Python 2.7 才得到支持。
  • 由于这个答案递归遍历整个解码后的对象,它具有一些不良的性能特征,可以通过非常谨慎地使用 object_hookobject_pairs_hook 参数来避免。目前唯一成功实现了这一点的是Mirec Miskuf 的答案,但相应地,它比我的方法复杂得多。

1
我喜欢这个 - 这不是忽略 - 而是认识到当人们说“字符串”和“ASCII”时,他们大多数情况下是天真地想要字节,而不是理论上的Unicode字符。(并且不是ASCII,因为他们仍然希望在另一端看到英镑符号) - Danny Staple
我喜欢这个,它的工作方式几乎与我的漂亮打印机工作方式相同,因为我知道json不会生成元组,所以你也应该为元组添加例外。 - y.petremann
这种方法效率非常低下,要求您递归遍历可能不需要的节点。json模块提供了更高效的钩子来实现这一点。下面使用“object_hook”的答案比这个更糟糕,但是使用“object_pairs_hook”,您可以提出一种相当有效的方法,不需要递归或者重新访问不包含字符串的节点。 - Travis Jensen
1
有趣。object_pairs_hook方法可能比这个稍微难理解一点(您需要了解参数的工作原理以及为什么列表和字典需要不同的处理方式),而且大多数人不会关心性能优势...但我希望它存在,特别是对于处理异常深度嵌套的JSON对象的任何人。 - Mark Amery
plus1 这是最简洁的答案;此外,安装 PyYAML 很麻烦。更好的方法是以某种方式微流式转换,这样就不会使用 4 倍的内存。 - personal_cloud

116

使用object_hook的解决方案

这适用于Python 2.7和3.x。

import json

def json_load_byteified(file_handle):
    return _byteify(
        json.load(file_handle, object_hook=_byteify),
        ignore_dicts=True
    )

def json_loads_byteified(json_text):
    return _byteify(
        json.loads(json_text, object_hook=_byteify),
        ignore_dicts=True
    )

def _byteify(data, ignore_dicts = False):
    if isinstance(data, str):
        return data

    # If this is a list of values, return list of byteified values
    if isinstance(data, list):
        return [ _byteify(item, ignore_dicts=True) for item in data ]
    # If this is a dictionary, return dictionary of byteified keys and values
    # but only if we haven't already byteified it
    if isinstance(data, dict) and not ignore_dicts:
        return {
            _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True)
            for key, value in data.items() # changed to .items() for Python 2.7/3
        }

    # Python 3 compatible duck-typing
    # If this is a Unicode string, return its string representation
    if str(type(data)) == "<type 'unicode'>":
        return data.encode('utf-8')

    # If it's anything else, return it in its original form
    return data

示例用法:

>>> <b><i>json_loads_byteified('{"Hello": "World"}')</i></b>
{'Hello': 'World'}
>>> <b><i>json_loads_byteified('"I am a top-level string"')</i></b>
'I am a top-level string'
>>> <b><i>json_loads_byteified('7')</i></b>
7
>>> <b><i>json_loads_byteified('["I am inside a list"]')</i></b>
['I am inside a list']
>>> <b><i>json_loads_byteified('[[[[[[[["I am inside a big nest of lists"]]]]]]]]')</i></b>
[[[[[[[['I am inside a big nest of lists']]]]]]]]
>>> <b><i>json_loads_byteified('{"foo": "bar", "things": [7, {"qux": "baz", "moo": {"cow": ["milk"]}}]}')</i></b>
{'things': [7, {'qux': 'baz', 'moo': {'cow': ['milk']}}], 'foo': 'bar'}
>>> <b><i>json_load_byteified(open('somefile.json'))</i></b>
{'more json': 'from a file'}

这是如何工作的,为什么要使用它?

Mark Amery的函数比这些函数更短、更清晰,那么它们的意义何在?为什么要使用它们?

纯粹是为了性能。Mark的答案首先使用Unicode字符串完全解码JSON文本,然后递归整个解码值以将所有字符串转换为字节字符串。这有一些不良影响:

  • 在内存中创建整个解码结构的副本
  • 如果您的JSON对象是真的嵌套得很深(500层或更多),那么您将达到Python的最大递归深度

这个答案通过使用json.loadjson.loadsobject_hook参数来缓解这两个性能问题。从文档中可以看出:

object_hook是一个可选函数,它将被调用与任何对象文本解码(一个dict)的结果。object_hook的返回值将被用于代替dict。这个特性可以用来实现自定义解码器。

由于嵌套在其他字典中的字典会在object_hook 解码时被传递,因此我们可以在那个点上将其中的任何字符串或列表变成字节,并避免以后进行深度递归。

Mark的答案不能作为object_hook使用,因为它递归进入嵌套的字典。我们通过_byteifyignore_dicts参数在这个答案中防止了递归,该参数在除了object_hook将一个新的dict传递给它以外的所有时间都被传递给它。 ignore_dicts标志告诉_byteify忽略dict,因为它们已经被转换成字节。

最后,我们的json_load_byteifiedjson_loads_byteified的实现调用json.loadjson.loads返回的结果上的_byteify(带有ignore_dicts=True),以处理解码文本顶层没有dict的情况。


1
这里的方法很好,我一开始读的时候没有完全理解,但在Travis Jensen的回答中重新阅读后终于明白了。为了澄清它的工作原理以及与我的答案相比的优势,我进行了相当激进的编辑。代码的核心思想保持不变,但我修改了几乎所有其他内容。如果您反对此举,请随意撤销我的编辑 - 这是您的答案! - Mark Amery
2
这是一个很好的解决方案,高效而优雅。然而,如果你像我一样被困在 Python < 2.7 的领域中,你需要将这行代码替换为 return dict((_byteify(key, ignore_dicts=True), _byteify(value, ignore_dicts=True)) for key, value in data.iteritems()) 才能使其正常工作。 - Richard Dunn
@MarkAmery,你对我上面的评论有什么看法?(我刚刚在编辑历史中看到,实际上是你添加了那个声明)。 - Stefan Pochmann
@StefanPochmann 嗯。我不完全记得16个月前我测试了什么,但是有一件事情立刻引起了我的注意,那就是你正在测试深度嵌套的列表/数组而不是深度嵌套的字典/对象。object_hook只会在每个字典/对象上调用,而不是在每个列表/数组上调用,因此你的特定测试没有显示出我所描述的递归限制的避免是不足为奇的。请改为使用深度嵌套的字典/对象进行重试,我相信你会看到不同的结果! - Mark Amery
你的代码已经是通用的了,移除 ignore_dict 就可以正常工作了。不需要担心 ignore_dict,它已经被处理好了。def _byteify(data):if isinstance(data, unicode): return data.encode('utf-8') if isinstance(data, list): return [ _byteify(item) for item in data ] if isinstance(data, dict): return { _byteify(key): _byteify(value) for key, value in data.iteritems() } return data - Ram Sharan Mittal
显示剩余4条评论

76
您可以使用json.loadsobject_hook参数来传递转换器。您不必事后进行转换。json模块将始终仅传递object_hook字典,并且它将递归地传入嵌套字典,因此您不必自己递归到嵌套字典中。我认为不应该像Wells shows那样将Unicode字符串转换为数字。如果它是Unicode字符串,则在JSON文件中将其引用为字符串,因此应该是字符串(或文件有问题)。

另外,我建议避免在unicode对象上执行类似于str(val)这样的操作。您应该使用具有有效编码的value.encode(encoding),具体取决于您的外部库需要什么。

所以,例如:

def _decode_list(data):
    rv = []
    for item in data:
        if isinstance(item, unicode):
            item = item.encode('utf-8')
        elif isinstance(item, list):
            item = _decode_list(item)
        elif isinstance(item, dict):
            item = _decode_dict(item)
        rv.append(item)
    return rv

def _decode_dict(data):
    rv = {}
    for key, value in data.iteritems():
        if isinstance(key, unicode):
            key = key.encode('utf-8')
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        elif isinstance(value, list):
            value = _decode_list(value)
        elif isinstance(value, dict):
            value = _decode_dict(value)
        rv[key] = value
    return rv

obj = json.loads(s, object_hook=_decode_dict)

3
如果s中的对象是JSON Object(一个无序的键值对集合,用冒号':'分隔键和值,逗号分隔并用花括号括起来),那么这个方法是有效的。但如果它是一个JSON Array,比如["a", "b"],那么结果仍然会是[u'a', u'b']。目前json.loads()提供的其他自定义钩子类型参数也无法实现这一功能。 - martineau
2
由于您提到的json模块将递归传递嵌套的dict,因此在这两个函数中检查它们是不必要的 - 因此应该删除检查它们的两个elif子句。 - martineau
1
请注意,以下划线开头的函数名称对于导入语句具有特殊含义。如果您将这些函数放在名为Utility.py的文件中,然后在另一个文件中执行from Utility import *,那么由于那个下划线,这些函数将不会被识别。 - M Katz
1
这是一个非常糟糕的想法。object_hook会在解析每个JSON对象时被调用,因此如果您递归到所给出的内容中,那么您正在重新“字节化”已经“字节化”的内容。性能将随着对象大小的增长而呈几何级数增长。我在这里提供了一个答案,使用object_pairs_hook并且不会遇到这个问题。链接 - Travis Jensen

39
这是因为json()在字符串对象和Unicode对象之间没有区别。它们在JavaScript中都是字符串。
我认为JSON返回Unicode对象是正确的。实际上,我不会接受任何少于此的内容,因为JavaScript字符串实际上是unicode对象(即,JSON(JavaScript)字符串可以存储任何类型的Unicode字符),因此在从JSON翻译字符串时创建unicode对象是有意义的。普通字符串就不适合,因为库必须猜测您想要的编码方式。
最好在所有地方使用unicode字符串对象。因此,您最好更新您的库,以便它们可以处理Unicode对象。
但是,如果您真的想要字节串,只需将结果编码为所选的编码方式即可:
>>> nl = json.loads(js)
>>> nl
[u'a', u'b']
>>> nl = [s.encode('utf-8') for s in nl]
>>> nl
['a', 'b']

谢谢nosklo,这是我首先做的事情。但是正如我所说,我使用的真实数据非常嵌套,因此这引入了相当大的开销。 我仍在寻找自动解决方案...至少有一个错误报告,人们抱怨simplejson返回字符串对象而不是Unicode。 - Brutus
1
@Brutus:我认为json返回unicode对象是正确的。事实上,我不会接受任何不是unicode对象的返回值,因为javascript字符串实际上就是unicode对象。我的意思是,json(javascript)字符串可以存储任何类型的unicode字符,因此在从json翻译时创建unicode对象是有意义的。你应该真正修复你的库。 - nosklo
除非你有一个Python库,它在底层传递给一个C库并且它期望一个ASCII字符串。我就是遇到了这种情况,而绑定的C库正在引发“类型为'std :: string const&'的参数”。 - MikeyE

16

有一个简单的解决方法。

TL;DR - 使用ast.literal_eval()替代json.loads()。两者都在标准库中。

虽然不是一个“完美”的答案,但如果您打算完全忽略Unicode,它可以让您走得更远。在Python 2.7中实现。

import json, ast
d = { 'field' : 'value' }
print "JSON Fail: ", json.loads(json.dumps(d))
print "AST Win:", ast.literal_eval(json.dumps(d))

提供:

JSON Fail:  {u'field': u'value'}
AST Win: {'field': 'value'}

当一些对象真正成为Unicode字符串时,情况就变得更加复杂了。完整的答案很快就会变得棘手。


11
请确保您的JSON不包含任何nulltruefalse值,因为它们在Python中无效,会导致literal_eval()失败。 - ʇsәɹoɈ
3
希望你的 JSON 内容不包含在字符串中被转义的斜杠(\/),或者 Unicode 转义序列(比如 "\u0061",也可以写成 "a")。Python 的字面语法和 JSON 在几个方面是不兼容的,如果我不是要扔掉这个脚本,我就不会相信这个答案。 - Mark Amery
人们说得对,如果字符串确实是Unicode,那么这个答案就失败了,但如果是这种情况,我们也无法将其转换为字符串。对于一个只在正常情况下有效并在其他情况下抛出异常的答案,我给予+1的评价。 - Stefan Sullivan
如果可能的话,不要使用json转储数据,只需在运行Python时使用print。然后使用ast.literal_eval即可。 - Jean-François Fabre

13

迈克·布伦南(Mike Brennan)的回答非常接近,但没有任何理由重新遍历整个结构。如果使用object_hook_pairs(Python 2.7+)参数:

object_pairs_hook是一个可选函数,将使用成对有序列表解码的任何对象文本结果调用。 object_pairs_hook的返回值将替代dict。该功能可用于实现依赖于键和值对解码顺序的自定义解码器(例如,collections.OrderedDict将记住插入的顺序)。如果还定义了object_hook,则object_pairs_hook具有优先权。

使用它,您可以获得每个JSON对象,因此您可以进行解码而无需递归:

def deunicodify_hook(pairs):
    new_pairs = []
    for key, value in pairs:
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        if isinstance(key, unicode):
            key = key.encode('utf-8')
        new_pairs.append((key, value))
    return dict(new_pairs)

In [52]: open('test.json').read()
Out[52]: '{"1": "hello", "abc": [1, 2, 3], "def": {"hi": "mom"}, "boo": [1, "hi", "moo", {"5": "some"}]}'

In [53]: json.load(open('test.json'))
Out[53]:
{u'1': u'hello',
 u'abc': [1, 2, 3],
 u'boo': [1, u'hi', u'moo', {u'5': u'some'}],
 u'def': {u'hi': u'mom'}}

In [54]: json.load(open('test.json'), object_pairs_hook=deunicodify_hook)
Out[54]:
{'1': 'hello',
 'abc': [1, 2, 3],
 'boo': [1, 'hi', 'moo', {'5': 'some'}],
 'def': {'hi': 'mom'}}

请注意,由于在使用object_pairs_hook时每个对象都会传递给钩子函数,因此我从未必须递归调用该钩子函数。您确实需要关心列表,但是正如您所看到的,列表中的对象将被正确转换,而且您不必递归调用。

一位同事指出Python2.6没有object_hook_pairs。但是您仍然可以通过进行非常小的更改来在Python2.6中使用它。在上面的钩子函数中,更改:

for key, value in pairs:

for key, value in pairs.iteritems():

那么,请使用 object_hook,而不是 object_pairs_hook

In [66]: json.load(open('test.json'), object_hook=deunicodify_hook)
Out[66]:
{'1': 'hello',
 'abc': [1, 2, 3],
 'boo': [1, 'hi', 'moo', {'5': 'some'}],
 'def': {'hi': 'mom'}}

使用object_pairs_hook可以使得在JSON对象中每个对象都少实例化一个字典,如果你正在解析一个庞大的文档,这可能是值得的。


1
这很不错,看起来非常接近值得获得绿色勾号的标准(Brutus已经慷慨地传递了更好的答案)。但是...为什么不在你展示的deunicodify_hook中正确处理列表呢?目前,您有一个deunicodify_hook的实现,它不会迭代列表并对其中的字符串和列表进行解码,因此您展示的输出与您的钩子实际产生的输出匹配。修复这个问题,这个答案将优于我的。 - Mark Amery
不太重要的是,我建议您使用普通的CPython解释器演示该函数,而不是您在此处使用的那个(我认为是IronPython)? CPython解释器对大多数Python用户来说更加熟悉,并且在我看来更加美观。 - Mark Amery
这对我不起作用,但我确定这是我所做的某些怪癖...我正在将一个较大的json文档中的一个列表存储到文件中。无论我使用还是不使用此object_pairs_hook进行加载,每个项目都会出现unicode。该死。 - rsaw
1
@rsaw 很好!由于 object_pairs_hook 仅在 对象 中调用,如果您的 JSON 文本在顶层具有字符串列表,则此解决方案将失败。 没有办法修复这个问题,除非对从 json.load 返回的东西调用一些函数; json.load 钩子中没有一个可以保证您能够处理每个字符串。 我认为这是足够严重的缺陷,因此我仍然建议使用我的解决方案而不是使用钩子。 - Mark Amery
1
-1 因为我刚意识到 Mirec Miskuf 已经发布了一个对象钩子答案,既没有 Mike Brennan 方法的缺点(多次重新字节化相同的字典),也没有这个方法的缺点(无法字节化嵌套列表或顶级列表或字符串)。我不确定为什么他的答案几乎没有得到关注,而这个答案 - 这个答案是劣质的 - 却迅速获得了投票。 - Mark Amery

9

很抱歉,simplejson库中没有自动实现此功能的方法。

simplejson中的扫描器和解码器被设计用于生成Unicode文本。为此,该库使用一个名为c_scanstring(如果可用,用于提高速度)或py_scanstring(如果C版本不可用)的函数。几乎每个用于解码可能包含文本的结构的simplejson例程都会多次调用scanstring函数。您必须要么猴子补丁simplejson.decoder中的scanstring值,要么子类化JSONDecoder并提供几乎完全由您自己实现的任何可能包含文本的内容。

simplejson 输出 Unicode 的原因是 JSON 规范 明确提到 "字符串是零个或多个Unicode字符的集合",规范本身默认支持Unicode。 simplejsonscanstring 实现甚至扫描并解释Inicode转义(甚至检查有误的多字节字符集表示形式),所以它能够可靠地将值返回给您,唯一的方式就是作为Unicode格式。
如果您有一个需要使用str的旧库,我建议您要么在解析之后费力地搜索嵌套数据结构(这正是您明确说想避免的...抱歉),要么在某种外观中包装您的库,以便您可以在更细粒度的级别上调整输入参数。如果您的数据结构确实是深度嵌套的,则第二种方法可能比第一种更可管理。

4

正如Mark (Amery)正确指出的那样:在JSON转储上使用PyYAML的反序列化器仅在您只有ASCII时才起作用。至少是开箱即用的。

关于PyYAML方法的两个快速评论:

  1. Never use yaml.load() on data from the field. It’s a feature(!) of YAML to execute arbitrary code hidden within the structure.

  2. You can make it work also for non ASCII via this:

     def to_utf8(loader, node):
         return loader.construct_scalar(node).encode('utf-8')
     yaml.add_constructor(u'tag:yaml.org,2002:str', to_utf8)
    

但就性能而言,与Mark Amery的答案无法相比:

将一些深度嵌套的示例字典放入这两种方法中,我得到了以下结果(其中dt[j] = json.loads(json.dumps(m))的时间差):

     dt[yaml.safe_load(json.dumps(m))] =~ 100 * dt[j]
     dt[byteify recursion(Mark Amery)] =~   5 * dt[j]

所以反序列化,包括完全遍历树和编码,在JSON的基于C的实现范围之内。我发现这非常快,也比嵌套结构中的yaml加载更健壮。而且,它不容易出现安全错误,观察yaml.load。
=>虽然我会感激一个仅基于C的转换器的指针,但byteify函数应该是默认答案。
如果您的JSON结构来自领域,包含用户输入,则尤其如此。因为那么你可能需要在你的结构上走路 - 独立于你想要的内部数据结构('unicode sandwich'或仅字节字符串)。
为什么?
Unicode规范化。对于不知情的人:吃止痛药并阅读this
因此,使用byteify递归可以一举两得:
1.从嵌套的JSON转储中获取您的字节串 2.获取用户输入值的规范化,以便您在存储中找到东西。
在我的测试中,使用unicodedata.normalize('NFC', input).encode('utf-8')替换input.encode('utf-8'),甚至比不使用NFC更快 - 但我想这严重依赖于样本数据。

3
问题在于,simplejsonjson 是两个不同的模块,至少在它们处理 Unicode 方式上是如此。在 Python 2.6+ 中有 json,它返回 Unicode 值,而 simplejson 返回字符串对象。
尝试在您的环境中使用 easy_install 安装 simplejson 并查看是否有效。对我来说有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接