PHP feof() 函数

说明: feof() 函数用于检测文件指针是否到达文件末尾(End Of File)。

语法

bool feof ( resource $handle )

参数说明

参数 描述 必需
handle fopen() 打开的文件指针

返回值

  • 如果文件指针到达文件末尾(EOF),返回 TRUE
  • 如果文件指针没有到达文件末尾,返回 FALSE
  • 如果文件指针无效,返回 TRUE 并发出警告

注意事项

  • feof() 仅在尝试读取超过文件末尾后返回 true,所以不应该在读取操作前使用它作为循环条件
  • 对于网络流,feof() 可能不会立即返回 true,因为服务器可能没有关闭连接
  • 如果文件指针无效,feof() 也会返回 true,所以需要先检查文件指针的有效性
  • feof() 不适用于标准输入(STDIN)
  • 在 Windows 系统上,文本文件和二进制文件的 EOF 处理可能不同

EOF 检测的正确用法

错误用法:
// ❌ 错误:在读取前检查EOF
while (!feof($handle)) {
    $line = fgets($handle);
    // 如果最后一行是空行,这里可能会出错
}
正确用法:
// ✅ 正确:在读取后检查数据
while (($line = fgets($handle)) !== false) {
    // 处理数据
}
或者:
// ✅ 正确:读取后立即检查EOF
while (true) {
    $line = fgets($handle);
    if ($line === false) {
        // 检查是否是EOF
        if (feof($handle)) {
            break; // 到达文件末尾
        } else {
            // 读取错误
            break;
        }
    }
    // 处理数据
}

示例

示例 1:基本使用 - 逐行读取文件

<?php
// 创建一个测试文件
$filename = "test.txt";
$content = "第一行\n第二行\n第三行\n第四行\n第五行";
file_put_contents($filename, $content);

// 打开文件
$handle = fopen($filename, "r");
if (!$handle) {
    die("无法打开文件");
}

echo "逐行读取文件内容:\n";

// 正确的方式:在读取后检查数据
while (($line = fgets($handle)) !== false) {
    echo "行内容: " . trim($line) . "\n";
}

// 检查是否到达文件末尾
if (feof($handle)) {
    echo "\n✅ 已到达文件末尾 (EOF)";
} else {
    echo "\n❌ 未到达文件末尾";
}

// 关闭文件
fclose($handle);

// 清理测试文件
unlink($filename);
?>

示例 2:逐块读取大文件

<?php
/**
 * 逐块读取大文件,避免内存溢出
 */
function read_file_in_chunks($filename, $chunk_size = 4096) {
    $handle = fopen($filename, "r");
    if (!$handle) {
        return false;
    }

    $chunks = [];
    $chunk_count = 0;

    echo "开始读取大文件: $filename\n";
    echo "块大小: " . $chunk_size . " 字节\n\n";

    while (!feof($handle)) {
        // 读取一个块
        $chunk = fread($handle, $chunk_size);

        if ($chunk !== false) {
            $chunks[] = $chunk;
            $chunk_count++;

            echo "读取块 #$chunk_count, 大小: " . strlen($chunk) . " 字节\n";

            // 模拟处理数据
            usleep(10000); // 10ms延迟,模拟处理时间
        } else {
            // 读取失败
            if (!feof($handle)) {
                echo "读取错误(非EOF)\n";
            }
            break;
        }
    }

    fclose($handle);

    return [
        'chunks' => $chunks,
        'total_chunks' => $chunk_count,
        'total_size' => array_sum(array_map('strlen', $chunks))
    ];
}

// 创建一个大文件进行测试
$big_filename = "big_file.txt";
$lines = [];
for ($i = 1; $i <= 1000; $i++) {
    $lines[] = "这是第 $i 行,包含一些测试数据用于演示大文件读取。" . str_repeat('.', rand(10, 50));
}
file_put_contents($big_filename, implode("\n", $lines));

// 读取大文件
$result = read_file_in_chunks($big_filename, 1024); // 1KB块

if ($result) {
    echo "\n✅ 文件读取完成\n";
    echo "总块数: " . $result['total_chunks'] . "\n";
    echo "总大小: " . $result['total_size'] . " 字节 (" .
         round($result['total_size'] / 1024, 2) . " KB)\n";
}

// 清理
unlink($big_filename);
?>

示例 3:读取二进制文件

