【问题标题】:regular expressions: find every word that appears exactly one time in my document正则表达式:查找在我的文档中恰好出现一次的每个单词
【发布时间】:2011-08-03 07:18:37
【问题描述】:

尝试学习正则表达式。作为一种实践,我试图找到在我的文档中恰好出现一次的每个单词——在语言学中,这是一个 hapax legemenon (http://en.wikipedia.org/wiki/Hapax_legomenon)

所以我认为下面的表达式给了我想要的结果:

\w{1}

但这不起作用。 \w 返回一个字符而不是整个单词。此外,它似乎并没有给我只出现一次的字符(它实际上返回 25873 个匹配项——我假设它们都是字母数字字符)。谁能给我一个例子,说明如何用正则表达式找到“hapax legemenon”?

【问题讨论】:

  • 你使用什么风格的正则表达式(即你用什么语言编程?)
  • 嗯,现在我正在使用一个 XML 编辑器,该编辑器在其“查找和替换”功能中识别正则表达式(我认为它说了一些关于 Pearl 的内容)通常,我在编程时使用 PHP。你能告诉我它在两种语言中的样子吗?
  • +1 获取 hapax legomenon 的有趣链接
  • 根据您目前对正则表达式的理解,正如您的问题所证明的那样(没有冒犯的意思),我认为最好先read a basic regex tutorial,然后再着手处理诸如此类的难题。顺便说一句,它是 Perl,而不是 Pearl。
  • @tim - 没有被拿走 - 我绝对是在我的婴儿期。感谢您的链接。

标签: regex


【解决方案1】:

如果您尝试将此作为学习练习,那么您选择了一个非常难的问题:)

首先,解决方法如下:

\b(\w+)\b(?<!\b\1\b.*\b\1\b)(?!.*\b\1\b)

现在,解释如下:

  • 我们想匹配一个词。这是\b\w+\b - 一个或多个 (+) 单词字符 (\w) 的运行,两边都有一个“断字” (\b)。单词字符和非单词字符之间会发生断字,因此这将匹配(例如)单词字符和空格之间,或字符串的开头和结尾。我们还使用括号 ((...)) 将单词捕获到反向引用中。这意味着我们可以稍后参考比赛本身。

  • 接下来,我们要排除这个词已经出现在字符串中的可能性。这是通过使用消极的后视 - (?&lt;! ... ) 来完成的。如果其内容与字符串匹配,则否定的后向 不匹配。因此,如果我们匹配的单词已经出现,我们想匹配。我们通过对已经捕获的单词使用反向引用 (\1) 来做到这一点。这里的最终匹配是 \b\1\b.*\b\1\b - 当前匹配的两个副本,由任意数量的字符串 (.*) 分隔。

  • 最后,如果字符串的其余部分中的任何位置存在该单词的另一个副本,我们不希望匹配。我们通过使用否定前瞻 - (?! ... ) 来做到这一点。如果它们的内容在字符串中的这一点匹配,则负前瞻不匹配。我们想在任意数量的字符串之后匹配当前单词,所以我们使用 (.*\b\1\b)。

