跳转到主要内容

合并一系列JSON文档。

项目描述

此Python模块允许您将一系列JSON文档合并成一个。

例如,当不同的作者填写共同文档的不同部分,并且您需要构建一个包含所有作者贡献的文档时,这个问题经常出现。它还有助于处理文档的连续版本,其中不同的字段会随着时间的推移而更新。

考虑一个简单的例子,有两个文档

>>> base = {
...         "foo": 1,
...         "bar": [ "one" ],
...      }

>>> head = {
...         "bar": [ "two" ],
...         "baz": "Hello, world!"
...     }

我们将要合并更改的文档称为 base,更改的文档称为 head。要使用 jsonmerge 合并这两个文档

>>> from pprint import pprint

>>> from jsonmerge import merge
>>> result = merge(base, head)

>>> pprint(result, width=40)
{'bar': ['two'],
 'baz': 'Hello, world!',
 'foo': 1}

如您所见,当遇到JSON对象时,jsonmerge 默认返回出现在 basehead 文档中的字段。对于其他JSON类型,它简单地替换旧值。这些原则也适用于多个嵌套JSON对象的情况。

然而,在更实际的使用案例中,您可能希望对文档的不同部分应用不同的 合并策略。您可以使用基于 JSON模式 的语法来告诉 jsonmerge 如何做到这一点。

如果您已经有了文档的模式,您只需扩展它们以包含一些额外的关键字。除了下面描述的自定义关键字之外,jsonmerge 默认使用在 Draft 4 中定义的JSON模式语法。

您使用 mergeStrategy 模式关键字来指定策略。上面提到的默认两种策略分别称为 objectMerge(用于对象)和 overwrite(用于所有其他类型)。

假设您想指定示例文档中合并的 bar 字段应包含所有文档的元素,而不仅仅是最新的一份。您可以使用如下模式实现

>>> schema = {
...             "properties": {
...                 "bar": {
...                     "mergeStrategy": "append"
...                 }
...             }
...         }

>>> from jsonmerge import Merger
>>> merger = Merger(schema)
>>> result = merger.merge(base, head)

>>> pprint(result, width=40)
{'bar': ['one', 'two'],
 'baz': 'Hello, world!',
 'foo': 1}

另一个常见示例是当您需要保留一系列文档中出现的值的版本列表时。

>>> schema = {
...             "properties": {
...                 "foo": {
...                     "type": "object",
...                     "mergeStrategy": "version",
...                     "mergeOptions": { "limit": 5 }
...                 }
...             },
...             "additionalProperties": False
...         }
>>> from jsonmerge import Merger
>>> merger = Merger(schema)

>>> rev1 = {
...     'foo': {
...         'greeting': 'Hello, World!'
...     }
... }

>>> rev2 = {
...     'foo': {
...         'greeting': 'Howdy, World!'
...     }
... }

>>> base = None
>>> base = merger.merge(base, rev1, merge_options={
...                     'version': {
...                         'metadata': {
...                             'revision': 1
...                         }
...                     }
...                 })
>>> base = merger.merge(base, rev2, merge_options={
...                     'version': {
...                         'metadata': {
...                             'revision': 2
...                         }
...                     }
...                 })
>>> pprint(base, width=55)
{'foo': [{'revision': 1,
          'value': {'greeting': 'Hello, World!'}},
         {'revision': 2,
          'value': {'greeting': 'Howdy, World!'}}]}

请注意,我们在模式中使用 mergeOptions 关键字来提供合并策略的附加选项。在这种情况下,我们告诉 version 策略只保留该字段的最后 5 个版本。

我们还使用了 merge_options 参数来提供一些特定于每次 merge 方法调用的选项。以这种方式指定的选项应用于模式中特定策略的所有调用(与仅应用于模式中该特定位置策略调用的 mergeOptions 相比)。在 mergeOptions 模式关键字中指定的选项覆盖了在 merge_options 参数中指定的选项。

version 策略的 metadata 选项可以包含一些文档元数据,这些元数据包含在字段的每个版本中。metadata 可以包含一个任意的 JSON 对象。

上面的示例还演示了在合并多个文档时通常如何使用 jsonmerge。通常,您从一个空的 base 开始,然后依次将不同的 heads 合并到其中。

