PAYATHON 2026

在 PHP 中使用 Curl 读取 XSLX 文件为何返回零字节?

排障赵师傅

PHP cURL 可以直接下载 XLSX、ZIP 等二进制文件,无需额外开启“二进制模式”。CURLOPT_RETURNTRANSFER 返回的字符串可以保存二进制数据,strlen() 也能统计其中的字节数。

先给这段代码补上错误检查。curl_exec() 可能返回 false,也可能返回空字符串,仅凭“0 bytes”无法区分,更不能据此判断 cURL 不支持二进制文件。要查明此次请求失败的原因,还需要错误信息和 HTTP 响应状态。

为什么看起来没有报错

设置 CURLOPT_RETURNTRANSFER 后,curl_exec() 在传输失败时返回 false,通常不会直接抛出异常。原代码既没有检查返回值,也没有读取 curl_error(),因此可能漏掉了传输错误。

检查返回值时,需要区分以下情况:

  • $contents === false:cURL 传输失败,应检查错误码和错误信息。
  • $contents === '':响应体为空,应检查 HTTP 状态码、重定向和服务端行为。
  • $contents 是非空字符串:已经收到数据,但内容仍可能是登录页或错误页面。

默认情况下,HTTP 404、403 等响应不一定会让 curl_exec() 返回 false。因此,传输是否成功、HTTP 状态是否符合预期,需要分别检查。

原代码对文件写入结果的判断也有问题:

if (!file_put_contents($targetFilename, $contents))

file_put_contents() 成功时返回写入的字节数,失败时返回 false。写入空字符串会返回 0,同样会进入这个分支,所以这里的提示不能证明文件系统写入失败。应使用 === false 判断写入是否出错。

先记录错误和 HTTP 响应,再保存文件

保留原来的下载方式,补上错误检查和跟随重定向的设置。只有 HTTP 响应成功且内容非空时,才写入文件。

<?php

$sourceURL = 'https://www.gencon.com/downloads/events.xlsx';
$targetFilename = __DIR__ . '/GenConEvents.xlsx';

$c = curl_init($sourceURL);

if ($c === false) {
    throw new RuntimeException('无法初始化 cURL');
}

curl_setopt_array($c, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_MAXREDIRS => 5,
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_TIMEOUT => 120,
]);

$contents = curl_exec($c);

// 在关闭句柄前读取诊断信息。
$errorNumber = curl_errno($c);
$errorMessage = curl_error($c);
$httpStatus = curl_getinfo($c, CURLINFO_HTTP_CODE);
$effectiveURL = curl_getinfo($c, CURLINFO_EFFECTIVE_URL);
$contentType = curl_getinfo($c, CURLINFO_CONTENT_TYPE);

curl_close($c);

if ($contents === false) {
    throw new RuntimeException(
        "cURL 下载失败({$errorNumber}):{$errorMessage}"
    );
}

if ($httpStatus < 200 || $httpStatus >= 300) {
    throw new RuntimeException(
        "HTTP 状态异常:{$httpStatus};最终 URL:{$effectiveURL}"
    );
}

if ($contents === '') {
    throw new RuntimeException(
        "响应体为空;HTTP 状态:{$httpStatus};最终 URL:{$effectiveURL}"
    );
}

echo "Content-Type: " . ($contentType ?: '未提供') . PHP_EOL;
echo "File returned " . strlen($contents) . " bytes" . PHP_EOL;

$written = file_put_contents($targetFilename, $contents);

if ($written === false) {
    throw new RuntimeException('文件写入失败,请检查目录权限和磁盘空间');
}

if ($written !== strlen($contents)) {
    throw new RuntimeException('文件未完整写入');
}

echo "Saved {$written} bytes to {$targetFilename}" . PHP_EOL;

超时时间和重定向次数可按实际下载情况调整。file_put_contents() 默认会覆盖已有文件,无需在请求前删除旧文件。

根据结果判断需要修改什么

收到重定向响应

下载地址可能先返回 301、302 等响应,再跳转到实际文件地址。cURL 默认不会自动跟随重定向。

如果下载地址采用了这种流程,需要设置:

curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);

重定向响应可以没有响应体,所以可能出现“没有 cURL 错误,但内容为空”的情况。题中没有提供响应头,目前还不能确定此次问题是否由重定向引起。

curl_exec() 返回 false

根据 curl_errno() 和 curl_error() 的结果,排查连接、超时、TLS 证书校验等问题。

如果错误指向证书校验,应检查运行 PHP 的环境是否配置了正确的 CA 证书。不要把关闭证书校验当作正式修复。

浏览器能下载,PHP 请求却失败

浏览器可能携带登录 Cookie,或已经完成下载前的跳转。PHP cURL 请求不会自动继承这些状态。

需要根据响应状态和服务端要求,补充必要的认证信息、Cookie 或请求头。CSV、HTML 可以下载,XLSX、ZIP 却不行,也可能是因为这些 URL 的服务端处理流程不同,单凭文件扩展名无法解释原因。

下载后还要确认文件内容

XLSX 文件的标准扩展名是 .xlsx,采用 ZIP 容器格式,下载时无需为 cURL 设置特殊的二进制选项。

收到非零字节的数据后,仍要确认保存的内容确实是表格文件。HTTP 200 响应也可能返回 HTML 登录页。Content-Type 可以辅助判断,但不能单独证明文件有效。必要时,可用 ZipArchive 检查压缩包结构,或用支持 XLSX 的解析库验证。

文件较大时,可以改用 CURLOPT_FILE,将响应直接写入通过 fopen($path, 'wb') 打开的文件流,减少内存占用。调整存储方式后,仍需检查传输错误和 HTTP 状态。

备注:内容仅供参考。