Может ли PHP cURL получить заголовки ответа и тело в одном запросе?

314

Есть ли способ получить заголовки и тело для запроса cURL, используя PHP? Я обнаружил, что этот вариант:

curl_setopt($ch, CURLOPT_HEADER, true);

собирается вернуть тело плюс заголовки , но затем мне нужно разобрать его, чтобы получить тело. Есть ли способ получить как более удобный (и безопасный) способ?

Обратите внимание, что для «одного запроса» я имею в виду избегать выдачи запроса HEAD до GET / POST.

Gremo
источник
3
Для этого есть встроенное решение, см. Этот ответ: stackoverflow.com/a/25118032/1334485 (добавил этот комментарий, потому что этот пост все еще получает много просмотров)
Skacc
Посмотрите на этот хороший комментарий: secure.php.net/manual/en/book.curl.php#117138
user956584
Мне сказали, что мой вопрос был дубликатом этого вопроса. Если это не дубликат, может кто-нибудь открыть его? stackoverflow.com/questions/43770246/… В моем вопросе у меня есть конкретное требование использовать метод, который возвращает объект с заголовками и телом отдельно, а не с одной строкой.
1,21 гигаватта

Ответы:

466

Одно из решений было опубликовано в комментариях к документации 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_VERBOSE, 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);

Предупреждение: как отмечено в комментариях ниже, это может быть ненадежно при использовании с прокси-серверами или при обработке определенных типов перенаправлений. Ответ @ Джеффри может обработать их более надежно.

iblue
источник
22
Вы также можете list($header, $body) = explode("\r\n\r\n", $response, 2), но это может занять немного больше времени, в зависимости от размера вашего запроса.
iblue
43
это плохое решение, потому что, если вы используете прокси-сервер и ваш прокси-сервер (например, fiddler), добавляете собственные заголовки к ответу - эти заголовки list($header, $body) = explode("\r\n\r\n", $response, 2)
нарушают
5
@msangel Ваше решение не работает, когда в ответе несколько заголовков, например, когда сервер выполняет перенаправление 302. Какие-либо предложения?
Ноябрь
4
@ Нейт, да, я знаю это. AFAIK, но есть только один возможный дополнительный заголовок - с кодом 100(Продолжить). Для этого заголовка вы можете использовать правильную опцию запроса: curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); отключить отправку ответа этого заголовка. Что касается 302этого, этого не должно происходить, потому что заголовок 302 - это перенаправление, он не ожидает тела, однако я знаю, что иногда серверы отправляют какое-то тело с 302ответом, но браузеры все равно будут игнорироваться, почему же curl должен справиться с этим? )
msangel
5
CURLOPT_VERBOSEпредназначен для вывода информации о процессе STDERR(может беспокоить в CLI) и для обсуждаемой проблемы бесполезно.
Хейдав
205

Многие из других решений , предлагаемых эта нить не делают это правильно.

  • Разделение на \r\n\r\nне надежно, когда CURLOPT_FOLLOWLOCATIONвключено, или когда сервер отвечает кодом 100.
  • Не все серверы соответствуют стандартам и передают данные только \nдля новых линий.
  • Определение размера заголовков через CURLINFO_HEADER_SIZEтакже не всегда надежно, особенно когда используются прокси или в некоторых из тех же сценариев перенаправления.

Самый правильный метод использует CURLOPT_HEADERFUNCTION.

Вот очень чистый метод выполнения этого с использованием замыканий PHP. Он также преобразует все заголовки в нижний регистр для согласованной обработки между серверами и версиями HTTP.

Эта версия сохранит дублированные заголовки

Это соответствует RFC822 и RFC2616, пожалуйста, не предлагайте правки для использования mb_строковых функций, это неверно!

$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);
Джеффри
источник
12
ИМО, это лучший ответ в этой теме и исправляет проблемы с перенаправлениями, которые возникали с другими ответами. Лучше всего прочитать документацию для CURLOPT_HEADERFUNCTION, чтобы понять, как она работает и потенциальные ошибки. Я также внес некоторые улучшения в ответ, чтобы помочь другим.
Саймон Ист
Отлично, я обновил ответ, чтобы обслужить дублированные заголовки. В будущем не переформатируйте код так, как вам кажется. Это написано так, чтобы было ясно, где находятся границы функции замыкания.
Джеффри
@ Geoffrey $headers = [];Действительный php?
thealexbaron
6
@thealexbaron Да, по состоянию на PHP 5.4, см .: php.net/manual/en/migration54.new-features.php
Джеффри
4
Этот ответ крайне недооценен для такого аккуратного и RFC-совместимого подхода. На это следует сделать липкий ответ и перенести наверх. Я просто хотел бы, чтобы был более быстрый подход для получения значения желаемого заголовка, вместо того, чтобы сначала анализировать все заголовки.
Fr0zenFyr
114

