如何将json序列化比Python中的yaml序列化快得多?

时间:2010-03-16 02:23:12

标签: python json serialization yaml

我的代码在很大程度上依赖于yaml进行跨语言序列化,在加快某些事情的速度时,我注意到yaml与其他序列化方法(例如,pickle,json)相比非常慢。

所以真正让我感到震惊的是,当输出几乎相同时,json比yaml快得多。

>>> import yaml, cjson; d={'foo': {'bar': 1}}
>>> yaml.dump(d, Dumper=yaml.SafeDumper)
'foo: {bar: 1}\n'
>>> cjson.encode(d)
'{"foo": {"bar": 1}}'
>>> import yaml, cjson;
>>> timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
44.506911039352417
>>> timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml; d={'foo': {'bar': 1}}", number=10000)
16.852826118469238
>>> timeit("cjson.encode(d)", setup="import cjson; d={'foo': {'bar': 1}}", number=10000)
0.073784112930297852

PyYaml的CSafeDumper和cjson都是用C语言编写的,所以这不是C和Python速度问题。我甚至添加了一些随机数据来查看cjson是否正在进行任何缓存,但它仍然比PyYaml快。我意识到yaml是json的超集,但是如果使用这么简单的输入,yaml序列化器怎么会慢2个数量级呢?

5 个答案:

答案 0 :(得分:57)

通常,输出的复杂性决定了解析的速度,而不是接受输入的复杂性。 JSON语法是very concise。 YAML解析器为comparatively complex,导致开销增加。

  

JSON最重要的设计目标是   简单和普遍性。从而,   JSON生成和解析是微不足道的,   以减少人为代价   可读性。它也使用最低   公分母信息模型,   确保任何JSON数据都很容易   由每个现代节目处理   环境。

     

相比之下,YAML最重要的设计   目标是人类的可读性和   支持序列化任意   原生数据结构。因此,YAML   允许极其可读的文件,   但生成和更复杂   解析。此外,YAML冒险   超出最低共同标准   数据类型,要求更复杂   跨越时的处理   不同的编程环境。

我不是YAML解析器实现者,因此如果没有一些分析数据和大量的示例,我不能专门讲述数量级。无论如何,在对基准数字充满信心之前,一定要测试大量输入。

