截断包含HTML的文本,忽略标签

我想截断一些文本(从数据库或文本文件加载),但它包含HTML,因此标签被包括在内,文本将被返回。 这可能会导致标签不被closures,或者部分closures(所以Tidy可能无法正常工作,而且内容更less)。 如何根据文本截断(并且可能在到达表格时停止,因为这可能导致更复杂的问题)。

substr("Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;ma web developer.",0,26)."..." 

会导致:

 Hello, my <strong>name</st... 

我想要的是:

 Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;m... 

我该怎么做?

虽然我的问题是如何在PHP中做到这一点,这将是很好的知道如何在C#中做…或者应该是好的,因为我认为我将能够移植的方法(除非它是一个内置的方法)。

另外请注意,我已经包含了一个HTML实体&acute; – 必须将其视为单个字符(而不是本例中的7个字符)。

strip_tags是一个后备,但我会失去格式和链接,它仍然有HTML实体的问题。

假设您使用的是有效的XHTML,那么parsingHTML并确保正确处理标签很简单。 你只需要跟踪到底打开了哪些标签,并确保在“出门”时再次closures它们。

 <?php header('Content-type: text/plain; charset=utf-8'); function printTruncated($maxLength, $html, $isUtf8=true) { $printedLength = 0; $position = 0; $tags = array(); // For UTF-8, we need to count multibyte sequences as one character. $re = $isUtf8 ? '{</?([az]+)[^>]*>|&#?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}' : '{</?([az]+)[^>]*>|&#?[a-zA-Z0-9]+;}'; while ($printedLength < $maxLength && preg_match($re, $html, $match, PREG_OFFSET_CAPTURE, $position)) { list($tag, $tagPosition) = $match[0]; // Print text leading up to the tag. $str = substr($html, $position, $tagPosition - $position); if ($printedLength + strlen($str) > $maxLength) { print(substr($str, 0, $maxLength - $printedLength)); $printedLength = $maxLength; break; } print($str); $printedLength += strlen($str); if ($printedLength >= $maxLength) break; if ($tag[0] == '&' || ord($tag) >= 0x80) { // Pass the entity or UTF-8 multibyte sequence through unchanged. print($tag); $printedLength++; } else { // Handle the tag. $tagName = $match[1][0]; if ($tag[1] == '/') { // This is a closing tag. $openingTag = array_pop($tags); assert($openingTag == $tagName); // check that tags are properly nested. print($tag); } else if ($tag[strlen($tag) - 2] == '/') { // Self-closing tag. print($tag); } else { // Opening tag. print($tag); $tags[] = $tagName; } } // Continue after the tag. $position = $tagPosition + strlen($tag); } // Print any remaining text. if ($printedLength < $maxLength && $position < strlen($html)) print(substr($html, $position, $maxLength - $printedLength)); // Close any open tags. while (!empty($tags)) printf('</%s>', array_pop($tags)); } printTruncated(10, '<b>&lt;Hello&gt;</b> <img src="world.png" alt="" /> world!'); print("\n"); printTruncated(10, '<table><tr><td>Heck, </td><td>throw</td></tr><tr><td>in a</td><td>table</td></tr></table>'); print("\n"); printTruncated(10, "<em><b>Hello</b>&#20;w\xC3\xB8rld!</em>"); print("\n"); 

编码注意 :上面的代码假设XHTML是UTF-8编码的。 ASCII兼容的单字节编码(如Latin-1 )也被支持,只是传递false作为第三个参数。 其他多字节编码不受支持,尽pipe您可能在调用函数之前使用mb_convert_encoding转换为UTF-8,然后在每个print语句中再次转换回来。

(不过你应该总是使用UTF-8。)

编辑 :更新处理字符实体和UTF-8。 修正了如果该字符是字符实体,该函数将打印一个字符太多的错误。

100%准确,但相当困难的做法:

  1. 使用DOM迭代字符
  2. 使用DOM方法删除剩余的元素
  3. 序列化DOM

简单的暴力方法:

  1. 使用preg_split('/(<tag>)/')preg_split('/(<tag>)/')拆分为标签(不是元素)和文本片段。
  2. 测量你想要的文本长度(这将是每隔一秒的元素,你可以使用html_entity_decode()来帮助准确测量)
  3. 删除string(trim &[^\s;]+$在最后摆脱可能切碎的实体)
  4. 用HTML Tidy修复它

我已经写了一个截断HTML的函数,正如你所build议的那样,但不是将其打印出来,而是将其全部保存在一个stringvariables中。 也处理HTML实体。

  /** * function to truncate and then clean up end of the HTML, * truncates by counting characters outside of HTML tags * * @author alex lockwood, alex dot lockwood at websightdesign * * @param string $str the string to truncate * @param int $len the number of characters * @param string $end the end string for truncation * @return string $truncated_html * * **/ public static function truncateHTML($str, $len, $end = '&hellip;'){ //find all tags $tagPattern = '/(<\/?)([\w]*)(\s*[^>]*)>?|&[\w#]+;/i'; //match html tags and entities preg_match_all($tagPattern, $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ); //WSDDebug::dump($matches); exit; $i =0; //loop through each found tag that is within the $len, add those characters to the len, //also track open and closed tags // $matches[$i][0] = the whole tag string --the only applicable field for html enitities // IF its not matching an &htmlentity; the following apply // $matches[$i][1] = the start of the tag either '<' or '</' // $matches[$i][2] = the tag name // $matches[$i][3] = the end of the tag //$matces[$i][$j][0] = the string //$matces[$i][$j][1] = the str offest while($matches[$i][0][1] < $len && !empty($matches[$i])){ $len = $len + strlen($matches[$i][0][0]); if(substr($matches[$i][0][0],0,1) == '&' ) $len = $len-1; //if $matches[$i][2] is undefined then its an html entity, want to ignore those for tag counting //ignore empty/singleton tags for tag counting if(!empty($matches[$i][2][0]) && !in_array($matches[$i][2][0],array('br','img','hr', 'input', 'param', 'link'))){ //double check if(substr($matches[$i][3][0],-1) !='/' && substr($matches[$i][1][0],-1) !='/') $openTags[] = $matches[$i][2][0]; elseif(end($openTags) == $matches[$i][2][0]){ array_pop($openTags); }else{ $warnings[] = "html has some tags mismatched in it: $str"; } } $i++; } $closeTags = ''; if (!empty($openTags)){ $openTags = array_reverse($openTags); foreach ($openTags as $t){ $closeTagString .="</".$t . ">"; } } if(strlen($str)>$len){ // Finds the last space from the string new length $lastWord = strpos($str, ' ', $len); if ($lastWord) { //truncate with new len last word $str = substr($str, 0, $lastWord); //finds last character $last_character = (substr($str, -1, 1)); //add the end text $truncated_html = ($last_character == '.' ? $str : ($last_character == ',' ? substr($str, 0, -1) : $str) . $end); } //restore any open tags $truncated_html .= $closeTagString; }else $truncated_html = $str; return $truncated_html; } 

在这种情况下,可能会使用DomDocument和一个令人讨厌的正则expression式黑客,最糟糕的是会发生一个警告,如果有一个破碎的标签:

 $dom = new DOMDocument(); $dom->loadHTML(substr("Hello, my <strong>name</strong> is <em>Sam</em>. I&acute;ma web developer.",0,26)); $html = preg_replace("/\<\/?(body|html|p)>/", "", $dom->saveHTML()); echo $html; 

应该给出输出: Hello, my <strong>**name**</strong>

反弹为SørenLøvborg的解决scheme增加了多字节字符支持 – 我添加了:

  • 支持不成对的HTML标记(例如<hr><br> <col>等不会被closures – 在HTML中,'/'不是必须的(尽pipe在XHTML中是这样)),
  • 可定制截断指示器(默认为&hellips; ie …),
  • 作为一个string返回而不使用输出缓冲区
  • unit testing覆盖率达到100%。

所有这些在Pastie 。

你也可以使用整齐 :

 function truncate_html($html, $max_length) { return tidy_repair_string(substr($html, 0, $max_length), array('wrap' => 0, 'show-body-only' => TRUE), 'utf8'); } 

以下是一个简单的状态机parsing器,它可以成功处理你的testing用例。 我失败了嵌套标签,因为它不跟踪标签本身。 我也扼杀了HTML标签中的实体(例如,在<a> -tag的href属性中)。 所以它不能被认为是解决这个问题的100%解决scheme,但是因为它很容易理解,所以它可能成为更高级function的基础。

 function substr_html($string, $length) { $count = 0; /* * $state = 0 - normal text * $state = 1 - in HTML tag * $state = 2 - in HTML entity */ $state = 0; for ($i = 0; $i < strlen($string); $i++) { $char = $string[$i]; if ($char == '<') { $state = 1; } else if ($char == '&') { $state = 2; $count++; } else if ($char == ';') { $state = 0; } else if ($char == '>') { $state = 0; } else if ($state === 0) { $count++; } if ($count === $length) { return substr($string, 0, $i + 1); } } return $string; } 

我对SørenLøvborg的printTruncated函数做了轻微的改变,使UTF-8兼容:

  /* Truncate HTML, close opened tags * * @param int, maxlength of the string * @param string, html * @return $html */ function html_truncate($maxLength, $html){ mb_internal_encoding("UTF-8"); $printedLength = 0; $position = 0; $tags = array(); ob_start(); while ($printedLength < $maxLength && preg_match('{</?([az]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position)){ list($tag, $tagPosition) = $match[0]; // Print text leading up to the tag. $str = mb_strcut($html, $position, $tagPosition - $position); if ($printedLength + mb_strlen($str) > $maxLength){ print(mb_strcut($str, 0, $maxLength - $printedLength)); $printedLength = $maxLength; break; } print($str); $printedLength += mb_strlen($str); if ($tag[0] == '&'){ // Handle the entity. print($tag); $printedLength++; } else{ // Handle the tag. $tagName = $match[1][0]; if ($tag[1] == '/'){ // This is a closing tag. $openingTag = array_pop($tags); assert($openingTag == $tagName); // check that tags are properly nested. print($tag); } else if ($tag[mb_strlen($tag) - 2] == '/'){ // Self-closing tag. print($tag); } else{ // Opening tag. print($tag); $tags[] = $tagName; } } // Continue after the tag. $position = $tagPosition + mb_strlen($tag); } // Print any remaining text. if ($printedLength < $maxLength && $position < mb_strlen($html)) print(mb_strcut($html, $position, $maxLength - $printedLength)); // Close any open tags. while (!empty($tags)) printf('</%s>', array_pop($tags)); $bufferOuput = ob_get_contents(); ob_end_clean(); $html = $bufferOuput; return $html; } 

SørenLøvborgprintTruncated函数的另一个亮点变为UTF-8(需要mbstring)兼容,并使其返回string不能打印。 我认为这更有用。 而我的代码不使用缓冲像Bounce变种,只是一个variables。

UPD:使用utf-8字符在标记属性中正常工作,需要mb_preg_match函数,如下所示。

非常感谢SørenLøvborg的这个function,非常好。

 /* Truncate HTML, close opened tags * * @param int, maxlength of the string * @param string, html * @return $html */ function htmlTruncate($maxLength, $html) { mb_internal_encoding("UTF-8"); $printedLength = 0; $position = 0; $tags = array(); $out = ""; while ($printedLength < $maxLength && mb_preg_match('{</?([az]+)[^>]*>|&#?[a-zA-Z0-9]+;}', $html, $match, PREG_OFFSET_CAPTURE, $position)) { list($tag, $tagPosition) = $match[0]; // Print text leading up to the tag. $str = mb_substr($html, $position, $tagPosition - $position); if ($printedLength + mb_strlen($str) > $maxLength) { $out .= mb_substr($str, 0, $maxLength - $printedLength); $printedLength = $maxLength; break; } $out .= $str; $printedLength += mb_strlen($str); if ($tag[0] == '&') { // Handle the entity. $out .= $tag; $printedLength++; } else { // Handle the tag. $tagName = $match[1][0]; if ($tag[1] == '/') { // This is a closing tag. $openingTag = array_pop($tags); assert($openingTag == $tagName); // check that tags are properly nested. $out .= $tag; } else if ($tag[mb_strlen($tag) - 2] == '/') { // Self-closing tag. $out .= $tag; } else { // Opening tag. $out .= $tag; $tags[] = $tagName; } } // Continue after the tag. $position = $tagPosition + mb_strlen($tag); } // Print any remaining text. if ($printedLength < $maxLength && $position < mb_strlen($html)) $out .= mb_substr($html, $position, $maxLength - $printedLength); // Close any open tags. while (!empty($tags)) $out .= sprintf('</%s>', array_pop($tags)); return $out; } function mb_preg_match( $ps_pattern, $ps_subject, &$pa_matches, $pn_flags = 0, $pn_offset = 0, $ps_encoding = NULL ) { // WARNING! - All this function does is to correct offsets, nothing else: //(code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER) if (is_null($ps_encoding)) $ps_encoding = mb_internal_encoding(); $pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding)); $ret = preg_match($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset); if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE)) foreach($pa_matches as &$ha_match) { $ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding); } return $ret; } 

CakePHP框架在TextHelper中有一个支持HTML的truncate()函数。 请参阅Core-Helpers / Text 。 MIT许可证。

如果不使用validation器和parsing器,这是非常困难的,原因是如果有的话

 <div id='x'> <div id='y'> <h1>Heading</h1> 500 lines of html ... etc ... </div> </div> 

你如何计划截断,并最终有效的HTML?

经过简短的search,我发现这个链接可以帮助。