当前位置 博文首页 > 编程哲学家的专栏:浅谈PHP导出百万excel问题解决思路与解决方案

    编程哲学家的专栏:浅谈PHP导出百万excel问题解决思路与解决方案

    作者:[db:作者] 时间:2021-07-09 19:01

    前两年做外包时候的事了,记录一下吧,也正好梳理一下思路,温故而知新。

    php导出excel,数据量小的时候使用phpexcel可轻松解决问题,仅仅限于5w行以下数据,但实际上我在使用phpexcel导出4w行数据的时候就出问题了:

    • 一开始是报内存溢出,然后随手给了1g内存,当然是程序里ini_set动态设置没动配置文件。
    • 然后又报运行时间超过30秒,我又设置了在程序执行完在结束
    • 又过了一段时间,随后随着数据的增长又因为执行时间太长,老是断开连接,程序死掉,浏览器报响应时间太长,经过几番反复的测试分析,我加了ob函数来控制输出缓存buffer来防止程序死掉,更高效的使用内存。关于php的ob函数请参阅:PHP flush()与ob_flush()的区别详解
    • 自上次改过,没多久,大概是半个月后吧,导出功能的又出现问题了,又不能用了,导不出来了,我登录数据库查看了数据:78w条记录,半个月增长了几十万条数据,我第一感觉就知道该完全重写这个excel导出了,phpexcel太庞大、太笨重了,根本无法导出几十万数据,即使给再多的内存也无济于事,关键还是phpexcel这庞然大物太吃内存了,在吃内存上简直可以和java的jvm相提并论了,于是我痛定思痛,决定必须重写。

    在我简单看了下excel文件的格式规律后,简单写下如下代码:

     function exportExcel(){
         set_time_limit(0);
         $filename = '网格化排查';
         $title = array('编号','内容','时间','类型','用户id','区域');
         $data=$this->m->selectAll('clue');
         $filename=iconv("UTF-8", "GB2312",$filename);
         header("Content-type:application/octet-stream"); 
         header("Accept-Ranges:bytes"); 
         header("Content-type:application/vnd.ms-excel");
         header("Content-Disposition:attachment;filename=".$filename.".xls");
         header("Pragma: no-cache"); 
         header("Expires: 0");
         
         //导出xls 开始
         if (!empty($title)){
             foreach ($title as $k => $v) {
                 $title[$k]=iconv("UTF-8", "GB2312",$v);
             }
             $title= implode("\t", $title); 
             echo "$title\n";
         }
             if (!empty($data)){
                 foreach($data as $key=>$val){
                  foreach ($val as $ck => $cv) {
                     $data[$key][$ck]=mb_convert_encoding($cv, "gb2312","UTF-8");
                   } 
                   $data[$key]=implode("\t", $data[$key]); 
             }
          echo implode("\n",$data);
     
       }
     }

    ?

    ?48w行数据,导出只需要不到5秒,开启opcache后时间缩短到3秒。自己手写的导出实现果然占用资源小,运行快,不需要加载那么多没用类库文件,我依稀记得phpexcel1.8解压后是12m,570多个php文件,至于加载多少文件我们就不得而知了。

    以上代码确实解决了问题,但我感觉不是最优、最好解决方案,对于一个精益求精的,我岂能安于问题解决就高枕无忧了?

    不断的探索问题的最优解决方案才是我的目的。

    解决方案1改进:为加强程序的前瞻性和健壮性,把单次查询改成分批查询导出

     function exportExcel(){
         set_time_limit(0);
    
         $filename = '网格化排查';
         $title = array('编号','内容','时间','类型','用户id','区域');
        
         $filename=iconv("UTF-8", "GB2312",$filename);
         header("Content-type:application/octet-stream"); 
         header("Accept-Ranges:bytes"); 
         header("Content-type:application/vnd.ms-excel");
         header("Content-Disposition:attachment;filename=".$filename.".xls");
         header("Pragma: no-cache"); 
         header("Expires: 0");
         
         $total=$this->m->count('clue');
    
         //导出xls 开始
         if (!empty($title)){
             foreach ($title as $k => $v) {
                 $title[$k]=iconv("UTF-8", "GB2312",$v);
             }
             $title= implode("\t", $title); 
             echo "$title\n";
         }
    
           for($p=0;$total>$p;$p++){
           $data=$this->m->select('clue',$p,10000);//将一次查询改为分批查询,解决百万数据查询导出问题,百万数据只需要循环查询十次即可导出百万数据
     
                 foreach($data as $key=>$val){
                  foreach ($val as $ck => $cv) {
                     $data[$key][$ck]=mb_convert_encoding($cv, "gb2312","UTF-8");
                   } 
                   $data[$key]=implode("\t", $data[$key]); 
             }
            echo implode("\n",$data);//这种方式直接输入到页面,然后让用户下载当前页面数据,可改成file_put_content先分批写入到单文件里,每次写入在文件末尾追加数据,追加完毕后下载。
     
         } 
       
     }

    ?

    解决方案2:先把要把excel文件准备好

    在用户未执行导出操作的时候,服务器定时任务每隔一段时间以命令行的方式去运行导出方法,使其先把文件导出到一个文件夹下,用户点击导出的时候无需等待直接弹出文件让其下载即可,而且命令行运行效率极高,但这种方法也有缺点,就是用户下载的文件可能不是最新的。针对这个设计缺陷,这里有个候补改进方案的方案:

    取消定时任务,改用用户点击导出的时候,php收到请求后执行命令行,发送信号给系统以命令行的方式去运行php导出excel的方法,然后导出完成后再将用户导向excel文件下载。这种方式的劣势就是有些许的等待时间,时间具体长短根据要导出的数据量而定。

    ?

    至此,php导出百万excel难题彻底解决,目前使用的是方案1,最多的时候成功导出108w行数据,而且只用10秒,在开启cache的情况下只用了7秒,快如闪电。

    在数据量更大的时候还需要再改进,最好是导出csv格式的,更快,格式更简单,更适合导出海量数据,而且也可以用excel直接打开,逻辑和上面的一样,只不过稍有区别。

    ?

    总结,解决问题的思路和方法尤其重要,一定要培养自己的解决能力,我们的工作其实就是解决问题,技术/编程只不过是解决问题的手段/工具,不是目的,解决问题才是目的,以目标为导向来工作会异常高效,可以为公司带来盈利同时也大幅度的提升了自己的技术能力,培养了解决问题思维,学会如何去思考,如何跟着灵感走,去大胆假设小心求证。

    重中之重:学会如何去思考问题,发散性思维从方面去想解决方案—>解决问题的思路与方法——>求证/试错、实现阶段。

    哲学里有两个概念,经验和超验,当你经历的多了,解决的问题多了,再遇到新的问题,脑子一下子蹦出很多解决方案可以直接感觉出那些想法是可行,那些是不可行的,这就是超验。

    ?

    附上尼采的一句名言:自从厌倦于追寻,我已学会一觅即中;自从一股逆风袭来,我已能抗御八面来风,驾舟而行.

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    cs