CURL

Источник: phpsu

libcurl для обмена данными

libcurl это библиотека функций, которая позволяет взаимодействовать (обмениваться информацией) с различными серверами по различным протоколам. В настоящее время libcurl поддерживает протоколы http, https, ftp, gopher, telnet, dict, file, и ldap. libcurl также умеет работать с сертификатами HTTPS, посылать запросы к HTTP серверам методами POST и PUT, закачивать файлы по протоколам HTTP и FTP (последнее можно сделать также используя модуль FTP ), использовать прокси-серверы, cookies и аутентификацию пользователей.

Нас поймут только по протоколу

Любые сервера, в том числе и web, умеют реагировать на присланные им данные, но только в том случае, если они понимают, что им прислали. Для этого, данные посылаемые им оформляются согласно определенным правилам. Такой набор правил и называется протоколом. Данные по протоколу оформит libcurl сама, мы же лишь рассмотрим далее как их ей передать.

Раз, два, начали.

Функций в библиотеке совсем не много, но одну из них, приводящую шестеренки в действие, вы встретите в любом скрипте с curl. curl_init - Инициализирует сеанс CURL. Другими словами - эта функция запускает механизм curl и возвращает указатель, дескриптор на созданный механизм.

Напомню что такое ресурс (указатель), тем кто забыл.

Дескриптор ( Resource )

Дескриптор представляет из себя указатель, ссылку, на внешний ресурс.
Представим автосервис с большим количеством машин, которым заливают несколько по литров масла в двигатель. Со свистом тормозов из подворотни вылетает феррари и паркуется на очередной сервисной парковке. Хозяин сервиса уже кричит рабочему, залить 5 литров масла и тычет большим пальцем в красную феррари.
Наш рабочий получает в данном случае два типа данных - число (литров масла) и указатель на машину, то-есть определение - какому именно объекту из всех вокруг нужно подлить чего-нибудь.

Подмечу, что рабочий получает от босса вовсе не саму машину, а лишь дескриптор (указатель) машины с которой предстоит работать. В php этим дескриптором является тип данных resource.

Функция curl_init также может сразу принимать url, адрес того сервера с которым будем общаться. Можно его и не указывать, а указать попозже. Инициализировав механизм, можно сразу отправить запрос, ну и наконец освободить память от этого механизма.
Вот что в итоге получится:

<?PHP
$ch = curl_init('http://php.su');
curl_exec($ch); // выполняем запрос curl - обращаемся к сервера php.su
curl_close($ch);
?>

Результатом этого кода будет прямой вывод содержимого главной страницы php.su. Не всегда нужно вывести результат запроса прямо в браузер и для этого достаточно просто покрутить пару настроек. Сейчас выясним как это сделать.

Конфигурируем общение

curl_setopt - устанавливает параметр для сеанса CURL
параметры бывают разные, и их много, очень. Какой за что отвечает можно посмотреть, само собой, в справочнике функции curl_setopt . Мы же сейчас рассмотрим парочку основных и выясним как ими манипулировать.

Но прежде я хочу вернуться на момент к обсуждению протокола http. Из чего же состоит набор правил этого протокола. Давай посмотрим как браузер общается с нашим сервером, что он ему посылает, и что тот от него получает. Я для этого использую расширение браузера livehttpheaders. И вот, что происходит при общении браузера с сервером:

Общение браузера с сервером получилось не слишком замысловатое. Смотрим:

GET /index.php HTTP/1.1 Дай страницу index.php. данные пришли правильно оформленные
Host: php.su с сайта php.su
User-Agent: Mozilla/5.0 А вот такой вот!
Accept: text/html И понимаю я только текст и html.
Accept-Language: ru,en-us; И говори по русски или я твоя не понимать
Accept-Charset: windows-1251,utf-8; Со специями пожалуйста.
Connection: keep-alive Жду от тебя ответа
Keep-Alive: 300 но терпения у меня мало.
Cookie: lastvisit=1243232518; А еще я заходил вчера и заказывал столик. Помнишь меня ? Нет? ну не важно, ты просил напомнить что я заходил во столько то. Теперь вспомнил? Отлично. Где мой столик ?

