fgetcsv() 是 PHP 內建的 CSV 行解析函數,能正確處理欄位包含逗號、引號、換行等複雜情況。本文詳細說明其參數、編碼陷阱、大檔案處理策略與實務最佳實踐。
fgetcsv(
resource $handle, // fopen() 回傳的檔案指標
int $length = 0, // 最大行長度 (0 = 無限制,PHP 5.1+ 建議省略)
string $separator = ',', // 分隔符 (單一字元)
string $enclosure = '"', // 包裹字元 (單一字元)
string $escape = '\\' // 跳脫字元 (單一字元,PHP 7.4+)
): array|false
<?php
$file = fopen('data.csv', 'r');
if (!$file) die('無法開啟檔案');
// 可選:跳過標題列
$header = fgetcsv($file);
echo "欄位: " . implode(', ', $header) . "\n";
while (($row = fgetcsv($file)) !== false) {
// $row 為數值索引陣列 [欄位1, 欄位2, ...]
echo "ID: {$row[0]}, Name: {$row[1]}, Email: {$row[2]}\n";
}
fclose($file);
?>
測試檔案 test.csv 內容:
id,name,description 1,Normal,"Simple value" 2,"With, comma","Contains, comma" 3,"Quotes ""escaped""","Double quotes" 4,"Multi line","Newline in field" 5,,Empty field 6,"Trailing comma,",
fgetcsv() 不處理字元編碼,它按位元組切割。若 CSV 為 Big5/GBK 但 PHP 腳本為 UTF-8,中文會變亂碼或切割錯誤。
<?php
function fopen_utf8($filename, $mode = 'r') {
$handle = fopen($filename, $mode);
if (!$handle) return false;
// 偵測 BOM
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle); // 無 BOM,回到開頭
}
return $handle;
}
$handle = fopen_utf8('big5.csv');
if (!$handle) die('開啟失敗');
// 逐行讀取並轉碼
while (($row = fgetcsv($handle)) !== false) {
$row = array_map(function($v) {
return mb_convert_encoding($v, 'UTF-8', 'BIG-5');
}, $row);
// 處理 $row...
}
fclose($handle);
?>
<?php
$file = new SplFileObject('big5.csv');
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$file->setCsvControl(','); // 分隔符
// 註冊編碼轉換過濾器 (需 php_stream_filter)
stream_filter_register('convert.big5-utf8', 'php_user_filter'); // 簡化示意
foreach ($file as $row) {
// $row 已是陣列,但編碼仍為原始
$row = array_map('mb_convert_encoding', $row, array_fill(0, count($row), 'UTF-8'), array_fill(0, count($row), 'BIG-5'));
print_r($row);
}
?>
# Linux 指令列預處理
iconv -f BIG5 -t UTF-8 big5.csv > utf8.csv
# PHP 內執行
exec('iconv -f BIG5 -t UTF-8 ' . escapeshellarg($input) . ' > ' . escapeshellarg($output));
// 然後用 UTF-8 模式讀取 utf8.csv
<?php
function processLargeCsv($filename, callable $callback, $chunkSize = 1000) {
$handle = fopen($filename, 'r');
if (!$handle) throw new Exception("無法開啟: $filename");
$header = fgetcsv($handle);
$buffer = [];
$count = 0;
while (($row = fgetcsv($handle)) !== false) {
$buffer[] = array_combine($header, $row); // 關聯陣列
$count++;
if (count($buffer) >= $chunkSize) {
$callback($buffer, $count);
$buffer = [];
// 可選:釋放記憶體
gc_collect_cycles();
}
}
// 處理剩餘
if ($buffer) {
$callback($buffer, $count);
}
fclose($handle);
return $count;
}
// 使用:分批寫入資料庫
processLargeCsv('huge.csv', function($rows, $total) use ($pdo) {
$sql = "INSERT INTO users (name, email, phone) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
$pdo->beginTransaction();
foreach ($rows as $row) {
$stmt->execute([$row['name'], $row['email'], $row['phone']]);
}
$pdo->commit();
echo "已處理 $total 筆\n";
}, 5000);
?>
| 場景 | 參數設定 | 說明 |
|---|---|---|
| 分號分隔 (歐洲格式) | fgetcsv($fp, 0, ';') | 第二參數 0 表示無長度限制 |
| Tab 分隔 (TSV) | fgetcsv($fp, 0, "\t") | 分隔符可為任何單一字元 |
| 單引號包裹 | fgetcsv($fp, 0, ',', "'") | 第三參數 enclosure |
| 欄位含逗號、引號、換行 | 預設即支援 | 需正確包裹在 enclosure 內 |
| 跳脫字元非反斜線 | fgetcsv($fp, 0, ',', '"', '|') | 第五參數 escape (PHP 7.4+) |
<?php
function validateCsvRow(array $row, array $rules) {
$errors = [];
foreach ($rules as $field => $rule) {
$value = $row[$field] ?? null;
// 必填
if (($rule['required'] ?? false) && ($value === null || $value === '')) {
$errors[$field] = '必填欄位';
continue;
}
if ($value === null) continue; // 非必填且無值,跳過後續驗證
// 型別/格式
if (isset($rule['type'])) {
switch ($rule['type']) {
case 'int':
if (!ctype_digit($value)) $errors[$field] = '需為整數';
break;
case 'email':
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) $errors[$field] = 'Email 格式錯誤';
break;
case 'date':
$d = DateTime::createFromFormat('Y-m-d', $value);
if (!$d || $d->format('Y-m-d') !== $value) $errors[$field] = '日期格式 YYYY-MM-DD';
break;
}
}
// 長度
if (isset($rule['max_len']) && mb_strlen($value) > $rule['max_len']) {
$errors[$field] = "長度超過 {$rule['max_len']}";
}
}
return $errors;
}
// 整合範例
$rules = [
'id' => ['required'=>true, 'type'=>'int'],
'name' => ['required'=>true, 'max_len'=>50],
'email' => ['required'=>true, 'type'=>'email'],
];
$handle = fopen('users.csv', 'r');
$header = fgetcsv($handle);
$line = 1;
while (($row = fgetcsv($handle)) !== false) {
$line++;
$data = array_combine($header, $row);
$errors = validateCsvRow($data, $rules);
if ($errors) {
error_log("第 $line 行驗證失敗: " . json_encode($errors));
continue; // 或寫入錯誤檔
}
// 正常處理...
}
fclose($handle);
?>
| 方法 | 速度 | 記憶體 | 功能 | 建議場景 |
|---|---|---|---|---|
fgetcsv() 迴圈 | 快 | 低 (串流) | 基礎解析 | 大檔案、記憶體受限 |
SplFileObject::READ_CSV | 快 | 低 | 迭代器介面、可求長度 | 物件導向風格 |
str_getcsv() + file() | 慢 | 高 (全載入) | 單行解析 | 小檔案、簡單腳本 |
| League\Csv (套件) | 中 | 中 | 豐富 (轉換、驗證、選單) | 複雜專案、需維護性 |
fgetcsv() 迴圈,不要 file() 全讀入