突出显示 preg_match_all() 的主题字符串中的匹配结果

Posted

技术标签:

【中文标题】突出显示 preg_match_all() 的主题字符串中的匹配结果【英文标题】:Highlight match result in subject string from preg_match_all() 【发布时间】:2012-08-09 19:06:36 【问题描述】:

我正在尝试使用 preg_match_all() 返回的 $matches 数组突出显示主题字符串。让我从一个例子开始:

preg_match_all("/(.)/", "abc", $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);

这将返回:

Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => a
                    [1] => 0
                )

            [1] => Array
                (
                    [0] => a
                    [1] => 0
                )

        )

    [1] => Array
        (
            [0] => Array
                (
                    [0] => b
                    [1] => 1
                )

            [1] => Array
                (
                    [0] => b
                    [1] => 1
                )

        )

    [2] => Array
        (
            [0] => Array
                (
                    [0] => c
                    [1] => 2
                )

            [1] => Array
                (
                    [0] => c
                    [1] => 2
                )

        )

)

在这种情况下,我想做的是突出显示整体消耗的数据和每个反向引用。

输出应如下所示:

<span class="match0">
    <span class="match1">a</span>
</span>
<span class="match0">
    <span class="match1">b</span>
</span>
<span class="match0">
    <span class="match1">c</span>
</span>

另一个例子:

preg_match_all("/(abc)/", "abc", $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);

应该返回:

<span class="match0"><span class="match1">abc</span></span>

我希望这已经足够清楚了。

我想突出显示整体消耗的数据并突出显示每个反向引用。

提前致谢。如果有什么不清楚的地方请追问。

注意:它不能破坏 html。正则表达式 AND 输入字符串在代码中都是 未知完全动态。所以搜索字符串可以是html,匹配的数据可以包含类似html的文本等等。

【问题讨论】:

如果我到电脑前还是没有接听,我试试看。 在最后澄清您的注释,给定输入“an item”和正则表达式“/(.*?) /”,结果应该是“<ul><li>an item</li></ul>" ?因为我不确定是否有任何其他方法可以确保 HTML 输出正常 输出应该是这样的:&amp;lt;ul&amp;gt;&lt;span class="match0"&gt;&lt;span class="match1"&gt;&amp;lt;li&amp;gt;an item&lt;/span&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&amp;lt;/ul&amp;gt; 【参考方案1】:

到目前为止,这似乎对我抛出的所有示例都正确。请注意,为了在其他情况下的可重用性,我已经从 HTML 处理部分中打破了抽象突出显示部分:

<?php

/**
 * Runs a regex against a string, and return a version of that string with matches highlighted
 * the outermost match is marked with [0]...[/0], the first sub-group with [1]...[/1] etc
 *
 * @param string $regex Regular expression ready to be passed to preg_match_all
 * @param string $input
 * @return string
 */
function highlight_regex_matches($regex, $input)

    $matches = array();
    preg_match_all($regex, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);

    // Arrange matches into groups based on their starting and ending offsets
    $matches_by_position = array();
    foreach ( $matches as $sub_matches )
    
            foreach ( $sub_matches as $match_group => $match_data )
            
                    $start_position = $match_data[1];
                    $end_position = $start_position + strlen($match_data[0]);

                    $matches_by_position[$start_position]['START'][] = $match_group;

                    $matches_by_position[$end_position]['END'][] = $match_group;
            
    

    // Now proceed through that array, annotoating the original string
    // Note that we have to pass through BACKWARDS, or we break the offset information
    $output = $input;
    krsort($matches_by_position);
    foreach ( $matches_by_position as $position => $matches )
    
            $insertion = '';

            // First, assemble any ENDING groups, nested highest-group first
            if ( is_array($matches['END']) )
            
                    krsort($matches['END']);
                    foreach ( $matches['END'] as $ending_group )
                    
                            $insertion .= "[/$ending_group]";
                    
            

            // Then, any STARTING groups, nested lowest-group first
            if ( is_array($matches['START']) )
            
                    ksort($matches['START']);
                    foreach ( $matches['START'] as $starting_group )
                    
                            $insertion .= "[$starting_group]";
                    
            

            // Insert into output
            $output = substr_replace($output, $insertion, $position, 0);
    

    return $output;


/**
 * Given a regex and a string containing unescaped HTML, return a blob of HTML
 * with the original string escaped, and matches highlighted using <span> tags
 *
 * @param string $regex Regular expression ready to be passed to preg_match_all
 * @param string $input
 * @return string HTML ready to display :)
 */
function highlight_regex_as_html($regex, $raw_html)

    // Add the (deliberately non-HTML) highlight tokens
    $highlighted = highlight_regex_matches($regex, $raw_html);

    // Escape the HTML from the input
    $highlighted = htmlspecialchars($highlighted);

    // Substitute the match tokens with desired HTML
    $highlighted = preg_replace('#\[([0-9]+)\]#', '<span class="match\\1">', $highlighted);
    $highlighted = preg_replace('#\[/([0-9]+)\]#', '</span>', $highlighted);

    return $highlighted;

注意:正如 hakra 在聊天中向我指出的那样,如果正则表达式中的子组可以在一个整体匹配中出现多次(例如 '/a(b|c)+/ '), preg_match_all 只会告诉你最后一场比赛 - 所以highlight_regex_matches('/a(b|c)+/', 'abc') 返回'[0]ab[1]c[/1][/0]' 而不是'[0]a[1]b[/1][1]c[/1][/0]' 你可能期望/想要的。尽管如此,所有匹配组之外的所有匹配组仍然可以正常工作,因此highlight_regex_matches('/a((b|c)+)/', 'abc') 给出了'[0]a[1]b[2]c[/2][/1][/0]',这仍然是正则表达式匹配方式的一个很好的指示。

【讨论】:

是的,重复的子组是问题所在,这就是为什么我在回答中也链接了***.com/q/6371226/367456。我前段时间偶然发现了这个问题。只要那些不存在,它就“起作用”。但是,我有点确定让 OP 提出问题的部分问题是这些重复的子组 - 已知与否。无论如何 +1 这个答案。 啊,没有意识到那篇文章是关于这个的——它有点逃避阅读。 :|这实际上是 clusteringcapturing 之间的区别 - 您可以指定正则表达式的“集群”应以某种方式重复,但每对括号“捕获”单值。所以正则表达式 /(thing)+/ 只包含一个“捕获组”,它被分配给反向引用 1;该组匹配多次,但每次反向引用 1 都会被覆盖。 [这在 Perl 中也是一样的,顺便说一句] 简而言之,我的函数会显示“将被捕获的内容”,但它不能显示“正则表达式引擎如何处理字符串”(我猜你可以说它是一个正则表达式调试器,而不是一个正则表达式引擎调试器:P) 一些正则表达式 enines 可以返回 all 匹配项。例如在 Perl 中。在迄今为止尚未实现的 PHP 中。可能最终应该打开一个功能请求。 我很想知道你如何在 Perl 中做到这一点;据我所知,它在我提到的捕获组和 backrefs 之间具有 1:1 的关系,一切都从那里开始......【参考方案2】:

阅读您在第一个答案下的评论,我很确定您并没有真正按照您的意图提出问题。但是,按照您的具体要求:

$pattern = "/(.)/";
$subject = "abc";

$callback = function($matches) 
    if ($matches[0] !== $matches[1]) 
        throw new InvalidArgumentException(
            sprintf('you do not match thee requirements, go away: %s'
                    , print_r($matches, 1))
        );
    
    return sprintf('<span class="match0"><span class="match1">%s</span></span>'
                   , htmlspecialchars($matches[1]));
;
$result = preg_replace_callback($pattern, $callback, $subject);

在您现在开始抱怨之前,先看看您在描述问题时的不足之处。我有一种感觉,您实际上想要实际解析匹配结果。但是,您想做子匹配。除非您同时解析正则表达式以找出使用了哪些组,否则这不起作用。到目前为止,情况并非如此,在你的问题中也不是在这个答案中。

因此,请仅针对一个子组使用此示例,该子组也必须是整个模式作为要求。除此之外,这是完全动态的。

相关:

How to get all captures of subgroup matches with preg_match_all()? Ignore html tags in preg_replace

【讨论】:

我很困惑,为什么$matches[0]$matches[1] 需要匹配?这个问题的意图对我来说非常清楚。虽然使用起来很繁琐,但我认为所需的所有数据都在preg_match_all返回的捕获数据中@ 它们需要匹配,因为这是本示例的要求。缺点在答案中进行了说明。其他一切都需要一个 PHP 正则表达式解析器,恕我直言,它实际上不可能作为答案,因为它还不存在。 PCRE 语法也很重。 我在任何地方都没有看到这个要求。并且捕获组的列表是已知的,这正是 preg_match_all 返回的内容。 答案中给出了要求:“所以请仅对一个子组进行此示例,该子组也必须是整个模式作为要求。除此之外,这是完全动态的。” 阅读。它在答案中。 - 这一个子组作为整个模式完全省去了我所说的子模式的解析。 哦,我明白了,我以为你的意思是这是问题的要求。不过,我不相信您需要按照您建议的方式对正则表达式进行逆向工程 - preg_match_all 使用的标志可为您提供所需的所有数据。【参考方案3】:

我对在 *** 上发帖不太熟悉,所以我希望我不会把这件事搞砸。我这样做的方式与@IMSoP 几乎相同,但略有不同:

我这样存储标签:

$tags[ $matched_pos ]['open'][$backref_nr] = "open tag";
$tags[ $matched_pos + $len ]['close'][$backref_nr] = "close tag";

如您所见,与@IMSoP 几乎相同。

然后我像这样构造字符串,而不是像 @IMSoP 那样插入和排序:

$finalStr = "";
for ($i = 0; $i <= strlen($text); $i++) 
    if (isset($tags[$i])) 
        foreach ($tags[$i] as $tag) 
            foreach ($tag as $span) 
                $finalStr .= $span;
            
        
    
    $finalStr .= $text[$i];

其中$textpreg_match_all() 中使用的文本

认为我的解决方案比@IMSoP 的解决方案稍微快一些,因为他每次都必须进行排序,什么都不需要。但我不确定。

我现在主要担心的是性能。但它可能无法让它比这更快地工作吗?

我一直试图让递归的preg_replace_callback() 事情顺利进行,但到目前为止我还不能让它工作。 preg_replace_callback() 似乎非常非常快。无论如何,比我现在做的要快得多。

【讨论】:

就性能而言,字符串操作是 PHP 中最昂贵的操作之一。您的解决方案需要更多字符串连接作为原始字符串长度的一个因素,而我的解决方案执行相对于匹配数量的插入。 ksort 电话可能会被排除在我的解决方案之外。但是,反转结束标签的krsort 至关重要,否则输出将无法正确嵌套。目前尚不清楚您的版本是否处理此问题 - '[1]a[2]b[/1][/2]' 应该是 '[1]a[2]b[/2][/1]' 是的,你是对的。但实际上它没有区别,因为结束标签总是相同的 (&lt;/span&gt;)。所以你的[/2][/1] 在现实世界中都是&lt;/span&gt;,因此它们是否有序并不重要。我将尝试实现您的版本,看看它是如何进行的。一旦我得到它的工作,我会报告。 好的。我已经试过了。在由 'a' 组成的约 500 字符长的字符串上运行 /(.)/ 您的代码在我的计算机上执行大约 7.81 秒(是的,慢速计算机)。除了最初的krsort 之外,所有排序都被删除了。我的代码花了 5.7 秒。虽然它们都很慢,但我的实际上更快。我没有测试过更大的字符串,因为在我的计算机上执行这两种情况下的代码都需要很长时间。我在想 substr_replace() 在后台做了一些可疑/缓慢的事情。我希望有一个递归的preg_replace_callback()-solution。 是的,我在发表评论后才意识到结束标签。您的测试不是特别公平,因为它使匹配数是初始字符串中字符数的两倍。虽然不是为了速度而写的,但我希望我的版本对于在 500 个字符的字符串中匹配 2 或 3 次的模式会更快。 ~2300 个字符。你的:9.19s。我的:5.54 秒。我也认为你的应该更快,但由于某种原因它不是......你知道怎么回事吗?对于任何普通的旧表达式和主题字符串,它们都可以正常工作,但对于这种“最坏情况”的情况,它们都非常糟糕。我希望有人想出一个巧妙的解决方案,哈哈。【参考方案4】:

快速混搭,为什么要使用正则表达式?

$content = "abc";
$endcontent = "";

for($i = 0; $i > strlen($content); $i++)

    $endcontent .= "<span class=\"match0\"><span class=\"match1\">" . $content[$i] . "</span></span>";


echo $endcontent;

【讨论】:

我正在创建一个正则表达式服务。用户正则表达式和主题字符串将被使用并通过 preg_match_all() 运行。我想根据返回的 $matches 数组突出显示主题字符串中的匹配结果。你明白吗?

以上是关于突出显示 preg_match_all() 的主题字符串中的匹配结果的主要内容,如果未能解决你的问题,请参考以下文章

markdown 禁用Argento2主题编辑器突出显示

Eclipse Py-dev,深色主题在键入时删除文本突出显示?

在 Sublime Text 3 中更改突出显示颜色?

如何在 Rider 中更改自动完成突出显示颜色

PHP/REGEX:获取括号内的字符串

Datagrip 中这些突出显示的栏是啥?