这是一个示例(使用 C#):

var s = "goat goat leopard bird leopard horse";

foreach (Match m in Regex.Matches(s, @"\b(\w+)\b(?<!\b\1\b.*\b\1\b)(?!.*\b\1\b)"))
    Console.WriteLine(m.Value);

输出:

bird
horse

【讨论】:

  • 很好的解释和高级主题的介绍。 +1
  • 哇,它的美丽几乎是雄伟的。我收回我之前关于它是一个丑陋的正则表达式的评论。 +1(我想知道这有多快?)
  • 哇——确实是个难题。我应该将此作为官方答案吗?好像有人喜欢这个。
  • @OverZealous:我不认为它会很快,但也不会慢得可怕。在我的笔记本电脑上(在 Linqpad 中运行),10,000 个单词(Lorem Ipsum)大约需要 15 秒。
【解决方案2】:

如果您的正则表达式引擎支持后向断言中的无限重复(例如 .NET),则可以在单个正则表达式中完成:

Regex regexObj = new Regex(
    @"(       # Match and capture into backreference no. 1:
     \b       # (from the start of the word)
     \p{L}+   # a succession of letters
     \b       # (to the end of a word).
    )         # End of capturing group.
    (?<=      # Now assert that the preceding text contains:
     ^        # (from the start of the string)
     (?:      # (Start of non-capturing group)
      (?!     #  Assert that we can't match...
       \b\1\b #  the word we've just matched.
      )       #  (End of lookahead assertion)
      .       #  Then match any character.
     )*       # Repeat until...
     \1       # we reach the word we've just matched.
    )         # End of lookbehind assertion.
    # We now know that we have just matched the first instance of that word.
    (?=       # Now look ahead to assert that we can match the following:
     (?:      # (Start of non-capturing group)
      (?!     #  Assert that we can't match again...
       \b\1\b #  the word we've just matched.
      )       #  (End of lookahead assertion)
      .       #  Then match any character.
     )*       # Repeat until...
     $        # the end of the string.
    )         # End of lookahead assertion.", 
    RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace);
Match matchResults = regexObj.Match(subjectString);
while (matchResults.Success) {
    // matched text: matchResults.Value
    // match start: matchResults.Index
    // match length: matchResults.Length
    matchResults = matchResults.NextMatch();
} 

【讨论】:

  • 好吧,在任何情况下,我都会寻求一个更易读的解决方案,即使这会导致多行代码或循环。如果性能是一个参数:升级运行该代码的机器;-)
  • 差不多,但不完全。 "goat goat goa leopard bird leopard horse goat" - "goa" 有效但未被捕获。
  • @Porges:对。我在反向引用周围添加了单词边界。
【解决方案3】:

如果你想匹配一个英文单词,最好的形式是:

[a-zA-Z]+

\w 的问题在于它还包含_ 和数字数字0-9

如果您需要包含其他字符,您可以在Z 之后、] 之前附加它们。或者,您可能需要先规范化输入文本。

现在,如果您想计算所有单词的数量,或者只想查看不多次出现的单词,则无法使用单个正则表达式来实现。您需要花一些时间来编写更复杂的逻辑。它可能非常需要数据库或某种内存结构的支持来跟踪计数。解析并统计整个文本后,您可以搜索计数为1 的单词。

【讨论】:

  • 当然可以在单个正则表达式中完成。不过,这并不是首选的解决方案:)
  • 我不知道,正则表达式不是Turing complete - 那将是一个讨厌的正则表达式! X-D
【解决方案4】:

(\w+){1} 将匹配每个单词。 之后,您总是可以对比赛进行计数....

【讨论】:

    【解决方案5】:

    更高层次的解决方案:

    创建一个匹配数组:

    preg_match_all("/([a-zA-Z]+)/", $text, $matches, PREG_PATTERN_ORDER);
    

    让 PHP 计算你的数组元素:

    $tmp_array = array_count_values($matches[1]);
    

    遍历 tmp 数组并检查字数:

    foreach ($tmp_array as $word => $count) {
        echo $word . '  ' . $count;
    }
    

    【讨论】:

      【解决方案6】:

      低级,但为所欲为:

      使用 split 将文本传递到数组中:

      $array = split('\s+', $text);
      

      遍历该数组:

      foreach ($array as $word) { ... }
      

      检查每个单词是否是单词:

      if (!preg_match('/[^a-zA-Z]/', $word) continue;
      

      将单词作为键添加到临时数组中:

      if (!$tmp_array[$word]) $tmp_array[$word] = 0;
      $tmp_array[$word]++;
      

      循环之后。遍历 tmp 数组并检查字数:

      foreach ($tmp_array as $word => $count) {
          echo $word . '  ' . $count;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-07-30
        • 2016-11-24
        • 1970-01-01
        • 2021-12-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多