CSV 是資料交換最通用的格式,但跨平台(Windows ↔ Linux)匯入時,編碼問題層出不窮:中文變亂碼、欄位錯位、甚至導入失敗。本文系統整理兩大核心問題的成因與解決方案,並提供可直接部署的完整導入代碼。
Windows 繁體/簡體中文版預設編碼為 CP950 (Big5) / CP936 (GBK),而 PHP 內部與資料庫多使用 UTF-8。直接用 fgetcsv() 讀取 GBK/Big5 編碼的 CSV,中文會被誤判為單字節字元,導致截斷或亂碼。
<?php
// 讀取每行後,立即轉換編碼
$line = fgetcsv($handle, 1000, ',');
if ($line) {
// 方法一:mb_convert_encoding(需安裝 mbstring 擴充)
$line = array_map(function($v) {
return mb_convert_encoding($v, 'UTF-8', 'GBK'); // 或 'CP950'、'Big5'
}, $line);
// 方法二:iconv(較通用)
$line = array_map(function($v) {
return iconv('GBK', 'UTF-8//IGNORE', $v); // IGNORE 忽略無法轉換字元
}, $line);
}
?>
fgetcsv() 讀取單位元編碼錯誤Linux 伺服器 Locale 常設為 en_US.UTF-8。fgetcsv() 對 Locale 設定敏感:當 Locale 為 UTF-8 時,遇到單位元編碼(GBK/Big5)的 CSV,會因多字節字元被誤判為分隔符或換行,導致欄位錯位、行數錯誤。
<?php // 在 fopen/fgetcsv 之前呼叫,僅影響當前程序 setlocale(LC_ALL, 'zh_CN.GBK'); // 簡體中文 GBK // 或 setlocale(LC_ALL, 'zh_TW.Big5'); // 繁體中文 Big5 // 或通用 setlocale(LC_ALL, 'zh_CN'); // 視系統安裝的 locale 而定 // 驗證是否生效 var_dump(setlocale(LC_CTYPE, 0)); // 輸出當前 LC_CTYPE 設定 ?>
關鍵點:
locale -a 查看)。zh_CN.GBK,需安裝語言包(Ubuntu: apt-get install language-pack-zh-hans)。setlocale() 設定的是 進程級 Locale,不影響其他 PHP-FPM worker 或系統全域設定。<?php
/**
* 通用 CSV 導入類
* 自動偵測編碼、設定 Locale、批次寫入資料庫
*/
class CsvImporter {
private $handle;
private $encoding = 'UTF-8'; // 目標編碼
private $source_encoding = 'GBK'; // 來源編碼(可由 mb_detect_encoding 偵測)
private $locale_set = false;
public function __construct(string $file_path) {
// 1. 設定 Locale(解決 Linux fgetcsv 單位元問題)
$this->setLocale();
// 2. 開啟檔案
$this->handle = fopen($file_path, 'r');
if (!$this->handle) throw new Exception("無法開啟檔案: {$file_path}");
// 3. 偵測來源編碼(讀取前幾行樣本)
$sample = fread($this->handle, 8192);
rewind($this->handle);
$detected = mb_detect_encoding($sample, ['GBK', 'BIG5', 'UTF-8', 'CP950'], true);
if ($detected) $this->source_encoding = $detected;
}
private function setLocale(): void {
if ($this->locale_set) return;
$locales = ['zh_CN.GBK', 'zh_CN', 'zh_TW.Big5', 'zh_TW', 'C'];
foreach ($locales as $loc) {
if (setlocale(LC_ALL, $loc) !== false) {
$this->locale_set = true;
break;
}
}
if (!$this->locale_set) {
trigger_error('無法設定任何中文 Locale,fgetcsv 可能異常', E_USER_WARNING);
}
}
public function import(callable $row_handler, int $batch_size = 100): int {
$count = 0;
$batch = [];
// 跳過標題列(視需求)
fgetcsv($this->handle);
while (($row = fgetcsv($this->handle, 4096, ',')) !== false) {
// 編碼轉換
$row = array_map(function($v) {
return mb_convert_encoding($v, $this->encoding, $this->source_encoding);
}, $row);
$batch[] = $row;
$count++;
if (count($batch) >= $batch_size) {
$row_handler($batch);
$batch = [];
}
}
if ($batch) $row_handler($batch);
fclose($this->handle);
return $count;
}
}
// === 使用範例 ===
try {
$importer = new CsvImporter($_FILES['csv_file']['tmp_name']);
$imported = $importer->import(function($rows) {
// $rows 為二維陣列,每筆已轉為 UTF-8
foreach ($rows as $row) {
// 寫入資料庫(PDO 範例)
$stmt = $pdo->prepare("INSERT INTO products (name, price, description) VALUES (?, ?, ?)");
$stmt->execute([$row[0], $row[1], $row[2]]);
}
});
echo "成功導入 {$imported} 筆資料";
} catch (Exception $e) {
echo "導入失敗: " . $e->getMessage();
}
?>
| Locale 識別碼 | 適用編碼 | 說明 |
|---|---|---|
| zh_CN.GBK / zh_CN | GBK / CP936 | 大陸簡體中文預設 |
| zh_TW.Big5 / zh_TW | Big5 / CP950 | 台灣繁體中文預設 |
| zh_HK.Big5-HKSCS | Big5-HKSCS | 香港繁體擴展字符集 |
| en_US.UTF-8 | UTF-8 | 英文系統預設,讀取單位元 CSV 會出錯 |
EF BB BF),導致第一欄位多出隱形字元。讀取首行後用 ltrim($row[0], "\xEF\xBB\xBF") 移除。CRLF、Linux LF、舊 Mac CR。PHP 5.3+ fgetcsv() 已支援 auto_detect_line_endings,可在開啟檔案前設定 ini_set('auto_detect_line_endings', true)。fgetcsv() 會自動處理,但若資料本身含雙引號需轉義(雙寫 "")。fgetcsv() 逐行讀取而非 file() 一次載入全檔,配合批次寫入資料庫,避免記憶體耗盡。CSV 匯入亂碼的核心在於 來源編碼識別 與 Locale 正確設定。採用「偵測編碼 → 設定 Locale → 逐行讀取 → 即時轉碼 → 批次入庫」的標準化流程,並封裝為可複用類別,即可一次解決 Windows/Linux 跨平台所有編碼難題。