问题的常见来源是不匹配用于合并的模式文档。jsonmerge 本身不验证输入文档。它只使用模式来获取应用适当合并策略所需的信息。由于默认策略用于模式未涵盖的文档部分,因此很容易在没有明显的错误提示的情况下获得意外的输出。

在以下示例中,属性 Foo(大写 F)与模式中的 foo(小写 f)不匹配,因此 version 策略没有像前两个版本那样应用。

>>> rev3 = {
...     'Foo': {
...         'greeting': 'Howdy, World!'
...     }
... }

>>> base = merger.merge(base, rev3, merge_options={
...                     'version': {
...                         'metadata': {
...                             'revision': 3
...                         }
...                     }
...                 })

>>> pprint(base, width=55)
{'Foo': {'greeting': 'Howdy, World!'},
 'foo': [{'revision': 1,
          'value': {'greeting': 'Hello, World!'}},
         {'revision': 2,
          'value': {'greeting': 'Howdy, World!'}}]}

因此,建议在将文档传递给 jsonmerge 之前对输入文档进行模式验证。如果模式中包含比 jsonmerge 严格必要的信息更多,则此做法更为有效(例如,添加有关类型的信息,使用 additionalProperties 限制有效的对象属性等)。

>>> from jsonschema import validate
>>> validate(rev1, schema)
>>> validate(rev2, schema)
>>> validate(rev3, schema)
Traceback (most recent call last):
    ...
jsonschema.exceptions.ValidationError: Additional properties are not allowed ('Foo' was unexpected)

如果您关心文档的规范性,您可能还希望获取由 merge 方法创建的文档的模式。jsonmerge 提供了一种从输入文档的模式自动生成它的方法。

>>> result_schema = merger.get_schema()

>>> pprint(result_schema, width=80)
{'additionalProperties': False,
 'properties': {'foo': {'items': {'properties': {'value': {'type': 'object'}}},
                        'maxItems': 5,
                        'type': 'array'}}}

请注意,由于 version 策略,foo 字段的类型从 object 变为 array

合并策略

这些是目前实现的合并策略。

overwrite

使用 base 中的值覆盖 head 中的值。与任何类型都兼容。

discard

保留 base 中的值,即使 head 包含不同的值。与任何类型都兼容。

默认情况下,如果 base 不包含任何值(即文档的这一部分未定义),合并后的值保持未定义。这可以通过 keepIfUndef 选项进行更改。如果此选项为 true,则在这种情况下将保留 head 中的值。这在合并一系列文档时很有用,您想保留系列中首次出现的值,但想丢弃进一步的修改。

append

追加数组。仅适用于数组。

您可以通过指定 sortByRef 合并选项来指示用于对数组中的项进行排序的键。此选项可以是任意的 JSON 指针。当解析指针时,根位于数组项的根处。可以通过设置 sortReverse 选项来反转排序顺序。

arrayMergeById

通过 ID 字段标识要合并的项来合并数组。结果数组包含来自 basehead 数组的项。具有相同 ID 的任何项将根据层次结构中更下方的指定策略进行合并。

默认情况下,期望数组项是对象,项的 ID 从对象的 id 属性中获得。

您可以使用任意的JSON指针来指定使用idRef合并选项的项目ID。在解析指针时,文档根被放置在数组项的根目录下(例如,默认情况下,idRef为‘/id’)。您还可以将idRef设置为‘/’将整数或字符串数组视为一组唯一值。

head中的数组项无法识别ID(例如,idRef指针无效)将被忽略。

您可以使用ignoreId合并选项指定要忽略的额外项目ID。

通过将idRef设置为指针数组,可以指定复合ID。在这种情况下,如果数组中的任何指针在head中的对象无效,则该对象将被忽略。如果使用数组作为idRef,并且定义了ignoreId选项,则ignoreId也必须为数组。

您可以使用sortByRef合并选项来指定将用于对数组中的项目进行排序的键。此选项可以是任意的JSON指针。指针的解析方式与idRef相同。可以通过设置sortReverse选项来反转排序顺序。

arrayMergeByIndex

