處理「含 HTML 標籤的字符串截取」是經典難題:若直接按字符數 substr(),極易產生未閉合標籤、屬性斷裂,導致前端渲染錯亂。本文對比兩種思路,推薦使用 Tidy 擴展 將字符串解析為 DOM 樹,再針對文本節點截斷,最後序列化回 HTML,確保結構完整。
需求:從 <div><a>張三</a> 評論了 <a>李四</a>…</div> 截取前 N 個「可見字符」(中文算 2 寬度),保留所有標籤結構。手動遍歷字符、狀態機判斷 </>、UTF-8 多字節跳過,邏輯極其繁瑣且易出錯(UTF-8 中文可能 3 或 4 字節)。
<?php
// 1. 解析為 Tidy 文檔對象(注意編碼參數為 'utf8' 而非 'utf-8')
$tidy = tidy_parse_string($htmlStr, [], 'utf8');
$body = tidy_get_body($tidy); // 取得 <body> 節點
// 2. 遞歸遍歷節點樹,針對文本節點做寬度截取
function trimHtml($node, &$len, &$out) {
foreach ($node->child as $child) {
if ($child->type === TIDY_NODETYPE_TEXT) { // 文本節點
$w = mb_strwidth($child->value, 'utf-8');
if ($w >= $len) {
$child->value = mb_strimwidth($child->value, 0, $len, '…', 'utf-8');
$out .= $child->value;
$len = 0;
return false; // 停止遍歷
} else {
$out .= $child->value;
$len -= $w;
}
} elseif ($child->type === TIDY_NODETYPE_START || $child->type === TIDY_NODETYPE_END) {
$out .= $child->value; // 直接輸出標籤原文
if ($len > 0) trimHtml($child, $len, $out);
}
if ($len <= 0) return false;
}
return true;
}
$len = 100; // 目標寬度(中文算 2)
$trimed = '';
trimHtml($body, $len, $trimed);
echo $trimed;
?>
mb_strwidth():按「寬度」計算長度,ASCII 1、中文 2,完美契合排版需求。mb_strimwidth():同樣按寬度截取,自動補全尾部省略號,避免半個中文字符亂碼。PHP 5.4+ 可用 DOMDocument::loadHTML() 搭配 DOMXPath 定位目標容器,再遍歷 childNodes 處理 DOMText 節點,邏輯同 Tidy 但無需額外擴展。注意 loadHTML() 會自動補全 <html><body>,需手動提取片段。
若僅為展示截斷,CSS text-overflow: ellipsis 配合 display: -webkit-box; -webkit-line-clamp: 3; 可實現多行截斷,無需後端介入。但涉及「存入數據庫的純文本摘要」、「RSS/JSON 輸出」等場景,後端截取仍不可或缺。