<?php
/**
 * 读取二进制文件并分析
 */
function analyze_binary_file($filename) {
    $handle = fopen($filename, "rb"); // 二进制模式
    if (!$handle) {
        return false;
    }

    $analysis = [
        'total_bytes' => 0,
        'byte_frequency' => array_fill(0, 256, 0),
        'chunks' => [],
        'is_eof' => false
    ];

    echo "分析二进制文件: $filename\n";
    echo str_repeat("-", 40) . "\n";

    $chunk_size = 512;
    $chunk_index = 0;

    while (!feof($handle)) {
        $chunk = fread($handle, $chunk_size);

        if ($chunk === false) {
            if (!feof($handle)) {
                echo "读取错误\n";
            }
            break;
        }

        $chunk_length = strlen($chunk);
        $analysis['total_bytes'] += $chunk_length;
        $analysis['chunks'][] = [
            'index' => $chunk_index,
            'size' => $chunk_length,
            'start_pos' => $chunk_index * $chunk_size
        ];

        // 统计字节频率
        for ($i = 0; $i < $chunk_length; $i++) {
            $byte = ord($chunk[$i]);
            $analysis['byte_frequency'][$byte]++;
        }

        $chunk_index++;

        if ($chunk_length < $chunk_size) {
            // 最后一块,可能文件已结束
            break;
        }
    }

    $analysis['is_eof'] = feof($handle);

    fclose($handle);

    return $analysis;
}

/**
 * 生成测试二进制文件
 */
function create_test_binary_file($filename, $size_kb = 10) {
    $handle = fopen($filename, "wb");
    if (!$handle) {
        return false;
    }

    $size_bytes = $size_kb * 1024;

    // 写入各种字节模式
    for ($i = 0; $i < $size_bytes; $i += 1024) {
        $chunk = '';

        // 创建包含不同字节模式的块
        for ($j = 0; $j < min(1024, $size_bytes - $i); $j++) {
            // 生成一些有意义的字节模式
            $byte = ($i + $j) % 256;
            $chunk .= chr($byte);
        }

        fwrite($handle, $chunk);
    }

    fclose($handle);

    echo "创建测试二进制文件: $filename (" . $size_kb . " KB)\n";
    return true;
}

// 创建并分析测试文件
$binary_file = "test.bin";
if (create_test_binary_file($binary_file, 5)) {
    $analysis = analyze_binary_file($binary_file);

    if ($analysis) {
        echo "\n分析结果:\n";
        echo "文件大小: " . $analysis['total_bytes'] . " 字节\n";
        echo "块数量: " . count($analysis['chunks']) . "\n";
        echo "是否到达EOF: " . ($analysis['is_eof'] ? '是' : '否') . "\n";

        // 显示最常见的字节
        arsort($analysis['byte_frequency']);
        $top_bytes = array_slice($analysis['byte_frequency'], 0, 5, true);

        echo "\n最常见的字节:\n";
        foreach ($top_bytes as $byte => $count) {
            printf("字节 0x%02X (%3d): %6d 次\n", $byte, $byte, $count);
        }
    }
}

// 清理
if (file_exists($binary_file)) {
    unlink($binary_file);
}
?>

示例 4:feof() 在网络流中的应用

<?php
/**
 * 从网络流读取数据
 * 注意:feof() 对网络流的处理与普通文件不同
 */
function read_from_network_stream($url, $timeout = 10) {
    // 设置上下文选项
    $context_options = [
        'http' => [
            'method' => 'GET',
            'header' => "User-Agent: PHP feof() demo\r\n",
            'timeout' => $timeout
        ]
    ];

    $context = stream_context_create($context_options);

    // 打开网络流
    $handle = fopen($url, 'r', false, $context);
    if (!$handle) {
        return ['error' => '无法打开网络流'];
    }

    $content = '';
    $bytes_read = 0;
    $chunk_size = 8192; // 8KB
    $start_time = microtime(true);

    echo "开始从网络流读取数据: $url\n";

    // 对于网络流,feof() 的行为可能不同
    // 服务器可能不会立即关闭连接,所以我们需要设置超时或读取限制
    while (!feof($handle)) {
        $chunk = fread($handle, $chunk_size);

        if ($chunk === false) {
            // 读取错误
            if (!feof($handle)) {
                echo "读取错误(非EOF)\n";
            }
            break;
        }

        if (strlen($chunk) === 0) {
            // 空块,可能暂时没有数据
            usleep(100000); // 等待100ms
            continue;
        }

        $content .= $chunk;
        $bytes_read += strlen($chunk);

        echo "已读取: " . $bytes_read . " 字节\n";

        // 检查是否超时
        if ((microtime(true) - $start_time) > $timeout) {
            echo "读取超时\n";
            break;
        }

        // 限制读取大小(防止下载过大文件)
        if ($bytes_read > 1024 * 1024) { // 1MB限制
            echo "达到大小限制\n";
            break;
        }
    }

    $is_eof = feof($handle);
    fclose($handle);

    $duration = microtime(true) - $start_time;

    return [
        'success' => true,
        'bytes_read' => $bytes_read,
        'duration' => $duration,
        'is_eof' => $is_eof,
        'speed' => $bytes_read / $duration, // 字节/秒
        'content_preview' => substr($content, 0, 500) . (strlen($content) > 500 ? '...' : '')
    ];
}

