正则表达式查找不连续的重复单词(即在字符串中多次出现)

Regex to find inconsecutive duplicate words (i.e. occurs more than once in a string)

提问人:Sam 提问时间:9/17/2020 最后编辑:Sam 更新时间:3/1/2021 访问量:899

问:

什么是正则表达式,它查找字符串中出现多次(不一定连续出现)的所有单词的所有实例?

例如,在字符串中:

如果土拨鼠可以夹木头,土拨鼠可以夹多少木头? 如果土拨鼠可以夹木头,土拨鼠会夹掉他能夹住的所有木头。

它会找到每个重复单词的实例;在上面的示例中,它将找到以下单词:。"wood","could","a","woodchuck","chuck","if"

我在互联网上搜索了这样的正则表达式,但无济于事。有人会认为这就是 SO 上关于“使用正则表达式查找副本”的所有问题都会讨论的内容,但它们都只讨论相邻的单词,例如“the”。

正则表达式 重复与 语言无关

评论

1赞 41686d6564 9/17/2020
您可以使用并过滤掉重复项。这是一个演示\b(\w+)\b(?=.*\1)
0赞 Sam 9/17/2020
@41686d6564号我问这些词什么时候不挨着。
0赞 Sam 9/17/2020
@41686d6564我希望它突出显示每个实例。
0赞 ikegami 9/17/2020
请不要使用与问题无关的语言发送垃圾邮件。你已经在一个现已删除的答案的评论中明确表示你想要一个正则表达式。如果你想指定一个引擎,很好,但指定 4 是不可接受的。
0赞 Sam 9/17/2020
@ikegami你是绝对正确的。感谢您的反馈。我仍在学习如何提出一个好的 SO 问题。

答:

1赞 Polar Bear 9/17/2020 #1

也许下面的演示代码很容易理解

use strict;
use warnings;
use feature 'say';

my $text = do { local $/, <DATA> };

my(%count,@found,$regex);

$count{$_}++ for split '[ .,?]', $text;
$count{$_} > 1 && push @found, $_ for keys %count;
$regex  = join('|',@found);

say 'Before: ' . $text;
$text =~ s/\b($regex)\b/<$1>/g;
say 'After: ' . $text;

__DATA__
How much wood could a woodchuck chuck if a woodchuck could chuck wood? A woodchuck would chuck all the wood he could chuck if a woodchuck could chuck wood.

输出

Before: How much wood could a woodchuck chuck if a woodchuck could chuck wood? A woodchuck would chuck all the wood he could chuck if a woodchuck could chuck wood.
After: How much <wood> <could> <a> <woodchuck> <chuck> <if> <a> <woodchuck> <could> <chuck> <wood>? A <woodchuck> would <chuck> all the <wood> he <could> <chuck> <if> <a> <woodchuck> <could> <chuck> <wood>.

评论

0赞 Polar Bear 9/17/2020
@ikegami -- 我希望它突出显示以下每个实例:“wood”、“could”、“a”、“woodchuck”、“chuck”、“if”。在代码示例中,单词包装成 .key<>
0赞 Sam 9/17/2020
这似乎只是将正则表达式转换为“”wood|could|a|woodchuck|chuck|if”。不是吗?这将是完全无济于事的,因为我不知道我在寻找什么。我没有事先说这些话。
0赞 Polar Bear 9/17/2020
@Sam - 那么在您的问题中,您应该问“如何突出显示文本中重复的单词。并说明是否应该突出显示该单词的第一个实例。在目前的形式下,你的问题并不清楚你的最终目标是什么。您可以提供输入和处理输出的样本以使其透明。
0赞 Polar Bear 9/17/2020
@Sam -- 请参阅我更新的代码,我们事先不知道重复了哪些单词。
0赞 Sam 9/17/2020
这是用什么语言写的?
2赞 ikegami 9/17/2020 #2

您将需要以下内容:

\b\w+\b
(?: (?= .* \b(\1)\b )
|   (?<= \b\1\b .* \1 )
)

