【问题标题】:Only extract those words from a list that include no repeating letters, using regex仅使用正则表达式从不包含重复字母的列表中提取那些单词
【发布时间】:2011-08-04 00:33:39
【问题描述】:

我有一个很大的单词列表文件,每行一个单词。我想过滤掉重复字母的单词。

INPUT:
  abducts
  abe
  abeam
  abel
  abele

OUTPUT:
  abducts
  abe
  abel

我想使用正则表达式(grep 或 perl 或 python)来执行此操作。这可能吗?

【问题讨论】:

  • 为什么abel 在输出中?我原以为abe 会胜过它。
  • @chrisaycock:我认为 OP 意味着拒绝任何(单独)包含多个字母实例的单词
  • 为什么要使用正则表达式?虽然它可能是可行的,但它肯定会比仅计算字符的性能更差,可读性也更差。
  • @joe_coolish:请停止传播 FUD:!/(\pL).*?\1/si 很容易实现。当理论遇到实践时,理论就输了。
  • @Mark:除非你用低级 C 语言编写,我不相信这是一种选择,否则在高级语言中计数字符将花费更多时间而不是标注该语言正则表达式库,它已经在紧凑的 C 代码中,很可能会采用。这并不总是我们所期望的,但确实如此。

标签: python regex perl grep


【解决方案1】:

编写一个匹配确实有重复字母的单词的正则表达式,然后否定匹配要容易得多:

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

(此代码假定@input 每个条目包含一个单词。)但这个问题不一定最好用正则表达式解决。

我已经给出了 Perl 中的代码,但它可以很容易地转换成任何支持反向引用的正则表达式风格,包括 grep(它也有 -v 开关来否定匹配)。

【讨论】:

  • 不错。我想知道如何将速度方面与像这样的拆分和计数方法进行比较:my @output = grep { my @l = split ''; my %l; @l{@l} = (); keys %l == @l } @input; 看起来你可以有很多回溯......
  • @daotoad,正则表达式运行时间是单词长度的二次方。 split 方法是线性的。但是单词足够短,以至于我不确定哪种方法在实践中实际上会更快。您必须对其进行基准测试。
  • FWIW 反向正则表达式(简化为假设字符串中只有单词字符)是 /^(?:(.)(?!.*?\1))+\z/
  • 正则表达式的二次时间是最坏的情况。最好的情况是恒定时间(但您必须让列表中的所有单词都以一对匹配的字母开头才能得到它)。在它会随单词列表而变化。正则表达式非常擅长aardvark 之类的词,但不擅长bumblebee 之类的词。这些词是长的还是短的(对于短和长的一些定义)?重复字符多还是少?同意,使用具有代表性的语料库进行基准测试是了解的唯一方法。
  • 对于实际的字长,我希望正则表达式比重复计数要快得多。不过我会把它改成非贪婪的。
【解决方案2】:
$ egrep -vi '(.).*\1' wordlist

【讨论】:

  • 是的,这样更好。我总是倾向于使用 shell 而不是像 Perl 或 Python 这样的“大”语言。
【解决方案3】:

可以使用正则表达式:

import re

inp = [
    'abducts'
,   'abe'
,   'abeam'
,   'abel'
,   'abele'
]

# detect word which contains a character at least twice
rgx = re.compile(r'.*(.).*\1.*') 

def filter_words(inp):
    for word in inp:
        if rgx.match(word) is None:
            yield word

print list(filter_words(inp))

【讨论】:

  • 好吧,我承认:这是一个形式上优雅的解决方案,据我所知,它会起作用。它是否会在合理的时间内发挥作用,我不太确定。
  • 当然。如果您不强迫自己使用正则表达式,则 chrisaycock 的解决方案是最好的解决方案。
【解决方案4】:

简单的东西

尽管有不准确的声明,即使用正则表达式这是不可能的,但它确实是。

虽然@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}" 之类的字符串才能正确检测到重复的“字母é's”——即所有规范等效的字素簇单元。

为了解决这些问题,您至少必须首先通过 NFD 分解运行您的字符串,然后再通过\X 使用字素簇,而不是通过. 使用任意代码点。

因此,对于英语,您可能希望在积极匹配中遵循这些行,并根据上面的替换给出相应的否定匹配:

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

但即便如此,仍然存在一些悬而未决的问题,例如 \N{EN DASH}\N{HYPHEN} 是否应该被视为等效元素或不同元素。

那是因为写得好,将两个元素如 red-violetcolored 连成一个复合词 red-violet–colored,如果一对中的至少一个已经包含连字符,则需要使用 EN DASH 作为分隔符,而不是仅仅使用连字符。

通常,EN DASH 保留给性质相似的化合物,例如 时空权衡。但是,使用 typewriter-English 的人甚至不会这样做,而是使用超大量重载的遗留代码点 HYPHEN-MINUS,用于两者:red-violet-colored

这仅取决于您的文本是否来自某些 19 世纪的手动打字机——或者它是否代表了在现代排版规则下正确呈现的英文文本。 :)

认真的不区分大小写

