codeigniter 3.x 中应手动设置 header 并用 fputcsv 写入 php://output 实现流式 csv 下载,因 force_download() 仅支持本地文件路径、不接受字符串,且需清除输出缓冲、添加 utf-8 bom 防中文乱码。

直接用 header() 和 fputcsv() 输出 CSV,不生成临时文件,就能实现流式下载 —— 这是 CodeIgniter(尤其是 3.x)里最轻量、最可控的方式。
为什么不能直接用 force_download() 传 CSV 字符串
CodeIgniter 的 force_download() 函数在 CI 3.x 中只接受**已存在的本地文件路径**,传入字符串会失败(报错 File does not exist)。它内部调用 readfile(),不是为内存内容设计的。
- 想绕过文件系统?别传字符串给
force_download(),它不吃这套 - CI 4 有
Response::setBody()+ 手动设 header 的方式,但 CI 3.x 必须自己控制输出流 - 临时写文件再删?多此一举,还可能因权限或并发出问题
正确做法:手动设置 header + fputcsv() 写到 php://output
核心是跳过框架的输出缓冲干扰,把 CSV 行直接写进响应体。关键三步:关缓存、设 header、逐行写入。
使用 qbo-mileage CLI 及用户凭证,从 Airtable、Outlook 或 Google Calendar 记录生成 QuickBooks Online 里程 CSV 文件。
- 开头加
ob_end_clean()或if (ob_get_level()) ob_end_flush();,防止 CI 已经启了输出缓冲导致 header 失败 - 必须设这三组 header:
Content-Type: text/csv、Content-Disposition: attachment; filename="data.csv"、Cache-Control: no-cache - 用
fopen('php://output', 'w')获取输出流句柄,再用fputcsv($fp, $row)写每行 —— 它自动处理引号、逗号转义 - 中文字段要加 BOM(
\xEF\xBB\xBF)才能让 Excel 正确识别 UTF-8
ob_end_clean();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="export.csv"');
header('Cache-Control: no-cache');
$fp = fopen('php://output', 'w');
fputs($fp, "\xEF\xBB\xBF"); // BOM
fputcsv($fp, ['姓名', '邮箱', '注册时间']);
foreach ($data as $row) {
fputcsv($fp, $row);
}
fclose($fp);
exit;
常见坑:中文乱码、Excel 打不开、下载后文件为空
这些问题几乎都源于输出前有意外输出,或 header 位置不对。
- 控制器方法末尾或多处有
echo、var_dump()、空格或换行 —— 会导致 header 发送失败,CSV 变成 HTML 混合体 - 没加 BOM?Windows 上 Excel 默认当 ANSI 解码,中文全变问号
- 数据里含换行符(
\n)?fputcsv()会自动包裹双引号,但若字段本身含双引号,它会转义为"",这是标准行为,不用额外处理 - 用了 CI 的
$this->output->set_output()?别用,它和php://output冲突,直接绕过 Output 类
如果数据量大,怎么避免内存溢出
流式导出本身就是为大数据设计的 —— 关键是别把所有数据 array 读进内存再循环。
- 用数据库游标式查询:CI 3.x 中用
$this->db->query()->result()是全量加载;改用$this->db->query()->unbuffered_row()逐行取(需 MySQLi 驱动且未禁用) - 更稳妥的是分页查 + 合并输出:每次查 1000 行,
fputcsv写完立即flush()(但注意部分服务器会忽略) - 真正上万行以上,建议走队列生成文件 + 提供下载链接,而非实时流式 —— 用户体验和服务器负载得权衡
流式导出看着简单,但 header 时机、输出缓冲、BOM、字符编码这四点,漏一个就卡在「下载了但打不开」或者「中文全是方块」。实际写的时候,先确保 controller 方法里除了导出逻辑外,绝对没有其他 echo 或空行。