更新哎呀,误读了这个问题。 :-(尽管输入语法很大,但序列化仍然可以非常快;但是,浏览源代码,它看起来像PyYAML的Python级序列化constructs a representation graph而simplejson将内置的Python数据类型直接编码为文本块。

答案 1 :(得分:27)

在我曾经使用过的应用程序中,字符串与数字之间的类型推断(float / int)是解析yaml的最大开销,因为字符串可以不用引号编写。因为json中的所有字符串都在引号中,所以在解析字符串时没有回溯。这将减速的一个很好的例子是值0000000000000000000s。在读到它之前,你不能告诉这个值是一个字符串。

其他答案是正确的,但这是我在实践中发现的具体细节。

答案 2 :(得分:20)

谈到效率,我使用YAML一段时间,并被这种语言中某些名称/值赋值的简单性所吸引。然而,在这个过程中,我经常谈论YAML的一个事件,语法的细微变化,允许你以更简洁的方式编写特殊情况等。最后,尽管YAML的语法几乎与某些形式一致,但它给我留下了一种“模糊”的感觉。然后我限制自己不要触及现有的,有效的YAML代码,并用更环绕,更安全的语法编写所有新内容 - 这让我放弃了所有的YAML。结果是YAML试图看起来像W3C标准,并产生一个关于其概念和规则的难以阅读的文献库。

我认为,这远远超过了所需的智力开销。看看SGML / XML:由IBM在咆哮的60年代开发,由ISO标准化,已知(以一种愚蠢和修改的形式)作为HTML,无数数百万人,记录和记录,并在全世界再次记录。提出小JSON并杀死龙。 JSON如何在如此短的时间内如此广泛地使用,只有一个微薄的网站(以及支持它的javascript杰出人物)?它的简单性,语法上的质疑,以及学习和使用它的简易性。

XML和YAML对人类来说很难,而且它们很难用于计算机。 JSON非常友好,对人类和计算机都很容易。

答案 3 :(得分:12)

粗略看看python-yaml表明它的设计要比cjson复杂得多:

>>> dir(cjson)
['DecodeError', 'EncodeError', 'Error', '__doc__', '__file__', '__name__', '__package__', 
'__version__', 'decode', 'encode']

>>> dir(yaml)
['AliasEvent', 'AliasToken', 'AnchorToken', 'BaseDumper', 'BaseLoader', 'BlockEndToken',
 'BlockEntryToken', 'BlockMappingStartToken', 'BlockSequenceStartToken', 'CBaseDumper',
'CBaseLoader', 'CDumper', 'CLoader', 'CSafeDumper', 'CSafeLoader', 'CollectionEndEvent', 
'CollectionNode', 'CollectionStartEvent', 'DirectiveToken', 'DocumentEndEvent', 'DocumentEndToken', 
'DocumentStartEvent', 'DocumentStartToken', 'Dumper', 'Event', 'FlowEntryToken', 
'FlowMappingEndToken', 'FlowMappingStartToken', 'FlowSequenceEndToken', 'FlowSequenceStartToken', 
'KeyToken', 'Loader', 'MappingEndEvent', 'MappingNode', 'MappingStartEvent', 'Mark', 
'MarkedYAMLError', 'Node', 'NodeEvent', 'SafeDumper', 'SafeLoader', 'ScalarEvent', 
'ScalarNode', 'ScalarToken', 'SequenceEndEvent', 'SequenceNode', 'SequenceStartEvent', 
'StreamEndEvent', 'StreamEndToken', 'StreamStartEvent', 'StreamStartToken', 'TagToken', 
'Token', 'ValueToken', 'YAMLError', 'YAMLObject', 'YAMLObjectMetaclass', '__builtins__', 
'__doc__', '__file__', '__name__', '__package__', '__path__', '__version__', '__with_libyaml__', 
'add_constructor', 'add_implicit_resolver', 'add_multi_constructor', 'add_multi_representer', 
'add_path_resolver', 'add_representer', 'compose', 'compose_all', 'composer', 'constructor', 
'cyaml', 'dump', 'dump_all', 'dumper', 'emit', 'emitter', 'error', 'events', 'load', 
'load_all', 'loader', 'nodes', 'parse', 'parser', 'reader', 'representer', 'resolver', 
'safe_dump', 'safe_dump_all', 'safe_load', 'safe_load_all', 'scan', 'scanner', 'serialize', 
'serialize_all', 'serializer', 'tokens']

更复杂的设计几乎总是意味着更慢的设计,这比大多数人需要的要复杂得多。

答案 4 :(得分:2)

尽管您已经接受了答案,但不幸的是,只有这样 朝着PyYAML文档的方向挥舞着手 引用该文档中不正确的声明:PyYAML 在转储期间制作表示图,它会创建一个 lineair流(就像json保留一堆ID来查看是否有 递归)。


首先,您必须意识到cjson自卸车是 仅手工制作的C代码,YAML的CSafeDumper共享四个转储阶段中的两个 (RepresenterResolver)与普通的纯Python SafeDumper 并且其他两个阶段(序列化器和发射器)不 完全用C语言手工编写,但包含Cython模块 调用C库libyaml进行发射。


除了重要部分外,您问题的简单答案 为什么需要更长的时间,是因为转储YAML可以完成更多工作。不是这样 很大程度上是因为YAML像@flow所说的那样困难,但这是因为额外的 YAML可以做到的功能,使其比JSON强大得多,并且功能更强大 用户友好,如果您需要使用编辑器处理结果。那 意味着即使应用这些额外功能,YAML库也会花费更多时间, 而且在许多情况下,也只是检查是否适用。

这里是一个例子:即使您从未经历过PyYAML 代码,您会注意到转储程序没有引用foo并且 bar。那不是因为这些字符串是键,因为YAML并不是 具有JSON的限制,即映射键需要 是字符串。例如。 Python字符串,它是映射中的值可以 也不要加引号(即纯文本)。

重点是可以,因为并非总是如此。采取 例如一个仅包含数字字符的字符串: 12345678。这需要用引号写出来,否则 看起来就像一个数字(并在解析时读回)。

PyYAML如何知道何时引用字符串,何时不引用字符串?倾销时 它实际上首先转储字符串,然后解析结果以使 当然,当它读回该结果时,它将获得原始值。 如果事实并非如此,则使用引号。

让我再次重复上一句话的重要部分,所以 您不必重新阅读它:

  

它转储字符串,然后解析结果

这意味着它将应用与之匹配的所有正则表达式 加载以查看生成的标量是否以整数形式加载, 浮点,布尔值,日期时间等,以确定是否需要使用引号 是否应用。¹


在任何具有复杂数据的真实应用程序中,基于JSON 翻斗车/装载机太简单了,无法直接使用,甚至更多 与转储相同程序相比,程序中必须包含智能 复杂数据直接发送到YAML。一个简单的例子是当你想工作时 带有日期时间戳,在这种情况下,您必须将字符串转换回去 如果您使用的是JSON,则自己来回datetime.datetime。加载期间 您必须基于这是一个事实来做到这一点 与某些(希望能够识别的)键相关联:

{ "datetime": "2018-09-03 12:34:56" }

或在列表中排名:

["FirstName", "Lastname", "1991-09-12 08:45:00"]

或基于字符串的格式(例如使用正则表达式)。

在所有这些情况下,您的程序都需要做更多的工作。相同 可以进行转储,这不仅意味着额外的开发时间。

让我用机器上的东西重新生成您的计时 因此我们可以将它们与其他度量进行比较。我改写了你的代码 有点,因为它不完整(timeit?)并导入了其他 事情两次。由于出现>>>提示,因此仅剪切和粘贴也是不可能的。

from __future__ import print_function

import sys
import yaml
import cjson
from timeit import timeit

NR=10000
ds = "; d={'foo': {'bar': 1}}"
d = {'foo': {'bar': 1}}

print('yaml.SafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.SafeDumper)
print('cjson.encode:   ', cjson.encode(d))
print()


res = timeit("yaml.dump(d, Dumper=yaml.SafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.SafeDumper ', res)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup="import yaml"+ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("cjson.encode(d)", setup="import cjson"+ds, number=NR)
print('cjson.encode    ', res)

并输出:

yaml.SafeDumper: foo: {bar: 1}
cjson.encode:    {"foo": {"bar": 1}}

yaml.SafeDumper  3.06794905663
yaml.CSafeDumper 0.781533956528
cjson.encode     0.0133550167084

现在让 转储包含datetime

的简单数据结构
import datetime
from collections import Mapping, Sequence  # python 2.7 has no .abc

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}

def stringify(x, key=None):
    # key parameter can be used to dump
    if isinstance(x, str):
       return x
    if isinstance(x, Mapping):
       res = {}
       for k, v in x.items():
           res[stringify(k, key=True)] = stringify(v)  # 
       return res
    if isinstance(x, Sequence):
        res = [stringify(k) for k in x]
        if key:
            res = repr(res)
        return res
    if isinstance(x, datetime.datetime):
        return x.isoformat(sep=' ')
    return repr(x)

print('yaml.CSafeDumper:', end=' ')
yaml.dump(d, sys.stdout, Dumper=yaml.CSafeDumper)
print('cjson.encode:    ', cjson.encode(stringify(d)))
print()

这给出了:

yaml.CSafeDumper: foo: {bar: '1991-09-12 08:45:00'}
cjson.encode:     {"foo": {"bar": "1991-09-12 08:45:00"}}

出于上述原因,我创建了一个包装了myjson的模块 cjson.encode并已定义以上stringify。如果您使用它:

d = {'foo': {'bar': datetime.datetime(1991, 9, 12, 8, 45, 0)}}
ds = 'import datetime, myjson, yaml; d=' + repr(d)
res = timeit("yaml.dump(d, Dumper=yaml.CSafeDumper)", setup=ds, number=NR)
print('yaml.CSafeDumper', res)
res = timeit("myjson.encode(d)", setup=ds, number=NR)
print('cjson.encode    ', res)

给予:

yaml.CSafeDumper 0.813436031342
cjson.encode     0.151570081711

那还是相当简单的输出,已经使您从两个订单中恢复过来 速度差的幅度小于一个数量级。


YAML的普通标量和块样式格式可提供更好的可读性。 您可以在序列(或映射)中使用逗号结尾, 手动编辑与JSON中的相同数据的YAML数据时,失败更少。

YAML标记允许您对(复杂)类型进行数据内指示。什么时候 使用JSON ,您必须在代码中注意其他事项 比映射,序列,整数,浮点数,布尔值和 字符串。这样的代码需要开发时间,不太可能 与python-cjson一样快(您当然可以编写代码 在C语言中也是如此。

转储某些数据,例如递归数据结构(例如拓扑结构) 数据)或复杂键是在PyYAML库中预定义的。那里 JSON库只是出错了,为此实现了解决方法 不平凡且最有可能减慢速度差异无关紧要的事情。

这种功能和灵活性是以较低速度为代价的。什么时候 转储许多简单的东西JSON是更好的选择,您不太可能 无论如何都要手动编辑结果。对于涉及的任何内容 编辑或复杂对象或两者兼而有之,您仍应考虑使用 YAML。


¹可以将所有Python字符串强制转储为YAML 标有(双引号)的标量,但是设置样式不足以 防止所有回读。