【问题标题】:Write to PHP output buffer and then download CSV from buffer写入 PHP 输出缓冲区,然后从缓冲区下载 CSV
【发布时间】:2013-04-02 16:49:08
【问题描述】:

我需要将 CSV 文件写入 PHP 输出缓冲区,然后在完成写入后将该文件下载到客户端计算机。 (我想把它写在服务器上然后下载它,它正在工作,但事实证明我在生产服务器上没有写权限)。

我有以下 PHP 脚本:

$basic_info = fopen("php://output", 'w');
$basic_header = array(HEADER_ITEMS_IN_HERE);

@fputcsv($basic_info, $basic_header);

while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {
    @fputcsv($basic_info, $user_row);
}

@fclose($basic_info);

header('Content-Description: File Transfer');
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=test.csv');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize("php://output"));
ob_clean();
flush();
readfile("php://output");

我不知道该怎么办。 CSV 文件下载但不显示任何内容。我认为这与我的 ob_clean() 和 flush() 命令的顺序有关,但我不确定订购这些东西的最佳方法是什么。

感谢任何帮助。

【问题讨论】:

  • 如果您写入输出缓冲区,您会立即将其发送到客户端。在这种情况下,需要先发送任何标头。
  • ob_clean() 是没有意义的,除非你在某处做了 ob_start() 。并考虑 readfile 写入输出,但您正在从该输入读取。除非您在所有这些代码之前完成了 ob_start(),否则您的 fputcsv() 已写入输出并在任何标头代码被执行之前进入浏览器。
  • 只要有理由使用“@”,我就可以在 PHP 中直接命名一种方法。它不在您的代码中,因此您可以在开始时正确编写代码,而不是在每一行上种植@?
  • @WebnetMobile unserialize()?
  • @WebnetMobile 我猜是mail()

标签: php csv


【解决方案1】:

你做的有点过头了。创建脚本的唯一目的是输出 CSV。只需将其直接打印到屏幕上即可。不要担心标题或缓冲区或 php://output 或类似的东西。

一旦您确认将数据正确地打印到屏幕上,只需在开头添加这些标题:

<?php
header("Content-disposition: attachment; filename=test.csv");
header("Content-Type: text/csv");
?>

...确认正确下载文件。然后,您可以根据需要添加其他标头(我上面包含的标头是我自己使用的标头,没有任何额外的麻烦来使其正常工作,其他标头基本上是为了提高效率和缓存控制,其中一些可能已经适当处理由您的服务器提供,并且对于您的特定应用程序可能重要也可能不重要)。

如果您愿意,可以使用带有ob_start()ob_get_clean() 的输出缓冲将输出内容转换为一个字符串,然后您可以使用该字符串来填写Content-Length

【讨论】:

  • 感谢您真正帮助我,不像其他人只会抱怨我做错的事情
【解决方案2】:

正如我在 Edson 的回答中提到的,我希望在最后一行代码出现“标头已发送”警告:

header('Content-Length: '.$streamSize);

因为输出是在发送此标头之前写入的,但他的示例工作正常。

一些调查使我得出以下结论:

在您使用输出缓冲区时(无论是用户缓冲区,还是 默认 PHP 之一),您可以按照自己的方式发送 HTTP 标头和内容 想。您知道任何协议都需要在正文之前发送标头 (因此称为“标题”),但是当您使用输出缓冲层时,PHP 会为你解决这个问题。任何带有输出的 PHP 函数 headers (header(), setcookie(), session_start()) 实际上会使用 内部 sapi_header_op() 函数,仅填充标题 缓冲。然后,当您使用 printf() 编写输出时,它会写入 输出缓冲区(假设一个)。 当输出缓冲区是 发送,PHP 开始首先发送标头,然后是正文。 PHP 为你照顾一切。如果你不喜欢这种行为,你 除了禁用任何输出缓冲层之外别无选择。

大多数配置下PHP缓冲区的默认大小是4096 字节 (4KB),这意味着 PHP 缓冲区最多可以容纳 4KB 的数据。 一旦这 超出限制或 PHP 代码执行完成,缓冲内容 会自动发送到任何使用 PHP 的后端(CGI、 mod_php,FastCGI)。在 PHP-CLI 中输出缓冲总是关闭。

