ChatGPT解决这个技术问题 Extra ChatGPT

PHP cURL 可以在单个请求中检索响应标头和正文吗?

有没有办法使用 PHP 获取 cURL 请求的标头和正文?我发现这个选项:

curl_setopt($ch, CURLOPT_HEADER, true);

将返回正文和标题,但随后我需要解析它以获取正文。有没有办法以更可用(和安全)的方式获得两者?

请注意,对于“单个请求”,我的意思是避免在 GET/POST 之前发出 HEAD 请求。

有一个内置的解决方案,请参阅此答案:stackoverflow.com/a/25118032/1334485(添加此评论“因为这篇文章仍然获得很多浏览量)
看看这个不错的评论:secure.php.net/manual/en/book.curl.php#117138
有人告诉我我的问题与这个问题重复。如果它不是重复的,有人可以重新打开它吗? stackoverflow.com/questions/43770246/… 在我的问题中,我有一个具体的要求,即使用一种方法,该方法返回一个标题和正文分开的对象,而不是一个字符串。

l
laurent

PHP 文档评论中发布了一个解决方案:http://www.php.net/manual/en/function.curl-exec.php#80442

代码示例:

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// ...

$response = curl_exec($ch);

// Then, after your curl_exec call:
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$body = substr($response, $header_size);

警告:如以下评论中所述,与代理服务器一起使用或处理某些类型的重定向时,这可能不可靠。 @Geoffrey 的回答可能会更可靠地处理这些问题。


您也可以list($header, $body) = explode("\r\n\r\n", $response, 2),但这可能需要更长的时间,具体取决于您的请求大小。
这是一个糟糕的解决方案,因为如果您使用代理服务器并且您的代理服务器(例如 fiddler)添加自己的标头来响应 - 此标头破坏了所有偏移量,您应该使用 list($header, $body) = explode("\r\n\r\n", $response, 2) 作为唯一的工作变体
@msangel 当响应中有多个标头时,您的解决方案不起作用,例如当服务器执行 302 重定向时。有什么建议么?
@Nate,是的,我知道这一点。 AFAIK,但只有一个可能的附加标头 - 带有代码 100(继续)。对于此标头,您可以使用正确定义请求选项:curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); ,禁用发送此标头响应。至于 302,这不应该发生,因为 302 标头是重定向的,它不期望正文,但是我知道,有时服务器会发送一些带有 302 响应的正文,但浏览器无论如何都会忽略它,到目前为止,为什么 curl 应该处理这个?)
CURLOPT_VERBOSE 旨在将进程信息输出到 STDERR(可能会在 CLI 中出现问题),并且对于讨论的问题是无用的。
h
hakre

该线程提供的许多其他解决方案都没有正确执行此操作。

当 CURLOPT_FOLLOWLOCATION 打开或服务器以 100 代码 RFC-7231、MDN 响应时,在 \r\n\r\n 上的拆分不可靠。

并非所有服务器都符合标准并且只为新行传输一个 \n(并且接收者可能会丢弃行终止符中的 \r)问答。

通过 CURLINFO_HEADER_SIZE 检测标头的大小也并不总是可靠的,尤其是在使用代理 Curl-1204 或在某些相同的重定向场景中时。

最正确的方法是使用 CURLOPT_HEADERFUNCTION

这是使用 PHP 闭包执行此操作的一种非常简洁的方法。它还将所有标头转换为小写,以便跨服务器和 HTTP 版本进行一致处理。

此版本将保留重复的标题

这符合RFC822和RFC2616,请不要使用mb_(和类似的)字符串函数,这不仅是错误的,甚至是一个安全问题 RFC-7230!< /强>

$ch = curl_init();
$headers = [];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

// this function is called by curl for each header received
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
  function($curl, $header) use (&$headers)
  {
    $len = strlen($header);
    $header = explode(':', $header, 2);
    if (count($header) < 2) // ignore invalid headers
      return $len;

    $headers[strtolower(trim($header[0]))][] = trim($header[1]);
    
    return $len;
  }
);

$data = curl_exec($ch);
print_r($headers);

IMO,这是该线程中的最佳答案,并修复了其他答案发生的重定向问题。最好阅读 CURLOPT_HEADERFUNCTION 的文档以了解其工作原理和潜在问题。我还对答案进行了一些改进以帮助其他人。
太好了,我已经更新了答案以迎合重复的标题。将来不要将代码重新格式化为您认为应该的格式。这是为了明确闭包函数边界在哪里而编写的。
@thealexbaron 是的,它从 PHP 5.4 开始,请参阅:php.net/manual/en/migration54.new-features.php
对于这种简洁且符合 RFC 的方法,这个答案被高度低估了。这应该是粘性答案并移到顶部。我只是希望有一种更快的方法来获取所需标头的值,而不是先解析所有标头。
@Mahesh.D 不,当根据提供的示例设置 CURLOPT_RETURNTRANSFER 时,$data = curl_exec($ch); 返回内容。
h
hakre

