正则表达式匹配一个或两个,但不是两次
Regex match either or both, but not twice
我正在努力思考如何编写一个匹配以下任何一个的正则表达式:
pirates
scallywags
pirates scallywags
scallywags pirates
但这些都不是:
pirates pirates
scallywags scallywags
pirates booty scallywags
booty pirates
当然我可以列出所有可能的排列作为替代:
(pirates|scallywags|pirates scallywags|scallywags pirates)
但我觉得应该有一个 easier/more 有效的方法。
[当我写这篇文章时,我想象在感兴趣的单词之前、之后和之间可能会有其他单词。但这不是你问的。我会在这里留下答案,以防万一有人觉得它有用。]
使用多个匹配项最易读。
/\b(?:pirates|scallywags)\b/
&& !/\b booty \b/x &&
&& !/\b(pirates|scallywags)\b .* \b\b/xs
仅使用两个已经影响可读性。
/\b(?:pirates|scallywags)\b/
&& !/ \b (?: booty | (pirates|scallywags)\b .* \b ) \b/xs
一个就可以完成。
/
^
(?! .* \b (?: booty | (pirates|scallywags)\b .* \b ) \b )
.* \b(?:pirates|scallywags)\b
/xs
如果你想避免扫描字符串两次,你可以使用以下方法:
/
^
(?:(?! \b(?:booty|pirates|scallywags)\b ).)*
\b(?:pirates|scallywags)\b
(?:(?! \b(?:booty|pirates|scallywags)\b ).)*
\z
/xs
事实证明,对于熟悉 (?:(?!PATTERN).)*
惯用语的人来说,它相当易读。
这三个中哪个最快可能取决于被搜索字符串的长度、它们包含 pirates
或 scallywags
的频率、它们包含 booty
的频率以及它们如何接近他们的起点 pirates
或 scallywags
时通常会被发现。
仍然不够聪明,但会工作:
^(pirates|scallywags)(?! )( (pirates|scallywags))?$
可能的解决方案,但可能远非最佳(否定匹配)
use strict;
use warnings;
use feature 'say';
my $re = qr/\b(pirates|scallywags)\b\s+|\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
如果你只有两个词,那么你已经有了最好的解决方案(除了不必要的捕获和丢失的锚点)。
如果您有更多的单词,那么正则表达式引擎不是您的最佳选择。
最有效的基于正则表达式的方法是您拥有的方法:
$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";
我正在努力思考如何编写一个匹配以下任何一个的正则表达式:
pirates
scallywags
pirates scallywags
scallywags pirates
但这些都不是:
pirates pirates
scallywags scallywags
pirates booty scallywags
booty pirates
当然我可以列出所有可能的排列作为替代:
(pirates|scallywags|pirates scallywags|scallywags pirates)
但我觉得应该有一个 easier/more 有效的方法。
[当我写这篇文章时,我想象在感兴趣的单词之前、之后和之间可能会有其他单词。但这不是你问的。我会在这里留下答案,以防万一有人觉得它有用。]
使用多个匹配项最易读。
/\b(?:pirates|scallywags)\b/
&& !/\b booty \b/x &&
&& !/\b(pirates|scallywags)\b .* \b\b/xs
仅使用两个已经影响可读性。
/\b(?:pirates|scallywags)\b/
&& !/ \b (?: booty | (pirates|scallywags)\b .* \b ) \b/xs
一个就可以完成。
/
^
(?! .* \b (?: booty | (pirates|scallywags)\b .* \b ) \b )
.* \b(?:pirates|scallywags)\b
/xs
如果你想避免扫描字符串两次,你可以使用以下方法:
/
^
(?:(?! \b(?:booty|pirates|scallywags)\b ).)*
\b(?:pirates|scallywags)\b
(?:(?! \b(?:booty|pirates|scallywags)\b ).)*
\z
/xs
事实证明,对于熟悉 (?:(?!PATTERN).)*
惯用语的人来说,它相当易读。
这三个中哪个最快可能取决于被搜索字符串的长度、它们包含 pirates
或 scallywags
的频率、它们包含 booty
的频率以及它们如何接近他们的起点 pirates
或 scallywags
时通常会被发现。
仍然不够聪明,但会工作:
^(pirates|scallywags)(?! )( (pirates|scallywags))?$
可能的解决方案,但可能远非最佳(否定匹配)
use strict;
use warnings;
use feature 'say';
my $re = qr/\b(pirates|scallywags)\b\s+|\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
如果你只有两个词,那么你已经有了最好的解决方案(除了不必要的捕获和丢失的锚点)。
如果您有更多的单词,那么正则表达式引擎不是您的最佳选择。
最有效的基于正则表达式的方法是您拥有的方法:
$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";