如何递归搜索 JSON 文件以查找与给定模式匹配的所有节点,并将 JSON 'path' 返回到该节点及其值?

ske*_*tax 4 perl json data-structures

假设我在文本文件中有这个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;"
    }
}}
Run Code Online (Sandbox Code Playgroud)

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

如何搜索$json_obj所有调用的节点name并返回/打印以下作为结果/输出:

widget->window->name: main_window
widget->image->name: sun1
widget->text->name: text1
Run Code Online (Sandbox Code Playgroud)

笔记:

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

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

widget->window->name: main_window
widget->image->name: sun1
widget->image->alignment: center
widget->text->name: text1
widget->text->alignment: center
Run Code Online (Sandbox Code Playgroud)

由于 JSON 主要只是文本,我还不确定使用 JSON::XS 的好处,因此非常欢迎任何关于为什么更好或更坏的建议。

不言而喻,它需要是递归的,所以它可以搜索n任意深度的深度。

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

widget->window->name: main_window
widget->image->name: sun1
widget->text->name: text1
Run Code Online (Sandbox Code Playgroud)

上述脚本的预期结果是:

widget/image/src: Images/Sun.png
Run Code Online (Sandbox Code Playgroud)

zdi*_*dim 8

一旦 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" 
    }   
}
Run Code Online (Sandbox Code Playgroud)

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

以上打印,对于问题中提供的样本数据

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

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

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

另一个模块是Data::Traverse,它是满足可能的特定需求的一个很好的起点。特别简单,70多行代码,非常容易定制。


bri*_*foy 7

根据您的任务,您可能会考虑使用jq。此输出很简单,但您可以随心所欲地变得复杂:

$ jq -r '.. | .image? | .src | strings'  test.json
Images/Sun.png
$ jq -r '.. | .name? | strings'  test.json
main_window
sun1
text1
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

接下来,我处理队列中的每个项目 (the while)。我希望 的每个值node都是一个散列,因此我将获得该散列的所有键 (the 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 };
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

我最终得到如下输出:

widget->text->name = text1
widget->image->name = sun1
widget->window->name = main_window
Run Code Online (Sandbox Code Playgroud)

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