文章摘要、列表頁截取內容時,若直接 substr 截斷 HTML 字串,極易導致標籤未閉合、版面崩壞。本函數可在指定長度截斷時,自動補全未閉合標籤。
~(<[^>]+?>)~si 將 HTML 拆分為「標籤」與「純文字」片段。$tagStack 堆疊:遇到開始標籤 <tag> 入堆,遇到結束標籤 </tag> 匹配出堆,單標籤 <br/> 忽略。<?php
/**
* 截取 HTML 並自動補全閉合標籤
* @param string $html 原始 HTML
* @param int $length 最大文字長度(不含標籤)
* @return string 處理後 HTML
*/
function subHtml($html, $length = 50) {
$result = '';
$tagStack = [];
$len = 0;
// 分割:保留分隔符(標籤)
$contents = preg_split("~(<[^>]+?>)~si", $html, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
foreach ($contents as $tag) {
if (trim($tag) === '') continue;
// 單閉合標籤
...
if (preg_match("~<([a-z0-9]+)[^/>]*?/>~si", $tag)) {
$result .= $tag;
}
// 結束標籤
elseif (preg_match("~</([a-z0-9]+)[^/>]*?>~si", $tag, $match)) {
if (!empty($tagStack) && $tagStack[count($tagStack)-1] === $match[1]) {
array_pop($tagStack);
$result .= $tag;
}
}
// 開始標籤
elseif (preg_match("~<([a-z0-9]+)[^/>]*?>~si", $tag, $match)) {
array_push($tagStack, $match[1]);
$result .= $tag;
}
// 註解
elseif (preg_match("~<!--.*?-->~si", $tag)) {
$result .= $tag;
}
// 純文字
else {
if ($len + mstrlen($tag) < $length) {
$result .= $tag;
$len += mstrlen($tag);
} else {
$result .= msubstr($tag, 0, $length - $len + 1);
break;
}
}
}
// 補全剩餘未閉合標籤
while (!empty($tagStack)) {
$result .= '</' . array_pop($tagStack) . '>';
}
return $result;
}
/**
* 多位元組安全截取字串
*/
function msubstr($string, $start, $length, $dot = '', $charset = 'UTF-8') {
$string = str_replace(['&', '"', '<', '>', ' '], ['&', '"', '<', '>', ' '], $string);
if (mb_strlen($string, $charset) <= $length) return $string;
if (strtolower($charset) === 'utf-8') {
$n = $tn = $noc = 0;
while ($n < strlen($string)) {
$t = ord($string[$n]);
if ($t <= 127) { $tn = 1; $n++; }
elseif ($t <= 223) { $tn = 2; $n += 2; }
elseif ($t <= 239) { $tn = 3; $n += 3; }
elseif ($t <= 247) { $tn = 4; $n += 4; }
elseif ($t <= 251) { $tn = 5; $n += 5; }
elseif ($t <= 253) { $tn = 6; $n += 6; }
else { $n++; }
$noc++;
if ($noc >= $length) break;
}
if ($noc > $length) $n -= $tn;
$strcut = substr($string, 0, $n);
} else {
$strcut = '';
for ($i = 0; $i < $length; $i++) {
$strcut .= (ord($string[$i]) > 127) ? $string[$i] . $string[++$i] : $string[$i];
}
}
return $strcut . $dot;
}
/**
* 取得字串長度(中英文皆計 1)
*/
function mstrlen($str, $charset = 'UTF-8') {
if (function_exists('mb_strlen')) return mb_strlen($str, $charset);
if (function_exists('iconv_strlen')) return iconv_strlen($str, $charset);
preg_match_all('/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|\xe0[\xa0-\xbf][\x80-\xbf]|[\xe1-\xef][\x80-\xbf][\x80-\xbf]|\xf0[\x90-\xbf][\x80-\xbf][\x80-\xbf]|[\xf1-\xf7][\x80-\xbf][\x80-\xbf][\x80-\xbf]/', $str, $ar);
return count($ar[0]);
}
// 測試
$str = "<div><table>x<tr>1s<td>測試<td>124";
echo subHtml($str, 10); // 輸出:x1s測試
?>
mb_substr/mb_strlen 簡化。<script>/<style> 內容,若需保留請自行擴展。