Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

Say I have this JSON in a text file:

{"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;"
    }
}}

Using Perl I have read the file into a JSON object called $json_obj using JSON::XS.

How do I search $json_obj for all nodes called name and return/print the following as the result/output:

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

Notes:

  • node names matching the search term could appear at any level of the tree
  • search terms could be plain text or a regular expression
  • I'd like to be able to supply my own branch separator to override a default of, say, ->
    • example / (for simplicity, I'll just put this in a perl $variable)
  • I would like to be able to specify multiple node levels in my search, so as the specify a path to match, for example: specifying id/colour would return all paths that contain a node called id that is also a parent with a child node called colour
  • displaying double quotes around the result values is optional
  • I want to be able to search for multiple patterns, e.g. /(name|alignment)/ for "find all nodes called name or alignment

Example showing results of search in last note above:

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

Since JSON is mostly just text, I'm not yet sure of the benefit of even using JSON::XS so any advice on why this is better or worse is most welcome.

It goes without saying that it needs to be recursive so it can search n arbitrary levels deep.

This is what I have so far, but I'm only part way there:

#!/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;

The expected result from the above script would be:

widget/image/src: Images/Sun.png
like image 623
skeetastax Avatar asked Dec 05 '22 08:12

skeetastax


1 Answers

Once that JSON is decoded, there is a complex (nested) Perl data structure that you want to search through, and the code you show is honestly aiming for that.

However, there are libraries out there which can help; either to do the job fully or to provide complete, working, and tested code that you can fine tune to the exact needs.

The module Data::Leaf::Walker seems suitable. A simple example

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" 
    }   
}

This 'walker' goes through the data structure, keeping the list of keys to each leaf. This is what makes this module particularly suitable for your quest, along with its simplicity of purpose in comparison to many others. See documentation.

The above prints, for the sample data provided in the question

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

The implementation of the criterion for which key-paths get selected in the code above is rather simple-minded, since it checks for 'name' anywhere in the path, once, and then prints the whole path. While the question doesn't specify what to do about matches earlier in the path, or with multiple ones, this can be adjusted since we always have the full path.

The rest of your wish list is fairly straightforward to implement as well. Peruse List::Util and List::MoreUtils for help with array analysis.

Another module, that is a great starting point for possible specific needs, is Data::Traverse. It is particularly simple, at 70-odd lines of code, so very easy to customize.

like image 154
zdim Avatar answered Dec 21 '22 22:12

zdim