如何在网页上突出显示搜索匹配文本

How to highlight search-matching text on a web page

我正在尝试编写一个 PHP 函数,将一些文本显示在网页上,然后根据输入的一些搜索词,突出显示文本的相应部分。不幸的是,我有几个问题。
为了更好地解释我遇到的两个问题,让我们假设正在搜索以下无害的字符串并将显示在网页上:

My daughter was born on January 11, 2011.

我的第一个问题是,如果输入了多个搜索词,我用来标记第一个词的任何匹配项的开始和结束的任何占位符文本可能会被第二个词匹配。
例如,我目前正在使用以下定界字符串来标记匹配项的开始和结束(在此基础上,我在函数末尾使用 preg_replace 函数将定界符转换为 HTML span 标签):

'#####highlightStart#####'
'#####highlightEnd#####'

问题是,如果我像 2011 light 这样进行搜索,那么 2011 将首先匹配,给我:

My daughter was born on January 11, #####highlightStart#####2011#####highlightEnd#####.

据此,当搜索 light 时,它将匹配 #####highlightStart##########highlightEnd##### 中的单词 light,这是我不想要的。

我的一个想法是创建一些可能永远不会被搜索到的非常晦涩的分隔字符串(可能是外语),但我不能保证任何特定的字符串永远不会被搜索到,它只是似乎是一个非常笨拙的解决方案。基本上,我认为有更好的方法。
对于第一点的任何建议,我们将不胜感激。

我的第二个问题与如何处理重叠匹配有关。
比如同一个字符串My daughter was born on January 11, 2011.,如果输入的搜索是Jan anuar,那么会先匹配到Jan,得到:

My daughter was born on #####highlightStart#####Jan#####highlightEnd#####uary 11, 2011.

并且由于分隔文本现在是字符串的一部分,因此永远不会匹配第二个搜索词 anuar

关于这个问题,我很困惑,真的不知道如何解决。
我觉得我需要以某种方式分别对原始字符串执行所有搜索操作,然后以某种方式在最后将它们组合起来,但同样,我不知道如何执行此操作。
也许有更好的解决方案,但我不知道那会是什么。

任何关于如何解决其中一个或两个问题的建议或指导将不胜感激。
谢谢。

不要修改原始字符串并将匹配项存储在单独的数组中,以奇数元素开始并以偶数元素结束或将它们存储在记录中(包含两项的数组)。

在搜索了几个关键字之后,您最终得到了几个匹配的数组。所以现在的任务是如何合并两个段列表,生成覆盖这些区域的段。由于列表已排序,因此这是一项可以在 O(n) 时间内解决的微不足道的任务。

然后只需将高亮标记插入结果数组中记录的位置即可。

在这种情况下,我认为使用 str_replace 更简单(尽管它并不完美)。

假设您有一系列要突出显示的术语,为了便于讨论,我将其称为 $aSearchTerms...并将突出显示的术语包装在 HTML5 中<mark> 标签是可以接受的(为了易读性,你已经声明它在网页上并且很容易从你的搜索词中 strip_tags()):

$aSearchTerms = ['Jan', 'anu', 'Feb', '11'];
$sinContent = "My daughter was born on January 11, 2011.";

foreach($aSearchTerms as $sinTerm) {
    $sinContent = str_replace($sinTerm, "<mark>{$sinTerm}</mark>", $sinContent);
}

echo $sinContent;
// outputs: My d<mark>au</mark>ghter was born on <mark>Jan</mark>uary <mark>11</mark>, 20<mark>11</mark>.

它并不完美,因为使用该数组中的数据,第一遍会将 January 更改为 <mark>Jan</mark>uary,这意味着 anu 将不再匹配 January - 然而,像这样的内容将满足大多数使用需求。


编辑

Oki - 我不是 100% 确定这是理智的,但我采取了一种完全不同的方法来查看 link @AlexAtNet 发布的:

我所做的是查看字符串中以数字形式找到搜索词的点(索引),并构建了一个开始和结束索引数组,其中 <mark></mark> 标签将被输入。

然后使用上面的答案将这些开始和结束索引合并在一起 - 这涵盖了您的重叠匹配问题。

然后我循环那个数组并将原始字符串剪切成子字符串并将其粘在一起,在相关点(基于索引)插入 <mark></mark> 标记。这应该涵盖你的第二个问题,所以你没有用字符串替换替换字符串替换。

完整代码如下:

<?php
$sContent = "Captain's log, January 11, 2711 - Uranus";
$ainSearchTerms = array('Jan', 'asduih', 'anu', '11');

//lower-case it for substr_count
$sContentForSearching = strtolower($sContent);

//array of first and last positions of the terms within the string
$aTermPositions = array();

//loop through your search terms and build a multi-dimensional array
//of start and end indexes for each term
foreach($ainSearchTerms as $sinTerm) {

  //lower-case the search term
  $sinTermLower = strtolower($sinTerm);

  $iTermPosition = 0;
  $iTermLength = strlen($sinTermLower);
  $iTermOccursCount = substr_count($sContentForSearching, $sinTermLower);

  for($i=0; $i<$iTermOccursCount; $i++) {

    //find the start and end positions for this term
    $iStartIndex = strpos($sContentForSearching, $sinTermLower, $iTermPosition);
    $iEndIndex = $iStartIndex + $iTermLength;
    $aTermPositions[] = array($iStartIndex, $iEndIndex);

    //update the term position
    $iTermPosition = $iEndIndex + $i;
  }
}

//taken directly from this answer 
//just replaced $data with $aTermPositions
//this sorts out the overlaps so that 'Jan' and 'anu' will merge into 'Janu'
//in January - whilst still matching 'anu' in Uranus
//
//This conveniently sorts all your start and end indexes in ascending order
usort($aTermPositions, function($a, $b)
{
        return $a[0] - $b[0];
});

$n = 0; $len = count($aTermPositions);
for ($i = 1; $i < $len; ++$i)
{
        if ($aTermPositions[$i][0] > $aTermPositions[$n][1] + 1)
                $n = $i;
        else
        {
                if ($aTermPositions[$n][1] < $aTermPositions[$i][1])
                        $aTermPositions[$n][1] = $aTermPositions[$i][1];
                unset($aTermPositions[$i]);
        }
}

$aTermPositions = array_values($aTermPositions);

//finally chop your original string into the bits
//where you want to insert <mark> and </mark>
if($aTermPositions) {
    $iLastContentChunkIndex = 0;
    $soutContent = "";

    foreach($aTermPositions as $aChunkIndex) {
        $soutContent .= substr($sContent, $iLastContentChunkIndex, $aChunkIndex[0] - $iLastContentChunkIndex)
            . "<mark>" . substr($sContent, $aChunkIndex[0], $aChunkIndex[1] - $aChunkIndex[0]) . "</mark>";

        $iLastContentChunkIndex = $aChunkIndex[1];
    }

    //... and the bit on the end
    $soutContent .= substr($sContent, $iLastContentChunkIndex);
}

//this *should* output the following:
//Captain's log, <mark>Janu</mark>ary <mark>11</mark>, 27<mark>11</mark> - Ur<mark>anu</mark>s
echo $soutContent;

不可避免的陷阱! 在已经 HTML 的内容上使用它可能会严重失败。

给定字符串。

In <a href="#">January</a> this year...

Jan 的 search/mark 将在 'Jan' 周围插入 <mark>/</mark>,这很好。然而,像 In Jan 这样的搜索标记将会失败,因为在途中有标记:\

恐怕想不出一个好的解决办法。