【问题标题】:Remove duplicate keys from a hash of hashes and arrays (and ensure any resulting empty hashs are also removed)从散列和数组的散列中删除重复的键(并确保任何生成的空散列也被删除)
【发布时间】:2019-02-24 04:34:11
【问题描述】:

我有一个 JSON 格式的数据流,我的脚本可以从内部网站访问该数据流。我的脚本使用 JSON.pm 将 JSON 转换为 perl 哈希(我在 RHEL 6.9 上使用 perl 5.10.1)

在这个散列中有多个嵌套散列和嵌套数组,其中一些嵌套在大散列内的其他散列/数组中。

我需要遍历散列的整个结构,包括所有数组和嵌套散列,并删除整个结构中任何位置的任何键,这些键与任何其他键具有相同的名称(尽管仅适用于特定键名)。

此外,由于数据的结构,一些嵌套散列只有现在被删除的键,一些键的值作为空散列。我还需要删除那些其值为空哈希的键

这是我转换为 perl 后的数据:

$VAR1 = {
          'cat' => 'meow',
          'dog' => [
                     {
                        'a' => {
                                  'husky' => {
                                                'name' => 'fred'
                                             },
                                  'chow' => {
                                               'name' => 'barney'
                                            }
                               },
                     },
                     {
                        'b' => {
                                  'husky' => 'wilma',
                                  'lab' => 'betty'
                               },
                        'c' => 'pebbles' # yes this is intentionally a scalar in the example
                     },
                     {
                        'd' => {
                                  'shihtzu' => 'bambam'
                               },
                     },
                     {
                        'e' => {
                                  'husky' => 'dino'
                               },
                     },
                   ],
        }

我们要删除所有名为“husky”的键

它应该是这样的:

$VAR1 = {
          'cat' => 'meow',
          'dog' => [
                     {
                        'a' => {
                                  'chow' => {
                                               'name' => 'barney'
                                            }
                               },
                     },
                     {
                        'b' => {
                                  'labrador' => 'betty'
                               },
                        'c' => 'pebbles'
                     },
                     {
                        'd' => {
                                  'shihtzu' => 'bambam'
                               },
                     },
                   ],
        }

