【问题标题】:In RegEx, how do you find a line that contains no more than 3 unique characters?在 RegEx 中,如何找到包含不超过 3 个唯一字符的行?
【发布时间】:2010-11-27 23:38:54
【问题描述】:

我正在循环浏览一个大文本文件,并寻找包含不超过 3 个不同字符的行(但是,这些字符可以无限重复)。我假设最好的方法是使用某种正则表达式。

感谢所有帮助。

(我正在用 PHP 编写脚本,如果有帮助的话)

【问题讨论】:

  • 哇,感谢所有建议。我无法告诉你我对帮助的感激之情。

标签: php regex search string find


【解决方案1】:

儿童正则表达式优化有趣的时间练习!以gnarf的正则表达式为起点:

^(.)\1*(.)?(?:\1*\2*)*(.)?(?:\1*\2*\3*)*$

我注意到这里有嵌套和连续的 *s,这会导致很多回溯。例如,在“abcaaax”中,它将尝试将最后一个字符串“a”匹配为长度为 3 的单个 \1*、长度为 2 的 \1* 后跟单个 \1、\1 后跟 2 长度\1*,或三个单匹配 \1。当你有更长的字符串时,这个问题会变得更糟,特别是当由于正则表达式没有什么可以阻止 \1 与 \2 相同的字符时。

^(.)\1*(.)?(?:\1|\2)*(.)?(?:\1|\2|\3)*$

这是在 Python 的 PCRE 匹配器上测试的原始速度的两倍多。 (这比在 PHP 中设置要快,抱歉。)

这仍然有一个问题,(.)? 无法匹配任何内容,然后继续进行其余的匹配。 \1|\2 仍将匹配 \1 即使没有匹配的 \2,这会导致潜在的回溯尝试在无法匹配时更早地引入 \1|\2 和 \1|\2|\3 子句。这可以通过在整个尾随子句周围移动 ? 可选性来解决:

^(.)\1*(?:(.)(?:\1|\2)*(?:(.)(?:\1|\2|\3)*)?)?$

这又是原来的两倍。

还有一个潜在的问题是 \1、\2 和 \3 中的任何一个都可以是同一个字符,当表达式不匹配时可能会导致更多的回溯。这将通过使用否定前瞻来阻止它与前一个字符不匹配:

^(.)\1*(?:(?!\1)(.)(?:\1|\2)*(?:(?!\1|\2)(.)(?:\1|\2|\3)*)?)?$

但是在 Python 中使用我的随机测试数据,我并没有注意到由此带来的显着加速。根据测试数据,您在 PHP 中的表现可能会有所不同,但它可能已经足够好了。如果在此处可用,所有格匹配 (*+) 可能会有所帮助。

没有任何正则表达式比更易于阅读的 Python 替代方法表现更好:

len(set(s))<=3

PHP 中的类似方法可能是count_chars:

strlen(count_chars($s, 3))<=3

我还没有测试过速度,但我非常希望它比正则表达式更快,而且更易读。

所以基本上我只是完全浪费时间摆弄正则表达式。不要浪费你的时间,在使用正则表达式之前先寻找简单的字符串方法!

【讨论】:

    【解决方案2】:

    冒着被否决的风险,我建议正则表达式不适用于处理这种情况。

    您可以匹配一个字符或一组字符,但您不能让它记住已经找到一组字符以排除进一步匹配的字符。

    我建议您保留一个字符集,在开始新行之前将其重置,并在越行时添加元素。一旦集合中的元素数超过 3,就删除当前行并继续下一行。

    【讨论】:

    • 正则表达式并不真正适用于它们习惯的许多情况,不过这个是正则表达式。 RegExp 构建用于解析字符串的确定性状态机,给定反向引用,它们可以记住匹配的字符。当正则表达式引擎可以为您处理时,为什么还要花时间编写自己的字符串解析状态机?
    • @gnaft:正则表达式会变得复杂且不可读,而代码会很清晰。例如,在 ruby​​ 中:“line_string.chars.to_a.uniq.length
    【解决方案3】:

    也许这会起作用:

    preg_match("/^(.)\\1*(.)?(?:\\1*\\2*)*(.)?(?:\\1*\\2*\\3*)*$/", $string, $matches);
    // aaaaa:Pass
    // abababcaaabac:Pass
    // aaadsdsdads:Pass
    // aasasasassa:Pass
    // aasdasdsadfasf:Fail
    

    说明:

    /
     ^                 #start of string
     (.)               #match any character in group 1
     \\1*              #match whatever group 1 was 0 or more times
     (.)?              #match any character in group 2 (optional)
     (?:\\1*\\2*)*     #match group 1 or 2, 0 or more times, 0 or more times 
                       #(non-capture group)
     (.)?              #match any character in group 3 (optional)
     (?:\\1*\\2*\\3*)* #match group 1, 2 or 3, 0 or more times, 0 or more times
                       #(non-capture group)
     $                 #end of string
    /
    

    另一个好处,$matches[1], [2], [3] 将包含您想要的三个字符。正则表达式查找第一个字符,然后将其存储并匹配,直到找到该字符以外的其他字符,将其作为第二个字符捕获,尽可能多次匹配这些字符中的任何一个,捕获第三个字符,然后匹配所有三个,直到匹配失败或字符串结束并且测试通过。

    编辑

    由于解析引擎和回溯的工作方式,这个正则表达式会快得多,请阅读 bobince 的答案以获得解释:

    /^(.)\\1*(?:(.)(?:\\1|\\2)*(?:(.)(?:\\1|\\2|\\3)*)?)?$/
    

    【讨论】:

    • 不错的一个!正则表达式可能不是最好的方法,但这是可行的,它为 OP 提供了一个具体的基础来决定是使用正则表达式还是其他一些技术。
    • 仅供参考,我在一个较大的文件上尝试了所有这些正则表达式,并且您的原始正则表达式挂起。我通过在捕获组之后删除问号来使其工作,但性能并不是很令人印象深刻。但是当我在每个量词中添加'+' 以使它们具有所有格时,它的表现优于其他所有量词。
    【解决方案4】:

    对我来说 - 作为一名具备足够正则表达式知识的程序员,这听起来不像是一个只能使用 Regexp 解决的问题。

    您更有可能需要构建一个 hashMap/array 数据结构 key: character value:count 并迭代大文本文件,为每一行重建映射。在每个新字符处检查已经遇到的字符数是否为 2,如果是,则跳过当前行。

    但是如果一个疯狂的正则表达式黑客会想出一个解决方案,我会很惊讶。

    【讨论】:

      猜你喜欢
      • 2021-02-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多