PHP 內建的 `substr()`、`mb_substr()` 按字節或字符截取,但在中文場景下常面臨三大痛點:
PHP 7.2+ 環境強烈建議啟用 `mbstring` 擴展,直接使用多字節字符串函數:
<?php
/**
* 安全截取 UTF-8 字符串(按字符數)
* @param string $str 輸入字符串
* @param int $length 截取長度(字符數)
* @param string $suffix 截斷後綴
* @param bool $stripTags 是否去除 HTML 標籤
* @return string
*/
function mbStrCut(string $str, int $length, string $suffix = '...', bool $stripTags = true): string
{
// 1. 可選:去除 HTML 標籤,避免標籤破壞
if ($stripTags) {
$str = strip_tags($str);
}
// 2. 統一內部編碼為 UTF-8
mb_internal_encoding('UTF-8');
// 3. 若長度足夠,直接返回
if (mb_strlen($str) <= $length) {
return $str;
}
// 4. 截取並添加後綴
return mb_substr($str, 0, $length) . $suffix;
}
// 測試
$text = "PHP 8.3 引入了 readonly 類屬性、隨機擴展等新特性,大幅提升開發體驗。";
echo mbStrCut($text, 20); // 輸出:PHP 8.3 引入了 readonly 類屬性、隨機...
echo mbStrCut($text, 20, '【閱讀全文】', false); // 保留標籤(若輸入含標籤)
當無法使用 `mbstring` 時(極少數舊環境),需手動解析 UTF-8 編碼規則:
<?php
/**
* 純 PHP 實現 UTF-8 字符串截取(無 mbstring 依賴)
* 原理:根據 UTF-8 首字節高位判斷字符字節數
* 0xxxxxxx (1字節) | 110xxxxx 10xxxxxx (2字節) | 1110xxxx 10xxxxxx 10xxxxxx (3字節) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (4字節)
*/
function utf8StrCut(string $str, int $length, string $suffix = '...'): string
{
$len = strlen($str);
if ($len <= $length) return $str; // 字節長度已小於目標,直接返回(近似)
$charCount = 0;
$bytePos = 0;
while ($bytePos < $len && $charCount < $length) {
$byte = ord($str[$bytePos]);
if ($byte < 0x80) { // 1 字節 (ASCII)
$bytePos += 1;
} elseif (($byte & 0xE0) === 0xC0) { // 2 字節
$bytePos += 2;
} elseif (($byte & 0xF0) === 0xE0) { // 3 字節 (中文常見)
$bytePos += 3;
} elseif (($byte & 0xF8) === 0xF0) { // 4 字節 (Emoji 等)
$bytePos += 4;
} else {
$bytePos += 1; // 非法字節,跳過
}
$charCount++;
}
// 截取字節並添加後綴
return substr($str, 0, $bytePos) . $suffix;
}
使用 `DOMDocument` 解析 HTML,按文本節點截取並自動閉合標籤:
<?php
/**
* HTML 安全截取:保留標籤結構,自動閉合未閉合標籤
* @param string $html 輸入 HTML
* @param int $maxLength 最大文本字符數(不含標籤)
* @param string $suffix 截斷後綴
* @return string 合法 HTML 片段
*/
function htmlStrCut(string $html, int $maxLength, string $suffix = '...'): string
{
$dom = new DOMDocument();
// 解決 DOMDocument 不識別 HTML5 實體、編碼問題
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
@$dom->loadHTML('' . $html . '', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$body = $dom->getElementsByTagName('div')->item(0);
$textCount = 0;
$truncated = false;
// 深度優先遍歷文本節點
$walker = new DOMXPath($dom);
$textNodes = $walker->query('//text()');
foreach ($textNodes as $textNode) {
$text = $textNode->nodeValue;
$remaining = $maxLength - $textCount;
if ($remaining <= 0) {
// 已達限制,移除後續所有節點
$textNode->nodeValue = '';
$truncated = true;
continue;
}
if (mb_strlen($text) > $remaining) {
// 當前文本節點需截斷
$textNode->nodeValue = mb_substr($text, 0, $remaining) . $suffix;
$truncated = true;
// 移除後續兄弟節點
$next = $textNode->nextSibling;
while ($next) {
$tmp = $next->nextSibling;
$next->parentNode->removeChild($next);
$next = $tmp;
}
break;
}
$textCount += mb_strlen($text);
}
// 輸出處理後的 HTML(僅 body 內容)
$result = '';
foreach ($body->childNodes as $child) {
$result .= $dom->saveHTML($child);
}
return $result ?: $html; // 異常回退原文
}
// 測試
$html = '<p>這是一段<strong>包含加粗</strong>和<em>斜體</em>的富文本內容。</p>';
echo htmlStrCut($html, 15);
// 輸出:<p>這是一段<strong>包含加粗</strong>和<em>斜體...</em></p>(標籤完整閉合)
中文字符視覺寬度 ≈ 2 × 英文字符。按「視覺寬度單位」截取:
<?php
/**
* 按視覺寬度截取(中文計 2,英文/數字/標點計 1)
* 適用於固定寬度容器的文本截斷對齊
*/
function visualWidthCut(string $str, int $maxWidth, string $suffix = '...'): string
{
$width = 0;
$suffixWidth = strlen($suffix); // 後綴按半角計算
$result = '';
// 使用 grapheme 分簇遍歷(支援 Emoji 組合字符)
$clusters = grapheme_str_split($str);
foreach ($clusters as $char) {
$charWidth = (preg_match('/^[\x{4e00}-\x{9fff}\x{3000}-\x{303f}\xff00-\xffef]+$/u', $char)) ? 2 : 1;
if ($width + $charWidth + $suffixWidth > $maxWidth) {
break;
}
$result .= $char;
$width += $charWidth;
}
return $result . ($width < $maxWidth ? '' : $suffix);
}
// 測試
$mixed = "Hello 世界 2024!";
echo visualWidthCut($mixed, 15); // 輸出:Hello 世界 2...(視覺寬度約 15)
// 解析:H(1)e(1)l(1)l(1)o(1) (1)世(2)界(2) (1)2(1)0(1)2(1)4(1)!(2) = 總寬度 18 > 15
| 方案 | 適用場景 | 性能(10萬次/秒級) | 依賴 | 推薦指數 |
|---|---|---|---|---|
| `mb_substr` + `mb_strlen` | 通用純文本截取,現代 PHP 專案 | ★★★★★ 極快(C 層實現) | `mbstring` 擴展(默認開啟) | ⭐⭐⭐⭐⭐ |
| 手動 UTF-8 解析 | 極舊環境(PHP < 5.6 無 mbstring) | ★★☆☆☆ 慢(PHP 層循環) | 無 | ⭐ |
| `DOMDocument` HTML 截取 | 富文本、CMS 摘要、郵件模板 | ★★★☆☆ 中等(DOM 解析開銷) | `libxml`、`dom` 擴展 | ⭐⭐⭐⭐ |
| 視覺寬度截取 | 固定寬度 UI、多語言對齊、PDF 生成 | ★★★★☆ 快(正則 + grapheme) | PHP 7.4+ (`grapheme_str_split`) | ⭐⭐⭐⭐ |
現代 PHP 專案建議:直接引入
symfony/string組件,使用u($str)->truncate($len, '...')享受開箱即用的 Unicode 感知截取、單詞邊界保護、HTML 安全截取等企業級能力,避免重複造輪子。