导出数据表格这个需求基本在所有互联网上的后台都会存在,你会很疑惑,为什么不导出excel而是csv,PHP就有一个导出excel的插件--PHPExcel啊,直接用多方便,下面我们来看一下excel的一下限制:
Excel2003及以下的版本一张表最大支持65536行,256列
Excel2007-2016版本一张表最大支持1048576行,16384列
而PHPExcel,在创建表之前,都是讲数据存储在变量中的,所以,当数据量到达一定的时候,就会报内存溢出
这个时候csv出现,csv以文本的格式导出,效率高,导出后可以用Excel的方式打开,简单方便
1. 使用set_limit_time(0),设置导出的时候,程序执行时间不受限制(效果不大,因为Nginx也还会出现连接超时)
2. 分批读取数据,PHP运行的时候是将数据放在内存中的,数据量太大会导致内存溢出
导出时,查库分页数据不宜太多,每次分页执行完之后,需要unset数据集
在处理数据集输出时,不能再有数据库查询操作,否则,每一次循环就会产生n次数据库操作,占用大量内存空间--
--到达一定量只有,内存就会爆满,最终导致php线程异常退出,导出中断
3. csv导出可以使用putcsv函数或者直接echo对应的格式,但是这些操作并不会直接输出到浏览器,而是去到PHP的
输出缓存buffer里,要想把数据从缓存中读取到页面,则需要调用:
ob_flush(); //output_buffer
flush(); //buffer
这两个知识点,请自己去Google
4. 上代码:编写调用类
class Array2Csv
{
public $_useGbk = true;
private $fp = null;
public function __construct(){
//打开PHP文件句柄,php://output 表示直接输出到php缓存
$this->fp = fopen('php://output', 'w');
}
//设置头部
public function cvsHeader($filename)
{
//error_reporting(0);
if($this->_useGbk) {
header("Content-type:text/csv;charset=gbk");//application/vnd.ms-excel
} else {
header("Content-type:text/csv;charset=utf-8");
}
header("Content-Disposition:attachment;filename=" . $filename);
header('Cache-Control:must-revalidate,post-check=0,pre-check=0,max-age=0');
header('Expires:0');
header('Pragma:public');
}
//采用putcsv封装格式
public function outputData($data){
foreach ($data as $key => $value) {
//CSV的Excel支持GBK编码,一定要转换,否则乱码
$data[$key] = mb_convert_encoding($value, 'GBK', 'UTF-8');
}
putcsv($this->fp,$data);
}
//刷新缓存,将PHP的输出缓存输出到浏览器上
public function csvFlush(&$cnt){
ob_flush();
flush();
$cnt = 0;
}
//关闭输出流
public function closeFile(){
fclose($this->fp);
}
}
调用:
/**
* 导出订单列表
*/
public function export()
{
set_time_limit(0);
$csv = new Array2Csv();
$filename = '用户列表_' . date('YmdHis') . '.csv';
// 输出Excel文件头,可把user.csv换成你要的文件名
$csv->cvsHeader($filename);
$head = [ '用户名称','用户ID', '电话号码', '邮箱','注册来源'];
outputData($head);
$limit = 10000; //刷新缓存限制,避免php缓存过大
$cnt = 0; //用于计算缓存数量
$size = 2000;
while(1){
$query = User::query()->orderBy('id', 'desc');
$items = $query->take($size)->get();
if(empty($items) || count($items) < 1){
break;
}
foreach ($items as $key => $item) {
//这里循环,不要再有查库操作或者其他请求操作,这样会占用大量PHP内存
$data = [
empty($item->username) ? '' : intval($item->username),
empty($item->id) ? 0 : intval($item->id),
$item->phone,
$item->email
$item->source
];
$cnt++;
if ($limit == $cnt) {
//刷新一下输出buffer,防止由于数据过多造成问题
$csv->csvFlush($cnt);
}
outputData($data);
}
//释放内存
unset($items,$paySource,$channels);
}
$csv->closeFile(); //每生成一个文件关闭
exit;
}
5. 数据量超过104w怎么办?分割数据,然后再压缩,下面贴一个测试代码,具体可以自己去实现
//导出说明:因为EXCEL单表只能显示104W数据,同时使用PHPEXCEL容易因为数据量太大而导致占用内存过大,
//因此,数据的输出用csv文件的格式输出,但是csv文件用EXCEL软件读取同样会存在只能显示104W的情况,所以将数据分割保存在多个csv文件中,并且最后压缩成zip文件提供下载
function putCsv(array $head, $mark = 'zip_info', $fileName = "test.csv")
{
set_time_limit(0);
$sqlCount = 40;
// 输出Excel文件头,可把user.csv换成你要的文件名
/*header('Content-Type: application/vnd.ms-excel;charset=utf-8');
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Cache-Control: max-age=0');*/
$sqlLimit = 10;//每次只从数据库取100000条以防变量缓存太大
// 每隔$limit行,刷新一下输出buffer,不要太大,也不要太小
$limit = 10000;
// buffer计数器
$cnt = 0;
//总数计数器
$totalCount = 0;
$fileNameArr = array();
//转码
foreach ($head as $key => $val){
$head[$key] = mb_convert_encoding($val,'gbk','utf-8');
}
// 逐行取出数据,不浪费内存
for ($i = 0; $i < ceil($sqlCount / $sqlLimit); $i++) {
//$fp = fopen('php://output', 'w'); //生成临时文件
$fp = fopen($mark . '_' . $i . '.csv','w');
//chmod($mark . '_' . $i . '.csv',777);//修改可执行权限
$fileNameArr[] = $mark . '_' . $i . '.csv';
// 将数据通过fputcsv写到文件句柄
fputcsv($fp, $head);
$maxId = 1;
$fileCount = 1;
while (1){
//每个文件生成的行数,这里是测试用的,具体根据自己的需求
if($fileCount >= $sqlLimit || $totalCount >= 40){
break;
}
//$dataArr = User::where('id','>',$maxId)->take(5000)->get()->toArray();
$dataArr = $head;
if(empty($dataArr) || count($dataArr) < 1){
break;
}
foreach ($dataArr as $a) {
$cnt++;
$totalCount++;
$fileCount++;
$maxId = $a['id'];
if ($limit == $cnt) {
//刷新一下输出buffer,防止由于数据过多造成问题
ob_flush();
flush();
$cnt = 0;
}
foreach ($a as $key => $val){
$a[$key] = mb_convert_encoding($val,'gbk','utf-8');
}
fputcsv($fp, $a);
}
}
fclose($fp); //每生成一个文件关闭
}
//进行多个文件压缩
$zip = new \ZipArchive();
$filename = $mark . ".zip";
//$zip->open($filename, \ZipArchive::CREATE); //打开压缩包
if ($zip->open($filename, \ZipArchive::CREATE)!==TRUE) {
exit('无法打开文件,或者文件创建失败');
}
foreach ($fileNameArr as $file) {
//$zip->addFile($file, basename($file)); //向压缩包中添加文件
if(file_exists($file)){
$zip->addFile( $file, basename($file));//第二个参数是放在压缩包中的文件名称,如果文件可能会有重复,就需要注意一下
}
}
$zip->close(); //关闭压缩包
foreach ($fileNameArr as $file) {
unlink($file); //删除csv临时文件
}
if(!file_exists($filename)){
exit("无法找到文件"); //即使创建,仍有可能失败。。。。
}
//输出压缩文件提供下载
header("Cache-Control: public");
header("Content-Description: File Transfer");
header('Content-disposition: attachment; filename=' . basename($filename)); // 文件名
header("Content-Type: application/zip"); // zip格式的
header("Content-Transfer-Encoding: binary"); //
header('Content-Length: ' . filesize($filename)); //
ob_clean(); //调用这个主要是为了清除压缩过程中产生的缓存,不调用时可能会导致压缩包解压失败
@readfile($filename);//输出文件;
//清空输出缓存
ob_flush();
flush();
unlink($filename); //删除压缩包临时文件
}
$head = ['序号', '订单ID', '订单号', '名称', '用户名','用户ID', '电话号码', '邮箱', '实付(元)','优惠(元)', '支付方式', '状态', '创建时间','渠道号','支付来源','推荐人手机'];
putCsv($head);
完事,走人。。。