// 使用示例(使用一个公开的API作为示例)
// 注意:在实际环境中可能需要处理代理、SSL证书等问题
/*
$result = read_from_network_stream('https://httpbin.org/bytes/1024');
if (isset($result['error'])) {
    echo "错误: " . $result['error'] . "\n";
} else {
    echo "\n网络流读取结果:\n";
    echo "读取字节数: " . $result['bytes_read'] . "\n";
    echo "读取时间: " . round($result['duration'], 3) . " 秒\n";
    echo "读取速度: " . round($result['speed'] / 1024, 2) . " KB/秒\n";
    echo "是否到达EOF: " . ($result['is_eof'] ? '是' : '否') . "\n";
    echo "内容预览: " . $result['content_preview'] . "\n";
}
*/

// 本地演示版本
echo "网络流读取演示(模拟):\n";
echo "由于网络请求可能需要真实环境,这里显示模拟结果:\n\n";

$mock_result = [
    'success' => true,
    'bytes_read' => 1024,
    'duration' => 0.5,
    'is_eof' => true,
    'speed' => 2048,
    'content_preview' => '模拟的网络数据内容...'
];

echo "读取字节数: " . $mock_result['bytes_read'] . "\n";
echo "读取时间: " . $mock_result['duration'] . " 秒\n";
echo "读取速度: " . round($mock_result['speed'] / 1024, 2) . " KB/秒\n";
echo "是否到达EOF: " . ($mock_result['is_eof'] ? '是' : '否') . "\n";
echo "内容预览: " . $mock_result['content_preview'] . "\n";
?>

示例 5:feof() 的高级错误处理

<?php
/**
 * 安全的文件读取类,包含完整的错误处理
 */
class SafeFileReader {
    private $handle = null;
    private $filename;
    private $errors = [];
    private $position = 0;

    public function __construct($filename, $mode = 'r') {
        $this->filename = $filename;
        $this->handle = @fopen($filename, $mode);

        if (!$this->handle) {
            $this->errors[] = "无法打开文件: {$filename}";
            $this->handle = null;
        } else {
            $this->position = 0;
        }
    }

    /**
     * 安全地读取一行
     */
    public function readLine() {
        if (!$this->handle) {
            $this->errors[] = "文件未打开或无效";
            return false;
        }

        $line = fgets($this->handle);

        if ($line === false) {
            // 读取失败,检查原因
            if (feof($this->handle)) {
                // 正常到达文件末尾
                return false;
            } else {
                // 读取错误
                $this->errors[] = "读取文件时发生错误 (位置: {$this->position})";
                return false;
            }
        }

        $this->position += strlen($line);
        return rtrim($line, "\r\n");
    }

    /**
     * 安全地读取指定字节数
     */
    public function readBytes($length) {
        if (!$this->handle) {
            $this->errors[] = "文件未打开或无效";
            return false;
        }

        $data = fread($this->handle, $length);

        if ($data === false) {
            if (feof($this->handle)) {
                // 到达文件末尾,返回已读取的数据
                return '';
            } else {
                $this->errors[] = "读取文件时发生错误 (位置: {$this->position})";
                return false;
            }
        }

        $this->position += strlen($data);
        return $data;
    }

    /**
     * 检查是否到达文件末尾
     */
    public function isEof() {
        if (!$this->handle) {
            return true;
        }

        return feof($this->handle);
    }

