CSV 匯入是常見需求,但編碼問題頻發:Windows 產出的 CSV 多為 Big5/GBK,Linux 環境預設 UTF-8,直接 fgetcsv 會產生亂碼。本文提供雙重解決方案:PHP 端編碼轉換與系統 Locale 設定。
<?php
function get_csv_contents($file_target) {
$handle = fopen($file_target, 'r');
while ($data = fgetcsv($handle, 1000, ",")) {
$num = count($data);
echo " $num fields in line $row:
\n";
$row++;
for ($c = 0; $c < $num; $c++) {
echo $data[$c] . "
\n"; // 這裡出現亂碼
}
}
fclose($handle);
}
?>
原因: CSV 為 ANSI/GBK 編碼,PHP 腳本執行環境為 UTF-8,瀏覽器以 UTF-8 顯示 → 亂碼。
<?php
function readCsvAutoEncoding($filePath, $targetEncoding = 'UTF-8') {
$handle = fopen($filePath, 'r');
if (!$handle) throw new Exception("無法開啟檔案: $filePath");
$rows = [];
// 偵測編碼(簡易版:嘗試常見編碼)
$encodings = ['GBK', 'BIG5', 'UTF-8', 'UTF-16LE', 'UTF-16BE'];
while (($data = fgetcsv($handle, 0, ",")) !== false) {
$converted = [];
foreach ($data as $cell) {
$detected = mb_detect_encoding($cell, $encodings, true);
if ($detected && $detected !== $targetEncoding) {
$converted[] = mb_convert_encoding($cell, $targetEncoding, $detected);
} else {
$converted[] = $cell;
}
}
$rows[] = $converted;
}
fclose($handle);
return $rows;
}
// 使用
$rows = readCsvAutoEncoding('data.csv');
foreach ($rows as $row) {
print_r($row);
}
?>
<?php
// 明確指定來源為 GBK
$data = fgetcsv($handle, 1000, ",");
$data = array_map(function($cell) {
return mb_convert_encoding($cell, 'UTF-8', 'GBK');
}, $data);
// 或使用 eval 技巧(原文方法,不推薦生產環境)
$data = eval('return ' . iconv('GBK', 'UTF-8', var_export($data, true)) . ';');
?>
fgetcsv 依賴 C 函式庫 strtok 解析,受 LC_CTYPE 影響。若系統 Locale 不支援來源編碼,解析即會錯誤(不只是顯示亂碼,連欄位分割都會錯)。
# Linux 終端機執行
locale -a | grep -E 'zh_(CN|TW)'
# 輸出範例:
# zh_CN.gb2312
# zh_CN.gbk
# zh_CN.gb18030
# zh_CN.utf8
# zh_TW.big5
# zh_TW.utf8
<?php
// 在 fgetcsv 前設定
$localeSet = setlocale(LC_ALL, [
'zh_CN.gbk',
'zh_CN.gb2312',
'zh_CN.gb18030',
'zh_TW.big5',
'C' // 兜底
]);
if ($localeSet === false) {
error_log('Warning: 無法設定中文 Locale,CSV 解析可能異常');
}
// 現在可正確解析 GBK/Big5 CSV
$handle = fopen('data.csv', 'r');
while (($data = fgetcsv($handle, 0, ",")) !== false) {
// $data 已正確分割,但編碼仍為原始編碼
// 後續仍需 mb_convert_encoding 轉 UTF-8
}
fclose($handle);
?>
<?php
class CsvImporter {
private $sourceEncoding;
private $targetEncoding = 'UTF-8';
public function __construct($sourceEncoding = 'auto') {
$this->sourceEncoding = $sourceEncoding;
}
public function import($filePath, callable $rowHandler) {
// 1. 設定 Locale 確保正確分割
$this->setLocaleForEncoding($this->sourceEncoding);
$handle = fopen($filePath, 'r');
if (!$handle) throw new Exception("檔案不存在或不可讀: $filePath");
$headers = null;
$rowNum = 0;
while (($data = fgetcsv($handle, 0, ",")) !== false) {
$rowNum++;
// 2. 編碼轉換
if ($this->sourceEncoding !== 'auto') {
$data = $this->convertRow($data, $this->sourceEncoding);
} else {
$data = $this->autoConvertRow($data);
}
// 3. 標題列處理
if ($rowNum === 1) {
$headers = $data;
continue;
}
// 4. 組合關聯陣列
if ($headers) {
$row = array_combine($headers, $data);
} else {
$row = $data;
}
// 5. 交給呼叫端處理
$rowHandler($row, $rowNum);
}
fclose($handle);
}
private function setLocaleForEncoding($encoding) {
$localeMap = [
'GBK' => ['zh_CN.gbk', 'zh_CN.gb2312', 'zh_CN.gb18030'],
'BIG5' => ['zh_TW.big5'],
'UTF-8' => ['zh_CN.utf8', 'zh_TW.utf8', 'en_US.utf8'],
];
$locales = $localeMap[strtoupper($encoding)] ?? ['C'];
setlocale(LC_ALL, $locales);
}
private function convertRow($row, $fromEncoding) {
return array_map(fn($cell) => mb_convert_encoding($cell, $this->targetEncoding, $fromEncoding), $row);
}
private function autoConvertRow($row) {
return array_map(function($cell) {
$detected = mb_detect_encoding($cell, ['GBK', 'BIG5', 'UTF-8'], true);
return $detected && $detected !== $this->targetEncoding
? mb_convert_encoding($cell, $this->targetEncoding, $detected)
: $cell;
}, $row);
}
}
// 使用範例
$importer = new CsvImporter('GBK'); // 或 'auto'
$importer->import('data.csv', function($row, $rowNum) {
// $row 為 ['欄位名' => '值', ...] 已轉 UTF-8
User::create($row);
});
?>
| 層面 | 方案一:PHP 轉碼 | 方案二:Locale 設定 |
|---|---|---|
| 解決問題 | 顯示層亂碼 | 解析層欄位錯誤 |
| 適用場景 | 已正確分割欄位,僅編碼不符 | 中文欄位含逗號、引號導致分割錯誤 |
| 可攜性 | 高(純 PHP) | 依賴系統安裝的 Locale |
| 效能 | 略低(逐儲存格轉碼) | 高(C 層級正確解析) |
| 建議 | 必須做 | Linux 伺服器必須做 |
核心結論:Locale 確保正確分割,PHP 轉碼確保正確顯示,兩者緩一不可,共同構建健壯的 CSV 匯入管道。