HTTP/1.1 200 OK Данные принял, состояние 200.
Date: Mon, 25 May 2009 06:33:05 GMT
Server: Apache Вас обслуживает ООО "Сервер Apache"
X-Powered-By: PHP/5.2.6 Старший шеф повар, php 5.2.6
Transfer-Encoding: chunked Это порция первая, вторая ща будет
Connection: close А теперь получил?, распишись, больше не жди
Content-Type: text/html; charset=cp1251 Ваша пицца по русски, с грибами

Сразу после заголовков ответа идёт сам ответ, тоесть html страницы. А мы, тем временем, что стали свидетелями общения моего браузера с сервером php.su. А раз браузер может, то и мы с нашей программой можем.

Заказываем данные с нужными опциями

После того, как мы запустили наш код простой код

<?PHP
$ch = curl_init('http://php.su');
curl_exec($ch); // выполняем запрос curl - обращаемся к сервера php.su
curl_close($ch);
?>

Мы увидели в браузере только html код полученный от сервера. Возможно нам захочется также посмотреть на заголовки, который прислал сервер, а то вдруг я все выше написанное выдумал ?

Для этого установим опцию "показывать заголовки"
CURLOPT_HEADER : При установке этого параметра в ненулевое значение результат будет включать полученные заголовки.
Разумеется параметры устанавливать нужно до того, как отправим сам запрос серверу. И так, получилось вот такое:

<?PHP
$ch = curl_init('http://php.su');
curl_setopt  ($ch, CURLOPT_HEADER, true);
curl_exec($ch); // выполняем запрос curl
curl_close($ch);
?>

Возможно нам также захочется получит содержимое в переменную, а вовсе не выводить сразу в браузер. Для этого нам придется установить такое значение среди прочих командой: curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
Хотя документация видимо ошибается в плане настроек..

Позволю себе заметить, что в документации написано что названием параметра должна быть строка. Мы же передаем функции setopt вовсе не строку (кавычек то нет). Мы передаем константу. Предопределенную переменную , значение которой изменить нельзя. Эти константы сами определяются библиотекой, интересно другое... значения этих констант вовсе не строковые, а численные. Поэтому если кто-то узнает почему в документации написано "строка" вместо числа - дайте мне знать.

А мы пока попробуем авторизоваться на форуме. А для того, чтобы имитировать обычный браузер, нам нужно посмотреть что говорит браузер серверу, что тот ему отвечает и что потом.

Строим из себя пользователя

В первую очередь, чтоб все это показалось правдоподобным, посмотрим что происходит когда мой браузер авторизовывается на форуме. Для этого я опять же воспользуюсь расширением для FireFox под названием liveHttpHeaders и вот что я вижу.

CODE:

http://php.su/forum/loginout.php

POST /forum/loginout.php HTTP/1.1
Host: php.su
User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.10) Gecko/2009042523 Ubuntu/8.10 (intrepid) Firefox/3.0.10
Accept: text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://php.su/forum/loginout.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 71
action=login&imembername=valenok&ipassword=ne_skaju&submit=%C2%F5%EE%E4

HTTP/1.x 302 Found
Date: Tue, 26 May 2009 14:09:09 GMT
Server: Apache
X-Powered-By: PHP/5.2.6
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: lastvisit=1243346949; expires=Wed, 26-May-2010 14:09:09 GMT; path=/
Set-Cookie: exbbn=19; expires=Wed, 26-May-2010 14:09:09 GMT; path=/
Set-Cookie: exbbp=1234567525d2b72bcb01cd2ffe123456; expires=Wed, 26-May-2010 14:09:09 GMT; path=/
Set-Cookie: PHPSESSID=123456789e4eef401e4539060010cc0f;
Set-Cookie: lastvisit=1243346949; expires=Wed, 26-May-2010 14:09:09 GMT; path=/
Location: index.php
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 26
Connection: close
Content-Type: text/html; charset=cp1251

Запрос отличается от предыдущего лишь немногим. рассмотрим разницу.

POST /forum/loginout.php HTTP/1.1 На этот раз мы не просто просим дать нам содержимое страницы, но отправляем серверу свои данные.
Referer: http://php.su/forum/loginout.php Откуда мы шлем данные (на какой страницы только что был браузер).
Content-Type: application/x-www-form-urlencoded тип данных (данные из формы).
Content-Length: 71 длинна посылаемых данных.
action=login&imembername=valenok&ipassword=ne_skaju&submit=%C2%F5%EE%E4 ну и сами данные.. Заметь что отправляются все данные из формы.

