文件讀取是 PHP 後端開發的基礎技能。本文深入解析 feof() 配合 fgets()、fread() 的正確使用姿勢,揭示常見陷阱(無限循環、大文件內存溢出、編碼問題),並提供生產級最佳實踐代碼。
| 函數 | 用途 | 返回值 |
|---|---|---|
| fopen() | 打開文件/URL,返回資源句柄 | resource|false |
| feof() | 檢測文件指針是否到達 EOF | bool |
| fgets() | 按行讀取(含換行符),最多 length-1 字節 | string|false |
| fread() | 按字節讀取指定長度 | string|false |
| fclose() | 關閉文件句柄 | bool |
<?php
// ❌ 錯誤示範:fopen 失敗返回 false,feof(false) 永遠為 false
$file = @fopen("not_exist.txt", "r");
while (!feof($file)) { // 無限循環!
echo fgets($file);
}
fclose($file);
?>
<?php
$file = @fopen("data.txt", "r");
if ($file === false) {
throw new RuntimeException("無法打開文件:data.txt");
}
while (!feof($file)) {
$line = fgets($file);
if ($line === false) break; // 讀取錯誤保護
echo $line;
}
fclose($file);
?>
<?php
function readLines(string $path, callable $callback): void {
$fp = fopen($path, 'r');
if (!$fp) throw new RuntimeException("打開失敗:$path");
while (($line = fgets($fp)) !== false) {
$callback(rtrim($line, "\r\n"));
}
fclose($fp);
}
// 使用
readLines('access.log', function($line) {
if (strpos($line, 'ERROR') !== false) {
error_log("[ALERT] $line");
}
});
?>
<?php
function streamCopy(string $src, string $dest, int $chunkSize = 8192): void {
$in = fopen($src, 'rb');
$out = fopen($dest, 'wb');
if (!$in || !$out) throw new RuntimeException("文件打開失敗");
while (!feof($in)) {
$data = fread($in, $chunkSize);
if ($data === false) break;
fwrite($out, $data);
}
fclose($in);
fclose($out);
}
?>
<?php
// 適用於配置文件、模板等小於 memory_limit 的文件
$config = json_decode(file_get_contents('config.json'), true);
?>
fsockopen() 打開的網絡流同樣適用 feof(),但需注意:
feof() 會阻塞至超時(預設 60s)stream_set_timeout($fp, 10) 設定超時秒數feof() 返回 true,但 fgets() 可能返回空字符串<?php
$fp = fsockopen('www.example.com', 80, $errno, $errstr, 5);
if (!$fp) throw new RuntimeException("連接失敗:$errstr ($errno)");
stream_set_timeout($fp, 10); // 10 秒讀取超時
fwrite($fp, "GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n");
$response = '';
while (!feof($fp)) {
$response .= fread($fp, 8192);
$info = stream_get_meta_data($fp);
if ($info['timed_out']) {
error_log("讀取超時");
break;
}
}
fclose($fp);
?>
<?php
function removeBom(string $path): void {
$content = file_get_contents($path);
if (strlen($content) >= 3 && ord($content[0]) === 0xEF && ord($content[1]) === 0xBB && ord($content[2]) === 0xBF) {
file_put_contents($path, substr($content, 3));
}
}
?>
<?php
$content = str_replace(["\r\n", "\r"], "\n", $content);
?>
<?php
class FileReader {
public static function readLines(string $path, callable $processor, int $bufferSize = 4096): int {
$fp = @fopen($path, 'r');
if (!$fp) throw new RuntimeException("無法打開文件:$path");
$count = 0;
try {
while (!feof($fp)) {
$line = fgets($fp, $bufferSize);
if ($line === false) break;
$processor(rtrim($line, "\r\n"));
$count++;
}
} finally {
fclose($fp);
}
return $count;
}
public static function readChunks(string $path, callable $processor, int $chunkSize = 8192): int {
$fp = @fopen($path, 'rb');
if (!$fp) throw new RuntimeException("無法打開文件:$path");
$count = 0;
try {
while (!feof($fp)) {
$chunk = fread($fp, $chunkSize);
if ($chunk === false || $chunk === '') break;
$processor($chunk);
$count++;
}
} finally {
fclose($fp);
}
return $count;
}
public static function tail(string $path, int $lines = 10): array {
// 高效尾部讀取:反向讀取塊,統計換行符
$fp = @fopen($path, 'rb');
if (!$fp) return [];
$blockSize = 4096;
$lineCount = 0;
$content = '';
$fileSize = fstat($fp)['size'];
$pos = $fileSize;
while ($pos > 0 && $lineCount < $lines) {
$readSize = min($blockSize, $pos);
$pos -= $readSize;
fseek($fp, $pos);
$block = fread($fp, $readSize);
$content = $block . $content;
$lineCount += substr_count($block, "\n");
}
fclose($fp);
$allLines = explode("\n", $content);
return array_slice($allLines, -$lines);
}
}
?>
fgets() 讀完最後一行後指針在 EOF 前,feof() 仍為 false,再進循環一次讀到 false 才結束。建議用 while (($line = fgets($fp)) !== false) 模式避免此問題。file_get_contents 或 file()。必要時用 yield 生成器逐行產出。str_replace(["\r\n", "\r"], "\n", $line) 統一轉為 LF,或開啟 fopen($path, 'rt') 文本模式(PHP 會自動轉換)。文件讀取看似簡單,實則暗藏陷阱。牢記三原則:先判斷句柄有效性、優先流式處理大文件、善用 finally 確保資源釋放。掌握 feof()、fgets()、fread() 的配合模式,配合生成器、異步隊列,即可輕鬆應對從配置文件到 TB 級日誌的各類讀取場景。