如何基于另一个哈希表的键/值删除一个[子]哈希表?

4
假设我有两个哈希表。其中一个包含一组数据,只需要保留出现在另一个哈希表中的内容。
例如:
my %hash1 = ( 
        test1 => { inner1 => { more => "alpha", evenmore => "beta" } },
        test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
        test3 => { inner9999 => { ohlookmore => "golf", somethingelse => "foxtrot" } }
    );

my %hash2 = (
        major=> { test2 => "inner2",
              test3 => "inner3" }  );

我希望做的是,如果hash1中的子哈希在hash2{major}中不存在作为键/值,则删除整个子哈希,最好不使用模块。 "innerX"中包含的信息无关紧要,只需将其保留(除非要删除子哈希,然后它可以消失)。
在执行此操作后,如上面的示例所示,hash1将如下所示:
my %hash1 = ( 
        test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
        );

它删除了hash1{test1}和hash1{test3},因为它们在hash2中没有匹配项。

这是我目前尝试过的方法,但它不起作用。也许这不是最安全的做法,因为我正在循环遍历哈希表,同时试图从中删除。但是,我在每个地方都进行了删除,这应该没问题吧?

这是我的尝试,然而perl会报错:

在使用“strict refs”时,不能将字符串(“inner1”)用作HASH ref

while(my ($test, $inner) = each %hash1)
{
    if(exists $hash2{major}{$test}{$inner})
    {
        print "$test($inner) is in exists.\n";
    }
    else
    {
        print "Looks like $test($inner) does not exist, REMOVING.\n";
       #not to sure if $inner is needed to remove the whole entry
         delete ($hash1{$test}{$inner});
    } 
}
4个回答

5
你接近了答案。请记住,$hash2{major}{$test}是一个标量而不是哈希引用。
#! /usr/bin/perl

use strict;
use warnings;

my %hash1 = ( 
  test1 => { inner1 => { more => "alpha", evenmore => "beta" } },
  test2 => { inner2 => { more => "charlie", somethingelse => "delta" } },
  test3 => { inner9999 => { ohlookmore => "golf", somethingelse => "foxtrot" } }
);

my %hash2 = (
  major => { test2 => "inner2",
             test3 => "inner3" }
);

foreach my $k (keys %hash1) {
  my $delete = 1;
  foreach my $inner (keys %{ $hash1{$k} }) {
    $delete = 0, last if exists $hash2{major}{$k} &&
                                $hash2{major}{$k} eq $inner;
  }
  delete $hash1{$k} if $delete;
}

use Data::Dumper;
$Data::Dumper::Indent = 1;
print Dumper \%hash1;

$delete = 0, ...开头的代码有点可爱。它相当于在另一个条件语句中使用$delete = 0; last;,但已经嵌套了两次。不想建立一个套娃,我使用了语句修饰符,但是正如其名称所示,它只能修改单个语句。

这就是Perl逗号运算符的用处:

二元,是逗号运算符。在标量上下文中,它评估左参数,丢弃该值,然后评估右参数并返回该值。这与C语言的逗号运算符一样。

在本例中,左参数是表达式$delete = 0,右参数是last

这个条件语句可能看起来有点繁琐,但是

... if $hash2{major}{$k} eq $inner;

当探测%hash2中未提及的测试(如test1/inner1)时,会产生未定义值警告。使用以下方式可以避免此问题:

.. if $hash2{major}{$k} && $hash2{major}{$k} eq $inner;

如果%hash2的“内部名称”是一个假值,例如字符串"0",那么would incorrectly delete a test mentioned in将错误地删除在其中提到的测试。是的,在这里使用exists可能过于麻烦,但由于不知道您的实际哈希键,我选择了保守路线。

输出:

$VAR1 = {
  'test2' => {
    'inner2' => {
      'somethingelse' => 'delta',
      'more' => 'charlie'
    }
  }
};

虽然您没有违反它,但请注意使用each时与以下警告相关的注意事项:

