【问题标题】:keyword highlight is highlighting the highlights in PHP preg_replace()关键字 highlight 突出显示 PHP preg_replace() 中的亮点
【发布时间】:2012-02-23 15:21:50
【问题描述】:

我有一个小型搜索引擎在做它的事情,并且想要突出显示结果。我以为我已经解决了所有问题,直到我今天使用的一组关键字将其从水中吹走。

问题是 preg_replace() 正在循环替换,后来的替换正在替换我插入到以前的文本中的文本。使困惑?这是我的伪函数:

public function highlightKeywords ($data, $keywords = array()) {
    $find = array();
    $replace = array();
    $begin = "<span class=\"keywordHighlight\">";
    $end = "</span>";
    foreach ($keywords as $kw) {
        $find[] = '/' . str_replace("/", "\/", $kw) . '/iu';
        $replace[] = $begin . "\$0" . $end;
    }
    return preg_replace($find, $replace, $data);
}

好的,所以它在搜索“fred”和“dagg”时有效,但遗憾的是,在搜索“class”和“lass”和“as”时,突出显示“Joseph's Class Group”时会遇到真正的问题

Joseph's <span class="keywordHighlight">Cl</span><span <span c<span <span class="keywordHighlight">cl</span>ass="keywordHighlight">lass</span>="keywordHighlight">c<span <span class="keywordHighlight">cl</span>ass="keywordHighlight">lass</span></span>="keywordHighlight">ass</span> Group

我怎样才能让后面的替换只适用于非 HTML 组件,但也允许标记整个匹配?例如如果我正在搜索“cla”和“lass”,我希望“class”全部突出显示,因为两个搜索词都在其中,即使它们重叠,并且应用于第一个匹配项的突出显示具有“class” ",但不应突出显示。

叹息。

我宁愿使用 PHP 解决方案,也不愿使用 jQuery(或任何客户端)解决方案。

注意:我尝试按长度对关键字进行排序,先做长的,但这意味着交叉搜索不会突出显示,这意味着“cla”和“lass”只是单词“class”的一部分会突出显示,它仍然谋杀了替换标签:(

编辑:我已经搞砸了,从铅笔和纸开始,开始胡言乱语,然后想出了一些非常乏味的代码来解决这个问题。这不是很好,因此仍然非常感谢您提出建议来调整/加快速度:)

public function highlightKeywords ($data, $keywords = array()) {
    $find = array();
    $replace = array();
    $begin = "<span class=\"keywordHighlight\">";
    $end = "</span>";
    $hits = array();
    foreach ($keywords as $kw) {
        $offset = 0;
        while (($pos = stripos($data, $kw, $offset)) !== false) {
            $hits[] = array($pos, $pos + strlen($kw));
            $offset = $pos + 1;
        }
    }
    if ($hits) {
        usort($hits, function($a, $b) {
            if ($a[0] == $b[0]) {
                return 0;
            }
            return ($a[0] < $b[0]) ? -1 : 1;
        });
        $thisthat = array(0 => $begin, 1 => $end);
        for ($i = 0; $i < count($hits); $i++) {
            foreach ($thisthat as $key => $val) {
                $pos = $hits[$i][$key];
                $data = substr($data, 0, $pos) . $val . substr($data, $pos);
                for ($j = 0; $j < count($hits); $j++) {
                    if ($hits[$j][0] >= $pos) {
                        $hits[$j][0] += strlen($val);
                    }
                    if ($hits[$j][1] >= $pos) {
                        $hits[$j][1] += strlen($val);
                    }
                }
            }
        }
    }
    return $data;
}

【问题讨论】:

  • $hits[$i][0] 表示给出了 0 $hits?哦,我的这个想法……
  • $hits[$i][0]是关键字的起点,$hits[$i][1]是终点。它在纸上不那么令人困惑:)
  • 再读一遍我的,在精神上用S替换$......恐怕只是个坏笑话

标签: php regex preg-replace highlight keyword


【解决方案1】:

我使用以下方法解决了这个问题:

<?php

$protected_matches = array();
function protect(&$matches) {
    global $protected_matches;
    return "\0" . array_push($protected_matches, $matches[0]) . "\0";
}
function restore(&$matches) {
    global $protected_matches;
    return '<span class="keywordHighlight">' .
              $protected_matches[$matches[1] - 1] . '</span>';
}

preg_replace_callback('/\x0(\d+)\x0/', 'restore',
    preg_replace_callback($patterns, 'protect', $target_string));

第一个preg_replace_callback 取出所有匹配项并用空字节包装的占位符替换它们;第二遍将它们替换为 span 标签。

编辑:忘了提$patterns 是按字符串长度排序的,从最长到最短。

编辑;另一种解决方案

<?php
        function highlightKeywords($data, $keywords = array(),
            $prefix = '<span class="hilite">', $suffix = '</span>') {

        $datacopy = strtolower($data);
        $keywords = array_map('strtolower', $keywords);
        $start = array();
        $end   = array();

        foreach ($keywords as $keyword) {
            $offset = 0;
            $length = strlen($keyword);
            while (($pos = strpos($datacopy, $keyword, $offset)) !== false) {
                $start[] = $pos;
                $end[]   = $offset = $pos + $length;
            }
        }

        if (!count($start)) return $data;

        sort($start);
        sort($end);

        // Merge and sort start/end using negative values to identify endpoints
        $zipper = array();
        $i = 0;
        $n = count($end);

        while ($i < $n)
            $zipper[] = count($start) && $start[0] <= $end[$i]
                ? array_shift($start)
                : -$end[$i++];

        // EXAMPLE:
        // [ 9, 10, -14, -14, 81, 82, 86, -86, -86, -90, 99, -103 ]
        // take 9, discard 10, take -14, take -14, create pair,
        // take 81, discard 82, discard 86, take -86, take -86, take -90, create pair
        // take 99, take -103, create pair
        // result: [9,14], [81,90], [99,103]

        // Generate non-overlapping start/end pairs
        $a = array_shift($zipper);
        $z = $x = null;
        while ($x = array_shift($zipper)) {
            if ($x < 0)
                $z = $x;
            else if ($z) {
                $spans[] = array($a, -$z);
                $a = $x;
                $z = null;
            }
        }
        $spans[] = array($a, -$z);

        // Insert the prefix/suffix in the start/end locations
        $n = count($spans);
        while ($n--)
            $data = substr($data, 0, $spans[$n][0])
            . $prefix
            . substr($data, $spans[$n][0], $spans[$n][1] - $spans[$n][0])
            . $suffix
            . substr($data, $spans[$n][1]);

        return $data;
    }

【讨论】:

  • 看起来这可能有效....我想在一个对象中使用它,所以可能需要转换为匿名函数并放弃“全局”变量。我有戏!
  • Hmmm.... 似乎在交叉测试中失败,即在搜索在同一文本中交叉的几个关键字时,例如HELL 和 LLO 作为文本 HELLO 上的关键字只会找到一个匹配项,而不是两个匹配项,因为新引入的字符,因此只突出显示文本中的一个匹配项,而不是两个匹配项。
  • 对不起,我错过了原始问题中的规定。第二个函数应该做你想做的。
【解决方案2】:

我今天不得不自己重新审视这个主题,并写了一个更好的版本。我会把它包括在这里。相同的想法只是更容易阅读并且性能应该更好,因为它使用数组而不是串联。

<?php

function highlight_range_sort($a, $b) {
    $A = abs($a);
    $B = abs($b);
    if ($A == $B)
        return $a < $b ? 1 : 0;
    else
        return $A < $B ? -1 : 1;
}

function highlightKeywords($data, $keywords = array(),
       $prefix = '<span class="highlight">', $suffix = '</span>') {

        $datacopy = strtolower($data);
        $keywords = array_map('strtolower', $keywords);
        // this will contain offset ranges to be highlighted
        // positive offset indicates start
        // negative offset indicates end
        $ranges = array();

        // find start/end offsets for each keyword
        foreach ($keywords as $keyword) {
            $offset = 0;
            $length = strlen($keyword);
            while (($pos = strpos($datacopy, $keyword, $offset)) !== false) {
                $ranges[] = $pos;
                $ranges[] = -($offset = $pos + $length);
            }
        }

        if (!count($ranges))
            return $data;

        // sort offsets by abs(), positive
        usort($ranges, 'highlight_range_sort');

        // combine overlapping ranges by keeping lesser
        // positive and negative numbers
        $i = 0;
        while ($i < count($ranges) - 1) {
            if ($ranges[$i] < 0) {
                if ($ranges[$i + 1] < 0)
                    array_splice($ranges, $i, 1);
                else
                    $i++;
            } else if ($ranges[$i + 1] < 0)
                $i++;
            else
                array_splice($ranges, $i + 1, 1);
        }

        // create substrings
        $ranges[] = strlen($data);
        $substrings = array(substr($data, 0, $ranges[0]));
        for ($i = 0, $n = count($ranges) - 1; $i < $n; $i += 2) {
            // prefix + highlighted_text + suffix + regular_text
            $substrings[] = $prefix;
            $substrings[] = substr($data, $ranges[$i], -$ranges[$i + 1] - $ranges[$i]);
            $substrings[] = $suffix;
            $substrings[] = substr($data, -$ranges[$i + 1], $ranges[$i + 2] + $ranges[$i + 1]);
        }

        // join and return substrings
        return implode('', $substrings);
}

// Example usage:
echo highlightKeywords("This is a test.\n", array("is"), '(', ')');
echo highlightKeywords("Classes are as hard as they say.\n", array("as", "class"), '(', ')');
// Output:
// Th(is) (is) a test.
// (Class)es are (as) hard (as) they say.

【讨论】:

    【解决方案3】:

    OP - 问题中不清楚的是 $data 是否可以从一开始就包含 HTML。你能澄清一下吗?

    如果 $data 可以包含 HTML 本身,那么您将进入尝试使用常规语言解析器解析非常规语言的领域,这不会很好。

    在这种情况下,我建议将 $data HTML 加载到 PHP DOMDocument 中,获取所有 textNode 并依次对每个文本块的内容运行其他非常好的答案之一。

    【讨论】:

    • 是的,应该说清楚。数据都是文本,而不是 HTML。我意识到这会遇到特殊字符翻译的问题,但突出显示是为关键字设计的,而不是标点符号,如果 & 和 " 没有突出显示,我可以接受,只要实际单词是。
    猜你喜欢
    • 2011-08-15
    • 1970-01-01
    • 2016-06-16
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-07
    • 2017-10-18
    相关资源
    最近更新 更多