А вот ответ немного отличается от предыдущего. В нем появляются новые заголовки. В первую очередь нас интересуют Set-Cookie и Location. Остальные особой роли не играют и что они означают можно найти в википедии.

Set-Cookie как ты видишь, и их много, возможно тебе уже знаком. Задача данного заголовка это наклеить на тебя наклейку с именем, чтобы потом сервер мог тебя по нему узнать и сказать, ах, да, точно, это ты. Разумеется для этого при каждом следующем обращении к серверу нужно будет приходить с этой наклейкой.

Location: index.php перенаправляет браузер на другую страницу, после авторизации.

Ну что, теперь попробуем сыграть за браузер ? Я тоже браузер
Будем анализировать на практике. Код получился следующий.

<?PHP

$ch = curl_init('http://php.su/forum/loginout.php');
# /forum/loginout.php HTTP/1.1

curl_setopt($ch, CURLOPT_POST, 1);
# POST /forum/..

curl_setopt ($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (бла бла бла..) "); 
# User-Agent

$headers = array
(
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*;q=0.8',
    'Accept-Language: ru,en-us;q=0.7,en;q=0.3',
    'Accept-Encoding: deflate',
    'Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7'
); 

curl_setopt($ch, CURLOPT_HTTPHEADER,$headers); 
# добавляем заголовков к нашему запросу. Чтоб смахивало на настоящих

curl_setopt($ch, CURLOPT_REFERER, "http://php.su/forum/loginout.php");
# Подделываем значение - откуда пришли данные.

curl_setopt($ch, CURLOPT_POSTFIELDS, 'action=login&imembername=valenok&ipassword=ne_skaju&submit=%C2%F5%EE%E4');
# post данные.
# умная libcurl сама добавит заголовки
# Content-Type: application/x-www-form-urlencoded и Content-Length: 71

curl_setopt($ch, CURLOPT_COOKIEJAR, "my_cookies.txt");  
curl_setopt($ch, CURLOPT_COOKIEFILE, "my_cookies.txt");  
# Функции для обработки установливаемых форумом кук.
# подробнее рассмотрим далее.

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
# Убираем вывод данных в браузер. Пусть функция их возвращает а не выводит

$result = curl_exec($ch); // выполняем запрос curl
curl_close($ch);
?>

Какой параметр за что отвечает я показал в коде. Если что-то не до конца ясно, всегда можно посмотреть в документации. Тем не менее, двумя словами опишу параметры cookiejar и cookiefile.

Когда сервер выдает нам куки, тоесть наклейку - Ты такой-то, он потом смотрит на эту наклейку и вспоминает тебя. Но нам для этого разумеется нужно обращаться к серверу когда наклейка у нас висит на видном месте. libcurl может за нас сохранять наклейку в файл, если мы его укажем в параметре cookiejar и также посылать куки, тоесть обращаться вместе с наклейкой, если мы укажем файл в котором эту наклейку мы сохранили cookiefile. А так как нужно было для авторизации чтоб сервер запомнил что мы, это мы при следующем обращение, то на самом деле нужно было просто получить при авторизации куки. Поэтому вот как мы это сделаем.

<?PHP
$ch = curl_init('http://php.su/forum/loginout.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'action=login&imembername=valenok&ipassword=ne_skaju&submit=%C2%F5%EE%E4');
curl_setopt($ch, CURLOPT_COOKIEJAR, "my_cookies.txt");  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
curl_exec($ch); 
curl_close($ch);
?>

Здесь я сократил несколько этапов. Причиной стало то, что сервер на самом деле не проверяет откуда пришли данные авторизации, поэтомум реферер указывать не имеет смысла. Какой браузер авторизовывается - ему тоже все равно. Уберем.
Заголовки о том, что мы хотим получить в ответ - тоже отправлять не будем. В ответ же мы все равно получим куки и перенаправление. Кроме того почти ни один программист все равно эти заголовки не учитывает. Они как бараны решили что знают как правильно и что нужно пользователю - и все тут.
Также установлен параметр - без тела (nobody) который говорит что весь html нам не нужен, нужны только заголовки. На самом деле его там и нет, но это только в нашем случае. На самом деле скрипт может и проводить авторизацию и ругаться в одном флаконе.
Присвоение результата в переменную я тоже убрал. Зачем нам в памяти результат запроса.. Мы и так знаем что он успешен. Но если не знаем, можно для примера проверять на наличие перенаправления Location:index.php и на основании этого решать, правильно ли авторизовались или нет. Но ничего ли мы не забыли ?

Пара рекомендаций

На самом деле все что бы могло тебе потребоваться - мы уже прошли. Ты умеешь уже авторизоваться где нибудь, отправить форму (пост данные) средствами библиотеки curl при этом выдавать себя за другой браузер. И напоминать серверу кто ты - с прошлого раза при помощи кук.

Я же лишь скажу что иногда тебе вовсе не нужна страница, а лишь её заголовки, как в нашем примере авторизации. Еще не нужно для каждого запроса создавать в памяти экземпляр механизма curl. Один раз его инициализируем, потом просто меняем параметры и адрес url. Думаю этого вполне хватит для успешного обмена данным с другими сайтами по началу, ну а если не хватит, то можно рассмотреть несколько сложных случаев далее. Несколько параллельных запросов и curl_multi_init

Если бы нам нужно было бы получить допустим содержания трех страниц, то наверное мы бы отправили запрос сначала на одну, получили результат, отправили на вторую и потом только на третью. Но вот чудо - эта библиотека позволяет отправить запрос сразу в 3 источника параллельно, при этом затратить на все это дело столько же времени сколько на один запрос.

Для следующего кода примеры данных $data могут быть следующими:

<?PHP
// GET
$data = Array
(
    'http://yandex.ru',
    'http://php.su',
    'http://google.com'
);

// POST
$data = Array
(
    Array('url' => 'http://yandex.ru/login.php', 'post' => 'a=b&c=d'),
    Array('url' => 'http://php.su/index.php', 'post' => 'a=b&c=d'),
    Array('url' => 'http://google.com/search.py', 'post' => 'a=b&c=d')
);
?>

Сама функция:

<?PHP
function multiCurl($data, $options = array()) 
{

  $curls = array();
  // Массив дескрипторов. Библиотека создает много экземпляров своего 
  // механизма, но работать они будут параллельно
  
  $result = array();
  // массив с результатами запрошенных страниц которые наша функция вернет.

  $mh = curl_multi_init();
  // Дескриптор мульти потока. Тоесть эта штука отвечает за то, чтобы много
  // запросов шли параллельно.

  foreach ($data as $id => $d) {

    $curls[$id] = curl_init();
    // Для каждого url создаем отдельный curl механизм чтоб посылал запрос)

    $url = (is_array($d) && !empty($d['url'])) ? $d['url'] : $d;
    // Если $d это массив (как в случае с пост), то достаем из массива url
    // если это не массив, а уже ссылка - то берем сразу ссылку

    curl_setopt($curls[$id], CURLOPT_URL,            $url);
    curl_setopt($curls[$id], CURLOPT_HEADER,         0);
    curl_setopt($curls[$id], CURLOPT_RETURNTRANSFER, 1);

    // Если у нас есть пост данные, тоесть запрос отправляется постом 
    // устанавливаем флаги и добавляем сами данные
    if (is_array($d) && !empty($d['post'])) 
    {
        curl_setopt($curls[$id], CURLOPT_POST,       1);
        curl_setopt($curls[$id], CURLOPT_POSTFIELDS, $d['post']);
    }
  

    // Если указали дополнительные параметры $options то устанавливаем их
    // смотри документацию функции curl_setopt_array
    if (count($options)>0) curl_setopt_array($curls[$id], $options);

    // добавляем текущий механизм к числу работающих параллельно
    curl_multi_add_handle($mh, $curls[$id]);
  }

  // число работающих процессов.
  $running = null;

  // curl_mult_exec запишет в переменную running количество еще не завершившихся
  // процессов. Пока они есть - продолжаем выполнять запросы.
  do { curl_multi_exec($mh, $running); } while($running > 0);

  // Собираем из всех созданных механизмов результаты, а сами механизмы удаляем
  foreach($curls as $id => $c) 
  {
    $result[$id] = curl_multi_getcontent($c);
    curl_multi_remove_handle($mh, $c);
  }

  // Освобождаем память от механизма мультипотоков
  curl_multi_close($mh);

  // возвращаем данные собранные из всех потоков.
  return $result;
}
?>


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=24855