【问题标题】:Parse any number of subkeys in a perl hash在 perl 哈希中解析任意数量的子键
【发布时间】:2017-04-08 02:23:54
【问题描述】:

我有一个通过解析 JSON 获得的 perl 哈希。 JSON 可以是用户定义的 API 可以生成的任何内容。目标是获取日期/时间字符串并根据用户定义的阈值确定该日期/时间是否超出范围。我唯一的问题是 perl 在处理哈希键/子键迭代时似乎有点麻烦。如何查看所有键并确定整个哈希中是否存在键或子键?我已经阅读了整个 stackoverflow 中的许多线程,但没有什么能完全满足我的需求。我上周才开始使用 perl,所以我可能会遗漏一些东西......如果是这样,请告诉我。

以下是“相关”代码/子项。所有代码见:https://gitlab.com/Jedimaster0/check_http_freshness

use warnings;
use strict;
use LWP::UserAgent;
use Getopt::Std;
use JSON::Parse 'parse_json';
use JSON::Parse 'assert_valid_json';
use DateTime;
use DateTime::Format::Strptime;

# Verify the content-type of the response is JSON
eval {
        assert_valid_json ($response->content);
};
if ( $@ ){
        print "[ERROR] Response isn't valid JSON. Please verify source data. \n$@";
        exit EXIT_UNKNOWN;
} else {
        # Convert the JSON data into a perl hashrefs
        $jsonDecoded = parse_json($response->content);
        if ($verbose){print "[SUCCESS] JSON FOUND -> ", $response->content , "\n";}

        if (defined $jsonDecoded->{$opts{K}}){
                if ($verbose){print "[SUCCESS] JSON KEY FOUND -> ", $opts{K}, ": ", $jsonDecoded->{$opts{K}}, "\n";}
                NAGIOS_STATUS(DATETIME_DIFFERENCE(DATETIME_LOOKUP($opts{F}, $jsonDecoded->{$opts{K}})));
        } else {
                print "[ERROR] Retreived JSON does not contain any data for the specified key: $opts{K}\n";
                exit EXIT_UNKNOWN;
        }
}




sub DATETIME_LOOKUP {
        my $dateFormat = $_[0];
        my $dateFromJSON = $_[1];

        my $strp = DateTime::Format::Strptime->new(
                pattern   => $dateFormat,
                time_zone => $opts{z},
                on_error  => sub { print "[ERROR] INVALID TIME FORMAT: $dateFormat OR TIME ZONE: $opts{z} \n$_[1] \n" ; HELP_MESSAGE(); exit EXIT_UNKNOWN; },
        );

        my $dt = $strp->parse_datetime($dateFromJSON);
        if (defined $dt){
                if ($verbose){print "[SUCCESS] Time formatted using -> $dateFormat\n", "[SUCCESS] JSON date converted -> $dt $opts{z}\n";}
                return $dt;
        } else {
                print "[ERROR] DATE VARIABLE IS NOT DEFINED. Pattern or timezone incorrect."; exit EXIT_UNKNOWN
        }
}




# Subtract JSON date/time from now and return delta
sub DATETIME_DIFFERENCE {
        my $dateInitial = $_[0];
        my $deltaDate;
        # Convert to UTC for standardization of computations and it's just easier to read when everything matches.
        $dateInitial->set_time_zone('UTC');

        $deltaDate = $dateNowUTC->delta_ms($dateInitial);
        if ($verbose){print "[SUCCESS] (NOW) $dateNowUTC UTC - (JSON DATE) $dateInitial ", $dateInitial->time_zone->short_name_for_datetime($dateInitial), " = ", $deltaDate->in_units($opts{u}), " $opts{u} \n";}

        return $deltaDate->in_units($opts{u});
}

样本数据

{
  "localDate":"Wednesday 23rd November 2016 11:03:37 PM",
  "utcDate":"Wednesday 23rd November 2016 11:03:37 PM",
  "format":"l jS F Y h:i:s A",
  "returnType":"json",
  "timestamp":1479942217,
  "timezone":"UTC",
  "daylightSavingTime":false,
  "url":"http:\/\/www.convert-unix-time.com?t=1479942217",
  "subkey":{
    "altTimestamp":1479942217,
    "altSubkey":{
      "thirdTimestamp":1479942217
    }
  }
}

[已解决]

我使用了@HåkonHægland 提供的答案。以下是以下代码更改。使用 flatten 模块,我可以使用任何与 JSON 键匹配的输入字符串。我还有一些工作要做,但您可以看到问题已解决。谢谢@HåkonHægland。

use warnings;
use strict;
use Data::Dumper;
use LWP::UserAgent;
use Getopt::Std;
use JSON::Parse 'parse_json';
use JSON::Parse 'assert_valid_json';
use Hash::Flatten qw(:all);
use DateTime;
use DateTime::Format::Strptime;

