PHP批量导出大数据的解决方案

3,189 阅读3分钟

首先在php的库中有一些是操作导出数据的,也是很常用的。

如:

  1. phpexcel

  2. phpspreadsheet


phpexcel

现在该库已经不在更新了,它的版本支持php5.3至php5.6,现在随着php版本的升级,性能也是随之往上升。


phpspreadsheet


是phpexcel的替代品,支持php7的版本,在性能上肯定是大于phpexcel。如果使用库来导出我还是推荐使用这个库


回到正文,今天我们使用的是csv进行导出。


csv是什么,那他有什么优点?


逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。通常,所有记录都有完全相同的字段序列。通常都是纯文本文件。


它的优点如下:

  1. csv文件较小

  2. csv文件数据可存放数量大或小

  3. csv生成不需要占用大量cpu资源以及时间,而excel的生成时间以及对服务器的消耗文件数据量的增加成指数上涨,如果有用过excel导出的同学会发现php服务器爆内存或则是导出的时间过长。


数据随机生成(实际情况应该是分批调用数据库获取数据)


$arr = [];
for ($i = 0; $i < 2080000; $i++) {     
     $arr[] = [          
        '测试1',        
        '测试2',          
        '测试3'      
    ];  
}


编写php核心导出代码


在编写的时候我们中会使用到php的协程和操作csv的函数,那问题又来了。


什么是协程?


Coroutine是协程序的名称,协程是一种比线程更加小的执行单元。我们可以换个角度理解,是线程的轻量级版本。那么协程所占有的栈是比线程要更加少。众所周知,线程的调用是系统级别的,而协程的调用是用户级别的。因此在这个层面上性能得到了很大的提升,并且不需要线程切换那样需要消耗资源。


在php中我们怎么使用协程,php中使用协程是借助了yield来完成的,重点,yield并不是指的协程,而是借助它来实现协程的特性。


php代码:

set_time_limit(0);// 设置不超时
$output = fopen('xxx.csv', 'w');
// 写入头部栏目fputcsv($output, ["1","2","3"]);
// 使用yield去调度数据源
$data = foreach ($dataList as $k => $v) {    
    yield $v;
}
// 使用最终的返回的调度器进行操作写文件
foreach ($data as $value) { 
   fputcsv($output, $value);
}
// 关闭文件fclose($output);


但是在使用这种代码的时候,我们需要考虑一下excel打开csv文件的数量限制。

excel2003,只能显示65536行。

excel2007及以上版本可以显示1048576行。

细心的同学又会发现如果导出超过上百万甚至千万的数据那怎么观看。其实换个角度我们可以使用分包的形式批量分成104W的数据集合进行批量打包成zip下发给到用户。


然后同学可能会发现为什么我用excel打开csv既然都是乱码。其实excel是需要对bom头处理,而我们导出是没有bom头的。在导出的时候加入以下代码即可.


$bom = chr(0xEF) . chr(0xBB) . chr(0xBF);
// 在写入标题前面可以写入bom头先
fputcsv($output, [$bom]);


如果中文的字符出现乱码,可以尝试以下代码


mb_convert_encoding($str, 'gbk', 'utf8')


如果你不想数字变成科学计数法,可以尝试以下方案


使用=字符可以让单元格的内容变为字符串,如'="10000000000000000"'


最终的代码:

/**    
 * 协程导出大量数据    
 * @param array $dataList 数据源     
 * @param string $path 对应文件路径    
 * @param string $filename 文件名字 如 [部门,名字]     
 * @param callable $callback 自定义回调函数 返回一位数组     
 * @param array $config     
 * @return bool     
 * @throws \Exception    
 */    
public static function createMoreDataToCsvFile( array $dataList,string $path, string $filename,allable $callback,array $config)
{       
    set_time_limit(0);// 设置不超时        
    ini_set('memory_limit', '1024M');​        
    $zipFilename = $path . $filename;​        
    $fileInfoArr = explode('.', $filename);​        
    $zip = new \ZipArchive();​        
    $opened = $zip->open($zipFilename, ZIPARCHIVE::CREATE);​        
    if ($opened !== true) {           
         throw new \Exception('[Zip Error Code]: ' . $opened);        
    }​        
    $newFilename = $fileInfoArr[0] . '_' . $config['num'] . '.csv';        
    $newPathFilename = $path . $newFilename;​        
    // 缓冲区写临时文件        
    $output = fopen($newPathFilename, 'w');​        
    // 写头        
    if (!empty($config['headerRow'])) {​           
         // 给excel添加bom头            
        $bom = chr(0xEF) . chr(0xBB) . chr(0xBF);​            
        foreach ($config['headerRow'] as $key => $value) {                
            $config['headerRow'][$key] = (string)$value;            
        }​            
        fputcsv($output, [$bom]);​           
        fputcsv($output, $config['headerRow']);       
     }​        
    // 执行协程        
    $data = (            
        function () use ($dataList, $callback) {                
            foreach ($dataList as $k => $v) {                   
                 yield $callback($v);               
             }            
        })($dataList, $callback);​        

    foreach ($data as $value) {           
         fputcsv($output, $value);        
    }​       

     $newFileSize = filesize($newPathFilename);​        
     $status = $zip->addFile(           
         $newPathFilename,            
        $newFilename,            
        0,            
        $newFileSize        
    );​        

    if ($status === false) {           
         $zip->close();            
        @unlink($newPathFilename);            
        throw new \RuntimeException(                
            sprintf('添加文件进压缩包错误:%s:%s', $filename, $newPathFilename)           
         );        
    }​        

    $zip->close();        
    @unlink($newPathFilename);        
    fclose($output);​        
    return true;    
}

最终演示操作代码:


$stime = microtime(true);
$arr = [];
$num = 0;
for ($i = 0; $i < 2080000; $i++) {  
    $arr[] = [     
         '测试1',     
         '测试2',     
         '测试3'  
    ];​  
    if (count($arr) === 1040000) {​   
         $num++;​    
         ExportHelper::createMoreDataToCsvFile(       
             $arr,        
            './csv/',
            '测试.zip',        
            function ($arr) {            
                return $arr;        
            },        
            [            
                'headerRow' => [               
                     '测试列数1',                
                     '测试列数2',                
                     '测试列数3',            
                ],           
                'num' => $num,        
            ]    
    );   
     $arr = [];    
    } 
}​ $
etime = microtime(true);
//获取程序执行结束的时间​ echo $etime - $stime; 
// 输出时间 
// 208W大概是32s左右

如果您对这些感兴趣,欢迎评论,收藏、转发给予支持!> 欢迎关注我的公众号:猿力说,每日分享学习之路的点点滴滴,以及程序人生.