前言

导出文件时,如果不需要任何复杂的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 文件。

/**
* @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.

PHP实现csv导出(多种方法对比及原理解析)的更多相关文章

  1. Android ListView 单条刷新方法实践及原理解析

    对于使用listView配合adapter进行刷新的方法大家都不陌生,先刷新adapter里的数据,然后调用notifydatasetchange通知listView刷新界面. 方法虽然简单,但这里面 ...

  2. 转载“启动\关闭Oracle数据库的多种方法”--来自百度#Oracle

    启动\关闭Oracle数据库的多种方法 启动和关闭oracle有很多种方法. 这里只给出3种方法: l         Sql*plus l         OEM控制台 l         Wind ...

  3. MVC批量导出数据方法

    近段时间做了个数据平台,其中涉及到批量导出CSV格式数据的业务,主要使用了部分视图和视图之间传值等知识点,今天做了下整理,特此分享下: 主要分为四步: 1:要打印的数据格式陈列View: 2:自定义导 ...

  4. css隐藏页面元素的多种方法

    在平常的样式排版中,我们经常遇到将某个模块隐藏,下面我整理了一下隐藏元素的多种方法以及对比(有的占据空间,有的不占据空间.有的可以点击,有的不能点击.): ( 一 )  display:  none; ...

  5. PHP csv导出数据 (二)

    全部导出和时间导出 html代码,全程并不需要引用什么插件 <include file="public@header"/> <link href="__ ...

  6. CSS导航菜单水平居中的多种方法

    CSS导航菜单水平居中的多种方法 在网页设计中,水平导航菜单使用是十分广泛的,在CSS样式中,我们一般会用Float元素或是「display:inline-block」来解决.而今天主要讲解如何让未知 ...

  7. 用 Python 排序数据的多种方法

    用 Python 排序数据的多种方法 目录 [Python HOWTOs系列]排序 Python 列表有内置就地排序的方法 list.sort(),此外还有一个内置的 sorted() 函数将一个可迭 ...

  8. yii的csv导出

    数据导出,简单的csv导出, public static function export($parameter){ if (is_array($parameter)) { $filename = da ...

  9. js判断移动端是否安装某款app的多种方法

    本文实例讲解了js判断移动端是否安装某款app的多种方法,分享给大家供大家参考,具体内容如下 第一种方法: 一:判断是那种设备 ? || u.indexOf(; //android终端或者uc浏览器 ...

  10. Python中读取csv文件内容方法

    gg 224@126.com 85 男 dd 123@126.com 52 女 fgf 125@126.com 23 女 csv文件内容如上图,首先导入csv包,调用csv中的方法reader()创建 ...

随机推荐

  1. 浪潮计算平台之AI方向——AI_Station开发环境的使用总结

    概览: 1.   开发环境 使用默认的设置,不改挂载路径: 可以看到在容器内对挂载的目录进行文件操作是可以真实记录到实际的文件目录内的. 对挂载路径的另一种设置: 不使用默认的设置,手动更改挂载路径: ...

  2. 【转载】 Visual Studio Code几款FTP插件使用总结

    ===================================================== 平时要维护类似wordpress这样的网站,然后虚拟主机又不支持远程仓的版本管理.总而言之, ...

  3. MFC对话框程序:实现程序启动画面

    MFC对话框程序:实现程序启动画面 对于比较大的程序,在启动的时候都会显示一个画面,以告诉用户程序正在加载,或者显示一些关于软件的信息,如Visual C++,Word, PhotoShop等.那么对 ...

  4. 【Jmeter】之进行单接口批量压力测试

    目录: 一.安装Jmeter 二.接口压力测试 p.p1 { margin: 0; font: 14px ".PingFang SC"; color: rgba(17, 31, 4 ...

  5. 基于 Quanto 和 Diffusers 的内存高效 transformer 扩散模型

    过去的几个月,我们目睹了使用基于 transformer 模型作为扩散模型的主干网络来进行高分辨率文生图 (text-to-image,T2I) 的趋势.和一开始的许多扩散模型普遍使用 UNet 架构 ...

  6. Postman Code Java-Unirest 代码的依赖

    本来是Postman的Code直接使用的,结果根据这个名字 Unirest,搜出来了很多依赖,使用了排名第一的, https://search.maven.org/search?q=Unirest 结 ...

  7. 用描述程序的方式emo,扎心了...

    用描述程序的方式emo,扎心了... 众所周知写程序是个枯燥无聊的过程,再加上生活的不顺与坎坷,当程序语言与emo结合起来,看谁还说程序员不懂感情! 首当其冲的就是循环语句了 世界上最寂寞的感觉,是我 ...

  8. 开源项目管理工具 Plane 安装和使用教程

    说到项目管理工具,很多人脑海中第一个蹦出来的可能就是 Jira 了.没错,Jira 确实很强大,但是...它也有点太强大了,既复杂又昂贵,而且目前也不再提供私有化部署版本了. 再说说飞书,作为国产之光 ...

  9. 【YashanDB知识库】YAS-02024 lock wait timeout, wait time 0 milliseconds

    [标题]错误码处理 [问题分类]锁等待超时 [关键字]YAS-02024 [问题描述]执行语句时候,因锁等待超时执行语句失败 [问题原因分析]数据库默认锁等待时间为0秒,如果执行语句存在锁等待过长会执 ...

  10. RGB、HSV和HSL颜色空间

    这个文章写的很清楚了 https://zhuanlan.zhihu.com/p/67930839