    /**
     * 获取当前读取位置
     */
    public function getPosition() {
        return $this->position;
    }

    /**
     * 获取文件大小
     */
    public function getFileSize() {
        if (!$this->handle) {
            return false;
        }

        $current_pos = ftell($this->handle);
        fseek($this->handle, 0, SEEK_END);
        $size = ftell($this->handle);
        fseek($this->handle, $current_pos);

        return $size;
    }

    /**
     * 获取错误信息
     */
    public function getErrors() {
        return $this->errors;
    }

    /**
     * 关闭文件
     */
    public function close() {
        if ($this->handle) {
            fclose($this->handle);
            $this->handle = null;
        }
    }

    public function __destruct() {
        $this->close();
    }

    /**
     * 读取整个文件(自动处理EOF)
     */
    public function readAll() {
        if (!$this->handle) {
            return false;
        }

        $content = '';

        while (!$this->isEof()) {
            $chunk = $this->readBytes(8192);

            if ($chunk === false) {
                // 读取错误
                break;
            }

            $content .= $chunk;
        }

        return $content;
    }
}

// 使用示例
echo "安全文件读取器演示:\n";
echo str_repeat("=", 50) . "\n";

// 创建测试文件
$test_file = "safe_test.txt";
$test_content = "第一行\n第二行\n第三行\n第四行\n第五行";
file_put_contents($test_file, $test_content);

// 使用安全读取器
$reader = new SafeFileReader($test_file);

if ($reader->getErrors()) {
    echo "打开文件时发生错误:\n";
    foreach ($reader->getErrors() as $error) {
        echo "- $error\n";
    }
} else {
    echo "文件大小: " . $reader->getFileSize() . " 字节\n";
    echo "读取位置: " . $reader->getPosition() . "\n";
    echo "EOF状态: " . ($reader->isEof() ? '是' : '否') . "\n\n";

    echo "逐行读取内容:\n";
    $line_number = 1;

    while (($line = $reader->readLine()) !== false) {
        echo "行 {$line_number}: {$line}\n";
        $line_number++;
    }

    echo "\n读取完成后:\n";
    echo "读取位置: " . $reader->getPosition() . "\n";
    echo "EOF状态: " . ($reader->isEof() ? '是' : '否') . "\n";

    // 检查错误
    $errors = $reader->getErrors();
    if (!empty($errors)) {
        echo "\n读取过程中发生错误:\n";
        foreach ($errors as $error) {
            echo "- $error\n";
        }
    } else {
        echo "\n✅ 读取完成,无错误\n";
    }
}

$reader->close();

// 测试读取整个文件
echo "\n测试读取整个文件:\n";
$reader2 = new SafeFileReader($test_file);
$all_content = $reader2->readAll();
echo "文件内容长度: " . strlen($all_content) . " 字节\n";
echo "EOF状态: " . ($reader2->isEof() ? '是' : '否') . "\n";
$reader2->close();

// 清理
unlink($test_file);
?>

示例 6:feof() 的常见误用和正确用法对比

<?php
// 创建一个包含空行的测试文件
$test_file = "eof_test.txt";
file_put_contents($test_file, "第一行\n\n第三行\n第四行\n");

echo "测试 feof() 的不同用法:\n";
echo str_repeat("=", 50) . "\n\n";

// 场景1:常见错误用法
echo "场景1: 常见错误用法 - 在读取前检查EOF\n";
$handle1 = fopen($test_file, "r");
$count1 = 0;
while (!feof($handle1)) {
    $line = fgets($handle1);
    if ($line === false) {
        break; // 避免无限循环
    }
    $count1++;
    echo "  读取行 {$count1}: " . trim($line) . "\n";
}
fclose($handle1);
echo "  总行数: {$count1} (注意:这里可能会多一次)\n\n";

// 场景2:正确用法 - 在读取后检查数据
echo "场景2: 正确用法 - 在读取后检查数据\n";
$handle2 = fopen($test_file, "r");
$count2 = 0;
while (($line = fgets($handle2)) !== false) {
    $count2++;
    echo "  读取行 {$count2}: " . trim($line) . "\n";
}
fclose($handle2);
echo "  总行数: {$count2}\n\n";