您会注意到,我在这里将仅区分大小写的字母视为相同的字母。那是因为我使用了 /i 正则表达式开关,ᴀᴋᴀ (?i) 模式修饰符。

相当就像说它们与排序规则强度 1 相同——但不完全是,因为 Perl 只使用大小写折叠(尽管 完全 大小写折叠而不是 简单),因为它不区分大小写匹配,而不是比可能首选的第三级更高的排序规则。

主要排序规则强度的完全等价是一个明显更强的陈述,但在一般情况下可能需要完全解决问题。然而,在许多特定情况下, 需要的工作量比问题所需要的工作量要多得多。简而言之,对于实际出现的许多特定情况来说,它是多余的,无论假设的一般情况可能需要多少。

这变得更加困难,因为尽管您可以这样做:

我的 $collat​​or = new Unicode::Collat​​e::Locale:: 级别 => 1, locale => "de__phonebook", 标准化 => undef, ; if ($collat​​or->cmp("müß", "MUESS") == 0) { ... }

并期望得到正确的答案 - 你做到了,万岁! — 这种强大的字符串比较不容易扩展到正则表达式匹配。

然而。 :)

总结

选择是否对解决方案进行设计不足或过度设计将根据个人情况而有所不同,没有人可以为您决定。

我喜欢 CJM 的否定正匹配的解决方案,我自己,虽然它认为重复的字母有点漫不经心。注意:

while ("de__phonebook" =~ /(?=((\w).*?\2))/g) { print "字母 在子字符串 中重复。\n"; }

产生:

字母 在子字符串 中重复。 字母 <_> 在子字符串 <__> 中重复。 字母 在子字符串 中重复。 字母 在子字符串 中重复。

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

当然,当您需要匹配字母时,您需要使用\p{alpha}ᴀᴋᴀ\p{Alphabetic},这与单纯的字母完全不同——这与流行的误解相反。 :)