If you add or delete elements of a hash while you're iterating over it, you may get entries skipped or duplicated, so don't. Exception: It is always safe to delete the item most recently returned by each, which means that the following code will work:

    while (($key, $value) = each %hash) {
      print $key, "\n";
      delete $hash{$key};   # This is safe
    }

更新:将哈希值视为数组进行搜索(可以说“线性搜索而不是对数搜索”来给计算机科学迷朋友留下印象)是一个红旗,上面的代码就是这样做的。更好的方法类似于Penfold的答案,即

%hash1 = map +($_ => $hash1{$_}),
         grep exists $hash2{major}{$_} &&
              exists $hash1{$_}{ $hash2{major}{$_} },
         keys %hash1;

以简洁的陈述方式,它描述了 %hash1 所需内容,即:
  1. %hash1 的一级键应在 $hash2{major} 中提到,并且
  2. $hash2{major} 中与每个一级键对应的值本身应是该键在 %hash1 中的子键。
(哇,令人眼花缭乱。我们需要多个占位符变量来表示!) +($_ => $hash1{$_}) 中的一元加号可消除解析器的歧义,以便它知道我们希望将表达式视为“对”。请参见 perlfunc 关于 map 的文档结尾,了解其他可能需要这样做的情况。

有可能解释一下这行代码吗: $delete = 0, last if exists $hash2{major}{$k} && $hash2{major}{$k} eq $inner;我有点理解,但是逗号和last的使用让我感到困惑。 - Zack
@Zack 感谢你的勾选!在更新的答案中提供了解释,还有额外的奖励。 - Greg Bacon

4

您可以将其作为一行代码完成,这是因为delete()将接受一个键数组。这并不像我最初想的那样容易,但现在我已经仔细阅读了问题...

delete @hash1{ 
        grep(
            !(
              exists($hash2{major}->{$_}) 
                && 
              exists( $hash1{$_}->{ $hash2{major}->{$_} } )
            ),
            keys %hash1
        )
    };

1
这是我会做的方式:(第三次尝试才是成功)
foreach ( map { [ $_ => $hash2{major}{$_} ] } keys %hash1 ) { 
    my ( $key, $value ) = @$_;
    if ( defined $value and my $new_value = $hash1{$key}{$value} ) { 
        $hash1{$key} = $new_value;
    }
    else { 
        delete $hash1{$key};
    }
}

1
# This is the actual hash we want to iterate over.
my $keepers = $hash2{major};

%hash1 = map { $_ => $hash1{$_} }  # existing key and hash contents in %hash1
             grep { exists $keepers->{$_} and               # key there?
                    exists $hash1{$_}->{ $keepers->{$_} } } # key in hash there?
             (keys %hash1);        # All the keys we might care about

这个方法的原理是我们将想要/不想要的列表分为三个独立的阶段:

  1. keys调用一步获取hash1中所有键。
  2. grep生成(作为一步)符合我们条件的键列表。
  3. map生成(作为一步)一组键和值,这些键和值是我们想要的。

这样我们就不会在准备好之前改变主要的哈希表。如果%hash1包含许多键,我们将使用大量内存。如果您担心这个问题,可以采取以下措施:

# Initialization as before ...

use File::Temp qw(tempfile);

my ($fh, $file) = tempfile();
my $keepers = $hash2{major};

print $fh "$_\n" for (keys %hash1);
close $fh;
open $fh, "<", $file or die "can't reopen tempfile $file: $!\n";
while ( defined ($_ = <$fh>) ) {
  chomp;
  delete $hash1{$_} 
    unless exists $keepers->{$_} and 
           exists $hash1{$_}->{ $keepers->{$_} }; 
}

这个可以工作,因为我们不是在哈希表上进行迭代,而是在其键的存储副本上进行迭代。


1
为什么要写入文件,当你可以直接使用my @keys = keys %hash1;呢? - Penfold
这会使内存占用量加倍,因为你刚刚复制了所有键的副本。 - Joe McMahon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接