【问题标题】:Process a file containing a collection of JSON strings处理包含 JSON 字符串集合的文件
【发布时间】:2020-08-03 15:08:48
【问题描述】:

(标题已编辑。原标题:“json 内容:按原样打印部分内容,通过管道部分获取人类可读的时间戳,从同一命令输出”)


我在一个文件中有一个类似 json 的内容:

{
  "newState": "runnable",
  "setAt": 1587421159359
}
{
  "newState": "running",
  "setAt": 1587421282891
}
{
  "newState": "debug_hold",
  "setAt": 1587422014895
}
{
  "newState": "terminating",
  "setAt": 1587424788577
}
{
  "newState": "failed",
  "setAt": 1587424796544
}

我可以通过cat timestamps.json | jq -r '.newState'提取'newState':

runnable
running
debug_hold
terminating
failed

我可以通过cat timestamps.json | jq -r '.setAt' | rev | cut -c 4- | rev | perl -pe 's/(\d+)/localtime($1)/e'提取纪元时间戳并将其格式化为人类可读的形式:

Mon Apr 20 18:19:19 2020
Mon Apr 20 18:21:22 2020
Mon Apr 20 18:33:34 2020
Mon Apr 20 19:19:48 2020
Mon Apr 20 19:19:56 2020

如何将两个输出组合起来,结果变成

runnable Mon Apr 20 18:19:19 2020
running Mon Apr 20 18:21:22 2020
debug_hold Mon Apr 20 18:33:34 2020
terminating Mon Apr 20 19:19:48 2020
failed Mon Apr 20 19:19:56 2020

我想我可以为循环和数组输入做一些 bash,但想知道 jq 是否有一些东西可以将部分内容(例如,在这种情况下的纪元时间)输出,处理它,然后将值反馈回jq 解析输出。

【问题讨论】:

  • cat 的无用使用。将cat timestamps.json | 替换为<timestamps.json

标签: json file perl pipe


【解决方案1】:

你可能看起来像这样。

cat timestamps.json | jq -r '[.newState, .setAt] | join(" ")'

