第十五章-PHP文件编程
第十四章-PHP文件编程
一,文件编程的基本概念
1.文件编程的意义
文件编程是指通过程序对文件进行读取、写入、修改、删除等操作。在 PHP 中,文件编程是构建动态 Web 应用的重要能力之一。它允许服务器端代码与持久化数据进行交互,例如保存用户提交的信息、记录日志、读取配置文件等。文件相比数据库更轻量,适用于结构简单、数据量较小的场景。
2.文件是如何被表示的
在 PHP 中,文件通常被当作一种“资源”(resource)来处理。资源是一种特殊类型,代表一个打开的外部数据源,如文件、网络连接或数据库连接。当打开一个文件时,PHP 会返回一个指向该文件的资源指针,该资源在使用完成后应被关闭以释放系统资源。
3.文件路径的逻辑
PHP 访问文件时需要提供路径,路径分为绝对路径和相对路径。
- 绝对路径指明了文件在系统中的完整位置,与当前 PHP 文件位置无关。
- 相对路径是相对于当前脚本执行的目录。
理解路径是文件编程的基础,因为文件读取失败最常见的原因之一就是路径错误或文件不存在。
4.文件的权限控制
每个文件在操作系统中都带有权限设置。即使 PHP 代码语法正确,如果 PHP 运行的用户(如 Apache 的 www-data)没有对应权限,也无法访问目标文件。这些权限通常分为读、写、执行三种,操作文件前应确认目标文件对 PHP 有相应的权限授权。
5.文件的类型与结构
PHP 可以处理的文件类型非常广泛,从文本文件(如 .txt
, .csv
, .json
)到二进制文件(如图片、音频、PDF)都能通过文件编程接口进行读写。
- 文本文件通常以字符为单位进行操作。
- 二进制文件则以字节为单位,需要特殊处理避免破坏数据格式。
理解文件类型有助于选择正确的读取方式和编码方式。
6.输入与输出流的概念
在文件编程中,文件被视为一种“流”(stream)。流是一种以顺序方式访问数据的机制。可以将文件理解为一串连续的字节流,通过输入流(读)和输出流(写)与之交互。PHP 的文件函数大多基于流操作模型,允许开发者按需从文件中提取数据或将数据写入文件。
7.缓冲与性能的基本理解
在进行文件写入时,数据不会立即写入磁盘,而是先存放在缓冲区中。当缓冲区满了,或者显式调用刷新函数,数据才会被真正写入。这种机制提高了性能,但也带来数据未及时写入的风险,因此编程时需要根据实际情况考虑是否立即刷新或关闭文件来保证数据落盘。
8.并发访问与数据一致性
当多个用户或进程同时访问同一个文件时,可能会发生并发冲突,如同时写入导致数据错乱。为此,文件编程中引入了**锁机制(Lock)**来确保操作的原子性和一致性。理解并发访问问题对于构建健壮的应用尤为重要。
9.文件操作的安全性
由于文件操作涉及对服务器资源的直接读写,存在一定的安全风险。常见的攻击包括目录遍历、远程文件包含(RFI)、恶意上传等。开发者必须理解文件路径验证、扩展名过滤、MIME 类型校验等安全概念,以防止攻击者绕过逻辑访问敏感文件或写入恶意内容。
10.与其他数据存储方式的区别
文件编程是一种直接、低成本的持久化方案,适用于简单、独立的数据存取。而数据库编程则更适合结构化、可查询、可扩展的大规模数据。两者各有优劣,文件通常用于配置、日志、缓存、小规模数据存储,数据库用于业务核心数据存储。
11.抽象与封装的思维
虽然 PHP 提供了大量面向过程的文件函数(如 fopen
、fwrite
等),但在实际开发中,常常需要将文件操作封装为类或模块,隐藏底层细节,提升代码可读性和可维护性。这种面向抽象的思维,也是理解文件编程从“技巧”走向“系统能力”的关键一步。
12.文件编程的典型应用场景
理解文件编程的基本概念后,我们会发现它在实际开发中无处不在,例如:
- 用户日志记录(如访问日志、错误日志);
- 上传图片或文档的保存;
- 配置文件(如 JSON、INI、XML)的解析;
- 简易缓存系统;
- 临时数据存储(如导出文件、自动生成的静态页面);
这些应用都需要在稳定性、安全性、性能之间取得良好平衡。
综上所述,PHP 文件编程不仅是基础技能,更是一种理解服务端数据流动的方式。掌握文件的结构、权限、路径、并发控制、安全机制等基本概念,有助于开发者设计出更高效、可靠的系统。理解这些核心思想,比掌握具体函数更重要,是构建 Web 后端能力的基石。
二, 路径操作
文件操作
-
读取文件内容
file_get_contents()
:读取整个文件。
$content = file_get_contents('example.txt'); if ($content !== false) {echo $content; }
-
写入文件内容
file_put_contents()
:写入内容到文件。
$bytes = file_put_contents('example.txt', 'Hello, World!'); if ($bytes !== false) {echo "写入成功,写入字节数:$bytes"; }
-
逐行读取文件
fopen()
结合fgets()
:
$handle = fopen('example.txt', 'r'); if ($handle) {while (($line = fgets($handle)) !== false) {echo $line;}fclose($handle); }
目录操作
-
创建目录
mkdir()
:创建目录,支持递归创建。
if (!file_exists('path/to/dir')) {mkdir('path/to/dir', 0755, true); }
-
遍历目录
scandir()
:获取目录内容。
$files = scandir('mydir'); foreach ($files as $file) {if (!in_array($file, ['.', '..'])) {echo $file . PHP_EOL;} }
-
删除非空目录
- 递归删除函数:
function deleteDir($dir) {if (!is_dir($dir)) return;foreach (scandir($dir) as $item) {if ($item == '.' || $item == '..') continue;$path = $dir . DIRECTORY_SEPARATOR . $item;is_dir($path) ? deleteDir($path) : unlink($path);}rmdir($dir); } deleteDir('path/to/dir');
路径处理
-
解析路径信息
pathinfo()
:获取路径各部分。
$info = pathinfo('/var/www/index.php'); echo $info['dirname']; // 输出:/var/www echo $info['basename']; // 输出:index.php echo $info['extension']; // 输出:php
-
处理绝对路径
realpath()
:转换为绝对路径。
$absPath = realpath('../file.txt'); if ($absPath !== false) {echo "绝对路径:$absPath"; }
-
跨平台路径拼接
- 使用
DIRECTORY_SEPARATOR
:
$dir = 'path/to'; $file = 'file.txt'; $fullPath = $dir . DIRECTORY_SEPARATOR . $file;
- 使用
安全注意事项
-
过滤用户输入:使用
basename()
处理文件名,避免路径遍历。$userFile = basename($_POST['filename']); // 防止类似../../的路径
-
文件上传验证:检查MIME类型和扩展名,使用
move_uploaded_file()
确保安全。if (move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . basename($_FILES['file']['name']))) {echo "文件已安全上传。"; }
实用函数
- 检查文件类型:
is_file()
和is_dir()
。 - 文件锁:
flock()
处理并发访问。 - 文件信息:
filesize()
,filemtime()
获取大小和修改时间。
三,递归遍历目录
方法 1:自定义递归函数
通过 scandir()
遍历目录,遇到子目录时递归调用自身。
function recursiveScan($dir) {$result = [];// 检查目录是否存在if (!is_dir($dir)) {return $result;}// 扫描目录内容$items = scandir($dir);foreach ($items as $item) {// 跳过 "." 和 ".."if ($item == '.' || $item == '..') {continue;}$path = $dir . DIRECTORY_SEPARATOR . $item;// 如果是目录,递归遍历if (is_dir($path)) {$result[$path] = recursiveScan($path);} else {// 如果是文件,记录路径$result[] = $path;}}return $result;
}// 使用示例
$dirTree = recursiveScan('/path/to/dir');
print_r($dirTree);
输出示例:
Array
([/path/to/dir/file1.txt] => file1.txt[/path/to/dir/subdir] => Array([/path/to/dir/subdir/file2.txt] => file2.txt)
)
方法 2:使用 SPL 迭代器
PHP 的 RecursiveDirectoryIterator
和 RecursiveIteratorIterator
提供了更简洁的遍历方式。
function recursiveScanWithIterator($dir) {$result = [];$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::SELF_FIRST);foreach ($iterator as $item) {$path = $item->getPathname();if ($item->isDir()) {$result[$path] = [];} else {$result[] = $path;}}return $result;
}// 使用示例
$dirTree = recursiveScanWithIterator('/path/to/dir');
print_r($dirTree);
特点:
- 自动跳过
.
和..
。 - 支持
SELF_FIRST
(先遍历目录自身)或CHILD_FIRST
(先遍历子内容)。 - 性能更优,适合大目录。
关键注意事项
-
符号链接(Symlinks):
- 默认情况下,迭代器会跟随符号链接,可能导致死循环。添加
FilesystemIterator::FOLLOW_SYMLINKS
作为第二个参数(需谨慎)。 - 自定义函数需手动处理
is_link()
。
- 默认情况下,迭代器会跟随符号链接,可能导致死循环。添加
-
权限问题:
if (!is_readable($dir)) {throw new Exception("目录不可读: $dir"); }
-
排除特定文件/目录:
// 在自定义函数中添加过滤条件 if (in_array($item, ['node_modules', '.git'])) {continue; }
-
性能优化:
- 对于海量文件,优先使用
RecursiveDirectoryIterator
。 - 避免在递归中频繁调用
is_dir()
或is_file()
,直接依赖迭代器的isDir()
方法。
- 对于海量文件,优先使用
应用场景
- 统计文件数量:遍历时计数。
- 批量修改文件:例如重命名、调整权限。
- 搜索文件:根据名称或扩展名过滤。
- 生成目录树:输出为 JSON 或 HTML 结构。
完整示例:输出目录树
function printDirectoryTree($dir, $indent = '') {$items = scandir($dir);foreach ($items as $item) {if ($item == '.' || $item == '..') continue;$path = $dir . DIRECTORY_SEPARATOR . $item;echo $indent . "├── " . $item . PHP_EOL;if (is_dir($path)) {printDirectoryTree($path, $indent . "│ ");}}
}// 使用示例
printDirectoryTree('/path/to/dir');
输出效果:
├── file1.txt
├── subdir
│ ├── file2.txt
│ └── subsubdir
│ └── file3.txt
四,文件操作
一、基础文件操作
1. 读取文件内容
-
一次性读取全部内容:
$content = file_get_contents('example.txt'); if ($content !== false) {echo $content; } else {echo "读取文件失败!"; }
-
逐行读取文件(适合大文件):
$handle = fopen('example.txt', 'r'); if ($handle) {while (($line = fgets($handle)) !== false) {echo $line;}fclose($handle); }
2. 写入文件内容
-
覆盖写入:
$bytes = file_put_contents('example.txt', 'Hello, World!'); if ($bytes !== false) {echo "写入成功,字节数:$bytes"; }
-
追加写入:
$bytes = file_put_contents('example.txt', "\nNew Line", FILE_APPEND);
-
使用
fopen
+fwrite
(更灵活):$handle = fopen('example.txt', 'a'); // 'a' 表示追加模式 if ($handle) {fwrite($handle, "Appended Content\n");fclose($handle); }
3. 删除文件
if (file_exists('file_to_delete.txt')) {if (unlink('file_to_delete.txt')) {echo "文件删除成功!";} else {echo "删除失败,检查权限或文件是否被占用。";}
}
二、高级文件操作
1. 处理大文件(分块读取)
$handle = fopen('large_file.txt', 'r');
if ($handle) {while (!feof($handle)) {$chunk = fread($handle, 4096); // 每次读取 4KBecho $chunk;}fclose($handle);
}
2. 文件锁定(防止并发冲突)
$handle = fopen('data.txt', 'a');
if ($handle) {if (flock($handle, LOCK_EX)) { // 独占锁fwrite($handle, "数据更新\n");flock($handle, LOCK_UN); // 释放锁}fclose($handle);
}
3. 文件重命名与移动
if (rename('old_name.txt', 'new_name.txt')) {echo "重命名成功!";
}
// 移动文件到其他目录
rename('source/file.txt', 'destination/file.txt');
4. 文件复制
if (copy('source.txt', 'destination.txt')) {echo "复制成功!";
}
三、文件元数据与状态检查
1. 获取文件信息
$size = filesize('example.txt'); // 文件大小(字节)
$mtime = filemtime('example.txt'); // 最后修改时间(时间戳)
$isReadable = is_readable('example.txt'); // 是否可读
$isWritable = is_writable('example.txt'); // 是否可写
2. 检查文件类型
if (is_file('example.txt')) {echo "这是一个文件。";
}
if (is_dir('example_dir')) {echo "这是一个目录。";
}
3. 获取 MIME 类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, 'example.jpg');
finfo_close($finfo);
echo "MIME 类型:$mime"; // 输出:image/jpeg
四、特殊文件格式处理
1. 操作 CSV 文件
// 读取 CSV
$handle = fopen('data.csv', 'r');
while (($row = fgetcsv($handle)) !== false) {print_r($row); // 数组形式输出每行数据
}
fclose($handle);// 写入 CSV
$data = [['Name', 'Email'],['Alice', 'alice@example.com'],
];
$handle = fopen('data.csv', 'w');
foreach ($data as $row) {fputcsv($handle, $row);
}
fclose($handle);
2. 操作 JSON 文件
// 读取 JSON
$json = file_get_contents('data.json');
$data = json_decode($json, true); // 转为关联数组// 写入 JSON
$data = ['key' => 'value'];
file_put_contents('data.json', json_encode($data, JSON_PRETTY_PRINT));
五、临时文件处理
// 创建临时文件
$tempFile = tmpfile();
fwrite($tempFile, "临时内容");
fseek($tempFile, 0); // 重置指针
echo fread($tempFile, 1024);
fclose($tempFile); // 自动删除临时文件// 获取临时目录路径
$tempDir = sys_get_temp_dir();
六、安全注意事项
-
路径验证:
$userInput = $_GET['file']; $safePath = realpath(basename($userInput)); // 防止路径遍历攻击
-
文件上传安全:
if (is_uploaded_file($_FILES['file']['tmp_name'])) {$target = 'uploads/' . basename($_FILES['file']['name']);move_uploaded_file($_FILES['file']['tmp_name'], $target); }
-
错误处理:
set_error_handler(function($errno, $errstr) {throw new Exception("文件操作错误:$errstr"); }); try {file_get_contents('nonexistent.txt'); } catch (Exception $e) {echo $e->getMessage(); } restore_error_handler();
七、实用场景示例
1. 日志记录
function logMessage($message) {$logFile = 'app.log';$timestamp = date('Y-m-d H:i:s');file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
}
logMessage("用户登录成功");
2. 配置文件读取
$config = parse_ini_file('config.ini');
echo "数据库主机:" . $config['db_host'];
3. 数据备份
$backupFile = 'backup_' . date('YmdHis') . '.txt';
copy('data.txt', 'backups/' . $backupFile);
八、文件操作函数一览表
分类 | 函数 | 描述 | 示例 | 注意事项 |
---|---|---|---|---|
基础文件读写 | file_get_contents() | 读取整个文件内容到字符串。 | $content = file_get_contents('file.txt'); | 大文件可能导致内存溢出,需谨慎使用。 |
file_put_contents() | 将内容写入文件,支持追加模式。 | file_put_contents('file.txt', 'Hello', FILE_APPEND); | 默认覆盖写入,需显式使用 FILE_APPEND 追加。 | |
fopen() + fwrite() | 打开文件并写入内容,支持多种模式(如 r , w , a )。 | php $handle = fopen('file.txt', 'w'); fwrite($handle, 'Text'); fclose($handle); | 必须手动关闭句柄(fclose() ),否则可能导致资源泄漏。 | |
fgets() | 逐行读取文件内容。 | php while ($line = fgets($handle)) { ... } | 适用于大文件逐行处理,需结合 fopen() 使用。 | |
文件元数据 | filesize() | 获取文件大小(字节)。 | echo filesize('file.txt'); | 文件不存在返回 false ,需先检查。 |
filemtime() | 获取文件最后修改时间(时间戳)。 | echo date('Y-m-d', filemtime('file.txt')); | 结果需用 date() 格式化输出。 | |
file_exists() | 检查文件或目录是否存在。 | if (file_exists('file.txt')) { ... } | 不区分文件和目录,需结合 is_file() 或 is_dir() 进一步判断。 | |
文件操作 | unlink() | 删除文件。 | if (unlink('file.txt')) { echo '删除成功'; } | 权限不足或文件被占用时可能失败。 |
rename() | 重命名或移动文件。 | rename('old.txt', 'new.txt'); | 目标路径需有写入权限。 | |
copy() | 复制文件到新路径。 | copy('source.txt', 'dest.txt'); | 目标文件若存在会被覆盖。 | |
目录操作 | mkdir() | 创建目录,支持递归创建。 | mkdir('path/to/dir', 0755, true); | 需检查目录是否存在(file_exists() ),避免重复创建。 |
rmdir() | 删除空目录。 | if (rmdir('empty_dir')) { ... } | 只能删除空目录,非空目录需先递归删除内容。 | |
scandir() | 列出目录中的文件和子目录。 | $items = scandir('mydir'); | 返回包含 . 和 .. 的数组,需手动过滤。 | |
路径处理 | realpath() | 返回规范化的绝对路径。 | echo realpath('../file.txt'); | 路径不存在返回 false 。 |
basename() | 获取路径中的文件名部分。 | echo basename('/var/www/file.txt'); // file.txt | 常用于过滤用户输入,防止路径遍历攻击。 | |
dirname() | 获取路径中的目录部分。 | echo dirname('/var/www/file.txt'); // /var/www | 与 basename() 结合使用可拆分路径。 | |
pathinfo() | 返回路径的组成部分(目录名、文件名、扩展名等)。 | $info = pathinfo('/var/www/file.txt'); | 可通过键名(如 $info['extension'] )获取特定部分。 | |
高级操作 | flock() | 对文件加锁(共享锁或独占锁)。 | php if (flock($handle, LOCK_EX)) { ... } | 需在 fopen() 后使用,避免并发写入冲突。 |
tmpfile() | 创建临时文件,关闭后自动删除。 | php $tmp = tmpfile(); fwrite($tmp, 'temp data'); | 适用于短期临时存储,脚本结束自动清理。 | |
finfo_file() | 获取文件的 MIME 类型。 | php $finfo = finfo_open(FILEINFO_MIME); echo finfo_file($fin | 需要 Fileinfo 扩展支持。 |
项目名称:LFI File Viewer Demo
目录结构示意:
lfi-demo/
├── uploads/ ← 存储上传文件的目录
├── index.php ← 主页面,文件上传表单
├── view.php ← 文件包含与展示页面(存在LFI漏洞)
└── .htaccess ← 防止Apache阻止访问uploads
index.php(上传页面)
<!DOCTYPE html>
<html>
<head><title>File Upload Demo</title>
</head>
<body><h2>Upload a File</h2><form action="index.php" method="POST" enctype="multipart/form-data"><input type="file" name="file"><input type="submit" value="Upload"></form><?phpif ($_SERVER['REQUEST_METHOD'] == 'POST') {$uploadDir = 'uploads/';$filename = basename($_FILES['file']['name']);$targetPath = $uploadDir . $filename;if (move_uploaded_file($_FILES['file']['tmp_name'], $targetPath)) {echo "File uploaded successfully!<br>";echo "View it: <a href='view.php?page=uploads/$filename'>Click here</a>";} else {echo "Upload failed!";}}?>
</body>
</html>
view.php(存在 LFI 的文件包含页面)
<?php
if (isset($_GET['page'])) {$page = $_GET['page'];// 基本过滤绕过演示(可改进为更复杂)if (strpos($page, '..') !== false) {die("Directory traversal not allowed!");}include($page); // LFI 漏洞点
} else {echo "No file specified.";
}
?>
uploads/.htaccess(确保 Apache 允许访问)
Options +Indexes
Allow from all