fopen('data.txt', 'r') 打开一个文件并返回一个句柄,也就是一个流资源,其他文件函数都把它作为第一个参数。用 fgets()(一行)或 fread()(指定字节数)读取,用 fwrite() 写入,用 fclose() 关闭。第二个参数是模式:'r' 读取,'w' 写入,'a' 追加。
输出:
Got: apple
Got: banana
Got: cherry
fopen() 无法打开文件时返回 false(r 模式下文件不存在、文件夹不存在、没有权限),PHP 还会输出类似这样的警告:
Warning: fopen(missing.txt): Failed to open stream: No such file or directory in /home/index.php on line 2
所以使用句柄之前要先检查是否为 false。如果要一次性读取或写入整个小文件,file_get_contents() 和 file_put_contents() 写起来更短;fopen() 适用于分段读取或需要保持打开的文件。
fopen 的模式
| 模式 | 读 | 写 | 文件已存在时 | 文件不存在时 | 起始位置 |
|---|---|---|---|---|---|
r | 是 | 否 | 打开 | 失败 | 开头 |
r+ | 是 | 是 | 打开,保留内容 | 失败 | 开头 |
w | 否 | 是 | 清空 | 创建 | 开头 |
w+ | 是 | 是 | 清空 | 创建 | 开头 |
a | 否 | 是 | 保留内容 | 创建 | 末尾,每次写入都追加 |
a+ | 是 | 是 | 保留内容 | 创建 | 末尾,每次写入都追加 |
x | 否 | 是 | 失败 | 创建 | 开头 |
x+ | 是 | 是 | 失败 | 创建 | 开头 |
c | 否 | 是 | 保留内容 | 创建 | 开头 |
c+ | 是 | 是 | 保留内容 | 创建 | 开头 |
有两行常让人意外。r+ 在原位置覆盖已有的字节,而不是插入;a+ 总是写到末尾,即使你用 fseek() 移动了指针也一样(移动指针只影响读取):
输出:
Xbc
Xbc
Xbc
end
在 Windows 上你可能会看到 'rb' 或 'wb':b 标志表示二进制模式,可以阻止 Windows 转换行尾符。它在 Linux 和 macOS 上没有任何作用,所以加上它既无害,又便于跨平台。
用 fgets() 逐行读取文件
fgets() 返回下一行(包括末尾的 \n),到达文件末尾时返回 false。因为内存中每次只有一行,这个循环能处理任意大小的日志文件,你还可以用 break 提前结束:
输出:
2 errors in 5 lines
line 2: database timeout
line 4: disk full
要用 !== 与 false 比较,不要直接写 while ($line = fgets($fh)):一行只有 "0" 且没有换行符时(文件的最后一行可能就是这样),它是假值,会让循环提前结束。
用 fwrite() 写入和追加
fwrite() 在当前位置写入一个字符串,返回写入的字节数,失败时返回 false。fputs() 是同一个函数的别名。用 'a' 打开文件,就能在末尾添加内容,而不改动已有的内容:
输出:
wrote 12 bytes
wrote 13 bytes
wrote 15 bytes
visit /home
visit /about
visit /contact
visit /home
PHP_EOL 是 PHP 所在系统的行尾符(Linux 和 macOS 上是 \n,Windows 上是 \r\n)。对于要给其他程序读取的文件,明确选定一种,通常是 "\n"。
用 fgetcsv() 和 fputcsv() 读写 CSV 文件
fputcsv() 把一个数组写成一行 CSV,并给含有逗号、引号或换行符的字段加上引号。fgetcsv() 把一行读回为数组。要显式传入分隔符、包围符和空的转义字符:这样得到的是标准 CSV,而且在 PHP 8.4 中省略转义参数会输出弃用提示。
输出:
name,city,note
Ada,London,"likes ""math"", a lot"
Linus,Helsinki,kernel
Ada lives in London: likes "math", a lot
Linus lives in Helsinki: kernel
用 explode(',', $line) 分割行,一旦字段里含有逗号就会出错;fgetcsv() 会替你处理引号。对于用分号分隔的文件(在欧洲的 Excel 导出文件中很常见),把 ';' 作为分隔符传入。
用 fseek()、ftell() 和 rewind() 在文件中移动
打开的文件有一个当前位置。读取和写入都从这里开始,并让位置向前移动。ftell() 告诉你当前位置,fseek() 把它移动到某个字节偏移量,rewind() 回到开头。用 w+ 模式,你可以通过同一个句柄写入文件再把它读回来:
输出:
11
world
hello world
用 flock() 给文件加锁
两个请求同时读取一个计数器、加一再写回去,可能都读到 41,又都写入 42。使用 LOCK_EX 的 flock() 会让第二个请求等待,直到第一个释放锁。这里正好用 c+ 模式:它会在需要时创建文件,而且与 w+ 不同,它不会在你拿到锁之前清空文件。
输出:
Visitor number 1
Visitor number 2
Visitor number 3
锁是建议性的:它只能阻止同样调用了 flock() 的其他代码。如果只是追加一行,file_put_contents($path, $line, FILE_APPEND | LOCK_EX) 一次调用就能做到同样的事。
常见错误:while (!feof($fh))
很多老教程用 feof() 控制循环。它只有在某次读取已经碰到末尾之后才返回 true,所以循环会比行数多跑一次,最后一次 fgets() 返回 false:
输出:
string(2) "a
"
string(2) "b
"
string(2) "c
"
bool(false)
4 rounds for 3 lines
多出来的这个 false 会变成报表中的一个空行,或者数组中的一个空元素。应该直接以读取操作作为循环条件,即 while (($line = fgets($fh)) !== false),本页的其他示例都是这样写的。
常见问题
PHP 中 fopen 有哪些模式?
r 用于读取,w 先清空文件再写入,a 在末尾写入,x 创建新文件(文件已存在则失败),c 写入但不清空文件。加上 +(r+、w+、a+、x+、c+)就可以同时读写。除 r 和 r+ 外,所有模式在文件不存在时都会创建它。
fopen 中的 w 和 a 有什么区别?
w 在打开文件的同时就清空它,并从头开始写入。a 保留原有内容,每次 fwrite() 都写到文件末尾,即使调用过 fseek() 也一样,所以它是写日志文件用的模式。
PHP 中怎么读取 CSV 文件?
用 fopen($path, 'r') 打开,然后在循环中调用 fgetcsv():while (($row = fgetcsv($fh, null, ',', '"', '')) !== false) { ... }。每次调用以字段数组的形式返回一行,引号和字段内的逗号都会自动处理好。
PHP 中需要调用 fclose 吗?
脚本结束时 PHP 会关闭所有打开的文件,所以短脚本不调用也能正常运行。不过用完之后还是应该立即调用 fclose():它会刷新缓冲区中的写入,释放用 flock() 获取的锁,还能防止长时间运行的脚本耗尽文件句柄。