Skip to content

文件读写

概述

PHP 提供了丰富的文件读写函数,核心围绕 fopen() / fread() / fwrite() / fclose() 四大函数展开。理解文件模式(r/w/a/x/r+/w+/a+)是掌握文件操作的基础。PHP 8.0+ 引入了更多类型安全的改进,PHP 8.1 进一步增强了文件操作的返回值类型声明。

适用场景

  • 日志文件记录
  • 配置文件读写
  • CSV/文本数据处理
  • 数据导出导入

基础概念

文件模式对照表

模式名称指针位置文件不存在截断
r只读开头报错
r+读写开头报错
w只写开头创建
w+读写开头创建
a追加末尾创建
a+读写末尾创建
x排斥写开头创建
x+排斥读写开头创建
c只写开头创建
c+读写开头创建

注意

xx+ 模式在文件已存在时会返回 false 并产生 E_WARNING,适用于独占创建场景。c/c+ 不会截断文件(PHP 5.2.6+)。

核心函数

  • fopen(string $filename, string $mode, bool $useIncludePath = false, ?resource $context = null): resource|false
  • fread(resource $handle, int $length): string|false
  • fwrite(resource $handle, string $data, ?int $length = null): int|false
  • fclose(resource $handle): bool
  • fgets(resource $handle, ?int $length = null): string|false
  • fgetc(resource $handle): string|false
  • fputs(resource $handle, string $data, ?int $length = null): int|false

fputs 与 fwrite

fputs()fwrite() 的别名,两者完全等价。推荐使用 fwrite() 以提高代码可读性。

语法与代码示例

基本文件写入

php
<?php

// 写入文件 —— w 模式(覆盖)
$handle = fopen('/tmp/example.txt', 'w');
if ($handle === false) {
    throw new RuntimeException('无法打开文件');
}

$content = "第一行内容\n第二行内容\n第三行内容\n";
$bytesWritten = fwrite($handle, $content);

if ($bytesWritten === false) {
    fclose($handle);
    throw new RuntimeException('写入失败');
}

echo "写入 {$bytesWritten} 字节\n";
fclose($handle);

追加写入

php
<?php

// 追加写入 —— a 模式
$handle = fopen('/tmp/log.txt', 'a');

if ($handle) {
    $timestamp = date('Y-m-d H:i:s');
    $logLine = "[{$timestamp}] 用户登录成功\n";
    fwrite($handle, $logLine);
    fclose($handle);
}

读取文件内容

php
<?php

// 读取整个文件
$handle = fopen('/tmp/example.txt', 'r');

if ($handle) {
    // 方式一:fread 读取指定长度
    $content = fread($handle, filesize('/tmp/example.txt'));
    echo $content;

    // 方式二:逐行读取 fgets
    rewind($handle); // 将指针重置到开头
    while (($line = fgets($handle)) !== false) {
        echo "行: " . trim($line) . PHP_EOL;
    }

    fclose($handle);
}

逐字符读取

php
<?php

// fgetc 逐字符读取 —— 适用于字符级处理
$handle = fopen('/tmp/example.txt', 'r');

if ($handle) {
    $charCount = 0;
    $digitCount = 0;

    while (($char = fgetc($handle)) !== false) {
        $charCount++;
        if (ctype_digit($char)) {
            $digitCount++;
        }
    }

    echo "总字符数: {$charCount}, 数字数: {$digitCount}\n";
    fclose($handle);
}

独占创建文件(x 模式)

php
<?php

// x 模式:文件必须不存在,否则失败
$handle = fopen('/tmp/lock.txt', 'x');

if ($handle === false) {
    die('文件已存在,无法创建' . PHP_EOL);
}

fwrite($handle, '独占创建的文件内容');
fclose($handle);

// 使用 x+ 模式可以同时读写
$handle = fopen('/tmp/data.txt', 'x+');
if ($handle) {
    fwrite($handle, "初始数据\n");
    rewind($handle);
    echo fread($handle, 1024);
    fclose($handle);
}

实战示例

日志记录类

php
<?php

declare(strict_types=1);

class FileLogger
{
    private string $logFile;
    private ?resource $handle = null;

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
        $dir = dirname($logFile);

        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
    }

    public function open(): void
    {
        $this->handle = fopen($this->logFile, 'a');
        if ($this->handle === false) {
            throw new RuntimeException("无法打开日志文件: {$this->logFile}");
        }
    }

    public function log(string $level, string $message, array $context = []): void
    {
        if ($this->handle === null) {
            $this->open();
        }

        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}" . PHP_EOL;

        fwrite($this->handle, $line);
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    public function close(): void
    {
        if ($this->handle !== null) {
            fclose($this->handle);
            $this->handle = null;
        }
    }

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

// 使用示例
$logger = new FileLogger('/tmp/app.log');
$logger->info('应用启动');
$logger->error('数据库连接失败', ['host' => 'localhost', 'error' => 'timeout']);
$logger->close();

CSV 文件读写

php
<?php

declare(strict_types=1);

// 写入 CSV
function writeCsv(string $filePath, array $headers, array $rows): void
{
    $handle = fopen($filePath, 'w');
    if ($handle === false) {
        throw new RuntimeException("无法创建 CSV 文件: {$filePath}");
    }

    // 写入 BOM 以支持 Excel 正确识别 UTF-8
    fwrite($handle, "\xEF\xBB\xBF");
    fputcsv($handle, $headers);

    foreach ($rows as $row) {
        fputcsv($handle, $row);
    }

    fclose($handle);
}

