substr 按位元組截取會導致中文亂碼。本文提供一個健壯的 cutstr 函數,正確按字元數截取 UTF-8 與雙位元組編碼 (GBK/BIG5),並支援 HTML 實體轉義與尾碼附加。
<?php
/**
* 多位元組安全截取字串
* @param string $string 原始字串
* @param int $length 截取字元長度 (非位元組)
* @param string $dot 尾碼,預設 ' ...'
* @param string $charset 編碼,預設 'utf-8'
* @return string
*/
function cutstr($string, $length, $dot = ' ...', $charset = 'utf-8') {
if (mb_strlen($string, $charset) <= $length) {
return $string;
}
// 預處理:HTML 實體還原,避免截斷實體導致顯示異常
$string = html_entity_decode($string, ENT_QUOTES, $charset);
$strcut = '';
if (strtolower($charset) === 'utf-8') {
// UTF-8 逐字元處理
$n = $noc = 0;
while ($n < strlen($string) && $noc < $length) {
$t = ord($string[$n]);
if ($t <= 127) { // ASCII 1 位元組
$tn = 1; $noc++;
} elseif ($t < 224) { // 2 位元組
$tn = 2; $noc += 2;
} elseif ($t < 240) { // 3 位元組 (常見中日韓)
$tn = 3; $noc += 2;
} elseif ($t < 248) { // 4 位元組 (Emoji 等)
$tn = 4; $noc += 2;
} else { // 非法/保留
$tn = 1; $noc++;
}
$strcut .= substr($string, $n, $tn);
$n += $tn;
}
} else {
// 雙位元組編碼 (GBK/BIG5) 簡易處理
for ($i = 0; $i < $length; $i++) {
$strcut .= (ord($string[$i]) > 127) ? $string[$i] . $string[++$i] : $string[$i];
}
}
// 還原 HTML 實體並附加尾碼
return htmlspecialchars($strcut, ENT_QUOTES, $charset) . $dot;
}
// 使用範例
echo cutstr('你好世界 Hello', 6); // 你好世界 ...
echo cutstr('你好世界 Hello', 6, '>>'); // 你好世界 >>
echo cutstr('測試<b>粗體</b>', 4); // 測試<b>... (實體安全)
function cutstr_modern($string, $length, $dot = ' ...', $charset = 'UTF-8') {
if (mb_strlen($string, $charset) <= $length) return $string;
return mb_substr($string, 0, $length, $charset) . $dot;
}
強烈建議:生產環境務必啟用 mbstring 擴展並使用 mb_substr/mb_strlen,效能更高、邊界情況處理更完善 (如組合字元、Emoji)。
| 特性 | 原生 substr | 本函數 cutstr | mb_substr |
|---|---|---|---|
| 中文截取 | 亂碼 | 正常 | 正常 |
| Emoji 支援 | 損壞 | 部分 (視為 2 字元) | 完整 (需 PHP 7.2+) |
| HTML 實體保護 | 無 | 有 | 無 (需自行處理) |
| 效能 | 最高 | 中等 | 高 |