Curl 有一个内置选项,称为 CURLOPT_HEADERFUNCTION。此选项的值必须是回调函数的名称。 Curl 将逐行传递标题(并且仅标题!)到此回调函数(因此将从标题部分的顶部开始为每个标题行调用该函数)。然后你的回调函数可以用它做任何事情(并且必须返回给定行的字节数)。这是一个经过测试的工作代码:

function HandleHeaderLine( $curl, $header_line ) {
    echo "<br>YEAH: ".$header_line; // or do whatever
    return strlen($header_line);
}


$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://www.google.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, "HandleHeaderLine");
$body = curl_exec($ch); 

以上适用于所有内容,也适用于不同的协议和代理,您无需担心标头大小,或设置许多不同的 curl 选项。

PS:要使用对象方法处理标题行,请执行以下操作:

curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($object, 'methodName'))

需要注意的是,每个标头都调用了回调函数,并且似乎没有修剪它们。您可以使用全局变量来保存所有标头,也可以使用匿名函数进行回调并使用局部变量(对于父作用域而言是局部变量,而不是匿名函数)。
@MV谢谢,是的,“逐行”我的意思是“每个标题”。为了清楚起见,我编辑了我的答案。要获取整个标题部分(也称为所有标题),您还可以使用对象方法进行回调,以便对象属性可以保存所有标题。
这是国际海事组织的最佳答案。使用 CURLOPT_FOLLOWLOCATION 时不会导致多个“\r\n\r\n”出现问题,我猜它不会受到来自代理的其他标头的影响。
对我来说效果很好,如果出现问题,另请参阅 stackoverflow.com/questions/6482068/…
是的,这是最好的方法,但是@Geoffrey 的回答通过使用不需要全局变量等的匿名函数使这个更干净。
p
papsy

这是你想要的吗?

curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
$response = curl_exec($ch); 
list($header, $body) = explode("\r\n\r\n", $response, 2);

这正常工作,除非有一个 HTTP/1.1 100 Continue 然后是一个中断然后 HTTP/1.1 200 OK。我会用另一种方法。
在实施类似的操作之前,请先查看 stackoverflow.com/questions/14459704/… 的选定答案。 w3.org/Protocols/rfc2616/rfc2616-sec14.html (14.20) A server that does not understand or is unable to comply with any of the expectation values in the Expect field of a request MUST respond with appropriate error status. The server MUST respond with a 417 (Expectation Failed) status if any of the expectations cannot be met or, if there are other problems with the request, some other 4xx status.
当 curl 设置为跟随位置标头时,此方法在 302 重定向上也会失败。
这也不适用于某些 POST 请求。
p
pr1001

如果您特别想要 Content-Type,可以使用特殊的 cURL 选项来检索它:

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
$content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);

OP 询问是否有办法检索标头,而不是一个特定的标头,这不能回答 OP 的问题。
@Geoffrey 不,它对于只需要获取 Content-Type 的其他人很有用
@Acuna 不管它有多有用,它都不能回答 OPs 的问题!
@Geoffrey 是的,但是所有答案对其他用户也有用,不要忘记它,OP 也找到了答案,所以每个人都很满意
C
Cyril H.

只需设置选项:

CURLOPT_HEADER, 0

CURLOPT_RETURNTRANSFER, 1

并将 curl_getinfo 与 CURLINFO_HTTP_CODE 一起使用(或没有 opt 参数,您将拥有一个包含所有您想要的信息的关联数组)

更多信息:http://php.net/manual/fr/function.curl-getinfo.php


这似乎根本不会将响应标头返回给您。或者至少没有办法使用 curl_getinfo() 检索它们。
E
Enyby
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);

$parts = explode("\r\n\r\nHTTP/", $response);
$parts = (count($parts) > 1 ? 'HTTP/' : '').array_pop($parts);
list($headers, $body) = explode("\r\n\r\n", $parts, 2);

在其他标题之前与 HTTP/1.1 100 Continue 一起使用。

如果您需要使用仅发送 LF 而不是 CRLF 作为换行符的错误服务器,您可以使用 preg_split,如下所示:

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);

$parts = preg_split("@\r?\n\r?\nHTTP/@u", $response);
$parts = (count($parts) > 1 ? 'HTTP/' : '').array_pop($parts);
list($headers, $body) = preg_split("@\r?\n\r?\n@u", $parts, 2);

