如何 grep/perl/awk 重叠正则表达式

Posted

技术标签:

【中文标题】如何 grep/perl/awk 重叠正则表达式【英文标题】:How to grep/perl/awk overlapping regex 【发布时间】:2021-11-26 06:15:58 【问题描述】:

尝试将字符串通过管道传输到 grep/perl 正则表达式中以提取重叠匹配项。目前,结果似乎只提取了没有任何“回溯”的连续匹配:

尝试使用 egrep(在 GNU 和 BSD 上):

$ echo "bob mary mike bill kim jim john" | egrep -io "[a-z]+ [a-z]+"
bob mary
mike bill
kim jim

尝试使用 perl 风格的 grep (-P):

$ echo "bob mary mike bill kim jim john" | grep -oP "()[a-z]+ [a-z]+"
bob mary
mike bill
kim jim

尝试使用 awk 仅显示第一个匹配项:

$ echo "bob mary mike bill kim jim john" | awk 'match($0, /[a-z]+ [a-z]+/) print substr($0, RSTART, RLENGTH)'
bob mary

我希望从一个简单的工作 bash 管道命令中看到的重叠结果是:

bob mary
mary mike
mike bill
bill kim
kim jim
jim john

有什么想法吗?

【问题讨论】:

查看“前瞻断言”。您可以将这些与全局 (/g) 选项和断言中的捕获组结合起来以检索匹配项。 【参考方案1】:

Lookahead 是你的朋友

echo "bob mary mike bill kim jim john" | 
    perl -wnE'say "$1 $2" while /(\w+)\s+(?=(\w+))/g'

关键是,作为“零宽度断言”的前瞻不会消耗任何东西——但它仍然允许我们捕获其中的模式。

因此,当正则表达式引擎匹配一个单词和空格 ((\w+)\s+) 时,将它们吞噬,然后停在那里并“向前看”,只是为了“断言”所寻找的模式在那里;它不会从最后一个空格和下一个 \w 之间的位置移动,也不会像他们所说的那样“消耗”下一个单词。

很高兴我们也可以捕捉到“看到”的模式,即使它没有被消耗掉也很困难!所以我们得到了$1$2,这两个词。

然后,由于/g 修饰符,引擎继续前进,找到另一个单词+空格,后面还有另一个单词。下一个词是我们的前瞻发现的词——所以现在一个词被消耗了,而下一个词“寻找”(并捕获)了。等等。

见Lookahead and lookbehind assertions in perlretut

【讨论】:

【参考方案2】:

使用下面的 Perl 单行代码,避免前瞻(它仍然可以成为您的朋友): 对于空格分隔的单词:

echo "bob mary mike bill kim jim john" | perl -lane 'print "$F[$_] $F[$_+1]" for 0..($#F-1);'

对于在 Perl 中定义为 \w+ 的单词,由非单词字符 \W+ 分隔:

echo "bob.mary,mike'bill kim jim john" | perl -F'/\W+/' -lane 'print "$F[$_] $F[$_+1]" for 0..($#F-1);'

Perl 单行程序使用这些命令行标志:-e:告诉 Perl 查找内联代码,而不是在文件中。-n:循环输入一行一次,默认将其分配给$_-l:在执行内联代码之前剥离输入行分隔符(默认为 *NIX 上的"\n"),并在打印时附加它。-a :在空格或-F 选项中指定的正则表达式上将$_ 拆分为数组@F-F'/\W+/' :在\W+ 上拆分为@F(一个或多个非单词字符),而不是空格。

$#F : 数组@F 的最后一个索引,输入行被分割成其中。0..($#F-1) : 索引(数字)的范围,从第一个 (0) 到数组@F 的倒数第二个($#F-1)索引。$F[$_]$F[$_+1]:数组@F 的两个连续元素,索引分别为$_$_+1

另请参阅:perldoc perlrun: how to execute the Perl interpreter: command line switchesperldoc perlre: Perl regular expressions (regexes)perldoc perlre: Perl regular expressions (regexes): Quantifiers; Character Classes and other Special Escapes; Assertions; Capture groupsperldoc perlrequick: Perl regular expressions quick start

【讨论】:

【参考方案3】:

你也可以使用awk

awk 'for(i=1;i<NF;i++) print $i,$(i+1)' <<< 'bob mary mike bill kim jim john'

请参阅online demo。此解决方案遍历所有以空格分隔的字段并打印当前字段 ($i) + 字段分隔符(此处为空格)+ 后续字段值 ($(i+1))。

或者,另一个使用very common technique 的perl 解决方案在正向前瞻中捕获重叠模式:

perl -lane 'while (/(?=\b(\pL+\s+\pL+))/g) print $1' <<< 'bob mary mike bill kim jim john'

请参阅online demo。 详情

(?= - 积极前瞻的开始 \b - 单词边界 (\pL+\s+\pL+) - 捕获组 1:一个或多个字母、一个或多个空格、一个或多个字母 ) - 前瞻结束。

此处仅打印第 1 组值 (print $1)。

性能考虑

至于这里的 Perl 解决方案,我的解决方案最慢,而 Timur 的解决方案最快,但是,awk 解决方案结果证明比任何 Perl 解决方案都快。结果:

# ./wiktor_awk.sh

real    0m17.069s
user    0m12.264s
sys     0m5.314s

# ./timur_perl.sh

real    0m18.201s
user    0m15.612s
sys     0m6.139s

# ./zdim.sh

real    0m23.559s
user    0m19.883s
sys     0m7.359s

# ./wiktor_perl.sh

real    2m12.528s
user    1m52.857s
sys     0m20.201s

注意我为每个解决方案创建了 *.sh 文件,例如

#!/bin/bash
N=10000
time(
 for i in $(seq 1 $N); do
   <SOLUTION_HERE> &>/dev/null;
done)

并运行for f in *.sh; do chmod +x "$f"; done(借用自here)。

【讨论】:

以上是关于如何 grep/perl/awk 重叠正则表达式的主要内容,如果未能解决你的问题,请参考以下文章

如何检测两个正则表达式在它们可以匹配的字符串中是不是重叠?

如何将重叠字符串与正则表达式匹配?

如何使用正则表达式找到最短的重叠匹配?

Vim 多行正则表达式给出重叠匹配

正则表达式中的重叠匹配

在 C# 中获取重叠的正则表达式匹配