// 读取 CSV
function readCsv(string $filePath): array
{
    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new RuntimeException("无法读取 CSV 文件: {$filePath}");
    }

    $headers = fgetcsv($handle);
    if ($headers === false) {
        fclose($handle);
        return [];
    }

    $data = [];
    while (($row = fgetcsv($handle)) !== false) {
        if ($row === [null]) {
            continue; // 跳过空行
        }
        $data[] = array_combine($headers, $row);
    }

    fclose($handle);
    return $data;
}

// 使用示例
$headers = ['姓名', '年龄', '城市'];
$rows = [
    ['张三', '28', '北京'],
    ['李四', '32', '上海'],
    ['王五', '25', '广州'],
];

writeCsv('/tmp/users.csv', $headers, $rows);

$users = readCsv('/tmp/users.csv');
print_r($users);

文件模板渲染

php
<?php

declare(strict_types=1);

class TemplateRenderer
{
    public static function render(string $templatePath, array $variables): string
    {
        $handle = fopen($templatePath, 'r');
        if ($handle === false) {
            throw new RuntimeException("模板文件不存在: {$templatePath}");
        }

        $template = fread($handle, filesize($templatePath));
        fclose($handle);

        foreach ($variables as $key => $value) {
            $template = str_replace('{{' . $key . '}}', (string)$value, $template);
        }

        return $template;
    }
}

// 创建模板文件
$templateContent = "尊敬的 {{name}},\n\n您的订单 {{orderId}} 已于 {{date}} 发货。\n快递单号: {{trackingNumber}}\n\n感谢您的购买!";
$handle = fopen('/tmp/email_template.txt', 'w');
fwrite($handle, $templateContent);
fclose($handle);

// 渲染模板
$html = TemplateRenderer::render('/tmp/email_template.txt', [
    'name' => '张三',
    'orderId' => 'ORD-20260712-001',
    'date' => date('Y-m-d'),
    'trackingNumber' => 'SF1234567890',
]);

echo $html;

注意事项

文件锁定

php
<?php

// 使用 flock 进行文件锁定,防止并发写入冲突
$handle = fopen('/tmp/counter.txt', 'c+');
if ($handle === false) {
    throw new RuntimeException('无法打开文件');
}

// 获取独占锁
if (flock($handle, LOCK_EX)) {
    $count = (int)fread($handle, 1024);
    $count++;
    ftruncate($handle, 0);
    rewind($handle);
    fwrite($handle, (string)$count);
    flock($handle, LOCK_UN); // 释放锁
} else {
    echo '无法获取文件锁';
}

fclose($handle);

flock 限制

flock() 在某些网络文件系统(如 NFS)上可能不工作。对于高并发场景,建议使用 Redis 等外部锁机制。

二进制安全

PHP 的文件操作函数是二进制安全的,可以正确处理包含 null 字节的字符串:

php
<?php

// 二进制安全写入
$binaryData = "\x00\x01\x02\x03\xff\xfe\xfd";
$handle = fopen('/tmp/binary.bin', 'wb');
fwrite($handle, $binaryData);
fclose($handle);

// 二进制安全读取
$handle = fopen('/tmp/binary.bin', 'rb');
$data = fread($handle, filesize('/tmp/binary.bin'));
echo bin2hex($data); // 输出: 00010203fffeff
fclose($handle);

文件模式与二进制标志

在 Windows 上使用 b 标志(如 rbwb)进行二进制文件的读写,避免换行符转换:

php
<?php

// Windows 上读取二进制文件务必加 b
$handle = fopen('/tmp/image.jpg', 'rb');

跨平台兼容

b 标志在 Linux/macOS 上会被忽略,不影响行为。为了跨平台兼容性,始终在处理二进制文件时添加 b 标志。

最佳实践

1. 始终检查返回值

php
<?php

// 不好
$handle = fopen('file.txt', 'r');
fwrite($handle, 'data'); // $handle 可能为 false
fclose($handle);

// 好
$handle = fopen('file.txt', 'r');
if ($handle === false) {
    throw new RuntimeException('无法打开文件');
}

$result = fwrite($handle, 'data');
if ($result === false) {
    fclose($handle);
    throw new RuntimeException('写入失败');
}

fclose($handle);

2. 使用 try-finally 确保关闭

php
<?php

$handle = fopen('/tmp/data.txt', 'w');
if ($handle === false) {
    throw new RuntimeException('无法打开文件');
}

try {
    fwrite($handle, '重要数据');
    // ... 其他操作
} finally {
    fclose($handle);
}

3. 优先使用便捷函数

对于简单的读写场景,优先使用 file_get_contents()file_put_contents()(见下一章节):

php
<?php

// 简单场景用便捷函数
$content = file_get_contents('config.json');
file_put_contents('config.json', $updatedContent);

// 复杂场景用 fopen 系列函数
$handle = fopen('large_file.csv', 'r');
while (($line = fgets($handle)) !== false) {
    // 逐行处理大文件
}
fclose($handle);

4. 使用 DIRECTORY_SEPARATOR

php
<?php

// 跨平台路径拼接
$path = '/tmp' . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . 'app.log';

参考链接