【问题标题】:Identifying elements in one array of hashes that are not in another array of hashes (perl)识别一个哈希数组中不在另一个哈希数组中的元素(perl)
【发布时间】:2019-07-30 23:01:41
【问题描述】:

我是一名新手 perl 程序员,试图识别哪些元素在一个哈希数组中,但不在另一个哈希数组中。我正在尝试搜索“新”数组,识别“旧”数组中不存在的 id、标题和创建的元素。

我相信我可以使用一组基本的 for() 循环,但我想更有效地做到这一点。这只是在尝试使用 grep() 并失败后才出现的。

这些数组是从数据库中构建的:

use DBI;
use strict;
use Data::Dumper;
use Array::Utils qw(:all);
sub db_connect_new();
sub db_disconnect_new($);
sub db_connect_old();
sub db_disconnect_old($);

my $dbh_old   = db_connect_old();
my $dbh_new   = db_connect_new();

# get complete list of articles on each host first (Joomla! system)
my $sql_old   = "select id,title,created from mos_content;"; 
my $sql_new   = "select id,title,created from xugc_content;";

my $sth_old   = $dbh_old->prepare($sql_old);
my $sth_new   = $dbh_new->prepare($sql_new);

$sth_old->execute();
$sth_new->execute();

my $ref_old;
my $ref_new;

while ($ref_old = $sth_old->fetchrow_hashref()) {
  push @rv_old, $ref_old;
}

while ($ref_new = $sth_new->fetchrow_hashref()) {
  push @rv_new, $ref_new;
}

my @seen = ();
my @notseen = ();
foreach my $i (@rv_old) {
   my $id = $i->{id};
   my $title = $i->{title};
   my $created = $i->{created};
   my $seen = 0;
   foreach my $j (@rv_new) {
      if ($i->{id} == $j->{id}) {
         push @seen, $i;
         $seen = 1;
      }
   }
   if ($seen == 0) {
       print "$i->{id},$i->{title},$i->{state},$i->{catid},$i->{created}\n";
      push @notseen, $i;
   }
}

使用 Dumper(@rv_old) 打印数组时,数组如下所示:

$VAR1 = {
          'title' => 'Legal Notice',
          'created' => '2004-10-07 00:17:45',
          'id' => 14
        };
$VAR2 = {
          'created' => '2004-11-15 16:04:06',
          'id' => 86096,
          'title' => 'IRC'
        };
$VAR3 = {
          'id' => 16,
          'created' => '2004-10-07 16:15:29',
          'title' => 'About'
        };

我尝试通过数组引用来使用 grep(),但我认为我对数组、哈希和引用的理解不够好,无法正确执行。我失败的 grep() 尝试如下。对于如何正确执行此操作的任何想法,我将不胜感激。

我认为问题在于我不知道如何引用第二个哈希数组中的 id 字段。我见过的大多数使用 grep() 的示例只是查看整个数组,就像使用常规 grep(1) 一样。我需要遍历一个数组,检查 id 字段中的每个值与另一个数组中的 id 字段。

  my $rv_old_ref        = \@rv_old;
  my $rv_new_ref        = \@rv_new;

  for my $i ( 0 .. $#rv_old) {
    my $match = grep { $rv_new_ref->$_ == $rv_old_ref->$_ } @rv_new;
    push @notseen, $match if !$match;
  }

我还尝试了上面 grep() 的变体:

1) if (($p) = grep ($hash_ref->{id}, @rv_old)) {
2) if ($hash_ref->{id} ~~ @rv_old) {

【问题讨论】:

    标签: arrays database perl mariadb


    【解决方案1】:

    有许多比较数组的库。但是,您的比较涉及复杂的数据结构(数组具有 hashrefs 作为元素),这至少使我知道的所有模块的使用变得复杂。

    所以这里有一种手工操作的方法。我使用显示的数组及其副本,其中一个值已更改。

    use warnings;
    use strict;
    use feature 'say';
    
    use List::Util qw(none);   # in List::MoreUtils with older Perls
    use Data::Dump qw(dd pp);
    
    sub hr_eq {
        my ($e1, $e2) = @_; 
        return 0 if scalar keys %$e1 != scalar keys %$e2;
        foreach my $k1 (keys %$e1) {
           return 0 if !exists($e2->{$k1}) or $e1->{$k1} ne $e2->{$k1};            
        }   
        return 1
    }
    
    my @a1 = ( 
        { 'title' => 'Legal Notice', 'created' => '2004-10-07 00:17:45', 'id' => 14 },
        { 'created' => '2004-11-15 16:04:06', 'id' => 86096, 'title' => 'IRC' },  
        { 'id' => 16, 'created' => '2004-10-07 16:15:29', 'title' => 'About' }
    );        
    my @a2 = ( 
        { 'title' => 'Legal Notice', 'created' => '2004-10-07 00:17:45', 'id' => 14 },
        { 'created' => '2004-11-15 16:xxx:06', 'id' => 86096, 'title' => 'IRC' },  
        { 'id' => 16, 'created' => '2004-10-07 16:15:29', 'title' => 'About' }
    );
    
    my @only_in_two = grep { 
        my $e2 = $_; 
        none { hr_eq($e2, $_) } @a1;
    } @a2;
    
    dd \@only_in_two;
    

    这可以正确识别@a2 中不存在于@a1 中的元素(时间戳中带有xxx)。

    注意事项

    • 这会找到一个数组的哪些元素不在另一个数组中,而不是数组之间的全部差异。这是问题的具体要求。

    • 比较依赖于数据结构(hashref)的细节;除非您想使用更全面的库(例如 Test::More),否则无法避免。

    • 这使用字符串比较,ne,即使是数字和时间戳。看看你的真实数据对特定元素使用更合适的比较是否有意义。

    • 在整个列表中搜索列表中的每个元素是一个 O(N*M) 算法。只要数据不是太大,这种(二次)复杂性的解决方案是可用的;但是,一旦数据变得足够大,以至于大小的增加会产生明显的影响,它们就会迅速分解(减慢到无用的地步)。时间来感受一下你的情况。

      O(N+M) 方法存在于此,利用哈希,如 ikegami 答案所示。一旦数据大到足以显示,这在算法上要好得多。但是,由于您的数组带有复杂的数据结构(hashrefs),因此需要做一些工作才能提出一个工作程序,特别是因为我们不知道数据。但是,如果您的数据量很大,那么您肯定希望实现这一点。


    一些关于过滤的cmets。

    问题正确地观察到,对于数组的每个元素,因为它在grep 中处理,所以需要检查整个其他数组。

    这是在 grep 的主体中使用来自 List::Utilnone 完成的。如果其块中的代码对列表的所有元素评估为 false,则返回 true;因此,如果“没有”元素满足该代码。这是要求的核心:不能在另一个数组中找到一个元素。

    需要注意默认的$_ variable,因为grepnone 都使用它。

    grep 的块中$_ 为列表中当前处理的元素起别名,因为grep 一个一个地遍历它们;我们将它保存到一个命名变量中($e2)。然后none 出现并在其块中“占有”$_,在处理它们时将@a1 的元素分配给它。 @a2 的当前元素也可用,因为我们已将其复制到 $e2

    none 中执行的测试被拉入一个 a 子例程,我称之为 hr_eq 以强调它专门用于 hashrefs 中的(元素)的相等比较。

    在这个子中可以调整细节。首先,您可以为特定键添加自定义比较(数字必须使用==等),而不是直接使用ne作为每个键的值。然后,如果您的数据结构发生变化,这就是您需要调整细节的地方。

    【讨论】:

    • 是的,我无法理解你的所作所为:-(我会试着弄清楚。
    • @AlexRegan 哦,对不起......您自己的代码离我们不远了,所以我想这会很清楚。我可以添加解释——哪一部分是不透明的?
    • 这是一种较慢的方法 (O(N*M) [基本上是 O(N^2)]) 与更快的方法 (O(N+M) [基本上是 O(N) ])。
    • @ikegami 是的。我考虑过(简单地说,要回到这个问题上),并寻找与他们的grep 努力相关的东西。此外,使用哈希(对于一个工作示例)似乎并不那么干净,因为需要以某种方式考虑整个 hashref。我们甚至不知道是否有任何键可以被认为是唯一的(重复的 hashref 除外)。我已经为此添加了一个项目符号。谢谢。
    • Re "我们甚至不知道是否有任何键可以被认为是唯一的",OP 实际上提到他们只想报告缺少的 id 值,但是哈希通过使用sub key { pack '(J/a*)*', @{ $_[0] }{ @key_fields } } 之类的东西来生成哈希键,方法仍然可以与多字段键一起使用。
    【解决方案2】:

    你可以使用grep

    for my $new_row (@new_rows) {
       say "$new_row->{id} not in old"
          if !grep { $_->{id} == $new_row->{id} } @old_rows;
    }
    
    for my $old_row (@old_rows) {
       say "$old_row->{id} not in new"
          if !grep { $_->{id} == $old_row->{id} } @new_rows;
    }
    

    但这是一个 O(N*M) 解决方案,而存在一个 O(N+M) 解决方案会快得多。

    my %old_keys;  ++$old_keys{ $_->{id} } for @old_rows;
    my %new_keys;  ++$new_keys{ $_->{id} } for @new_rows;
    
    for my $new_row (@new_rows) {
       say "$new_row->{id} not in old"
          if !$old_keys{$new_row->{id}};
    }
    
    for my $old_row (@old_rows) {
       say "$old_row->{id} not in new"
          if !$new_keys{$old_row->{id}};
    }
    

    如果您的两个数据库连接都指向同一个数据库,则可以在数据库本身内更有效地完成此操作。

    1. 创建一个包含三个字段的临时表,idold_count (DEFAULT 0) 和 new_count (DEFAULT 0)。
    2. INSERT OR UPDATE从旧表到临时表,在这个过程中递增old_count
    3. INSERT OR UPDATE 从新表到临时表,过程中递增new_count
    4. SELECT 临时表的行,其中0 对应old_count0 对应new_count

    【讨论】:

      【解决方案3】:
      select id,title,created from mos_content
           LEFT JOIN xugc_content USING(id)
           WHERE xugc_content.id IS NULL;
      

      为您提供mos_content 中但不在xugc_content 中的行。

      这比 Perl 代码还要短。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-26
        • 1970-01-01
        • 2014-05-18
        • 2014-04-18
        • 2012-08-22
        • 2020-07-19
        • 2018-11-04
        • 2021-08-01
        相关资源
        最近更新 更多