# Verify the content-type of the response is JSON
eval {
        assert_valid_json ($response->content);
};
if ( $@ ){
        print "[ERROR] Response isn't valid JSON. Please verify source data. \n$@";
        exit EXIT_UNKNOWN;
} else {
        # Convert the JSON data into a perl hashrefs
        my $jsonDecoded = parse_json($response->content);
        my $flatHash = flatten($jsonDecoded);

        if ($verbose){print "[SUCCESS] JSON FOUND -> ", Dumper($flatHash), "\n";}

        if (defined $flatHash->{$opts{K}}){
                if ($verbose){print "[SUCCESS] JSON KEY FOUND -> ", $opts{K}, ": ", $flatHash>{$opts{K}}, "\n";}
                NAGIOS_STATUS(DATETIME_DIFFERENCE(DATETIME_LOOKUP($opts{F}, $flatHash->{$opts{K}})));
        } else {
                print "[ERROR] Retreived JSON does not contain any data for the specified key: $opts{K}\n";
                exit EXIT_UNKNOWN;
        }
}

例子:

./check_http_freshness.pl -U http://bastion.mimir-tech.org/json.html -K result.creation_date -v
[SUCCESS] JSON FOUND -> $VAR1 = {
          'timestamp' => '20161122T200649',
          'result.data_version' => 'data_20161122T200649_data_news_topics',
          'result.source_version' => 'kg_release_20160509_r33',
          'result.seed_version' => 'seed_20161016',
          'success' => 1,
          'result.creation_date' => '20161122T200649',
          'result.data_id' => 'data_news_topics',
          'result.data_tgz_name' => 'data_news_topics_20161122T200649.tgz',
          'result.source_data_version' => 'seed_vtv: data_20161016T102932_seed_vtv',
          'result.data_digest' => '6b5bf1c2202d6f3983d62c275f689d51'
        };

Odd number of elements in anonymous hash at ./check_http_freshness.pl line 78, <DATA> line 1.
[SUCCESS] JSON KEY FOUND -> result.creation_date:
[SUCCESS] Time formatted using -> %Y%m%dT%H%M%S
[SUCCESS] JSON date converted -> 2016-11-22T20:06:49 UTC
[SUCCESS] (NOW) 2016-11-26T19:02:15 UTC - (JSON DATE) 2016-11-22T20:06:49 UTC = 94 hours
[CRITICAL] Delta hours (94) is >= (24) hours. Data is stale.

【问题讨论】:

  • 您能否展示一些您希望处理的更复杂的结构以及您在其中寻找哪些键?
  • 我添加了一些示例代码。我可以对其进行硬编码以使其看起来有 2 或 3 个“层”深,但目标是处理您向其抛出的任何输出。因此,它需要简单地遍历任何/所有键/子键,并将您定义的键与键/子键进行比较,并在匹配后获取数据。
  • 我主要使用 javascript,这种事情在 JS 中轻而易举:/
  • 所以我看到你添加了示例数据;您想在该数据中找到哪个键?到目前为止,我真的不明白这个问题。也许显示你会在 javascript 中做什么?在 perl 中做起来不会更难
  • 如果您想对自己的风格进行一些反馈,请随时在Code Review 上发布此代码(或完成的代码)。您可以改进很多东西,但它们超出了这里的答案范围。

标签: json perl hash iteration


【解决方案1】:

您可以尝试使用Hash::Flatten。例如:

use Hash::Flatten qw(flatten);

my $json_decoded = parse_json($json_str);
my $flat = flatten( $json_decoded );
say "found" if grep /(?:^|\.)\Q$key\E(?:\.?|$)/, keys %$flat;

【讨论】:

  • 我正在考虑使用该方法,但我通常不赞成将对象转换为字符串。如果可能,我更喜欢使用该对象并访问它们的属性。如果我必须这样做,我会这样做。感谢您的建议。
  • @Jedimaster0 Perl 哈希没有属性,除非您自己创建它们。如果您需要速度或担心内存使用情况,当然可以编写自己的 JSON 解析器。然后您可以将您喜欢的任何属性插入到哈希中。我还注意到JSON::XSJSON::PP 有过滤方法,可以在解析过程中调用回调。也许这是可以使用的东西?
  • @Jedimaster0 另一种选择是复制Hash::Flatten 的源代码并对其进行修改,使其不会创建所有不必要的密钥,而只会执行您想要的操作。
  • @Jedimaster0 JSON 格式用于复杂的数据结构,我认为期望语言本身的调用来遍历它是不正确的。但是有一些工具,比如这个似乎是为你的要求量身定做的。如果您出于某种原因想直接使用 JSON hashref,那么在 Perl 中完成它相当简单——而且也有许多模块。
  • @HåkonHægland,感谢您所做的一切,我能够达成解决方案。非常感谢。
【解决方案2】:

您可以使用Data::Visitor::Callback 来遍历数据结构。它允许您为结构内的不同类型的数据类型定义回调。由于我们只查看哈希,因此相对简单。

以下程序有一个预定义的要查找的键列表(在您的情况下,这些键是用户输入的)。我将您的示例 JSON 转换为 Perl hashref 并将其包含在代码中,因为转换不相关。程序访问此数据结构(包括顶层)中的每个 hashref 并运行回调。

Perl 中的回调是代码引用。这些可以通过两种方式创建。我们正在执行匿名子例程(有时在其他语言中称为lambda function)。回调传递了两个参数:访问者对象和当前数据子结构。