这是我添加@Shawn 的代码并对其进行调整后得到的(这非常接近,但我们需要考虑空哈希:

$VAR1 = {
          'cat' => 'meow',
          'dog' => [
                     {
                        'a' => {
                                  'chow' => {
                                               'name' => 'barney'
                                            }
                               },
                     },
                     {
                        'b' => {
                                  'lab' => 'betty'
                               },
                        'c' => 'pebbles' # yes this is intentionally a scalar in the example
                     },
                     {
                        'd' => {
                                  'shihtzu' => 'bambam'
                               },
                     },
                     {
                        'e' => {},
                     },
                   ]
        }

我尝试了一些在 SO 和 perlmonks 上其他地方发现的变体。 keys %$_ == 0!%$_ 仅举几例。但似乎没有一个适用于这个哈希片。

代码:

use 5.008008;
use strict;
use warnings;
use English; # I know I know, don't use English...
use JSON;
use YAML::Tiny qw(Dump);
# proprietary modules I wrote added here, which themselves load in LWP, HTTP::Cookie and others, and they do the bulk of building and sending the request. They are the back end to this script's front end.

[-snipped a ton of code-]

sub _count_keys
{
    my ($j, $seen) = @ARG;
    my $type = ref $j;
    if ($type eq "ARRAY")
    {
        for (@{$j})
        {
            _count_keys($ARG, $seen);
        }
    }
    elsif ($type eq "HASH")
    {
        while (my ($key, $val) = each %{$j})
        {
            $seen->{$key}++;
            if (ref $val)
            {
                _count_keys($val, $seen);
            }
        }
    }
    return $seen;
}

sub _remove_duplicate_keys
{
    my ($j, $seen) = @ARG;
    $seen //= _count_keys($j, {});

    my $type = ref $j;
    if ($type eq "ARRAY")
    {
        return [ map { _remove_duplicate_keys($ARG, $seen) } @{$j} ];
    }
    elsif ($type eq "HASH")
    {
        my %obj = %{$j};
        delete @obj{grep { $seen->{$ARG} > 1 and $ARG eq 'keyNameToBeExcluded'} keys %obj};
# Here is where I have been putting another delete line but I can't seem to find the right parameters for the grep to make it delete the empty anon hashes. Example of what I tried is the next comment below
#        delete @obj{grep { $seen->{$ARG} > 1 and keys $ARG{assetDetails} == 0 } keys %obj};

        while (my ($key, $val) = each %obj)
        {
            if (ref $val)
            {
                $obj{$key} = _remove_duplicate_keys($val, $seen);
            }
        }
        return \%obj;
    }
    else
    {
        return $j;
    }
}

sub _process_json
{
    my $JSONOUTPUT   = shift;
    my $OPTIONS      = shift;

    # Change true to 1 and false to 0 to prevent blessed objects from appearing in the JSON, which prevents the YAML::Tiny module from barfing
    foreach (@{$JSONOUTPUT})
    {
        s{true(,\n)}{1$1}gxms;
        s{false(,\n)}{0$1}gxms;
    }

    my $JSONPERLOBJ  = JSON->new->utf8->decode(@{$JSONOUTPUT});

# Test code below here; real code not in use while I test getting the output right.
use Data::Dumper;
my $BEFORE = $JSONPERLOBJ;
my $AFTER = _remove_duplicate_keys($JSONPERLOBJ);
#    $JSONPERLOBJ = _remove_duplicate_keys($JSONPERLOBJ);
#print Dumper $BEFORE;
print Dumper $AFTER;
exit 1;
# End test code
}
sub _main
{
    [-snip private code-]
    my @JSONOUTPUT = $RESPONSE->decoded_content;
    my $RC = _process_json(\@JSONOUTPUT, $OPTIONS);

    exit ($RC == 1)?0:1;
}

【问题讨论】:

  • [ ... ] 创建一个数组。请use Data::Dumperprint Dumper \%obj 让我们看到您实际拥有的东西。此外,在 Perl 中,“对象”是一个被祝福到一个类中的数据项。您在这里拥有的只是一个数据结构。
  • 这是(表示)JSON 返回的内容吗?那将是一个 hashref,my $hr = {...}。您正在显示一个 arrayref [...],分配给一个哈希 (%) 变量。
  • 编辑了 OP。没有理由对一个简单的错字投反对票。我不能给你一个转储,因为它包含数千行信息,其中大部分是敏感的。以我给出的例子为例,因为这就是我所能提供的。 @zdim 是的,它代表 JSON 数据。我手动输入了示例并使用了错误的符号。我的错。我们现在可以专注于实际问题吗?
  • 感谢您的更新,但是您显示的数据结构是不可能的,如果您尝试编译它,您会看到Odd number of elements in anonymous hash 错误。请转储您的真实数据,否则我们无法帮助您,
  • OP 当然可以使用改进,但不难理解他想要做什么,因为他有 JSON,他想删除在 anywhere 中找到的具有重复键的条目JSON 数据。

标签: json perl hash-of-hashes


【解决方案1】:

我认为这是你想要的:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw/say/;
use JSON::XS; # Better than JSON; also see JSON::MaybeXS

my $j = <<EOJSON;
{
  "foo": 1,
  "bar": {
      "foo": true,
      "baz": false
      },
  "dog": "woof",
  "cat": [ { "foo": 3 } ]
}
EOJSON

sub count_keys {
  my ($j, $seen) = @_;
  my $type = ref $j;
  if ($type eq "ARRAY") {
    count_keys($_, $seen) for @$j;
  } elsif ($type eq "HASH") {
    while (my ($key, $val) = each %$j) {
      $seen->{$key}++;
      count_keys($val, $seen) if ref $val;
    }
  }
  return $seen;
}

sub remove_dups {
  my ($j, $seen) = @_;
  $seen //= count_keys($j, {});

  my $type = ref $j;
  if ($type eq "ARRAY") {
    return [ map { remove_dups($_, $seen) } @$j ];
  } elsif ($type eq "HASH") {
    my %obj = %$j;
    delete @obj{grep { $seen->{$_} > 1 } keys %obj};
    while (my ($key, $val) = each %obj) {
      $obj{$key} = remove_dups($val, $seen) if ref $val;
    }
    return \%obj;
  } else {
    return $j;
  }
}

my $parsed = decode_json $j;
my $printer = JSON::XS->new->pretty->canonical;
say "Before:";
print $printer->encode($parsed);
say "After:";
my $dedup = remove_dups $parsed;
print $printer->encode($dedup);

生产

Before:
{
   "bar" : {
      "baz" : false,
      "foo" : true
   },
   "cat" : [
      {
         "foo" : 3
      }
   ],
   "dog" : "woof",
   "foo" : 1
}
After:
{
   "bar" : {
      "baz" : false
   },
   "cat" : [
      {}
   ],
   "dog" : "woof"
}

编辑解释:

remove_dups 第一次在表示 json 值(不一定是 json 对象)的 perl 数据结构上调用,它调用 count_keys 递归遍历该结构并创建所有键和每个键出现的次数。然后它再次递归遍历结构,返回一个没有在原始中多次出现的键的深层副本。

这条线是真正的魔法:

delete @obj{grep { $seen->{$_} > 1 } keys %obj};

它使用 hash slice 一次性删除一堆键,grep 位返回一个多次出现的键列表。 More information 切片。

【讨论】:

  • 我会做类似的事情,对 JSON 进行两次迭代:1) 使用键/出现次数构建散列 2) 创建散列,或从源中删除,保留只出现一次的条目。跨度>
  • 实现具有深度或广度下降,采用函数 ref arg 调用哈希条目,传递构造键/发生哈希的 func ref,再次迭代,传递条目删除 func --- presto,O (n)。
  • 谢谢@Shawn,如果遇到问题我会尝试更新。
  • 最好使用Scalar::Util::reftype而不是ref,因为如果引用的是祝福数据,后者将返回包名。
  • @Speeddymon 我接受这个(Shawn 的)回答已经完成了这项工作(已经 +1 编辑了它),它被很好地接受了——我只为空结构的后续问题发布。
