【问题标题】:Export large CSV file导出大型 CSV 文件
【发布时间】:2013-09-28 19:10:26
【问题描述】:

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

对于我的主表中的每一行,我必须执行大约 100 个单独的 sql 查询来获取字段、字段选项和字段选项值。这些查询非常快,因为它们都使用了正确的索引,但我们仍然在谈论针对单个记录的 100 个查询,我希望我的主表中只有大约 50k 条记录才能开始。

现在,我要做的是:

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 调用)。
这是上述的一个很好的选择吗?

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

请告诉我你的想法。
谢谢。

【问题讨论】:

  • 如果你的数据是结构化的,为什么不选择 JSON 格式而不是 CSV?只是一个“最初的想法”......
  • 它将在 OpenOffice/Office 中使用并导入其他仅支持 csv 的系统中,并且 json 不是一个选项,它必须是 csv :)

标签: php csv


【解决方案1】:

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

  1. 循环向服务器发出 Ajax 请求。下面将是 AJAX 调用过程。
  2. 服务器将分批读取第一条记录 一次记录( chunkSize )以避免从 MySQL 一次返回太多结果。
  3. exported_file.csv 文件将在 第一个请求中的写入模式和后续请求中的附加模式。
  4. 将结果写入此文件。当没有 更多记录添加到文件中,js函数将文件发送到下载。

下面是示例 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;
?>

【讨论】:

    【解决方案2】:

    我的最后一种方法是第二种方法,经过大量测试后,我得出结论,就我而言,第二种方法在内存使用方面要好得多,即使完成整个导出的时间更长,但这并没有很重要,因为 GUI 将使用有关导出的实时统计信息进行更新,并且在等待导出完成时总体上是良好的用户体验。

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

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

    【讨论】:

      【解决方案3】:

      感谢 Twisted1919 的帖子给了我一些启发。我知道这篇文章有点老了,但我想我会发布一些我的解决方案的代码,以防它帮助其他人。

      它使用一些 Wordpress 函数进行数据库查询。

      我将您的步骤 3 和 4 替换为。

      <?php
      // if its a fist run truncate the file. else append the file
      if($start==0) {
          $handle = fopen( 'temp/prod-export'. '.csv', 'w' );
      }else{
          $handle = fopen( 'temp/prod-export'. '.csv', 'a' );
      }
      ?>
      

      一些基本的jQuery

      <script>
          // do stuff on the form submit
          $('#export-form').submit(function(e){
              e.preventDefault();
              var formData = jQuery('#export-form').serializeObject();
              var chunkAndLimit = 1000;
              doChunkedExport(0,chunkAndLimit,formData,$(this).attr('action'),chunkAndLimit);
          });
          // function to trigger the ajax bit
          function doChunkedExport(start,limit,formData,action,chunkSize){
              formData['start']  = start;
              formData['limit']  = limit;
              jQuery.ajax({
                  type : "post",
                  dataType : "json",
                  url :  action,
                  data : formData,
                  success: function(response) {
                      console.log(response);
                      if(response.result=='next'){
                          start = start + chunkSize;
                          doChunkedExport(start,limit,formData,action,chunkSize);
                      }else{
                          console.log('DOWNLOAD');
                      }
                  }
              });
          }
          // A function to turn all form data into a jquery object
          jQuery.fn.serializeObject = function(){
              var o = {};
              var a = this.serializeArray();
              jQuery.each(a, function() {
                  if (o[this.name] !== undefined) {
                      if (!o[this.name].push) {
                          o[this.name] = [o[this.name]];
                      }
                      o[this.name].push(this.value || '');
                  } else {
                      o[this.name] = this.value || '';
                  }
              });
              return o;
          };
      </script>
      

      php 位

      <?php
      global $wpdb;
      
      $postCols = array(
          'post_title',
          'post_content',
          'post_excerpt',
          'post_name',
      );
      
      header("Content-type: text/csv");
      
      $start = intval($_POST['start']);
      $limit = intval($_POST['limit']);
      
      // check the total results to workout the finish point
      $query = "SELECT count(ID) as total FROM `wp_posts` WHERE post_status = 'publish';";
      $results = $wpdb->get_row( $query, ARRAY_A );
      $totalResults = $results['total'];
      $result = 'next';
      if( ($start + $limit ) >= $totalResults){
          $result = 'finished';
      }
      
      // if its a fist run truncate the file. else append the file
      if($start==0) {
          $handle = fopen( 'temp/prod-export'. '.csv', 'w' );
      }else{
          $handle = fopen( 'temp/prod-export'. '.csv', 'a' );
      }
      
      $cols = implode(',',$postCols);
      //The query
      $query = "SELECT {$cols} FROM `wp_posts` WHERE post_status = 'publish' LIMIT {$start},{$limit};";
      $results = $wpdb->get_results( $query, ARRAY_A );
      
      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, array_keys($data));
              $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'        => $result,
          'start'         => $start,
          'limit'         => $limit,
          'totalResults'  => $totalResults
      );
      echo json_encode($response);
      
      
      // Make sure nothing else is sent, our file is done
      exit;
      ?>
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-12-27
        • 2017-01-05
        • 2014-12-17
        • 2012-09-19
        • 1970-01-01
        • 1970-01-01
        • 2014-12-10
        • 2012-07-10
        相关资源
        最近更新 更多