如何递归地在 JSON 文件中搜索与给定模式匹配的所有节点,并将 JSON "路径"返回到节点及其值?



假设我在文本文件中有这个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)/"查找名为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数据结构,你想要搜索,而你显示的代码诚实地瞄准了这一点。

但是,有一些库可以提供帮助;要么完全完成工作,要么提供完整的、有效的和经过测试的代码,你可以根据确切的需求进行微调。

模块数据::叶::沃克似乎很合适。一个简单的例子

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 小部件->文本>名称 => 文本1 widget->image->name => sun1

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

您的愿望清单的其余部分也相当容易实现。仔细阅读List::Util和List::MoreUtils以获得数组分析的帮助。

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

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

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

走数据结构并不是那么糟糕,尽管前几次你这样做有点奇怪。CPAN 上有各种模块可以为您做(如 zdim 所示),但这是您可能应该知道如何自己做的事情。我们在中级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

从那里,您可以自定义它以获得所需的其他功能。如果你想找到一个复杂的键,你只需做更多的工作来比较键路径与你想要的。

最新更新