У 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'))
Skacc
источник
Как примечание, функция обратного вызова вызывается для каждого заголовка, и кажется, что они не обрезаются. Вы можете использовать глобальную переменную для хранения всех заголовков или анонимную функцию для обратного вызова и использовать локальную переменную (локальную для родительской области, а не анонимную функцию).
М.В.
2
@MV Спасибо, да, под «построчно» я имел в виду «каждый заголовок». Я отредактировал свой ответ для ясности. Чтобы получить весь раздел заголовка (он же все заголовки), вы также можете использовать метод объекта для обратного вызова, чтобы свойство объекта могло содержать их все.
Skacc
8
Это лучший ответ ИМО. Это не вызывает проблем с несколькими "\ r \ n \ r \ n" при использовании CURLOPT_FOLLOWLOCATION, и я думаю, что это не будет зависеть от дополнительных заголовков от прокси.
Рафал Г.
Работал очень хорошо для меня, также см. Stackoverflow.com/questions/6482068/… в случае проблем
RHH
1
Да, это лучший подход, однако ответ @ Джеффри делает это чище, используя анонимную функцию без необходимости использования глобальных переменных и тому подобное.
Саймон Ист
39

это то, что вы ищете?

curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
$response = curl_exec($ch); 
list($header, $body) = explode("\r\n\r\n", $response, 2);
user1031143
источник
8
Это работает нормально, за исключением случаев, когда есть HTTP / 1.1 100 Continue с последующим перерывом, а затем HTTP / 1.1 200 OK. Я бы пошел с другим методом.
призрак
1
Посмотрите на выбранный ответ 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.
Алрик,
Этот метод также дает сбой при переадресации 302, если для curl задан заголовок местоположения.
Саймон Ист
10

Просто установите параметры:

  • CURLOPT_HEADER, 0

  • CURLOPT_RETURNTRANSFER, 1

и используйте curl_getinfo с CURLINFO_HTTP_CODE (или без параметра opt, и у вас будет ассоциативный массив со всей необходимой информацией)

Больше на: http://php.net/manual/fr/function.curl-getinfo.php

Кирилл Х.
источник
5
Похоже, это не возвращает вам заголовки ответа. Или, по крайней мере, нет способа восстановить их, используя curl_getinfo().
Саймон Ист
8

Если вы конкретно хотите 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);
pr1001
источник
ОП спросил, есть ли способ получить заголовки, а не один конкретный заголовок, это не отвечает на вопрос ОП.
Джеффри
2
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);
Enyby
источник
Не должен $parts = explode("\r\n\r\nHTTP/", $response);иметь 3-й параметр для взрыва как 2?
user4271704
@ user4271704 Нет. Позволяет найти последнее HTTP-сообщение. HTTP/1.1 100 Continueможет появиться много раз.
Enyby
Но он говорит что-то еще: stackoverflow.com/questions/9183178/… кто из вас прав?
user4271704
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...его код не работает должным образом
Enyby
1
Разделение на \ r \ n ненадежно, некоторые серверы не соответствуют спецификациям HTTP и отправляют только \ n. Стандарт RFC гласит, что приложения должны игнорировать \ r и разделяться на \ n для максимальной надежности.
Джеффри
1

Мой путь

$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 и удалите предел разнесения.

Рой
источник
1

Вот мой вклад в дебаты ... Это возвращает один массив с разделенными данными и заголовками в списке. Это работает на основе того, что 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;
    }
}
Antony
источник
0

Проблема многих ответов здесь заключается в том, что они "\r\n\r\n"могут законно появляться в теле html, поэтому вы не можете быть уверены, что правильно разбиваете заголовки.

Кажется, что единственный способ хранить заголовки отдельно с одним вызовом curl_exec- это использовать обратный вызов, как это было предложено выше в https://stackoverflow.com/a/25118032/3326494

А затем, чтобы (надежно) получить только тело запроса, вам нужно передать значение Content-Lengthзаголовка substr()как отрицательное начальное значение.

ТЗА
источник
1
Это может показаться законным, но ваш ответ неверен. Content-Length не обязательно должен присутствовать в ответе HTTP. Правильный метод ручного разбора заголовков - поиск первого экземпляра \ r \ n (или \ n \ n). Это может быть сделано просто путем ограничения взрываются возвращать только два элемента, а именно: list($head, $body) = explode("\r\n\r\n", $response, 2);, однако CURL уже делает это для вас , если вы используетеcurl_setopt($ch, CURLOPT_HEADERFUNCTION, $myFunction);
Geoffrey
-1

На тот случай, если вы не можете / не используете 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));
}
K-Gun
источник
-2

Вернуть заголовки ответа со ссылочным параметром:

<?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;
         }
?>
diyism
источник
Ты уверен $rtn=explode("\r\n\r\nHTTP/", $rtn, 2); это правильно? Не следует ли удалить 3-й параметр разнесения?
user4271704
@ user4271704, третий параметр должен иметь дело с заголовком «HTTP / 1.1 100 Continue \ r \ n \ r \ nHTTP / 1.1 200 OK ... \ r \ n \ r \ n ...»
diyism
Но он сказал что-то еще: stackoverflow.com/questions/9183178/… кто из вас прав?
user4271704
@ user4271704 ссылка, на которую вы ссылаетесь, также используется: explode("\r\n\r\n", $parts, 2); так что оба правы.
Киборг
-5

Если вам не нужно использовать 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

Беван
источник
16
хм, вам на самом деле тоже не нужен PHP, но именно об этом и идет вопрос ...
Ханс З.