【问题标题】:Optimized regex for N words around a given word (UTF-8)针对给定单词周围的 N 个单词优化正则表达式 (UTF-8)
【发布时间】:2010-08-27 15:43:45
【问题描述】:

我正在尝试找到一个优化的正则表达式来返回围绕另一个单词的 N 个单词(如果可用)以构建摘要。该字符串采用 UTF-8 格式,因此“单词”的定义不仅仅是 [a-z]。用作参考词的字符串可以在词的中间,也可以不直接被空格包围。

我已经得到了以下有效的方法,但在寻找超过 6-7 个单词时看起来实际上很贪婪和窒息:

/(?:[^\s\r\n]+[\s\r\n]+[^\s\r\n]*){0,4}lorem(?:[^\s\r\n]*[\s\r\n]+[^\s\r\n]+){0,4}/u

这是我为此构建的 PHP 方法,但我需要帮助让正则表达式不那么贪婪,并且可以处理任意数量的单词。

/**
 * Finds N words around a specified word in a string.
 *
 * @param string $string The complete string to look in.
 * @param string $find The string to look for.
 * @param integer $before The number of words to look for before $find.
 * @param integer $after The number of words to look for after $find.
 * @return mixed False if $find was not found and all the words around otherwise.
 */
private function getWordsAround($string, $find, $before, $after)
{
    $matches = array();
    $find = preg_quote($find);
    $regex = '(?:[^\s\r\n]+[\s\r\n]+[^\s\r\n]*){0,' . (int)$before . '}' .
        $find . '(?:[^\s\r\n]*[\s\r\n]+[^\s\r\n]+){0,' . (int)$after . '}';
    if (preg_match("/$regex/u", $string, $matches)) {
        return $matches[0];
    } else {
        return false;
    }
}

如果我有以下 $string:

"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras auctor, 
felis non vehicula suscipit, enim quam adipiscing turpis, eget rutrum 
eros velit non enim. Sed commodo cursus vulputate. Aliquam id diam sed arcu 
fringilla venenatis. Cras vitae ante ut tellus malesuada convallis. Vivamus 
luctus ante vel ligula eleifend condimentum. Donec a vulputate velit. 
Suspendisse velit risus, volutpat at dapibus vitae, viverra vel nulla."

并调用getWordsAround($string, 'vitae', 8, 8) 我想得到以下结果:

"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras auctor, 
felis non vehicula suscipit,"

感谢您的帮助正则表达式专家。

【问题讨论】:

  • 对于初学者来说,\s 包括\r\n,因此将它们添加到相同的字符类是多余的。同样[^\s] 等价于\S
  • 提示,感谢 NullUserException。
  • 顺便说一句,这是一个有趣的问题。当我回来时,我会尝试提出更好的解决方案。 +1
  • @NullUserException 谢谢!我也玩得很开心。如果您找到更好的解决方案,请告诉我,与此同时,我会看看我是否也能想出一些办法,我从下面的内容中得到了一些不错的想法。

标签: php regex utf-8 pcre


【解决方案1】:

如何使用正则表达式或其他方法将输入文本拆分为单词数组。然后通过循环查找目标单词来遍历单词。找到后,获取所需的数组切片,将其连接在一起并打印。

要保持单词之间的原始空白,您可以将其包含在每个单词的末尾。

此外,这可以实现为流解析器,而不是先拆分整个字符串。

【讨论】:

  • 我喜欢纸面上的想法,但是当你开始实施时,你会遇到障碍(例如:你应该如何将各个部分重新组合在一起,同时保持它们原来的分隔符)?
  • @NullUserException,您可以在单词标记中包含空格,或者实现一个流解析器,在它通过字符串时保存最后 N 个单词边界。
  • 如果他不使用正则表达式,他还不如循环遍历字符串,直到找到他想要的单词,然后来回查找周围的单词。它会更快,而且内存效率肯定更高。
  • 他甚至不必向后退。他可以在遍历字符串时将之前最后一个单词的位置保存在一个大小为n(其中n 是他想要在匹配前保留的单词数)的循环数组中。
【解决方案2】:

如前所述,问题在于大量的回溯。为了解决这个问题,我尝试使用lookbehind 和lookahead 将匹配锚定到字符串。所以我想出了:

/consectetur(?<=((?:\S+\s+){0,8})\s*consectetur)\s*(?=((?:\S+\s+){0,8}))/

不幸的是,这不起作用,因为在 PCRE(或 perl 中)不支持可变长度的lookbehinds。所以我们只剩下:

/consectetur\s*(?:\S+\s+){0,8}/

仅捕获匹配字符串和匹配后最多 8 个单词。但是,如果你use the PREG_OFFSET_CAPTURE flag,得到$match[0]的偏移量,取子串到那个点,用strrev反转字符串,得到前0-8个字(使用/\s*(?:\S+\s+){0,8}/),反转匹配,并重组:

$s = "put test string here";
$matches = array();
if (preg_match('/consectetur\s*(?:\S+\s+){0,8}/', $s, $matches, PREG_OFFSET_CAPTURE)) {
  $before = strrev(substr($s, 0, $matches[0][1]));
  $before_match = array();
  preg_match('/\s*(?:\S+\s+){0,8}/', $before, $before_match);
  echo strrev($before_match[0]) . $matches[0][0];
}

您可以通过在匹配之前获取一个安全的字符子集(例如 100)来加快处理非常大的字符串的速度。然后您只需反转 100 个字符的字符串。

话虽如此,不使用正则表达式的解决方案可能会更好。

【讨论】:

  • 已编辑以添加实际的 PHP 代码。似乎在测试字符串上效果很好。
  • 我认为我在某处读过 PREG_OFFSET_CAPTURE 存在问题,因为它返回字节偏移量而不是实际字符数,并且 strrev 不兼容多字节。这对 latin-1 字符串很有效,但恐怕不是 UTF-8。而且在PHP中反转UTF-8效率不高,至少我尝试过的功能是这样。
  • 您实际上想要substr 的字节偏移量,而不是字符偏移量。至于在 UTF-8 中反转字符串,如果您建立一个合理的 substr 长度来捕获,则此类代码的效率可以忽略不计,例如($before * 20) 字节。任何编码问题都将出现在字符串的开头,当您匹配 $before 单词时应将其切断。
  • @lpfavreau 没有理由在 PHP 中反转不应该是有效的。
  • @Artefacto 你需要为mb_strrev写一个C函数。 :)
【解决方案3】:

这是一个内部 PHP 函数,可以满足您的需求。在用户级功能中,您不太可能在性能方面超越这一点。

将它用于 UTF-8 函数应该没有问题,因为 '\r'、'\n' 和 ' '(通常是所有 ASCII 字符)不能作为另一个字符序列的一部分出现。因此,如果您将有效的 UTF-8 数据传递给两个参数,您应该没问题。像通常反转单字符编码(使用strrev)那样反转 UTF-8 数据确实会带来麻烦,但这个函数不会这样做。