$parts = explode("\r\n\r\nHTTP/", $response); 不应该将第三个参数作为 2 展开吗?
@user4271704 不。它允许查找最后一条 HTTP 消息。 HTTP/1.1 100 Continue 可以出现多次。
但他说了些别的:stackoverflow.com/questions/9183178/…你们谁是正确的?
HTTP/1.1 100 Continue 可以出现多次。如果它只出现一次,他会查看案例,但在普通案例中它是错误的。例如对于 HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n...,他的代码不能正常工作
在\r\n 上拆分是不可靠的,有些服务器不符合HTTP 规范,只会发送一个\n。 RFC 标准规定应用程序应忽略 \r 并在 \n 上拆分以获得最大的可靠性。
t
tony gil

我的方法是

$response = curl_exec($ch);
$x = explode("\r\n\r\n", $v, 3);
$header=http_parse_headers($x[0]);
if ($header=['Response Code']==100){ //use the other "header"
    $header=http_parse_headers($x[1]);
    $body=$x[2];
}else{
    $body=$x[1];
}

如果需要,应用 for 循环并删除爆炸限制。


A
Antony

这是我对辩论的贡献……这将返回一个单独的数组,其中包含分隔的数据并列出了标题。这是基于 CURL 将返回一个标题块 [空白行] 数据

curl_setopt($ch, CURLOPT_HEADER, 1); // we need this to get headers back
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, true);

// $output contains the output string
$output = curl_exec($ch);

$lines = explode("\n",$output);

$out = array();
$headers = true;

foreach ($lines as $l){
    $l = trim($l);

    if ($headers && !empty($l)){
        if (strpos($l,'HTTP') !== false){
            $p = explode(' ',$l);
            $out['Headers']['Status'] = trim($p[1]);
        } else {
            $p = explode(':',$l);
            $out['Headers'][$p[0]] = trim($p[1]);
        }
    } elseif (!empty($l)) {
        $out['Data'] = $l;
    }

    if (empty($l)){
        $headers = false;
    }
}

C
Community

这里有许多答案的问题是 "\r\n\r\n" 可以合法地出现在 html 的正文中,因此您不能确定您是否正确拆分了标题。

似乎通过一次调用 curl_exec 来单独存储标头的唯一方法是使用上面在 https://stackoverflow.com/a/25118032/3326494 中建议的回调

然后(可靠地)获取请求的正文,您需要将 Content-Length 标头的值作为负起始值传递给 substr()


它可以合法地出现,但您的答案不正确。 Content-Length 不必出现在 HTTP 响应中。手动解析标头的正确方法是查找 \r\n(或 \n\n)的第一个实例。这可以简单地通过限制explode 只返回两个元素来完成,即:list($head, $body) = explode("\r\n\r\n", $response, 2);,但是如果你使用curl_setopt($ch, CURLOPT_HEADERFUNCTION, $myFunction);,CURL 已经为你做了这个
j
james-geldart

更好的方法是使用可以通过管道传输到临时流的详细 CURL 响应。然后,您可以在响应中搜索标头名称。这可能需要一些调整,但它对我有用:

class genericCURL {
    /**
     * NB this is designed for getting data, or for posting JSON data
     */
    public function request($url, $method = 'GET', $data = array()) {
        $ch = curl_init();
        
        if($method == 'POST') {
            
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
            curl_setopt($ch, CURLOPT_POSTFIELDS, $string = json_encode($data));
            
        }

        
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_VERBOSE, true);
        
        //open a temporary stream to output the curl log, which would normally got to STDERR
        $err = fopen("php://temp", "w+");
        curl_setopt($ch, CURLOPT_STDERR, $err);
        

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $server_output = curl_exec ($ch);
        
        //rewind the temp stream and put it into a string   
        rewind($err);
        $this->curl_log = stream_get_contents($err);
        
        curl_close($ch);
        fclose($err);

    
        return $server_output;
        
    }
    
    /**
     * use the curl log to get a header value
     */
    public function getReturnHeaderValue($header) {
        $log = explode("\n", str_replace("\r\n", "\n", $this->curl_log));
        foreach($log as $line) {
            //is the requested header there
            if(stripos($line, '< ' . $header . ':') !== false) {
                $value = trim(substr($line, strlen($header) + 3));
                return $value;
            }
        }
        //still here implies not found so return false
        return false;
        
    }
}

s
sneaky

杰弗里斯回答的改进:

我无法使用 $headerSize = curl_getinfo($this->curlHandler, CURLINFO_HEADER_SIZE); 获得正确的标题长度 - 我必须自己计算标题大小。

此外还有一些改进以提高可读性。

