本文实现php批量导出数据到csv,并打包成zip
注意: PHP需要安装zip扩展
都知道普通的phpExcel导出量大的数据会占用很大的内存空间,甚至服务器会扛不住导致内存溢出,网站崩溃。此时我们可以将部分用到大量导出的地方换为导出批量导出csv然后打包成zip提供下载,因csv读写性能比excel高很多,再配合分批导出打包下载,效率和体验上会比phpExcel好。
少废话,上代码
准备测试数据:
数据表
CREATE TABLE `test_user` (`id` bigint(11) NOT NULL AUTO_INCREMENT,`name` varchar(20) NOT NULL COMMENT \'名字\',`age` tinyint(2) NOT NULL,`hobby` varchar(20) NOT NULL COMMENT \'爱好\',`create_time` datetime DEFAULT NULL,PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1183001 DEFAULT CHARSET=utf8 COMMENT=\'测试表\';
插入百万测试数据
ini_set (\'memory_limit\', \'256M\');set_time_limit(0);$user = db(\'test_user\');$time = microtime(true);$create_time = date(\'Y-m-d H:i:s\');$num = 5000;//一次插入5000条$data = [];for ($i=0; $i < $num; $i++) {$data[] = [\'name\' => \'小爱同学\',\'age\' => 18,\'hobby\' => \'摄影\',\'create_time\' => $create_time,];}for ($i=0; $i < 200; $i++) {$user->insertAll($data);}echo microtime(true)-$time;
方法:
/*** 导出csv并压缩成zip* @param array $header 表头* @param object $model 模型对象* @param string $file 导出地址* @param string $name 导出文件名,不支持中文* @param int $num 每次查询条数/每个csv文件的记录条数,默认1w* @return array [ \'status\' => true,\'data\' => \'export/file/downcsv20190121_106.zip\' ]* @author smt date 2019-1-21*/public function putCsv($header, $model,$file = \'export/file\', $name = \'down_csv\',$num = 20000){set_time_limit(0);//设置不限制脚本执行时间ini_set(\'memory_limit\', \'128M\');//设置脚本所能够申请到的最大内存// 参数校验if (!is_array($header) || empty($header)) return [ \'status\'=>false , \'data\' => \'表头参数不正确\' ];if (!is_object($model)) return [ \'status\'=>false , \'data\' => \'数据对象错误\' ];try {$sqlCount = $model->count();if ($sqlCount < 1) return [ \'status\'=>false , \'data\' => \'数据总数为0\' ];} catch (Exception $e) {return [ \'status\'=>false , \'data\' => $e->getMessage() ];}$pattern = \'/[^\\x00-\\x80]/\';if ( preg_match($pattern,$file) ) return [ \'status\'=>false , \'data\' => \'导出地址不能包含中文\' ];if ( preg_match($pattern,$name) ) return [ \'status\'=>false , \'data\' => \'导出文件名不能包含中文\' ];if ( $num > 20000 ) return [ \'status\'=>false , \'data\' => \'查询条数最多每页2万条\' ];if ( !$this->mkdirMore($file) ) return [ \'status\'=>false , \'data\' => \'创建目录失败\' ];// 处理表头编码$header = implode(\",\",$header);$header = iconv(\'UTF-8\', \'GBK//IGNORE\', $header);$header = explode(\",\", $header);$sqlLimit = $num;//每次只从数据库取 $num 条以防变量缓存太大$limit = $num;// 每隔$limit行,刷新一下输出buffer,不要太大,也不要太小$cnt = 0;// buffer计数器$fileNameArr = array();//文件名集合$file_name = $file.\'/\'.$name.date(\'YmdHis\').mt_rand(10000,99999);//组成路径及文件名// 逐行取出数据,不浪费内存for ($i = 0; $i < ceil($sqlCount / $sqlLimit); $i++) {//生成临时文件$inum = $i+1;$fp = fopen($file_name . \'_\' . $inum . \'.csv\', \'w\');$fileNameArr[] = $file_name . \'_\' . $inum . \'.csv\';// 将行格式化为 CSV 并写入一个打开的文件fputcsv($fp, $header);// 分批查询数据$dataArr = $model->limit($i * $sqlLimit,$sqlLimit)->select();foreach ($dataArr as $row) {$cnt++;if ($limit == $cnt) {//刷新一下输出buffer,防止由于数据过多造成问题ob_flush();flush();$cnt = 0;}$str = implode(\"@@@@\",$row);$str = iconv(\'UTF-8\', \'GBK//IGNORE\', $str);$str = str_replace(\",\",\"|\",$str);$row = explode(\"@@@@\", $str);fputcsv($fp, $row);}//关闭文件fclose($fp);}//进行多个文件压缩$zip = new \\ZipArchive();$filename = $file_name . \".zip\";$zip->open($filename, \\ZIPARCHIVE::CREATE); //打开压缩包foreach ($fileNameArr as $file) {$zip->addFile($file, basename($file)); //向压缩包中添加文件}$zip->close(); //关闭压缩包foreach ($fileNameArr as $file) {unlink($file); //删除csv临时文件}return [ \'status\'=>true , \'data\' => $filename];}// 递归创建目录public function mkdirMore($path, $mode = 0777){if(is_dir($path)){return true;}else{if(mkdir($path, $mode, true)) {return true;}else{return false;}}}
测试:
// 这里查询20万条数据测试$model = db(\'test_user\')->where(\'id\',\'<=\',200000);$header = [\'用户ID\',\'姓名\',\'年龄\',\'爱好\',\'注册时间\'];// 表头$file = \'export/file\';// 导出地址$name = \'downcsv\';// 导出文件名,结果为:downcsv2019012116453395196.zip(文件名+年月日时分秒+5个随机数)$res = $this->putCsv( $header,$model,$file,$name,20000 );if ($res[\'status\']) {echo \'成功:\';echo $res[\'data\'];}else{echo \'失败:\';echo $res[\'data\'];}
完 ···