【讨论】:

    【解决方案2】:

    输入是(不相关的)有效 JSON 字符串的集合,您可以在 {} 块中读取。

    input record separator ($/) 设置为},然后<> 运算符每次读取到}

    use warnings;
    use strict;
    use feature 'say';
    
    use JSON qw(decode_json);
    
    my $file = shift // die "Usage: $0 file\n";  #/
    
    open my $fh, '<', $file or die "Can't open $file: $!";
    
    local $/ = '}';  # presumably all this code is in some local scope
    
    while (my $record = <$fh>) { 
        next if not $record =~ /\S/; 
    
        my $json = decode_json($record); 
    
        say $json->{newState}, ' ', scalar localtime $json->{setAt}/1000; 
    }
    

    评论

    • 这依赖于输入的显示格式,特别是它没有嵌套对象。如果有嵌套的 {...},则 slurp 整个文件并使用 Text::Balanced 或等效方法提取 JSON 字符串(或者,当然,使用另一种方法)

    • 我实际上建议使用Cpanel::JSON::XS

    • 当需要更改像 $/ 这样的全局变量时,最好在所需的最小范围内使用 local 进行更改。在这里没关系,但我认为这是更大计划的一部分

    • 这样读取时可能会有空字符串,尤其是换行符,因此检查记录是否包含任何非空格

    • 您输入中的时间戳与自纪元以来的秒数相差数千倍,我猜是因为它们也带有毫秒。为简单起见,我只是除以 1000

    • 请注意,如果涉及夏令时,显示的所需时间戳可能会成为问题,如果是这种情况,您还想提取并包含时区


    从纪元获取时区的最简单(且灵活)的方法是使用POSIX::strftime。它从localtime 获取列表并返回根据给定格式生成的字符串。

    %z 说明符生成时区作为 UTC 偏移量,而 %Z 生成(臭名昭著且不可移植的)短名称。有关详细信息,请参阅系统的 strftime 联机帮助页。示例

     use POSIX qw(strftime);
     say strftime "%z %Z", localtime;  #--> -0700 PDT
    

    (感谢 ikegami 的回答促使我添加了时区讨论)

    【讨论】:

      【解决方案3】:

      使用 JSON 解析器的增量解析 功能,可以安全地解析 JSON 文档序列,例如您只需很少的代码即可。这意味着使用正则表达式匹配来破解 JSON 解析器是没有意义的。

      use Cpanel::JSON::XS qw( );
      
      my $decoder = Cpanel::JSON::XS->new();
      while (<>) {
         $decoder->incr_parse($_);
         while ( my $rec = $decoder->incr_parse() ) {
            say sprintf "%-11s %s",
               $rec->{newState},
               format_ts($rec->{setAt});
         }
      }
      

      完整的程序:

      #!/usr/bin/perl
      
      use strict;
      use warnings;
      use feature qw( say );
      
      use utf8;
      use open ':std', ':encoding(UTF-8)';
      
      use Cpanel::JSON::XS qw( );
      use POSIX            qw( strftime );
      
      sub format_ts {
         my ($ts) = @_;
         my $ms = $ts % 1000;
         my $epoch = ( $ts - $ms ) / 1000;
         my @lt = localtime($epoch);
         return sprintf("%s.%03d %s",
            strftime("%a %b %d %H:%M:%S", @lt),
            $ms,
            strftime("%Y %z", @lt),
         );
      }
      
      my $decoder = Cpanel::JSON::XS->new();
      while (<>) {
         $decoder->incr_parse($_);
         while ( my $rec = $decoder->incr_parse() ) {
            say sprintf "%-11s %s",
               $rec->{newState},
               format_ts($rec->{setAt});
         }
      }
      

      输出:

      runnable    Mon Apr 20 18:19:19.359 2020 -0400
      running     Mon Apr 20 18:21:22.891 2020 -0400
      debug_hold  Mon Apr 20 18:33:34.895 2020 -0400
      terminating Mon Apr 20 19:19:48.577 2020 -0400
      failed      Mon Apr 20 19:19:56.544 2020 -0400
      

      请注意,我添加了时区信息,因为如果没有它,时间戳会变得不明确(因为从夏令时切换到标准时间时会出现重叠)。如果您愿意,我还展示了如何保持毫秒。

      【讨论】:

        【解决方案4】:

        一个小的perl脚本可以轻松处理这些数据

        用法:script_name.pl timestamps.json

        #!/usr/bin/perl
        
        use strict;
        use warnings;
        
        my($state,$time);
        
        while(<>) {
            chomp;
            $state = $1 if /"newState": "(.*)"/;
            $time  = $1 if /"setAt": (\d+)/;
            printf "%-12s %s\n", $state, "".localtime($time/1000) if /}/;
        }
        

        替代版本

        use strict;
        use warnings;
        
        my $data = do { local $/; <> };
        my %state = $data =~ /"newState": "(.*?)".*?"setAt": (\d+)/sg;
        
        while(my($s,$t) = each %state) {
            printf "%-12s %s\n", $s, "".localtime($t/1000);
        }
        

        输入文件timestamps.json

        {
          "newState": "runnable",
          "setAt": 1587421159359
        }
        {
          "newState": "running",
          "setAt": 1587421282891
        }
        {
          "newState": "debug_hold",
          "setAt": 1587422014895
        }
        {
          "newState": "terminating",
          "setAt": 1587424788577
        }
        {
          "newState": "failed",
          "setAt": 1587424796544
        }
        

        输出

        runnable     Mon Apr 20 15:19:19 2020
        running      Mon Apr 20 15:21:22 2020
        debug_hold   Mon Apr 20 15:33:34 2020
        terminating  Mon Apr 20 16:19:48 2020
        failed       Mon Apr 20 16:19:56 2020
        

        【讨论】:

        • 对不起,我意识到我在原始 json 内容中犯了一个错误。时间戳应该是纪元时间。我已经更正了。
        • @MrJanitor -- 代码已更新以使用更正的输入数据
        猜你喜欢
        • 2011-02-19
        • 2022-11-12
        • 2016-02-25
        • 2016-02-27
        • 1970-01-01
        • 2019-04-03
        • 2012-06-03
        • 2016-01-24
        • 2021-04-03
        相关资源
        最近更新 更多