PHP 字串本質是位元組序列,不攜帶編碼資訊。檢測編碼屬於「啟發式推測」,無 100% 準確方案。常見需求:匯入 CSV/日誌檔、處理第三方 API 回應、舊系統資料遷移。
<?php
// 基本用法
$encoding = mb_detect_encoding($str, array('UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII'), true);
// 參數三 true = 嚴格模式,false = 寬鬆模式(預設)
// 簡化判斷函數
function isUtf8($str) {
return mb_detect_encoding($str, 'UTF-8', true) === 'UTF-8';
}
function detectEncoding($str) {
$enc = mb_detect_encoding($str, array('UTF-8', 'GBK', 'GB2312', 'ASCII'), true);
return $enc ?: 'UNKNOWN';
}
// 注意:需啟用 mbstring 擴展(php.ini: extension=mbstring)
?>
<?php
/**
* 利用 UTF-8 位元組模式特徵快速判斷
* @param string $str
* @return bool
*/
function isUtf8Regex($str) {
// u 修飾符啟用 UTF-8 模式匹配
// 空字串視為合法 UTF-8
if ($str === '') return true;
return preg_match('//u', $str) === 1;
}
// 進階:檢測是否包含中文(漢字 Unicode 範圍)
function hasChinese($str) {
return preg_match('/[\x{4e00}-\x{9fa5}]/u', $str) === 1;
}
?>
UTF-8 有嚴格位元組結構:ASCII (0xxxxxxx)、雙位元組 (110xxxxx 10xxxxxx)、三位元組 (1110xxxx 10xxxxxx 10xxxxxx)、四位元組 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)。PCRE 的 //u 會驗證此結構,不合法即匹配失敗。
<?php
/**
* 手寫 UTF-8 驗證(參考 RFC 3629)
* @param string $str
* @return bool
*/
function isUtf8Manual($str) {
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$c = ord($str[$i]);
if ($c <= 0x7F) {
// ASCII: 0xxxxxxx
continue;
} elseif ($c >= 0xC2 && $c <= 0xDF) {
// 2-byte: 110xxxxx 10xxxxxx
if ($i + 1 >= $len) return false;
if ((ord($str[$i+1]) & 0xC0) !== 0x80) return false;
$i++;
} elseif ($c >= 0xE0 && $c <= 0xEF) {
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
if ($i + 2 >= $len) return false;
if (($c === 0xE0 && (ord($str[$i+1]) & 0xE0) !== 0xA0) || // 過長編碼檢查
($c === 0xED && (ord($str[$i+1]) & 0xE0) === 0xA0) || // 代理區檢查
((ord($str[$i+1]) & 0xC0) !== 0x80) ||
((ord($str[$i+2]) & 0xC0) !== 0x80)) return false;
$i += 2;
} elseif ($c >= 0xF0 && $c <= 0xF4) {
// 4-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
if ($i + 3 >= $len) return false;
if (($c === 0xF0 && (ord($str[$i+1]) & 0xF0) !== 0x90) ||
($c === 0xF4 && (ord($str[$i+1]) & 0xF0) !== 0x80) ||
((ord($str[$i+1]) & 0xC0) !== 0x80) ||
((ord($str[$i+2]) & 0xC0) !== 0x80) ||
((ord($str[$i+3]) & 0xC0) !== 0x80)) return false;
$i += 3;
} else {
// 非法首位元組:0x80-0xBF (延續位元組), 0xC0-0xC1 (過長), 0xF5-0xFF (超出 Unicode)
return false;
}
}
return true;
}
?>
<?php
/**
* 嘗試轉碼,若失敗或長度異常則非目標編碼
*/
function isEncoding($str, $toEncoding, $fromEncoding) {
$converted = @iconv($fromEncoding, $toEncoding . '//IGNORE', $str);
if ($converted === false) return false;
// 進一步驗證:轉回原編碼是否一致
$back = @iconv($toEncoding, $fromEncoding . '//IGNORE', $converted);
return $back === $str;
}
// 使用
$isGbk = isEncoding($str, 'UTF-8', 'GBK');
$isUtf8 = isEncoding($str, 'GBK', 'UTF-8');
?>
<?php
/**
* 簡易 GBK/Big5 機率判斷:高位元組頻率特徵
*/
function detectGbkBig5($str) {
$len = strlen($str);
$highByte = 0;
$gbkLead = 0; // 0x81-0xFE
$big5Lead = 0; // 0xA4-0xF9
for ($i = 0; $i < $len; $i++) {
$c = ord($str[$i]);
if ($c >= 0x80) {
$highByte++;
if ($c >= 0x81 && $c <= 0xFE) $gbkLead++;
if ($c >= 0xA4 && $c <= 0xF9) $big5Lead++;
}
}
if ($highByte === 0) return 'ASCII/UTF-8';
$gbkRatio = $gbkLead / $highByte;
$big5Ratio = $big5Lead / $highByte;
if ($gbkRatio > 0.8) return 'GBK/GB2312';
if ($big5Ratio > 0.8) return 'BIG5';
return 'UNKNOWN';
}
?>
<?php
/**
* 智慧轉換為 UTF-8
* @param mixed $data 字串或陣列
* @return mixed
*/
function toUtf8($data) {
if (is_array($data)) {
foreach ($data as $k => $v) {
$data[$k] = toUtf8($v);
}
return $data;
}
if (!is_string($data)) return $data;
// 快速檢測
if (isUtf8Regex($data)) return $data; // 已是 UTF-8
// 嘗試常見編碼
$encodings = array('GBK', 'GB2312', 'BIG5', 'ISO-8859-1');
foreach ($encodings as $enc) {
$converted = @iconv($enc, 'UTF-8//IGNORE', $data);
if ($converted !== false && $converted !== '') {
// 驗證轉換合理性:轉回測試
$back = @iconv('UTF-8', $enc . '//IGNORE', $converted);
if ($back === $data || strlen($back) > strlen($data) * 0.8) {
return $converted;
}
}
}
// 兜底:強制移除非法位元組
return @iconv('UTF-8', 'UTF-8//IGNORE', $data);
}
// 使用
$cleanStr = toUtf8($dirtyStr);
$cleanArray = toUtf8($_POST); // 整組 POST 資料清洗
?>
<?php
function benchmark($func, $str, $iterations = 10000) {
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
$func($str);
}
return microtime(true) - $start;
}
$testStr = str_repeat("測試中文字串abc123", 100); // ~2KB
echo "mb_detect_encoding: " . benchmark('mb_detect_encoding', $testStr) . "s\n";
echo "preg_match //u: " . benchmark('isUtf8Regex', $testStr) . "s\n";
echo "manual scan: " . benchmark('isUtf8Manual', $testStr) . "s\n";
echo "iconv verify: " . benchmark(function($s){ return isEncoding($s, 'UTF-8', 'GBK'); }, $testStr) . "s\n";
/* 典型結果(PHP 8.1, 2KB 字串, 10000 次):
mb_detect_encoding: 0.42s
preg_match //u: 0.18s ← 最快且準確
manual scan: 0.35s
iconv verify: 1.25s ← 最慢
*/
?>
preg_match('//u', $str)(最快、準確、無依賴)mb_detect_encoding($str, ['UTF-8','GBK','BIG5'], true)isUtf8Manual() + 特徵頻率判斷toUtf8() 綜合函數| 陷阱 | 現象 | 解決 |
|---|---|---|
| GB2312 vs GBK | GBK 是 GB2312 超集,iconv 常將 GB2312 識別為 GBK | 統一視為 GBK 處理,或用 CP936 別名 |
| BOM 干擾 | UTF-8 BOM (EF BB BF) 導致檢測異常 | 處理前移除:str_replace("\xEF\xBB\xBF", '', $str) |
| 純 ASCII 誤判 | 純英文數字同時符合 UTF-8/GBK/ASCII | 視為 UTF-8 處理(ASCII 為 UTF-8 子集) |
| iconv //IGNORE 靜默丟失 | 非法字元直接刪除,無警告 | 關鍵資料避免用 //IGNORE,改用 //TRANSLIT 或拋出異常 |
| mb_detect_encoding 列表順序影響 | 前列編碼優先匹配 | 將 UTF-8 放首位,GBK/BIG5 次之 |
編碼檢測本質是機率遊戲,無完美解。現代專案強制規範輸入為 UTF-8(資料庫、API、前端表單)是根治之道。舊系統整合時,建立統一清洗入口 toUtf8(),搭配 preg_match('//u') 快速判斷、mb_detect_encoding 精確識別、iconv 安全轉碼,並記錄無法識別案例人工介入,構建穩健的編碼防線。