strlen() 返回字節數,mb_strlen() 返回編碼感知的字符數,但兩者在「用戶感知字符」(用戶可見字符單元)層面均有盲區:
| 字符串 | strlen (UTF-8) | mb_strlen (UTF-8) | 用戶可見字符數 |
|---|---|---|---|
| Hello | 5 | 5 | 5 |
| 你好 | 6 | 2 | 2 |
| 👨👩👧👦 | 25 | 7 | 1 (家庭表情符號) |
| é (e+組合鋭音符) | 3 | 2 | 1 |
| 🏳️🌈 (旗幟+彩虹) | 14 | 4 | 1 |
表情符號序列、組合字符、零寬連接符(ZWJ)導致「代碼點數」≠「字素簇數」。表單驗證、截斷展示、數據庫字段長度檢查必須基於 字素簇。
// 僅能分割碼點,無法處理組合字符/ZWJ 序列
preg_match_all('/./u', $str, $m);
$len = count($m[0]);
上文提到的 preg_match_all('/./us', $str, $m) 中的 s 修飾符僅影響 . 匹配換行,對字素簇無效。
$tmp = @iconv('GBK', 'UTF-8', $str);
if ($tmp !== false) $str = $tmp;
preg_match_all('/./u', $str, $m);
$len = count($m[0]);
解決了編碼檢測問題,但仍按碼點計數,表情符號計數錯誤。
// 扣除組合字符、ZWJ 等非獨立字符
$len = mb_strlen($str, 'UTF-8');
$len -= preg_match_all('/[\x{0300}-\x{036F}\x{200D}\x{FE0F}]/u', $str);
維護成本高,Unicode 版本更新需同步調整範圍。
PHP 7.0+ 核心內建 intl 扩展(基於 ICU 庫),提供 Unicode 字素簇分割算法(UAX #29)實現:
// 直接返回用戶可見字符數
$len = grapheme_strlen($str);
優勢:
grapheme_substr()、grapheme_strpos()、grapheme_stripos()、grapheme_extract()/**
* 獲取字符串的用戶可見字符數(字素簇計數)
* @param string $str 輸入字符串
* @param string|null $encoding 輸入編碼,null 自動檢測
* @return int
*/
function grapheme_len(string $str, ?string $encoding = null): int {
// 1. 統一轉 UTF-8
if ($encoding !== null) {
$str = mb_convert_encoding($str, 'UTF-8', $encoding);
} elseif (!mb_check_encoding($str, 'UTF-8')) {
// 嘗試常見編碼
$enc = mb_detect_encoding($str, ['GBK', 'GB2312', 'BIG5', 'UTF-8'], true);
if ($enc) $str = mb_convert_encoding($str, 'UTF-8', $enc);
}
// 2. 優先使用 grapheme_strlen (PHP 7.0+ & intl)
if (function_exists('grapheme_strlen')) {
return grapheme_strlen($str);
}
// 3. 降級:mb_strlen + 組合字符修正 (PHP 5.6+)
$len = mb_strlen($str, 'UTF-8');
// 扣除常見組合字符範圍:Combining Diacritical Marks, ZWJ, Variation Selector-16
$len -= (int)preg_match_all('/[\x{0300}-\x{036F}\x{1AB0}-\x{1AFF}\x{1DC0}-\x{1DFF}\x{20D0}-\x{20FF}\x{FE00}-\x{FE0F}\x{E0100}-\x{E01EF}]/u', $str);
return max(0, $len);
}
// 測試
$tests = [
'Hello世界' => 7,
'👨👩👧👦' => 1,
'e\u{0301}' => 1, // e + 組合鋭音符
'🏳️🌈' => 1, // 旗幟 + VS16 + 彩虹
'張三丰' => 3,
];
foreach ($tests as $str => $expect) {
$got = grapheme_len($str);
echo "[$got/$expect] " . json_encode($str) . "\n";
}
function validate_length(string $input, int $min, int $max, string $field): array {
$len = grapheme_len($input);
if ($len < $min || $len > $max) {
return [
'valid' => false,
'message' => "{$field} 長度需在 {$min}~{$max} 字符之間(當前 {$len} 字符)",
];
}
return ['valid' => true];
}
// 使用示例
$rules = [
'username' => ['min'=>2, 'max'=>20],
'bio' => ['min'=>0, 'max'=>160],
];
foreach ($rules as $field => $rule) {
$res = validate_length($_POST[$field] ?? '', $rule['min'], $rule['max'], $field);
if (!$res['valid']) $errors[$field] = $res['message'];
}
| 場景 | 推薦字段類型 | 長度設置 | 說明 |
|---|---|---|---|
| 純 ASCII(用戶名、代碼) | VARCHAR(n) | n = 字符上限 | MySQL utf8mb4 下 1 字符 ≤ 4 字節,VARCHAR 存儲長度按字符計 |
| 含中文/表情(暱稱、評論) | VARCHAR(n) | n = 字符上限 | 同樣按字符計,但實際存儲字節可能達 4n |
| 長文本(文章、日誌) | TEXT/MEDIUMTEXT | 無需指定 | 應用層用 grapheme_len 校驗,DB 層不限長 |
| 固定長度標識(UUID、MD5) | CHAR(32/36) | 固定 | CHAR 按字節存,ASCII 安全 |
關鍵原則:應用層驗證用 grapheme_len(),數據庫字段定義按「最大字符數」設置 VARCHAR(n),啟用 utf8mb4 字符集。切勿在 DB 層用 CHAR 或按字節限制長度,會導致多字節字符截斷亂碼。
grapheme_strlen() 為 C 函數調用,單次耗時 < 1μs(百萬字符級字符串除外),無需緩存。Intl.Segmenter 或 grapheme-splitter 庫),後端僅做二次校驗。grapheme_extract($str, $max, GRAPHEME_EXTR_COUNT) 分塊截取,避免一次性分配巨大數組。字符串長度計算看似簡單,實則觸及 Unicode 深層規範。PHP 開發者應摒棄 strlen/mb_strlen 直覺,擁抱 grapheme_strlen 這一現代標準答案。若環境受限無 intl 扩展,請升級 PHP 或啟用扩展——這是 2024 年專業 PHP 專案的基礎設施標配。