仅用正则提取列表中没有重复字母的单词

2 投票
10 回答
3238 浏览
提问于 2025-04-16 15:35

我有一个很大的单词列表文件,每行一个单词。我想把那些字母重复的单词筛选掉。

INPUT:
  abducts
  abe
  abeam
  abel
  abele

OUTPUT:
  abducts
  abe
  abel

我想用正则表达式来实现这个功能(可以用grep、perl或者python)。这样做可以吗?

10 个回答

3

简单的东西

尽管有人说用正则表达式做不到,但其实是可以的。

虽然 @cjm 说得对,否定一个正匹配的条件比用一个单一模式表达一个负匹配要简单得多,但其实有一种方法是大家都知道的,只要把东西放进那个模型里就行了。假设:

    /X/

匹配某个东西,那么用一个正匹配的模式来表达条件

    ! /X/

可以写成

    /\A (?: (?! X ) . ) * \z /sx

因此,既然正模式是

    / (\pL) .* \1 /sxi

对应的负模式就应该是

    /\A (?: (?! (\pL) .* \1  ) . ) * \z /sxi

通过简单的替换 X.

现实世界的顾虑

不过,有些特殊情况可能需要更多的工作。例如,\pL 描述的是任何具有 GeneralCategory=Letter 属性的代码点,但它没有考虑像 red‐violet–colored’Tisn’tfiancée 这样的词——后者在 NFD 和 NFC 形式下是不同的。

所以你必须先对字符串进行完全分解,这样像 "r\x{E9}sume\x{301}" 这样的字符串才能正确识别重复的“字母 é”——也就是说,所有规范上等价的字形聚合单位。

为了处理这些情况,你至少要先对字符串进行 NFD 分解,然后再使用字形聚合,通过 \X 而不是用任意代码点 .

所以对于英语,你需要的正匹配模式大致是这样的,负匹配模式则根据上面的替换来确定:

    NFD($string) =~ m{
        (?<ELEMENT>
           (?= [\p{Alphabetic}\p{Dash}\p{Quotation_Mark}] ) \X 
        )
        \X *
        \k<ELEMENT>
    }xi

但即便如此,仍然有一些未解决的问题,比如 \N{EN DASH}\N{HYPHEN} 是否应该被视为相同的元素或不同的元素。

这是因为正确书写时,把像 red‐violetcolored 这样的两个元素用连字符连接成一个复合词 red‐violet–colored,其中至少有一个元素 已经包含了连字符,需要使用 EN DASH 作为分隔符,而不是简单的 HYPHEN。

通常,EN DASH 是用来连接性质相似的复合词,比如 a time–space trade‐off。不过,使用打字机英语的人甚至不这么做,他们用那个超负荷的旧代码点 HYPHEN-MINUS 来表示两者:red-violet-colored

这就要看你的文本是来自19世纪的手动打字机,还是按照现代排版规则正确呈现的英语文本。 :)

认真对待大小写不敏感

你会注意到,我在这里把仅仅在大小写上不同的字母视为相同的。这是因为我使用了 /i 的正则表达式开关,也就是 (?i) 模式修饰符。

这有点像说它们的比较强度是1——但又不完全一样,因为 Perl 只使用大小写折叠(虽然是 完全 的大小写折叠,而不是 简单 的)来进行大小写不敏感的匹配,而不是某种比三级更高的比较强度。

在主要比较强度下的完全等价是一个更强的说法,但在一般情况下可能确实需要这样做。然而,这比许多具体情况所需的工作要多得多。简而言之,对于许多实际出现的特定情况来说,这种做法是过于复杂的,尽管在假设的普遍情况下可能是必要的。

这变得更加困难,因为,虽然你可以这样做:

    my $collator = new Unicode::Collate::Locale::
                       level => 1, 
                       locale => "de__phonebook",
                       normalization => undef,
                    ;

    if ($collator->cmp("müß", "MUESS") == 0) { ... }

并期待得到正确的答案——而且你确实得到了,太好了!——但这种强大的字符串比较并不容易扩展到正则表达式匹配。

还没有。 :)

总结

是否选择简单解决方案还是复杂解决方案,取决于具体情况,这没人能替你决定。

我个人喜欢 CJM 的方法,它否定了一个正匹配,尽管它在处理重复字母时有点随意。注意:

    while ("de__phonebook" =~ /(?=((\w).*?\2))/g) {
        print "The letter <$2> is duplicated in the substring <$1>.\n";
    } 

产生:

    The letter <e> is duplicated in the substring <e__phone>.
    The letter <_> is duplicated in the substring <__>.
    The letter <o> is duplicated in the substring <onebo>.
    The letter <o> is duplicated in the substring <oo>.

这就说明了为什么当你需要匹配一个字母时,应该 始终 使用 \pL 也就是 \p{Letter},而不是 \w,因为后者实际上匹配的是 [\p{alpha}\p{GC=Mark}\p{NT=De}\p{GC=Pc}]

当然,当你需要匹配一个字母时,你需要使用 \p{alpha} 也就是 \p{Alphabetic},这和单纯的字母并不相同——这与普遍的误解相反。 :)

5
$ egrep -vi '(.).*\1' wordlist

当然可以!请把你想要翻译的内容发给我,我会帮你用简单易懂的语言解释清楚。

7

写一个正则表达式来匹配那些重复字母的单词要简单得多,然后再把匹配结果取反就行了:

my @input = qw(abducts abe abeam abel abele);
my @output = grep { not /(\w).*\1/ } @input;

(这段代码假设@input每一项只包含一个单词。)不过,这个问题不一定非得用正则表达式来解决。

我给出的代码是用Perl写的,但其实可以很容易地转换成任何支持回溯引用的正则表达式,比如grep(它还有-v这个选项可以用来取反匹配结果)。

撰写回答