需求:從包含 HTML 標籤的中英混合字串中,按**可見字元長度**截取,標籤不計長度,且輸出結果標籤結構完整(自動補全未閉合標籤)。提供兩種實作。
function mysubstr($str, $length) {
$tagcnt = 0; $charcnt = 0; $tag = '';
$maxlen = strlen($str);
$result = '';
$tagstack = [];
for ($i = 0; $i < $maxlen && $charcnt < $length; $i++) {
if ($str[$i] === '<') {
$result .= $str[$i];
// 讀取完整標籤
for ($j = $i; $j < $maxlen && $str[$j] !== '>'; $j++) {
$tag .= $str[$j];
}
$tag .= '>';
$result .= $tag;
$i = $j; // 跳過標籤內容
// 標籤堆疊處理
if (preg_match('/<([^\/]+?)>/i', $tag, $m)) {
array_push($tagstack, $m[1]); // 開始標籤入堆疊
} elseif (preg_match('/<\/'.preg_quote(end($tagstack), '/').'>/i', $tag)) {
array_pop($tagstack); // 結束標籤出堆疊
}
$tag = '';
continue;
}
$charcnt++;
$result .= $str[$i];
}
// 補全未閉合標籤(逆序關閉)
while (!empty($tagstack)) {
$t = array_pop($tagstack);
$result .= "$t>";
}
return $result;
}
function html_substr($str, $len, $mode = 0) {
// 分割:保留標籤為獨立元素
$parts = preg_split('/(|<[^>]*>)/s', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $k => $v) {
if ($v[0] !== '<') { // 純文字
$len -= strlen($v);
if ($len < 0) {
$parts[$k] = substr($v, 0, $len + strlen($v)); // 截取文字部分
$len = 0;
}
} else {
$parts[$k] = strtolower($v); // 標籤轉小寫便於比對
}
if ($len <= 0) break;
}
$parts = array_slice($parts, 0, $k + 1);
// 處理未閉合標籤
if ($mode === 1) { // 模式1:補全
$openTags = [];
foreach ($parts as $v) {
if (preg_match('/<([a-z]+)[^>]*>/i', $v, $m) && !preg_match('/<\//', $v)) {
$openTags[] = $m[1];
} elseif (preg_match('/<\/([a-z]+)>/i', $v, $m)) {
$pos = array_search($m[1], $openTags);
if ($pos !== false) unset($openTags[$pos]);
}
}
foreach (array_reverse($openTags) as $t) {
$parts[] = "$t>";
}
} else { // 模式0:移除未閉合開始標籤
// 略
}
return join('', $parts);
}
$str = "123abc456def789";
echo html_substr($str, 5); // 123ab (移除未閉合)
echo html_substr($str, 5, 1); // 123ab (補全)
Masterminds/HTML5 或 symfony/string 的 u()->slice()(需 PHP 8.0+)。