Skip to main content

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 操作,尤其是在需要输出大量数据或对输出内容进行复杂处理时,可以显著提高性能。

• 内容控制: 通过缓冲区,开发者可以在脚本结束前完全控制输出内容。可以随时修改、重新排序、或完全丢弃输出内容。这对生成复杂的页面或在输出前进行数据处理非常有用。

• 错误处理: 在缓冲区启用的情况下,发生错误时可以修改或取消输出内容。例如,可以在检测到错误时清空缓冲区,并输出一个自定义的错误页面,而不是显示部分已经输出的内容。

• 调试和测试: 使用缓冲区可以方便地捕获脚本的输出内容,进行分析或日志记录。这在调试和测试时非常有帮助。

二、伪协议

PHP伪协议

三、浏览器下载文件

PHP浏览器下载文件

四、其他

fputcsv()函数

fputcsv() 是 PHP 中用于将一行数据格式化为 CSV(逗号分隔值)格式,并写入文件的函数。它常用于将数据导出为 CSV 文件。


/**

* @attentionCSV 文件中的数据通常是文本格式,因此在处理数值或日期等特殊数据时,可能需要特别处理。

* @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。

*/

intfputcsv( 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($dataas$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($dataas$row) {

if($index== 1000) {

$index= 0;

ob_flush(); // 刷新输出缓冲区

flush(); // 强制刷新系统输出缓冲区

}

$index++;

fputcsv($output, $row);

}

ob_flush(); // 输出缓冲区内容

flush(); // 强制刷新系统输出缓冲区

fclose($output);

exit;

方法三: 保存为文件并输出(适合大数据集,或低更新频率的数据,需要保存文件时)

• 将数据写入文件,然后再读取文件下载,减少内存占用。

• 多了一步文件写入和读取操作,效率略低。

• 后续下载时可先查看文件是否存在


$output= fopen($filename, 'w');

foreach($dataas$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 或文件中逐行读取数据时,可以一边读取一边处理。

• 数据不需要一次性加载到内存中,只处理当前行的数据,因此内存占用非常小。


functiongenerateCsv($stream, $outputFile) {

$output= fopen($outputFile, 'w');

// 写入BOM头,解决Excel打开乱码问题

fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF));

foreach($streamas$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($dataas$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($dataas$row) {

$row= array_map(function($value) {

returniconv('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= newSplFileObject('php://output', 'w');

$output->fwrite(chr(0xEF) . chr(0xBB) . chr(0xBF)); // 写入BOM头

foreach($dataas$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($dataas$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);

}

方法九: 方法一到八的自由组合封装,由你创造~


来自 <https://www.cnblogs.com/cyamazing/p/18297046>