Edson 的代码有效,因为输出缓冲区没有自动刷新,因为它没有超过缓冲区大小(并且在发送最后一个标头之前脚本没有明显终止)。

一旦输出缓冲区中的数据超过缓冲区大小,就会引发警告。或者在他的例子中,当

$get_users_stmt->fetch(PDO::FETCH_ASSOC)

太大了。

为了防止这种情况,您应该使用 ob_start() 和 ob_end_flush() 管理自己的输出缓冲;如下:

// Turn on output buffering
ob_start();

// Define handle to output stream
$basic_info   = fopen("php://output", 'w');

// Define and write header row to csv output 
$basic_header = array('Header1', 'Header2');  
fputcsv($basic_info, $basic_header);  

$count = 0; // Auxiliary variable to write csv header in a different way

// Get data for remaining rows and write this rows to csv output
while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {  
    if ($count == 0) {
        // Write select column's names as CSV header
        fputcsv($basic_info, array_keys($user_row));  
    } else {
        //Write data row
        fputcsv($basic_info, $user_row);
    }
    $count++;
}  

// Get size of output after last output data sent  
$streamSize = ob_get_length();

//Close the filepointer
fclose($basic_info);  

// Send the raw HTTP headers
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=test.csv');  
header('Expires: 0');  
header('Cache-Control: no-cache');
header('Content-Length: '. ob_get_length());

// Flush (send) the output buffer and turn off output buffering
ob_end_flush();

不过,你仍然受到其他限制。

【讨论】:

    【解决方案3】:

    我对你的代码做了一些调整。

    • 根据PHP's doc 的建议,在任何输出之前移动了标头;

    请记住,必须在任何实际输出之前调用 header() 通过普通 HTML 标记、文件中的空白行或 PHP 发送

    • 删除了一些没有太大变化的标题;
    • 评论了使用选择列的名称编写 csv 标题的另一个选项;
    • 现在内容长度有效;
    • 无需回显 $basic_info,因为它已经在输出缓冲区中,我们通过标题将其重定向到文件中;
    • 删除了@(PHP 错误控制运算符),因为它可能会导致开销,目前没有可显示的链接,但您可能会在搜索时找到它。在消除错误之前您应该三思而后行,大多数情况应该修复而不是消除错误。

    header('Content-Type: text/csv');
    header('Content-Disposition: attachment; filename=test.csv');  
    header('Expires: 0');  
    header('Cache-Control: no-cache'); 
    
    $basic_info   = fopen("php://output", 'w');  
    $basic_header = array(HEADER_ITEMS_IN_HERE);  
    
    fputcsv($basic_info, $basic_header);  
    $count = 0; // auxiliary variable to write csv header in a different way
    
    while($user_row = $get_users_stmt->fetch(PDO::FETCH_ASSOC)) {  
        // Write select column's names as CSV header
        if ($count == 0) {
            fputcsv($basic_info, array_keys($user_row));  
        }
    
        fputcsv($basic_info, $user_row);  
        $count++;
    }  
    
    // get size of output after last output data sent  
    $streamSize = ob_get_length(); 
    fclose($basic_info);  
    
    header('Content-Length: '.$streamSize);
    

    【讨论】:

    • 标题不是已经发送到 header('Content-Length: '.$streamSize);因为你之前写过 php://output?
    • @dnFer 是的,但是我只能在写完文件后才能知道文件大小,而且这个工作,浏览器在下载弹出时显示文件大小。
    • 我确实意识到了。但我有点想知道为什么“标头已发送”警告没有弹出。
    • 我在添加 Content-Length 标头时收到“标头已发送”错误消息,如上所述。设法通过在 csv 输出之前使用 ob_start() 和在 Content-Length 标头之后使用 ob_end_flush() 来解决这个问题。根据您的脚本处理下载所需的时间,下载将比不使用输出缓冲区稍晚一些,因为整个文件将首先准备好,然后发送到客户端。好处是用户可以看到下载的大小,浏览器可以更好地估计剩余的下载时间。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-13
    • 2012-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多