导出大型CSV文件

时间:2013-09-28 19:10:26

标签: php csv

我有一个mysql表,其中每个记录可以有无限的自定义字段(EAV模型,无关紧要),每个字段可以有无限的选项,每个选项可以有无限的值。
现在我正在尝试构建一个导出工具,它将使用它们的值导出所有这些自定义字段,即:name =>每个字段的值对。这不是重要的部分,它只是强调我们正在讨论单个记录的大量mysql查询,并且导出的大小将非常大。

对于主表中的每一行,我必须执行大约100个单独的sql查询来获取字段,字段选项和字段选项值。这些查询非常快,因为它们都使用正确的索引,但我们仍然在讨论单个记录的100个查询,并且我希望在主表中有大约5万条记录。

现在,我所做的是:

set_time_limit(0);
ini_set('memory_limit', '1G');
ini_set("auto_detect_line_endings", true);

$count = $export->count();
$date = date('Y-m-d-H-i-s');
$fileName = CHtml::encode($export->name) .'-'. $date . '.csv';

$processAtOnce = 100;
$rounds = round($count / $processAtOnce);

header("Content-disposition: attachment; filename={$fileName}");
header("Content-Type: text/csv");

$headerSet = false;
for ($i = 0; $i < $rounds; ++$i) {

    $limit = $processAtOnce;
    $offset = $i * $processAtOnce;
    $rows = $export->find($limit, $offset);

    if (empty($rows)) {
        continue;
    }

    $outStream = fopen('php://output', 'w');

    if (!$headerSet) {
        fputcsv($outStream, array_keys($rows[0]), ',', '"');    
        $headerSet = true;
    }

    foreach ($rows as $row) {
        fputcsv($outStream, array_values($row), ',', '"');
    }

    echo fgets($outStream);

    fclose($outStream);
}

基本上我统计所有记录并将其“分页”以便导出,然后遍历页面以避免一次加载太多的sql结果。
我想知道这是否是一种有效的方法?有什么想法吗?

我的替代方案是计算所有记录,将它们分成“页面”,并为每个页面执行ajax请求(在前一个请求成功完成后调用的递归函数)。在执行ajax请求时,可以同时处理1k条记录(这些1k也会像上例中那样被拆分,内部运行10次,例如100条结果),将它们写入临时目录(如part-1.csv, part-2.csv)并在处理完所有记录的最后,从包含所有csv部分的文件夹创建一个存档,并强制浏览器下载它然后从服务器中删除它(window.location.href最后ajax电话)。
这是上面的一个很好的替代品吗?

请注意,我的目标是限制内存使用量,这就是为什么我认为第二种方法可以帮助我更多。

请让我知道你的想法。
感谢。

2 个答案:

答案 0 :(得分:4)

我的最后一个方法是第二个,在经过大量测试后我得出结论,在我的情况下,第二种方法在内存使用方面更好,即使完成整个导出的时间更长,也没有因为GUI将使用有关导出的实时统计信息进行更新,因此在等待导出完成时总体上是一种良好的用户体验。

这些是我采取的步骤:
1)加载页面并向服务器发出第一个ajax请求 2)服务器将一次批量读取100条记录中的前1000条记录,以避免从mysql中同时返回多条结果。
3)结果作为part-x.csv写入文件,其中x是ajax发送的请求号。
4)当没有更多记录要添加到文件中时,最后一个ajax调用将创建存档,并删除包含part-x.csv文件的文件夹。然后服务器将返回一个名为“download”的json param,它将包含通过PHP下载文件的url(fopen + fread + flush + fclose,然后取消链接存档文件)
5)使用“下载”参数,浏览器将执行window.location.href = json.download并强制下载文件。

我知道,这样的工作更多,但正如我所说的,最终结果似乎比我第一次加载所有方式更好。

答案 1 :(得分:1)

以下是导出大型CSV文件的更优化方法(感谢@Joe以上代码) -

  1. 将Ajax请求循环到服务器。下面是AJAX 通话过程。
  2. 服务器将批量读取第一条记录 记录(chunkSize),一次避免从MySQL中获得太多结果。
  3. 文件exported_file.csv将在中打开 在后续请求中的第一个请求和附加模式中写入模式。
  4. 结果将写入此文件。什么时候没有 要添加到文件的记录越多,js函数就会发送文件进行下载。
  5. 下面是示例JS函数 -

    <script>
    var exportedRecords = 0;
    var chunkSize = 500; // as per query performance
    
    for( start=0; start <= totalRecords; start += chunkSize){
        chunkCSVExport(,0, chunkSize);
    }
    
    function chunkCSVExport(start,chunkSize){
            requestData['start']  = start;
            requestData['limit']  = chunkSize;
            jQuery.ajax({
                type : "post",
                dataType : "json",
                url :  action,
                data : formData,
                success: function(response) {
                    console.log(response);
                    exportedRecords += chunkSize;
                    downloadfile();
                }
            });
        }
    
    function downloadfile(){   
             if(exportedRecords>=totalRecords){
                         // call download file function here
                    }
    }
    
    </script>
    

    下面是示例PHP代码 -

    <?php
    $start = $_POST['start']; //added the missing closing single quote 
    $limit = $_POST['limit'];
    
    
    if($start==0) {
        $handle = fopen( 'file-export.csv', 'w' );
    }else{
        $handle = fopen( 'file-export.csv', 'a' );
    }
    
    
    // Run The query from start to limit
    $results = getresults($query)
    
    if($start==0) {
        $headerDisplayed = false;
    }else{
        $headerDisplayed = true;
    }
    
    foreach ( $results as $data ) {
        // Add a header row if it hasn't been added yet
        if ( !$headerDisplayed ) {
            // Use the keys from $data as the titles
            fputcsv($handle, $arrHeaders);
            $headerDisplayed = true;
        }
        // Put the data into the stream
        fputcsv($handle, $data);
    }
    
    // Close the file
    fclose($handle);
    
    // Output some stuff for jquery to use
    $response = array(
        'result'        => 'success'
    );
    echo json_encode($response);
    exit;
    ?>