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

gui*_*ism 53 python serialization json 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
Run Code Online (Sandbox Code Playgroud)

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

cdl*_*ary 59

通常,输出的复杂性决定了解析的速度,而不是接受输入的复杂性.JSON语法非常简洁.YAML解析器相对复杂,导致开销增加.

JSON最重要的设计目标是简单性和通用性.因此,以降低人类可读性为代价,生成和解析JSON是微不足道的.它还使用最小公分母信息模型,确保每个现代编程环境都可以轻松处理任何JSON数据.

相比之下,YAML最重要的设计目标是人类可读性和对序列化任意本机数据结构的支持.因此,YAML允许极其可读的文件,但生成和解析更复杂.此外,YAML超越了最低公分母数据类型,在不同编程环境之间交叉时需要更复杂的处理.

我不是YAML解析器实现者,所以如果没有一些分析数据和一大堆示例,我就无法专门讨论数量级.无论如何,在对基准数字充满信心之前,一定要测试大量输入.

更新哎呀,误读了这个问题.:-(尽管输入语法很大,但序列化仍然可以非常快;但是,浏览源代码,看起来PyYAML的Python级序列化构建了一个表示图,而simplejson将内置的Python数据类型直接编码到文本块中.

  • 他在询问编码,而不是解析. (4认同)
  • YAML构建图形,因为它是一种通用序列化格式,能够表示对同一对象的多个引用.如果您知道没有重复任何对象并且只显示基本类型,则可以使用json序列化程序,它仍然是有效的YAML. (4认同)
  • (只是一个JSON语法简洁的例子,你不能在数组的末尾有一个无关的尾随逗号.过度使用IMO.) (3认同)

two*_*nac 29

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

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


flo*_*low 20

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

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

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


Gle*_*ard 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']
Run Code Online (Sandbox Code Playgroud)

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

  • 公平地说,您只是评论顶层公共API中暴露了多少复杂性.(只是巧合的是,YAML实际上比JSON更复杂,因此大多数可以用JSON表达的数据表示为JSON会更有效.)但是没有什么能阻止yaml的python库呈现更简单的API,或者公开更多内部细节的json API. (5认同)

Ant*_*hon 9

尽管您有一个可接受的答案,但不幸的是,它只是朝着 PyYAML 文档的方向进行了一些操作,并引用了该文档中不正确的声明:PyYAML在转储期间不会制作表示图,它会创建一个 lineair 流(并且只是比如json保留一桶 ID 以查看是否有递归)。


首先,您必须意识到,虽然cjson转储程序只是手工编写的 C 代码,但 YAML 的 CSafeDumper与普通的纯 Python SafeDumper共享四个转储阶段中的两个(RepresenterResolver),而其他两个阶段(序列化程序和发射器)不是完全用 C 编写,但包含一个 Cython 模块,该模块调用 C 库libyaml进行发射。


除了这一重要部分之外,您的问题为什么需要更长的时间的简单答案是,转储 YAML 会做得更多。这并不是因为 YAML 比 @flow 声称的更难,而是因为 YAML 可以做的额外工作使它比 JSON 强大得多,而且如果您需要使用编辑器处理结果,它也更加用户友好。这意味着即使在应用这些额外功能时,也会在 YAML 库中花费更多时间,并且在许多情况下也只是检查某些内容是否适用。

这是一个示例:即使您从未阅读过 PyYAML 代码,您也会注意到转储程序没有引用foobar。这不是因为这些字符串是键,因为 YAML 没有 JSON 的限制,映射的键必须是字符串。例如一个Python字符串,它是在映射值可以 也无引号(即纯)。

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

PyYAML 如何知道何时引用字符串,何时不引用?在转储时,它实际上首先转储字符串,然后解析结果以确保当它读回该结果时,它获得了原始值。如果事实证明并非如此,则会应用引号。

让我再次重复上一句话的重要部分,这样您就不必重新阅读:

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

这意味着它应用加载时匹配的所有正则表达式,以查看结果标量是否会加载为整数、浮点数、布尔值、日期时间等,以确定是否需要应用引号。¹


在任何具有复杂数据的实际应用程序中,基于 JSON 的转储程序/加载程序太简单而无法直接使用,并且与将相同的复杂数据直接转储到 YAML 相比,您的程序必须具有更多的智能。一个简化的例子是当您想要使用日期时间戳时,在这种情况下,datetime.datetime如果您使用的是 JSON,则必须将字符串来回转换为您自己。在加载过程中,您必须根据以下事实来执行此操作:这是与某些(希望可识别的)键关联的值:

{ "datetime": "2018-09-03 12:34:56" }
Run Code Online (Sandbox Code Playgroud)

或在列表中的位置:

["FirstName", "Lastname", "1991-09-12 08:45:00"]
Run Code Online (Sandbox Code Playgroud)

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

在所有这些情况下,您的程序需要做更多的工作。倾销也是如此,这不仅意味着额外的开发时间。

让我们用我在机器上得到的数据重新生成您的计时,以便我们可以将它们与其他测量值进行比较。我稍微重写了你的代码,因为它不完整(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)
Run Code Online (Sandbox Code Playgroud)

这输出:

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

yaml.SafeDumper  3.06794905663
yaml.CSafeDumper 0.781533956528
cjson.encode     0.0133550167084
Run Code Online (Sandbox Code Playgroud)

现在让我们转储一个简单的数据结构,其中包括 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()
Run Code Online (Sandbox Code Playgroud)

这给出:

yaml.CSafeDumper: foo: {bar: '1991-09-12 08:45:00'}
cjson.encode:     {"foo": {"bar": "1991-09-12 08:45:00"}}
Run Code Online (Sandbox Code Playgroud)

对于上述时间,我创建了一个模块 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)
Run Code Online (Sandbox Code Playgroud)

给予:

yaml.CSafeDumper 0.813436031342
cjson.encode     0.151570081711
Run Code Online (Sandbox Code Playgroud)

那个仍然相当简单的输出,已经让你从两个数量级的速度差异恢复到不到一个数量级。


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

YAML 标签允许在数据中指示您的(复杂)类型。使用 JSON 时,必须在代码中注意比映射、序列、整数、浮点数、布尔值和字符串更复杂的任何内容。这样的代码需要开发时间,并且不太可能像python-cjson(您当然也可以自由地用 C 编写代码。

转储一些数据,如递归数据结构(例如拓扑数据)或复杂的键是在 PyYAML 库中预定义的。那里的 JSON 库只是出错,并为此实施变通方法很重要,并且很可能会减慢速度差异不太相关的事情。

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


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