PHP_FUNCTION(surrounding_text)
{
    struct circ_array {
        int *offsets;
        int cur;
        int size;
    } circ_array;
    long before;
    long after;
    char *haystack, *needle;
    int haystack_len, needle_len;
    int i, in_word = 0, in_match = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ssll",
        &haystack, &haystack_len, &needle, &needle_len, &before, &after) 
        == FAILURE)
        return;

    if (needle_len == 0) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,
            "Cannot have empty needle");
        return;
    }

    if (before < 0 || after < 0) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,
            "Number of words after and before should be non-negative");
        return;
    }

    /* saves beggining of match and words before */
    circ_array.offsets = safe_emalloc(before + 1, sizeof *circ_array.offsets, 0);
    circ_array.cur = 0;
    circ_array.size = before + 1;

    for (i = 0; i < haystack_len; i++) {
        if (haystack[i] == needle[in_match]) {
            in_match++;
            if (!in_word) {
                in_word = 1;
                circ_array.offsets[circ_array.cur % circ_array.size] = i;
                circ_array.cur++;
            }
            if (in_match == needle_len)
                break; /* found */
        } else {
            int is_sep = haystack[i] == ' ' || haystack[i] == '\n' || haystack[i] == '\r';

            if (in_match)
                in_match = 0;

            if (is_sep) {
                if (in_word)
                    in_word = 0;
            } else { /* not a separator */
                if (!in_word) {
                    in_word = 1;
                    circ_array.offsets[circ_array.cur % circ_array.size] = i;
                    circ_array.cur++;
                }
            }
        }
    }

    if (in_match != needle_len) {
        efree(circ_array.offsets);
        RETURN_FALSE;
    }


    /* find words after; in_word is 1 */
    for (i++; i < haystack_len; i++) {
        int is_sep = haystack[i] == ' ' || haystack[i] == '\n' || haystack[i] == '\r';
        if (is_sep) {
            if (in_word) {
                if (after == 0)
                    break;
                after--;
                in_word = 0;
            }
        } else {
            if (!in_word)
                in_word = 1;
        }
    }

    {
        char *result;
        int start, result_len;
        if (circ_array.cur < circ_array.size)
            start = circ_array.offsets[0];
        else
            start = circ_array.offsets[circ_array.cur % circ_array.size];

        result_len = i - start;
        result = emalloc(result_len + 1);
        memcpy(result, &haystack[start], result_len);
        result[result_len] = '\0';

        efree(circ_array.offsets);
        RETURN_STRINGL(result, result_len, 0);
    }

}

根据我的测试,C函数比wuputah的版本快4倍(并且没有strrev的问题)。

【讨论】:

  • 哇,这令人印象深刻。 +1 可能找到解决此问题的最快方法。我没有时间测试它,事实上,我从来没有编译过我自己的 PHP 函数,我不确定它是否便于分发,但是,它并没有消除任何你如何使用的东西解决了这个问题。我仍在寻找仅 PHP 的解决方案,但无论如何这应该得到积分!谢谢!
  • 顺便说一句,在声明 is_sep 时,您检查了两次 '\n',所以我想您可以在那里删除一次检查。
  • @Ipfavreau 好的,我删除了多余的 \n。谢谢。
【解决方案4】:

这在这里工作得很好:

(?:[^\s\r\n]*[\s\r\n]+){0,8}(?:[^\s\r\n]*)consectetur(?:[^\s\r\n]*)(?:[\s\r\n]+[^\s\r\n]*){0,8}

给予:

Lorem ipsum dolor sit amet,consectetur adipiscing elit。克拉斯拍卖师, felis nonvehicula suscipit,

然而,这个正则表达式的性能绝对是垃圾。我真的不知道如何提高效率,除了没有正则表达式。

对于接近结尾的单词,性能“绝对垃圾”的原因是引擎尝试在每个字符上开始匹配,然后前进几十个字符,直到它发现,在最后,它找不到您要查找的字符串并丢弃所有内容。

【讨论】:

  • 我的例子不好,抱歉。试试用简历这个词。我不知道为什么,但是当它在字符串中更远时,它似乎变得非常非常慢。
  • 啊,没看到编辑。我知道我可以在没有正则表达式的情况下做到这一点,但我仍然想看看是否有人有想法,以便我可以从中学习。 +1 用于解释为什么性能绝对是垃圾的简单文字解释。 :-)
【解决方案5】:

使用这个正则表达式的问题是它会导致正则表达式引擎灾难性地回溯。尝试的次数随着字符串的大小呈指数增长,这没有好。您可能需要查看atomic grouping 以提高性能。

或者,您可以找到给定单词的第一次出现,然后开始向后和向前查找直到所需长度的单词。伪代码:

$pos = strpos($find);
$result = $find;

foreach $word before $pos {
    $result = $word . $result;
    $count++
    if ($count >= $target)
        break;
}

foreach $word after $pos {
    $result .= $word;
    $count++
    if ($count >= $target)
        break;
}

当然,查找前后的单词,以及处理部分字符串会很麻烦。

【讨论】:

  • 您应该使用我在评论中对 ar 的回答所说的循环数组。向后遍历 UTF-8 字符串效率低下,向前遍历非常有效。
猜你喜欢
  • 2018-10-11
  • 1970-01-01
  • 2021-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多