通过数组中的索引合并数组项。与arrayMergeById策略类似,结果数组包含来自basehead数组的项。在两个数组中位置相同的项将根据层次结构中指定的策略进行合并。

objectMerge

合并对象。结果对象具有来自basehead的属性。在basehead中都存在的任何属性都根据层次结构中指定的策略进行合并(例如,在propertiespatternPropertiesadditionalProperties模式关键字中)。

objClass选项允许您请求使用不同的字典类来存储JSON对象。可能的值是与特定Python类对应的名称。内置名称包括OrderedDict,使用collections.OrderedDict类,或dict,使用Python的dict内置。如果没有指定,则默认使用dict

请注意,可以通过Merger()构造函数配置额外的类或不同的默认值(见下文)。

version

将值的类型更改为数组。以包含value属性的对象形式将新值附加到数组中。这样,在合并期间看到的所有值都被保留。

您可以使用metadata选项向附加的对象添加其他属性。此外,您可以使用metadataSchema选项指定metadata选项中对象的模式。

您可以使用mergeOptions关键字中的limit选项限制列表的长度。

默认情况下,如果head文档包含与base文档相同的值,则不会附加新版本。您可以通过将ignoreDups选项设置为false来更改此设置。

如果在模式中没有指定合并策略,则对于对象使用objectMerge,对于所有其他值使用overwrite(但请参阅下面有关应用于子模式的键字的章节)。

您可以通过创建jsonmerge.strategies.Strategy的子类并传递给Merger()构造函数来实现自己的策略(见下文)。

Merger类

Merger类允许您通过以下方式进一步自定义JSON数据的合并:

  • 设置包含合并策略配置的模式,

  • 提供额外的策略实现,

  • 设置用于存储JSON对象数据的默认类,

  • 通过objClass合并选项配置可选的JSON对象类。

Merger构造函数接受以下参数(所有参数均为可选,除schema外)

schema

包含合并策略指令的JSON Schema,以JSON对象的形式提供。如果不需要策略配置,则应提供一个空字典。

策略

将策略名称映射到策略类实例的字典。这些将与内置策略合并(如果有相同名称的实例,则会被覆盖)。

objclass_def

默认情况下用于在合并结果中保存JSON数据的支持字典类名称。名称必须与内置名称或objclass_menu参数中提供的名称匹配。

objclass_menu

提供用于作为JSON对象容器的额外类的字典。键是可以用作objectMerge策略的objClass选项或objclass_def参数值的名称。每个值是一个函数或类,它生成JSON对象容器的一个实例。它必须支持一个可选的字典样对象作为参数,用于初始化其内容。

validatorclass

jsonschema.Validator的子类。这可以用来指定在合并期间将使用哪个JSON Schema草稿版本。版本之间的某些细节(如参考解析)可能不同。默认情况下,使用Draft 4验证器。

支持应用子架构的关键字

对于使用关键字allOfanyOfoneOf的架构的复杂文档合并可能会出现问题。这样的文档没有定义良好的类型,可能需要合并不同类型的数据,这可能导致某些策略失败。在这种情况下,get_schema()也可能返回永远不会验证的架构。

对于此类架构,overwrite策略通常是安全的选择。

如果在allOfanyOfoneOf关键字与相同级别的合并策略定义,则jsonmerge将使用定义的策略,并且不会进一步处理这些关键字下的任何子架构。然而,策略会像往常一样向下扩展(例如,objectMerge将考虑与allOf关键字在同一级别的properties关键字下的子架构)。

如果没有显式定义合并策略并且存在allOfanyOf关键字,则jsonmerge将引发错误。

如果没有显式定义合并策略并且存在oneOf关键字,则jsonmerge将继续在同时验证baseheadoneOf分支上。如果没有任何分支验证,它将引发错误。

您可以通过为您的自定义策略定义更复杂的行为来定义更复杂的行为,该策略定义在这种情况下要做什么。有关如何做到这一点,请参阅Strategy类的文档字符串。

安全考虑

JSON架构文档可以包含指向外部架构的$ref引用。jsonmerge使用由jsonschema模块提供的机制解析这些引用。外部引用可能导致HTTP或类似网络请求的执行。

