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

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

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

На тот момент лучшим вариантом решения подобной задачи, из мною найденных, стал класс Rolling Curl:
http://code.google.com/p/rolling-curl/source/browse/#svn%2Ftrunk

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

$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 на основе данных о заголовках запроса и частоте обращений. Тут и встала идея написания улучшенной версии Rolling Curl, получившей название AngryCurl.

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

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

  • многопоточность
  • загрузка proxy-list из файла/массива
  • удаление дубликатов прокси-серверов
  • проверка работоспособности proxy
  • проверка отдаваемого прокси-сервером контента
  • загрузка useragent-list из файла/массива
  • подмена proxy/useragent «на лету»
  • предотвращение установления прямых соединений без использования proxy/useragent, при использовании списков
  • работа с callback function
  • работа с цепочками запросов
  • режим «веб-консоли»
  • логирование действий

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

Парсер на php — пример использования AngryCurl:

ini_set('max_execution_time', 0);
ini_set('memory_limit', '128M');

require("RollingCurl.class.php");
require("AngryCurl.class.php");

# Определение функции, вызываемой при завершении потока
function callback_function($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('callback_function');
# Включаем принудительный вывод логов без буферизации в окно браузера
$AC->init_console(); 

# Загружаем список прокси-серверов из /import/proxy_list.txt
# Опционально: задаем количество потоков, regexp и url для проверки работоспособности
# Можно использовать импорт из массива: $AC->load_proxy_list($proxy array);
$AC->load_proxy_list(
    # путь до файла
    'proxy_list.txt',
    # опционально: количество потоков
    200,
    # опционально: тип proxy (http/socks5)
    'http',
    # опционально: URL для проверки proxy
    'http://google.com',
    # опционально: regexp для проверки валидности отдаваемой прокси-сервером информации
    'title>G[o]{2}gle'
);
# Загружаем список useragent из /import/useragent_list.txt
$AC->load_useragent_list( 
    'useragent_list.txt'
);

# Организуем очередь запросов (варианты дополнительных возможностей организации запросов -
# POST, HEADERS, OPTIONS - можно посмотреть в примере extended_requests в папке demo
$AC->request('http://ya.ru');
$AC->get('http://ya.ru');
$AC->post('http://ya.ru');

/**
 * Можно использовать следующие конструкции для добавления запросов:
 *  $AC->request($url, $method = "GET", $post_data = null, $headers = null, $options = null);
 *  $AC->get($url, $headers = null, $options = null);
 *  $AC->post($url, $post_data = null, $headers = null, $options = null);
 * - где:
 *  $options - cURL options, передаваемые в  curl_setopt_array;
 *  $headers - HTTP заголовки, устанавливаемые CURLOPT_HTTPHEADER;
 *  $post_data - массив передаваемых POST-параметров;
 *  $method - GET/POST;
 *  $url - собственно сам путь до страницы, которую необходимо "спарсить".
*/

# Задаем количество потоков и запускаем
$AC->execute(200);

# Вывод лога при выключенном console_mode
//AngryCurl::print_debug(); 

Другие примеры расположены в папке demo с исходниками php парсера.

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

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

    Кстати, буду признателен, если кто-нибудь, в качестве «Спасибо», — поделится инвайтом на habrahabr.ru 🙂
    login: 2naive, mail: to.naive[at]gmail.com

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

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

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

      В случае работы с большим количеством 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. Как я понял, Вы писали парсер гугла.
    Мне нужно слить все урлы по 320к запросам в гугле. Как это сделать быстро ума не приложу. Знаю что нужен антигейт+ 100500 прокси.
    Если у вас есть решения, поделитесь пожалуйста.

    1. Например, здесь (если не ошибаюсь):
      http://proxytime.ru
      можно за символическую плату получить доступ к >3000 прокси-серверам.
      Поискав на просторах интернета, думаю, можно найти достаточное количество прокси-серверов.

      При наличии списка проксей всё-равно придётся проверять валидность возвращаемого контента.

      Год назад Гугл выдавал каптчу при попытке просмотре результатов >100.
      После ~100 обращений с одного ip в короткий период времени отдается 503.

      В итоге, если предположить, что по 100 результатов для ~100 ключевиков можно обработать с помощью одного валидного прокси-сервера за короткий промежуток времени, то приходим к следующему: ~3 200 прокси серверов будет достаточно для получения первых 100 результатов с 320 000 ключевиков.

      Соответственно, при увеличении числа необходимых результатов — кратно увеличивается либо время задержки, либо количество прокси-серверов.

      Это то, что смог ответить навскидку.
      Изначально парсер писался не для Гугла, а в случае с Гуглом задача была менее масштабна.

    1. В конструктор передаётся имя callback-функции, которая вызывается по факту обработки запроса.

      У функции три параметра, подробнее про них можно почитать в мануале RollingCurl. Одним из параметров является $response. Собственно его и нужно использовать для получения доступа к результатам.

      Пример реализации функции:

      function request_callback($response, $info, $request) {
      // parse the page title out of the returned HTML
      if (preg_match(/* regexp here */, $response, $out)) {
      $result = $out[1];
      }
      echo $result;
      }

  4. Первый раз решил прочитать README (в загруженных файлах) перед тем как что-либо делать, и тут такое: «Информация появится позднее» :).
    Спасибо автору за статью!

    1. Алексей, спасибо за отзыв.

      Извиняюсь, что небрежно отнесся к README-файлу, тому было несколько причин:
      — большинство проектов переехали с github на bitbucket в закрытые репозитории (скоро сменю ссылку с github’a);

      мне показалось достаточным:
      — описание к RollingCurl;
      — подробнейшие комментарии в коде AngryCurl;
      — говорящий сам за себя пример example.php.

      — более того, счёл, что большинство людей будут переходить отсюда, прочитав текст, а так же имеют определенный уровень и README им будет не интересен.

      Тем не менее, постараюсь в ближайшее время исправить этот недостаток.

      Пишите, если будут какие-либо комментарии и недостатки.
      Спасибо.

  5. Прекрасная реализация автору Спасибо ! 600 потоков полет нормальный)
    Единственное что хотелось бы заметить, для формирования запроса лучше использовать mt_rand вместо rand.

    И раз уж тут вы используете RollingCurl хотелось бы спросить как вы поддерживаете сессию ? … тесть мне нужно пройти авторизацию на сайт затем пройти по ссылке и только потом парсить . Я понимаю что это нужно делать это под одним прокси и кукисами. Как заставить работать RollingCurl (по сценарию).

    https://github.com/takinbo/rolling-curl.git
    Вот нашол реализацию RollingCurlGroup.php пока что не разобрался как он работает, но мне кажется это то что мне нужно. но пример с группами у меня не работает 🙁

    1. Спасибо за отзыв) я сейчас в отпуске, по возвращению (в эти выходные),отвечу на Ваше сообщение подробно.

  6. спасибо за оперативность вы очень быстро реагируете …. но проблему я не решил хотя пробывал уже многое 🙂
    Может быть кто то даст ссылку готовый парсер с регистраыией на Злом курле или на Прокатывающем (rolling curl) буду благодарен даже за наводку. Спс

    1. В чём именно кроется проблема?
      В простейшем случае, для имитации авторизации достаточно установки соответствующих cookies и следования редиректам, какая сложность у Вас?

      Вы можете:
      1) посмотреть примеры имитации авторизации для простого cURL, принципы и параметры — те же;
      2) кинуть линк на сервис, в котором необходимо авторизироваться;
      3) написать мне в ICQ: 4772O4, — попробую помочь.

      Скинуть примеры авторизации, к сожалению, на текущий момент не могу, тк проблемы с RDP к компу, на котором они лежат.

      ps: не накруткой ли занимаетесь ? 😉

  7. Tак уж случилось что когда используешь списки проксей там много парных, было даже как то так что из 3к проксей уникальных было только 700 🙂
    И тут нам поможет:
    array_unique — Убирает повторяющиеся значения из массива

    1. так как этот класс заточен под прокси думаю стоит ввести

  8. Заменено:
    rand => mt_rand

    Добавлено:
    array_unique для proxy (хотя мне и кажется, что это слегка лишнее, но пусть будет)

  9. Здравствуйте. Хотелось бы уточнить: можно ли налету добавлять урлы для парсинга? конкретнее: Реально ли после проверки $response в callback функции отправить урл обратно в обработчик командой $AC->get(‘http://ya.ru’);

    PS стучу к вам в асю. не могу попасть. Если будет время, отпишите в 702-121

    1. День добрый.

      Все зависит от того, какую именно нужно решить задачу.
      Я планирую в будущем дописать возможность парсинга цепочки адресов для одного прокси.
      Ранее такой задачи у меня не стояло, как я понимаю, в RollingCurl — тоже.

      Тем не менее, если Ваша задача — пройтись по списку из N адресов, передать информацию из результатов в новый список M адресов и пройтись уже по нему,- Вы можете сделать так:


      $AC1 = new AngryCurl('callback1');
      $AC2 = new AngryCurl('callback2');

      function callback1($response, $info, $request)
      {
      global $AC2;
      /* обработка первой серии запросов */
      $AC2->get( /*адрес на основе $response*/);
      }

      function callback2($response, $info, $request)
      {
      /* обработка второй серии запросов */
      }

      $AC1->execute();
      $AC2->execute();

      Можете также посмотреть:
      — реализацию метода filter_alive_proxy в AngryCurl;
      — мануал RollingCurl;
      — вызвать конструктор и деструктор внутри callback для каждого отдельно взятого URL из response;

      function callback($response, $info, $request)
      {
      /* обработка первой серии запросов */
      $AC2 = new AngryCurl(/* callback2 */);
      $AC2->get( /*адрес на основе $response*/);
      $AC2->execute();
      unset($AC2);
      }

      — прочитать про roupRollingCurl.

      PS: я сейчас снова нахожусь вдали от Родины, поэтому ICQ недоступно. Вы можете описать Вашу задачу в письме и отправить мне на почту (указана в комментариях к классу) — постараюсь Вам ответить предметно. Почту проверяю регулярно.

    2. $AC1 = new AngryCurl('callback1');
      $AC2 = new AngryCurl('callback2');

      Как при таком подходе избежать двойной проверки load_proxy_list ?

    3. Как вариант — используйте

      $AC->flush_requests();

      вместо нового экземпляра.

    4. Так не получиться, т.к. формирование $AC2->get( /*адрес на основе $response*/); идет внутри callback функции $AC1

    5. > $AC->flush_requests();
      Я так понимаю эта команда чистит то , что мы добавили через
      $AC->get();
      А как сменить callback функцию? На втором прогоне (для AC2) должна быть другая callback функцию

  10. Добрый день. Подскажите, почему на kodingen.com этот пример не работает, хотя локально работает нормально?

    1. Добрый день.
      Поясните более подробно, что именно Вы имеете ввиду?
      Вы запускаете на площадке сайта kodingen.com или пытаетесь парсить оттуда данные?
      Что не работает? Какие ошибки? С какими параметрами вы запускаете скрипт?

      При попытке парсить kodingen.com всё отрабатывает верно:
      # Console mode activated
      # Start loading proxies
      Removed duplicates: 0
      Proху type: HTTP
      Proху test RegExp: title>G[o]{2}gle
      Proху test URL: http://google.com
      Loaded proxies: 2
      # Start testing proxies
      # Running proху test connections
      1-> 41.35.45.81:8080 OK 200 19.859 http://www.google.com.eg/
      2-> 85.92.159.84:8080 FAILED 0 29.874 http://google.com
      Alive proxies: 1/2
      # Testing proxies finished in 39.89s
      # Start loading useragent list
      # Loaded useragents: 1004
      -> 41.35.45.81:8080 OK 200 5.601 https://kodingen.com/

    2. Благодарю за быстрый ответ.
      Да, я запускаю этот скрипт на площадке kodingen.com, он отрабатывает пару секунд и выдает пустую страницу, хотя, как я понимаю, должны хотя бы логи выводится, но их нет.
      init_console() включено, max_execution_time ставил от нуля до 50, количество потоков ставил от 10 до 300, список проксей присутствует. На локальном сервере все работает отлично, а вот на этом не хочет, хотя другие парсеры там работают.
      Еще я попробовал поставить после каждой функции метки echo «Test» — echo «Test5», чтобы посмотреть где прерывается работа, так вот, выводятся только две первые метки Test и Test1. Собственно тест1 находится тут:
      $AC = new AngryCurl(‘nothing’);
      echo «Test1»;
      //Включаем принудительный вывод log’f без буферизации в окно браузера
      $AC->init_console()

    3. 1. Попробуйте включить в конфиге php:
      display_errors и error_reporting
      Видимо php не отображает ошибки.

      2. Возможно так же проверить error_log.

      3. Попробуйте запустить скрипт в чистом виде:


      require("RollingCurl.class.php");
      require("AngryCurl.class.php");

      function nothing($response, $info, $request)
      {
      echo $response;

      }
      $AC = new AngryCurl('nothing');
      $AC->get('http://ya.ru');
      $AC->execute();

      4. Выложите куда-нибудь и скиньте ссылку, либо отправьте мне на почту phpinfo(). Возможно смогу подсказать что-то, видя конфиг.

    4. Привет. В общем, дела обстоят так, скрипт в чистом виде работает, но только если у $options[CURLOPT_FOLLOWLOCATION] поменять значение с 1 на 0 (видимо из за настроек сервера).
      Пример скрипта тоже работает, но только если в init_console() за комментировать строку @apache_setenv(‘no-gzip’, 1);, но в этом случае логи выводятся после того, когда когда скрипт полностью отработает, и при больших списках, это несколько неудобно.

      >Выложите куда-нибудь и скиньте ссылку, либо отправьте мне на почту phpinfo()
      http://zprox.koding.com/phpinfo.php

    5. Добрый вечер.
      Что касается CURLOPT_FOLLOWLOCATION — спасибо за информацию. Действительно, судя по информации stackoverflow и багтрекеру php — он выдает ошибку при одном из двух условий:
      safe_mode = On;
      open_basedir is set.

      Добавлю этот момент в описание, поскольку CURLOPT_FOLLOWLOCATION по умолчанию TRUE в AngryCurl (RollingCurl).

      Что касается no-gzip. Safe_mode должен быть отключён, однако, судя по Вашему phpinfo — это так и есть.
      Пишет ли что-либо в error_log? Если убрать @ — какую ошибку выдает?
      Скорее всего, проблема в том, что apache_setenv будет работать, только с mod_php (и, соответственно, не будет при CGI, как, судя по всему сейчас у Вас).
      Поэтому, для того, чтобы всё заработало, необходимо отключить gzip у Apache. Можно попробовать сделать это на уровне .htaccess в директории следующего содержания:


      <IfModule mod_env.c>
      SetEnv no-gzip dont-vary
      RemoveOutputFilter php
      </IfModule>

      Скажите, если метод сработает, — я включу описание этой проблемы и её решение в репозиторий AngryCurl.

      К слову, Вы всегда можете вызывать скрипт напрямую из консоли ssh.

  11. Привет, naive. В error_log ничего не пишет, он скорей всего отключен. Если убрать @, то выдает такую ошибку:
    Fatal error: Call to undefined function apache_setenv()
    К сожалению решение с .htaccess не помогло. Если решения не найдется, то можно в CURLOPT_CONNECTTIMEOUT установить небольшое значение, или же найти другой сервер:)

    1. Привет. В общем, предположение было верным apache_setenv() не отрабатывает.

      Сейчас нужно закомментировать apache_setenv и отключить gzip в Apache.

      Странно, что .htaccess не помог. Если это AWS, то .htaccess может не работать при базовых настройках или отсутствует mod_env. Попробуйте в конфиге Apache напрямую указать:

      SetEnv no-gzip 1

      (можно указать и для конкретного URI)

      ps: нажимайте Ответить на Ваш пост, дабы сохранять иерархию. Убрал вложенность > 2х уровней по причине неудобства шаблона

    2. Хотя нет, конфигурация .htaccess помогла, просто я использовал маленькие списки, а со списками ~1000 логи выдаются не сразу, а постепенно добавляются.
      naive, спасибо за помощь, и отдельно спасибо за скрипт.

    3. Буферизация отключается не сразу и разнятся размеры буфера.

      А так — не за что, обращайтесь с вопросам, предложениями по улучшению/исправлению недочётов или конкретными предложениями реализаций улучшений.

      Кстати, так же было бы интересно узнать о use cases, особенно если Вы работаете над koding.com (читал о нём статью на Хабре).

      ps: советую убрать phpinfo, который Вы для меня выкладывали.

    4. >Кстати, так же было бы интересно узнать о use cases, особенно если Вы работаете над koding.com (читал о нём статью на Хабре)
      Я туда совсем недавно перебрался, по этому всех возможностей не знаю. Но если вам интересна эта площадка, могу выслать инвайт.

    5. Про use cases я говорил в контексте того, какие задачи Вы решаете AngryCurl’ом 🙂

      Про koding — буду благодарен, интересно было бы попробовать.

    6. Привет, naive. Не нашел на сайте мыло куда инвайт отправить, по этому можно попробовать этот код twitterfriends. Если уже не работает, то напишете свое мыло и я вышлю инвайт.

    7. Привет. Мыло есть в комментариях к классу AngryCurl или можно просто отправить через
      🙂

    8. >Про use cases я говорил в контексте того, какие задачи Вы решаете AngryCurl’ом
      Ааа:) Ну сейчас AngryCurl’ом чекаю прокси, а в дальнейших планах, если примут закон запрещающий прокси, впн, тор, и т.д., сделать анонимайзер.

    9. Ну, я надеюсь, что эту муть не примут 😉 Всё же верю, в то, что не 146% в Думе деревянные))
      Если займётесь анонимайзером — скажите 😉

    10. >Мыло есть в комментариях к классу AngryCurl
      Точно. Помню, что где-то видел. Выслал.

    11. Добрый день, naive. Появилась еще одна проблема, на nginx при включенной функции print_debug(), и при больших списках, часто появляется 504 ошибка. Вроде бы header(‘HTTP/1.1 102 Processing’); может помочь в таки случаях, но я что-то не могу понять куда же вставить этот заголовок?

    12. Добрый день. Что говорят логи?
      Вообще, я специально убрал логгирование в debug_log по умолчанию, чтобы не засорять память на больших объёмах.
      Помимо прочего — о каких объёмах идёт речь?
      На какой итерации работы парсера падает с 504?
      Если Вы закомментируете print_debug() — скрипт парсера полностью отрабатывает?

    13. Привет.
      >Что говорят логи?
      Логи пхп отключены на той площадке, как я уже говорил выше, по этому не могу посмотреть их содержимое.
      >Помимо прочего — о каких объёмах идёт речь?
      Объемы листа 6-8к проксей.
      >Если Вы закомментируете в php print_debug()
      Да, если его закомментировать, и раскомментировать init_console(), то скрипт отрабатывает полностью.

      Вообще, задумка была такая, чтобы при включенном print_debug() появлялось окно сохранения тестового файла с проверенными проксями. С небольшими списками все работает отлично, прокси чекаются, в файл сохраняются. Хотя этот подход, может быть, в корне не верный?

    14. Вообще, задумка была такая, чтобы при включенном print_debug() появлялось окно сохранения тестового файла с проверенными проксями.

      Cтоп, а причем здесь вообще print_debug()? Он лишь делает вывод информации отладки, а не отфильтрованный список прокси?

      В общем, подход в корне неверен. Если я правильно Вас понял, то Вам нужно следующее:

      
      < ?php
      
      ini_set('max_execution_time',0);
      ini_set('memory_limit', '128M');
      
      require("RollingCurl.class.php");
      require("AngryCurl.class.php");
      
      class ProxyChecker extends AngryCurl {
          public function export_proxy_list()
          {
              header('Content-type: text/plain');
              header('Content-Disposition: attachment; filename="proxy_list.txt"');
              echo implode("\r\n", $this->array_proxy);
          }
      }
      
      $AC = new ProxyChecker();
      $AC->__set('window_size', 200);
      $AC->load_proxy_list('./lib/proxy_list.txt','http','http://google.com','title>G[o]{2}gle');
      $AC->export_proxy_list();
      

      И всё.

      Отпишитесь, если подходит.
      PS: Спасибо за идею — думаю, что нужно это добавить, может кому-то пригодится.

    15. >Cтоп, а причем здесь вообще print_debug()? Он лишь делает вывод информации отладки, а не отфильтрованный список прокси?
      Странно, у меня он выводит все тоже самое, что и init_console(), но не постепенно, а весь список сразу, правда стоит еще прошлая версия AngryCurl.

      >В общем, подход в корне неверен. Если я правильно Вас понял, то Вам нужно следующее
      Благодарю. Это вполне работает. Но опять же при 6-7к проксей, выскакивает 504 ошибка.

    16. Странно, у меня он выводит все тоже самое, что и init_console(), но не постепенно, а весь список сразу, правда стоит еще прошлая версия AngryCurl.

      Всё верно, но это именно log, а не вывод отфильтрованных прокси, который Вам нужен.
      А, поскольку, они хранятся в одном массиве, почему бы их не вывести?

      Благодарю. Это вполне работает. Но опять же при 6-7к проксей, выскакивает 504 ошибка.

      Честно говоря, — без идей в чем причина. На мой взгляд — нужно включать логи и смотреть.
      Либо, как вариант, использовать set_error_handler(), останавливать работу скрипта при ошибке и выводить сообщение.

  12. Внес изменения в README и вызов init_console()


    - @apache_setenv('no-gzip', 1);
    + # Internal Server Error fix in case no apache_setenv() function exists
    + if (function_exists('apache_setenv'))
    + {
    + @apache_setenv('no-gzip', 1);
    + }

  13. Было внесено пару изменений:
    — возможность включения/отключения логгирования в $debug_info в целях уменьшения потребляемой памяти (по умолчанию $debug_log=false в конструкторе);
    — проверка на наличие установленного cURL;
    — подсчет времени выполнения всех запросов при вызове execute();
    — предотвращение возникновения ситуации, когда use_proxy_list и use_useragent_list установлены, но на выходе, после проверки и фильтрации proxy/useragent, при alive_proxies/useragent = 0, — соединения проходили напрямую с текущего ip;
    — улучшение логгирования и вывода логов;
    — проверка на наличие apache_setenv();
    — добавлен .htaccess для PHP as CGI;
    — мелкие правки по тексту, комментариям, readme;
    — что-то ещё …

  14. Большое спасибо за скрипт.
    Есть в нём проблема: не отрабатывает задание до конца. Поставил я проверять больше 1000 страниц на наличие в них моего кода. По разному отрабатывал, когда 100, когда 200, когда 400 страниц проверял и завершался как бы штатно. Словно задания закончились. Полагаю, в функции rolling_curl по какой-то причине неправильно определяется завершение задания. Если много потоков поставить, то как бы незаметно, а я поставил 2 — баг стабилен.
    Ещё мелочь: При загрузке списка прокси грузится перевод строки, как отдельный прокси 🙂

    1. Доброй ночи.
      Не могли бы Вы пояснить проблему, сделать выкладки с кодом и конфигурацией сервера? Если честно, то не очень понятна ситуация, которую Вы описываете и ее причина.

      Вы используете прокси? Какой ресурс вы парсите? Каковы настройки сервера? Можно ли увидеть сам обозначенный кусок кода? Что говорят логи? Как вы определяете «штатность» завершения? Может просто выключен вывод ошибок и кончается max_execution_time?

      Для проверки Ваших слов создал локально (запускал без прокси) следующие файлы:

      github/angrycurl/test.php

      < ?php ini_set('max_execution_time',0); ini_set('memory_limit', '128M'); require("RollingCurl.class.php"); require("AngryCurl.class.php"); $AC = new AngryCurl('nothing'); $AC->init_console();
      $AC->__set('window_size', 2);

      $i=0;
      while($i<1000)
      {
      $AC->get("github/angrycurl/1.php?i=$i");
      $i++;
      }
      $AC->execute();

      function nothing($response, $info, $request)
      {
      static $i=0;
      $i++;
      if($info['http_code']!==200)
      {
      AngryCurl::add_debug_msg("$i->\t" . $request->options[CURLOPT_PROXY] . "\tFAILED\t" . $info['http_code'] . "\t" . $info['total_time'] . "\t" . $info['url']);
      return;
      }else
      {
      echo "Response: $response\r\n";
      AngryCurl::add_debug_msg("$i->\t" . $request->options[CURLOPT_PROXY] . "\tOK\t" . $info['http_code'] . "\t".$info['total_time'] . "\t" . $info['url']);
      return;
      }
      }

      github/angrycurl/1.php

      < ?php echo $_GET['i']; ?>

      Результат:
      http://stupid.su/files/AngryCurl_test_2_threads_1000.log_.txt
      — абсолютно ничего не выявил.

      Кстати, тестирование прокси перед подгрузкой ведётся теми же методами, что и основная работа с request’ами. Лично у меня, количество проверенных прокси всегда совпадало с загруженым (хотя я не использовал 2 потока).

      Про загрузку из файла — да, я знаю, осознанно не стал писать допонительных проверок на очевидные вещи, чтобы не громоздить код и из-за лени. Но, если руки дойдут — допишу. Или сами можете предложить коммит на гитхабе;)

  15. Спасибо за быстрый ответ. Отчасти проблема оказалась в старом Денвере, который не позволял по-человечески обрабатывать больше двух потоков 🙂 Поставил новый, теперь вроде всё нормально (нужно больше тестов). Но что интересно: заставил отрабатывать скрипт до конца и на старом Денвере, немного поменяв функцию rolling_curl.
    Было:
    if ($i requests) && isset($this->requests[$i]) && $i requests)) {
    $ch = curl_init();
    $options = $this->get_options($this->requests[$i]);
    curl_setopt_array($ch, $options);
    curl_multi_add_handle($master, $ch);

    // Add to our request Maps
    $key = (string) $ch;
    $this->requestMap[$key] = $i;
    $i++;
    }
    Стало:
    if ($i requests) && isset($this->requests[$i])) {
    $ch = curl_init();
    $options = $this->get_options($this->requests[$i]);
    curl_setopt_array($ch, $options);
    curl_multi_add_handle($master, $ch);

    // Add to our request Maps
    $key = (string) $ch;
    $this->requestMap[$key] = $i;
    $i++;
    $running=true;
    }
    Верну функцию к первоначальному виду (хоть и не понимаю смысла конструкции if ($i requests) && isset($this->requests[$i]) && $i requests)) { 🙂 ) и потестирую на новом Денвере. Будут проблемы — отпишусь.

    1. Немножко побился код при вставке, но в общем понятно.

    2. Добрый день.
      Вообще, я тоже не понял смысла, 13.10.2009 здесь:
      http://code.google.com/p/rolling-curl/source/detail?r=6

      - if ($i < sizeof($requests) && isset($this->requests[$i++])) {
      + if ($i < sizeof($requests) && isset($this->requests[$i++]) && $i < count($this->requests)) {

      добавляют 3-ую проверку, а потом 28.11.2009 тут:
      http://code.google.com/p/rolling-curl/source/detail?r=8

      - if ($i < sizeof($requests) && isset($this->requests[$i++]) && $i < count($this->requests)) {
      + if ($i < sizeof($this->requests) && isset($this->requests[$i++]) && $i < count($this->requests)) {

      — убирают с комментарием:

      $requests should have been $this->requests. oops.

      Наверное, это — баг.
      Оффтоп: вообще, уже задумываюсь о том, чтобы переписать RollingCurlи сделать AngryCurl независимым, раз тот всё-равно не поддерживают.

      Но, я всё-равно не понял, что именно вы изменили своим кодом? $running = true; — не должно никак влиять, с учётом того, что не зависит от внутреннего цикла.

      Если Вам все же удастся смоделировать/объяснить/описать/устранить проблему — было бы интересно.

  16. Hi, nice implementation i used rolling curl for long time and just recently was thinking on implementing something you did.
    There are are few issues as described above with proxy that die but honestly just as you mentioned speed over integrity!! is someone is concerend with integrity just log errors and reprocess those urls, maybe something like that could be implemented directly in the class also another possibility is to set a check flag after x number of calls to recheck proxies and filter the list

    1. Hi, Steven.
      Thank you for your response. About the issues you’ve mentioned — now I’m shure that I gonna deal with them in nearest future, because too much people face with this problems.

      Talking about solutions, I may comment, that I don’t think that your first offer is good (I mean implementing re-check of failed URLs directly in class). I don’t think that it worth it. Moreover, one can just implement that all in callback in the way one likes. Just re-add URLs to queue in callback-function and re-start AngryCurl after first call of execute() finishes.

      There are three solutions I accept:
      1. Almost like as you said — let user to set an interval in seconds or connections. After time passed or number of connections closed — re-check proxy list.
      2. Store an amount of fails (it’s better to store it in percents for some past time, for example — 3 of 10 failed for last 5 seconds or 10 connections, which is eq to 30% failure rate). So the user can set failure limit value at which re-check started.
      3. Count an amount of fails for each proxy and exclude it (or re-check proxy) if the amount exceeds the limit.

      I don’t want to make a huuuge php class and try to keep it simple.
      That’s why we have to choose just one solution. And I’ll implement it in the nearest future possible.

      Actually, I suppose that the best solution is the third one with re-checking proxy that fails too often.

  17. Hi there,
    bit of time has passed since my last post meanwile i did some heavy lifting with the class going trough million’s of urls, evaluating the results in such a big scale i honestly have to say that the class is just good as it is!
    I did implement callbacks after the first run storing a list of fails(this only for one project as the other one handled file downloads) and all thing that are discussed are simply relative to what you need to do!
    Once you are in high scale you simply know there will pe percentages of errors and simply deal with them at a later stage of whateven process you are dooing.
    Sorting the proxies is from my point of view now it’s not necessary during the process just adds overhead, simply start with 1000’s of pre tested proxies with some adequate software ie SB. and monitor the process sometimes, i like to just echo «+» or «-» and watch the nice design that gets created as it flies trought the urls.

    1. Hi, Steven.

      Thank you for your response.

      As you said:

      Once you are in high scale you simply know there will pe percentages of errors and simply deal with them at a later stage of whateven process you are dooing.

      I was thinking actually the same.

      But, nevertheless, I still think that I gonna implement the third solution described earlier, but I have so much other things to do 🙂 Thats why I will add this feature, but, as you said, because It shouldn’t influence on speed and must be tested and optimized — I need some more time to do this.

      Btw, when you said words «file» and «downloads» I remembered one more problem I was thinking about some time ago.
      When trying to download files with AngryCurl — useful thing could be this:

      curl_setopt ($ch, CURLOPT_FILE, $fp);

      to send file directly to file pointer and filesystem. But the problem is, that you have to open new file for writing each time before curl starts its job and send the pointer to Request options. In the same time there is a limit for an amount of simultaneously opened files on filesystem, that’s why you can’t just open N files for writing, add N Requests and start AngryCurl — you need to handle it inside the php class I suppose.
      I still want to solve it too in the future.

      Moreover, you said:

      simply start with 1000′s of pre tested proxies with some adequate software ie SB

      why It’s not suitable for you to use AngryCurl as proxy-checker? May be you could advise to add or change somthing?

      Thanks.

  18. Actually about proxy checking i did change my mind!
    Before i totally overlooked the power of the regex match, so now i just check the proxy against my custom proxy judge against my ip and that said it’s 1000000% faster than and proxy software i have tried. A word of cation for wo tries this to make sure the server you checking against can handle the extream calling.
    Tho reduce little the load on huge lists with 10000 more proxys, not caring wethere i get 100 more or less proxyies i’m writing the checked list to file and in consequent calls simply check if the file exists and use that as new list, that helps filtering out all those proxyies that are just death.
    For files and downloads obbviously having them in memory is not the best solution when they are big BUT big files require more bandwith therefore you would automatically reduce windows size making it feasable again in some sort of way all really depends what you want to do.
    so far i would say that thys two classes together make up a EXTREMLY powerfull php scraper, probably the Most powerfull.
    Love it!

  19. Доброго времени суток. Хороший класс. Спасибо большое за проделанную вместо меня работу 🙂 . Но есть одно пожелание. Поддержка куки и отправка пост запросов. А так, очень удобная штука у Вас получилась.

    1. Добрый день, Дмитрий. Спасибо за отзыв 🙂
      Функционал, о котором вы говорите — есть, я чуть позже распишу, как им пользоваться сюда, если вы не нашли этого.

  20. naive, а не подскажите, мельком глянул код, но так и не понял, в парсере идет замещаемость соединений? Т.е. пока последний запрос из группы запросов не обработается, к следующей группе не переходим.

    1. Как только освобождается один из числа заданного количества «потоков» — сразу стартует новый.
      Подробнее здесь

    2. naive, а не подскажите еще, как налету можно реализовать перезагрузку не отвечающих соединений? Или лучше плохие ответы собрать в массив и заново «прогнать»?

    3. При большом количестве запросов лучше собрать в массив/записать в БД и пройтись ещё раз для того, чтобы ускорить и упростить работу php парсера.
      Если это всё же необходимо — возможно инициализировать новый экземпляр класса AngryCurl в callback function, добавив запрос, который необходимо повторить. При вызове execute() автоматически запустится метод single_curl() класса RollingCurl.

    4. Спасибо, что-то голова совсем последнее время не работает :). Как раз немного плохих данных получается, поэтому проверка в 1 поток пойдет. Кстати, в AngryCurl в execute почему-то нет возврата значений:
      $output = parent::execute();
      return $output;

    5. Спасибо за замечание — будет исправлено в следующем коммите php парсера.

    6. Как организовывать цепочки запросов php парсера и повторно использовать инстанс класса AngryCurl — теперь подробно описано здесь:
      github.

    7. Спасибо, буду ковыряться.
      Кстати, сразу попробовал пример, на Денвере не захотел require_once и @fopen открываться, сделал через
      $_SERVER[‘DOCUMENT_ROOT’].DIRECTORY_SEPARATOR . ‘classes’

    8. Можно версии (Денвера, php итп), а так же информацию об ошибке?

    9. Проблема в __DIR__, доступной только при PHP >= 5.3, заменено на dirname(__FILE__) для совместимости.

    10. naive, denwer от 20-го июня сего года (Apache/2.2.22 (Win32) mod_ssl/2.2.22 OpenSSL/1.0.1c PHP/5.3.13). Попробовал, все равно ругается

      Warning: require_once(Z:\home\nprog.ru\www\..\classes\RollingCurl.class.php) [function.require-once]: failed to open stream: No such file or directory in Z:\home\nprog.ru\www\chains_of_requests.php on line 9 

      Убрал «. DIRECTORY_SEPARATOR . ‘..'», заработало без проблем.

    11. chains_of_requests.php по умолчанию лежит в папке /demo php-парсера AngryCurl, поэтому путь к классу прописан, как ../classes/AngryCurl.class.php

      Я так понимаю, Вы вынесли его из папки demo парсера — в корень — в этом и проблема.

    12. naive, а у Вас не было такого, что в requestMap еще есть данные, а он их не обрабатывает?
      Добавляю запросы в поток, они обрабатываются, плохие запросы добавляю снова в поток, на повторную обработку, но парсер до конца не отрабатывает, оставляя некоторые запросы не обработанные.

    13. Такого не наблюдалось. Можете предоставить выкладку с логом/кодом/результатом процесса, чтобы я мог воссоздать ошибку и разобраться, если таковая имеется?

    14. Интересный момент.
      Разберёмся в переписке по почте — потом сюда выложим результаты.

  21. ВАЖНО!
    Внесены изменения в код, в том числе, некоторые — критические.
    Настоятельно рекомендую использовать последнюю версию класса php-парсера AngryCurl на github.

    Изменения, повлиявшие на совместимость:

    • __set(‘window_size’, N ) заменено на execute(N)
      — подробнее в example.php;
    • load_proxy_list( FILE/ARRAY, PROXY_TYPE, TARGET_URL, TARGET_REGEXP)
      заменено на
      load_proxy_list( FILE/ARRAY, WINDOW_SIZE, PROXY_TYPE, TARGET_URL, TARGET_REGEXP),
      где WINDOW_SIZE — количество потоков для проверки списка proxy;
    • отмена __set(‘use_proxy_list’, ‘true’) и __set(‘use_useragent_list’, ‘true’), теперь при использовании импорта списков — выставляются по умолчанию;
    • импортируемые списки теперь лежат в папке import;
    • файлы классов теперь лежат в папке classes.

    Критические изменения:

    • исправлен баг при удалении дубликатов proxy, в случае проявления которого, могли проходить запросы без использования прокси-сервера;
    • исправлен баг при котором количество потоков ограничивалось количеством загруженных из прокси-листа.

    Прочие изменения:

    • добавлено возвращение значение в метод execute();
    • добавлены исключения;
    • переработан example.php;
    • добавлена папка demo с примером Proxy Checker и расширенным вариантом работы с запросами (POST запросы, CURL OPTIONS), в дальнейшем там будут появляться новые примеры;
    • описание работы с цепочками запросов в /demo/chains_of_requests.php;
    • обновлен список proxy-серверов;
    • комментарии/описание/рефакторинг;
    • что-то ещё …
  22. Доброго времени суток, спасибо огромное за прекрасное решенеи!
    Хочу с вами посоветоваться как лучше организовать и оптимизировать скрипт если задача такая :

    Есть сайт с рубриками, в каждой рубрике N страниц, на странице много нужных повторяющихся блоков которые я и собираю.

    Надо завершать получение данных и их парсинг в цепочке страниц рубрики, как только видим что определенный блок на странице уже был ранее сграблен.

    Думал как-то в эту сторону:

    $AC1 = new AngryCurl(‘callback’); //цепочка страниц рубрики1

    $AC2 = new AngryCurl(‘callback’); //цепочка страниц рубрики2

    function callback($response, $info, $request)
    {
    /* обрабатываем респонс и если есть уже встречавшиеся данные, прекращаем перебирать страницы из текущей рубрики, завершая очередь запросов под вызвавшим этот колбек АС…, но как ? 🙂 */
    }

    Может я конечно усложняю, и можно попорядку без параллели перебирать страницы внутри каждой рубрики и далее переходить к другой….
    Спасибо!

    1. Доброй ночи.

      Вообще ваш вопрос написан довольно сумбурно, поэтому сложно ответить, но попробую.

      Если я правильно понял вашу задачу, то ваша задача парсинга страниц идентична задаче парсинга поисковой выдачи.

      Если мы примем за «Рубрику» — поисковый запрос, за «страницы» — пагинацию, а за «блоки» — блоки выдачи, — то, я так понимаю, получается тоже самое.

      Как я понял — блоки в разных «Рубриках» у вас оформлены одинаково.

      При этом, почему Вы гипотетически можете получить «уже встречавшиеся данные» — не совсем понятно. Вы парсите одну и ту же страницу, чтобы получить новые «блоки»? Зачем?

      Если я правильно понял Вашу задачу, — алгоритм выглядит довольно просто:

      1. На первом этапе вы собираете массив всех ссылок (все страницы во всех рубриках), который имеет размерность N*M, где N — количество рубрик, M — количество страниц в рубрике (если оно везде одинаково). Делать это можно либо параллельно либо последовательно — не важно. Если у Вас количество страниц в рубрике различно — вы добавляете на вход php-парсеру — ссылки на рубрики, далее в callback function — с помощью preg_match_all() выбираете все ссылки на страницы.
      Итог — у Вас полный массив ссылок на конечные страницы с нужными Вам блоками.

      2. Далее Вы подаёте этот массив на вход AngryCurl в режиме многопоточности, при этом, в callback, вы опять же должны обработать все блоки на странице за один запрос (как вариант — опять тем же preg_match_all() в callback function Вы выбираете все блоки со страницы сразу и обрабатываете их).

      При таком подходе — я не вижу, зачем Вам нужно «прекращать перебирать страницы из текущей рубрики» и как Вы можете увидеть «наличие уже встречавшихся данных», если количество страниц Вам известно, а данные не могут повторяться.

      Если я не смог ответить на Ваш вопрос — Вы можете написать мне напрямую на email (есть в комментариях к классу), либо написать здесь пример страниц с информацией, которую Вам нужно спарсить и Ваш код.

      Если же всё-таки попытаться ответить на Ваш прямой вопрос:

      завершая очередь запросов под вызвавшим этот колбек АС…, но как ?

      — то вы всегда можете сделать так:

      function callback_function($response, $info, $request)
      {
          global $AC;
          if(is_duplicated_data())
          {
              unset($AC);
          }
      }

      Но мне кажется, что проблема именно в Вашей общей логике и алгоритме решения задачи.

    2. Извиняюсь за сумбур ), написано достаточно быстро и непоследовательно.
      Попробую уточнить свой вопрос:
      1) Задача (действительно довольно простая, но с нюансами)
      Переодически заходить на один ресурс объявлений, пройтись по всем категориям постранично и собирать только новую информацию (новые «блоки» на страницах категорий). Вот почему надо прекращать запрашивать оставшиеся страницы в данной категории как только нахожу уже скопированный раннее контент на какой-то из них — дабы ускорить процесс и не делать лишние действия. Данные хранятся так — самые новые на первой странице каждой категории.

      Если объявление еще не встречалось — запрашиваем контент по его ссылке и парсим себе в базу в зависимости от того в какой мы категории находимся и других деталей.

      2) Что имеем на данный момент:
      Я было уже реализовал почти все c помощью phpquery, и задумался над глобальными вопросами в будущем (скорость работы, бан по айпи, оптимизированная работа с памятью и тд) и тут наткнулся на ваш блог и angrycurl. то есть я не использовал прокси и «многопоточность». Понял что angrycurl это то что мне нужно 🙂

      Алгоритм работы в однопоточном режиме такой:
      а) имею начальные урлы (статика) всех категорий
      б) хожу по ним последовательно постранично (получаю список внутренних урлов (объявлений) на каждой странице
      в) запрашиваю данные урла-страницы объявления если оно раньше еще не скачивалось и затем записываю новый урл для последующих сравнений, если встречался — прекращаю процесс перебора страниц в этой категории и перехожу к следующей (либо если достигнут конец категории — получена последняя страница)

      УРЛы по которым я уже скачивал инфу хранятся в простом тхтшнике, который по прегматчу матчится с текущей ссылкой и принимается решение идти по ней или нет.

      Спасибо в любом случае за вашу отзывчивость!!!

    3. В таком случае, в силу «многопоточности», — вариант с уничтожением инстанса AngryCurl при нахождении повторных блоков — может вызвать ошибки и трудности.

      Собственно, тогда есть следующие варианты организации парсера на php:
      1. Как было написано выше — создавать отдельный инстанс AngryCurl для каждого раздела, собираем все ссылки на страницы выдачи (или генерируем их), при этом уничтожать инстанс, если в буфере собралось M+N повторяющихся блоков, где M — количество блоков на странице выдачи, а N — количество потоков php-парсера. Это необходимо, дабы избежать ошибок в ситуации, когда дублирующий блок был обработан раньше не дублирующего.
      Минусы: всё-равно могут возникнуть ошибки, как при unset’e из-за принудительного уничтожения активных потоков, так и при проверках (вдруг один из потоков «сильно убежит вперед»).

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

      3. Вместо того, чтобы «уничтожать» инстанс при наличии «дублирующих блоков» — возможно стоит добавлять новые ссылки, если количество повторяющихся блоков < M, где M — количество элементов на странице.
      Минус: возможна одна лишь ошибка — обсуждение которой есть внизу страницы, тем не менее — вероятность ошибки сравнительно мала; гипотетически может возникнуть проблема количеством потоков.
      (в минусах рассмотрен наихудший вариант, скорее всего он не оправдается, но заранее решил написать)
      Плюсы: экономия времени, ресурсов; отсутствие необходимости в K инстансах AngryCurl (достаточно одного и добавлять в него ссылки).

      При третьем пункте, реализация выглядит следующим образом:
      а) загоняем сразу первые страницы в один инстанс AngryCurl;
      б) callback обрабатывает и сравнивает блоки, ведет подсчёт по каждому разделу — повторяющихся блоков;
      в) если количество повторяющихся блоков по разделу превысит M — новая ссылка на следующую страницу раздела не добавляется; если всё ок — добавляется новая ссылка раздела;
      г) обрабатываем, пока не кончатся все страницы.

      Пример работы с добавлением ссылок есть на github.
      На текущий момент — мне больше всего нравятся: сначала 3ий, потом 2ой вариант.

      Пишите, что у Вас получилось.

      * Отвечал тоже бегло, что-то мог упустить или ошибиться.

  23. Здравствуйте. Вопросы следующие.
    Как перенести проверку прокси load_proxy_list() в execute()? Точнее как загрузить proxy_list.txt без проверки каждого прокси? И как чтобы если результат был FAILED (код не равен 200), то запускать его заново? Т.е. мне важен полный результат, а сейчас получается что в результате выполнения часть запросов FAILED, а часть 200.

    1. Добрый вечер. Если честно, я не до конца понял Ваши вопросы по парсеру.
      По пунктам:

      1. Зачем переносить load_proxy_list() в execute()? Это принципиально разные методы. В любом случае, Вы всегда можете использовать наследование и переопределить необходимое.
      2. Загрузить список прокси без проверки каждого из них Вы можете, используя метод __set() php-класса RollingCurl, и установив значения следующих значений свойств:

      // Загружаете список из файла в массив, 
      // для этого придётся изменить модификатор protected на public
      // метода load_from_file() в исходнике AngryCurl
      // или отнаследоваться и сделать обёртку
      $proxy_list_array = $AC->load_from_file($filename); 
      // Сообщаем AngryCurl о том, 
      // что мы используем списки прокси-серверов
      $AC->__set('use_proxy_list', TRUE);
      // Передаем массив прокси-серверов
      $AC->__set('array_proxy', $proxy_list_array);
      // Устанавливаем значение количества прокси-серверов
      $AC->__set('n_proxy', count($proxy_list_array)); 
      

      3. Как добавлять в процессе выполнения скрипта «неудачные» запросы повторно — описано в комментариях выше и в этом примере на github. Один из вариантов — правите callback и добавляете новые request’ы.
      4. Для получения полного результата необходимо вести список ошибочных результатов и прогонять их снова, либо «на лету». Об этом писалось выше.

      Я так и не понял, почему нельзя использовать проверку proxy? Ведь если многие из серверов будут мертвы — Вы будете генерить лишнюю и ненужную нагрузку?
      Не логичнее ли сначала отсеять живые прокси и их использовать, вести учёт не успешных соединений и осуществлять несколько прогонов?

  24. Спасибо. Извините, что торопился и вопросы неграмотно составил, они как бы все об одном: что у меня не получается спарсить выдачу гугла и яндекса 🙂 Кусок кода по загрузке списка прокси-серверов то что надо. Бывает так, что при проверке прокси может пройти проверку 200 + regexp match, но при выполнении реквеста FAILED и наоборот, поэтому мне кажется проверка прокси не так уж полезна. К тому же мне необходимо найти ошибки дальше по коду в обработке запросов и после обработки запросов где уже вычисления и запись в базу, а так постоянно приходилось ждать проверку прокси-серверов — никак не могу привыкнуть к задачам парсинга психологически. По прокси-серверам я решил ввести счетчики сколько раз прокси-сервер FAILED в callback_function и хранить их в базе. Потом когда скрипт запускается повторно, он выбирает 100 прокси-серверов наименее failed и 10 новых из proxy_list.txt, которых еще нет в базе. Какие необходимо параметры CURL выставлять, чтобы Google и Yandex не просили капчу и не выдавали 403. Вообще Google такое хороший, что разрешает запросы каждые 30 секунд даже обычным file_get_contents(), а Yandex очень плохой. Я думаю может попробовать webscraping.com?

    1. Степень полезности участка кода по предварительной проверке списка прокси зависит от процентного соотношения изначально мёртвых, некачественных или фиктивных прокси-серверов в списке.
      Если их немного, — то да, можно обойтись и без предварительных проверок, в противном случае — это необходимо.
      Однако, я подумаю над тем, чтобы предоставить возможность отключения данной функции в классе парсера.

      Идея с счётчиками — верная, о ней и писалось выше. Надеюсь, что я найду время реализовать её в классе.

      Что касается Яндекса — не проще ли использовать стандартный Яндекс API? При необходимости — подключить N симок, можно даже их купить — всяко дешевле выйдет, чем морочиться с proxy.

  25. Добрый вечер, naive.
    Прочитал всю тему, но ответа на свой вопрос так и не нашел. Может быть из за плохого английского, чтобы подчерпнуть информацию в примерах.

    Задача:
    1. Зайти на сайт и получить cookie.
    2. Используя полученные cookie, отправить запрос на авторизацию.Получить взамен sessionid.
    3. Перейти по ссылке.
    4. Получить контент.

    Сама реализация, с помощью cURL, вопросов не вызывает.

    Вопрос в следующем:

    Как, используя ваш класс (а именно, многопоточность и proxy), добиться выполнения очереди данных запросов, чтобы сookie и proxy для каждой цепочки, оставались неизменными, до момента получения контента.

    Допустим, у меня сто аккаунтов, по которым мне нужно получить информацию.

    Из того что я понял, а может быть и нет 🙂 ,мне следует создать 100 экземпляров AngryCurl и столько же callback функций, а так-же 100 файлов cookie.txt . Так ли это ?
    Можно ли постучаться к вам в асю для получения консультации по данному вопросу ?

    1. Добрый вечер, отписал Вам на почту.
      Пишите туда или в icq — поясню этот момент.

    2. naive — Спасибо! Отличная система, работает быстро и надежно!

      А можно, «этот момент» здесь пояснить или тоже мне на почту?
      Так как, иногда нужна цепочка запросов от «одного пользователя».
      Каждый может костыли приклеить, но у Вас код такой лаконичный и оптимальный… 😉

    3. Пример с цепочками запросов не помог?
      Пробовали разбивать на группы запросов и хранить сессионные данные во внешнем массиве?

  26. здравствуйте, появился такой вопрос:
    запускаю скрипт( http://pst.kz/x9m6 ), проверяются прокси( живые есть ), после этого хочу открыть нигму 100 раз, но в итоге на открытых страницах из колбэка в причине бана написан мой ip, а не какая-то из прокси.

    немного сумбурно описал, но надеюсь все таки понятно.

    собственно вопрос, что не правильно в коде?

    1. Добрый вечер, вероятно, причина в том, что куки, которые Вы подгружаете из файла, ассоциируются с ранее созданной сессией и вашим реальным ip. Замените nigma.ru на какой-нибудь internet.yandex.ru. Если ip будет в ответе разный — теория подтвердится. Попробуйте куки получить сначала через прокси, а потом уже использовать

    2. naive, спасибо за такой быстрый ответ, но если открывать вместо нигмы internet.yandex.ru или любой другой определятель ip, то все равно показывается мой реальный ip.

    3. Понял в чём проблема, сразу не заметил.
      Надо всё-таки где-то это выделить в мануале или переопределить метод add.
      Суть раскрывается здесь.

      Суть в том, что подстановка прокси «на лету» реализована переопределением метода request класса. Как следствие — она не будет работать, если Вы используете добавление request’ов через AngryCurlRequest. Я изначально не уделил достаточно внимания этому моменту, поскольку не видел необходимости использования отдельного класса для request’a, а потом — как-то были другие приоритеты 🙂

      Замените на get/post/request и всё будет работать.

      В Вашем случае — нужно заменить строчки 56-84 листинга на:

          $options = array(
              CURLOPT_COOKIEJAR       => COOKIEFILE,
              CURLOPT_COOKIEFILE      => COOKIEFILE,
              CURLOPT_REFERER         => 'http://nigma.ru',
              CURLOPT_AUTOREFERER     => TRUE,
              CURLOPT_FOLLOWLOCATION  => TRUE
          );
          $AC->get('http://nigma.ru', NULL, $options);

      Прошу прощения, что Вы потратили время.

    4. Добавил в TODO и сюда.

      Как освобожусь немного от текущего проекта — займусь некоторыми доработками, которые давно надо было сделать.

    5. еще раз спасибо, теперь все работает)
      правда в некоторых случаях яндекс пишет такое:

      Мой IPv4: тут реальный ip
      Внешний IP: прокси

      но в основном так:

      Мой IPv4: прокси

      я так понимаю это уже от моих прокси зависит…

    6. Я думаю, что это из-за класса прокси.
      Но на всякий случай, я бы попросил Вас проверить — дампнуть в callback function $info/$request и посмотреть — действительно ли ip прокси фигурирует в настройках curl.

    7. да, фигурирует
      значит все таки от прокси зависит

    8. Если мне память не изменяет, proxy может передавать заголовок X_FORWARDER_FOR, в котором, собственно, реальный ip.
      Категорирование обычно есть на всех ресурсах, например тут.
      Проверить это можно, дампнув заголовки запроса.

  27. Привет.
    Пользуюсь AngryCurl практически с первых версий, постоянно слежу за обновлениями, но кое какого функционала нехватает, а именно:
    — удаление не работающего прокси из массива.
    Объясню подробней, что имею ввиду. Загрузили список прокси, допустим 1000 штук, удалились дубликаты, удалились не рабочие, протестировали прокси на работоспасобность и в массиве остались живые прокси. Начали парсить. т.к. прокси из массива выбираются случайным образом, со временем работы парсера, некоторые «живые» прокси умерли. И вот тут хотелось бы иметь возможность удалить умерший прокси из массива живых, а то так и будет парсер случайно выбирать уже мертвый прокси их массива живых.

    1. Привет,
      эта тема уже обсуждалась выше, как и её реализации. И воплощена она обязательно будет.

      Проблема с заминкой по этому вопросу в том, что для грамотной её реализации, видимо, необходимо отказаться от RollingCurl’а вообще, переписать/перенести часть кода из RollingCurl в AngryCurl.

      Изначально, я думал, что AngryCurl будет некой надстройкой над «RollingCurl». Но с каждым новым желанием и нововведением — я часто упирался в базовый класс RollingCurl.

      Теперь, когда ясно, что Александр Макаров занят Yii и заниматься поддержкой/развитием RollingCurl не будет — я думаю о том, что в ближайшем будущем реализую описанное выше и сделаю AngryCurl независимым, но, тк этот этап будет:
      а) спорным;
      б) требовать времени на тестирование и отладку;
      в) относительно трудозатратным;
      г) всплывут вещи, которые ранее нельзя было реализовать из-за данного ограничения, которые захочется непременно сделать;
      — то я выжидаю подходящий момент с наличием свободного времени.
      Ближайшие же месяц-два я занят в свободное время своим проектом, поэтому глобальных перемен затевать бы не хотел.

      PS: Более того, Вы спокойно можете реализовать этот функционал сами, если он нужен «здесь и сейчас», подсчитывая количество ошибок для прокси в callback и исключая их из массива при необходимости.
      PSPS: Помимо всего прочего, вы всегда можете присоединиться к разработке на GitHub и ускорить процесс 🙂

  28. 2-й день пытаюсь понять детально как тут все устроено и результата пока не добился.

    Подскажите подробней куда смотреть, какую функцию ковырять.

    Я в коде у себя сдедал по следующему принципу:

    1. Авторизовался на сайте который парсить требуется сохранил кукисы

    Далее

    $AC = new AngryCurl('nothing');
    $AC->init_console();
    //$AC->__set('window_size', 100);
    $AC->__set('options',array(CURLOPT_COOKIEFILE => '/путь до сохраненной/cookies.txt', CURLOPT_COOKIEJAR => '/путь до сохраненной/cookies.txt'));
    
    $AC->load_proxy_list_no_test('/путь до/proxy_list.txt');
    //$AC->load_useragent_list('/путь до/useragent_list.txt');
    
    $AC->__set('use_proxy_list',true);
    //$AC->__set('use_useragent_list',false);
    
    /* код формирования и добавления ссылок откуда парсим */
    
    $AC->execute(100);
    //AngryCurl::print_debug(); // if console_mode is off
    unset($AC);
    

    Полученный результат разбирается в функции «nothing» часть её…

    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']);
        url_page($info['url']);
        }
    }
    

    Если результат ответа вернувшийся через прокси не равен 200, то тот же самый url перезапускаю через функцию «url_page»

    function url_page($url){
        $AC = new AngryCurl('nothing');
        $AC->__set('options',array(CURLOPT_COOKIEFILE => '/путь до/cookies.txt', CURLOPT_COOKIEJAR => '/путь до/cookies.txt'));
        $AC->load_proxy_list_no_test('/путь до/proxy_list.txt');
        $AC->__set('use_proxy_list',true);
        $AC->get($url);
        $AC->execute(100);
    }
    
    

    т.е. опять все возвращается в ‘nothing’ и так по кругу пока не обработается весь список url.

    Подскажите как из функции ‘nothing’ добраться до массива с живыми проксями. Из файла AngryCurl.class.php массив с живыми проксями $array_alive_proxy, все перепробывал, менял

    public static $array_alive_proxy=array();
    

    просто

    public $array_alive_proxy=array();
    

    даже выносил в глобальную переменную, результата не достиг.

    Помогите разобраться с этим кастылём, не мне одному я думаю требуется эта незначительная косметическая модернизация.

    1. Описанный Вами случай лучше реализовывать, как здесь, чтобы не плодить экземпляры класса в callback’е, а добавлять «неверные» запросы обратно в очередь.

      Что касается того, где находится список прокси, то он здесь в $array_proxy.

      Доступ к нему можно получить используя геттер/сеттер.

  29. Naive, спасибо тебе огромное. Все переделал как ты написал. Получилось и Proxy чистить на лету и скорость парсинга возрасла внесколько десятков раз!!! Сам удивлен был. Еще раз СПАСИБО!

  30. Добрый день!
    Не совсем разобрался, как в callback_function, получить сам контент страницы для дальнейшего его парсинга?

  31. Тестил Ваши классы. Проходят как ы без ошибок. Но далее Лекбез вопрос. Как и где собственно получить пропарсенный (сграбленный) контент. Можно пример использования и вывода пропарсенного текста

    1. В callback function:

      function callback($response, $info, $request) {
        // Обработка результатов
      }
      

      $response хранит в себе результат работы парсера.
      Для работы с контентом можно использовать regexp или Simple HTML DOM Parser.

  32. Вопрос такой — как прицепить к урлу еще пользовательские данные что б когда уже обрабатываешь запрос знать что ты обрабатываешь например у меня есть список сайтов — как передать с урлом айди сайта и получить его в обрабатывающей функции

    1. Вижу три варианта:

      — хранить внешний массив соответствия url — списку параметров (например, url — ключ массива), в callback прописать global $params_array; для получения доступа к данному массиву из функции;
      — передавать доп. параметры (или ключ элемента в массиве с доп. параметрами) в доступные $headers или $options при формировании request’a, получая их в callback_function в параметре $request;
      — массив запросов находится в $this->requests и представляет собой массив экземпляров Angry/RollingCurlRequest, в связи с чем, возможно дополнить функционал соответствующих классов доп. параметрами и устанавливать их при инициализации, в последствии, получая их, опять-таки, в $request.

    2. naive, я заметил, что если было перенаправление со страницы, которую парсим, то данные, которые передаем в $headers, теряются и callback_function не обрабатывает так как нужно. Подскажи пожалуйста, можно ли как-то передавать $headers, даже если было перенаправление?
      И еще, если не затруднит, напиши пример, как «дополнить функционал соответствующих классов доп. параметрами и устанавливать их при инициализации, в последствии, получая их, опять-таки, в $request».

  33. Подскажите пожалуйста такой момент — каким образом можно добиться того, чтобы скрипт не проверял все прокси перед парсингом, а например просто взял одну прокси из списка, проверил, если рабочая, то спарсил через нее, если нет — проверил вторую и тд. Может быть это реализовано здесь, просто не разобрался еще.

    1. Во-первых, хотелось бы уточнить — какую именно задачу Вы решаете.
      Возможно, вариант, о котором Вы говорите — не оптимален.

      По существу же: описанный вами вариант — практически линеен и выглядит следующим образом:

      1. Выбираем прокси из очереди.
      2. Проверяем прокси
      3. Используем прокси для парсинга, пока количество ошибок < N (здесь возможна многопоточность)
      4. Повторяем п. 1-3. пока список URL не кончится.

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

      Реализовать данный вариант с помощью AngryCurl можно следующим образом:
      1. Проверяем все прокси (как в примере ) и получаем отфильтрованный массив активных прокси.
      2. Создаем 2 массива URL: текущий и выполненный. Адреса удаляются из текущего и попадают в выполненный в callback_function после обработки полученных данных.
      3. Добавляем адреса из текущего массива в очередь AngryCurl с 1ым активным прокси в качестве прокси-сервера.
      4. Запускаем выполнение, обрабатываем результаты.
      5. В callback_function, при срабатывании условия (некорректная информация/недоступен прокси итп) — останавливаем процессы и формируем новую очередь из текущего списка с новым прокси, повторяя п.3-4.

    2. naive, спасибо! Мне нужно парсить по 10 урлам в режиме реального времени. Задержка на выполнение должна быть минимальной, поэтому чекать все прокси не выгодно. Как вариант, есть уже прочеканые прокси и нужно чтобы проверка не выполнялась, а сразу парсились сайты через прокси. Буду благодарен за пример кода.
      Еще есть вопрос. Он наверно такой же как и у предыдущего автора коммента. Можно простой пример того как внести в переменную результаты callback_function. То есть парсим 10 сайтов, обрабатываем их в callback и выводим результат в виде переменной.

    3. // Массив заранее проверенных и отфильтрованных прокси
      $proxy_filtered_arr = array();
      
      // Массив адресов сайтов и результатов парсинга
      $target_sites       = array(
                              'http://site1.ru' => '',
                              'http://site2.ru' => ''
                          );
      
      // Массив параметров curl
      $options            = array();
      
      // Функция обработки результатов
      function callback_function($response, $info, $request)
      {
          global $target_sites;
          
          // Проверяем корректность статуса ответа
          if($info['http_code']==200)
          {
              // Записываем результат в глобальный массив
              // в соответствии с указанным URL
              $target_sites[$info['url']] = $response;
          }
          else
          {
              // Обработка ошибок
          }
      }
      
      $AC = new AngryCurl('callback_function');
      $AC->init_console();
      
      foreach($target_sites as $url => $response)
      {
          // Устанавливаем значение (рандомное) прокси-сервера
          $options[CURLOPT_PROXY] = $proxy_filtered_arr[ mt_rand(0, count($proxy_filtered_arr) -1) ];
          // Добавляем запрос в очередь
          $AC->request($url, 'GET', null, null, $options);
      }
      
      // Запускаем AngryCurl
      $AC->execute(10);
      
  34. naive, благодарю за ответы, все работает и это похоже то что нужно! Последний вопрос по нагрузке — здесь нагрузка на сервер будет меньше, я так понял? По сравнению с обычным парсингом в цикле с помощью curl.

    1. Как же нагрузка будет меньше, если вместо 1 задачи за единицу времени будет выполняться N задач?

  35. Добрый день!
    Как увеличить время проверки прокси, то есть проверка на гугл все нормально видимо потому что очень быстро возвращает ответ, а вот с другим сайтом беда все прокси считает не живыми.

  36. Привет, Naive. Спасибо большое за работу, сегодня тестировал и нашел баг. Или это баг в моем мозгу, прошу подсказать и поправить.
    https://github.com/2naive/AngryCurl/blob/master/demo/chains_of_requests.php
    Если запускать единственный парсинг, то в callback-функции нельзя добавить на лету новый парсинг

    $AC->get('http://ya.ru/?only_one');
    
    // В callback ничего не делает
    global $AC;
    $AC->get('http://ya.ru/?callback'); //Я не заработаю
    

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

    
    $AC->get('http://ya.ru/?one');
    $AC->get('http://ya.ru/?two');
    $AC->get('http://ya.ru/?tree');
    

    Для всех данных кроме ‘http://ya.ru/?one’ будет нормально обработана callback-функция и добавлен парсинг в очередь (если нужо).
    http://ya.ru/?one, же (если он последний в очереди) уже не сможет породить новый парсинг (добавить $AC->get() в очередь)

    Как-то можно это обойти?

    1. Добрый день,
      с мозгом у Вас всё в порядке 🙂

      В переписке по почте и здесь в комментариях мы обсуждали это проблему с Владом.

      Выводы были не очень радужные, действительно есть 2 проблемы:

      1. При использовании CURLOPT_CONNECTTIMEOUT и CURLOPT_TIMEOUT, если поток крешится и не успевает получить ответ сервера, то:
      1.1. никаких сообщений об ошибке не выводится;
      1.2. результат в http_code — 0;
      1.3. предположительно, потоки, которые умирают таким образом — не могут быть использованы повторно. Иными словами, если 5 из 5 соединений упадут по таймауту, то независимо от callback — повторные соединения запущены не будут. Это странно, поскольку судя по этой строчке добавляться новые потоки должны в любом случае. Было предположение, что тут происходит уничтожение потока (и каким-то образом ранее, чем добавлялся новый), однако, комментирование этой строчки не помогло.

      2. При запуске 1ого потока — действительно не будет работать добавление в очередь в callback, потому, что обработка 1ого потока идёт в однопоточном режиме.
      Прямого решения тут нет, возможны два варианта:
      2.1. дополнительно создавать 1 «псевдо-поток» (например, — на localhost, отсеивая его в callback), чтобы общее кол-во потоков было >= 2;
      2.2. инициализировать новый инстанс AngryCurl в callback, добавлять в него повторный запрос и запускать новый execute() прямо там.

      3. Что касательно той части, где Вы пишете про невозможность продолжения очереди, при обработке результатов у последнего — то тут я подтверждения Вашим словам не нашёл. Если модифицировать код из примера следующим образом:

      for($i=1; $i< =2; $i++)
          $AC->get("http://ya.ru/?$i");
      $AC->execute(10);
      die();
      # Callback function
      function callback_function($response, $info, $request)
      {
          global $AC;
          static $count=0;
          
          if($count<5)
          {
              $count++;
              $AC->get("http://ya.ru/?callback$count");
          }
      
          /**
           * Проверка кодов ответа
           **/
      
          return;
      }

      — то мы получим всё тот же результат:

      # Console mode activated
      # Start loading useragent list
      # Loaded useragents:	1004
      
      # Adding first 10 requests
      
      
      # Starting with number of threads = 10
      # After each of first 5 finished request - new one will be added
      # They all will proceed to execute immediatly (see callback_function)
      
       * Threads set to:	10
       * Starting connections
      ->		OK	200	10.047	http://ya.ru/?1
      ->		OK	200	10.047	http://ya.ru/?2
      ->		OK	200	0.031	http://ya.ru/?callback1
      ->		OK	200	0.046	http://ya.ru/?callback2
      ->		OK	200	0.047	http://ya.ru/?callback3
      ->		OK	200	0.047	http://ya.ru/?callback4
      ->		OK	200	0.031	http://ya.ru/?callback5
       * Finished in 10.2s
      # Finishing ...

      — что не подтверждает Вашу теорию. Поэтому, тут необходимо больше информации от Вас для выявления ошибки.

      Если у Вас будут предложения по исправлению багов, более подробная информация или другая помощь — буду признателен.

    2. $AC = new AngryCurl('callback_function');
      	$AC->init_console();
      	//$AC->load_proxy_list($proxy_list,10);
      	$AC->load_useragent_list($useragent_list);
      	
      	$AC->get('http://www.ya.ru/?1111=');
      	$AC->get('http://www.ya.ru/?2222=');
      	$AC->execute(5);
      
      # Callback function
      function callback_function($response, $info, $request)
      {
          global $AC;
       echo 1111; // Проверяем вызывается ли вообще каллбек
         if($info['url'] == 'http://www.ya.ru/?1111=')
          {
              $AC->get("http://ixbt.com");
          }
      	
          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']
              );
          }else
          {
              AngryCurl::add_debug_msg(
                  "->\t" .
                  $request->options[CURLOPT_PROXY] .
                  "\tOK\t" .
                  $info['http_code'] .
                  "\t" .
                  $info['total_time'] .
                  "\t" .
                  $info['url']
              );
      
          }
          
          return;
      }

      Жму переодически в браузере F5.
      Результата могут быть 2.

      
      # Console mode activated
      # Start loading useragent list
      # Loaded useragents:	1004
       * Threads set to:	5
       * Starting connections
      1111->		OK	200	0.031	http://www.ya.ru/?2222=
      1111->		OK	200	0.031	http://www.ya.ru/?1111=
       * Finished in 0.02s
      # Finishing ...

      или

      # Console mode activated
      # Start loading useragent list
      # Loaded useragents:	1004
       * Threads set to:	5
       * Starting connections
      1111->		OK	200	0.031	http://www.ya.ru/?1111=
      1111->		OK	200	0.031	http://www.ya.ru/?2222=
      1111->		OK	200	0.219	http://www.ixbt.com/
       * Finished in 0.25s
      # Finishing ...

      Т.е. третий запрос вызывается только в случае, если второй идет http://www.ya.ru/?2222=
      Если второй (последний в очереди) будет http://www.ya.ru/?1111=, то каллбек не вызовется вообще (?).

      Соответсвенно, добавив

      
      $AC->get('http://www.ya.ru/?3333=');
      

      Ситуация повторяется. Когда 1111 идет последним — новый запрос не создается.
      Соответственно, чем больше запросов в цепочке, тем менее заметен глюк.

      P.S. Я могу как-то размечать php-код здесь? ББкод там, или еще что-то?

    3. Код можно размечать тегами <pre><code class=»PHP»> eval(); <code/><pre/>

      Проблема очень странная, пока не могу понять в чём подвох, поскольку, например, если модифицировать код следующим образом:

       # ...
      $AC->get('http://www.ya.ru/?a');
      $AC->get('http://www.ya.ru/?a');
      # ...
         if($info['url'] == 'http://www.ya.ru/?a')
          {
              $AC->get("http://ixbt.com");
          }
      # ...
      

      — то во всех случаях отрабатывает:

      # Console mode activated
       * Threads set to:	5
       * Starting connections
      ->		OK	200	10.047	http://www.ya.ru/?a
      ->		OK	200	10.047	http://www.ya.ru/?a
      ->		OK	200	20.171	http://www.ixbt.com/
      ->		OK	200	40.233	http://www.ixbt.com/
       * Finished in 60.29s
      # Finishing ...

      Хотя в Вашем примере — действительно есть некая нестабильность.

    4. Я так понимаю ошибка где-то в 288 строке RollingCurl.class.php
      while ($done = curl_multi_info_read($master)) {
      Точнее в этом цикле.
      Сегодня тестировал и понял, что это кусок не выполняется для последнего добавленного в очередь парсинга. Но так мои эксперементы ни к чему и не привели. И я окончательно запутался =)

    5. Судя по тому, что я написал выше, проблема возникает не в случае «последнего потока», а в случае, когда добавленный 1ым поток отрабатывает 2ым.

      Воспроизвести, правда, на своём примере я это не смог.

  37. Здравствуйте, понравился ваш скрипт, спасибо!
    Подскажите можно ли просто добавить список прокси без их проверки?
    Спасибо

    1. Добрый вечер,

      Да, вы можете просто добавлять запросы в очередь следующим образом:

      $proxy_array = array(); // Массив прокси
      foreach($url_array as $k => $url)
      {
          $options[CURLOPT_PROXY] = $proxy_array[ mt_rand(0, count($proxy_array) - 1) ];
          $AC->request($url, 'GET', NULL, NULL, $options);
      }

      — без использования метода load_proxy_list()

    2. Спасибо за быстрый ответ, а не подскажите почему через сеттер не хочет, я уже все что можно перепробовал

      $AC->array_proxy=$proxies;
      $AC->array_alive_proxy=$proxies;
      $AC->use_proxy_list=true;
      $AC->n_proxy=count($proxies);

      при этом все var_dump($AC->array_proxy); значения есть
      но при работе в response ip прокси нет

    3. Потому, что при выполнении запросов парсером идёт проверка флага, устанавливаемого в load_proxy_list(). Если он равен false, то использования прокси не происходит.

      А в целом, зачем Вы решили трогать внутренние свойства, если можно было просто сделать способом, указанным выше?

    4. Точно, не заметил, что use_proxy_list. Пришлось поменять на protected, все заработало, спасибо

      Так просто удобнее

      if ($check) {
         $AC->load_proxy_list(
         ...
         );
      
      } else {
          $AC->array_proxy=$proxies;
          $AC->use_proxy_list=true;
          $AC->n_proxy=count($proxies);
      }
  38. Подскажите, пожалуйста, какие минимальные требования необходимые для полноценной работы скрипта?

    1. Выявленные моменты описаны здесь.

      Preferred environment configuration

      PHP as Apache module
      safe_mode Off
      open_basedir is NOT set
      PHP cURL installed
      gzip Off

      Depencies:

      PHP 5 >= 5.1.0
      RollingCurl
      cURL

  39. К сожалению не нашел, как ответить на Ваш ответ, такой момент, что проверки на тип прокси socks5 работают оочень медленно, с чем это может быть связано?

    ps Спасибо За Класс- очень достойная вещь!

    1. Парсер одинаково работает, как с http, так и с socks-прокси.
      По факту, он просто устанавливает через него соединение и проверяет результаты.

      Поэтому, проблемы могут быть только с окружением/прокси.

  40. Класс просто обалденный. Спасибо.
    При работе с классом возникла необходимость запускать последовательно два потока. Сначала собирать первым проходом данные со страниц, потом эти данные обрабатывать и запускать второй поток, передавая ему параметры.
    Подробнее:
    callback_function($response, $info, $request)
    выполняет свою работу, собирает нужные данные в массив $arr. При правильно спарсенной странице инициирует $AC2->get(«найденная нами страница»)
    Вот собственно тут и встал вопрос, как в callback_function2($response, $info, $request) оперировать нашим массивом с данными найденными в callback_function.

    1. Похожие вопросы уже обсуждались ранее.
      Вы можете изначально объявить массив url для парсинга с необходимыми параметрами, при этом в callback_function, используя global:

      function callback_function($response, $info, $request)
      {
          // Получаем доступ к массиву url, объявленному ранее
          global $url_array;
      
          ...
      }

      — получить все необходимые данные.

  41. Здравствуйте, спасибо вам за хороший класс.

    При изучении стало интересно почему был выбрана архитектура завязанная на callback? Разве не получилось бы намного более гибко, если бы к примеру выполняя exicute мы получали сразу же ретерном данные?

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

    Работая в архитектуре с callback придумываются такие решения, что надо либо создавать на каждую операцию новый экземпляр класса AngryCurl либо превращать callback функцию в хитрый контроллер, который будет анализировать входные данные пытаясь узнать откуда этот response, а затем формировать из этих данных новцую пачку url для парсинга (результаты опять же получает он и всё повторяется).

    Всё это в чистом виде как то не очень масштабируется и повторно не применить. Я пытался написать обертку, чтобы было удобно решать такие задачи. Создал класс, которой хранил в свойстве экземпляр AngryCurl, создал метод «выполняльщик» который делал что то вроде:

     	foreach($urls as $key=>$url)
    	{
    		$this->AC->get($url, nul, nul, $key); // точно уже не помню, $key потом оказывался в дополнительном поле массива $request, я немного менял ваш класс чтобы так работало
    	}
    
    	$this->AC->execute(20);
    

    callback Функцией выступил метод этого класса.

    Работать по задумке должно было так: формируется массив вида array(‘name’ => ‘url’,) который скармливается методу выполняльщику он формирует пачку запросов, для каждого указывая ‘name’. name — это имя метода, который в последствии должен получить на вход результат запроса, сами данные делегирует callback метод, он берет name из $request (куда мы его запихали ранее) и вызывает метод с этим именем передавая ему на вход $response, $info, $request.

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

    Но все уперлось в то, что callback методу не доступен внутренний указатель объекта $this..

    1. Добрый день,

      1. callback необходим для псевдо-асинхронной работы. Ничто не мешает собирать в callback массив результатов текущей задачи с использованием global — будет идентично return в execute. Добавлять же в свою очередь return в execute — опасно, как минимум из-за того, что на протяжении выполнения всех цепочек — придётся хранить все $request/$info/$response. А что если в ответе объёмы больше мегабайта? Креш?

      2. Мне кажется вполне логичным все варианты:
      — использования нового экземпляра AC под нову задачу,
      — смена callback при переходе к новой задаче,
      — усложнение логики callback.

      3. callback вызывается с помощью call_user_func_array(), которая допускает использовать вместо функций — методы классов, подробнее на php.net. Если вкратце, то ничего не мешает использовать:

      call_user_func(array($classname, 'say_hello'));
      call_user_func(array($myobject, 'say_hello'));

      — и $this во втором случае будет доступен, как и в 1ом — self.

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

      $AC_instances_array = array();
      $url_array = array();
      
      function callback_1($request, $info, $response)
      {
          global $url_array;
          global $AC_instances_array;
          
          $url_array[$info['url']] = $response;
          $AC_instances_array[2]->get($info['url']);
      }
      
      function callback_2($request, $info, $response)
      {
          global $url_array;
          global $AC_instances_array;
          
          $url_array[$info['url']] = $response;
          $AC_instances_array[3]->get($info['url']);
      }
      
      function callback_3($request, $info, $response)
      {
          global $url_array;
          global $AC_instances_array;
          
          $url_array[$info['url']] = $response;
      }
      
      for ($i = 1; $i < = STAGES_AMOUNT; $i++)
      {
          $callback_function_name  = 'callback_' . $i;
          $AC_instances_array[$i]   = new AngryCurl($callback_function_name);
      }
      
      foreach($url_array as $url => $result)
      {
          AC_instances_array[1]->get($url);
      }
      
      foreach($AC_instances_array as $stage_n => $AC)
      {
          $AC->execute();
      }
      

      — как-то так для 3х этапов.

      Если количество этапов парсинга — большое и callback`и зависят друг от друга, основаны на приблизительно одном коде — возможно объявление их тоже обернуть.

  42. Всегда выдает, что живых прокси нет. Когда проверяю другим сервисом, то все живые. В чем может быть проблема?

  43. Спасибо за развернутый и быстрый ответ.

    Вот такой еще вопрос. При повышении количества потоков через некоторое время работы скрипта начинает вываливаться ошибка:

    Fatal error: Uncaught exception ‘RollingCurlException’ with message ‘Window size must be greater than 1’ in RollingCurl.class.php:264

    Что может быть причиной и как бороться?

    1. Причем повышение не большое на одном парсере ошибка начинает появляться при числе потоков более 20, так сказать от числа потоков зависит стабильность работы и с его увлечением повышается шанс на появление этой ошибки.

    2. Я встречался с данной проблемой, но, к сожалению, не помню в каком контексте. Предлагаю Вам выслать Ваш код мне на почту — я могу посмотреть и сказать точнее.

    3. У меня аналогичная проблема: Вдруг откуда не возьмись появилась ошибка
      PHP Fatal error: Uncaught exception ‘RollingCurlException’ with message ‘Window size must be greater than 1’ in /var/www/site.com/parser/AngryCurl/classes/RollingCurl.class.php:264
      Stack trace:
      #0 /var/www/site.com/parser/AngryCurl/classes/RollingCurl.class.php(217): RollingCurl->rolling_curl(200)
      #1 /var/www/site.com/parser/AngryCurl/classes/AngryCurl.class.php(183): RollingCurl->execute(200)
      #2 /var/www/site.com/parser/angryparser.php(27): AngryCurl->execute(200)
      #3 {main}
      thrown in /var/www/site.com/parser/AngryCurl/classes/RollingCurl.class.php on line 264
      ИЧСХ, вроде только вчера все работало.
      naive, если скажешь email, могу выслать сбойный скрипт.

    4. Вроде нашел причину: Если вызывается метод execute, но при этом метод get еще не вызывался (например по причине отсутствия входных данных) — будет выше обозначенная ошибка

    5. Спасибо, проблема RollingCurl’a.
      В ближайшем будущем — будет устранено.

    1. Спасибо за полезное замечание.
      Будет добавлено в ближайшее время.
      Если будет желание — Вы сами можете предложить патч на github.

    2. Если бы я знал, что и где нужно подправить, то уже предложил бы) А так, пока что не хватает знаний в PHP. С заменой CURLOPT_FOLLOWLOCATION я делал только обычный однопоточный парсер, а здесь, увы, я не силен)

  44. По определенным причинам я не могу использовать опцию CURLOPT_FOLLOWLOCATION у себя на сервере:
    curl_setopt_array(): CURLOPT_FOLLOWLOCATION cannot be activated when safe_mode is enabled or an open_basedir is set

    Может ли класс нормально функционировать без нее?

    1. Да, вполне.
      Установка значения CURLOPT_FOLLOWLOCATION происходит здесь: https://github.com/2naive/AngryCurl/blob/master/classes/RollingCurl.class.php#L342

      Вы можете либо переопределять параметр при инициализации запроса, либо использовать ini_set, либо вообще «подправить» этот аспект для себя, пока не выпущен патч.

    2. Это понятно, просто лень вручную редиректы обрабатывать =)

  45. naive, здравствуй, назрел такой вопрос:
    Есть класс, примерно такой

    class Proxy extends Model
    {   
    	public function get()
    	{	
    		//...
    		$AC = new AngryCurl('callback');
    		//...
    	}
    
    	public function callback($response){
    
    	}
    }

    Проблема в то, что AngryCurl в get() «не видит» callback() или его нужно как то по другому объявлять?

    1. Добрый день,
      посмотрите, пожалуйста этот комментарий — там рассмотрена, втч и Ваша проблема.
      В конструктор можно передавать не только функцию, но и метод класса.

    2. что то не пойму как call_user_func использовать…
      Можете на моем примере показать как правильно должно быть?

    3. call_user_func() вызывается здесь:
      https://github.com/2naive/AngryCurl/blob/master/classes/RollingCurl.class.php#L300

      её описание здесь:
      http://www.php.net/manual/ru/function.call-user-func.php

      параметр $callback, передаваемый выше, передаётся в конструктор класса здесь:
      https://github.com/2naive/AngryCurl/blob/master/classes/RollingCurl.class.php#L128

      код я за Вас писать не буду.

    4. Пишу
      $AC = new AngryCurl(call_user_func_array(array($this, ‘callback’), array(&$response))); (Это правильно?)
      Ни ошибок, ни предупреждений не выдает, но каким образом теперь получить доступ к результатам запроса?

      public function callback($response){
      // в $response пусто
      }

    5. Нет, не правильно. Зачем вызывать call_user_func_array? Я же показал, как и где происходит вызов конструктора? В конструктор нужно передавать либо string, либо array, причём здесь результат выполнения ф-ции?

  46. Naive, здравствуйте еще раз. Подскажите, а как ограничить кол-во подгружаемых прокси? Т.е. у меня периодически попадаются группы из 3-5 парсингов и каждый раз загружать\проверять все прокс из файла нецелесообразно. Хотелось бы, чтоб были проверены, скажем 5 прокси, и дальше работа происходила только с ними.

    1. Добрый день,

      Вы можете воспользоваться методом load_from_file (внимание — protected), загрузить список в массив, далее выбрать 5ть элементов и уже их передать в AngryCurl.

  47. Приветствую, naive!
    Пользуюсь парсером и очень доволен его работой, за что отдельное спасибо!)
    Я тут описал одну проблемку: http://stupid.su/php-curl_multi/#comment-300 если идет редирект 301 или 302, то теряются данные из $headers. Может есть возможность передать эти данные на новый урл?
    И хочу спросить по поводу CURLOPT_FOLLOWLOCATION, есть какие-нибудь новости?)

    1. Доброй ночи,

      может стоит использовать свою собственную callback_function по аналогии с этим и цепочкой запросов?

  48. Здравствуйте! Подскажите, пожалуйста, подойдет ли ваша библиотека для моих целей: мне необходимо выполнить цепочку curl запросов, при этом таких цепочек должно быть достаточно много. То есть нужно, чтобы эти цепочки работали параллельно. Надеюсь, вы меня поняли.

    1. Вы формируете список адресов, передаете в объект, даете команду на получение документов. Запросы к документам из списка выполняются параллельно, но пока не будут получены результаты по каждому запросу дальше скрипт работать не будет.

    2. Посмотрите комментарии выше или примеры здесь.
      Можно делать «псевдо-параллельные» цепочки запросов, тогда новые будут появляться одновременно с выполняющимися старыми.
      В целом же, вопрос довольно общий.
      Если отвечать на него «в лоб» — то да, в PHP операторы выполняются последовательно, за исключением различных случаев

    3. Да, подойдёт.
      Отписал на почту и судя по всему решение Вы нашли)

  49. Спасибо огромное. Сейчас немного допилю под свои нужды и в работу пущу. Много времени мне сэкономил твой скрипт)

  50. Отчего в логе могут возникать ошибки с http кодом 0
    Вроде такого:
    -> [ip_proxy]:8080 FAILED 0 [url]

    1. При этом url валидный — в браузере открывается нормально

    2. Проблема может быть при подключении к прокси.
      Как вариант — неверный тип.

  51. Еще проблема:
    Если на вход $AC->get подать например вот этот адрес https://bitly.com/15FObSE , то парсер его видоизменяет: после символа ‘&’ он почему то добавляет ещё ‘&’ и
    ломиться по неправильному адресу.

    1. Ложная тревога. URL остается правильный,ошибка в AngryCurl::add_debug_msg. Когда запускаешь из консоли отображается неправильный URL с указанными выше симптомами.

  52. Привет.
    Давно искал что-то подобное. Возникло пару вопросов.
    Как реализовать с помощью ЗК подобное:
    Я объявляю классы, прокси и т.д. и т.п., в callack мне нужно спарсить информацию с нескольких разных страниц по одному $key (т.е. спарсить с 1 proxy несколько страниц), как спарсилось создать текстовый файл $i и все что спарсилось туда засунуть. Далее переходим к другому $key и действия повторяются($i++).
    Насколько я понял с каждым новым запросом берется другой proxy.

    ini_set('max_execution_time', 0);
    ini_set('memory_limit', '128M');
    
    require("RollingCurl.class.php");
    require("AngryCurl.class.php");
    
    $keys = file('1.txt');
    
    # Определение функции, вызываемой при завершении потока
    function callback_function($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;
        }
         
         тут обработка полученных страниц. Но как мне узнать с какой страницы (1,2,3,4) получили данные и далее их обрабатвыть
        // Здесь необходимо не забывать проверять целостность и валидность возвращаемых данных, о чём писалось выше.
    }
    
    $AC = new AngryCurl('callback_function');
    # Включаем принудительный вывод логов без буферизации в окно браузера
    $AC->init_console(); 
    
    $AC->load_proxy_list(
        # путь до файла
        'proxy_list.txt',
        # опционально: количество потоков
        200,
        # опционально: тип proxy (http/socks5)
        'http',
        # опционально: URL для проверки proxy
        'http://google.com',
        # опционально: regexp для проверки валидности отдаваемой прокси-сервером информации
        'title>G[o]{2}gle'
    );
    # Загружаем список useragent из /import/useragent_list.txt
    $AC->load_useragent_list( 
        'useragent_list.txt'
    );
    
    foreach($keys as $key){
    $AC->request('http://1.ru'.$key);
    $AC->request('http://2.ru'.$key);
    $AC->request('http://3.ru'.$key);
    $AC->request('http://4.ru'.$key);
    }
    
    
    # Задаем количество потоков и запускаем
    $AC->execute(200);
    

    В общем описал как я представляю. Проблема заключается в том, как узнать с какой страницы(1,2,3,4) пришел ответ. Или же собирать все страницы в массив,implode, и дальше выгребать?

    В общем непонятно. Надеюсь на доступный ответ. Я дуб.
    П.С. Добавь подписку на комментарии.

    1. Забыл спросить. Сначала чекаются все прокси, а потом идет парсинг.
      Или проверяем первую прокси, работает, загружаем старницу и т.д.
      Еще вопрос: если прокси нерабочая попадается на запросе 1, то он берет другую прокси, выполняет запрос 1 и т.д. или первый запрос пропускает?

    2. В callback помимо $response есть ещё $info, $request.
      Что мешает получить из них информацию о «странице, с которой пришёл ответ»?

      Сначала происходит проверка всех прокси.

      На текущий момент — дополнительных проверок во время парсинга не происходит. Если прокси стала невалидна в процессе парсинга — повторно она проверяться не будет, запрос вернёт ошибку.

  53. Все же если я правильно понял, то нужно сначала использовать конструкцию

    foreach($keys as $key){
     $AC->request(‘http://1.ru’.$key);
     $AC->request(‘http://2.ru’.$key);
     $AC->request(‘http://3.ru’.$key);
     $AC->request(‘http://4.ru’.$key);
     }

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

    1. 1. Примерно так.
      2. Отнаследоваться и подправить нужную часть под себя

  54. И еще один вопрос: как отобрать прокси у которых время отклика меньше 10 секунд?

    1. CURLOPT_CONNECTTIMEOUT — The number of seconds to wait while trying to connect. Use 0 to wait indefinitely.
      CURLOPT_TIMEOUT — The maximum number of seconds to allow cURL functions to execute.

  55. Прошу прощения за назойливость. 2 час ломаю голову. Подскажите пожалуйста как проверить загрузилась ли страница с прокси?
    Бывает так, что $response пуста, я проверяю в callback на пустое значение

    function callback_function($response, $info, $request){
    	echo '';
    	print_r($response);
    	print_r($request);
    	echo '';
    	
    	global $AC;
    	global $key;
    	
        if($info['http_code']!==200 or empty($response))
        {
    		$AC->get('http://ваыва/search?q='.urlencode($key));
            AngryCurl::add_debug_msg(
                "->\t" .
                $request->options[CURLOPT_PROXY] .
                "\tFAILED\t" .
                $info['http_code'] .
                "\t" .
                $info['total_time'] .
                "\t" .
                $info['url']
            );
        }else{
            AngryCurl::add_debug_msg(
                "->\t" .
                $request->options[CURLOPT_PROXY] .
                "\tOK\t" .
                $info['http_code'] .
                "\t" .
                $info['total_time'] .
                "\t" .
                $info['url']
            );
    
        }
        
        return;
    
    }

    Но вызов AC->get(‘http://ваыва/search?q=’.urlencode($key)); не происходит. Такое впечатление, что условие не срабатывает.

  56. Справился с предыдущими проблемами. Спасибо.
    Как сделать, чтобы страницы возвращались в строгой последовательности в которое были даны в
    foreach($keys as $key){
    $AC->get($key);
    }
    ?
    А то страницы возвращаются в порядке загрузки.

    1. Уважаемый, Вы хотите сделать из многопоточного парсера однопоточный?

  57. На сколько я понял, в $response кладется страница не в порядке подачи на парсинг, а в порядке загрузки. Мне же нужно, чтобы соблюдалась последовательность. Т.е. чтобы в response попадала не страница, которая загрузилась быстрее, а попадала страница в порядке очереди, поданная на парсинг. Либо, чтобы $response накапливал в себе все страницы, дожидался последней страницы, и только после уже отдавал.

  58. Скажите, а если парсить поисковые системы как вставить задержку между запросами?

  59. Привет, native.
    Ещё 1 вопрос:
    Есть сайт (реальный урл в последнем куске кода). Данные нужно передать POST запросом.
    Из консоли корректно отрабатывает такая строка:
    hostname$ curl -b nata -L -d «idcity=28&url=/prodazha_nedvizhimosti/kvartiry/» http://site.ru//setcity.php

    Из PHP корректно отрабатывает скрипт http://pastebin.com/udD2vu1M

    Не получается добиться работоспособности для AngryCurl.
    Делаю так: http://pastebin.com/8x28VR4M

    1. Почему здесь:

      public function request($url, $method = "GET", $post_data = null, $headers = null, $options = null)

      а у тебя:

      $AC->request($url, $method = "POST", $post_data,  $options);

      $options передаётся вместо $headers?

    2. Согласен, тут ошибся.
      Но вот так должно работать, но не работает:
      $options = array(CURLOPT_COOKIE => «city_id=28»);
      $url = ‘http://komnata.by/prodazha_nedvizhimosti/kvartiry/’;
      $AC->get($url, $options);

      (на чистом php_curl и из командной строки так работает)
      Я проверял через tcpdump. Кука не передается.

    3. $AC->get($url,null, $options);

      Здесь же написан синтаксис. Неужели так трудно прочесть документацию?

  60. Посыпьте мне голову пеплом.
    Возникла задача, как у этого парня:
    > Вопрос такой — как прицепить к урлу еще пользовательские данные
    > что б когда уже обрабатываешь запрос знать что ты обрабатываешь
    > например у меня есть список сайтов — как передать с урлом айди
    > сайта и получить его в обрабатывающей функции

    Я использовал параметр CURLOPT_CLOSEPOLICY для передачи id. Такая схема работала, и заставили меня думать, что я передаю его в $options.
    Теперь всё понятно, спасибо ещё раз, native, за терпение.

  61. А как с помощью этого скрипта получить контент страницы?
    Допустим поиска в Яндексе..

  62. PHP as Apache module
    ——-
    Использую php как FCGI. Класс ведет себя неадекватно. Для того,чтобы обработать данные в callback методе,а именно поставить страницу с защитой обратно в очередь, необходимо передавать экземпляр $AC в global переменную, затем в callback.
    Поразительно то, что все потоки обрабатывают один и тот же url одновременно.
    Цикл ...
    $AC->get($urlToGet); //
    /Цикл...
    $AC->execute(10);

    //callback
    Если результ не удовлетворяет
    $AC->get($response->url)

    1. может вместо етого $AC->get($response->url)
      ты хотел написать $AC->get($info[‘url’]);

      и есть вопрос
      у меня такой код
      # Callback function example
      function callback_function($response, $info, $request)
      {
      static $i=1;
      print $i++;
      global $AC;
      if($info[‘http_code’]!==200)
      {
      $AC->get($info[‘url’]);
      }
      if($info[‘http_code’]==200)
      {
      парсинг

      и в логах вижу 187.109.247.242:80 FAILED 0 3.963 http://адресс в последней строке , если ответ FAILED 0 то урл сюда if($info[‘http_code’]!==200) непопадает
      что посоветуете ?

  63. [Tue Oct 01 00:55:26 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 532 is not a valid cURL handle resource in Unknown on line 0
    [Tue Oct 01 00:55:26 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 532 is not a valid cURL handle resource in Unknown on line 0
    [Tue Oct 01 00:58:00 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 486 is not a valid cURL handle resource in Unknown on line 0
    [Tue Oct 01 00:58:00 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 486 is not a valid cURL handle resource in Unknown on line 0
    [Tue Oct 01 01:01:12 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 485 is not a valid cURL handle resource in Unknown on line 0
    [Tue Oct 01 01:01:12 2013] [error] [client 93.183.243.89] PHP Warning: (null)(): 485 is not a valid cURL handle resource in Unknown on line 0

    1. Какая версия os/apache/curl/php?
      Судя по этому — проблема существует под Win (может из-за проблем с ограничением кол-ва соедниений?).

  64. naive, привет, помоги пожалуйста

    1) при запуске скрипта на хостинге веб-сервер не отвечает до завершения его работы, возможно ли решить данную проблему заменой хостинг-провайдера/тарифного плана, или все же скрипт предназначен для запуска с локальной машины?

    2) После каждой итерации $AC появляется много отбракованных ссылок.
    Интересует красивое решение такого функционала:
    После первого прогона $AC1 собираем отбракованные ссылки, отправляем их во второй прогон $AC2, собираем еще раз и отправляем в 3-ий и т.д. до тех пор пока ссылки не закончатся.
    При этом:
    — тестирование прокси-серверов должно пройти по одному разу.
    — должен происходить именно сбор ссылок, а не отправка каждой «плохой» ссылки на отдельный callback.

    1. привет,

      1) не отвечает сам сервер? окно браузера? новые окна браузера? любой браузер? — уточни конкретику
      скрипт предназначен в большей степени для запуска на сервере, хотя можно и локально

      2) чем не подходит chains_of_requests.php из примеров?

    2. 1) да, действительно, не отвечает другая вкладка браузера именно при переходе по ссылкам этого домена, другие сайта работают отлично, в другом браузере мой домен отвечает.

      2) спасибо, обратил внимание, в итоге использую такую конструкцию с одним $AC

      do {
      foreach($kray as $link) {
      $link = ‘domain.com’;
      $AC->get($link);
      }
      unset($kray);
      $AC->execute(100);
      $AC->flush_requests();
      } while (count($link)>0);

  65. naive, а случайно не знаешь, как сделать, например, два независимых потока?
    Т.е. поток 1 и 2 работают сами по себе.
    Например, хочу получить данные с двух сайтов, но так, чтобы тормознутость загрузки одного сайта не распространялась на другой. Сейчас, например, при кол-ве потоков 2 и запросах:
    $AC->get(‘http://ya.ru’);
    $AC->get(‘http://yaffg.ru’);
    $AC->get(‘http://ya.ru’);
    $AC->get(‘http://yaffg.ru’);
    Яндекс естественно быстро загружается, а yaffg.ru нет и как следствие следующая загрузка Яндекса происходит только после решения по yaffg.ru.
    А в данном примере хотелось, чтобы все страницы Яндекса загрузил максимально быстро, а тормознутый сайт загружался сам по себе, не мешая остальным.

    1. Самый простой вариант — запускать в отдельных скриптах.

      Сложнее — можно на основе этого способа проверять в callback и добавлять новый запрос в зависимости от освободившегося.

    1. И еще вопрос, как можно поступить с прокси у которых авторизация?

    2. С точки зрения API — пока возможности нет. Реально -есть 2 варианта:
      1) через set установить значения array_proxy,n_proxy,use_proxy_list=true
      2) закомментировать эту строку

  66. Спасибо, огромное. По делу ничего написать не могу. Просто поблагодарю, спасибо вам за работу.

    1. Цитата из письма:

      Привет,

      я бы предложил использовать http://xml.yandex.ru
      количество запросов ограничено подтверждёнными в Я.Вебмастере ресурсами, тем не менее — это самый простой и удобный вариант.

      Что касательно валидности контента — то в callback проверяеет по regexp $result.
      Если не проходит — добавляете новый запрос в очередь, как это показано в примерах:
      https://github.com/2naive/AngryCurl/blob/master/demo/chains_of_requests.php

      Собственно, единственный вариант — определять каптчу и повторять запрос (если не использовать xml).

  67. Добрый день, читал много каментов тут, перед использованием решил уточнить:
    скрипт еще поддерживается? не заброшен ли? )
    Спасибо

    1. День добрый!

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

      Если будут вопросы или критичные баги — пишите — помогу/поправлю.

  68. Доброго времени суток, подскажите пожалуйста, можно ли использовать ваш клас для работы с прокси с авторизацией. Насколько я понял то, нет. Если можно подскажите пожалуйста как в ваш клас добавить поддержку прокси с авторизацией . Спасибо большое.

    1. Доброй ночи!

      Спасибо за нужное замечание.

      Добавить эту возможность можно, для этого нужно в метод request добавить установку CURLOPT_PROXYAUTH в формате username:password.

      Буду признателен за pull request на github.

  69. Добрый вечер!
    Столкнулся с проблемой повторного запроса, после авторизации. Не могли бы Буду очень благодарен если Вы подскажите, в чем может быть проблема?

    Задача: нужно авторизоваться в системе gogetlinks. А затем спарсить новые задания для сайта.
    Код:

    Подключаю классы...дальше:
    $cookie_jar = tempnam(PATH.'temp', "cookie");
    $data = array('e_mail' => $login,
    'password' => $pass,
    'remember' => "");
    $post_data = http_build_query($data);

    $AC = new AngryCurl('function_login_in_ggl');
    $AC->init_console();
    $AC->load_proxy_list(
    PATH . 'modules' . DIRECTORY_SEPARATOR . 'angry_curl' . DIRECTORY_SEPARATOR . 'proxy_list.txt',
    200,
    'http',
    'http://gogetlinks.net/'
    );
    $AC->load_useragent_list(PATH . 'modules' . DIRECTORY_SEPARATOR . 'angry_curl' . DIRECTORY_SEPARATOR . 'useragent_list.txt');
    $AC->request('http://gogetlinks.net/login.php', 'POST', $post_data, null, array(CURLOPT_COOKIEJAR => $cookie_jar));
    $AC->execute(200);

    function function_login_in_ggl($response, $info, $request) {
    $response = iconv("windows-1251", "utf-8", $response);
    if ($info['http_code'] !== 200) {
    AngryCurl::add_debug_msg("->\t" .$request->options[CURLOPT_PROXY] ."\t FAILED \t" .$info['http_code'] ."\t" .$info['total_time'] ."\t" .$info['url']);
    } else {
    if ($info['http_code'] == 200 && $response != "Некорректный Логин или Пароль") {
    $urlg = "http://gogetlinks.net/web_task.php";
    $AC2 = new AngryCurl('function_get_tasks');
    $AC2->request($urlg, $method = "POST", null, null, array(
    CURLOPT_PROXY => $request->options[CURLOPT_PROXY],
    CURLOPT_COOKIEFILE => $request->options[CURLOPT_COOKIEJAR],
    CURLOPT_COOKIEJAR => $request->options[CURLOPT_COOKIEJAR],
    CURLOPT_USERAGENT => $request->options[CURLOPT_USERAGENT]
    ));
    $AC2->execute();
    unset($AC2);

    }
    AngryCurl::add_debug_msg("->\t" .$request->options[CURLOPT_PROXY] ."\tOK\t" .$info['http_code'] ."\t" .$info['total_time'] ."\t" .$info['url']);
    }
    return;
    }
    function function_get_tasks($response, $info, $request) {
    $response = iconv("windows-1251", "utf-8", $response);
    if ($info['http_code'] !== 200) {
    AngryCurl::add_debug_msg($request->options[CURLOPT_PROXY] ."\t FAILED \t" .$info['http_code'] ."\t" .$info['total_time'] ."\t" .$info['url']);
    } else {
    print_r($response); // Выводим ответ ( ПРОБЛЕМА ТУТ)
    AngryCurl::add_debug_msg($request->options[CURLOPT_PROXY] ."\tOK\t" .$info['http_code'] ."\t" .$info['total_time'] ."\t" .$info['url']);
    }
    return;
    }

    Вот весь пока код… проблема в том, что выводится главная страница, и контент показывает, что авторизации нет (поэтому срабатывает ридирект со страницы с заданиями).

    Я проверяю $response в функции function_login_in_ggl, в нем находится скрипт, который редиректит нас на страницу наших сайтов (/my_sites.php), это значит авторизация проходит успешно.

    И ещё один вопрос, почему у меня локально при моем логине и пароле в ответе от GGL приходит «Некорректный Логин или Пароль«, а на сервере нормально авторизовывается?

    1. Такое чувтство, что не подхватываются куки (

      PS у меня прокси с авторизацией. добавил авторизацию, как Вы и сказали в функцию request. все нормально коннектится.

    2. Под множеством аккаунтов. Но пока делаю хоть под одним

  70. Подскажите что бы удалить из массива прокси можно ли так зделоть…
    $key = array_search($request->options[CURLOPT_PROXY], $AC->array_proxy, true);
    unset($AC->array_proxy[$key]);

    и данный прокси уже не будет встречаться?

    Дело в том что мне на первом прогоне нужно найти все плохие прокси и удалить их из массива и записать в файл….

    1. Да, можно (только через сеттер/геттер и добавив ещё $AC->__set(‘n_proxy’, $AC->__get(‘n_proxy’) — 1 ); )

      Только зачем это делать, если при загрузке прокси-листа это происходит автоматически (фильтрация неработающих прокси)

      Попробуйте пример.

  71. Исчо вопрос как узнать сколько потоков выполняется….
    Когда я задаю потики…
    $AC->post($result[‘url’],$result[‘post_data’],$options);
    $AC->post($result[‘url’],$result[‘post_data’],$options);
    $AC->post($result[‘url’],$result[‘post_data’],$options);
    в функции
    function callback_function($response, $info, $request)
    { осталось количество потоко Например 2 так как один уже выполнился…. }

  72. __set(‘n_proxy’, $AC->__get(‘n_proxy’) — 1 ); )
    Только зачем это делать, если при загрузке прокси-листа это происходит автоматически (фильтрация неработающих прокси) —>
    ————————————————————————————————————
    По поводу фильтрации я то понял…. Но вопрос в том что несколько потоков могут использовать один и тот же прокси… На каком ни будь потоке прокси может отвалится или запрос вывести каптчу в этот момент мне нужно удалить прокси из массива и из файла…… (я правильно понимаю)……

    А можно ли при фильтрации прокси их сразу удалять из файла…. или после проверки получить массив не работающих прокси в функцию callback….
    Я просто видел уже данная тема обсуждалось но не понял появился такой функционал….

    А так библиотека очень классная… Спасибо за Поддержку….

  73. У меня исчо одни вопрос к Вам сколько нужно ставить потоков на выполнение если загружаешь 1000 урлов….

    $AC->load_proxy_list(
    '../proxy_list.txt',
    1000,
    'http',
    'http://google.com',
    'title>G[o]{2}gle'
    );

    for ($i = 1; $i post('http://stupid.su/php-curl_multi/?'.$i,$value,$options);
    }
    $AC->execute(1000);//это правильно

    1. for ($i = 1; $i post(‘http://stupid.su/php-curl_multi/?’.$i,$value,$options);

      }

    2. В зависимости от настроек окружения — есть пределы по количеству открытых соединений.
      А так — на вкус и цвет.
      Попробуйте опытным путём найти наилучшее соотношение по скорости.

  74. Ребята подскажет )
    Вот тут такие дела, при фильтрации Proxy показывает ошибку:
    Undefined offset: 2 in /var/www/mag50.megamag.by/system/library/classes/AngryCurl.class.php on line 129
    ————————
    А вот сам пример кода:

    $nax=$AC->array_proxy;
    $key = array_search($request->options[CURLOPT_PROXY], $nax, true);
    if (isset($nax[$key])){
    unset($nax[$key]);
    $AC->__set(‘array_proxy’, $nax);
    $AC->__set(‘n_proxy’,count($nax)-1);
    }
    Где я ошибнулся Спасибо……

    1. Если array_search вернёт FALSE, то попытка найти элемент массива $nax со смещением $key вернёт undefined offset.

  75. Добрый день может у кого есть соображение по поводу ошибки http_code — 0;
    При попадание данной ошибки не хочет создаваться новый поток…..
    Как с этим жить…..

    1. Добрый день.

      Где происходит ошибка (на какой системе, окружении, итп)?
      Ошибка появляется в любом случае или только при подключении через прокси?
      Что даёт дамп ошибок curl?

      Чаще всего проблема обусловлена неверным адресом, маленьким таймаутом или отсутствием подключения к сети.

    1. День добрый,

      выложите готовый к запуску код на github/bitbucket — попробую посмотреть.

  76. Добрый день… Не подскажите почему не срабатывает таймаут… Мне нужно что бы была задержка при обращении к сайту……

     
    $options = array(
                    CURLOPT_CONNECTTIMEOUT => 60,
                    CURLOPT_TIMEOUT        => 40,
                    CURLOPT_REFERER        => 'https://google.ru/',
                    CURLOPT_AUTOREFERER     => TRUE,
                    CURLOPT_FOLLOWLOCATION  => TRUE,
                    CURLOPT_HEADER => true, 
                    CURLOPT_NOBODY => true,
                    CURLOPT_SSL_VERIFYPEER => 0
             );
    
        foreach ($mass_array as $key => $value){  
               $AC->get($value['url'],null,$options);
         }  
    
    ВОТ ЛОГИ.....
    Array
    (
        [url] => https://ipv4.google.com/sorry/IndexRedirect?continue=https://www.google.ru/search%3Fq%3DTactic%253A%2B14004%2B%25D0%259D%25D0%25B0%25D1%2581%25D1%2582%25D0%25BE%25D0%25BB%25D1%258C%25D0%25BD%25D0%25B0%25D1%258F%2B%25D0%25B8%25D0%25B3%25D1%2580%25D0%25B0%2B%25D0%2591%25D0%25B0%25D1%2588%25D0%25BD%25D1%258F%2B(%25D0%25BA%25D0%25BE%25D0%25BB%25D0%25BB%25D0%25B5%25D0%25BA%25D1%2586%25D0%25B8%25D0%25BE%25D0%25BD%25D0%25BD%25D0%25B0%25D1%258F%2B%25D1%2581%25D0%25B5%25D1%2580%25D0%25B8%25D1%258F)%2Bsite
        [content_type] => text/html
        [http_code] => 503
        [header_size] => 1282
        [request_size] => 1405
        [filetime] => -1
        [ssl_verify_result] => 0
        [redirect_count] => 1
        [total_time] => 1.405531
        [namelookup_time] => 1.7E-5
        [connect_time] => 2.0E-5
        [pretransfer_time] => 3.0E-5
        [size_upload] => 0
        [size_download] => 0
        [speed_download] => 0
        [speed_upload] => 0
        [download_content_length] => 3402
        [upload_content_length] => 0
        [starttransfer_time] => 0.485409
        [redirect_time] => 0.920083
        [certinfo] => Array
            (
            )
    
    )
    RollingCurlRequest Object
    (
        [url] => https://www.google.ru/search?q=
        [method] => GET
        [post_data] => 
        [headers] => 
        [options] => Array
            (
                [78] => 60
                [13] => 40
                [10016] => http://google.ru/
                [58] => 1
                [52] => 1
                [42] => 1
                [44] => 1
                [64] => 0
                [10004] => 177.85.235.243:8080
                [10018] => Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
            )
    
    )
    
    Array
    (
        [url] => https://www.google.ru/search?q=
        [content_type] => 
        [http_code] => 0
        [header_size] => 0
        [request_size] => 213
        [filetime] => -1
        [ssl_verify_result] => 0
        [redirect_count] => 0
        [total_time] => 0.791011
        [namelookup_time] => 2.2E-5
        [connect_time] => 0.395166
        [pretransfer_time] => 0
        [size_upload] => 0
        [size_download] => 0
        [speed_download] => 0
        [speed_upload] => 0
        [download_content_length] => -1
        [upload_content_length] => -1
        [starttransfer_time] => 0
        [redirect_time] => 0
        [certinfo] => Array
            (
            )
    
    )
    
    
    RollingCurlRequest Object
    (
        [url] => https://www.google.ru/search?q=
        [method] => GET
        [post_data] => 
        [headers] => 
        [options] => Array
            (
                [78] => 60
                [13] => 40
                [10016] => https://google.ru/
                [58] => 1
                [52] => 1
                [42] => 1
                [44] => 1
                [64] => 0
                [10004] => 180.180.123.56:3128
                [10018] => Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10
            )
    
    )
    
    1. CURLOPT_CONNECTTIMEOUT и CURLOPT_TIMEOUT не влияют на «задержку», см. документацию.

      Вам необходим однопоточный режим или многопоточный?

  77. Добрый день, а можете исчо ответить на один вопрос… У меня есть 100 рабочих прокси но почему google их банит сразу после 1-2 ого прогона…. Какое количество прокси должно быть для обработки 1000 урлов для GOOGLE спасибо…

  78. Добрый день по первому вопросу мне нужен многопоточный режим….Так как я думал что если поставить задержку то GOOGLE не так будет банить прокси… Может есть какие то варианты по поводу банна прокси…

    По второму вопросу у меня около 7000 useragent и 25 потоков…

  79. Добрый день… опять я со своими глупыми вопросами….Перечитал исчо раз Комменты и увидел что можно проверить IP адресс http://internet.yandex.ru/ и заметил что мой ip всегда повторяется в (IPv4)…. МОЖЕТ ли и за этого банить меня гугл….
    Мой IPv4: мой
    Мой IPv6:
    Внешний IP: 186.94.113.116

  80. Не подскажете, как в callback_function передать дополнительные параметры?
    Мне необходимо получать id товара, чтобы записывать в базу распарсеную страничку по данному товару.

    1. Хотя вроде разобрался 🙂
      При добавлении ссылок сделать $AC->post(ссыль,array(‘id_prod’=>’мой id товара’))

    2. нет. Все-таки не то 🙁 Получается мы таким образом передаем post-data
      Сайт отдает страницу без товаров. Если просто ссылку $AC->post(ссыль) — то отдается нормально, с товарами.
      Как все-же передать id моего товара в callback, который хотим спарсить?

    3. Вот и я столкнулся с такой проблемой.
      Я делаю выборку урлов из базы, соответственно хочу пометить какие урлы отработаны… Так как забивание их происходит постоянно.
      Надо передать переменную и вывести ее в callback_function.
      Но что-то никак не получается с этим вопросом….
      Помогите реализовать

  81. и за чего может выдавать данную ошибку
    Notice: Object of class AngryCurlRequest to string conversion in /var/www/mag50.megamag.by/system/library/classes/RollingCurl.class.php on line 310-> 193.203.220.15:8080 FAILED 0 31.017727 http://Object

  82. Здравствуйте, спасибо за проделанную работу. Angry curl очень понравился.
    Возник вопрос:
    если прокси на тесте оказался рабочим, но при запросе страницы отвалился, что происходит с запрашиваемым урлом? повторно, через другой прокси он не запрашивается?

    заранее, спасибо.

    1. Доброй ночи,

      На текущий момент, в такой ситуации, — повторного запроса не будет, поэтому запросы, вернувшие ошибку, иногда следует перепроверять повторно.

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

  83. и еще вопрос.
    Шлю запрос вида http://site.ru/?p=1&q=2 после отправки запроса происходит редирект и в callback-функцию возвращается $info[‘url’], но уже другой http://site.ru/lalala/tratata. Фактически я не могу сопоставить запрошенный урл и полученный.

  84. Честно сказать, комментарий проглядел, или когда читал не придал смысла ему. В своих рассуждениях пришел к той же мысли. Теперь мне не слезть с angry curl, айпи моего сервера забанен, так что теперь только вперед )))

  85. Добрый день. Спасибо, все получилось через заголовки.

    Возникли еще вопросы:
    в методе $AC->load_proxy_list можно задать количество потоков. Я правильно понял, что это число необходимо только для теста прокси-сервера? Как и юзерагенты в методе $AC->load_useragent_list нужны для теста прокси?

    Например, если мы загрузили массив из 100 прокси, а запрос отправить нам надо только на одну страницу. Получается, что пока все прокси не пройдут проверку не зависимо от их статуса придется подождать, прежде чем $AC->execute выполнится?

  86. Добрый день…
    не подскажите из за чего может появиться данная ошибка
    Warning: (null)(): 21 is not a valid cURL handle resource in Unknown on line 0

  87. Доброй ночи.
    Нужно вначале авторизироваться на сайте
    для дальнейшего парсинга товаров.
    Авторизация методом POST происходит.
    Я так понимаю что просто нужно
    передать на страницу авторизации свой логин/пароль методом POST
    или нет??

    Пытался сделать так авторизацию
    $login_params = array(’email_address’ => ‘artiomorange@yandex.ru’, ‘password’ => ‘powerbuilder’);
    $ac->post(‘http://www.snsgelezo.com/login.php?action=process’,
    $login_params);
    $ac->execute();
    что-то не выходит.

    1. Прошу прощения за поздний ответ.
      В заголовках ответа будут установлены сессионные куки, их необходимо использовать в последующих запросах.

  88. Добрый день…
    не подскажите из за чего может появиться данная ошибка
    Warning: (null)(): 21 is not a valid cURL handle resource in Unknown on line 0

    1. возможны ли конфликты с использованием библиотеки
      simple_html_dom
      ???

  89. Приветствую! Можно ли организовать на данном классе переход внутри соединения? например, соединились в одном из потоков к сайту, авторизовались и выполнили другие другие действия на этом сайте. При выполнении «переходов» на сайте соединение разрывать нельзя. Может есть возможность передать «переходы» в Callback функцию?

  90. Объясню проще для коммента выше: нужна возможность парсинга цепочки адресов для одного прокси. Такое возможно?

  91. Почему иногда класс может не срабатывать, а отдавать БЫСТРО белую страницу (без парсинга) или если вывести консоль, то отдает код 0? Если обновить страницу, то работает… Такой баг я замечал неоднократно.

    1. Причем, если «сломался» парсинг (быстро отдало код 0), то ломаются все потоки и ничего вообще не парсит.

  92. Здравствуйте.
    Некоторые сайты при большой частоте обращений начинают виснуть и выдавать ошибки (видимо, имеют слабый хостинг).
    Можно как-то ограничивать количество запросов в секунду?

    1. Предполагаю, что как-то так:
      Запросы разбить на пачки, и добавлять их только когда после предыдущего добавления прошло N-секунд/миллисекунд.
      Или может есть более рациональный способ?

  93. Добрый день!

    Не получается соединиться через прокси с https://kickstarter.com выдаёт ошибку 0 (FAILED 0)
    При этом через те же прокси с помощью file_get_contents через context подключается нормально.
    К https://google.com подключается и так и так.

    Как посмотреть подробнее в чём может быть дело?

    Спасибо.

    1. Отвечаю сам себе — чтобы посмотреть подробнее нужно выставить CURLOPT_VERBOSE=> true

      Проблема была в версии SSL (это видно из вывода curl’а при установленном CURLOPT_VERBOSE=> true) — ошибка «SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure», нужно установить CURLOPT_SSLVERSION=>4 и тогда всё работает.

  94. Не подскажете по какой причине логирование действий не отображаеться, локально все работает.

  95. Доброго всем времени суток.
    Отличный класс.
    Но хотел узнать, возможно ли обойти защиту cloudflare?
    jschl_answer и тд
    А то пару вкусняшек с 1 сайта хочу вытащить, а эта защита от антидос все портит

  96. Сергей, у меня в свое время защиту Incapsula (защита от DDoS) тоже не удалось обойти… И, кстати, мой вопрос связан с этим.

    Александр, не подскажите, почему данные cookie (с многопоточностью и прокси-серверами) не соответствуют своим потокам?
    Т.е., например, 10 потоков (а1, а2, … а10), я делаю запросы к одному сайту, cookie сохраняются в одном файле. На post-запрос а1 я получаю в файл cookie данные с1, на запрос а2 -> с2, а3 -> c3. И вот когда в многопоточном режиме, например, один из запросов оказывается ложным (добавляется соответствующие значения в cookie), этот cookie распространяется и на другие запросы.
    Вопрос в том, как сделать для каждого запрос/ответ свой cookie? Т.е. сделать например, 10 абсолютно независящих друг от друга потоков? А когда их кидают в одну кашу с этим много проблем возникает.

  97. А не подскажите, почему парсер сделан на curl, а не на сокетах? Сокеты практичнее и работают быстрее

    1. И не подскажите ли, как сделать задержку между запросами для разных сайтов? Т.е. в общем потоке multi_curl для сайта#1 задержка на выполнение следующего запроса 5 сек, а для сайта#2 30 сек.

  98. Подскажите пожалуйста, не работает Rollingcurl , в callback делаю print_r($info);
    получаю
    Array ( [url] => [content_type] => [http_code] => 0 [header_size] => 0 [request_size] => 0 [filetime] => 0 [ssl_verify_result] => 0 [redirect_count] => 0 [total_time] => 0 [namelookup_time] => 0 [connect_time] => 0 [pretransfer_time] => 0 [size_upload] => 0 [size_download] => 0 [speed_download] => 0 [speed_upload] => 0 [download_content_length] => -1 [upload_content_length] => -1 [starttransfer_time] => 0 [redirect_time] => 0 [redirect_url] => [primary_ip] => [certinfo] => Array ( ) [primary_port] => 0 [local_ip] => [local_port] => 0 )
    Оно почему то не обращается к сайтам. В чем может быть проблема?
    Использую php-fpm + nginx

  99. Подскажите как правильно реализовать следующее:

    — Есть 10 аккаунтов site.ru (кукизов от 10 аккаунтов)
    — Есть список прокси
    — Есть список юзер-агентов

    Нужно:
    Используя рабочий прокси№1 отправить GET-запрос на site.ru. Получить ответ, записать его в БД.
    Повторять эту процедуру 20 раз ИЛИ до тех пор пока ответ не будет содержать «ошибка».
    Далее продолжить отправлять GET-запросы, но уже под другим кукизом и с другого рабочего ПРОКСИ№2.
    Так пробежаться по всем аккаунтам (кукизам)

  100. Добрый вечер. Вопрос такой:
    Если работать через прокси, то для каждого IP определенный сайт выдает разные cookies. В скрипте, как я понял функционал подстановки в таком случае не предусмотрен? Подставляется последняя кука для этого сайта, все верно?

  101. Хорошо, тогда подскажите, пожалуйста, где поправить код, чтобы для каждой прокси выдавал свою куку для сайта?

  102. Добрый день!

    Задача:
    1) Не проверяя прокси-лист передать один определенный прокси для работы.
    2) Запустить парсинг страницы $AC->get($url);
    3) В колбек функции проверить, если через данный прокси не получилось, то только в этом случае загрузить весь прокси-лист и запустить заново парсинг


    if ($info['http_code']!=200){
    $AC->load_proxy_list(
    SITE_PATH . 'proxy_list.txt',
    # optional: number of threads
    200,
    # optional: proxy type
    'http',
    # optional: target url to check
    'http://google.com',
    # optional: target regexp to check
    'title>G[o]{2}gle'
    );

    $AC->get($url);
    $AC->execute(1);

    Вопрос по этапу №1 — как передать определенный прокси?

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

    Заранее спасибо!

  103. Задача2:

    1) Загрузить прокси-лист из файла.
    2) Получить из БД массив проксиков уже бывших в использовании
    3) Проверить если в загруженном прокси-листе есть те же прокси которые в массиве из БД, то их не использовать в парсинге. (но из листа не удалять)

  104. Ничего себе у Вас скорость реагирования на вопросы! Да еще и многопоточная! =)

    По задаче1 правильно понял? :


    $options[CURLOPT_PROXY] = "111.111.111.11:80";
    $AC->get($url, null, $options);
    $AC->execute(1);

    По задаче2:

    Если загрузим проки-лист из массив $AC->load_proxy_list($proxy array); он точно также будет чекаться как и при загрузке из листа? Или нужно в эту строку еще добавлять параметры как тут:

    // You may also import proxy from an array as simple as $AC->load_proxy_list($proxy array);
    $AC->load_proxy_list(
    // path to proxy-list file
    'proxy_list.txt',
    // optional: number of threads
    200,
    // optional: proxy type
    'http',
    // optional: target url to check
    'http://google.com',
    // optional: target regexp to check
    'title>G[o]{2}gle'
    );

  105. Добрый день. Что-то никак не могу наладить работу через прокси. Список прокси загружается, но все они помечаются как дохлые. Список тестировал, прокси живые, другие вводил. Все равно. Куда копать и что проверять? Заранее благодарен.
    Alive proxies: 0/30
    (!) Option ‘use_proxy_list’ is set, but no alive proxy available

    1. У кого проблема в том что недоступны прокси.

      Уберите из php.ini запрет на выполнение функции curl_multi_exec()

  106. callback_proxy_check вообще не вызывается ни разу. Поставил вывод в консоль при ее запуске. ничего не выводит.

  107. К сожалению, с проблемой не разобрался. «Чистый» класс запускается и работает. Проблема была при попытке подключения класса к yii фреймворку. Думаю, что-то с перепределением сеттеров в самом фреймворке.

  108. Привет! Спасибо за работу. Очень помогает!

    При использовании был найден баг:
    * При использовании несколько AngryCurl по очереди в одном скрипте, идет добавление переменной $alive_proxy и $rid, без стирания как таковых по завершению класса.
    Например:

    1-й цикл: $rid = 274 # Alive proxies: 45/275
    2-й цикл: $rid = 548 # Alive proxies: 87/275
    3-й цикл: $rid = 822 # Alive proxies: 134/275
    4-й цикл: $rid = 1096 # Alive proxies: 180/275
    5-й цикл: $rid = 1370 # Alive proxies: 219/275
    6-й цикл: $rid = 1644 # Alive proxies: 264/275
    7-й цикл: $rid = 1918 # Alive proxies: 310/275
    .....
    И далее в геометрической прогресии...

    Попытка очистки переменных не увенчалась успехом. Было добавлено:

    global $array_alive_proxy;
    global $array_proxy;
    global $rid;
    unset($array_alive_proxy);
    unset($array_proxy);
    unset($rid);

    В функцию: function __destruct() {….}

    Помогите, пожалуйста, найти решение! =)

    2) В такой же, геометрической прогрессии, идет загрузка процессора, вплоть до 220% от номинальной мощности (Linux, 15-ти минутный показатель ‘8,9’ на 4-х ядерном процессоре, т.е. номинал на сервере ‘4’).

  109. Выражаю огромную благодарность автору! В определенных моментах класс очень помогает. Творческих успехов и процветания!

  110. Подскажите, пожалуйста, как получить массив плохих прокси сразу после проверки списка прокси?


    // $proxy_from_db = прокси которые хранятся в БД
    // $already_used_proxy = эти прокси уже использовали, больше их нельзя использовать

    $proxy_to_check = array_diff($proxy_from_db, $already_used_proxy);

    $AC->load_proxy_list(
    $proxy_to_check,
    200,
    'http',
    'http://google.com',
    'title>G[o]{2}gle'
    );

    $alive_proxy = $AC->array_alive_proxy;
    $bad_proxy = array_diff($proxy_to_check, $alive_proxy);

  111. У меня одного трабла с socks5?
    пишу
    $AC->load_proxy_list($proxylist, 50, ‘socks5’, $site, »);
    и тупо висит, работает только с http

  112. Подскажите, пытаюсь разобраться с классом, не соображу.
    Взял example.php добавил прокси в файл.
    Далее запускаю скрипт, а он мне пишет:
    # Console mode activated
    # Start loading proxies
    * Proxy type set to: HTTP
    * Loaded proxies: 5
    * Removed duplicates: 0
    * Unique proxies: 5
    * Proxy test URL: http://google.com
    * Proxy test RegExp: title>G[o]{2}gle
    # Start testing proxies
    * Threads set to: 1
    * Starting connections

    И все, более ничего не происходит!

  113. Спасибо тебе за парсер. Все работает. Но у меня немного другая задача. У меня приватные прокси ( для стабильности) и их немного. всего 20 штук.

    Мне надо, чтобы мои 1000 ссылок парсились не скопом все сразу. ибо если сразу, то сайт будет их блочить из-за частых обращений. Как правильно установить интервалы между запросами с одно прокси?
    Или например, как все потоки 1 раз отработали, сделать слип секунду на 10-15.
    спасибо

  114. если я запущу 20 потоков просто так. то отработав, с этого же прокси начнет опять парсить и сайт клиент забанит его за частые запросы. Отклик от прокси хороший, потому что приватный. Поэтому это не подходит. для каждого прокси нужен интервал. Спасибо, но может еще по этому подскажете?

  115. Кто нибудь проверял есть разница с обычным курлом? Я что то ни какой разницы не заметил вовсе!

  116. Можно еще добавить работу с куки. Чтобы автоматически создавалась папка cookie и туда сохранялись куки в файл с названием НАЗВАНИЕ_ФАЙЛА-cookie-id. Как-нибудь так.
    И чтобы логи писались в файл с названием НАЗВАНИЕ_ФАЙЛА.log

  117. Блин, обрадовался, что такое чудо нашлось.. Но в local_ip упрямр всисит мой айпишник, при наличии 70 рабочих прокси. Не хочет меняться и все…

  118. Блин, обрадовался, что такое чудо нашлось.. Но в local_ip упрямр всисит мой айпишник, при наличии 70 рабочих прокси. Не хочет меняться и все…

  119. а что оно не работает? пробую запустить демо и везде одна и таже ошибка
    Warning: curl_setopt_array(): CURLOPT_FOLLOWLOCATION cannot be activated when an open_basedir is set in C:\***************\RollingCurl.class.php on line 275

  120. Приветствую. Использую Ваш класс (спасибо за реализацию), но вот появилась задача при использовании Proxy получить контент не всегда удаётся в связи с неработоспособностью самих прокси. Хотелось бы вернуть ссылку по которой небыл получен контент на повторную обработку:


    ini_set('max_execution_time', 0);
    ini_set('memory_limit', '128M');

    require("RollingCurl.class.php");
    require("AngryCurl.class.php");

    # Определение функции, вызываемой при завершении потока
    function callback_function($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']
    );
    // то что я побывал добавить для повторной обработки
    $AC->get($info['url']);
    // или
    $AC->add($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;
    }
    // Здесь необходимо не забывать проверять целостность и валидность возвращаемых данных, о чём писалось выше.
    }

    в код добавил пару строк в функцию, если ошибка обработки if($info[‘http_code’]!==200) то пытаюс добавить эту же ссылку на повторную обработку, но выводится ошибка
    PHP Fatal error: Call to a member function add() on a non-object in

    как исправить?

  121. Как обойти ошибку которая прерывает исполнение скрипта?

    $status = trim($value3->children(2)->children(0)->innertext);

    Fatal error: Call to a member function children() on a non-object in

    1. Извините, это к Вашему классу не относится.

  122. Подскажите как правильно сделать, желательно кодом.

    Есть запрос «https://1xstavka.ru/getTranslate/ViewGameResultsGroup» отдаёт json, запрос методом POST

    Пробую вот так

    $url = "https://1xstavka.ru/getTranslate/ViewGameResultsGroup";
    $headers = array('Content-Type: application/json; charset=utf-8');
    $options = array(
    CURLOPT_REFERER => 'https://1xstavka.ru/results/',
    CURLOPT_AUTOREFERER => TRUE,
    CURLOPT_FOLLOWLOCATION => TRUE,
    CURLOPT_HEADER => true,
    CURLOPT_NOBODY => true,
    );
    $post_data = array('Language' => 'ru', 'Params' => array(date("Y-m-d"), null, null, null, null, 0), 'Vers' => 6, 'Adult' => false, 'partner' => 51);
    $AC->post($url, $post_data, $headers, $options) ;

    в результате получаю ответ с кодом ошибки:
    -> FAILED 500 0.312645 https://1xstavka.ru/getTranslate/ViewGameResultsGroup

    вытащил из браузера:

    General

    Request URL:https://1xstavka.ru/getTranslate/ViewGameResultsGroup
    Request Method:POST
    Status Code:200 OK
    Remote Address:178.248.235.182:443
    Referrer Policy:no-referrer-when-downgrade

    Response Headers

    HTTP/1.1 200 OK
    Server: QRATOR
    Date: Sat, 10 Feb 2018 16:03:35 GMT
    Content-Type: application/json; charset=utf-8
    Transfer-Encoding: chunked
    Connection: keep-alive
    Keep-Alive: timeout=15
    Cache-Control: private
    Content-Encoding: gzip
    Vary: Accept-Encoding

    Request Headers

    POST /getTranslate/ViewGameResultsGroup HTTP/1.1
    Host: 1xstavka.ru
    Connection: keep-alive
    Content-Length: 109
    Accept: */*
    Origin: https://1xstavka.ru
    X-Requested-With: XMLHttpRequest
    User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 OPR/50.0.2762.67
    Content-Type: application/json
    DNT: 1
    Referer: https://1xstavka.ru/results/
    Accept-Encoding: gzip, deflate, br
    Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
    Cookie: lng=ru; typeBetNames=short; blocks=1%2C1%2C1%2C1%2C1%2C1%2C1%2C1; right_side=right; tzo=0.00; disable_live_express=true; disable_line_express=true; lite_version=1; indicator=3; completed_user_settings=true; SESSION=d13523f4e9b7dec26fc56c25d53bc764; unfixedRight=1; dnb=1

    Request Payload

    {"Language":"ru"}{"Params":["2018-02-10", null, null, null, null, 0]}{"Vers":6}{"Adult": false}{"partner":51}

    Так же не сильно понял как передать параметры POST запроса, правильно написал или нет, не уверен, прошу поправить если не так.

  123. у меня более простая реализация с полным контролем всей функциональности, код прост до безобразия, даже пачку уставки понижает, если сервер падает и держит на максимуме, при котором сервер отдаёт страницы, плавный старт — это уже само собой. И всё это на чистом пхп

  124. Добрый день, ну никак не могу разобраться с повторной обработкой ссылок на лету, понимаю, что в проверке на валидность надо заново запускать get, но оттуда взять именно саму ссылку, почитал, вроде люди писали, на счёт повторной проверки, но так и не понял, заранее спасибо.

Добавить комментарий для Alex I Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *