在字符串中关闭未闭合的HTML标签

22

Situation是一个字符串,其结果类似于此:

<p>This is some text and here is a <strong>bold text then the post stop here....</p>

因为函数返回文本的摘要(概述),所以它会在一定的词数后停止。而这种情况下,strong标签没有关闭。但整个字符串被包含在一个段落中。

是否有可能将上述结果/输出转换为以下内容:

<p>This is some text and here is a <strong>bold text then the post stop here....</strong></p>

我不知道从哪里开始。问题是..我在网络上找到了一个使用正则表达式的函数,但它将关闭标签放在字符串之后..因此,它无法通过验证,因为我希望所有开/闭标签都在段落标签内。我找到的函数也是错误的:

<p>This is some text and here is a <strong>bold text then the post stop here....</p></strong>

我想知道标签可以是加粗、斜体或其他任何样式。因此,我不能在函数中手动追加标签和关闭标签。有什么模式可以替我完成吗?


你不能在你的预览函数中添加闭合标签吗? - Ruel
预览器从用户处获取HTML(格式化)输入。我不能添加它,因为标签可能是其他任何东西...斜体...加粗等等。我认为最好的方法是在输出末尾搜索开放标记,然后将其关闭... - Ahmad Fouad
10个回答

47

这是我以前使用过的一个函数,它非常有效:

function closetags($html) {
    preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
    $openedtags = $result[1];
    preg_match_all('#</([a-z]+)>#iU', $html, $result);
    $closedtags = $result[1];
    $len_opened = count($openedtags);
    if (count($closedtags) == $len_opened) {
        return $html;
    }
    $openedtags = array_reverse($openedtags);
    for ($i=0; $i < $len_opened; $i++) {
        if (!in_array($openedtags[$i], $closedtags)) {
            $html .= '</'.$openedtags[$i].'>';
        } else {
            unset($closedtags[array_search($openedtags[$i], $closedtags)]);
        }
    }
    return $html;
} 

个人而言,我不会使用正则表达式来完成这件事,而是会使用像Tidy这样的库。下面是一个类似的示例:

$str = '<p>This is some text and here is a <strong>bold text then the post stop here....</p>';
$tidy = new Tidy();
$clean = $tidy->repairString($str, array(
    'output-xml' => true,
    'input-xml' => true
));
echo $clean;

你知道!那个正则表达式做到了...没有任何验证错误或其他问题。 - Ahmad Fouad
是的,那个正则表达式非常有效,如果Tidy不可用,那是一个很好的替代方案。 - alexn
3
非常感谢您向我介绍 Tidy。它真是太棒了! - user651390
@alexn,您能否详细说明为什么要使用output-xml而不是output-htmloutput-xhtml进行转换? - nirvana-msu
很好,但是对于那些有多个字符的标签呢?比如<h1> <span>等。我认为closetags函数无法关闭它们。 - MikeBau
显示剩余2条评论

10

对原回答进行了微小的修改...虽然原回答正确地去除了标签,但我发现在我截断文本时会导致标签被割裂。例如:

This text has some <b>in it</b>

截取前21个字符的结果为:

This text has some <
下面的代码基于最佳答案,并修复了这个问题。
function truncateHTML($html, $length)
{
    $truncatedText = substr($html, $length);
    $pos = strpos($truncatedText, ">");
    if($pos !== false)
    {
        $html = substr($html, 0,$length + $pos + 1);
    }
    else
    {
        $html = substr($html, 0,$length);
    }

    preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
    $openedtags = $result[1];

    preg_match_all('#</([a-z]+)>#iU', $html, $result);
    $closedtags = $result[1];

    $len_opened = count($openedtags);

    if (count($closedtags) == $len_opened)
    {
        return $html;
    }

    $openedtags = array_reverse($openedtags);
    for ($i=0; $i < $len_opened; $i++)
    {
        if (!in_array($openedtags[$i], $closedtags))
        {
            $html .= '</'.$openedtags[$i].'>';
        }
        else
        {
            unset($closedtags[array_search($openedtags[$i], $closedtags)]);
        }
    }


    return $html;
}


$str = "This text has <b>bold</b> in it</b>";
print "Test 1 - Truncate with no tag: " . truncateHTML($str, 5) . "<br>\n";
print "Test 2 - Truncate at start of tag: " . truncateHTML($str, 20) . "<br>\n";
print "Test 3 - Truncate in the middle of a tag: " . truncateHTML($str, 16) . "<br>\n";
print "Test 4: - Truncate with less text: " . truncateHTML($str, 300) . "<br>\n";

希望它能帮助到那些需要的人。


@Marcus,我想了解一下在preg_match_All()函数中$result参数的初始化。 - Techy
很久以前我在使用Joomla时,现在我已经不记得结果了。 - Sayed

5

那么使用PHP的本地DOMDocument类呢?它可以解析HTML并纠正语法错误... 例如:

$fragment = "<article><h3>Title</h3><p>Unclosed";
$doc = new DOMDocument();
$doc->loadHTML($fragment);
$correctFragment = $doc->getElementsByTagName('body')->item(0)->C14N();
echo $correctFragment;

然而,这种方法有几个缺点。首先,它将原始片段包装在<body>标记内。您可以通过像(preg_)replace()这样的方式轻松地摆脱它,或者通过将...->C14N()函数替换为一些自定义的innerHTML()函数,如http://php.net/manual/en/book.dom.php#89718所建议的那样。 第二个陷阱是,如果使用HTML5或自定义标记,PHP会抛出“实体中的无效标记”警告(尽管它仍然会正确进行)。


请注意,此 C14n() 函数会破坏多字节字符串。 - WoodrowShigeru
1
更正:loadHTML()已经不支持多字节字符串/UTF-8。请使用这个额外的步骤:@$doc->loadHTML(mb_convert_encoding($fragment, 'HTML-ENTITIES', "UTF-8")); - WoodrowShigeru

4

这个PHP方法一直对我有效。它将关闭所有未关闭的HTML标签。

function closetags($html) {
    preg_match_all('#<([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
    $openedtags = $result[1];

    preg_match_all('#</([a-z]+)>#iU', $html, $result);
    $closedtags = $result[1];
    $len_opened = count($openedtags);
    if (count($closedtags) == $len_opened) {
        return $html;
    }
    $openedtags = array_reverse($openedtags);
    for ($i=0; $i < $len_opened; $i++) {
        if (!in_array($openedtags[$i], $closedtags)){
            $html .= '</'.$openedtags[$i].'>';
        } else {
            unset($closedtags[array_search($openedtags[$i], $closedtags)]);
        }
    }
    return $html;
}

3

您的问题没有涵盖到需要解决的其他许多变量,因此无法提供完整的解决方案。

不过,我建议使用类似于HTML Tidy这样的工具,特别是利用它的repairFilerepaireString方法来进行修复。


问题在于字符串以HTML格式输入,有时摘要存在一个或多个标签未关闭,因为关闭标签在完整文章中。我只想在摘要结束之前关闭这些未关闭的标签。认为正则表达式可以做到这一点。 - Ahmad Fouad
您可以使用HTML Tidy选项。当访问HTML元素时,正则表达式并不受欢迎,因为解析HTML太不规则了,并且在正则表达式中涵盖所有其特殊性是一项巨大的任务。我建议您至少尝试一下HTML Tidy选项... - Russell Dias

1
如果已安装整洁模块,请使用 PHP 整洁扩展:
tidy_repair_string($html)

参考资料


0

使用解析HTML的最新解决方案如下:

function fix_html($html) {
    $dom = new DOMDocument();
    $dom->loadHTML( mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
    return $dom->saveHTML();
}

LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD是为了避免实现doctype、html和body而需要的.. 其余部分看起来非常明显 :)

更新: 经过一些测试发现,上面的解决方案一次又一次地破坏了正确的布局。下面的解决方案可以很好地解决这个问题:

function fix_html($html) {
    $dom = new DOMDocument();
    $dom->loadHTML( mb_convert_encoding( $html, 'HTML-ENTITIES', 'UTF-8' ) );
    $return = '';
    foreach ( $dom->getElementsByTagName( 'body' )->item(0)->childNodes as $v ) {
        $return .= $dom->saveHTML( $v );
    }
    return $return;
}

0

这对我来说是有效的,可以在脚本中关闭任何打开的HTML标签。

<?php
function closetags($html) {
preg_match_all('#<([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
$openedtags = $result[1];
preg_match_all('#</([a-z]+)>#iU', $html, $result);
$closedtags = $result[1];
$len_opened = count($openedtags);
if (count($closedtags) == $len_opened) {
    return $html;
}
$openedtags = array_reverse($openedtags);
for ($i=0; $i < $len_opened; $i++) {
    if (!in_array($openedtags[$i], $closedtags)) {
        $html .= '</'.$openedtags[$i].'>';
    } else {
        unset($closedtags[array_search($openedtags[$i], $closedtags)]);
    }
}
return $html;
}

0

使用正则表达式并不是处理这个问题的理想方法。相反,你应该使用 HTML 解析器来创建一个有效的文档对象模型。

作为第二个选项,根据你的需求,你可以使用正则表达式从字符串中删除所有的 HTML 标签,然后再把它放到 <p> 标签中。


为什么在标记了 PHP 的情况下,你会建议使用 Python 中的 HTML 解析器,而不是 PHP 的替代方案呢? - Russell Dias

0

我已经完成了这段代码,它可以正确地完成工作...

虽然这是老派的方法,但它很有效,并且我已经添加了一个标志来删除未完成的标签,例如“blah blah http://stackoverfl”

public function getOpennedTags(&$string, $removeInclompleteTagEndTagIfExists = true) {

    $tags = array();
    $tagOpened = false;
    $tagName = '';
    $tagNameLogged = false;
    $closingTag = false;

    foreach (str_split($string) as $c) {
        if ($tagOpened && $c == '>') {
            $tagOpened = false;
            if ($closingTag) {
                array_pop($tags);
                $closingTag = false;
                $tagName = '';
            }
            if ($tagName) {
                array_push($tags, $tagName);
            }
        }
        if ($tagOpened && $c == ' ') {
            $tagNameLogged = true;
        }
        if ($tagOpened && $c == '/') {
            if ($tagName) {
                //orphan tag
                $tagOpened = false;
                $tagName = '';
            } else {
                //closingTag
                $closingTag = true;
            }
        }
        if ($tagOpened && !$tagNameLogged) {
            $tagName .= $c;
        }
        if (!$tagOpened && $c == '<') {
            $tagNameLogged = false;
            $tagName = '';
            $tagOpened = true;
            $closingTag = false;
        }
    }

    if ($removeInclompleteTagEndTagIfExists && $tagOpened) {
        // an tag has been cut for exemaple ' blabh blah <a href="sdfoefzofk' so closing the tag will not help...
        // let's remove this ugly piece of tag
        $pos = strrpos($string, '<');
        $string = substr($string, 0, $pos);
    }

    return $tags;
}

使用示例:

$tagsToClose = $stringHelper->getOpennedTags($val);
$tagsToClose = array_reverse($tagsToClose);

foreach ($tagsToClose as $tag) {
    $val .= "</$tag>";
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接