【问题标题】:Regex match either or both, but not twice正则表达式匹配一个或两个,但不是两次
【发布时间】:2020-06-23 00:03:24
【问题描述】:

我正在努力思考如何编写一个匹配以下任何一个的正则表达式:

pirates
scallywags
pirates scallywags
scallywags pirates

但不是这些:

pirates pirates
scallywags scallywags
pirates booty scallywags
booty pirates

当然,我可以列出所有可能的排列作为替代:

(pirates|scallywags|pirates scallywags|scallywags pirates)

但我觉得应该有一种更简单/更有效的方法。

【问题讨论】:

  • Re "但我觉得应该有更简单/更有效的方式。",这绝对是最有效的方式
  • @ikegami 根据 regex101,我接受的答案需要 31 步才能匹配 pirates scallywags(如果在 cmets 中使用递归方法,则为 30),而 (pirates|scallywags|pirates scallywags|scallywags pirates) 则需要 33 步。虽然差别不大,但实际上效率并不高。
  • 在 Perl 中,'pirates scallywags' =~ /^(?:pirates|scallywags|pirates scallywags|scallywags pirates)\z/ 需要 24 步,而 pirates scallywags' =~ /^(pirates|scallywags)(?! \1)( (pirates|scallywags))?\z/ 需要 38 步。前者也是35-40% faster

标签: regex perl pcre


【解决方案1】:

如果你只有两个词,那么你已经有了最好的解决方案(除了不必要的捕获和丢失的锚点)。

如果你有更多的单词,那么正则表达式引擎不是你的最佳选择。


最有效的基于正则表达式的方法是您所拥有的:

$str =~ /^(?:pirates|scallywags|pirates scallywags|scallywags pirates)\z/

缺点是代码重复。可以通过动态构建模式来避免这种情况,同时保持最大的效率。

use Math::Combinatorics qw( );

sub build_re {
   my @quoted = map quotemeta, @words;
   my @alts;
   for my $r (1..$#words) {
      my $mc = Math::Combinatorics->new( count => $r, data => \@quoted );
      while ( my @combo = $mc->next_combination ) {
         push @alts, join " ", @combo;
      }
   }

   my $alt = join "|", @alts;
   return qr/^(?:$alt)\z/;
}

my @words = qw( pirates scallywags );
my $re = build_re(\@words, $re);

$str =~ $re
   or die "Invalid\n";

好的,所以两个字不值得,但如果有 5 个呢?手动创建 31 个字符串非常容易出错。上面的代码将创建这 31 个字符串,Perl 正则表达式引擎将从它们中创建一个高效的 trie。

但是,此时使用正则表达式引擎真的是最好的选择吗?让我们改用计数集。

sub check {
   my $words = shift;

   my %counts;
   ++$counts{$_} for split ' ', $_[0];

   my $any;
   for (@words) {
      my $count = delete($counts{$word})
         or next;

      return 0 if $count > 1;
      ++$any;
   }

   return $any && !%counts;
}

my @words = qw( pirates scallywags );
check(\@words, $str)
   or die "Invalid\n";

【讨论】:

    【解决方案2】:

    仍然不够聪明,但会工作:

    ^(pirates|scallywags)(?! \1)( (pirates|scallywags))?$
    

    【讨论】:

    • 一个变种:^(pirates|scallywags)(?: (?!\1)\g<1>)?$.
    • 这就像一个魅力。我有一种感觉,它会涉及负面的前瞻和反向引用,但我无法在脑海中将它们完全拼凑起来。谢谢!
    • $(在字符串结尾或字符串结尾换行)应该是\z(在字符串结尾)。第二次和第三次捕获正在扮演分组的角色,不必要地减慢速度。使用分组 ((?:...)) 而不是捕获 ((...))。
    【解决方案3】:

    [当我写这篇文章时,我想象在感兴趣的词之前、之后和中间可能还有其他词。但这不是你问的。我会把答案留在这里,以防有人觉得它有用。]

    使用多个匹配项更具可读性。

    /\b(?:pirates|scallywags)\b/
    && !/\b booty \b/x &&
    && !/\b(pirates|scallywags)\b .* \b\1\b/xs
    

    只使用两个已经影响可读性。

    /\b(?:pirates|scallywags)\b/
    && !/ \b (?: booty | (pirates|scallywags)\b .* \b\1 ) \b/xs
    

    可以用一个来完成。

    /
       ^
       (?! .* \b (?: booty | (pirates|scallywags)\b .* \b\1 ) \b )
       .* \b(?:pirates|scallywags)\b
    /xs
    

    如果您想避免两次扫描字符串,可以使用以下内容:

    /
       ^
       (?:(?! \b(?:booty|pirates|scallywags)\b ).)*
       \b(?:pirates|scallywags)\b
       (?:(?! \b(?:booty|pirates|scallywags)\b ).)*
       \z
    /xs
    

    事实证明,对于熟悉 (?:(?!PATTERN).)* 习语的人来说,它是相当易读的。

    这三个中哪一个最快可能取决于被搜索字符串的长度、它们包含piratesscallywags 的频率、它们包含booty 的频率以及距离它们的起始pirates 或多近的距离scallywags 通常会在它出现时找到。

    【讨论】:

      【解决方案4】:

      可能的解决方案,但可能远非最佳(否定匹配)

      use strict;
      use warnings;
      use feature 'say';
      
      my $re = qr/\b(pirates|scallywags)\b\s+\1|\bbooty\b/;
      
      while(<DATA>) {
          chomp;
          say if $_ !~ /$re/;
      }
      
      
      __DATA__
      pirates
      scallywags
      pirates scallywags
      scallywags pirates
      pirates pirates
      scallywags scallywags
      pirates booty scallywags
      booty pirates
      

      输出

      pirates
      scallywags
      pirates scallywags
      scallywags pirates
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-01-05
        • 2021-06-16
        • 1970-01-01
        • 2017-10-01
        • 2011-04-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多