中文亂碼本質是編碼不一致:資料庫儲存編碼、PHP 內部編碼、HTML 輸出編碼、瀏覽器解碼編碼、檔案儲存編碼,任一環節不匹配即產生亂碼。本文提供全鏈路診斷清單、統一編碼最佳實踐、轉碼函數對比、遺留系統兼容方案。
資料來源 → [儲存編碼] → 資料庫 → [連線編碼] → PHP 變數 → [內部編碼] → 處理邏輯 → [輸出編碼] → HTTP 響應 → [Content-Type charset] → 瀏覽器解碼 → 顯示
↑ ↑
檔案編碼 meta charset
| 環節 | 配置 | 驗證方式 |
|---|---|---|
| PHP 檔案 | 編輯器儲存為 UTF-8 無 BOM | Notepad++/VS Code/PhpStorm 右下角顯示 UTF-8 |
| HTML 輸出 | <meta charset="UTF-8"> 放在 <head> 首行 | 瀏覽器開發工具 Network → Response Headers: Content-Type: text/html; charset=UTF-8 |
| PHP 內部 | mb_internal_encoding('UTF-8') (PHP 5.6+ 預設即為 UTF-8) | echo mb_internal_encoding(); |
| 資料庫連線 | MySQL: SET NAMES utf8mb4 / PDO DSN charset=utf8mb4 | SHOW VARIABLES LIKE 'character_set%'; 全為 utf8mb4 |
| 資料表/欄位 | CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | SHOW CREATE TABLE tbl; |
| HTTP 響應頭 | Nginx: charset utf-8; / Apache: AddDefaultCharset UTF-8 / PHP: header('Content-Type: text/html; charset=UTF-8') | curl -I 檢查回應頭 |
| 表單提交 | <form accept-charset="UTF-8"> | 瀏覽器會以 UTF-8 編碼送出 |
| URL 參數 | 瀏覽器自動 URL 編碼,PHP urldecode() 自動解碼 | 確保前端 encodeURIComponent() |
| 檔案讀寫 | 指定編碼 fopen($f, 'r', false, stream_context_create(['http'=>['header'=>'Accept-Charset: UTF-8']])) | 或用 mb_convert_encoding() 讀取後轉碼 |
<?php
// 1. iconv:系統級、支援 //TRANSLIT //IGNORE、較嚴格
$utf8 = iconv('GBK', 'UTF-8//IGNORE', $gbkStr); // 忽略無法轉換字元
$utf8 = iconv('GBK', 'UTF-8//TRANSLIT', $gbkStr); // 近似轉換 (如 € → EUR)
// 2. mb_convert_encoding:PHP 內建、支援自動偵測、處理陣列/物件遞迴
$utf8 = mb_convert_encoding($gbkStr, 'UTF-8', 'GBK'); // 指定來源編碼
$utf8 = mb_convert_encoding($gbkStr, 'UTF-8', 'GBK, BIG5, UTF-8'); // 自動偵測
// 3. 自動偵測編碼 (不完全可靠)
$encoding = mb_detect_encoding($str, ['UTF-8', 'GBK', 'BIG5', 'ASCII'], true);
if ($encoding) $utf8 = mb_convert_encoding($str, 'UTF-8', $encoding);
// 4. 通用轉碼函數:支援字串、陣列、物件遞迴
function autoCharset(mixed $data, string $from = 'GBK', string $to = 'UTF-8'): mixed {
$from = strtoupper($from) === 'UTF8' ? 'UTF-8' : $from;
$to = strtoupper($to) === 'UTF8' ? 'UTF-8' : $to;
if ($from === $to || empty($data) || (is_scalar($data) && !is_string($data))) {
return $data;
}
if (is_string($data)) {
return function_exists('mb_convert_encoding')
? mb_convert_encoding($data, $to, $from)
: @iconv($from, $to . '//IGNORE', $data);
}
if (is_array($data)) {
$result = [];
foreach ($data as $key => $val) {
$newKey = autoCharset($key, $from, $to);
$result[$newKey] = autoCharset($val, $from, $to);
}
return $result;
}
if (is_object($data)) {
foreach ($data as $key => $val) {
$data->$key = autoCharset($val, $from, $to);
}
return $data;
}
return $data;
}
// 使用:資料庫查詢結果轉碼
$rows = $pdo->query('SELECT * FROM gbk_table')->fetchAll(PDO::FETCH_ASSOC);
$utf8Rows = autoCharset($rows, 'GBK', 'UTF-8');
?>
| 現象 | 可能原因 | 快速驗證 | 解決 |
|---|---|---|---|
| 菱形問號 / | UTF-8 字節序列被截斷、或雙重編碼 | 檢查資料庫欄位長度是否截斷多位元組字元 | 欄位用 VARCHAR(191) 或 TEXT;避免雙重 utf8_encode |
| 亂碼:锟斤拷 / 涓婁竴 | GBK/Big5 被當作 UTF-8 讀取、或 UTF-8 被當作 GBK 讀取 | 在 PHP 中 mb_detect_encoding 實際編碼 | 統一連線編碼、補充轉碼層 |
| 問號 ??? / 方框 □ | 字型不支援字元、或編碼轉換丟失字元 | 換字體測試、檢查 //IGNORE 是否丟失 | 用支援 CJK 的字體;避免 //IGNORE 靜默丟失 |
| Emoji 變成 ???? | 資料庫 utf8 (3字節) 不支援 Emoji (4字節) | SHOW CREATE TABLE 確認 CHARSET | 改用 utf8mb4,MySQL 5.5.3+ 支援 |
| JSON 輸出中文成 \uXXXX | json_encode 預設轉義 Unicode | 正常行為,非亂碼 | 加 JSON_UNESCAPED_UNICODE 旗標 |
| CSV 下載 Excel 打開亂碼 | Excel 預設用系統編碼 (中文 Windows 為 GBK/CP936) | 用記事本開啟檢查實際編碼 | 輸出 UTF-8 with BOM (echo "\xEF\xBB\xBF" . $csv;) 或轉 GBK |
若無法立即將舊系統 (GBK/Big5) 全面遷移至 UTF-8,可採用「邊界轉碼」:
<?php
// 進入點:統一將輸入轉為 UTF-8 內部處理
function inputToUtf8(mixed $input): mixed {
// 假設舊系統前端為 GBK
return autoCharset($input, 'GBK', 'UTF-8');
}
// 出口:根據目標編碼輸出
function outputFromUtf8(mixed $data, string $targetEncoding = 'UTF-8'): mixed {
if ($targetEncoding === 'UTF-8') return $data;
return autoCharset($data, 'UTF-8', $targetEncoding);
}
// 中介軟體範例 (Slim/Laravel/原生)
$app->add(function ($request, $handler) {
// 請求進入:GET/POST/JSON 轉 UTF-8
$_GET = inputToUtf8($_GET);
$_POST = inputToUtf8($_POST);
$_COOKIE = inputToUtf8($_COOKIE);
if ($request->getHeaderLine('Content-Type') === 'application/json') {
$parsed = json_decode($request->getBody()->getContents(), true);
$request = $request->withParsedBody(inputToUtf8($parsed));
}
$response = $handler->handle($request);
// 響應輸出:若客戶端需 GBK (如舊版 IE、特定 API)
if ($request->getHeader('Accept-Charset')[0] ?? '' === 'GBK') {
$body = outputFromUtf8((string)$response->getBody(), 'GBK');
$response = $response->withBody(new \Slim\Psr7\Stream(fopen('php://memory', 'rw')));
$response->getBody()->write($body);
$response = $response->withHeader('Content-Type', 'text/html; charset=GBK');
}
return $response;
});
?>
-- 1. 檢查現狀
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE your_table;
-- 2. 修改資料庫預設編碼
ALTER DATABASE your_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-- 3. 修改資料表編碼 (含現有資料轉換)
ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 4. 修改欄位編碼 (若 CONVERT 失敗)
ALTER TABLE your_table CHANGE col_name col_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 5. 連線指定編碼 (PDO)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci',
]);
中文亂碼無捷徑,唯有全鏈路 UTF-8 一致性。核心步驟:
<meta charset="UTF-8"> + HTTP Header Content-Type: text/html; charset=UTF-8mb_internal_encoding('UTF-8')autoCharset() 遞迴處理陣列/物件JSON_UNESCAPED_UNICODE,CSV 下載加 BOM徹底解決亂碼,不再是「試錯」,而是「配置」。