1、认识fprintf():不只是printf()的文件版本
PHP里做格式化输出,很多人第一反应是printf()和sprintf()。但当你需要把格式化后的内容直接写进文件、套接字或者数据库句柄时,fprintf()才是更合适的选择。
它的本质是:把格式化字符串写入一个已经打开的流资源。这个流可以是本地文件、远程URL、php://output,也可以是某些数据库扩展提供的流式句柄。
语法结构:
fprintf(stream, format, arg1, arg2, arg++)
| 参数 | 说明 | 是否必填 |
|---|---|---|
| stream | 目标流资源,通常由fopen()返回 | 必填 |
| format | 格式化模板字符串 | 必填 |
| arg1 | 第一个插入参数 | 必填 |
| arg2 | 第二个插入参数 | 可选 |
| arg++ | 后续插入参数 | 可选 |
返回值为写入字符串的字节长度。这一点在项目里经常被忽略,但它可以用来判断写入是否成功、计算偏移量,或者做简单的写入量统计。
2、fprintf()与printf()、sprintf()的取舍
这三个函数经常被混用,实际项目里要分清楚:
-
printf():直接输出到标准输出(浏览器、终端),适合调试和CLI脚本。 -
sprintf():返回格式化后的字符串,不直接输出,适合拼接后二次处理。 -
fprintf():写入指定流,适合日志文件、导出文件、流式响应。
个人经验:做日志系统时,早期我用sprintf()拼好字符串再fwrite(),多了一步内存拷贝。后来改成fprintf()直接写入文件句柄,代码更短,语义也更清晰。但要注意,fprintf()不适合频繁写入大量小片段,因为每次调用都有格式化开销,批量场景建议先拼再写。
3、格式占位符全解析
fprintf()的格式化规则继承自C语言风格,PHP在此基础上做了一些调整。下面按类型说明。
整数类
$num1 = 123456789;
$num2 = -123456789;
fprintf($file, "%%b = %b\n", $num1); // 二进制
fprintf($file, "%%c = %c\n", 50); // ASCII 字符
fprintf($file, "%%d = %d\n", $num1); // 有符号十进制
fprintf($file, "%%u = %u\n", $num2); // 无符号十进制
fprintf($file, "%%o = %o\n", $num1); // 八进制
fprintf($file, "%%x = %x\n", $num1); // 十六进制小写
fprintf($file, "%%X = %X\n", $num1); // 十六进制大写
这里有一个容易踩的坑:%u在64位PHP上对负数会得到一个很大的正数,因为底层按无符号解释。上面的$num2=-123456789输出为4171510507,这不是bug,而是补码表示的结果。如果你需要跨平台一致的负数处理,不要依赖%u。
浮点类
fprintf($file, "%%e = %e\n", $num1); // 科学计数法小写
fprintf($file, "%%E = %E\n", $num1); // 科学计数法大写
fprintf($file, "%%f = %f\n", $num1); // 浮点数,本地设置感知
fprintf($file, "%%F = %F\n", $num1); // 浮点数,非本地设置感知
fprintf($file, "%%g = %g\n", $num1); // %e 和 %f 中较短者
fprintf($file, "%%G = %G\n", $num1); // %E 和 %f 中较短者
%f和%F的区别在于小数点是否受locale影响。做多语言项目时,如果服务器locale设置了逗号作为小数点,%f可能输出123456789,000000,而%F始终输出点号。涉及金额、报表导出时,建议统一用%F,避免下游解析出错。
字符串与符号
fprintf($file, "%%s = %s\n", $num1); // 字符串
fprintf($file, "%%+d = %+d\n", $num1); // 强制带符号
fprintf($file, "%%+d = %+d\n", $num2);
%+d会强制显示正负号,适合做数据对比表、财务流水导出。
4、宽度、精度与填充:实际项目里真正有用的部分
官方文档对这部分讲得比较散,但项目里很常用。格式模板支持:
%[符号][宽度][.精度][类型]
示例:
$file = fopen("report.txt", "w");
// 右对齐,宽度 10,左侧补空格
fprintf($file, "%10s|%10s\n", "编号", "金额");
// 左侧补零
fprintf($file, "%05d\n", 42); // 00042
// 浮点保留两位小数
fprintf($file, "%.2F\n", 3.14159); // 3.14
// 左对齐
fprintf($file, "%-10s|\n", "代码号");
fclose($file);
做对账单导出时,我用%010.2F统一金额格式,保证每行长度一致,后续用固定宽度解析不会错位。这是sprintf()也能做,但fprintf()直接写文件更省事。
5、项目实例:用fprintf()写一个结构化日志
下面是一个简化但可用的日志写入示例,结合了时间戳、级别、格式化对齐。
<?php
$logFile = fopen("app.log", "a");
$level = "INFO";
$userId = 1024;
$action = "login";
$ip = "192.168.1.10";
fprintf(
$logFile,
"[%s] %-5s uid=%d action=%-8s ip=%s\n",
date("Y-m-d"),
$level,
$userId,
$action,
$ip
);
fclose($logFile);
写入结果:
[2026-10-10] INFO uid=1024 action=login ip=192.168.1.10
这里%-5s和%-8s保证不同级别的日志对齐,方便肉眼扫读,也方便后续用正则解析。如果是ERROR级别,可以换成%s不加宽度,或者统一宽度让所有行等长。
6、返回值处理与错误排查
fprintf()返回写入的字节数。如果返回false,说明写入失败。常见原因:
-
流未以可写模式打开,比如
fopen($file,"r")。 -
磁盘空间不足或权限不够。
-
流已被关闭。
-
格式化参数数量少于占位符数量,PHP8会抛出
ArgumentCountError。
个人踩坑经历:有一次在循环里反复fopen()同一个文件但忘记fclose(),导致文件句柄泄漏,最终fprintf()返回false。后来改成打开一次、循环内多次fprintf()、之后统一关闭。这个模式在写导出文件时更稳妥。
fprintf()不会自动加换行,需要自己在格式串里写\n或PHP_EOL。跨平台项目建议用PHP_EOL。
7、本节课程知识要点
-
fprintf()写入的是流资源,不是字符串返回值。 -
返回值是字节数,可用于写入校验。
-
%u对负数在不同平台表现不同,谨慎使用。 -
%f受locale影响,%F不受影响,金额场景优先%F。 -
宽度、精度、填充符可以组合使用,适合报表类输出。
-
打开一次、多次写入、之后关闭,是更安全的文件写入模式。
8、与数据库写入的关系
文章开头提到“写入文件或数据库”。严格来说,fprintf()本身不直接操作数据库,它操作的是流。某些数据库扩展或ORM提供流式接口时,才可能把流句柄交给fprintf()。更常见的做法是:用fprintf()生成格式化的SQL片段或CSV文件,再导入数据库。理解这一点,可以避免在数据库场景下误用fprintf()。
9格式占位符速查
| 占位符 | 含义 |
|---|---|
| %b | 二进制 |
| %c | ASCII字符 |
| %d | 有符号十进制 |
| %u | 无符号十进制 |
| %e/%E | 科学计数法 |
| %f/%F | 浮点数 |
| %g/%G | 较短浮点表示 |
| %o | 八进制 |
| %s | 字符串 |
| %x/%X | 十六进制 |
| %% | 百分号本身 |
fprintf()在PHP里不是最显眼的函数,但在日志、导出、流式响应这些场景里,它比sprintf()+fwrite()更直接。掌握格式占位符的细节,尤其是%u、%f和宽度填充的行为,能减少不少调试时间。项目里建议把常用的日志格式封装成函数,内部用fprintf()写入,既保持格式统一,也方便后续切换输出目标。