В данном топике представлена на мой взгляд удобная и функциональная реализация многопоточного скачивания на cURL для PHP. Возможно кому-то она будет полезна, а мне принесёт инвайт ;)
Скачиванием через cURL не пользовался пусть даже из интереса только ленивый. Будь-то из консоли, либо реализуя код на каком-либо ЯП. Решения блокирующего скачивания одной ссылки валяются на каждом углу сети, к примеру на php.net. Однако, если рассматривать реализации на PHP, то такой подход подчас не подходит ввиду высоких временных затрат на вспомогательные операции ( dns lookup, request waiting и подобные ). Для скачивания большого числа страниц последовательный вариант не приемлем. Если устраивает — дальше можно не читать :)
В Perl, к примеру, можно применять fork() либо нити (use threads) для распараллеливания однопоточных скачиваний. Это не считая богатых возможностей библиотек данного языка. Я лично применял нити и LWP. Однако, речь идёт о PHP, и тут с распараллеливанием большие проблемы ввиду отсутствия данной возможности в принципе. Если кто знает, как создавать нити, сообщите, но я не нашел пока достойных решений. Да, в cURL есть функции curl_multi_*, но вот примеры реализаций на их основе меня не устроили. И, в итоге, решил собрать свой велосипед.
Первоначально отсылаю к простейшему примеру из офф. справочника. Позволю себе привести его тут :)
<?php
// create both cURL resources
$ch1 = curl_init();
$ch2 = curl_init();
// set URL and other appropriate options
curl_setopt($ch1, CURLOPT_URL, «www.example.com»);
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, «www.php.net»);
curl_setopt($ch2, CURLOPT_HEADER, 0);
//create the multiple cURL handle
$mh = curl_multi_init();
//add the two handles
curl_multi_add_handle($mh,$ch1);
curl_multi_add_handle($mh,$ch2);
$running=null;
//execute the handles
do {
curl_multi_exec($mh,$running);
} while ($running > 0);
//close the handles
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
?>
Код отличается от однопоточного подхода более сложной организацией взаимодействия прикладного кода с библиотекой:
1) Для каждого соединения выполняется свой curl_init() и задаются параметры через curl_setopt(). Тут всё стандартно, привожу без объяснений.
2) Для общего управления скачиванием вызовом curl_multi_init() создается отдельный дескриптор, через который и будет производиться вся дальнейшая работа.
3) К данному дескриптору вызовом curl_multi_add_handle() цепляются созданные в начале отдельный соединения.
Подготовительный этап завершен, теперь непосредственно скачивание:
4) Скачивание библиотекой выполняется автоматически, явного вызова как было с curl_exec() теперь нет. Его заменяет многократный вызов curl_multi_exec(). Несмотря на схожее название, данная функция выполняет несколько другую роль — она блокирующе информирует об изменении числа активных потоков (ну и возникших ошибок). Второй параметр при вызове — ссылка на числовую переменную, в которую сохраняется число активных в данный момент соединений. Количество изменилось — значит какой-то поток завершил работу. Вот по этой причине цикл скачивания и реализован через
do {
curl_multi_exec($mh,$running);
} while ($running > 0);
5) Ну и наконец после скачивания выполняется освобождение ресурсов. Важно! Хоть соединения, созданные curl_init() и «цепляются» к основному дескриптору, он их автоматически не закрывает, это нужно делать вручную вызовом curl_multi_remove_handle() в добавление к curl_close().
Кому-то может хватить и такой реализации, и они могут дальше не читать. Я же пойду дальше.
Что в данной реализации плохого? Пара наиболее явных моментов:
Это лишь часть, остальное обсуждается далее.
Исправляю указанные недостатки и получаю, к примеру, следующее:
<?php
$urls = array( «www.example.com», «www.php.net» );
$mh = curl_multi_init();
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_HEADER, 0 );
// CURLOPT_RETURNTRANSFER - возвращать значение как результат функции, а не выводить в stdout
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_multi_add_handle( $mh, $ch );
}
$prev_running = $running = null;
do {
curl_multi_exec( $mh, $running );
if ( $running != $prev_running ) {
// получаю информацию о текущих соединениях
$info = curl_multi_info_read( $mh );
if ( is_array( $info ) && ( $ch = $info['handle'] ) ) {
// получаю содержимое загруженной страницы
$content = curl_multi_getcontent( $ch );
// тут какая-то обработка текста страницы
// пока пусть будет как и в оригинале - вывод в STDOUT
echo $content;
}
// обновляю кешируемое число текущих активных соединений
$prev_running = $running;
}
} while ( $running > 0 );
foreach ( $chs as $ch ) {
curl_multi_remove_handle( $mh, $ch );
curl_close( $ch );
}
curl_multi_close($mh);
?>
Далее, вряд ли в большинстве случаев будет достаточно просто выводить страницы в STDOUT. Тем более это происходит в произвольном порядке в зависимости от порядка реального скачивания (а не задания вызовами curl_multi_add_handle() ). Также, если скачивается большой объем, то нет смысла дожидаться получения всех страниц — можно уже начинать обрабатывать их по мере получения. Но и вариант с получением всех скопом также не стоит снимать со счетов.
Для этого: 1) реализую всё в виде функции, 2) введу параметр, задающий callback-функцию, которая будет вызываться для каждого полученного файла. Если callback не задан — применяется вариант с получением всех страниц сразу. Вот пример:
<?php
// пример простейшего callback'а. практически dummy-func.
function my_callback( $url, $content, $curl_status, $ch ) {
echo «Скачивание страницы [$url] »;
if ( !$curl_status ) {
echo «было успешным. текст страницы:\n$content\n»;
}
else {
echo «выполнилось с ошибкой #$curl_status: ».curl_error( $ch )."\n";
}
}
function http_load( $urls, $callback = false ) {
$mh = curl_multi_init();
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_HEADER, 0 );
// CURLOPT_RETURNTRANSFER - возвращать значение как результат функции, а не выводить в stdout
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_multi_add_handle( $mh, $ch );
}
// если $callback задан как false, то функция должна не вызывать $callback, а выдать страницы как результат работы
if ( $callback === false ) {
$results = array();
}
$prev_running = $running = null;
do {
curl_multi_exec( $mh, $running );
if ( $running != $prev_running ) {
// получаю информацию о текущих соединениях
$info = curl_multi_info_read( $ghandler );
if ( is_array( $info ) && ( $ch = $info['handle'] ) ) {
// получаю содержимое загруженной страницы
$content = curl_multi_getcontent( $ch );
// скаченная ссылка
$url = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
if ( $callback !== false ) {
// вызов callback-обработчика
$callback( $url, $content, $info['result'], $ch );
}
else {
// добавление в хеш результатов
$results[ $url ] = array( 'content' => $content, 'status' => $info['result'], 'status_text' => curl_error( $ch ) );
}
}
// обновляю кешируемое число текущих активных соединений
$prev_running = $running;
}
} while ( $running > 0 );
foreach ( $chs as $ch ) {
curl_multi_remove_handle( $mh, $ch );
curl_close( $ch );
}
curl_multi_close( $mh );
// результаты
return ( $callback !== false ) ? true : $results;
}
$urls = array( «www.example.com», «www.php.net» );
// вариант простой выдачи
print_r( http_load( $urls ) );
// вариант с callback
var_export( http_load( $urls, my_callback ) );
?>
Вот уже гораздо интереснее. Важный момент: при callback 4ый параметр — дескриптор соединения $ch, а при выдаче результатов хешем — просто строковое описание возникшей ошибки (ну или пустая строка, если всё нормально). Почему? Потому что curl_error() требует передачи дескриптора, который закрывается в конце работы функции. Так что в callback он еще существует и мы можем его использовать, а вот в хеше он уже ничего ценного дать не может. Как вариант, строковые описания кодов ошибок можно взять тут.
Итак, идём дальше. Хочется вызывать функцию не только для массива ссылок, но и иметь возможность скачать ей единственную страницу. Для этого нужно добавить всего-то одну строчку:
<?php function http_load( $urls, $callback = false ) {
…
// даже если передан единственный параметр - считаю его элементом массива
// это аналог: $urls = is_array( $urls ) ? $urls : array( $urls );
$urls = (array)$urls;
.... ?>
Вот теперь можно качать ссылки по одной: http_load( 'google.com' ). Этакий возврат к истокам.
Потом мне потребовалось задавать много больше передаваемых заголовков для соединений. Указывать их по одному через curl_setopt() не практично. Лучше воспользоваться функцией curl_setopt_array. Переделываю и получаю (часть кода):
<?php
{ // общие для всех соединений заголовки
$ext_headers = array(
'Expect:',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9',
'Accept-Language: ru,en-us;q=0.7,en;q=0.7',
//'Accept-Encoding: gzip,deflate', // нужно потом распаковывать. ну его пока…
'Accept-Charset: utf-8,windows-1251;q=0.7,*;q=0.5',
);
$curl_options = array(
CURLOPT_PORT => 80,
CURLOPT_RETURNTRANSFER => 1, // возвращать значение как результат функции, а не выводить в stdout
CURLOPT_BINARYTRANSFER => 1, // передавать в binary-safe
CURLOPT_CONNECTTIMEOUT => 10, // таймаут соединения ( lookup + connect )
CURLOPT_TIMEOUT => 30, // таймаут на получение данных
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.1) Gecko/20090716 Ubuntu/9.04 (jaunty) Shiretoko/3.5.1',
CURLOPT_VERBOSE => 2, // уровень информирования
CURLOPT_HEADER => 0, // заголовок не получается
CURLOPT_FOLLOWLOCATION => 1, // следовать редиректам
CURLOPT_MAXREDIRS => 7, // максимальное число редиректов
CURLOPT_AUTOREFERER => 1, // при редиректе подставлять в «Referer:» значение из «Location:»
// CURLOPT_FRESH_CONNECT => 0, // каждый раз использовать новое соединение
CURLOPT_HTTPHEADER => $ext_headers,
);
}
function http_load( $urls, $callback = false ) {
global $curl_options;
$mh = curl_multi_init();
if ( $mh === false ) return false;
$urls = (array)$urls;
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt_array( $ch, $curl_options ); // задаю заголовки скопом
curl_setopt( $ch, CURLOPT_URL, $url );
curl_multi_add_handle( $mh, $ch );
}
…
?>
Прикидываемся Огнелисом. Заголовки я прокомментировал. За подробным объяснением отправляю сюда.
И в догонку к этим заголовкам добавляется третий параметр в функцию:
в котором можно указывать свои заголовки, которые будут дополнительно преданы в соединения при их инициализации. Таким образом можно успешно отправлять POST запросы с параметрами либо указывать свои рефералы и формат передаваемых данных (при компрессии к примеру).
<?php
…
foreach ( $urls as $ind => $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt_array( $ch, $curl_options ); // задаю заголовки скопом
curl_setopt( $ch, CURLOPT_URL, $url );
// есть дополнительные параметры для инициализации данного соединения?
if ( isset( $urls_params[ $ind ] ) && is_array( $urls_params[ $ind ] ) ) {
curl_setopt_array( $ch, $urls_params[ $ind ] );
}
curl_multi_add_handle( $mh, $ch );
}
…
?>
Вот такая функция. Еще можно было бы написать про работу с куками и POST-запросами, но это уж если получу инвайт. И так понаписал много, многие ли осилили? ;)
Скачиванием через cURL не пользовался пусть даже из интереса только ленивый. Будь-то из консоли, либо реализуя код на каком-либо ЯП. Решения блокирующего скачивания одной ссылки валяются на каждом углу сети, к примеру на php.net. Однако, если рассматривать реализации на PHP, то такой подход подчас не подходит ввиду высоких временных затрат на вспомогательные операции ( dns lookup, request waiting и подобные ). Для скачивания большого числа страниц последовательный вариант не приемлем. Если устраивает — дальше можно не читать :)
В Perl, к примеру, можно применять fork() либо нити (use threads) для распараллеливания однопоточных скачиваний. Это не считая богатых возможностей библиотек данного языка. Я лично применял нити и LWP. Однако, речь идёт о PHP, и тут с распараллеливанием большие проблемы ввиду отсутствия данной возможности в принципе. Если кто знает, как создавать нити, сообщите, но я не нашел пока достойных решений. Да, в cURL есть функции curl_multi_*, но вот примеры реализаций на их основе меня не устроили. И, в итоге, решил собрать свой велосипед.
Первоначально отсылаю к простейшему примеру из офф. справочника. Позволю себе привести его тут :)
<?php
// create both cURL resources
$ch1 = curl_init();
$ch2 = curl_init();
// set URL and other appropriate options
curl_setopt($ch1, CURLOPT_URL, «www.example.com»);
curl_setopt($ch1, CURLOPT_HEADER, 0);
curl_setopt($ch2, CURLOPT_URL, «www.php.net»);
curl_setopt($ch2, CURLOPT_HEADER, 0);
//create the multiple cURL handle
$mh = curl_multi_init();
//add the two handles
curl_multi_add_handle($mh,$ch1);
curl_multi_add_handle($mh,$ch2);
$running=null;
//execute the handles
do {
curl_multi_exec($mh,$running);
} while ($running > 0);
//close the handles
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
?>
Код отличается от однопоточного подхода более сложной организацией взаимодействия прикладного кода с библиотекой:
1) Для каждого соединения выполняется свой curl_init() и задаются параметры через curl_setopt(). Тут всё стандартно, привожу без объяснений.
2) Для общего управления скачиванием вызовом curl_multi_init() создается отдельный дескриптор, через который и будет производиться вся дальнейшая работа.
3) К данному дескриптору вызовом curl_multi_add_handle() цепляются созданные в начале отдельный соединения.
Подготовительный этап завершен, теперь непосредственно скачивание:
4) Скачивание библиотекой выполняется автоматически, явного вызова как было с curl_exec() теперь нет. Его заменяет многократный вызов curl_multi_exec(). Несмотря на схожее название, данная функция выполняет несколько другую роль — она блокирующе информирует об изменении числа активных потоков (ну и возникших ошибок). Второй параметр при вызове — ссылка на числовую переменную, в которую сохраняется число активных в данный момент соединений. Количество изменилось — значит какой-то поток завершил работу. Вот по этой причине цикл скачивания и реализован через
do {
curl_multi_exec($mh,$running);
} while ($running > 0);
5) Ну и наконец после скачивания выполняется освобождение ресурсов. Важно! Хоть соединения, созданные curl_init() и «цепляются» к основному дескриптору, он их автоматически не закрывает, это нужно делать вручную вызовом curl_multi_remove_handle() в добавление к curl_close().
Кому-то может хватить и такой реализации, и они могут дальше не читать. Я же пойду дальше.
Что в данной реализации плохого? Пара наиболее явных моментов:
- жёсткое ограничение на скачивание 2х ссылок, заданное прямо в коде
- получаемые страницы выводятся прямо в STDOUT
Это лишь часть, остальное обсуждается далее.
Исправляю указанные недостатки и получаю, к примеру, следующее:
<?php
$urls = array( «www.example.com», «www.php.net» );
$mh = curl_multi_init();
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_HEADER, 0 );
// CURLOPT_RETURNTRANSFER - возвращать значение как результат функции, а не выводить в stdout
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_multi_add_handle( $mh, $ch );
}
$prev_running = $running = null;
do {
curl_multi_exec( $mh, $running );
if ( $running != $prev_running ) {
// получаю информацию о текущих соединениях
$info = curl_multi_info_read( $mh );
if ( is_array( $info ) && ( $ch = $info['handle'] ) ) {
// получаю содержимое загруженной страницы
$content = curl_multi_getcontent( $ch );
// тут какая-то обработка текста страницы
// пока пусть будет как и в оригинале - вывод в STDOUT
echo $content;
}
// обновляю кешируемое число текущих активных соединений
$prev_running = $running;
}
} while ( $running > 0 );
foreach ( $chs as $ch ) {
curl_multi_remove_handle( $mh, $ch );
curl_close( $ch );
}
curl_multi_close($mh);
?>
Далее, вряд ли в большинстве случаев будет достаточно просто выводить страницы в STDOUT. Тем более это происходит в произвольном порядке в зависимости от порядка реального скачивания (а не задания вызовами curl_multi_add_handle() ). Также, если скачивается большой объем, то нет смысла дожидаться получения всех страниц — можно уже начинать обрабатывать их по мере получения. Но и вариант с получением всех скопом также не стоит снимать со счетов.
Для этого: 1) реализую всё в виде функции, 2) введу параметр, задающий callback-функцию, которая будет вызываться для каждого полученного файла. Если callback не задан — применяется вариант с получением всех страниц сразу. Вот пример:
<?php
// пример простейшего callback'а. практически dummy-func.
function my_callback( $url, $content, $curl_status, $ch ) {
echo «Скачивание страницы [$url] »;
if ( !$curl_status ) {
echo «было успешным. текст страницы:\n$content\n»;
}
else {
echo «выполнилось с ошибкой #$curl_status: ».curl_error( $ch )."\n";
}
}
function http_load( $urls, $callback = false ) {
$mh = curl_multi_init();
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_HEADER, 0 );
// CURLOPT_RETURNTRANSFER - возвращать значение как результат функции, а не выводить в stdout
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_multi_add_handle( $mh, $ch );
}
// если $callback задан как false, то функция должна не вызывать $callback, а выдать страницы как результат работы
if ( $callback === false ) {
$results = array();
}
$prev_running = $running = null;
do {
curl_multi_exec( $mh, $running );
if ( $running != $prev_running ) {
// получаю информацию о текущих соединениях
$info = curl_multi_info_read( $ghandler );
if ( is_array( $info ) && ( $ch = $info['handle'] ) ) {
// получаю содержимое загруженной страницы
$content = curl_multi_getcontent( $ch );
// скаченная ссылка
$url = curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
if ( $callback !== false ) {
// вызов callback-обработчика
$callback( $url, $content, $info['result'], $ch );
}
else {
// добавление в хеш результатов
$results[ $url ] = array( 'content' => $content, 'status' => $info['result'], 'status_text' => curl_error( $ch ) );
}
}
// обновляю кешируемое число текущих активных соединений
$prev_running = $running;
}
} while ( $running > 0 );
foreach ( $chs as $ch ) {
curl_multi_remove_handle( $mh, $ch );
curl_close( $ch );
}
curl_multi_close( $mh );
// результаты
return ( $callback !== false ) ? true : $results;
}
$urls = array( «www.example.com», «www.php.net» );
// вариант простой выдачи
print_r( http_load( $urls ) );
// вариант с callback
var_export( http_load( $urls, my_callback ) );
?>
Вот уже гораздо интереснее. Важный момент: при callback 4ый параметр — дескриптор соединения $ch, а при выдаче результатов хешем — просто строковое описание возникшей ошибки (ну или пустая строка, если всё нормально). Почему? Потому что curl_error() требует передачи дескриптора, который закрывается в конце работы функции. Так что в callback он еще существует и мы можем его использовать, а вот в хеше он уже ничего ценного дать не может. Как вариант, строковые описания кодов ошибок можно взять тут.
Итак, идём дальше. Хочется вызывать функцию не только для массива ссылок, но и иметь возможность скачать ей единственную страницу. Для этого нужно добавить всего-то одну строчку:
<?php function http_load( $urls, $callback = false ) {
…
// даже если передан единственный параметр - считаю его элементом массива
// это аналог: $urls = is_array( $urls ) ? $urls : array( $urls );
$urls = (array)$urls;
.... ?>
Вот теперь можно качать ссылки по одной: http_load( 'google.com' ). Этакий возврат к истокам.
Потом мне потребовалось задавать много больше передаваемых заголовков для соединений. Указывать их по одному через curl_setopt() не практично. Лучше воспользоваться функцией curl_setopt_array. Переделываю и получаю (часть кода):
<?php
{ // общие для всех соединений заголовки
$ext_headers = array(
'Expect:',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9',
'Accept-Language: ru,en-us;q=0.7,en;q=0.7',
//'Accept-Encoding: gzip,deflate', // нужно потом распаковывать. ну его пока…
'Accept-Charset: utf-8,windows-1251;q=0.7,*;q=0.5',
);
$curl_options = array(
CURLOPT_PORT => 80,
CURLOPT_RETURNTRANSFER => 1, // возвращать значение как результат функции, а не выводить в stdout
CURLOPT_BINARYTRANSFER => 1, // передавать в binary-safe
CURLOPT_CONNECTTIMEOUT => 10, // таймаут соединения ( lookup + connect )
CURLOPT_TIMEOUT => 30, // таймаут на получение данных
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.1) Gecko/20090716 Ubuntu/9.04 (jaunty) Shiretoko/3.5.1',
CURLOPT_VERBOSE => 2, // уровень информирования
CURLOPT_HEADER => 0, // заголовок не получается
CURLOPT_FOLLOWLOCATION => 1, // следовать редиректам
CURLOPT_MAXREDIRS => 7, // максимальное число редиректов
CURLOPT_AUTOREFERER => 1, // при редиректе подставлять в «Referer:» значение из «Location:»
// CURLOPT_FRESH_CONNECT => 0, // каждый раз использовать новое соединение
CURLOPT_HTTPHEADER => $ext_headers,
);
}
function http_load( $urls, $callback = false ) {
global $curl_options;
$mh = curl_multi_init();
if ( $mh === false ) return false;
$urls = (array)$urls;
$chs = array();
foreach ( $urls as $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt_array( $ch, $curl_options ); // задаю заголовки скопом
curl_setopt( $ch, CURLOPT_URL, $url );
curl_multi_add_handle( $mh, $ch );
}
…
?>
Прикидываемся Огнелисом. Заголовки я прокомментировал. За подробным объяснением отправляю сюда.
И в догонку к этим заголовкам добавляется третий параметр в функцию:
<?php function http_load( $urls, $callback = false, $urls_params = array() ) {} ?>
в котором можно указывать свои заголовки, которые будут дополнительно преданы в соединения при их инициализации. Таким образом можно успешно отправлять POST запросы с параметрами либо указывать свои рефералы и формат передаваемых данных (при компрессии к примеру).
<?php
…
foreach ( $urls as $ind => $url ) {
$chs[] = ( $ch = curl_init() );
curl_setopt_array( $ch, $curl_options ); // задаю заголовки скопом
curl_setopt( $ch, CURLOPT_URL, $url );
// есть дополнительные параметры для инициализации данного соединения?
if ( isset( $urls_params[ $ind ] ) && is_array( $urls_params[ $ind ] ) ) {
curl_setopt_array( $ch, $urls_params[ $ind ] );
}
curl_multi_add_handle( $mh, $ch );
}
…
?>
Вот такая функция. Еще можно было бы написать про работу с куками и POST-запросами, но это уж если получу инвайт. И так понаписал много, многие ли осилили? ;)