如果jsonmerge在不受信任的输入上使用,这可能导致类似于XML外部实体(XXE)攻击的漏洞。

要求

jsonmerge支持Python 2(2.7)和Python 3(3.5及更高版本)。

您需要安装jsonschema模块(https://pypi.python.org/pypi/jsonschema)。

安装

要从Python包索引安装最新的jsonmerge版本

pip install jsonmerge

源代码

最新的开发版本可在GitHub上获得:https://github.com/avian2/jsonmerge

要从源代码安装,请在源代码分发的顶部运行以下命令

pip install .

jsonmerge使用Tox进行测试。要运行测试套件,请运行

tox

故障排除

《jsonmerge》最常见的问题是在合并时得到意外的结果。找出《jsonmerge》产生特定结果的确切原因可能很复杂,尤其是在头和基本结构非常大时。最常见的原因是传入《jsonmerge》的架构或头和基本结构存在问题,而不是《jsonmerge》本身的bug。

以下是一些调试《jsonmerge》问题的技巧:

  • 尽量缩小问题范围。修剪与您的问题无关的头和基本结构分支,然后重新运行合并。通常,只是更清楚地查看相关部分就能暴露问题。

  • 《jsonmerge》使用标准的logging Python模块来在合并过程中打印其操作。您需要将详细程度增加到DEBUG级别才能看到消息。

  • 一个非常常见的错误是误解架构的哪个部分适用于头和基本结构的哪个部分。前面提到的调试日志对此非常有帮助,因为它们显示了合并如何进入所有相关结构的层次结构以及何时使用默认策略。

  • 在头和基本结构很大时,它们的部分可能不是您想象的那样。在将它们传递给《jsonmerge》之前,使用《jsonschema》库验证您的输入与您的架构。确保您的架构限制足够严格。

  • 特别注意使用oneOfanyOfallOf关键字的架构部分。这些有时可以以意想不到的方式验证。

  • 另一个可能的问题是$ref指针,如果它们会导致递归。使用《jsonmerge》与递归架构是可以的,但它们通常会生成意想不到的结果。

报告bug和贡献代码

感谢您为《jsonmerge》做出贡献!没有像您这样的用户的贡献,自由软件是不可能的。然而,请考虑我是在业余时间维护这个项目的。因此,我要求您遵循以下简单的礼节,以最大限度地减少包含您的贡献所需的工作量。

请使用GitHub issues来报告bug。

在报告bug之前,请确保以下内容:

  • 您已阅读整个README文件。

  • 您已阅读README文件中的故障排除部分。

  • 如果您报告的bug已经被报告过,请查看现有的问题。

确保您的报告包括以下内容:

  • 一个最小但完整的代码示例,可以重现问题,包括运行它所需的任何JSON数据。它应该是可以复制粘贴到.py文件中并运行的内容。

  • 《jsonmerge》和《jsonschema》的相关版本 - 要么是PyPi上的发布号,要么是git提交哈希。

  • 如果您正在报告未处理的异常,请提供跟踪副本。

  • 您认为应该是正确输出的示例,如果您正在报告合并或架构生成错误的输出。

请使用GitHub pull requests来贡献代码。请确保您的pull request

  • 通过所有现有测试,并包括覆盖新增代码的新测试。

  • 更新README.rst以记录新增功能。

许可证

版权所有2023,Tomaz Solc <tomaz.solc@tablix.org>

MIT许可证(MIT)

本软件及其相关文档文件(以下简称“软件”)的使用权、复制权、修改权、合并权、发布权、分发权、再许可权和/或销售副本的权利,以及允许获得软件副本的个人在不限制的情况下处理软件,包括但不限于上述权利,并允许向提供软件的个人提供此类权限,但须遵守以下条件

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的,包括但不限于适销性、针对特定目的的适用性和非侵权性。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论其基于合同、侵权或其他原因,是否与软件、软件的使用或其他相关事宜有关。

项目详情


下载文件

下载适用于您的平台文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源代码发行版

jsonmerge-1.9.2.tar.gz (34.7 kB 查看哈希)

上传时间 源代码

构建发行版

jsonmerge-1.9.2-py3-none-any.whl (19.4 kB 查看哈希)

上传时间 Python 3

支持者