我该如何在Perl中合并两个复杂的数据结构?

3
我有一个JSON格式的配置文件,需要加载到Perl中,然后从另一个JSON文件中加载新设置并部分覆盖/扩展。可能添加/更改的特定设置因情况而异,因此我希望使此覆盖尽可能灵活。
我的计划是将新的部分配置合并到现有的完整配置对象中-可以是JSON格式的,也可以是使用decode_json后的Perl嵌套数据结构。这个转换很好用。
在Perl中有没有简单高效的方法来做到这一点,而不是递归遍历复杂的数据结构并进行大量特定比较?我已经研究了Hash::Merge,但它正在破坏我的数据。问题似乎是它查看高级键/值对(下面的“config”或“bookToolbar”)并覆盖该高级别上的完整键/值对。我想要的是深度优先搜索并覆盖它能找到的最具体的值,同时保留原始的其他键/值对。
例如,这是一个“完整”的配置:
{
    "config" : {
        "bookToolbar" : {
            "highlights" : {
                "enabled" : false
            },
            "bookmark" : {
                "enabled" : false
            }
        },
        "pageAspectRatio" : {
            "width" : "432",
            "height" : "648"
        },
        "highlighter" : {
            "sharedColor" : "#000000",
            "colors" : [
                "#ffff00"
            ]
        }
        "mainMenu" : {
            "index" : {
                "dataPath" : "data/index/",
                "enabled" : false
            },
            "media" : {
                "dataPath" : "data/media.xml",
                "enabled" : false
            },
            "toc" : {
                "dataPath" : "data/toc.xml"
            },
            "glossary" : {
                "audioPath" : "audio/glossary/",
                "dataPath" : "data/glossary.xml",
                "imagePath" : "img/glossary/",
                "enabled" : false
            }
        }
    },
    "pagelist" : [{
            "hasOnPageNotes" : true,
            "pageName" : "cover",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "cover"
        }, {
            "hasOnPageNotes" : true,
            "pageName" : "1",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "1"
        }
    ]
}

以下是我想要用来部分覆盖/扩展上述内容的数据:

{
    "config" : {
        "bookToolbar" : {
            "bookmark" : {
                "enabled" : true
            },
            "help" : {
                "data" : {
                    "url" : "aGreatHelpFile.html"
                },
                "enabled" : true
            },
            "links" : {
                "enabled" : true
            }
        }
    },
    "pagelist" : [{
            "hasOnPageNotes" : true,
            "pageName" : "2",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "2"
        }
    ]
}

我的期望输出是:

{
    "config" : {
        "bookToolbar" : {
            "highlights" : {
                "enabled" : false
            },
            "help" : {
                "data" : {
                    "url" : "aGreatHelpFile.html"
                },
                "enabled" : true
            },
            "bookmark" : {
                "enabled" : true
            }
            "links" : {
                "enabled" : false
            }
        },
        "pageAspectRatio" : {
            "width" : "432",
            "height" : "648"
        },
        "highlighter" : {
            "sharedColor" : "#000000",
            "colors" : [
                "#ffff00"
            ]
        },
        "mainMenu" : {
            "index" : {
                "dataPath" : "data/index/",
                "enabled" : false
            },
            "media" : {
                "dataPath" : "data/media.xml",
                "enabled" : false
            },
            "toc" : {
                "dataPath" : "data/toc.xml"
            },
            "glossary" : {
                "audioPath" : "audio/glossary/",
                "dataPath" : "data/glossary.xml",
                "imagePath" : "img/glossary/",
                "enabled" : false
            }
        }
    },
    "pagelist" : [{
            "hasOnPageNotes" : true,
            "pageName" : "cover",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "cover"
        }, {
            "hasOnPageNotes" : true,
            "pageName" : "1",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "1"
        }, {
            "hasOnPageNotes" : true,
            "pageName" : "2",
            "hasScreenReader" : false,
            "hasTextMarkup" : true,
            "hasLinks" : false,
            "pageId" : "2"
        }
    ]
}

