中文亂碼的本質只有一個:**編碼不一致**。資料流經「瀏覽器 → PHP 腳本 → MySQL 連接 → 資料表/欄位 → 存儲引擎」每一環,編碼必須**完全一致**。本文以實戰案例拆解配置要點,附帶 AJAX 特殊場景處理。
| 層級 | 檢查項 | 推薦設定 (UTF-8 體系) |
|---|---|---|
| 1. 資料庫/表/欄位 | CHARSET / COLLATION | utf8mb4 / utf8mb4_unicode_ci (支援 Emoji) |
| 2. PHP 腳本檔案 | 檔案儲存編碼 | **UTF-8 無 BOM** (IDE 右下角確認) |
| 3. HTTP 響應頭 | Content-Type | header('Content-Type: text/html; charset=utf-8'); |
| 4. HTML 聲明 | <meta> 標籤 | <meta charset="utf-8"> (HTML5 寫法) |
| 5. MySQL 連接 | 連接層字元集 | SET NAMES utf8mb4 (或驅動層配置) |
| 6. 表單提交 | 表單編碼 | <form accept-charset="utf-8"> |
**核心誤區:** 以為資料庫建表用 `utf8` 就萬事大吉。**連接層編碼 (第 5 項) 是最常被忽略、也是最關鍵的一環。**
老專案頁面編碼為 GBK,資料庫已遷移至 UTF-8,不想全站改碼時的處理:
CREATE TABLE `useradmin` (
`id` int(4) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`userpass` varchar(40) DEFAULT NULL,
`logins` int(4) NOT NULL DEFAULT '0',
`logintime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`mid` char(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
<?php
// 頁面編碼 GBK (已由 header/meta 聲明)
// 1. 建立連接 (mysqli 面向對象風格推薦)
$mysqli = new mysqli('localhost', 'user', 'password', 'message');
if ($mysqli->connect_errno) {
die("Connect failed: " . $mysqli->connect_error);
}
// 2. 【核心】設定連接層編碼為 UTF-8,匹配資料庫表編碼
// 告訴 MySQL:「我發送的 SQL 是 UTF-8,請用 UTF-8 返回結果」
if (!$mysqli->set_charset('utf8mb4')) {
die("Error loading character set utf8mb4: " . $mysqli->error);
}
// 3. 執行查詢/寫入 (此時 PHP 內部字串為 GBK,需轉碼)
// 寫入:GBK -> UTF-8
$username = iconv('GBK', 'UTF-8', $_POST['username']);
$sql = "INSERT INTO useradmin (username) VALUES (?)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('s', $username);
$stmt->execute();
// 讀出:UTF-8 -> GBK (輸出給 GBK 頁面)
$result = $mysqli->query("SELECT username FROM useradmin WHERE id=1");
$row = $result->fetch_assoc();
echo iconv('UTF-8', 'GBK', $row['username']); // 頁面顯示正常
$mysqli->close();
?>
**新專案零理由使用非 UTF-8 編碼。** 統一 UTF-8 後,代碼極度簡化:
<?php
// 1. 檔案存 UTF-8 無 BOM
// 2. 頁面頭
header('Content-Type: text/html; charset=utf-8');
// 3. HTML5
// <meta charset="utf-8">
// 4. PDO 連接 (DSN 指定 charset)
$dsn = "mysql:host=localhost;dbname=message;charset=utf8mb4";
$pdo = new PDO($dsn, 'user', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 使用真實預處理
]);
// 5. 直接操作,無需任何 iconv/set_charset
$stmt = $pdo->prepare("INSERT INTO useradmin (username) VALUES (?)");
$stmt->execute([$_POST['username']]); // $_POST 已是 UTF-8
$stmt = $pdo->query("SELECT username FROM useradmin WHERE id=1");
echo $stmt->fetchColumn(); // 直接輸出
?>
PDO DSN 中 `charset=utf8mb4` 等同於自動執行 `SET NAMES utf8mb4`,最優雅、最不易出錯。
**鐵律:AJAX 請求 (fetch/XMLHttpRequest/axios) 僅支援 UTF-8 傳輸。**
若主頁面為 GBK,AJAX 子請求**必須**在 PHP 端強制處理:
<?php
// AJAX 入口檔案 ajax_handler.php
// 1. 強制響應頭 UTF-8
header('Content-Type: application/json; charset=utf-8');
// 2. 若資料來源為 GBK (舊資料庫/快取),轉碼輸出
$data = getDataFromLegacyGBKSource(); // 返回 GBK 字串
echo json_encode([
'code' => 0,
'data' => array_map(function($v) { return iconv('GBK', 'UTF-8', $v); }, $data)
]);
?>
前端 JS 無需任何處理,直接接收 UTF-8 JSON。
| 現象 | 可能原因 | 排查方向 |
|---|---|---|
| (菱形問號) | UTF-8 資料被當作 GBK 解碼 | 檢查頁面 meta、header、資料庫連接 charset |
| 亂碼但長度變短 (如 3 字變 2 字) | 雙字節字元被單字節解析 | 同左,重點檢查連接層 SET NAMES |
| ????? (問號) | 字元超出目標編碼範圍 (如 GBK 存 Emoji) | 升級資料庫/表/欄位/連接至 utf8mb4 |
| 正常入庫,讀出亂碼 | 寫入連接編碼 ≠ 讀出連接編碼 | 統一連接 charset,或 PDO DSN 指定 charset |
CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci。做到這三點,中文亂碼將成為歷史。