中文亂碼是 PHP 開發者永恆的痛點:UTF-8、GBK、GB2312、BIG5、CP936 交織,加上瀏覽器、資料庫、API、檔案編碼不一致,稍有不慎即成「亂碼現場」。本文系統梳理編碼轉換核心函數、資料庫連線編碼設定、統一處理類封裝、以及**從源頭避免亂碼的工程化規範**。
| 函數 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
iconv() |
內建、支援 //TRANSLIT //IGNORE 模式、標準 POSIX | 某些字符無法轉換時靜默失敗、需知確切源編碼 | 已知確切編碼的單字串轉換 |
mb_convert_encoding() |
支援自動檢測、處理陣列/物件、多字節安全、效能較好 | 需 mbstring 擴展(現代 PHP 通常內建) |
首選方案:未知編碼、批次轉換、資料庫結果集 |
mb_detect_encoding() |
可檢測字串編碼 | 不可靠:短字串、相似編碼(GBK/GB2312)易誤判 | 輔助判斷,**不可作為唯一依據** |
場景:PHP (UTF-8) ↔ SQL Server (GBK/CP936),第三方系統固定編碼無法修改。
<?php
$dsn = "sqlsrv:Server=127.0.0.1;Database=MyDB;CharacterSet=UTF-8";
// CharacterSet=UTF-8 告訴驅動:PHP 端用 UTF-8,驅動自動與資料庫編碼轉換
$pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// 測試
$stmt = $pdo->query("SELECT TOP 1 name FROM users");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo "資料庫讀取: " . $row['name'] . "\n"; // 應正常顯示中文
?>
<?php
// 連線不指定編碼,讀出來是 GBK 位元組串流
$conn = odbc_connect("Driver={SQL Server};Server=127.0.0.1;Database=MyDB;", 'user', 'pass');
$result = odbc_exec($conn, "SELECT name FROM users");
$rows = [];
while ($row = odbc_fetch_array($result)) {
// 關鍵:將每個欄位從 GBK 轉 UTF-8
$rows[] = array_map(fn($v) => is_string($v) ? mb_convert_encoding($v, 'UTF-8', 'CP936') : $v, $row);
}
print_r($rows);
?>
<?php
/**
* 統一編碼轉換工具類
* 支援:字串、陣列(鍵值皆轉)、巢狀結構、物件(轉陣列)、null/純量保護
*/
class CharsetConverter {
// 常見編碼別名標準化
private static array $aliasMap = [
'utf8' => 'UTF-8', 'utf-8' => 'UTF-8',
'gbk' => 'GBK', 'cp936' => 'GBK', 'gb2312' => 'GBK',
'big5' => 'BIG-5', 'big-5' => 'BIG-5',
'latin1' => 'ISO-8859-1', 'iso-8859-1' => 'ISO-8859-1',
];
public static function normalize(string $encoding): string {
$key = strtolower($encoding);
return self::$aliasMap[$key] ?? strtoupper($encoding);
}
/**
* 核心轉換方法
* @param mixed $data 輸入資料
* @param string $from 源編碼 (預設 GBK)
* @param string $to 目標編碼 (預設 UTF-8)
* @return mixed 相同結構的轉換後資料
*/
public static function convert(mixed $data, string $from = 'GBK', string $to = 'UTF-8'): mixed {
$from = self::normalize($from);
$to = self::normalize($to);
// 1. 無需轉換情況
if ($from === $to) return $data;
if ($data === null || $data === '' || (is_scalar($data) && !is_string($data))) return $data;
// 2. 字串轉換
if (is_string($data)) {
if (!function_exists('mb_convert_encoding')) {
trigger_error('mbstring 擴展未安裝,無法轉換編碼', E_USER_WARNING);
return $data;
}
return mb_convert_encoding($data, $to, $from);
}
// 3. 陣列遞迴轉換(鍵值皆轉)
if (is_array($data)) {
$result = [];
foreach ($data as $key => $value) {
$newKey = self::convert($key, $from, $to);
$newVal = self::convert($value, $from, $to);
$result[$newKey] = $newVal;
}
return $result;
}
// 4. 物件轉陣列再轉換
if (is_object($data)) {
return self::convert((array)$data, $from, $to);
}
return $data;
}
// 便捷靜態方法
public static function toUtf8(mixed $data, string $from = 'GBK'): mixed {
return self::convert($data, $from, 'UTF-8');
}
public static function fromUtf8(mixed $data, string $to = 'GBK'): mixed {
return self::convert($data, 'UTF-8', $to);
}
}
// ========== 使用範例 ==========
// 1. 資料庫結果集轉換
$gbkRows = [
['id'=>1, 'name'=>'張三', 'city'=>'北京'],
['id'=>2, 'name'=>'李四', 'city'=>'上海'],
];
$utf8Rows = CharsetConverter::toUtf8($gbkRows); // 鍵值皆轉
// 2. API 請求參數轉換(接收 GBK POST)
$_POST = CharsetConverter::toUtf8($_POST, 'GBK');
// 3. 輸出 GBK 給舊系統
$gbkXml = CharsetConverter::fromUtf8($utf8Xml, 'GBK');
?>
utf8mb4_unicode_ci(支援 Emoji、完整 Unicode)。charset=utf8mb4、mysqli set_charset('utf8mb4')。header('Content-Type: application/json; charset=utf-8'); 或 Nginx/Apache 全域設定。<meta charset="utf-8"> 放在 <head> 首行。accept-charset="utf-8"。curl -v 檢查 Content-Type 與實際位元組。error_log 寫入 GBK 導致後續分析工具亂碼。| 現象 | 原因 | 修復 |
|---|---|---|
| (菱形問號) | UTF-8 解碼無效位元組序列 | 檢查源資料是否真為 UTF-8;用 mb_convert_encoding($str, 'UTF-8', 'GBK') |
| 鏈靈 鏈靈 (兩個中文變四個亂碼) | UTF-8 被當作 GBK 讀取(雙字節誤讀) | 資料庫連線指定 charset=utf8mb4;Header 宣告 UTF-8 |
| ??? (問號) | 目標編碼無法表示該字符(如 UTF-8 存入 latin1 欄位) | 修改欄位/資料庫編碼為 utf8mb4;或用 //IGNORE 模式丟棄 |
| 瀏覽器正常、資料庫亂碼 | 寫入時未轉碼、連線編碼不匹配 | PDO DSN 加 charset;mysqli_set_charset |
| BOM 頭干擾 JSON/API | 檔案儲存為 UTF-8 with BOM | IDE 另存為 UTF-8 無 BOM;PHP 端 substr($str, 0, 3) === "\xEF\xBB\xBF" && $str = substr($str, 3) |
PHP 中文亂碼的本質是**編碼不一致導致的位元組序列錯誤解碼**。解決之道:
mb_convert_encoding 為核心的轉換利器,封裝 CharsetConverter 統一處理陣列/巢狀結構。charset=utf8mb4,資料庫預設 utf8mb4_unicode_ci。.editorconfig、CI 檢查檔案編碼、API 合約註明編碼。**「預防勝於治療」**:從專案第一天起建立 UTF-8 全鏈路規範,能節省 90% 的亂碼排查時間。