 函数)
前言fgets()是 PHP 文件系统函数族里最常用的「按行读取」函数它从文件指针file pointer里读出一行并返回。很多人第一次用它时会带着两个想当然的假设以为它像file_get_contents()一样能把整个文件读进来或者以为它返回的内容里已经不含换行符了。事实上它每次只读一行而且把行尾的换行符原样保留在返回值里——这两点决定了它后面几乎所有用法。另一个高频误解是把fgets()返回的false一律理解成「文件结束」。PHP 官方手册的说明是文件指针里没有更多数据时返回false发生错误时同样返回false。两种情况的返回值完全一样必须借助feof()才能区分。这正是很多「文件读到一半就悄悄停住、还不报错」的诡异 bug 的根源。本文按官方手册讲清fgets()的签名与length参数的确切语义说明它和fread()、fgetc()、file()、stream_get_line()各自的分工并给出逐行处理大文件时真正安全的写法。一、函数签名与参数语义以 PHP 官方手册为准当前签名是fgets(resource $stream, ?int $length null): string | false三个要点第一个参数是流资源不是文件名字符串。必须先用fopen()、fsockopen()、popen()之类得到一个有效的、尚未被fclose()关闭的流资源。直接把路径传给fgets()会得到TypeError。length是「上限」而不是「目标长度」。手册原文读取在「已读满length - 1字节」「遇到换行符换行符会被包含在返回值中」「遇到 EOF」三者中最先发生的那一个时结束。注意是length - 1因为它要为末尾的终止符留位置。length是可选的当前签名里它的类型可空?int。省略它或传null时会一直读到行尾为止没有上限。老代码里常见的fgets($fp, 4096)是「最多读 4095 字节」的意思。调用方式实际行为fgets($fp)省略length读到换行符或 EOF长度不设上限fgets($fp, 4096)最多读 4095 字节超长行会被切成多段返回fgets($fp, 2)每次最多 1 字节退化成逐字节读取很慢length传0或负数在历史版本上的语义并不一致官方也没有把它们写成受支持的用法正确做法就是要么省略、要么传一个正常的正整数。二、返回值false的两种含义这是fgets()最容易踩的地方。手册写得很清楚文件指针里没有更多数据可读时返回false发生错误时也返回false。也就是说仅凭返回值无法判断是「正常读完了」还是「出错了」。正确姿势是循环结束后用feof()复核一次?php // 适用于 PHP 8.0$fp fopen(__DIR__ . /input.txt, rb);if ($fp false) {exit(打开文件失败\n);}$lines 0;while (($line fgets($fp)) ! false) {$lines;}fclose($fp);if (!feof($fp)) {// 走到这里说明 fgets() 是因为出错才返回 false 的exit(读取过程中发生错误\n);}echo 共读取 {$lines} 行\n;另外要记住fgets()不会剥离换行符。除了文件最后一行可能没有换行符以外其余每行返回值末尾都带着\nWindows 上文本文件里是\r\n。所以做等值比较前必须自己裁剪$line rtrim($line, \r\n);三、和其他读取函数的分工PHP 里读文件的手段不止一个选错了不是「跑不动」而是「数据被悄悄切碎」函数读取单位是否保留换行典型场景fgets()一行可设上限保留逐行处理大文件、按行读 socketfgetc()一个字节不涉及手写词法扫描器fread()指定字节数不涉及二进制数据、固定长度块stream_get_line()到指定分隔符分隔符被去掉自定义分隔符如\0file()整个文件按行拆开保留可用FILE_IGNORE_NEW_LINES去掉小文件一次性读成数组file_get_contents()整个文件不涉及小文件整体读取判断标准很简单文件大小是否可控。如果文件可能是几百 MB 的日志唯一合理的做法就是fgets()或fread()配合流式处理file()和file_get_contents()会把整个文件塞进内存。四、实战示例1. 逐行读取并裁剪?php // 适用于 PHP 8.0declare(strict_types1);$path __DIR__ . /access.log;$fp fopen($path, rb);if ($fp false) {fwrite(STDERR, 无法打开文件: {$path} . PHP_EOL);exit(1);}$lineNo 0;$bytes 0;while (($line fgets($fp, 8192)) ! false) {$lineNo;$bytes strlen($line);// fgets() 保留了行尾换行比较前先裁掉echo $lineNo . : . rtrim($line, \r\n) . PHP_EOL;}if (!feof($fp)) {fwrite(STDERR, 读取过程中出现错误 . PHP_EOL);}fclose($fp);echo 共 {$lineNo} 行累计 {$bytes} 字节 . PHP_EOL;这段代码无论日志多大内存占用都是恒定的每一轮循环只保留一行最多 8191 字节。2. 处理超长行分段拼接回完整的行给length设上限以后一个超过上限的长行会被切成多段返回。想「既限制单次读取的字节数又拿到完整的行」就自己判断是否读到换行符再来拼接?php // 适用于 PHP 8.0declare(strict_types1);function processLine(string $line): void{echo 行长度: . strlen(rtrim($line, \r\n)) . PHP_EOL;}$fp fopen(__DIR__ . /data.txt, rb);$buffer ;while (($part fgets($fp, 8192)) ! false) {$buffer . $part;if (str_ends_with($part, \n)) {processLine($buffer); // 读满一整行才处理$buffer ;}}if ($buffer ! ) {processLine($buffer); // 文件最后一行可能没有换行符}fclose($fp);str_ends_with()是 PHP 8.0 才有的函数在 PHP 7.x 上可以用substr($part, -1) \n代替。3. 从标准输入读数据fgets()不限于文件php://stdin也是合法的流。写命令行小工具时很常用?php // 适用于 PHP 8.0$stdin fopen(php://stdin, r);if ($stdin false) {exit(无法打开标准输入\n);}while (($line fgets($stdin)) ! false) {$line rtrim($line, \r\n);if ($line ) {continue;}echo strtoupper($line), PHP_EOL;}fclose($stdin);直接运行echo hello | php demo.php就能看到输出。常见坑点❌ 用feof()当循环条件while (!feof($fp)) { $line fgets($fp); echo $line; }——feof()只有在读操作撞到文件末尾之后才为真所以循环会多跑一轮把false当字符串输出如果文件本身就是空的循环体还会先执行一次。✅while (($line fgets($fp)) ! false) { ... }。❌ 用松散比较判断结束while ($line fgets($fp))或if (!$line)—— 内容为0\n的一行会被 PHP 的松散比较判成假值循环提前退出数据静默丢失。✅ 一律用恒等比较! false。❌ 以为fgets()会自动去掉换行 —— 结果字符串里\n或\r\n都还在$line admin这类判断永远不成立。✅rtrim($line, \r\n)如果连行首行尾的空格也想去掉再用trim()。❌ 用fgets()读二进制文件图片、压缩包、pack()出来的数据—— 二进制里任何一个0x0A字节都会被当成行结束数据被切得乱七八糟。✅ 二进制用fread()或stream_get_contents()。❌ 不加length去读机器生成的日志 —— 只要文件里有一行没有换行符比如某个程序把整个请求体当一行写进去fgets()就会一路读到内存耗尽。✅ 传入上限如 8192超长行自己拼接。❌ 继续用fgetss()去掉行里的 HTML 标签 —— 这个函数已在PHP 8.0.0 中被移除。✅ 用strip_tags(fgets($fp))更好的做法是根本不要在读取阶段「清洗」HTML输出时再按上下文转义。❌ 依赖auto_detect_line_endings这个 INI 选项去识别老式 Mac 的\r换行 —— 该选项自PHP 8.1.0 起废弃。✅ 自己处理读回来统一rtrim($line, \r\n)或把\r也当作行分隔符处理。❌ 忘了fclose()或者用file_get_contents()读 1 GB 的日志 —— 前者会一直占着文件句柄Windows 上还会导致该文件无法被删除或重命名后者直接把整个文件搬进内存。✅ 读完立即fclose()或用try/finally保证执行大文件一律走fgets()流式处理。总结要点结论签名fgets(resource $stream, ?int $length null)返回字符串或falselength语义最多读length - 1字节省略或传null表示不设上限换行符保留在返回值里比较前必须自己rtrim()结束判断用! false不要用!feof()或松散比较EOF 与错误都返回false靠循环结束后的feof()区分适用场景大文件逐行处理、socket 按行读、标准输入不适用场景二进制数据、需要一次性读入的小文件一句话总结fgets()的核心就是「一行、带换行符、可能失败」这三件事只要把length当作上限、把false当两种含义处理、并且记得自己裁剪换行符它就是一个既省内存又稳定的读取手段。