Многопоточный парсер на PHP с использованием cURL и прокси-серверов

Однажды встала задача реализовать многопоточный парсер на php. Чтобы не изобретать велосипед, первое, чем я решил заняться — это анализом существующих решений, которые бы позволяли не вникать разработчику в процесс организации и управления очередью потоков, как это предлагает нам стандартный набор curl_multi, а позволили бы сосредоточится на работе над основным функционалом.

Для нетерпеливых: ссылка на реализацию класса AngryCurl на PHP

На тот момент лучшим вариантом решения подобной задачи, из мною найденных, стал класс RollingCurl:

http://code.google.com/p/rolling-curl/source/browse/#svn%2Ftrunk

С помощью данного класса можно сразу приступить к решению своих задач. Простейший пример использования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
$urls = array(
  // Массив страниц, информацию с которых необходимо получить
);
 
/**
 * Функция обработки результатов запроса
 * 
 * @param string $response
 * @param Info $info
 * @param Request $request
 * 
 * @return void
 */
function callback($response, $info, $request) {
  // Обработка результатов
}
 
$rc = new RollingCurl("callback");
$rc->window_size = 20; // Количество одновременных соединений
 
foreach ($urls as $url) {
    $rc->get($url); // Формируем очередь запросов
}
 
$rc->execute(); // Запускаем

Задача была успешно выполнена, результатом использования данного класса на PHP я остался доволен. Однако, спустя время старая задача приняла новые формы. Необходимо было парсить большой объём данных с удалённого ресурса. Изначально проблема решалось связкой TOR + WinHTTrack с последующей обработкой результатов, поскольку время не поджимало, а что либо писать самому не хотелось. Однако, в последствии, владельцы ресурса позаботились о бане «ботов» по IP на основе данных о заголовках запроса и частоте обращений. Тут и встала идея написания улучшенной версии RollingCurl, получившей название AngryCurl.

Идея была в следующем: сохранив функционал предшественника, дать возможность программисту, не задумываясь об организации многопоточности на PHP, имея на руках список proxy-серверов и идентификаторов браузера (useragent), на лету переключаться между proxy/useragent, предварительно проверив прокси на доступность. Данного функционала вполне достаточно для обхода простеньких систем защиты. Помимо этого, необходимо было решить проблему того, что прокси-сервера могут отдавать неверных контент (например, в случае, если сайт содержит запрещенный контент), либо отдавать контент частично.

Результатом работы стал класс AngryCurl, доступный на GitHub’е (ссылка). На текущий момент его возможности:

  • загрузка proxy-list’a из файла/массива
  • проверка работоспособности прокси
  • проверка отдаваемого прокси-сервером контента
  • загрузка useragent-list из файла/массива
  • подмена proxy/useragent «на лету»
  • режим «веб-консоли»
  • логирование действий

Предложения по улучшению функциональности можно и нужно оставлять в разделе Контакты.
Пример использования AngryCurl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
 
ini_set('max_execution_time',0);
ini_set('memory_limit', '128M');
 
require("RollingCurl.class.php");
require("AngryCurl.class.php");
 
function nothing($response, $info, $request)
{
    if($info['http_code']!==200)
    {
        AngryCurl::add_debug_msg("->\t".$request->options[CURLOPT_PROXY]."\tFAILED\t".$info['http_code']."\t".$info['total_time']."\t".$info['url']);
        return;
    }else
    {
        AngryCurl::add_debug_msg("->\t".$request->options[CURLOPT_PROXY]."\tOK\t".$info['http_code']."\t".$info['total_time']."\t".$info['url']);
        return;
    }
    // Здесь необходимо не забывать проверять целостность и валидность возвращаемых данных, о чём писалось выше.
}
 
$AC = new AngryCurl('nothing');
// Включаем принудительный вывод log'f без буферизации в окно браузера
$AC->init_console(); 
// Задаем количество потоков
$AC->__set('window_size', 200); 
 
