| 場景 | 典型現象 | 核心原因 |
|---|---|---|
| Windows 開發環境 | 中文顯示為 或 縮 | CSV 為 GB2312/GBK,PHP 腳本為 UTF-8,未轉碼 |
| Linux 生產環境 | 同樣代碼讀取失敗或亂碼 | 系統 Locale 為 en_US.UTF-8,fgetcsv() 按單字節解析多字節字符 |
| 跨平台提交 | Windows 編寫 → Linux 運行失效 | 文件編碼 + 系統 Locale 雙重不匹配 |
<?php
// 必須在任何 fgetcsv() 呼叫之前
setlocale(LC_ALL, 'zh_CN.GBK', 'zh_CN', 'Chinese_China.936');
// 返回值檢查
if (setlocale(LC_ALL, 'zh_CN.GBK') === false) {
error_log("Locale zh_CN.GBK 不可用,請執行:locale-gen zh_CN.GBK");
}
?>
原理:fgetcsv() 依賴 C 库 setlocale 判斷多字節字符邊界。Linux 默認 en_US.UTF-8 會將 GBK 雙字節誤判為兩個單字節,導致欄位分割錯亂。
<?php
function readCsvSafe($filePath) {
$handle = fopen($filePath, 'r');
if (!$handle) throw new Exception("無法打開文件:$filePath");
// 讀取首行檢測編碼
$firstLine = fgets($handle, 4096);
rewind($handle); // 重置指針
$encoding = mb_detect_encoding($firstLine, ['GB2312', 'GBK', 'UTF-8', 'ASCII'], true);
if (!$encoding) throw new Exception("無法識別文件編碼");
$data = [];
while (($row = fgetcsv($handle, 0, ',')) !== false) {
// 統一轉為 UTF-8 供後續處理
$row = array_map(function($cell) use ($encoding) {
return $encoding === 'UTF-8' ? $cell : iconv($encoding, 'UTF-8//IGNORE', $cell);
}, $row);
$data[] = $row;
}
fclose($handle);
return $data;
}
?>
<?php
// import_csv.php
require_once 'vendor/autoload.php'; // 若使用 Composer
// 1. 設置 Locale(Linux 必須)
setlocale(LC_ALL, 'zh_CN.GBK', 'zh_CN', 'Chinese_China.936');
// 2. 文件上傳處理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) die("上傳失敗:錯誤碼 {$file['error']}");
// 副檔名校驗
if (pathinfo($file['name'], PATHINFO_EXTENSION) !== 'csv') die("僅支援 .csv 文件");
try {
$data = readCsvSafe($file['tmp_name']);
if (empty($data)) die("CSV 內容為空");
// 假設第一行為標題
$header = array_shift($data);
$header = array_map('trim', $header);
// 3. 業務邏輯:批量入庫示例(PDO 事務)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$pdo->beginTransaction();
$placeholders = implode(',', array_fill(0, count($header), '?'));
$sql = "INSERT INTO products (" . implode(',', $header) . ") VALUES ($placeholders)";
$stmt = $pdo->prepare($sql);
$success = 0;
foreach ($data as $rowNum => $row) {
if (count($row) !== count($header)) {
error_log("第 {$rowNum} 行欄位數不匹配,跳過");
continue;
}
$stmt->execute($row);
$success++;
}
$pdo->commit();
echo "✅ 成功導入 $success 條記錄";
} catch (Exception $e) {
if (isset($pdo)) $pdo->rollBack();
echo "❌ 導入失敗:" . $e->getMessage();
}
exit;
}
?>
<!DOCTYPE html>
<html><body>
<form method="post" enctype="multipart/form-data">
<input type="file" name="csv_file" accept=".csv" required>
<button type="submit">導入 CSV</button>
</form>
</body></html>
| 標識 | 編碼 | 適用系統 |
|---|---|---|
zh_CN.GBK / zh_CN | GBK/GB2312 | Linux (需 locale-gen zh_CN.GBK) |
Chinese_China.936 | GBK | Windows |
en_US.UTF-8 | UTF-8 | 通用(不適合 GBK CSV) |
zh_TW.BIG5 | BIG5 | 繁體中文環境 |
查看系統支援:locale -a (Linux) / 控制面板→地區 (Windows)。
header("Content-Type: ... charset=GB2312") 僅影響瀏覽器顯示,不改變 fgetcsv 解析行為。str_getcsv 替代:同樣受 Locale 影響,且需自行讀取文件內容。setlocale(LC_ALL, 'en_US.UTF-8'),徹底消除編碼痛苦。fgetcsv + 生成器 yield,避免一次性載入記憶體。<?php
function exportCsv(array $data, string $filename = 'export.csv', string $encoding = 'GBK') {
header("Content-Type: text/csv; charset=$encoding");
header("Content-Disposition: attachment; filename=\"$filename\"");
$out = fopen('php://output', 'w');
// 寫入 BOM(Excel 識別 UTF-8 必需)
if ($encoding === 'UTF-8') fwrite($out, "\xEF\xBB\xBF");
foreach ($data as $row) {
$row = array_map(fn($v) => $encoding === 'UTF-8' ? $v : iconv('UTF-8', "$encoding//IGNORE", $v), $row);
fputcsv($out, $row);
}
fclose($out);
}
?>