在 UTF-8 編碼中,中文字元佔 3 位元組,英文 1 位元組,Emoji 4 位元組。直接使用 substr 按位元組截取極易產生亂碼。本文對比三種安全截取方案:mbstring 標準函式、mb_strimwidth 一步到位、手寫 UTF-8 位元組解析器 (理解底層原理/無擴展環境)。
/**
* 多位元組安全截取
* @param string $str 輸入字串
* @param int $start 起始字元索引 (0基礎,支援負數)
* @param int|null $length 截取字元數 (null 截取到結束)
* @param string $encoding 編碼 (預設 UTF-8)
* @return string
*/
function mbSubstr(string $str, int $start, int $length = null, string $encoding = 'UTF-8'): string {
return mb_substr($str, $start, $length, $encoding) ?? '';
}
// 測試
$str = 'Hello世界😀PHP';
echo mbSubstr($str, 0, 5) . "\n"; // Hello
echo mbSubstr($str, 5, 2) . "\n"; // 世界
echo mbSubstr($str, -4) . "\n"; // 😀PHP
echo mbSubstr($str, 5, -1) . "\n"; // 世界😀 (PHP 7.1+ 支援負長度)
/**
* 截取指定寬度並自動加省略號
* @param string $str
* @param int $start 起始字元
* @param int $width 目標寬度 (字元數)
* @param string $trimmarker 省略標記
* @param string $encoding
* @return string
*/
function mbTrimWidth(string $str, int $start, int $width, string $trimmarker = '...', string $encoding = 'UTF-8'): string {
return mb_strimwidth($str, $start, $width, $trimmarker, $encoding);
}
// 實戰:文章摘要
$article = '這是一篇很長的文章內容,包含中文、English、數字123、符號!@#、Emoji😀🎉...';
echo mbTrimWidth($article, 0, 30, '...') . "\n";
// 輸出:這是一篇很長的文章內容,包含中文、Eng... (寬度含省略號)
原理:UTF-8 編碼規則決定字元邊界:
/**
* 純 PHP UTF-8 安全截取 (按字元)
* @param string $str
* @param int $length 截取字元數
* @return string
*/
function utf8Substr(string $str, int $length): string {
if ($length <= 0) return '';
$bytes = strlen($str);
$result = '';
$charCount = 0;
$offset = 0;
while ($charCount < $length && $offset < $bytes) {
$byte = ord($str[$offset]);
// 判斷字元長度
if (($byte & 0x80) === 0x00) { // 1 byte
$charLen = 1;
} elseif (($byte & 0xE0) === 0xC0) { // 2 bytes
$charLen = 2;
} elseif (($byte & 0xF0) === 0xE0) { // 3 bytes
$charLen = 3;
} elseif (($byte & 0xF8) === 0xF0) { // 4 bytes
$charLen = 4;
} else {
// 非法/延續位元組,單字節處理避免死循環
$charLen = 1;
}
// 邊界檢查
if ($offset + $charLen > $bytes) $charLen = $bytes - $offset;
$result .= substr($str, $offset, $charLen);
$offset += $charLen;
$charCount++;
}
return $result;
}
// 測試
$str = '中文English😀';
echo utf8Substr($str, 5) . "\n"; // 中文Eng
echo utf8Substr($str, 10) . "\n"; // 中文English😀
// 進階版:支援 start offset
function utf8SubstrAdvanced(string $str, int $start, int $length = null): string {
$bytes = strlen($str);
// 先定位到 start 字元位置
$offset = 0;
$charCount = 0;
while ($charCount < $start && $offset < $bytes) {
$byte = ord($str[$offset]);
$charLen = (($byte & 0x80) === 0) ? 1 :
(($byte & 0xE0) === 0xC0) ? 2 :
(($byte & 0xF0) === 0xE0) ? 3 :
(($byte & 0xF8) === 0xF0) ? 4 : 1;
if ($offset + $charLen > $bytes) break;
$offset += $charLen;
$charCount++;
}
// 再截取 length 字元
$result = '';
$charCount = 0;
$maxChars = $length ?? PHP_INT_MAX;
while ($charCount < $maxChars && $offset < $bytes) {
$byte = ord($str[$offset]);
$charLen = (($byte & 0x80) === 0) ? 1 :
(($byte & 0xE0) === 0xC0) ? 2 :
(($byte & 0xF0) === 0xE0) ? 3 :
(($byte & 0xF8) === 0xF0) ? 4 : 1;
if ($offset + $charLen > $bytes) $charLen = $bytes - $offset;
$result .= substr($str, $offset, $charLen);
$offset += $charLen;
$charCount++;
}
return $result;
}
| 維度 | mb_substr / mb_strimwidth | 手寫解析器 |
|---|---|---|
| 正確性 | ✅ 完整 Unicode 支援 (含組合字元需 intl) | ✅ 基本字元邊界正確 / ❌ 組合字元/ZWJ 不支援 |
| 效能 | C 層實現,極快 | PHP 迴圈,慢 10-50 倍 |
| 依賴 | mbstring (PHP 核心內建,幾乎必裝) | 零依賴 |
| 功能豐富度 | mb_strimwidth、mb_strpos、mb_strtolower 等完整生態 | 僅截取,需自行擴展 |
| 維護性 | 標準庫,社區維護 | 自行維護,易漏邊界情況 |
| 適用場景 | 所有生產環境 | 極簡容器、嵌入式、教學理解 UTF-8 結構 |
class TextHelper {
// 統一入口,自動選擇最佳實現
public static function substr(string $str, int $start, int $length = null): string {
return mb_substr($str, $start, $length, 'UTF-8') ?? '';
}
public static function trimWidth(string $str, int $width, string $marker = '...'): string {
return mb_strimwidth($str, 0, $width, $marker, 'UTF-8');
}
public static function words(string $str, int $maxWords): string {
// 簡易按空白分詞 (中文無空白需分詞庫)
$words = preg_split('/\s+/u', $str, $maxWords + 1);
if (count($words) > $maxWords) {
array_pop($words);
return implode(' ', $words) . '...';
}
return implode(' ', $words);
}
// 移除 Emoji (可選)
public static function stripEmoji(string $str): string {
return preg_replace('/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}]/u', '', $str);
}
}
# phpstan.neon
rules:
- PHPStan\Rules\Functions\FunctionCallParametersCheckRule
parameters:
forbiddenFunctions:
substr: 'Use TextHelper::substr() for UTF-8 strings'
strlen: 'Use mb_strlen() or TextHelper::length()'
strpos: 'Use mb_strpos()'
strtolower: 'Use mb_strtolower()'
# ... 其他位元組函式
中文字串截取的核心原則:**業務層字串操作一律使用 mb_* 函式族,嚴禁直接使用 substr/strlen/strpos 等位元組函式**。配合靜態分析強制執行,能從架構層面消除亂碼 Bug。