Парсер, парсер, парсер

Который год пишу парсеры, и знать ничего не знал про многопоточность. В продвинутых компилируемых языках типа 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-ами.

Оставлять комментарии могут только зарегистрированные пользователи

Комментарии  

Алексей
# Алексей 29.07.2011 14:53
"В следующей статье опишу как применить эти знания на практике и написать настоящий non-stop парсер в нескольких потоках, с дозаправкой на ходу новыми url-ами."



Когда же ждать продолжения статьи? :-)



Ну ооочень интересно!



И да, конечно спасибо за эту статью - мне очень помогло!
Yagnenok
# Yagnenok 31.07.2011 11:55
Ждать придется как минимум до 1 июня 2012 года, так как автор блога отдает долг родине в армии ))
Алексей
# Алексей 31.07.2011 18:35
Вон оно как.. Ну тогда ждем продолжения в любом случае!



А автору блога пожелайте удачной службы! :-)
Вепрь
# Вепрь 03.08.2011 06:10
"Как вы понимаете, количество $ch может быть любым. "



Попробуйте сделать хотя бы 5 потоков и поймете, что из 5 потоков данные в итоге будут получены с 3, а 2 просто "потеряются"
Денис
# Денис 30.10.2011 21:21
Вепрь, что бы этого не происходило, нужно вторым параметром в curl_multi_select($mh) указать время тимеоюта поболее, например 30 (по умолчанию секунда и некоторые урлы не успевают ответить) или вообще убрать это условие: $running and curl_multi_select($mh)!=-1
Евгений
# Евгений 30.06.2012 00:26
ну что там, отслужил автор нет? ;)

о чем думало государство когда его забирали, люди продолжения статьи ждут
Евгений
# Евгений 28.10.2012 19:33
Ну что? отдал долг родине?
Leroy
# Leroy 29.10.2012 14:13
ну так-то в июне еще отдал)
Wlad
# Wlad 06.11.2012 01:49
Подскажите, пожалуйста, почему у меня скрипт в кому впадает? А именно, никогда не совпадает условие в строке 18

if($running and curl_multi_select($mh)!=-1 )

сразу переходит на 33

usleep(100);

В переменной $mrc (строка 16) "0".
panha
# panha 24.01.2013 19:05
thanks
Артем
# Артем 21.02.2013 22:16
А слышали про pcntl_fork()?
Leroy
# Leroy 22.02.2013 11:26
Насколько помню работы с потоками в пхп нет, только если ставить какие-то дополнения.
altRUist
# altRUist 24.01.2014 01:28
статья очень хорошо написано, сразу прояснились эти циклы которые не мог понять на php.net спасибо автору, и тоже очень жду продолжения!

интересует как в поток добавлять новые задания url-ы?
aNyaNya
# aNyaNya 20.08.2014 02:29
то шо надо
Leroy
# Leroy 20.08.2014 12:30
есть новая версия http://xdsoft.net/jqplugins/flipcountdown/
vad1979
# vad1979 20.11.2014 01:57
Здравствуйте.





Меня интересует возможно ли создать модуль

или модули которые смогли помочь мне

реализовать сценарий регистрации у меня на

сайте партнеров с одновременным появлением

меток с их логотипами по указанным ими

адресам на карте .

После регистрации создовался бы материал,

страничка сайта(заполнение анкеты) которая раскрывалась при

клике по метке на карте.

В идеале нужно сделать так чтобы метки

появлялись только после оплаты мне на яндекс

кошелек денег и исчезали сами по прошествии

года, оплата в два тарифа с активной ссылкой

на страничке на сайт партнера и без нее.

Для посетителей сайта нужно создать

возможность забить свой адрес и получить в

итоге вид карты в центре которой его дом, а

по краям несколько улиц (маштабирование и

ненужные функции отключены) не найдя на них

компаний моих партнеров

посетитель просто передвигает карту пока не

найдет ближайший адрес.





С уважением Илья



скайп: hurghadahome
Рома$
# Рома$ 21.01.2018 11:49
Спасибо помогло
Ram
# Ram 24.01.2018 19:17
Спасибо
Владислав
# Владислав 08.01.2019 18:31
Да, статься очень крутя прям помогла!
RES
# RES 21.01.2020 06:25
Отличная статья. Реализовал - все получилось!