【问题标题】:How do I recursively search a JSON file for all nodes matching a given pattern and return the JSON 'path' to the node and it's value?如何递归搜索与给定模式匹配的所有节点的 JSON 文件并将 JSON“路径”返回到节点及其值?
【发布时间】:2021-05-23 01:25:26
【问题描述】:

假设我在一个文本文件中有this JSON:

{"widget": {
    "debug": "on",
    "window": {
        "title": "Sample Konfabulator Widget",
        "name": "main_window",
        "width": 500,
        "height": 500
    },
    "image": { 
        "src": "Images/Sun.png",
        "name": "sun1",
        "hOffset": 250,
        "vOffset": 250,
        "alignment": "center"
    },
    "text": {
        "data": "Click Here",
        "size": 36,
        "style": "bold",
        "name": "text1",
        "hOffset": 250,
        "vOffset": 100,
        "alignment": "center",
        "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
    }
}}

使用 Perl,我已使用 JSON::XS 将文件读入名为 $json_obj 的 JSON 对象。

如何在 $json_obj 中搜索所有名为 name 的节点并返回/打印以下内容作为结果/输出:

widget->window->name: main_window
widget->image->name: sun1
widget->text->name: text1

注意事项:

  • 与搜索词匹配的节点名称可以出现在树的任何级别
  • 搜索词可以是纯文本正则表达式
  • 我希望能够提供我自己的分支分隔符来覆盖默认值,例如 ->
    • 示例/(为简单起见,我将把它放在一个perl中$variable
  • 我希望能够在我的搜索中指定多个节点级别,因此指定 path 进行匹配,例如:指定 id/colour 将返回包含名为 id 的节点的所有路径,即还有一个父节点,其子节点名为colour
  • 在结果值周围显示双引号是可选的
  • 我希望能够搜索多种模式,例如/(name|alignment)/ for "查找所有名为 namealignment 的节点

上面最后一个注释中显示搜索结果的示例:

widget->window->name: main_window
widget->image->name: sun1
widget->image->alignment: center
widget->text->name: text1
widget->text->alignment: center

由于 JSON 大多只是文本,我还不确定使用 JSON::XS 的好处,所以欢迎任何关于为什么这是更好或更坏的建议。

不言而喻,它需要递归,因此它可以搜索n任意深度的级别。

这是我目前所拥有的,但我只是其中的一部分:

#!/usr/bin/perl

use 5.14.0;
use warnings;
use strict;
use IO::File;
use JSON::XS;

my $jsonfile = '/home/usr/filename.json';
my $jsonpath = 'image/src'; # example search path
my $pathsep = '/'; # for displaying results

my $fh = IO::File->new("$jsonfile", "r");
my $jsontext = join('',$fh->getlines());
$fh->close();

my $jsonobj = JSON::XS->new->utf8->pretty;

if (defined $jsonpath) {
    my $perltext = $jsonobj->decode($jsontext); # is this correct?
    recurse_tree($perltext);
} else {
    # print file to STDOUT
    say $jsontext;
}

sub recurse_tree {
    my $hash = shift @_;
    foreach my $key (sort keys %{$hash}) {
        if ($key eq $jsonpath) {
            say "$key = %{$hash}{$key} \n"; # example output
        }
        if (ref $hash->{$key} eq 'HASH' ||
            ref $hash->{$key} eq 'ARRAY') {
            recurse_tree($hash->{$key});
        }
    }
}

exit;

上述脚本的预期结果是:

widget/image/src: Images/Sun.png

【问题讨论】:

    标签: json perl data-structures


    【解决方案1】:

    一旦 JSON 被解码,就会有一个复杂的(嵌套的)Perl 数据结构供您搜索,而您展示的代码正是针对该结构的。

    但是,有一些图书馆可以提供帮助;要么完全完成这项工作,要么提供完整、有效且经过测试的代码,您可以根据具体需求进行微调。

    模块Data::Leaf::Walker 似乎很合适。一个简单的例子

    use warnings;
    use strict;
    use feature 'say';
    
    use Data::Dump qw(dd);
    use JSON;
    use List::Util qw(any);
    
    use Data::Leaf::Walker;
    
    my $file = shift // 'data.json';                       # provided data sample
    
    my $json_data = do { local (@ARGV, $/) = $file; <> };  # read into a string
    chomp $json_data;
    
    my $ds = decode_json $json_data;
    dd $ds; say '';                   # show decoded data
        
    my $walker = Data::Leaf::Walker->new($ds);
    
    my $sep = '->';
    while ( my ($key_path, $value) = $walker->each ) { 
        my @keys_in_path = @$key_path;
        if (any { $_ eq 'name' } @keys_in_path) {          # selection criteria
            say join($sep, @keys_in_path), " => $value" 
        }   
    }
    

    这个“walker”遍历数据结构,保留每个叶子的键列表。这就是使该模块特别适合您的任务的原因,并且与许多其他模块相比,其目的简单。请参阅文档。

    以上打印,用于问题中提供的示例数据

    小部件->窗口->名称 => main_window 小部件->文本->名称=> text1 小部件->图像->名称 => sun1

    在上面的代码中选择键路径的标准的实现相当简单,因为它会检查路径中任何位置的'name',一次,然后打印整个路径。虽然问题没有指定如何处理路径中较早的匹配项或多个匹配项,但可以调整这一点,因为我们始终拥有完整路径。

    您的愿望清单的其余部分也很容易实现。仔细阅读 List::UtilList::MoreUtils 以获得有关数组分析的帮助。

    另一个模块是Data::Traverse,它是满足可能的特定需求的一个很好的起点。它特别简单,70-odd lines of code,非常容易定制。

    【讨论】:

    • 欢迎 :) 如果出现问题,请告诉我。 (我不想转储可能不需要的代码“解释”,但如果需要澄清,请告诉我)
    【解决方案2】:

    根据您的任务,您可以考虑使用jq。此输出很简单,但您可以随意复杂:

    $ jq -r '.. | .image? | .src | strings'  test.json
    Images/Sun.png
    $ jq -r '.. | .name? | strings'  test.json
    main_window
    sun1
    text1
    

    遍历数据结构并没有那么糟糕,尽管前几次这样做有点奇怪。 CPAN 上有各种模块可以为您完成(如zdim shows),但您可能应该知道如何自己做。我们在Intermediate Perl 中有一些重要的例子。

    一种方法是从要处理的事情队列开始。这是迭代,而不是递归,根据您向队列中添加元素的方式,您可以进行深度优先或广度优先搜索。

    对于每个项目,我将跟踪到达那里的键的路径以及子哈希。这就是您的递归方法的问题:您不允许跟踪路径。

    一开始,队列只有一个项目,因为我们在顶部。我还将定义一个目标键,因为您的问题是:

    my @queue = ( { key_path => [], node => $hash } );
    my $target = 'name';
    

    接下来,我处理队列中的每个项目(while)。我希望node 的每个值都是一个散列,所以我将获得该散列的所有键(foreach)。这代表哈希的下一级。

    在 foreach 中,我创建了一个新的关键路径,其中包含与我正在处理的路径一起存在的路径。我还通过使用该键获得下一个值。

    之后,我可以进行特定于任务的处理。如果我找到了我的目标密钥,我会做任何我需要做的事情。在这种情况下,我输出了一些东西,但我可以添加到不同的数据结构等等。我使用next 停止处理该密钥(尽管我可以继续处理)。如果我没有找到目标键,如果值是另一个哈希引用,我会在队列中创建另一个条目。

    然后,我回去处理队列。

    use v5.24; # use postfix dereferencing
    
    while( my $h = shift @queue ) {
        foreach my $next_key ( keys $h->{node}->%* ) {
            my $key_path = [ $h->{key_path}->@*, $next_key ];
            my $value    = $h->{node}{$next_key};
    
            if( $next_key eq $target ) {
                say join( "->", $key_path->@* ), " = $value";
                next;
                }
            elsif( ref $value eq ref {} ) {
                push @queue, { key_path => $key_path, node => $value };
                }
            }
        }
    

    我最终得到如下输出:

    widget->text->name = text1
    widget->image->name = sun1
    widget->window->name = main_window
    

    从那里,您可以对其进行自定义以获得您需要的其他功能。如果您想找到一个复杂的密钥,您只需多做一些工作,将密钥路径与您想要的路径进行比较即可。

    【讨论】:

    • 哇,也非常感谢您的回答。我很荣幸收到您的回复,布赖恩。非常感激。我昨天确实尝试了 jq 但我只是无法正确使用语法来使其正常工作。不过,我会再试一次。 (不过,我确实有点想在蛇群中高举 Perl 旗帜 :))
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-12
    • 1970-01-01
    • 2021-01-06
    • 1970-01-01
    • 1970-01-01
    • 2012-02-06
    • 1970-01-01
    相关资源
    最近更新 更多