PHP实现csv导出(多种方法对比及原理解析)
前言
导出文件时,如果不需要任何复杂的Excel功能,请使用CSV
工作中最初遇到导出Excel的需求,都是使用的PHPExcel,它的功能非常强大,可以覆盖到绝大多数的定制化导出需求。也就一直用着了。
直到遇见了一次超大数据量导出的需求。我需要频繁调整算法,每次需要导出几百万的数据,也是那时知道Excel表格居然还有上限(104w)。再加上生成超慢,每一次替换算法,重新验证数据,都需要半个小时到两个小时左右的等待。验证时超大的Excel还经常要加载很久,或者根本打不开甚至搞崩电脑。亟需找到一个解决方法,于是,便发现了csv这个好东西。
它的速度有多快呢,每次需要导出两个小时的Excel文件,直接被优化到了秒级。至此之后,除非有插入图片之类的特殊需求,对文件的导出一律使用csv。
现在,就对使用PHP进行csv导出,做一个多种实现方法的对比总结,和简单的原理介绍。跳过原理,直达方法
CSV相关知识
一、定义和原理
CSV的定义: 逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。
换行: 对于CSV文件来说,通常建议使用标准的 \r\n
换行符(即Windows风格),因为这种格式在大多数CSV阅读器中(包括Microsoft Excel)都能正确显示,即使在Unix/Linux系统上也是如此。
简单来说,csv就是一个可以被当成excel表格格式打开的纯文本。生成的速度为什么那么快,也就可想而知了。
二、转义
从原理可以看出,当csv的cell中有分隔符时,会引起解析错误。所以对分隔符,需要做一个约定规则的转义处理。在csv中,对分隔符的转义,以逗号为例,使用双引号 ","
。而双引号本身,则使用两个双引号 ""
。
php如果使用 fputcsv()
函数进行导出,会自动进行转义处理。
三、BOM头
BOM(Byte Order Mark)是字节顺序标记,用于指示文本文件的编码方式。在UTF-8编码中,BOM头的字节序列是 0xEF 0xBB 0xBF
。在某些情况下,BOM头可以帮助文本编辑器和软件正确识别文件的编码方式。
介绍,和常见的乱码问题
对于UTF-8编码的文件,BOM头并不是必需的,因为UTF-8编码不依赖字节顺序,所有字符的字节顺序在UTF-8中是固定的。尽管UTF-8不需要BOM头来确定字节顺序,但在一些环境中,BOM头用于标识文件为UTF-8编码,特别是在一些旧版软件或特定应用中(如某些版本的Microsoft Excel),BOM头可以确保文件被正确识别为UTF-8编码,避免出现乱码问题。
在不支持BOM头的系统或软件中,BOM头可能被误处理为文件内容的一部分,导致显示问题或文件解析错误。特别是在纯文本文件或代码文件中,BOM头可能导致文件格式错误或程序异常。
解决这个问题,可以使用编辑器(notepad++等),“保存为无BOM头的UTF-8格式”的选项进行修复。(notepad++的作者是个台湾人,经常在软件中夹带反华等政治私货,推荐使用替代品,比如:notepad--
)
CSV中的BOM头
CSV文件可以使用不同的文本编码,包括UTF-8和ANSI。
对于Microsoft Excel来说,使用UTF-8编码的CSV文件时,BOM头是必需的。
ANSI编码本质上是单字节编码,没有字节顺序的问题,也没有多字节字符。因此,没有引入BOM头的需求。但它本身跨平台兼容性不好,且不支持中文。它本身的体积会更小些,适用于一些有特殊要求的场景。
PHP相关知识
一、输出缓冲区
介绍
工作机制
- 缓冲输出: 调用
ob_start()
开启缓冲区,当 PHP 脚本执行echo
或其他输出操作时,内容会先进入缓冲区,而不是直接发送到浏览器。 - 当调用
ob_flush()
或者flush()
函数时,缓冲区的内容会被发送到客户端(浏览器),同时缓冲区会被清空。
多层级
- PHP 支持多层缓冲区。可以通过多次调用
ob_start()
来创建嵌套的缓冲区,每个缓冲区可以独立地被清除、刷新或丢弃。 - 最顶层的缓冲区内容在脚本执行结束或调用
ob_end_flush()
时才会被发送到下一级缓冲区或直接输出。
底层实现
- PHP 缓冲区的底层实现依赖于操作系统的 I/O 缓冲机制,结合了内存管理技术,将输出内容存储在内存中,直到条件满足(如缓冲区已满、脚本执行结束等)才会触发实际的 I/O 操作。
- PHP 使用 C 语言的标准库 stdio 提供的缓冲机制作为底层实现的一部分,通过 ob_* 系列函数来控制这个缓冲机制。
方法
- ob_start(): 开启输出缓冲
- ob_get_contents(): 获取输出缓冲的内容。
- ob_clean(): 清空输出缓冲区而不输出内容。
- ob_end_clean(): 清空输出缓冲区并关闭输出缓冲。
- ob_flush(): 发送缓冲区内容到浏览器。
- flush(): 将缓冲内容发送给客户端。
好处
- 性能优化: PHP 缓冲区使得输出内容可以暂时存储在内存中,而不是立即发送到浏览器。这可以减少频繁的 I/O 操作,尤其是在需要输出大量数据或对输出内容进行复杂处理时,可以显著提高性能。
- 内容控制: 通过缓冲区,开发者可以在脚本结束前完全控制输出内容。可以随时修改、重新排序、或完全丢弃输出内容。这对生成复杂的页面或在输出前进行数据处理非常有用。
- 错误处理: 在缓冲区启用的情况下,发生错误时可以修改或取消输出内容。例如,可以在检测到错误时清空缓冲区,并输出一个自定义的错误页面,而不是显示部分已经输出的内容。
- 调试和测试: 使用缓冲区可以方便地捕获脚本的输出内容,进行分析或日志记录。这在调试和测试时非常有帮助。
二、伪协议
三、浏览器下载文件
四、其他
fputcsv()函数
fputcsv()
是 PHP 中用于将一行数据格式化为 CSV(逗号分隔值)格式,并写入文件的函数。它常用于将数据导出为 CSV 文件。
/**
* @attention CSV 文件中的数据通常是文本格式,因此在处理数值或日期等特殊数据时,可能需要特别处理。
* @attention 不同的 CSV 文件可能使用不同的分隔符(如逗号、制表符),可以通过设置 $delimiter 参数来适应这些需求。
*
* @params $handle:打开的文件指针,通常由 fopen() 创建。例如,$handle = fopen('file.csv', 'w');。
* @params $fields: 需要写入 CSV 文件的数组,数组中的每个元素会被当作 CSV 文件中的一列。
* @params $delimiter(可选): 列之间的分隔符,默认是逗号(,)。可以自定义为其他字符,如制表符 "\t"。
* @params $enclosure(可选): 包围每个字段的字符,默认是双引号(")。如果字段中包含分隔符、换行符或特殊字符,fputcsv() 会自动为该字段加上这个字符。
* @params $escape_char(可选): 转义字符,默认是反斜杠(\),用于转义特殊字符。
* @return: fputcsv() 返回写入文件的字节数。如果发生错误,则返回 false。
*/
int fputcsv ( resource $handle , array $fields [, string $delimiter = "," [, string $enclosure = '"' [, string $escape_char = "\\" ]]] )
生成器
生成器是 PHP 中的一个功能,通过 yield 关键字实现逐步生成数据,而不是一次性返回所有数据。
生成器可以显著减少内存使用,尤其是在处理大数据集或流数据时,因为它只在需要时生成数据。
SplFileObject 类
SplFileObject 是 PHP 的一个类,提供了一种面向对象的方式来读取和写入文件。与 fopen 等函数相比,它提供了更高级的功能,比如按行读取、CSV 处理等。
SplFileObject 适合处理文件的复杂操作,如逐行读取大文件、解析 CSV 文件等。
ANSI 编码
ANSI 编码指的是 Windows 系统中使用的 8 位字符编码,常见于旧版本的 Windows 文件。
ANSI 编码在处理非英语字符时可能导致显示问题,如无法正确显示中文。相比之下,UTF-8 是一种更加通用的编码格式,适合处理全球多语言文本。
PHP导出CSV代码实现
一般情况下,使用方法一、二、三,可以覆盖大部分的导出需求。
小到中等数据集与大数据集,MB(兆字节)作为单位:
- 小数据集:通常小于1MB。
- 中等数据集:1MB到10MB之间。
- 大数据集:大于10MB。
通用变量:
$data = [
["标题1", "标题2", "标题3"],
["内容1-1", "内容1-2", "内容1-3"],
["内容2-1", "内容2-2", "内容2-3"],
];
$filename = "data.csv";
方法一: php://output直接输出(适合中小数据集,少量下载,无需保存文件时)
- 简单直接,适合绝大多数场景。
- 直接输出CSV内容到浏览器,无需生成临时文件。
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
$output = fopen('php://output', 'w');
// 写入BOM头,解决Excel打开乱码问题
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;
方法二: php://output直接输出,加上缓冲区的处理(适合中大数据集,少量下载,无需保存文件时)
- 在非常大的数据集或网络传输速率较低时,使用缓冲区可以控制数据的发送节奏,防止浏览器或服务器在处理大量数据时出现过载。
- flush配合output实现用户无感知的即时输出,可以在脚本未执行完毕时,浏览器就开始接收数据。(在大多数导出CSV文件的场景下,由于缓冲区一般不大,flush()的作用可能并不显著)
- 对内存敏感,这里设置每1000行刷新输出缓冲区,1000可适当调整。(这个数值的合理性取决于多个因素,包括服务器内存、输出数据量、PHP的内存限制等。也可实现后进行监控调整)
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
ob_end_clean(); // 清除缓冲区,避免额外输出影响CSV文件内容
ob_start(); // 开启输出缓冲区
$output = fopen('php://output', 'w');
// 写入BOM头,解决Excel打开乱码问题
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
$index = 0;
foreach ($data as $row) {
if ($index == 1000) {
$index = 0;
ob_flush(); // 刷新输出缓冲区
flush(); // 强制刷新系统输出缓冲区
}
$index++;
fputcsv($output, $row);
}
ob_flush(); // 输出缓冲区内容
flush(); // 强制刷新系统输出缓冲区
fclose($output);
exit;
方法三: 保存为文件并输出(适合大数据集,或低更新频率的数据,需要保存文件时)
- 将数据写入文件,然后再读取文件下载,减少内存占用。
- 多了一步文件写入和读取操作,效率略低。
- 后续下载时可先查看文件是否存在
$output = fopen($filename, 'w');
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
// 下载文件
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($filename));
header('Content-Length: ' . filesize($filename));
readfile($filename);
exit;
方法四: 保存为文件,使用流+生成器(减少数组$data的内存使用,超大文件时)
- 当数据源是流式的,比如从数据库、API 或文件中逐行读取数据时,可以一边读取一边处理。
- 数据不需要一次性加载到内存中,只处理当前行的数据,因此内存占用非常小。
function generateCsv($stream, $outputFile) {
$output = fopen($outputFile, 'w');
// 写入BOM头,解决Excel打开乱码问题
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($stream as $row) {
fputcsv($output, $row);
yield;
}
fclose($output);
}
// 假设 $stream 是数据流,例如从数据库中逐行读取数据
$filename = "data.csv";
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($filename));
header('Content-Length: ' . filesize($filename));
// 调用生成器函数
foreach (generateCsv($stream, $filename) as $row) {
// 每行生成CSV内容
}
exit;
方法五: 使用php://memory配合缓存(适合短期内大量下载且不频繁变化的情况)
- php://memory不会写入文件,而是将数据保存在内存中。
- 内存敏感。
// 缓存标识符
$cacheKey = 'csv_data_cache';
// 检查缓存是否存在
if (apcu_exists($cacheKey)) {
// 从缓存中获取CSV数据
$csvData = apcu_fetch($cacheKey);
} else {
// 创建一个内存流
$output = fopen('php://memory', 'w');
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); // 写入BOM头
// 写入CSV数据
foreach ($data as $row) {
fputcsv($output, $row);
}
// 重置指针到流的开头
rewind($output);
// 将内存流中的数据读取到字符串变量
$csvData = stream_get_contents($output);
fclose($output);
// 将CSV数据写入缓存
apcu_store($cacheKey, $csvData, 3600); // 缓存1小时
}
// 输出CSV数据
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="data.csv"');
echo $csvData;
exit;
方法六: 使用ANSI编码的CSV文件(对文件、使用内存大小有要求,没有中文等字符的需求时)
- 需注意平台间兼容性,选择合适的编码
- 也可和方法五结合,进一步减少内存使用
$data = array(
array("Name", "Age", "Email"),
array("John Doe", 25, "johndoe@example.com"),
array("Jane Smith", 30, "janesmith@example.com"),
);
$output = fopen('php://output', 'w');
foreach ($data as $row) {
$row = array_map(function($value) {
return iconv('UTF-8', 'Windows-1252//IGNORE', $value);
}, $row);
fputcsv($output, $row);
}
fclose($output);
exit;
方法七: 使用文件类SplFileObject(没有必要,杀鸡用牛刀,其他封装类同理,可用于练手)
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
$output = new SplFileObject('php://output', 'w');
$output->fwrite(chr(0xEF) . chr(0xBB) . chr(0xBF)); // 写入BOM头
foreach ($data as $row) {
$output->fputcsv($row);
}
exit;
方法八: 手动拼接(没有必要,可用于练手)
// 设置Header头,输出为CSV文件
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
$output = fopen('php://output', 'w');
// 写入BOM头,解决Excel打开UTF-8编码文件时的乱码问题
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($data as $row) {
// 手动拼接CSV行
$escapedRow = array_map(function($field) {
// 如果字段中包含逗号、引号或换行符,则需要进行转义处理
if (strpos($field, ',') !== false || strpos($field, '"') !== false || strpos($field, "\n") !== false) {
// 将双引号转义为两个双引号
$field = str_replace('"', '""', $field);
// 将字段用双引号括起来
$field = '"' . $field . '"';
}
return $field;
}, $row);
// 拼接成CSV格式的字符串
$csvLine = implode(",", $escapedRow) . "\n";
// 输出拼接后的字符串
fwrite($output, $csvLine);
}
方法九: 方法一到八的自由组合封装,由你创造~
the end.