文件读写
概述
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+ | 读写 | 是 | 是 | 开头 | 创建 | 否 |
注意
x 和 x+ 模式在文件已存在时会返回 false 并产生 E_WARNING,适用于独占创建场景。c/c+ 不会截断文件(PHP 5.2.6+)。
核心函数
fopen(string $filename, string $mode, bool $useIncludePath = false, ?resource $context = null): resource|falsefread(resource $handle, int $length): string|falsefwrite(resource $handle, string $data, ?int $length = null): int|falsefclose(resource $handle): boolfgets(resource $handle, ?int $length = null): string|falsefgetc(resource $handle): string|falsefputs(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 标志(如 rb、wb)进行二进制文件的读写,避免换行符转换:
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';