我们将迭代所有我们想要找到的键,并简单地检查它们是否exist 在当前数据结构中。如果我们看到一个,我们会在 %seen 哈希中计算它的存在。使用散列来存储我们看到的东西是 Perl 中的一个常见习语。

我们在这里使用后缀 if,它既方便又易于阅读。 %seen 是一个hash,所以我们使用$seen{$key} 访问$key 后面的值,而$data 是一个hash 引用,所以我们使用解引用运算符 -&gt; 使用 $data-&gt;{$key} 访问 $key 后面的值。

回调需要我们再次返回 $data 以便它继续。最后一行就在那里,并不重要。

我使用Data::Printer 来输出%seen 哈希,因为它很方便。如果需要,您也可以使用Data::Dumper。在生产中,您将不需要它。

use strict;
use warnings;
use Data::Printer;
use Data::Visitor::Callback;

my $from_json = {
    "localDate"  => "Wednesday 23rd November 2016 11:03:37 PM",
    "utcDate"    => "Wednesday 23rd November 2016 11:03:37 PM",
    "format"     => "l jS F Y h:i:s A",
    "returnType" => "json",
    "timestamp"  => 1479942217,
    "timezone"   => "UTC",
    "daylightSavingTime" =>
        0,    # this was false, I used 0 because that's a non-true value
    "url"    => "http:\/\/www.convert-unix-time.com?t=1479942217",
    "subkey" => {
        "altTimestamp" => 1479942217,
        "altSubkey"    => {
            "thirdTimestamp" => 1479942217
        }
    }
};

my @keys_to_find = qw(timestamp altTimestamp thirdTimestamp missingTimestamp);

my %seen;
my $visitor = Data::Visitor::Callback->new(
    hash => sub {
        my ( $visitor, $data ) = @_;

        foreach my $key (@keys_to_find) {
            $seen{$key}++ if exists $data->{$key};
        }

        return $data;
    },
);
$visitor->visit($from_json);

p %seen;

程序输出以下内容。请注意,这不是 Perl 数据结构。 Data::Printer 不是序列化程序,它是一种使数据以方便的方式可读的工具。

{
    altTimestamp     1,
    thirdTimestamp   1,
    timestamp        1
}

由于您还想限制输入,这里有一个示例如何做到这一点。以下程序是对上述程序的修改。它允许为每个必需的键提供一组不同的约束。

我已经通过使用a dispatch table 做到了这一点。本质上,这是一个包含代码引用的哈希。有点像我们用于访问者的回调。

我所包含的限制是对日期做一些事情。在 Perl 中使用日期的一种简单方法是核心模块 Time::Piece。这里有很多关于各种日期的问题,其中 Time::Piece 是答案。

我对每个键只做了一个约束,但您可以轻松地在这些代码引用中包含多个检查,或者制作一个代码引用列表并将它们放入数组引用 (keys =&gt; [ sub(), sub(), sub() ]) 中,然后再进行迭代。

在访问者回调中,我们现在还跟踪具有%passed 约束检查的键。我们使用$coderef-&gt;($arg) 调用coderef。如果约束检查返回 a true value,它会在哈希中注明。

use strict;
use warnings;
use Data::Printer;
use Data::Visitor::Callback;
use Time::Piece;
use Time::Seconds; # for ONE_DAY

my $from_json = { ... }; # same as above

# prepare one of the constraints
# where I'm from, Christmas eve is considered Christmas
my $christmas = Time::Piece->strptime('24 Dec 2016', '%d %b %Y');

# set up the constraints per required key
my %constraints = (
    timestamp        => sub {
        my ($epoch) = @_;
        # not older than one day

        return $epoch < time && $epoch > time - ONE_DAY;
    },
    altTimestamp     => sub {
        my ($epoch) = @_;
        # epoch value should be an even number

        return ! $epoch % 2;
    },
    thirdTimestamp   => sub {
        my ($epoch) = @_;
        # before Christmas 2016

        return $epoch < $christmas;
    },
);

my %seen;
my %passed;
my $visitor = Data::Visitor::Callback->new(
    hash => sub {
        my ( $visitor, $data ) = @_;

        foreach my $key (%constraints) {
            if ( exists $data->{$key} ) {
                $seen{$key}++;
                $passed{$key}++ if $constraints{$key}->( $data->{$key} );
            }
        }

        return $data;
    },
);
$visitor->visit($from_json);

p %passed;

这次的输出是:

{
    thirdTimestamp   1,
    timestamp        1
}

如果您想了解有关调度表的更多信息,请查看 Mark Jason Dominus 所著的 Higher Order Perl 一书的第二章,该书可合法免费获得 here

【讨论】:

  • 不错的答案! Data::Visitor 似乎是一个有用的模块。但它似乎使用了Moose,这对于这项任务来说可能太重了?
  • @H Moose 遭受这种可怕的启动惩罚的时代早已过去。这应该不再是一个大问题了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 1970-01-01
  • 2013-02-04
  • 2017-09-04
  • 1970-01-01
  • 2016-04-20
  • 1970-01-01
相关资源
最近更新 更多