利用 strlen() (位元組長度) 與 mb_strlen() (字元長度) 的差異,可快速判斷字串組成:純 ASCII、純多位元組字元 (中日韓)、或混合型。本文提供完整檢測函數、原理解析、編碼陷阱與現代替代方案。
strlen($str):返回字串位元組數mb_strlen($str, 'UTF-8'):返回字串字元數 (Unicode 碼位)strlen = 3 * mb_strlen<?php
/**
* 判斷字串字元組成類型
* @param string $str 待檢測字串
* @param string $encoding 字串編碼 (預設 UTF-8)
* @return string 類型:'ascii' | 'cjk' | 'mixed' | 'empty' | 'other'
* ascii - 純 ASCII (英文、數字、常用符號)
* cjk - 純中日韓統一表意文字 (含漢字、假名、諺文)
* mixed - ASCII 與 CJK 混合
* other - 包含其他 Unicode 字元 (Emoji、特殊符號、拉丁擴展等)
*/
function detectStringType(string $str, string $encoding = 'UTF-8'): string {
if ($str === '') return 'empty';
$byteLen = strlen($str);
$charLen = mb_strlen($str, $encoding);
// 1. 純 ASCII:位元組數等於字元數
if ($byteLen === $charLen) {
// 進一步細分:純數字、純字母、字母數字混合
if (ctype_digit($str)) return 'ascii_digit';
if (ctype_alpha($str)) return 'ascii_alpha';
if (ctype_alnum($str)) return 'ascii_alnum';
return 'ascii'; // 含符號、空白等
}
// 2. 純多位元組:位元組數是字元數的整數倍 (通常 3 倍)
// 注意:Emoji 為 4 位元組,會導致非整數倍
$ratio = $byteLen / $charLen;
if (abs($ratio - 3) < 0.01 || abs($ratio - 4) < 0.01) {
// 粗略判斷為 CJK 區塊,更精確需用正則
if (preg_match('/^[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]+$/u', $str)) {
return 'cjk';
}
// 可能全為 Emoji 或其他 3/4 字節字元
return 'other';
}
// 3. 混合型:位元組數不等於字元數,且非整數倍
return 'mixed';
}
// 更精確的分類:使用 Unicode 正則
function classifyString(string $str): array {
$hasAscii = (bool)preg_match('/[\x00-\x7F]/', $str);
$hasHan = (bool)preg_match('/\p{Han}/u', $str); // 漢字
$hasKana = (bool)preg_match('/\p{Hiragana}|\p{Katakana}/u', $str); // 日文假名
$hasHangul = (bool)preg_match('/\p{Hangul}/u', $str); // 韓文諺文
$hasEmoji = (bool)preg_match('/\p{Extended_Pictographic}/u', $str); // Emoji
$hasOther = (bool)preg_match('/[^\x00-\x7F\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}\p{Extended_Pictographic}]/u', $str);
$types = [];
if ($hasAscii) $types[] = 'ascii';
if ($hasHan) $types[] = 'han';
if ($hasKana) $types[] = 'kana';
if ($hasHangul) $types[] = 'hangul';
if ($hasEmoji) $types[] = 'emoji';
if ($hasOther) $types[] = 'other';
return [
'types' => $types,
'desc' => implode(' + ', $types) ?: 'empty',
];
}
// 測試
$tests = [
'123456',
'abcdef',
'Hello World',
'中文測試',
'こんにちは',
'한글',
'Hello 世界',
'PHP 8.2 🎉',
'🎉🎊🎁',
'αβγ', // 希臘字母
];
foreach ($tests as $t) {
echo "{$t} => " . detectStringType($t) . " | " . classifyString($t)['desc'] . "\n";
}
/*
123456 => ascii_digit | ascii
abcdef => ascii_alpha | ascii
Hello World => ascii | ascii
中文測試 => cjk | han
こんにちは => cjk | kana
한글 => cjk | hangul
Hello 世界 => mixed | ascii + han
PHP 8.2 🎉 => mixed | ascii + emoji
🎉🎊🎁 => other | emoji
αβγ => other | other
*/
?>
| 場景 | 判斷邏輯 | 代碼片段 |
|---|---|---|
| 用戶名驗證:僅允許中英文數字 | 無 other、無 emoji、無特殊符號 | !$hasOther && !$hasEmoji && !$hasSpecial |
| 密碼強度:必須含大小寫、數字、特殊符號 | 檢測 ascii 子類型 | ctype_lower/cupper/digit + 符號正則 |
| 搜尋關鍵字:中文按字切分、英文按詞切分 | detectStringType 判斷後分流 | if($type==='cjk') jieba_cut(); else explode(' ') |
| 資料清洗:移除 Emoji、控制字元 | preg_replace 移除 \p{Cc}、\p{Extended_Pictographic} | preg_replace('/[\p{Cc}\p{Extended_Pictographic}]/u', '', $str) |
| 截斷字串:按字元數而非位元組數 | mb_substr / mb_strimwidth | mb_substr($str, 0, 50, 'UTF-8') |
mb_strlen($str, 'UTF-8') 必須與實際字串編碼一致。若字串為 GBK/Big5 卻用 UTF-8 計算,結果錯誤。統一專案編碼為 UTF-8。bom_remove() 或 mb_strpos 跳過。é 可為單碼位 (U+00E9) 或組合 e + ´ (U+0301)。mb_strlen 計算碼位數,顯示寬度需用 grapheme_strlen() (intl)。👨👩👧👦 由多個碼位組成 (ZWJ 連接),mb_strlen 計為 7/11 碼位,實際顯示為 1 個字符。需用 grapheme_* 函數處理。mb_strlen 較 strlen 慢。高頻調用時,可先 strlen 快速判斷:若相等則為純 ASCII,無需呼叫 mb_strlen。<?php
// 真正的「使用者可見字元數」 (書寫字素簇)
$str = '👨👩👧👦'; // 家庭 Emoji
echo "mb_strlen: " . mb_strlen($str, 'UTF-8') . "\n"; // 11 (碼位數)
echo "grapheme_strlen: " . grapheme_strlen($str) . "\n"; // 1 (字素簇數)
// 截斷保持字素簇完整
echo grapheme_substr($str, 0, 1); // 👨👩👧👦
// mb_substr 會截斷成亂碼
?>
判斷字串字元類型核心在於 strlen vs mb_strlen 差異與 Unicode 正則。建議:
detectStringType() 快速分類。classifyString() 正則檢測各 Unicode 區塊。grapheme_* 函數 (需 intl 擴展)。