//Загружаем список прокси-серверов, задаем regexp и url для проверки работоспособности
$AC->load_proxy_list('./lib/proxy_list.txt','http','http://google.com','title>G[o]{2}gle'); 
// Загружаем список useragent
$AC->load_useragent_list('./lib/useragent_list.txt'); 
 
// Устанавливаем флаг использования proxy-list'a
$AC->__set('use_proxy_list',true); 
// Устанавливаем флаг использования useragent-list'a
$AC->__set('use_useragent_list',true); 
 
// Организуем очередь запросов
$AC->get('http://ya.ru');
$AC->get('http://ya.ru');
$AC->get('http://ya.ru');
 
// Запускаем
$AC->execute();
 
// Вывод лога при выключенном console_mode
//AngryCurl::print_debug();

Комментарии (4) - Комментировать

  1. naive:

    Отписывайтесь об ошибках, откровенных недочетах, предложениях по улучшению и пр.

  2. Maxim:

    Было бы лучше сделать тест прокси перед самим парсингом страницы. Если парсер, например, будет работать в течении часа, многие прокси, которые тестируется в самом начале скрипта, могут отвалится.
    Хотя конечно можно тестировать прокси перед каждой группой запросов, но тогда это займет время. Т.к. если в пркоси листе будет больше 1000 прокси, то для их теста потребуется время, что не есть хорошо.
    Гуманней брать прокси, проверить его перед самим запросом страницы, если не рабочий взять другой.

    Плюс не увидел функцию очистки очереди URL. Например, добавил 150 урл, они отработались, а потом нужно добавить еще 100. В текущей версии скрипт понимает это как 150+100 и обработает 250.

    • naive:

      Касательно проверки прокси:
      Думал об этих же вопросах с самого начала, однако, как Вы заметили — проверка прокси перед каждым запросом — это дополнительная излишняя нагрузка.
      Более того, в итоге я пришёл к следующему выводу: работа с прокси-листами ( с учётом задач, которые решаются с их помощью ) сопряжена с различного рода потерями информации / доступности / искажениями, что я попытался, тем не менее, предусмотреть. Поэтому, в любом случае, имеет место вероятностный характер получения валидных конечных данных.

      В случае работы с большим количеством url-ов — важна итоговая скорость работы скрипта + мы в любом случае вынуждены делать несколько «прогонов» скрипта для получения 100%-ой целостности полученных данных. Помимо этого проверять работоспособность прокси лучше на том же домене (а в некоторых случаях и странице), что и целевой запрос, для того, чтобы убедится, что домен не блокируется, не срабатывает система защиты, информация не искажается прокси итп, — что в случае проверки прокси перед каждым запросом может дать обратный эффект, когда различного рода IDS заметят «похожести» поведения и парные запросы. Ввод различного рода задержек и искажений — необосновано усложняет задачу и не даёт итогового преимущества. В задачах, в которых использовался данный класс, проверка прокси перед каждым запросом была недопустима.

      Большие списки, как Вы заметили, можно разбивать на подгруппы, при необходимости. Что касается примера с 1000 прокси, то количество прокси должно определятся количеством запросов и потоков. Если группы маленькие — без надобности использовать на порядки большее количество проксей.

      При маленьком же списке url-ов с высокой долей вероятности прокси не успеют «умереть».

      Касательно очистки очереди:
      Задавался этим вопросом в отношении RollingCurl в момент написания. Честно сказать, уже не вспомню к каким выводам и рассуждениям я тогда пришёл.
      Если посмотреть тут: http://code.google.com/p/rolling-curl/source/browse/trunk/example.php, то подразумевается, что «один instanсe — один execute».
      Честно сказать, сейчас не приходит в голову практическое применение, при котором невозможно было обойтись без очистки очереди.
      Ну и как видно здесь: http://code.google.com/p/rolling-curl/source/browse/trunk/RollingCurl.php

      /**
      * @var Request[]
      *
      * The request queue
      */
      private $requests = array();

      - собственно не хотелось изменять чужой класс.

      В любом случае, обдумаю оба вопроса ещё раз. Спасибо за комментарий.

  3. simon:

    Неплохая реализация. Будем пробовать.

Оставить комментарий


− 9 = ноль