fgets() 逐行读取比 file_get_contents() 更安全,因其内存占用恒定且适合大文件或网络流;但需注意换行符兼容性、HTTP 流限制、CSV 应用 fgetcsv()、Generator 封装提升复用性、实时响应需禁用缓冲并配置 Web 服务器。

用 fgets() 逐行读取比 file_get_contents() 更安全
大文件或网络流场景下,一次性加载整个文本会爆内存,file_get_contents() 直接失败或拖慢整个请求。用 fgets() 按行读取是 PHP 原生最轻量的流式方案,它每次只从文件指针读取一行(含换行符),内存占用恒定在几百字节级别。
注意点:
-
fgets()默认读到\n或 EOF 停止,若源数据用\r\n或纯\r换行,需提前统一或用stream_set_line_buffer()调整缓冲行为 - 不能直接用于 HTTP 流(如
php://input)且未启用allow_url_fopen的环境;此时应改用php://stdin或cURL + CURLOPT_WRITEFUNCTION - 读取 CSV 类结构化文本时,别用
fgets()+explode()粗暴分割——字段内含逗号或换行会导致错切;优先用fgetcsv()
用 Generator 封装流式分割逻辑更易复用
把逐行处理包装成生成器,能自然支持 foreach 迭代、延迟计算,也避免手动维护文件指针。比如按自定义分隔符(非换行)切分文本块:
function splitByDelimiter($handle, $delimiter = "\n") {
$buffer = '';
while (!feof($handle)) {
$chunk = fread($handle, 8192);
if ($chunk === false) break;
$buffer .= $chunk;
$parts = explode($delimiter, $buffer);
$buffer = array_pop($parts); // 保留不完整末尾
foreach ($parts as $part) {
yield $part;
}
}
if ($buffer !== '') yield $buffer; // 最后一块
}
关键细节:
立即学习“PHP免费学习笔记(深入)”;
- 每次
fread()大小建议设为 4096–8192 字节,太小增加系统调用开销,太大可能卡住实时流 - 必须处理“跨 chunk 边界被截断的分隔符”,否则漏切;上面示例用
array_pop()保留在缓冲区是常见做法 - 生成器函数里不要用
return返回值,PHP 7.1+ 支持yield from,但此处需手动拼接缓冲,不宜直接委托
实时 HTTP 流响应需禁用输出缓冲并设置正确 header
如果后端要边读文件边往前端吐数据(比如日志 tail),光用流式读取不够,PHP 输出层必须配合:
- 执行
ob_end_flush()和flush()前,确认output_buffering = Off(php.ini)或运行时调用ob_implicit_flush(true) - 必须输出
Content-Type和Transfer-Encoding: chunked(由 Web 服务器自动加),否则浏览器会等 EOF 才渲染 - Nginx 默认缓存 1MB 或 1s 的响应体,加
fastcgi_buffering off;或proxy_buffering off;配置才能真正实时 - 客户端用
fetch()接流时,得用response.body.getReader()+read()循环,不是response.text()
mb_split() 不适合流式场景,别踩 Unicode 分割坑
遇到中文、emoji 等多字节字符时,有人想用 mb_split('/\s+/', $line) 做词粒度切分,但这要求整行已加载进内存,违背流式初衷。更严重的是:mb_split() 在 PHP 8.0+ 已被标记为废弃,且正则引擎对超长 UTF-8 字符串回溯容易触发 PREG_BACKTRACK_LIMIT_ERROR。
替代思路:
- 按字节边界切分?不行——UTF-8 变长编码,
substr()可能截断字符导致乱码 - 真需要 Unicode 意义上的“词”分割,用
IntlBreakIterator,但它必须传入完整字符串,无法增量处理 - 务实做法:流式阶段只做行/块级粗切,语义级分析放到后续异步任务或客户端做
流式处理的核心约束从来不是“能不能切”,而是“切完要不要立刻理解它”。多数真实场景里,先稳住不崩、不断流,比花式分词重要得多。











