Парсер, парсер, парсер
Который год пишу парсеры, и знать ничего не знал про многопоточность. В продвинутых компилируемых языках типа C++, Delphi и даже в старом добром, интерпретируемом Perl, многопоточность одна из главных составляющих. Никто бы не стал пользоваться приложением, если бы его окно зависало во время больших вычислений. Программистам PHP повезло меньше. Этот язык вырос из шаблонизатора, и каким бы он не был удобным по сути шаблонизатором и остается. Многопоточности в нем попросту НЕТ. Наверно поэтому большинством уже упомянутых сиплюсоидов и делфистов считают PHP недоязыком.
Однако порой наступает случай, когда запуск одного и того же PHP скрипта, одновременно с разными параметрами, здорово увеличивает производительность. И многопоточные парсеры тому пример. Однако стоит опять упомянуть, что потоков в языке нет, и все что будет изложено ниже, это всего лишь псевдо мультипоточность. Все дело в том, что запросов к серверу донору можно посылать хоть сколько, лишь бы канал работал. А потом, в цикле основного потока, проверять какой из запросов отработал. Вот и все чудеса. Обработка данных происходит также в одном потоке, а вот их прием и пересылка в нескольких. Это значит, что в момент пока запрос №2 еще не завершился, а №1 уже вернул результат, мы можем обрабатывать его труды.
Работать мы будем с библиотекой curl, так что самой второй строчкой вашего парсера должна быть этакая проверка
extension_loaded('curl') or die('cURL неустановлен:(');
Теперь с чистой совестью инициализируем мултипоточный cURL
$mh = curl_multi_init(); // $mc - это multi handle :) во всех примерах так называют, handle это типа ссылки на выделенный на поток ресурс, ай не заворачивайтесь
где-нибудь на предпоследней стречке сразу можем написать
curl_multi_close($mh);
вот и все парсер готов. Уходим пить пиво.
А нет, чего-то мы забыли. Ну конечно! Ему еще надо закинуть дровишек.
Каждый новый поток подключает функция
curl_multi_add_handle($mh,$ch);
которая в качестве параметров получает наш уже созданный мултихандл($mh) и вы не поверите монохандл($ch) тобишь один из потоков
Это и есть наш поток, а создается он точно также как и обычная cURL сессия.
$ch = curl_init(); curl_opt($ch,...);
вот только она не запускается. Т.е. curl_exec использовать не надо
скомпонуем все в один скрипт
extension_loaded('curl') or die('cURL неустановлен:('); $mh = curl_multi_init(); $ch = curl_init(); curl_opt($ch, CURLOPT_URL, "http://google.com"); curl_setopt ($ch, CURLOPT_HEADER, 0); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_multi_add_handle($mh,$ch); curl_multi_close($mh);
Этот скрипт пока ничего не делает. curl_exec мы не использовали, а значит, нам еще предстоит запустить все потоки. Делает это аналог curl_exec из мира multi
curl_multi_exec($mh, $active);
И делается это очень хитрыми циклами, по сути вся мультипоточность здесь и сосредоточена. От того, как Вы реализуете эту часть кода, и будет зависеть Ваш выигрыш в производительности.
Большинство примеров на том же php.net предлагают такой вариант:
do { $mrc = curl_multi_exec($mh, $active); } while ($active>0);
тут всего одна функция curl_multi_exec, которая и запускает все соединения и делает это в цикле до тех пор, пока они все не завершаться. Во второй параметр она возвращает количество еще не отработавших соединений. После этого просто читаем пришедшие данные с помощью функции curl_multi_getcontent($ch); Вот и все, данные получены.
Еще, чтобы не вешать процессор по самые уши циклом, иногда советуют вставлять в этот цикл usleep(100); В итоге, если у Вас будет дохлый прокси или слабый канал такой цикл будет работать до тех пор, пока не завершится самое медленное соединение.
Ну и где хваленая многопоточность?! - спросите вы, раз в итоге мы все равно ждем, пока завершатся все потоки. Во-первых, стоит все же в защиту этого цикла упомянуть, что 10 одновременных запросов при толстом канале, отработают быстрее, чем 10 последовательных. Ну а во вторых неужели вы подумали, что я бы назвал этот неказистый цикл хитрым :)
Вот цикл поинтереснее, точнее даже два:
$running=null; //просто запускаем все соединения while( ($mrc = curl_multi_exec($mh, $running))==CURLM_CALL_MULTI_PERFORM ); while($running && $mrc == CURLM_OK){ if($running and curl_multi_select($mh)!=-1 ){ do{ $mrc = curl_multi_exec($mh, $running); // если поток завершился if( $info=curl_multi_info_read($mh) and $info['msg'] == CURLMSG_DONE ){ $ch = $info['handle']; // смотрим http код который он вернул $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); // и собственно что он вернул $data=curl_multi_getcontent($info['handle']); curl_multi_remove_handle($mh, $ch); // удаляем наше соединение из стека curl_close($ch); // закрываем его } }while ($mrc == CURLM_CALL_MULTI_PERFORM); } usleep(100); }
Тут стоит обратить внимание на две функции curl_multi_select и curl_multi_info_read.
int curl_multi_select ( resource $mh [, float $timeout = 1.0 ] )
Эта функция блокирует вызывающий процесс, пока не будет активности на любом из соединений, или до того как истечет время ожидания. Другими словами, он ждет данные, которые будут получены из открытых соединений. Время ожидания подается вторым параметром в секундах.
Функция останавливается в 3 случаях:
1. Обнаружена подача данных на одном из сокетов;
2. Тайм-аут закончился (второй параметр);
3. Завершился сам процесс.
Функция возвращает целое число:
В случае успеха она возвращает число, как правило, 1. Я полагаю, она возвращает количество соединений обнаруженных завершенных соединений. Если таймаут истекает, она возвращает 0. В случае ошибки возвращается -1.
Вторая функция
array curl_multi_info_read ( resource $mh [, int &$msgs_in_queue = NULL ] );
проверяет, завершилось ли какое-нибудь из соединений. Вторым параметром возвращает количество еще не отработавших соединений.
Функция возвращает массив, содержащий указатель на завершенное соединение, т.е. в нашем случае либо $ch1, либо $ch лежит он тут $info['handle']. Далее с ними можно делать все что угодно. В примере мы смотрим на http код, который вернул сервер и если код равен 200, то смотрим присланные данные с помощью функции curl_multi_getcontent($info['handle']);
Мы повторно запускаем curl_multi_exec в цикле потому, что должны знать какой из потоков уже выполнил свою задачу.
Опять же скомпонуем полученный код
$mh = curl_multi_init(); $ch = curl_init(); $ch1 = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://www.google.ru"); curl_setopt ($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); curl_multi_add_handle($mh,$ch); curl_setopt($ch1, CURLOPT_URL, "http://xdan.ru"); curl_setopt ($ch1, CURLOPT_HEADER, 0); curl_setopt($ch1, CURLOPT_TIMEOUT, 10); curl_setopt ($ch1, CURLOPT_RETURNTRANSFER, 1); curl_multi_add_handle($mh,$ch1); $running=null; //просто запускаем все соединени while( ($mrc = curl_multi_exec($mh, $running))==CURLM_CALL_MULTI_PERFORM ); while($running && $mrc == CURLM_OK){ if($running and curl_multi_select($mh)!=-1 ){ do{ $mrc = curl_multi_exec($mh, $running); // если поток завершился if( $info=curl_multi_info_read($mh) and $info['msg'] == CURLMSG_DONE ){ $ch = $info['handle']; // смотрим http код который он вернул $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); // и собственно что он вернул $data=curl_multi_getcontent($info['handle']); curl_multi_remove_handle($mh, $ch); curl_close($ch); } }while ($mrc == CURLM_CALL_MULTI_PERFORM); } usleep(100); }
Это прототип нашего многопоточного парсера. Как вы понимаете, количество $ch может быть любым. Четких ограничений в документации нет.
Более подробную документацию на русском языке можно прочитать тут http://docs.php.net/manual/ru/book.curl.php, это переведенная документация с http://www.php.net/manual/en/book.curl.php , но без комментариев, в комментарии на php.net это самое ценное.
Те кто не верит что мультипоточный curl дает значительный прирост в скорости могут убедится в этом на моем примере. В архиве два файла, в одном скачиваются в два потока старинца гугла и страница моего сайта index.php, во втором once.php страницы парсятся обычным последовательным curl_exec.
Время на две страницы мультипоток: 0.495147943497 сек
время на две страницы последовательный curl_exec: 0.744149923325 сек
Даже тут с разницей всего в два потока мы видим, прирост почти в два раза.
В следующей статье опишу как применить эти знания на практике и написать настоящий non-stop парсер в нескольких потоках, с дозаправкой на ходу новыми url-ами.
Комментарии
Когда же ждать продолжения статьи?
Ну ооочень интересно!
И да, конечно спасибо за эту статью - мне очень помогло!
А автору блога пожелайте удачной службы!
Попробуйте сделать хотя бы 5 потоков и поймете, что из 5 потоков данные в итоге будут получены с 3, а 2 просто "потеряются"
о чем думало государство когда его забирали, люди продолжения статьи ждут
if($running and curl_multi_select($mh)!=-1 )
сразу переходит на 33
usleep(100);
В переменной $mrc (строка 16) "0".
интересует как в поток добавлять новые задания url-ы?
Меня интересует возможно ли создать модуль
или модули которые смогли помочь мне
реализовать сценарий регистрации у меня на
сайте партнеров с одновременным появлением
меток с их логотипами по указанным ими
адресам на карте .
После регистрации создовался бы материал,
страничка сайта(заполнение анкеты) которая раскрывалась при
клике по метке на карте.
В идеале нужно сделать так чтобы метки
появлялись только после оплаты мне на яндекс
кошелек денег и исчезали сами по прошествии
года, оплата в два тарифа с активной ссылкой
на страничке на сайт партнера и без нее.
Для посетителей сайта нужно создать
возможность забить свой адрес и получить в
итоге вид карты в центре которой его дом, а
по краям несколько улиц (маштабирование и
ненужные функции отключены) не найдя на них
компаний моих партнеров
посетитель просто передвигает карту пока не
найдет ближайший адрес.
С уважением Илья
скайп: hurghadahome