2
我正在使用 RIGHT_PRECEDENT 进行合并,看起来得到了你想要的结果。Hash::Merge 的文档提到在 Windows 上运行 ActiveState Perl 或旧版本的 Clone 模块时可能会出现一些问题。这两个问题可能会影响你吗? - rutter
1
嗯,我尝试了RIGHT_PRECEDENT但没有成功,但它听起来确实是我想要的。我确实在Windows上使用ActiveState发行版,所以可能是问题所在...让我在我的Mac上尝试一下,看看是否有所改善。 - uptownnickbrown
1
因此,在我的 Mac 上使用 Hash::MergeRIGHT_PRECEDENT 完美地工作了...在更新 Clone 到最新版本(Clone-0.34)后,它也在 Windows 上运行!感谢您重新审视该行为选项的提示。 - uptownnickbrown
1个回答

4

事实证明,在Windows上,Hash::Merge只是在使用过时的Clone模块版本时才会破坏我的数据。

使用最新版本(或者在我的Mac上)运行以下代码可以完全满足我的需求:

#!/usr/bin/env perl -w
use strict;
use JSON;
use Hash::Merge qw( merge );
Hash::Merge::set_behavior('RIGHT_PRECEDENT');

# Load full config into hashref
open (IN, "<:utf8", "full-config.txt");
my $app_data;
while(<IN>) {$app_data .= $_;}
my $app_json = decode_json($app_data);
close IN;

# Sample portion of config options to override/extend
my $app_override = '{"config": {
                        "bookToolbar": {
                            "bookmark": {
                                "enabled":false
                            }, "help": {
                                "data": {
                                    "url":"aGreatHelpFile.html"
                                }, "enabled":true
                            }, "closeBook": {
                                "enabled":true
                            }
                        }
                    },
                    "pagelist":[
                        {
                             "hasOnPageNotes" : true,
                             "pageName" : "25",
                             "hasTextMarkup" : true,
                             "hasScreenReader" : false,
                             "hasLinks" : false,
                             "pageId" : "0025"
                        }
                    ]
                }';
my $app_override_hash = from_json($app_override, {utf8 => 1});

# Merge with right precedent, $app_json hash ref has everything we need.
$app_json = merge( $app_json, $app_override_hash );

我发现这张表格对于分析Hash::Merge中不同的优先级选项非常有帮助(这来自于文档):

    LEFT TYPE   RIGHT TYPE      LEFT_PRECEDENT       RIGHT_PRECEDENT
     SCALAR      SCALAR            $a                   $b
     SCALAR      ARRAY             $a                   ( $a, @$b )
     SCALAR      HASH              $a                   %$b
     ARRAY       SCALAR            ( @$a, $b )          $b
     ARRAY       ARRAY             ( @$a, @$b )         ( @$a, @$b )
     ARRAY       HASH              ( @$a, values %$b )  %$b 
     HASH        SCALAR            %$a                  $b
     HASH        ARRAY             %$a                  ( values %$a, @$b )
     HASH        HASH              merge( %$a, %$b )    merge( %$a, %$b )

    LEFT TYPE   RIGHT TYPE  STORAGE_PRECEDENT   RETAINMENT_PRECEDENT
     SCALAR      SCALAR     $a                  ( $a ,$b )
     SCALAR      ARRAY      ( $a, @$b )         ( $a, @$b )
     SCALAR      HASH       %$b                 merge( hashify( $a ), %$b )
     ARRAY       SCALAR     ( @$a, $b )         ( @$a, $b )
     ARRAY       ARRAY      ( @$a, @$b )        ( @$a, @$b )
     ARRAY       HASH       %$b                 merge( hashify( @$a ), %$b )
     HASH        SCALAR     %$a                 merge( %$a, hashify( $b ) )
     HASH        ARRAY      %$a                 merge( %$a, hashify( @$b ) )
     HASH        HASH       merge( %$a, %$b )   merge( %$a, %$b )

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