RFC 4180 定義 CSV 格式,但實際檔案常見:編碼不一 (UTF-8/BIG5/GBK)、分隔符非逗號、欄位含換行/逗號/引號、BOM 頭、標題列缺失、行數百萬。本文提供生產級讀取方案。
| 工具 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| SplFileObject | 物件導向、可迭代、內建 CSV 模式、記憶體友善 | PHP 5.1+、編碼轉換需自行處理 | ✅ 首選 (中小檔) |
| fgetcsv() | 程序式、精細控制、相容舊代碼 | 需手動 fopen/fclose、易忘記釋放資源 | 舊專案維護 |
| League\Csv | 功能最全 (轉換、過濾、驗證、Chunk)、支援 PHP 8.1+ | 額外依賴、學習曲線 | ✅ 複雜需求首選 |
| fgets + str_getcsv | 自定義分隔符邏輯 | 效能差、易錯 | 極特殊格式 |
<?php
function readCsv(string $path, string $encoding = 'UTF-8', string $delimiter = ','): \Generator {
$file = new SplFileObject($path, 'r');
$file->setFlags(
SplFileObject::READ_CSV |
SplFileObject::READ_AHEAD | // 預讀下一行
SplFileObject::SKIP_EMPTY | // 跳過空行
SplFileObject::DROP_NEW_LINE
);
$file->setCsvControl($delimiter, '"', '\\'); // 分隔符、引號、跳脫
$header = null;
foreach ($file as $rowNum => $row) {
// 編碼轉換 (若檔案非 UTF-8)
if ($encoding !== 'UTF-8') {
$row = array_map(fn($v) => mb_convert_encoding($v, 'UTF-8', $encoding), $row);
}
if ($header === null) {
$header = $row; // 第一行當標題
continue;
}
// 組合關聯陣列
yield array_combine($header, $row);
}
}
// 使用:逐行處理,記憶體恆定
foreach (readCsv('data.csv', 'BIG5') as $record) {
// $record = ['id'=>'1', 'name'=>'張三', 'email'=>'...']
processRecord($record);
}
composer require league/csv
use League\Csv\Reader;
use League\Csv\Statement;
$reader = Reader::createFromPath('big_data.csv', 'r');
$reader->setHeaderOffset(0); // 第一行為標題
$reader->setDelimiter(';');
$reader->setEncoding('ISO-8859-1'); // 內部自動轉 UTF-8
// Statement: 類 SQL 查詢 (篩選、切片、排序)
$stmt = (new Statement())
->where(fn($row) => $row['status'] === 'active')
->offset(1000)
->limit(500);
$records = $stmt->process($reader);
foreach ($records as $record) {
// $record 為關聯陣列
exportToApi($record);
}
fopen 讀前 3 bytes,EF BB BF = UTF-8 BOM。mb_detect_encoding 輔助:mb_detect_encoding($sample, ['UTF-8', 'BIG5', 'GBK', 'ISO-8859-1'], true)。mb_convert_encoding($str, 'UTF-8', $from, 'UTF-8//IGNORE') 或拋出異常記錄錯誤行。<?php
class CsvStreamProcessor {
public function __construct(
private string $path,
private int $batchSize = 1000,
private string $encoding = 'UTF-8'
) {}
public function process(callable $handler): void {
$reader = Reader::createFromPath($this->path, 'r');
$reader->setHeaderOffset(0);
$reader->setEncoding($this->encoding);
$batch = [];
foreach ($reader as $record) {
$batch[] = $record;
if (count($batch) >= $this->batchSize) {
$handler($batch);
$batch = [];
// 可選:釋放記憶體、GC
gc_collect_cycles();
}
}
if ($batch) $handler($batch); // 餘數
}
}
// 使用:批次寫入資料庫、推送隊列
$processor = new CsvStreamProcessor('huge.csv', batchSize: 500);
$processor->process(function(array $batch) {
DB::table('users')->upsert($batch, 'email', ['name', 'phone']);
// 或推送 RabbitMQ
foreach ($batch as $record) $queue->publish($record);
});
| 錯誤現象 | 原因 | 解決 |
|---|---|---|
| 欄位數不一致警告 | 某行欄位含未閉合引號、換行 | 檢查來源匯出邏輯、或用 League\Csv 容錯模式 |
| 中文亂碼 | 編碼未正確轉換 | 確認來源編碼、統一轉 UTF-8、處理 BOM |
| 記憶體耗盡 | 一次性 file() 或 array_map 載入全檔 | 改用 Generator/Iterator 串流處理 |
| 科學記號 (1.2E+10) | Excel 匯出長數字自動科學記號 | 匯出前設定欄位格式為文字、或讀取後 sprintf('%.0f', $val) |
| 前導零消失 (00123 → 123) | PHP 自動轉型為整數 | 讀取時保持字串、array_combine 不轉型、資料庫存 CHAR/VARCHAR |
完整 Library 整合 (Laravel CSV Importer、Symfony CSV Bundle) 見 GitHub。