Menu

PHP fopen():打开、读取和写入文件(模式详解)

fopen('data.txt', 'r') 打开一个文件并返回文件句柄,失败时返回 false。用 fgets() 或 fread() 从句柄读取,用 fwrite() 写入,用 fclose() 关闭。模式('r'、'w'、'a'、'x'、'c' 及其带 + 的形式)决定了你能做什么。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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() 获取的锁,还能防止长时间运行的脚本耗尽文件句柄。

Coddy 编程语言插图

和 Coddy 一起学编程

开始