1 文件读写基础
一次性读写(推荐用于小文件)
file_get_contents() 和 file_put_contents() 是 PHP 中最简洁的文件读写方式,一行代码完成操作。
<?php
// 读取整个文件为字符串
$content = file_get_contents('config.txt');
// 从 URL 读取(需 allow_url_fopen=On)
$html = file_get_contents('https://example.com');
// 写入文件(覆盖已有内容)
file_put_contents('output.txt', "Hello, PHP!\n");
// 追加写入
file_put_contents('log.txt', date('[Y-m-d H:i:s]') . " 操作记录\n", FILE_APPEND);
// 写入数组(每个元素作为一行)
$lines = ["第一行\n", "第二行\n", "第三行\n"];
file_put_contents('lines.txt', $lines);
句柄式读写(适合大文件 / 精细控制)
<?php
// 打开文件获取句柄
$fp = fopen('data.txt', 'r');
if ($fp === false) {
die('无法打开文件');
}
// 读取指定字节
$chunk = fread($fp, 1024); // 读取 1024 字节
// 读完后关闭
fclose($fp);
// 写入示例
$fp = fopen('output.txt', 'w');
fwrite($fp, "写入的内容\n");
fwrite($fp, "第二行内容\n");
fclose($fp);
文件打开模式
| 模式 | 说明 | 文件不存在 | 指针位置 |
|---|---|---|---|
| r | 只读 | 报错 | 开头 |
| w | 只写(清空) | 自动创建 | 开头 |
| a | 追加写入 | 自动创建 | 末尾 |
| r+ | 读写 | 报错 | 开头 |
| w+ | 读写(清空) | 自动创建 | 开头 |
| a+ | 读 + 追加 | 自动创建 | 末尾 |
🔄 与 Python 对比
PHP 的 file_get_contents() 相当于 Python 的 open(f).read();fopen() 对应 Python 的 open()。
// PHP: 一次性读取
$text = file_get_contents('file.txt');
// Python 等价:
// text = open('file.txt').read()
// 或 with open('file.txt') as f: text = f.read()
// PHP: 句柄式
$fp = fopen('file.txt', 'r');
$line = fgets($fp);
fclose($fp);
// Python 等价:
// f = open('file.txt', 'r')
// line = f.readline()
// f.close()
PHP 没有 with 语法糖,需手动调用 fclose()。SplFileObject 在析构时自动关闭。
2 逐行读取
fgets() — 逐行读取
适合处理大文件,每次只在内存中保留一行。
<?php
$fp = fopen('access.log', 'r');
$lineNum = 0;
while (($line = fgets($fp)) !== false) {
$lineNum++;
// 去除行尾换行符
$line = rtrim($line, "\r\n");
if (str_contains($line, 'ERROR')) {
echo "第 {$lineNum} 行: {$line}\n";
}
}
fclose($fp);
file() — 一次读取所有行为数组
<?php
// 每行作为数组元素,自动去除换行符
$lines = file('config.ini', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $index => $line) {
echo ($index + 1) . ": {$line}\n";
}
// 统计行数
$totalLines = count(file('data.txt'));
SplFileObject — 面向对象方式
SplFileObject 实现了 Iterator 接口,可直接用 foreach 遍历,且自动管理资源。
<?php
$file = new SplFileObject('server.log', 'r');
$file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
foreach ($file as $lineNumber => $line) {
if (str_contains($line, '500')) {
printf("行 %d: %s\n", $lineNumber + 1, $line);
}
}
// 读取指定行(跳到第 100 行)
$file->seek(99);
echo $file->current();
💡 大文件处理建议
处理 GB 级日志文件时,避免 file_get_contents() 或 file()(一次性加载到内存)。用 fgets() 或 SplFileObject 逐行读取,内存占用恒定。
3 目录操作
基本目录操作
<?php
// 创建目录(第三个参数 true 表示递归创建)
mkdir('storage/uploads/images', 0755, true);
// 删除空目录
rmdir('temp');
// 列出目录内容
$items = scandir('storage');
// ['.' , '..' , 'uploads', 'cache', 'logs']
// 过滤掉 . 和 ..
$items = array_diff(scandir('storage'), ['.', '..']);
// 判断类型
foreach ($items as $item) {
$path = "storage/{$item}";
if (is_dir($path)) {
echo "📁 {$item}\n";
} elseif (is_file($path)) {
echo "📄 {$item}\n";
}
}
RecursiveDirectoryIterator — 递归遍历
<?php
$dir = new RecursiveDirectoryIterator('src', FilesystemIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir);
foreach ($iterator as $file) {
/** @var SplFileInfo $file */
echo $file->getPathname() . ' (' . $file->getSize() . " bytes)\n";
}
// 只遍历 PHP 文件
$phpFiles = new RegexIterator($iterator, '/\.php$/');
foreach ($phpFiles as $phpFile) {
echo $phpFile->getPathname() . "\n";
}
glob() — 模式匹配
<?php
// 查找所有 PHP 文件
$phpFiles = glob('src/*.php');
// 递归查找(PHP 本身不支持 **,需组合使用)
$phpFiles = glob('src/**/*.php'); // 仅匹配一层子目录
// 查找多种扩展名
$images = glob('uploads/*.{jpg,png,gif}', GLOB_BRACE);
// 只查找目录
$dirs = glob('modules/*', GLOB_ONLYDIR);
foreach ($phpFiles as $file) {
echo basename($file) . "\n";
}
4 路径处理
<?php
$path = '/var/www/html/uploads/photo.jpg';
// pathinfo() — 返回路径各部分
$info = pathinfo($path);
// ['dirname' => '/var/www/html/uploads',
// 'basename' => 'photo.jpg',
// 'extension' => 'jpg',
// 'filename' => 'photo']
echo pathinfo($path, PATHINFO_EXTENSION); // 'jpg'
// 其他常用函数
echo basename($path); // 'photo.jpg'
echo basename($path, '.jpg'); // 'photo'
echo dirname($path); // '/var/www/html/uploads'
echo dirname($path, 2); // '/var/www/html'
// 解析为绝对路径(解析符号链接和 ..)
echo realpath('../config'); // '/var/www/config'
// 魔术常量
echo __DIR__; // 当前脚本所在目录的绝对路径
echo __FILE__; // 当前脚本文件的绝对路径
路径函数速查表
| 函数 / 常量 | 作用 | 示例结果 |
|---|---|---|
| pathinfo($p) | 解析路径各部分 | 关联数组 |
| basename($p) | 文件名(含扩展名) | photo.jpg |
| dirname($p) | 父目录路径 | /var/www/html/uploads |
| realpath($p) | 解析为绝对路径 | /var/www/config |
| __DIR__ | 当前脚本目录 | /var/www/html |
| __FILE__ | 当前脚本完整路径 | /var/www/html/index.php |
| DIRECTORY_SEPARATOR | 系统目录分隔符 | / (Linux) 或 \ (Windows) |
💡 跨平台路径拼接
使用 DIRECTORY_SEPARATOR 或直接用 /(PHP 在 Windows 上也能正确处理正斜杠)。推荐写法:__DIR__ . '/config/app.php'。
5 CSV 处理
读取 CSV 文件
<?php
$fp = fopen('users.csv', 'r');
// 读取表头
$headers = fgetcsv($fp);
// ['姓名', '邮箱', '年龄']
// 逐行读取数据
$users = [];
while (($row = fgetcsv($fp)) !== false) {
// 将表头和数据组合为关联数组
$users[] = array_combine($headers, $row);
}
fclose($fp);
// $users = [
// ['姓名' => '张三', '邮箱' => 'zhang@example.com', '年龄' => '28'],
// ['姓名' => '李四', '邮箱' => 'li@example.com', '年龄' => '32'],
// ]
foreach ($users as $user) {
echo "{$user['姓名']} ({$user['邮箱']})\n";
}
写入 CSV 文件
<?php
$data = [
['产品', '价格', '库存'],
['PHP 入门', 59.9, 120],
['Laravel 实战', 79.9, 85],
['MySQL 优化', 69.9, 200],
];
$fp = fopen('products.csv', 'w');
// 写入 UTF-8 BOM(Excel 兼容中文)
fwrite($fp, "\xEF\xBB\xBF");
foreach ($data as $row) {
fputcsv($fp, $row);
}
fclose($fp);
echo "CSV 写入完成,共 " . (count($data) - 1) . " 条数据\n";
SplFileObject 读取 CSV
<?php
$file = new SplFileObject('users.csv', 'r');
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
$headers = null;
foreach ($file as $row) {
if ($headers === null) {
$headers = $row;
continue;
}
$user = array_combine($headers, $row);
echo "{$user['姓名']}\n";
}
6 JSON 文件操作
读取 JSON 文件
<?php
// 读取并解码 JSON 文件
$json = file_get_contents('config.json');
$config = json_decode($json, true); // true → 返回关联数组
if ($config === null && json_last_error() !== JSON_ERROR_NONE) {
die('JSON 解析错误: ' . json_last_error_msg());
}
echo $config['database']['host']; // localhost
echo $config['database']['port']; // 3306
写入 JSON 文件
<?php
$data = [
'app_name' => '我的应用',
'version' => '2.0.0',
'features' => ['用户管理', '权限控制', '数据统计'],
'database' => [
'host' => 'localhost',
'port' => 3306,
'name' => 'myapp',
],
];
// 写入格式化的 JSON
$jsonStr = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents('config.json', $jsonStr);
// 输出:
// {
// "app_name": "我的应用",
// "version": "2.0.0",
// "features": ["用户管理", "权限控制", "数据统计"],
// ...
// }
常用 JSON 编码标志
| 标志 | 作用 |
|---|---|
| JSON_PRETTY_PRINT | 格式化输出(缩进换行) |
| JSON_UNESCAPED_UNICODE | 中文等 Unicode 字符不转义为 \uXXXX |
| JSON_UNESCAPED_SLASHES | 不转义斜杠 /(URL 更可读) |
| JSON_THROW_ON_ERROR | 解析失败抛出 JsonException(推荐) |
| JSON_FORCE_OBJECT | 强制将数组编码为 JSON 对象 {} |
💡 PHP 8.x 推荐用法
使用 JSON_THROW_ON_ERROR 取代手动检查 json_last_error(),配合 try/catch 更加简洁:
try {
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo "解析失败: {$e->getMessage()}";
}
7 文件上传处理
HTML 上传表单
<!-- 必须使用 enctype="multipart/form-data" -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button type="submit">上传头像</button>
</form>
$_FILES 超全局变量
<?php
// $_FILES['avatar'] 的结构:
// [
// 'name' => 'photo.jpg', // 原始文件名
// 'type' => 'image/jpeg', // MIME 类型(不可信)
// 'tmp_name' => '/tmp/phpXXXX', // 临时文件路径
// 'error' => UPLOAD_ERR_OK, // 错误码(0 表示成功)
// 'size' => 245678, // 文件大小(字节)
// ]
完整的上传处理示例
<?php
function handleUpload(string $fieldName, string $uploadDir = 'uploads/'): string|false
{
if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) {
$errors = [
UPLOAD_ERR_INI_SIZE => '文件超过 php.ini 中 upload_max_filesize 限制',
UPLOAD_ERR_FORM_SIZE => '文件超过表单中 MAX_FILE_SIZE 限制',
UPLOAD_ERR_PARTIAL => '文件只上传了一部分',
UPLOAD_ERR_NO_FILE => '没有选择文件',
UPLOAD_ERR_NO_TMP_DIR => '服务器缺少临时目录',
];
$code = $_FILES[$fieldName]['error'] ?? -1;
echo '上传失败: ' . ($errors[$code] ?? '未知错误') . "\n";
return false;
}
$file = $_FILES[$fieldName];
// 1. 验证文件大小(限制 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
echo "文件过大,最大允许 5MB\n";
return false;
}
// 2. 验证文件类型(通过文件内容而非扩展名)
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes, true)) {
echo "不支持的文件类型: {$mimeType}\n";
return false;
}
// 3. 验证扩展名
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts, true)) {
echo "不允许的扩展名: {$ext}\n";
return false;
}
// 4. 生成安全的文件名(避免覆盖和目录穿越)
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = rtrim($uploadDir, '/') . '/' . $newName;
// 5. 确保目标目录存在
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 6. 移动文件(move_uploaded_file 会验证是否为合法上传文件)
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
echo "移动文件失败\n";
return false;
}
return $destPath;
}
// 使用
$path = handleUpload('avatar', 'uploads/avatars/');
if ($path) {
echo "上传成功: {$path}\n";
}
🔒 安全注意事项
- 永远不要信任
$_FILES['type'],用finfo检测真实 MIME - 使用随机文件名,防止覆盖和目录穿越攻击
- 上传目录禁止 PHP 执行(
.htaccess中设置php_flag engine off) - 配置
upload_max_filesize和post_max_size(php.ini) - 始终用
move_uploaded_file()而非copy()或rename()
8 文件信息与权限
<?php
$file = 'storage/data.json';
// 文件是否存在
if (file_exists($file)) {
echo "文件大小: " . filesize($file) . " 字节\n";
// 格式化文件大小
function formatSize(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
echo "可读大小: " . formatSize(filesize($file)) . "\n";
// 修改时间
$mtime = filemtime($file);
echo "最后修改: " . date('Y-m-d H:i:s', $mtime) . "\n";
// 权限检查
echo "可读: " . (is_readable($file) ? '是' : '否') . "\n";
echo "可写: " . (is_writable($file) ? '是' : '否') . "\n";
// 修改权限
chmod($file, 0644);
}
// 复制、重命名、删除
copy('source.txt', 'backup.txt');
rename('old.txt', 'new.txt'); // 也可用于移动文件
unlink('temp.txt'); // 删除文件
常用文件函数速查
| 函数 | 作用 | 返回值 |
|---|---|---|
| file_exists($p) | 文件或目录是否存在 | bool |
| is_file($p) | 是否为普通文件 | bool |
| is_dir($p) | 是否为目录 | bool |
| is_readable($p) | 是否可读 | bool |
| is_writable($p) | 是否可写 | bool |
| filesize($p) | 文件大小(字节) | int|false |
| filemtime($p) | 最后修改时间 | int|false (Unix 时间戳) |
| fileatime($p) | 最后访问时间 | int|false |
| chmod($p, $m) | 修改权限 | bool |
| copy($src, $dst) | 复制文件 | bool |
| rename($old, $new) | 重命名 / 移动 | bool |
| unlink($p) | 删除文件 | bool |
| tempnam($dir, $pre) | 创建临时文件 | string|false |
💡 注意 stat 缓存
PHP 会缓存 file_exists()、filesize() 等函数的结果。如果文件在脚本运行期间被外部修改,需调用 clearstatcache() 清除缓存后再次读取。
9 本章要点
📖 文件读写
- • 小文件用
file_get_contents()/file_put_contents() - • 大文件用
fopen()+fgets()逐行处理 - •
SplFileObject提供面向对象的文件操作 - • 追加写入用模式
'a'或FILE_APPEND标志
📁 目录与路径
- •
scandir()列目录,glob()模式匹配 - •
RecursiveDirectoryIterator递归遍历 - •
pathinfo()解析路径,__DIR__获取当前目录 - •
mkdir($dir, 0755, true)递归创建目录
📊 数据处理
- • CSV:
fgetcsv()/fputcsv() - • JSON:
json_decode()/json_encode() - • 中文 JSON 加
JSON_UNESCAPED_UNICODE - • PHP 8.x 推荐
JSON_THROW_ON_ERROR
🔒 上传与安全
- • 用
finfo检测真实 MIME,不信任客户端 - • 随机文件名防覆盖和目录穿越
- •
move_uploaded_file()移动上传文件 - • 上传目录禁止执行 PHP 脚本