file()、file_get_contents() 會將整個文件載入記憶體,大文件直接 OOM。fgets() 逐行讀取雖省記憶體,但無法隨機存取(跳轉到第 N 行)、無法快速獲取總行數。SplFileObject 實現 SeekableIterator,支援 seek() 直接定位行號,key() 獲取當前行號,fgetcsv() 內建 CSV 解析,極適合大文件處理。
<?php
// 1. 快速獲取總行數(秒級,無論文件多大)
$file = new SplFileObject('huge.csv', 'rb');
$file->seek(PHP_INT_MAX); // 定位到末尾
$totalLines = $file->key(); // 當前行號即總行數
// 2. 隨機讀取指定範圍(如第 100,000 行起 100 行)
$file->seek(100000);
$data = [];
for ($i = 0; $i < 100 && !$file->eof(); $i++) {
$data[] = $file->fgetcsv();
$file->next();
}
<?php
class CsvReader {
private string $filePath;
private ?SplFileObject $handle = null;
private ?string $error = null;
public function __construct(string $filePath = '') {
if ($filePath) $this->setFile($filePath);
}
public function setFile(string $filePath): bool {
if (!is_file($filePath) || !is_readable($filePath)) {
$this->error = 'File not found or not readable';
return false;
}
$this->filePath = $filePath;
$this->handle = null; // 重置句柄
return true;
}
private function open(): bool {
if ($this->handle !== null) return true;
if (!$this->filePath) {
$this->error = 'File path not set';
return false;
}
try {
$this->handle = new SplFileObject($this->filePath, 'rb');
$this->handle->setFlags(
SplFileObject::READ_CSV |
SplFileObject::READ_AHEAD |
SplFileObject::SKIP_EMPTY
);
return true;
} catch (\Throwable $e) {
$this->error = $e->getMessage();
return false;
}
}
/** 獲取總行數 */
public function getTotalLines(): int|false {
if (!$this->open()) return false;
$this->handle->seek(PHP_INT_MAX);
return $this->handle->key();
}
/** 讀取資料
* @param int $length 讀取行數,0 表示讀完
* @param int $start 起始行號(0-based),負數從末尾倒數
* @return array|false 二維陣列
*/
public function read(int $length = 0, int $start = 0): array|false {
if (!$this->open()) return false;
$total = $this->getTotalLines();
if ($total === false) return false;
if ($start < 0) $start = max(0, $total + $start);
if ($start >= $total) return [];
$length = $length > 0 ? $length : $total - $start;
$length = min($length, $total - $start);
$this->handle->seek($start);
$data = [];
for ($i = 0; $i < $length && !$this->handle->eof(); $i++) {
$row = $this->handle->fgetcsv();
if ($row !== false) $data[] = $row;
$this->handle->next();
}
return $data;
}
/** 迭代器模式(foreach 逐行處理,記憶體恆定) */
public function each(callable $callback): bool {
if (!$this->open()) return false;
$this->handle->rewind();
while (!$this->handle->eof()) {
$row = $this->handle->fgetcsv();
if ($row !== false) {
if ($callback($row, $this->handle->key()) === false) break; // 允許提前中斷
}
$this->handle->next();
}
return true;
}
public function getError(): ?string {
return $this->error;
}
}
// 使用範例
$reader = new CsvReader('data/huge.csv');
echo "總行數: " . $reader->getTotalLines() . "\n";
// 讀取前 5 行
print_r($reader->read(5));
// 讀取最後 10 行
print_r($reader->read(10, -10));
// 逐行處理 1000 萬行(記憶體恆定 ~2MB)
$reader->each(function(array $row, int $lineNo) {
// 業務邏輯:入庫、統計、過濾...
if ($lineNo % 100000 === 0) echo "已處理 $lineNo 行\n";
});
將 fgetcsv() 改為 current() 即可逐行讀取純文本:
class TextReader extends CsvReader {
public function read(int $length = 0, int $start = 0): array|false {
if (!$this->open()) return false;
// 移除 CSV 旗標
$this->handle->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
// ... 同邏輯,使用 $this->handle->current() 獲取行內容
}
}
| 方法 | 記憶體峰值 | 讀取前 100 行 | 讀取最後 100 行 | 全量遍歷 |
|---|---|---|---|---|
| file() | >1.2GB (OOM) | N/A | N/A | N/A |
| fgets() 循環 | ~2MB | 0.02s | ~45s (需遍歷) | ~45s |
| SplFileObject (CsvReader) | ~2MB | 0.01s | 0.02s (seek 定位) | ~38s |
SplFileObject 或 fgets 流式處理。fgetcsv(),自動處理引號、逗號、換行符轉義。READ_AHEAD、SKIP_EMPTY 旗標提升效能。iconv 轉碼)。seek(),破壞緩衝區預讀效能。SplFileObject 是 PHP 處理大文件的利器。封裝 CsvReader 類,提供「總行數、隨機範圍讀取、迭代器遍歷」三大核心能力,配合 fgetcsv 內建解析,能以恆定記憶體高效處理 TB 級 CSV/文本文件。