【讨论】:

    【解决方案5】:

    如果您正在处理可能有重复字母的 long 字符串,尽快停止可能会有所帮助。

    INPUT: for (@input) {
       my %seen;
       while (/(.)/sg) {
          next INPUT if $seen{$1}++;
       }
       say;
    }
    

    除非发现性能真的无法接受,否则我会选择最简单的解决方案。

    my @output = grep !/(.).*?\1/s, @input;
    

    【讨论】:

    • 可能希望应用/i 来实现不区分大小写的匹配。取决于 Roger 是否被认为包含重复的字母。我倾向于认为确实如此,但是,我对 René 也有同样的感觉,这有点更具挑战性。
    【解决方案6】:

    我很好奇其他作者为这个问题提交的各种基于 Perl 的方法的相对速度。所以,我决定对它们进行基准测试。

    如有必要,我稍微修改了每个方法,使其填充@output 数组,以保持输入和输出一致。我验证了所有方法都产生相同的@output,尽管我没有在此处记录该断言。

    这是对各种方法进行基准测试的脚本:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use Benchmark qw(cmpthese :hireswallclock);
    
    # get a convenient list of words (on Mac OS X 10.6.6, this contains 234,936 entries)
    open (my $fh, '<', '/usr/share/dict/words') or die "can't open words file: $!\n";
    my @input = <$fh>;
    close $fh;
    
    # remove line breaks
    chomp @input;
    
    # set-up the tests (
    my %tests = (
    
      # Author: cjm
      RegExp => sub { my @output = grep { not /(\w).*\1/ } @input },
    
      # Author: daotoad
      SplitCount => sub { my @output = grep { my @l = split ''; my %l; @l{@l} = (); keys %l == @l } @input; },
    
      # Author: ikegami
      NextIfSeen => sub {
        my @output;
        INPUT: for (@input) {
          my %seen;
          while (/(.)/sg) {
            next INPUT if $seen{$1}++;
          }
          push @output, $_;
        }
    
      },
    
      # Author: ysth
      BitMask => sub {
        my @output;
        for my $word (@input) {
          my $mask1 = $word x ( length($word) - 1 );
          my $mask2 = join( '', map { substr($word, $_), substr($word, 0, $_) } 1..length($word)-1 );
          if ( ( $mask1 ^ $mask2 ) !~ tr/\0// ) {
            push @output, $word;
          }
        }
      },
    
    );
    
    # run each test 100 times
    cmpthese(100, \%tests);
    

    这是 100 次迭代的结果。

               s/iter SplitCount    BitMask NextIfSeen     RegExp
    SplitCount   2.85         --       -11%       -58%       -85%
    BitMask      2.54        12%         --       -53%       -83%
    NextIfSeen   1.20       138%       113%         --       -64%
    RegExp      0.427       567%       496%       180%         --
    

    如您所见,cjm 的“RegExp”方法是迄今为止最快的。它比下一个最快的方法 ikegami 的“NextIfSeen”方法快 180%。我怀疑 RegExp 和 NextIfSeen 方法的相对速度会随着输入字符串平均长度的增加而收敛。但是对于“正常”长度的英文单词,RegExp 方法是最快的。

    【讨论】:

    • 我可以报告 Perl 5.14 运行这些(通常)比 5.12 快。使用 5.14 的 RC0 与 5.12.3 相比,SplitCount 运行时间为 93%,BitMask 运行时间为 72%,NextIfSeen 运行时间为 106%,RegExp 运行时间为 98%。
    【解决方案7】:

    cjm 给出了正则表达式,但这里有一个有趣的非正则表达式:

    @words = qw/abducts abe abeam abel abele/;
    for my $word (@words) {
        my $mask1 = $word x ( length($word) - 1 );
        my $mask2 = join( '', map { substr($word, $_), substr($word, 0, $_) } 1..length($word)-1 );
        if ( ( $mask1 ^ $mask2 ) !~ tr/\0// ) {
            print "$word\n";
        }
    }
    

    【讨论】:

      【解决方案8】:

      针对 cjm 的解决方案,我想知道它与一些相当简洁的 Perl 相比如何:

      my @output = grep { my @l = split ''; my %l; @l{@l} = (); keys %l == @l } @input;
      

      由于我在这里不受字符数和格式的限制,所以我会更清楚一点,甚至到过度记录的地步:

      my @output = grep {
      
          # Split $_ on the empty string to get letters in $_. 
          my @letters = split '';
      
          # Use a hash to remove duplicate letters.
          my %unique_letters;
          @unique_letters{@letters} = ();  # This is a hash slice assignment.
                                           # See perldoc perlvar for more info
      
          # is the number of unique letters equal to the number of letters?
          keys %unique_letters == @letters
      
      } @input;
      

      当然,在生产代码中,请执行以下操作:

      my @output = grep ! has_repeated_chars($_), @input;
      
      sub has_repeated_letters {
          my $word = shift;
          #blah blah blah
          # see example above for the code to use here, with a nip and a tuck.
      }
      

      【讨论】:

      • 正则表达式总是能胜过该解决方案。如果您发现自己将字符串拆分为单个字符,那么您几乎总是要付出沉重的代价。
      【解决方案9】:

      在带有正则表达式的python中:

      python -c 'import re, sys; print "".join(s for s in open(sys.argv[1]) if not re.match(r".*(\w).*\1", s))' wordlist.txt
      

      在没有正则表达式的python中:

      python -c 'import sys; print "".join(s for s in open(sys.argv[1]) if len(s) == len(frozenset(s)))' wordlist.txt
      

      我使用硬编码文件名执行了一些计时测试,并将输出重定向到 /dev/null 以避免在计时中包含输出:

      没有正则表达式的时间:

      python -m timeit 'import sys' 'print >> sys.stderr, "".join(s for s in open("wordlist.txt") if len(s) == len(frozenset(s)))' 2>/dev/null
      10000 loops, best of 3: 91.3 usec per loop
      

      正则表达式的时间安排:

      python -m timeit 'import re, sys' 'print >> sys.stderr, "".join(s for s in open("wordlist.txt") if re.match(r".*(\w).*\1", s))' 2>/dev/null
      10000 loops, best of 3: 105 usec per loop
      

      显然,正则表达式比 python 中简单的 freezeset 创建和 len 比较慢一点。

      【讨论】:

        【解决方案10】:

        你不能用正则表达式来做到这一点。正则表达式是一个有限状态机,这需要一个堆栈来存储所看到的字母。

        我建议使用 foreach 执行此操作,并使用代码手动检查每个单词。 类似的东西

        List chars
        foreach word in list
            foreach letter in word
                if chars.contains letter then remove word from list
                else
                    chars.Add letter
            chars.clear
        

        【讨论】:

        • 我认为固态并不像您认为的那样:en.wikipedia.org/wiki/Solid_state_(electronics)
        • 正则表达式(名义上)接受正则文法,这意味着它们相当于一种称为“有限状态自动机”的状态机。扩展的正则表达式规则允许更强大的匹配能力,使人们能够攀登乔姆斯基层次结构,甚至达到其顶峰,到达作为唯一领域图灵完备自动机的递归可枚举语言的神奇之地。
        • @daotoad:对不起,我的意思是有限状态机。现在还早。感谢您的指正。但无论如何,你做不到。如果你对那个错误投了反对票,我已经修复了它,我将不胜感激被设置回 0。我的其余评论是有效的,我什至为如何实现他正在寻找的东西制作了伪代码。所有这些都很清楚并且主题明确,除了基本的“你做不到”之外还有附加信息。我不觉得我在帖子中付出的额外努力在技术上被忽视了
        • 没有人(pax RE2 library)在现代模式匹配中使用正式的 ʀᴇɢᴜʟᴀʀ ᴇxᴘʀᴇssɪᴏɴs。事实上,即使是低级的 POSIX 正则表达式也需要更多,因为它们必须能够匹配 (.*)\1。听起来在课堂上的时间太多,而在现实世界中却不够。
        猜你喜欢
        • 2017-07-25
        • 2023-04-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-26
        • 1970-01-01
        • 2021-04-19
        相关资源
        最近更新 更多