Здравствуйте, использую для парсинга «simple html dom» (php), парсить в дальнейшем необходимо будет сайтов 10. При загрузке страницы, проходит довольного много времени пока вся информация подгрузится и страница отобразится. Если парсить 1-2 сайта еще терпимо, но если брать больше — худо дело будет. Я подумал о том, что было бы не плохо парсить данные, с периодичностью раз в 10-20 минут (необходимая мне информация часто обновляется), и сохранять это все в БД.
Подскажите, пожалуйста, как это лучше всего реализовать? Подойдет ли для этой задачи «cron» или существуют более «правильные» методы?
Что бы парсер сам запускался подходит cron. Но как я понимаю суть вопроса в другом: "как быстро парсить ХХ сайтов". Мультипоточно. mCurl в помощь.
Для ориентирования по скорости приведу используемую мной схему. У меня почти 42 000 URL на проверке. Перед началом работ они складываются в Redis в виде стека (что бы потом параллельные потоки не скачали одну и туже страницу несколько раз). После чего по cron через bash запускается 10 php скриптов, каждый за раз качает 100 адресов, парсит данные со страницы через DOM, полученные данные пишет в базу. Т.е. кроме скачки страниц тут еще и медленные операции как то построение DOM и запись в РСУБД. На все уходит менее 20 минут, т.е. минимальная скорость около 30 страниц/сек.
Michail Wowtschuk: Довольно просто, через LPUSH/RPOP, т.к. пихаем данные слева, забираем с права (FIFO). В моем контексте важно обеспечивать не дублирование загрузок, поэтому заполнение очереди всегда в один процесс (гарантируется семафорами).
<?php
namespace alekciy\Yii\crawler;
use
alekciy\Yii\crawler\Model\Page
, alekciy\Yii\crawler\Model\Task
;
/**
* Загруженной считается страница у которой date_load != null.
*/
class WebCrawlerModule extends \CModule
{
...
/**
* Заполнить очередь на закачку адресами страниц. Вернет количество страниц поставленных в очередь.
*/
public function fillQueue()
{
$sem_key = ftok(__FILE__, 'f');
$sem = sem_get($sem_key, 1);
sem_acquire($sem);
// Если очередь не пустая, то нет смыла заполнять её, т.к. это может привести к дублированную данных в ней
$query_size = $this->_redis->lSize('page_load_query');
if ( !empty($query_size) ) {
sem_release($sem);
return 0;
}
$sql = '
SELECT
p.id
, p.url
, p.user_agent
FROM
' . Page::model()->tableName() . ' AS p
INNER JOIN
' . Task::model()->tableName() . ' AS t ON t.id = p.id_task
WHERE
p.date_next_try < NOW()
AND
p.date_load IS NULL
AND
p.load_count < t.max_try
';
$db_result = \Yii::app()->db->createCommand($sql)->query();
while( false !== ($row = $db_result->read()) )
{
$this->_redis->lPush('page_load_query', $row);
}
sem_release($sem);
return $db_result->rowCount;
}
/**
* Мультипотоком загружает страницы до тех пор, пока в очереди есть ссылки.
* Гарантирует RPS < 5 на один домен.
*/
public function pageLoad($total_download_session = 100)
{
// Через семофоры гарантируем, что запустились не более чем в 15 экземплярах
$sem_key = ftok(__FILE__, 'p');
$sem = sem_get($sem_key, 15);
sem_acquire($sem);
$total_page_load = 0;
$query_size = $this->_redis->lSize('page_load_query');
while ( !empty($query_size) )
{
// [1- Накапливаем ссылки для мультизагрузки (не более $total_download_session штук)
$url_list = array();
$domain_list = array();
for ($i = 0; $i < $total_download_session; ++$i)
{
$query_elm = $this->_redis->rPop('page_load_query');
if ( empty($query_elm['url']) ) {
break;
}
$url_list[$query_elm['url']] = array(
CURLOPT_USERAGENT => $query_elm['user_agent'],
);
// [3- Гарантируем, что хотя бы в этом процессе в рамках одного вызова mCurl (сейчас это 3-5 сек) не шлем больше 5 запросов на один домен
$domain = \Url::parse($query_elm['url'], PHP_URL_HOST);
if ( !array_key_exists($domain, $domain_list) ) {
$domain_list[$domain] = 0;
}
++$domain_list[$domain];
if ($domain_list[$domain] > 5) {
break;
}
// -3]
}
// -1]
// [2- Загружаем страницы и сохраняем результат
$download_page = \Yii::app()->mcurl->loadPageList($url_list);
foreach ($download_page as $page)
{
Page::model()->tryPageLoad($page);
++$total_page_load;
}
// -2]
$query_size = $this->_redis->lSize('page_load_query');
}
sem_release($sem);
return $total_page_load;
}
...
}
Michail Wowtschuk: Книгу? Хм... Стек это стек, что тут можно рекомендовать... А команды redis-а описаны тут: https://redis.io/commands#list Там даже есть очень любопытные позволяющие перекладывать атомарно из очереди в очередь и тут можно поиграться с приоритетами и неудавшимися закачками. Но мне на практике такое пока не потребовалось.
В фоне конечно кроном, а так, я бы заменил simple html dom на https://github.com/rmccue/Requests + code.google.com/p/phpquery, вроде было про них и на хабре. Если вы хотя бы поверхностность знакомы с python, то там есть очень удобная штука для парсинга grab