(确保它可以匹配您正在使用的引擎中的任何角色。根据需要进行调整。.

您还没有指定正则表达式引擎,但我想不出任何支持可变宽度后视的引擎。[1] 然而,这是实现你想要的所必需的。

它的速度也非常慢,在单词方面有 O(N^2) 时间。[2]


好的,有人展示了可变长度后视:在Perl/PCRE中实际上是可能的!他们使用递归一次后退一个字符。玩得开心。


人们通常会使用两个通道,一个用于查找重复项,另一个用于“查找”。

my %seen;
my @dups = grep ++$seen{$_} == 2, $file =~ /\w+/g;
my $alt = join "|", @dups;
$file =~ s/\b($alt)\b/<$&>/g;

就单词而言,这是 O(N)。


  1. 从技术上讲,从 Perl 5.30 开始,lookbehinds “可以处理 1 到 255 个字符的可变长度作为实验性功能”。对于OP来说,这太小了,OP在现已删除的评论中谈到了GB。

  2. 想象一下,你有一个包含 N 个单词的文档,都是不同的。

    • 单词 1 需要与后面的 N-1 个单词和前面的 0 个单词进行比较。
    • 单词 2 需要与后面的 N-2 个单词和前面的 1 个单词进行比较。
    • ...
    • 单词 N-1 需要与后面的 1 个单词和前面的 N-2 个单词进行比较。
    • 单词 N 需要与后面的 0 个单词和前面的 N-1 个单词进行比较。
      O( (N-1)+0 + (N-2)+1 + ... + 1+(N-2) + 0+(N-1) )
    = O( [ (N-1)+(N-2)+...+1+0 ] + [ 0+1+...+(N-2)+(N-1) ] )
    = O( [ 0+1+...+(N-2)+(N-1) ] * 2 )
    = O( 1+...+(N-2)+(N-1) )               # Constant factors irrelevant in O()
    = O( (N-1) * ((N-1)+1) / 2 )           # 1+2+..x == x*(x+1)/2
    = O( (N-1) * N / 2 )
    = O( (N-1) * N )                       # Constant factors irrelevant in O()
    = O( N^2 - N )
    = O( N^2 )                             # An N term is subsumed by a N^2 term
    

评论

0赞 Sam 9/17/2020
“可变宽度后视”是什么意思?可变宽度将何去何从?
0赞 ikegami 9/17/2020
.*匹配可变数量的字符,是后视。可变宽度后视的示例也是如此。(?<=...)(?<=\b\1\b.*\b\1\b)
0赞 ikegami 9/17/2020
好的,所以可变宽度的 lookbeghind 在某些正则表达式引擎中技术上是可行的。请参阅更新的答案。
0赞 Sam 9/17/2020
为什么后面的看点必须有两个匹配实例?
0赞 Sam 9/17/2020
为什么它比展望更长?
2赞 zdim 9/17/2020 #3

使用正则表达式完成关键部分的一种方法是使用允许在正则表达式内执行代码的功能来构建频率哈希(映射、字典)。然后,可以使用它来选择重复的单词。

仅使用正则表达式完成所有操作,而不使用任何语言或工具,实际上是不可能的(除非使用递归,如果支持的话)。本文的其余部分在正则表达式功能的上下文中采用了编程语言的基础知识,该功能允许在正则表达式中执行代码。

我认为大多数引擎可用的一个功能是运行代码以准备替换字符串。我使用Perl进行以下简短示例。这里感兴趣的是使用副作用完成的,当然不是最佳的(通常会导致代码看起来很笨拙)。

要么运行正常替换并放回匹配的单词

$string =~ s/(\w+)/++$freq{$1}; $1/eg;

或者使用“非破坏性”替换,它不会更改目标(如果匹配失败,则返回更改的字符串或原始字符串)

my $toss = $string =~ s/(\w+)/++$freq{$1}/egr;

如上所述,任务不需要返回的字符串。在这两种情况下,当这不是我们实际需要的时,我们会对每个单词进行替换。

然后打印频率大于 1 的键(字)

foreach my $word (keys %freq) { say $word if $freq{$word} > 1 }

正则表达式匹配每个字符类的“单词”;根据您的需要进行调整。\w

总而言之,由于这对于正则表达式来说是一项棘手的任务,我建议将字符串拆分为单词并使用您的语言功能计算重复项,而不是推送正则表达式。任何有价值的语言都可以相当优雅和有效地做到这一点。


使用 Perl,另一种方法是使用嵌入式代码结构,它允许代码在匹配的部分中运行。据我所知,除了一些库之外,其他语言没有。

可以像这样在匹配部分运行代码

my @matches = $string =~ /(\w+)(?{++$freq{$1}})/g;

其中构造将执行嵌入式代码,此处构建频率哈希。使用此功能(安全)需要仔细阅读文档。(?{code})

以上内容包含所有内容,对所述问题不感兴趣;它在这里用于将正则表达式的匹配运算符放在“列表上下文”中,以便通过修饰符继续搜索字符串。@matches/g

评论

0赞 ikegami 9/23/2020
哎呀,我想我只看到了你答案的底部。