直接用 fopen("a") 反复打开大文件追加写入易出错:多源文件bom、换行不一致、读取中断会导致错位或截断;"a"模式无法控偏移、难校验落盘。应改用"ab"单次打开,小文件以"rb"分块读取,严格校验fwrite返回值并flush刷盘。

用 fopen 的 "a" 模式直接追加写入会出问题
直接对目标大文件反复用 fopen($bigfile, "a") 打开再 fwrite,看似合理,但实际每次 fopen 都会重新定位到文件末尾——这本身没错;可一旦多个小文件内容本身含 BOM、换行不一致(比如 Windows 的 \r\n 和 Linux 的 \n)、或其中某个文件读取失败导致写入中断,目标文件就会出现错位、重复或截断。更隐蔽的问题是:"a" 模式无法控制写入前的偏移量,也无法校验上一次写入是否真正落盘(尤其在 NFS 或某些容器挂载场景下)。
实操建议:
- 只对目标大文件 打开一次,用
fopen($bigfile, "ab")("ab"是二进制追加模式,避免 Windows 下文本模式自动换行转换) - 所有小文件统一用
"rb"模式读取,确保原始字节不被修改 - 用
fread分块读取(如 8192 字节),再fwrite写入,避免单次读取超大文件导致内存溢出 - 每次
fwrite后检查返回值是否等于预期字节数,不等则立即fclose并报错
必须处理小文件的路径与编码一致性
如果小文件来自不同来源(比如用户上传、日志切割、跨平台生成),它们的文件名编码(GBK/UTF-8)、路径分隔符(\ vs /)、甚至隐藏字符(BOM、零宽空格)都可能让 fopen 直接失败并静默返回 false,而不是抛出异常。
实操建议:
- 用
realpath($file)规范路径,再检查is_readable($file),失败则跳过或记录警告 - 对文件名做
mb_convert_encoding($filename, 'UTF-8', 'auto'),避免中文路径在某些系统下打不开 - 不要依赖
glob("*.log")的默认排序——它按字典序排,不是时间序;若需按生成时间合并,先scandir+filemtime排序 - 若小文件本身含 UTF-8 BOM(
\xEF\xBB\xBF),而你又希望大文件无 BOM,需在读取后手动ltrim($content, "\xEF\xBB\xBF")
fwrite 返回值为 0 不一定代表失败
常见误解:只要 fwrite($fp, $data) === false 就算写入失败。实际上,在磁盘满、权限不足等严重错误时,fwrite 可能返回 0(成功写入 0 字节),而非 false。而 0 在弱类型判断中常被当成 false,导致误判。
实操建议:
- 严格比较:用
!==判断fwrite返回值是否 不等于 当前待写字节数 - 示例:
$written = fwrite($big_fp, $chunk);<br>if ($written !== strlen($chunk)) {<br> error_log("Write failed: expected " . strlen($chunk) . ", got $written");<br> fclose($big_fp);<br> exit(1);<br>} - 写入后调用
fflush($big_fp)强制刷盘,尤其在长时间合并过程中防止缓存堆积
合并后文件大小与预期不符?检查 fstat 和 ftell
合并完成时,用 filesize($bigfile) 验证总大小,常发现比各小文件 filesize() 之和小几字节。原因通常是:某小文件末尾有换行,而你又在每个文件后额外加了 \n;或某个小文件读取时因编码问题被截断(比如遇到非法 UTF-8 字节序列,fread 提前终止)。
实操建议:
- 合并前先统计所有小文件的
filesize()总和,存为$expected_size - 合并结束后,用
fstat($big_fp)['size']获取真实大小(比filesize()更准,因无需重新 open) - 若偏差超过 10 字节,逐个检查小文件:用
hexdump -C $file | head -n 5看开头是否含 BOM,用file -i $file看编码类型 - 不要在小文件之间插入任何分隔符(包括空行),除非业务明确要求——那也要用固定字节(如
"\n---\n"),而非依赖PHP_EOL
fwrite 返回值的弱类型陷阱里。这两个点不盯住,其他逻辑再严谨也会产出损坏文件。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











