在生成时将大型CSV文件下载到浏览器

时间:2014-05-20 12:08:40

标签: php csv download

我有一个脚本,使用fputcsv生成一个大型CSV文件并将其发送到浏览器。它可以工作,但浏览器不显示文件下载提示(或开始下载文件),直到整个CSV文件在服务器端生成,这需要很长时间。

相反,我希望在仍然生成文件的其余部分时开始下载。我知道这是可能的,因为它是PHPMyAdmin中的'导出数据库'选项的工作原理 - 即使您的数据库很大,单击“导出”按钮也会立即开始下载。

如何调整下面的现有代码,让下载立即开始?

$csv = 'title.csv';
header( "Content-Type: text/csv;charset=utf-8" );
header( "Content-Disposition: attachment;filename=\"$csv\"" );
header( "Pragma: no-cache" );
header( "Expires: 0" );

$fp = fopen('php://output', 'w');
fputcsv($fp, array_keys($array), ';', '"');

foreach ($array as $fields) 
{
    fputcsv($fp, $fields, ';', '"');
}

fclose($fp);
exit();

4 个答案:

答案 0 :(得分:9)

根据经验,似乎在收到带有Content-Disposition: attachment标题的回复时,不同的浏览器会在以下时刻显示文件下载对话框:

  • Firefox会在收到标题后立即显示该对话框
  • 一旦收到标题加上响应正文的255个字节,Internet Explorer就会显示该对话框。
  • Chromium在收到标题加上响应正文的1023个字节后显示对话框。

我们的目标如下:

  1. 尽快将响应正文的第一个千字节刷新到浏览器,以便Chrome用户尽早看到文件下载对话框。
  2. 此后,定期向浏览器发送更多内容。
  3. 阻碍这些目标的可能是多级缓冲,你可以尝试以不同的方式进行战斗。

    PHP的output_buffer

    如果您将output_buffering设置为Off以外的值,PHP将自动创建一个输出缓冲区,用于存储脚本尝试发送到响应正文的所有输出。您可以通过确保output_buffering文件中Off设置为php.ini,或者apache.confnginx.conf等网络服务器配置文件来阻止此操作。或者,您可以使用ob_end_flush()ob_end_clean()在脚本开头关闭输出缓冲区(如果存在):

    if (ob_get_level()) {
        ob_end_clean();
    }
    

    由您的网络服务器完成缓冲

    一旦输出超过PHP输出缓冲区,它可能会被您的网络服务器缓冲。您可以通过定期调用flush()来尝试解决此问题(例如,每100行),尽管PHP手册对提供任何保证犹豫不决,列出了可能失败的特定情况:

      

    冲洗

         

    ...

         

    刷新PHP的写缓冲区以及PHP正在使用的任何后端(CGI,Web服务器等)。这会尝试将当前输出一直推送到浏览器,但需要注意几点。

         

    flush()可能无法覆盖Web服务器的缓冲方案......

         

    多个服务器,尤其是Win32上的服务器,仍会缓冲脚本的输出,直到它终止,然后才将结果传输到浏览器。

         

    像mod_gzip这样的Apache服务器模块可能会自行缓冲,导致 flush()不会导致数据立即发送到客户端。

    您可以在每次尝试回显任何输出时自动调用PHP flush(),方法是在脚本开头调用ob_implicit_flush - 但要注意,如果您通过一种机制启用了gzip尊重flush()调用,例如Apache的mod_deflate模块,这种定期刷新会削弱其压缩尝试,并可能导致您的压缩'输出大于未压缩的输出。明确地调用flush()每个 n 输出行,对于一些适度但非微小的 n ,因此可能是更好的做法。

    总而言之,你应该调整你的脚本看起来像这样:

    <?php
    
        if (ob_get_level()) {
            ob_end_clean();
        }
    
        $csv = 'title.csv';
        header( "Content-Type: text/csv;charset=utf-8" );
        header( "Content-Disposition: attachment;filename=\"$csv\"" );
        header( "Pragma: no-cache" );
        header( "Expires: 0" );
    
        flush(); // Get the headers out immediately to show the download dialog
                 // in Firefox
    
        $array = get_your_csv_data(); // This needs to be fast, of course
    
        $fp = fopen('php://output', 'w');
        fputcsv($fp, array_keys($array), ';', '"');
    
        foreach ($array as $i => $fields) 
        {
            fputcsv($fp, $fields, ';', '"');
            if ($i % 100 == 0) {
                flush(); // Attempt to flush output to the browser every 100 lines.
                         // You may want to tweak this number based upon the size of
                         // your CSV rows.
            }
        }
    
        fclose($fp);
    
    ?>
    

    如果这不起作用,那么我不认为您可以从PHP代码中做更多的事情来尝试解决问题 - 您需要弄清楚什么是&#39;导致您的Web服务器缓冲输出并尝试使用服务器的配置文件解决该问题。

答案 1 :(得分:0)

尚未对此进行测试。尝试在n个数据行之后刷新脚本。

flush();

答案 2 :(得分:0)

尝试 Mark Amery 的答案,但只强调以下内容:

$array = get_your_csv_data(); // This needs to be fast, of course

如果要获取大量记录,请按块(例如,每1000条记录)进行获取。

所以:

  1. 获取1000条记录
  2. 输出它们
  3. 重复

答案 3 :(得分:-2)

我认为你正在寻找octet-stream标题。

$csv = 'title.csv';

header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment;filename=\"$csv\"" );
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Expires: 0');

$fp = fopen('php://output', 'w');
fputcsv($fp, array_keys($array), ';', '"');

foreach ($array as $fields) 
{
    fputcsv($fp, $fields, ';', '"');
}

fclose($fp);
exit();