【问题标题】:Download large CSV file to browser while it is being generated在生成大型 CSV 文件时将其下载到浏览器
【发布时间】:2014-07-08 16:37:20
【问题描述】:

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

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

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

$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();

【问题讨论】:

    标签: php csv download


    【解决方案1】:

    根据经验,当接收到带有Content-Disposition: attachment 标头的响应时,不同的浏览器会在以下时刻显示文件下载对话框:

    • Firefox 收到标题后立即显示对话框
    • Internet Explorer 在收到标头和响应正文的 255 个字节后会显示该对话框。
    • Chromium 在收到标头以及响应正文的 1023 个字节后显示对话框。

    那么,我们的目标如下:

    1. 尽快将响应正文的第一个 KB 刷新到浏览器,以便 Chrome 用户尽早看到文件下载对话框。
    2. 此后,定期向浏览器发送更多内容。

    阻碍这些目标的实现可能是多层次的缓冲,您可以尝试以不同的方式与之抗争。

    PHP 的 output_buffer

    如果您将output_buffering 设置为Off 以外的值,PHP 将自动创建一个输出缓冲区来存储您的脚本尝试发送到响应body 的所有输出。您可以通过确保将php.ini 文件中的Offapache.confnginx.conf 等网络服务器配置文件中的output_buffering 设置为Off 来防止这种情况发生。或者,您可以在脚本开头使用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 - 但请注意,如果您通过尊重 flush() 的机制启用了 gzip调用,例如 Apache 的 mod_deflate 模块,这种定期刷新将削弱其压缩尝试,并可能导致您的“压缩”输出大于未压缩时的输出。因此,对于一些适度但不小的 n 行,每输出 n 行显式调用 flush() 可能是一种更好的做法。

    然后,将它们放在一起,您可能应该调整您的脚本,使其看起来像这样:

    <?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 代码中尝试解决问题 - 您需要弄清楚是什么导致您的 Web 服务器缓冲您的输出并尝试使用服务器的配置文件解决这个问题。

    【讨论】:

      【解决方案2】:

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

      flush();
      

      【讨论】:

        【解决方案3】:

        试试 Mark Amery 的回答,但只强调陈述:

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

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

        所以:

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

        【讨论】:

          【解决方案4】:

          我认为您正在寻找八位字节流标头。

          $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();
          

          【讨论】:

          • 我认为 application/octet-stream 对强制下载没有用处,它是 Content-Disposition: 标题对这种效果最关键。
          • 据我所知,八位字节流已经将数据流/输出到客户端,这可以防止浏览器“冻结”(如果我错了,请纠正我)。 Nelson Emeka Ameyo 说将 flush() 放入循环中是对的。
          猜你喜欢
          • 2015-10-27
          • 2015-09-05
          • 2014-12-09
          • 2012-07-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多