【解决方案2】:

我认为Shawn's answer 适用于删除重复项,它看起来很适合。

后续问题是我们最终可能会得到空结构,并且这些结构也需要被删除。但也可能存在只包含空结构等的结构,我认为所有这些都不需要。

我使用问题中的desired-result-hashref(我从中删除了一个name=&gt;...,以便没有重复)并添加一些空的麻烦。

use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd pp);

my $hr = {
    'cat' => 'meow',
    'dog' => [
        { 'a' => { 'chow' =>  { 'name' => 'barney' } }  },
        { 'b' => { 'lab' => 'betty' }, 'c' => 'pebbles' },
        { 'd' => { 'shihtzu' => 'bambam' }              },
        {   # all of the following need to go, and this hashref
            'e' => { },  
            'f' => { noval => { } },
            'g' => [ { }, { nada => { } }, [ ] ],
        },
    ],  
};
dd $hr; say '';

for my $k (sort keys %$hr) {
    next_level($hr, $k, $hr->{$k}, 'key');
}

# Takes: data structure (reference), key/index at which it is found, 
# its value for it, and description string of which it is, 'key|idx'
sub next_level {
    my ($ds, $index, $val, $kind) = @_;
    my $type = ref $val;

    if ($type eq 'ARRAY') {
        for my $i (0..$#$val) {
            next_level(
                ( $kind eq 'key' ? $ds->{$index} : $ds->[$index] ),
                $i, $val->[$i], 'idx' 
            );
        }

        # Collect indices for and delete elements that are empty
        my @to_delete;
        for my $i (0..$#$val) {
            if ( (ref $val->[$i] eq 'HASH'  and not keys %{$val->[$i]}) or
                 (ref $val->[$i] eq 'ARRAY' and not      @{$val->[$i]})  )
            {
                say "No value/empty for index $i, record for deletion";
                push @to_delete, $i;
            }
        }
        if (@to_delete) { 
            my %ref_idx = map { $_ => 1 } @to_delete;
            @$val = @$val[ grep { not exists $ref_idx{$_} } 0..$#$val ];
        }
    }
    elsif ($type eq 'HASH') {
        for my $k (sort keys %{$val}) {
            my $ds_next_level = 
                ($kind eq 'key') ? $ds->{$index} : $ds->[$index];

            next_level( $ds_next_level, $k, $val->{$k}, 'key' );

            # Delete if empty 
            if ( (ref $val->{$k} eq 'HASH'  and not keys %{$val->{$k}}) or
                 (ref $val->{$k} eq 'ARRAY' and not      @{$val->{$k}})  )
            {
                say "No value/empty for key $k, delete";
                delete $ds_next_level->{$k}
            }
        }
    }
    #elsif (not $type) { say "A value: ", $val }
}
say ''; dd $hr;

这是对复杂数据结构的正常递归遍历,但有一个转折:为了能够删除组件,递归子还需要数据结构本身,其中键(在 hashref 中)或索引(在arrayref) 找到它,以及它是两者中的哪一个,键或索引。

在递归之后,如果目标为空,如果它在 hashref 中,则删除它。首先扫描 arrayref 中的所有空元素,然后通过覆盖 arrayref 将它们删除,其中数组切片不包括仅包含空数据结构的元素的索引。

为了排除“坏”索引,使用参考哈希来提高效率。使用map(请参阅this post)覆盖数组可能更快,或者如果切片允许特定(解释器)优化,则可能不会。

输出

{ 猫=>“喵”, 狗 => [ { a => { chow => { name => "barney" } } }, { b => { 实验室 => "贝蒂" }, c => "鹅卵石" }, { d => { shihtzu => "bambam" } }, { e => {}, f => { noval => {} }, g => [{}, { nada => {} }, []] }, ], } 键 e 无值/为空,删除 键 noval 无值/空,删除 键 f 没有值/为空,删除 键 nada 无值/空,删除 索引 0 无值/空,删除记录 索引 1 无值/空,删除记录 索引 2 无值/空,删除记录 键 g 无值/空,删除 索引 3 无值/空,删除记录 { 猫=>“喵”, 狗 => [ { a => { chow => { name => "barney" } } }, { b => { 实验室 => "贝蒂" }, c => "鹅卵石" }, { d => { shihtzu => "bambam" } }, ], }

【讨论】:

  • @Speeddymon 更新了,因为它让我失望了,在递归之前没有充分的理由首先检查和删除空的东西。空数据结构的递归很快,因此所有空的都可以在它之后删除。这减轻了代码的负担。
  • 谢谢@zdim。一个问题...... Shawn 的代码遍历了一次哈希,这似乎是与他的代码一起添加的附加函数。我是否正确地解释了哈希将被他的代码遍历一次,然后被你的代码遍历一次?还是我只是那么密集而遗漏了一些非常明显的东西?
  • @Speeddymon 是的,这是正确的:这将遍历 hashref,因为它在 Shawn 的代码完成它的事情之后仍然存在(它遍历它 两次 - 一次舀出骗子然后删除它们)。这也可以用来查找和删除欺骗,但它会大大扩展它,而你有。或者,您可以将其合并到肖恩的第二次传球中?我认为拥有两个潜艇对于这项工作来说并不算太糟糕——数据有多大?如果时间有问题,那就另当别论了。原则上为此设置单独的潜艇也是合理的。
  • 好吧,你说得对,这将是第三次遍历。数据集的大小各不相同。我在有限测试中得到的最大输出不到 1mb,但理论上它可以发送更多,因为 webapp 的输出取决于传递给脚本的选项,以及 webapp 与 oracle db 的接口。我不介意遍历哈希一千次,只要它不添加 noticeable 延迟。当然,延迟有多明显,取决于 webapp 的输出。
  • 我想到的可以在较大数据集的情况下帮助加快速度的事情是实现线程,但我不知道 perl 5.10 是否有线程的概念。跨度>
猜你喜欢
  • 2014-01-16
  • 2015-01-28
  • 2019-11-05
  • 2014-08-27
  • 2017-05-01
  • 2023-03-24
  • 2013-06-17
  • 2011-02-03
  • 2023-03-22
相关资源
最近更新 更多