$headerSize = 0;
curl_setopt_array($this->curlHandler, [
CURLOPT_URL => $yourUrl,
CURLOPT_POST => 0,
CURLOPT_RETURNTRANSFER => 1,
// this function is called by curl for each header received
CURLOPT_HEADERFUNCTION =>
         function ($curl, $header) use (&$headers, &$headerSize) {
              $lenghtCurrentLine = strlen($header);
              $headerSize += $lenghtCurrentLine;
              $header = explode(':', $header, 2);
              if (count($header) > 1) { // store only vadid headers
                   $headers[strtolower(trim($header[0]))][] = trim($header[1]);
              }
              return $lenghtCurrentLine;
           },
]);
$fullResult = curl_exec($this->curlHandler);
$result = substr($fullResult, $headerSize);

K
K-Gun

以防万一您不能/不使用 CURLOPT_HEADERFUNCTION 或其他解决方案;

$nextCheck = function($body) {
    return ($body && strpos($body, 'HTTP/') === 0);
};

[$headers, $body] = explode("\r\n\r\n", $result, 2);
if ($nextCheck($body)) {
    do {
        [$headers, $body] = explode("\r\n\r\n", $body, 2);
    } while ($nextCheck($body));
}

t
tosturaw

如果您使用 GET,请尝试以下操作:

$curl = curl_init($url);

curl_setopt_array($curl, array(
    CURLOPT_URL => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "GET",
    CURLOPT_HTTPHEADER => array(
        "Cache-Control: no-cache"
    ),
));

$response = curl_exec($curl);
curl_close($curl);

d
diyism

返回带有引用参数的响应标头:

<?php
$data=array('device_token'=>'5641c5b10751c49c07ceb4',
            'content'=>'测试测试test'
           );
$rtn=curl_to_host('POST', 'http://test.com/send_by_device_token', array(), $data, $resp_headers);
echo $rtn;
var_export($resp_headers);

function curl_to_host($method, $url, $headers, $data, &$resp_headers)
         {$ch=curl_init($url);
          curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $GLOBALS['POST_TO_HOST.LINE_TIMEOUT']?$GLOBALS['POST_TO_HOST.LINE_TIMEOUT']:5);
          curl_setopt($ch, CURLOPT_TIMEOUT, $GLOBALS['POST_TO_HOST.TOTAL_TIMEOUT']?$GLOBALS['POST_TO_HOST.TOTAL_TIMEOUT']:20);
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
          curl_setopt($ch, CURLOPT_HEADER, 1);

          if ($method=='POST')
             {curl_setopt($ch, CURLOPT_POST, true);
              curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
             }
          foreach ($headers as $k=>$v)
                  {$headers[$k]=str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k)))).': '.$v;
                  }
          curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
          $rtn=curl_exec($ch);
          curl_close($ch);

          $rtn=explode("\r\n\r\nHTTP/", $rtn, 2);    //to deal with "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n..." header
          $rtn=(count($rtn)>1 ? 'HTTP/' : '').array_pop($rtn);
          list($str_resp_headers, $rtn)=explode("\r\n\r\n", $rtn, 2);

          $str_resp_headers=explode("\r\n", $str_resp_headers);
          array_shift($str_resp_headers);    //get rid of "HTTP/1.1 200 OK"
          $resp_headers=array();
          foreach ($str_resp_headers as $k=>$v)
                  {$v=explode(': ', $v, 2);
                   $resp_headers[$v[0]]=$v[1];
                  }

          return $rtn;
         }
?>

您确定 $rtn=explode("\r\n\r\nHTTP/", $rtn, 2); 是正确的吗?不应该删除爆炸的第三个参数吗?
@user4271704,第三个参数是处理“HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n...”标头
但他说的是别的:stackoverflow.com/questions/9183178/…你们谁是正确的?
@user4271704 您所指的链接也使用:explode("\r\n\r\n", $parts, 2); 所以两者都是正确的。
B
Bevan

如果你真的不需要使用 curl;

$body = file_get_contents('http://example.com');
var_export($http_response_header);
var_export($body);

哪个输出

array (
  0 => 'HTTP/1.0 200 OK',
  1 => 'Accept-Ranges: bytes',
  2 => 'Cache-Control: max-age=604800',
  3 => 'Content-Type: text/html',
  4 => 'Date: Tue, 24 Feb 2015 20:37:13 GMT',
  5 => 'Etag: "359670651"',
  6 => 'Expires: Tue, 03 Mar 2015 20:37:13 GMT',
  7 => 'Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT',
  8 => 'Server: ECS (cpm/F9D5)',
  9 => 'X-Cache: HIT',
  10 => 'x-ec-custom-error: 1',
  11 => 'Content-Length: 1270',
  12 => 'Connection: close',
)'<!doctype html>
<html>
<head>
    <title>Example Domain</title>...

请参阅http://php.net/manual/en/reserved.variables.httpresponseheader.php


嗯,你也不需要 PHP,但这恰好是问题所在......