PHP 文件读写
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
读写文件是最常见的持久化手段:日志、配置、导出数据、缓存。PHP 提供了从「一行读完」到「流式处理」的不同层次 API。
一次性读写
// 读整个文件
$content = file_get_contents("data.txt");
// 写入(覆盖)
file_put_contents("out.txt", "hello\n");
// 追加
file_put_contents("log.txt", "新的一行\n", FILE_APPEND);
file_put_contents 等价于 fopen + fwrite + fclose,简单场景用它就够了。它会返回写入的字节数,失败返回 false。
一次性读取会把整个文件读进内存,大文件(几百 MB)会撑爆内存限制,那种情况要用流式读取。
读取失败的处理
$content = @file_get_contents("missing.txt"); // @ 抑制警告,不推荐
if ($content === false) {
// 处理失败
}
更清晰的写法是先检查:
$path = "data.txt";
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException("文件不可读:{$path}");
}
$content = file_get_contents($path);
不要用 @ 抑制错误:它会把问题藏起来,让调试变困难。
按行读取
把文件读成数组,每行一个元素(包含行尾换行符):
$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
echo $line . PHP_EOL;
}
遍历时清理空白:
$names = array_values(array_filter(array_map("trim", $lines)));
流式读取:大文件用这个
$handle = fopen("big.log", "r");
if ($handle === false) {
throw new RuntimeException("无法打开文件");
}
while (($line = fgets($handle)) !== false) {
// 每次只处理一行,内存占用恒定
if (str_contains($line, "ERROR")) {
echo $line;
}
}
fclose($handle);
fgets 每次读一行;false 表示到达文件末尾。注意 fgets 会保留行尾的 \n,比较前先 trim。
写入与文件锁
多个进程同时写同一文件会互相覆盖,加锁解决:
file_put_contents("counter.txt", "1", LOCK_EX);
LOCK_EX 表示独占锁:写入期间其他进程会等待( flock 层面,跨进程有效)。
需要「读—改—写」完整原子操作时,用 fopen + flock:
$handle = fopen("counter.txt", "c+"); // 读写,不存在则创建,不截断
flock($handle, LOCK_EX);
$current = (int) stream_get_contents($handle);
$current++;
rewind($handle);
ftruncate($handle, 0);
fwrite($handle, (string) $current);
flock($handle, LOCK_UN);
fclose($handle);
目录操作
// 创建(递归创建多级目录)
if (!is_dir("storage/logs")) {
mkdir("storage/logs", 0755, true);
}
// 遍历目录
foreach (scandir("storage") as $entry) {
if ($entry === "." || $entry === "..") {
continue;
}
echo $entry . PHP_EOL;
}
递归遍历用 SPL 迭代器,比手写递归清晰:
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator("src", FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === "php") {
echo $file->getPathname() . PHP_EOL;
}
}
FilesystemIterator::SKIP_DOTS 用于跳过 . 和 ..。
路径处理
__DIR__; // 当前文件所在目录的绝对路径
dirname(__FILE__); // 同上(旧写法)
basename("/a/b/c.txt"); // "c.txt"
pathinfo("/a/b/c.txt"); // ["dirname" => "/a/b", "basename" => "c.txt", "extension" => "txt", "filename" => "c"]
拼接路径一律用 __DIR__ 作为起点,不要依赖相对路径或当前工作目录:
$logFile = __DIR__ . "/storage/app.log";
Web 环境(如 PHP-FPM)的当前工作目录与 CLI 不同,写相对路径会导致「命令行能跑、Web 里找不到文件」。
文件信息
$path = __DIR__ . "/data.txt";
file_exists($path); // 文件或目录是否存在
is_file($path); // 是普通文件
is_dir($path); // 是目录
is_readable($path); // 可读
is_writable($path); // 可写
filesize($path); // 字节数,失败返回 false
filemtime($path); // 最后修改时间(Unix 时间戳)
删除与重命名:
unlink("old.txt"); // 删除文件
rename("a.txt", "b.txt"); // 重命名或移动
copy("a.txt", "backup/a.txt"); // 复制
一个完整例子:简易日志
<?php
declare(strict_types=1);
function appLog(string $level, string $message, string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$file = $dir . "/" . date("Y-m-d") . ".log";
$line = sprintf(
"[%s] %s: %s%s",
date("H:i:s"),
strtoupper($level),
$message,
PHP_EOL
);
file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}
appLog("error", "数据库连接失败", __DIR__ . "/storage/logs");
生成的 storage/logs/2026-11-02.log 内容:
[14:32:10] ERROR: 数据库连接失败
常见错误与注意事项
- 用相对路径:Web 与 CLI 的工作目录不同,一律用
__DIR__拼接。 - 大文件用
file_get_contents一次读入:内存溢出,改用fgets流式读取。 - 多进程写同一文件不加锁:数据互相覆盖,用
LOCK_EX或flock。 - 用
@抑制警告代替检查:错误被隐藏,先is_file/is_readable再操作。 - 忘记
fclose:句柄泄漏,长时间运行的脚本会耗尽文件描述符。 - 目录权限过宽:
0777有安全风险,目录用0755、文件用0644起步,按最小权限原则调整。
小结
- 小文件用
file_get_contents/file_put_contents;大文件用fopen+fgets流式处理。 - 追加写加
FILE_APPEND,并发写加LOCK_EX。 - 路径用
__DIR__拼接,不依赖当前工作目录。 - 操作前检查
is_file/is_readable/is_writable,不用@抑制错误。 - 打开的句柄要
fclose。