// 场景3:混合用法 - 先读取后检查EOF
echo "场景3: 混合用法 - 读取后检查EOF\n";
$handle3 = fopen($test_file, "r");
$count3 = 0;
while (true) {
    $line = fgets($handle3);

    if ($line === false) {
        if (feof($handle3)) {
            echo "  到达文件末尾\n";
            break;
        } else {
            echo "  读取错误\n";
            break;
        }
    }

    $count3++;
    echo "  读取行 {$count3}: " . trim($line) . "\n";
}
fclose($handle3);
echo "  总行数: {$count3}\n\n";

// 场景4:逐块读取的正确用法
echo "场景4: 逐块读取的正确用法\n";
$handle4 = fopen($test_file, "r");
$chunk_size = 10; // 故意设置小值以演示多次读取
$total_read = 0;
$chunk_count = 0;

while (true) {
    $chunk = fread($handle4, $chunk_size);

    if ($chunk === false) {
        if (feof($handle4)) {
            echo "  到达文件末尾\n";
            break;
        } else {
            echo "  读取错误\n";
            break;
        }
    }

    if (strlen($chunk) === 0) {
        // 空块,但可能不是EOF(网络流等情况)
        continue;
    }

    $chunk_count++;
    $total_read += strlen($chunk);
    echo "  块 {$chunk_count}: " . strlen($chunk) . " 字节\n";

    // 如果读取的字节数小于请求的块大小,可能是EOF
    if (strlen($chunk) < $chunk_size) {
        if (feof($handle4)) {
            echo "  读取到文件末尾\n";
            break;
        }
    }
}
fclose($handle4);
echo "  总读取: {$total_read} 字节,{$chunk_count} 个块\n";

// 清理
unlink($test_file);

echo "\n结论:\n";
echo "1. 不要在读取前使用 feof() 作为循环条件\n";
echo "2. 应该在读取失败后使用 feof() 检查原因\n";
echo "3. 对于逐行读取,使用 while(($line = fgets()) !== false) 模式\n";
echo "4. 对于逐块读取,检查读取结果和 feof() 状态\n";
?>

feof() 在不同场景下的行为

场景 feof() 行为 说明
普通文本文件 读取超过文件末尾后返回 true 最常见的使用场景
二进制文件 同上 处理方式与文本文件相同
网络流 (HTTP) 服务器关闭连接后返回 true 可能需要设置超时
标准输入 (STDIN) 不适用 feof() 对 STDIN 无效
空文件 首次读取前就返回 true 因为文件指针已经在末尾
无效文件指针 返回 true 并发出警告 应先检查文件指针有效性

相关函数

  • fopen() - 打开文件或URL
  • fclose() - 关闭打开的文件
  • fgets() - 从文件指针中读取一行
  • fread() - 读取文件(可安全用于二进制文件)
  • fgetc() - 从文件指针中读取一个字符
  • ftell() - 返回文件指针读/写的位置
  • rewind() - 倒回文件指针的位置
  • fseek() - 在文件指针中定位
最佳实践
  1. 不要在循环条件中使用 feof():使用 while(($data = fread()) !== false) 模式
  2. 检查读取结果:读取失败时,使用 feof() 判断是否是正常结束
  3. 处理网络流要小心:网络流的 EOF 可能不会立即到达,需要设置超时
  4. 验证文件指针:在使用 feof() 前确保文件指针有效
  5. 区分空文件和错误:空文件的 feof() 立即返回 true,这不是错误
  6. 结合文件位置函数:使用 ftell() 和 fseek() 与 feof() 配合
  7. 二进制文件处理:对于二进制文件,使用 fread() 而不是 fgets()
  8. 错误日志:记录读取失败且不是 EOF 的情况,便于调试
feof() 使用模式总结
模式1:逐行读取(推荐)
$handle = fopen($file, "r");
while (($line = fgets($handle)) !== false) {
    // 处理 $line
}
fclose($handle);
模式2:逐块读取
$handle = fopen($file, "rb");
$chunk_size = 8192;
while (($chunk = fread($handle, $chunk_size)) !== false) {
    // 处理 $chunk
    if (strlen($chunk) < $chunk_size) {
        if (feof($handle)) break;
    }
}
fclose($handle);
模式3:带错误处理的读取
$handle = fopen($file, "r");
while (true) {
    $data = fread($handle, 1024);
    if ($data === false) {
        if (feof($handle)) {
            // 正常结束
            break;
        } else {
            // 读取错误
            // 处理错误
            break;
        }
    }
    // 处理 $data
}
fclose($handle);