如何在Perl中遍历哈希(嵌套哈希)?

19

我有一个哈希表,其中键的值是其他哈希表。

例如:{'key' => {'key2' => {'key3' => 'value'}}}

如何遍历这个结构?


另外,请阅读perldoc perldsc。您可以深入了解哈希表。 - ghostdog74
1
你能给一个更加实际的例子吗?在哪里会遇到这样的结构?它有什么用途?你想要做什么? 也许另一种数据结构更适合这个任务? - Aurril
@Aurril:嵌套哈希结构对于许多事情都很有用,可以查看我下面帖子中的链接以获取示例。 - Zaid
每个哈希表是否有多个键? - Svante
1
我相信其中一个 Perl XML 解析器可以将 XML 文件解析为嵌套的哈希表结构。 - Kevin Kibler
8个回答

26

这个答案基于 Dave Hinton 的想法,即编写一个通用的子程序来遍历哈希结构。这样的哈希遍历器接受一个代码引用,并简单地为哈希中的每个叶子节点调用该代码。

采用这种方法,同一个哈希遍历器可以用于执行许多不同的操作,具体取决于我们给它的回调函数。为了实现更大的灵活性,您需要传递两个回调——一个用于在值是哈希引用时调用,另一个用于在它是普通标量值时调用。类似这样的策略在 Marc Jason Dominus 的优秀书籍《Higher Order Perl》中深入探讨。

use strict;
use warnings;

sub hash_walk {
    my ($hash, $key_list, $callback) = @_;
    while (my ($k, $v) = each %$hash) {
        # Keep track of the hierarchy of keys, in case
        # our callback needs it.
        push @$key_list, $k;

        if (ref($v) eq 'HASH') {
            # Recurse.
            hash_walk($v, $key_list, $callback);
        }
        else {
            # Otherwise, invoke our callback, passing it
            # the current key and value, along with the
            # full parentage of that key.
            $callback->($k, $v, $key_list);
        }

        pop @$key_list;
    }
}

my %data = (
    a => {
        ab => 1,
        ac => 2,
        ad => {
            ada => 3,
            adb => 4,
            adc => {
                adca => 5,
                adcb => 6,
            },
        },
    },
    b => 7,
    c => {
        ca => 8,
        cb => {
            cba => 9,
            cbb => 10,
        },
    },
);

sub print_keys_and_value {
    my ($k, $v, $key_list) = @_;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $v, "@$key_list";
}

hash_walk(\%data, [], \&print_keys_and_value);

2
我必须在12年后评论这个问题,说声谢谢! - Vinnix

12

这符合你的需求吗?(未经测试)

sub for_hash {
    my ($hash, $fn) = @_;
    while (my ($key, $value) = each %$hash) {
        if ('HASH' eq ref $value) {
            for_hash $value, $fn;
        }
        else {
            $fn->($value);
        }
    }
}

my $example = {'key' => {'key2' => {'key3' => 'value'}}};
for_hash $example, sub {
    my ($value) = @_;
    # Do something with $value...
};

9

这篇文章可能会有所帮助。

foreach my $key (keys %hash) {
    foreach my $key2 (keys %{ $hash{$key} }) {
        foreach my $key3 (keys %{ $hash{$key}{$key2} }) {
            $value = $hash{$key}{$key2}->{$key3};
            # .
            # .
            # Do something with $value
            # .
            # .
            # .
        }
    }
}

在 OP 中,数据结构的第一个括号是花括号,这表明它是哈希引用。my $hash = {'key' => {'key2' => {'key3' => 'value'}}} 因此,您需要取消引用。 - ccheneson
1
如果子哈希的数量是固定的,那么这个解决方案才有效。如果哈希结构是自动生成的,则需要更通用的方法。递归算法可能是更好的解决方案。我不熟悉Perl,否则我会给出一个例子。 - Aurril
@ccheneson:不需要解引用。它就是它。 - Zaid

7
之前的答案展示了如何自己编写解决方案,至少这样做一次可以让你了解perl引用和数据结构的内部工作原理。如果你还没有阅读过perldoc perldscperldoc perlref,你应该一定要看一下。
然而,你不需要自己编写解决方案——CPAN上已经有一个模块可以帮助你遍历任意复杂的数据结构:Data::Visitor

+1 谢谢,Data::Visitor 看起来很有用。从文档中并不立即清楚如何做一些简单的事情--例如,遍历嵌套的哈希结构,打印叶子值及其键(直接和其祖先)。我相信这是可行的;只需要花点时间理解一下。 :) - FMc

2

这并不是一个全新的答案,但我想分享如何递归地打印所有哈希值,并在需要时修改它们。

这是对dave4420答案的微小修改,在其中将值作为引用传递给回调函数,以便我的回调程序可以修改哈希中的每个值。

我还必须重建哈希表,因为while each循环创建的是副本而不是引用。

sub hash_walk {
   my $self = shift;
    my ($hash, $key_list, $callback) = @_;
    while (my ($k, $v) = each %$hash) {
        # Keep track of the hierarchy of keys, in case
        # our callback needs it.
        push @$key_list, $k;

        if (ref($v) eq 'HASH') {
            # Recurse.
            $self->hash_walk($v, $key_list, $callback);
        }
        else {
            # Otherwise, invoke our callback, passing it
            # the current key and value, along with the
            # full parentage of that key.
            $callback->($k, \$v, $key_list);
        }

        pop @$key_list;
        # Replace old hash values with the new ones
        $hash->{$k} = $v;
    }
}

hash_walk(\%prj, [], \&replace_all_val_strings);

sub replace_all_val_strings {
    my ($k, $v, $key_list) = @_;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $$v, "@$key_list";
    $$v =~ s/oldstr/newstr/;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $$v, "@$key_list";
}

1
如果您将Perl用作“CPAN解释器”,除了Data::VisitorData::Deep之外,还有超级简单的Data::Traverse
use Data::Traverse qw(traverse);
 
my %test_hash = (
  q => [qw/1 2 3 4/],
  w => [qw/4 6 5 7/],
  e => ["8"],
  r => { 
         r => "9"  ,
         t => "10" ,
         y => "11" ,
      } ,
);

traverse { return if /ARRAY/; print "$a => $b\n" if /HASH/ && $b > 8 } \%test_hash;

Output:

t => 10
y => 11

$a$b 在这里被视为特殊变量(与 sort() 一样),在 traverse() 函数内部也是如此。 Data::Traverse 是一个非常简单但极其有用的模块,没有非核心依赖项。


0

你需要循环两次。

while ( ($family, $roles) = each %HoH ) {
   print "$family: ";
   while ( ($role, $person) = each %$roles ) {
      print "$role=$person ";
   }
print "\n";
}

0
foreach my $keyname (keys(%foo) {
  my $subhash = $foo{$keyname};
  # stuff with $subhash as the value at $keyname
}

1
应该是 $foo{$keyname} 而不是 %foo{$keyname}! - Leon Timmermans
那是应该的。这就是我在喝咖啡之前发布帖子的后果。 - monksp

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