Поиск по блогу

вторник, 15 июня 2010 г.

Разработка парсера каталога статей на движке WordPress с использованием PHP Simple HTML DOM Parser. Пошаговая инструкция

Всем доброго дня! Сегодня я опять отвечу на вопрос читателя блога. Вопрос был задан еще до объявления об Акции "Разобрать на моем примере", тем не менее:

Маша, помогите пожалуйста спарсить http://articlet.com/, точнее расскажите что нужно делать пошагово.


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

Изучение ресурса и разработка алгоритма



Итак, первое, что мы сделаем, — посмотрим, что из себя представляет ресурс. Это каталог статей, сделанный на WordPress. Структура рубрикатора — двухуровневая. То есть в корне находится несколько разделов, в каждом из которых могут находиться подразделы. Доступ к подразделам можно осуществить и с главной страницы и с внутренней страницы раздела. Нам, естественно, удобнее собрать все с главной.

Статьи могут находиться как в главных разделах, так и в подразделах.


Количество статей в подразделах указано в скобках после названия раздела. Так что можно проверить его, и, если оно равно 0, - не заходить. А можно не заморачиваться, заходить везде и на месте уже проверять, есть статьи или их нет.

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

Ссылка на страницы формируется как:
{ссылка на категорию}page/{номер страницы}


Определяемся с алгоритмом обхода: сначала парсим все названия и ссылки на разделы/категории с главной, потом заходим по всем полученным ссылкам, там парсим названия и ссылки всех статей, парсим содержимое этих статей (если выдача разбита на страницы - проходим по страницам).

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

CREATE TABLE `article` (
`n` int(11) NOT NULL auto_increment,
`rubrika_n` int(11) NOT NULL,
`title` varchar(255),
`link` varchar(255),
`content` text,
PRIMARY KEY (`n`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

CREATE TABLE `rubrika` (
`n` int(11) NOT NULL auto_increment,
`up` int(11),
`name` varchar(255),
`link` varchar(255),
PRIMARY KEY (`n`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;



Реализация алгоритма


Инструмент


Будем использовать библиотеку PHP Simple HTML DOM Parser.

Как я уже писала, на первом этапе просто спарсим все ссылки на разделы/категории. Для этого достаточно внимательно посмотреть на код страницы, понять структуру и задать "путь" для поиска нужных нам элементов.

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

<?php

include_once('simple_html_dom.php');

$html = file_get_html('http://articlet.com/');
$cat_n = 1;
foreach($html->find('div[id="categories"] ul li[id]') as $li)
{
// находим ссылку на раздел
$a = $li->find('div a',0);
if (isset($a)) {
echo 'n='.$cat_n.' '.$a->href.' '.$a->innertext.'<br>';
$up=$cat_n;
$cat_n++;

// находим дочерние категории
foreach ($li->find('ul li a') as $sub_a)
{
echo 'up='.$up.' '.$sub_a->href.' '.$sub_a->innertext.'<br>';
$cat_n++;
}
}
}
$html->clear();
?>


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

На этапе парсинга текста статей тоже не обойтись без "тестовых заходов". У сайтов на WordPress-е структура шаблонов похожая. Но весь div с классом post нам здесь не нужен, в нем много лишнего (информация о принадлежности к разделу, реклама гугля и связанные статьи). Поэтому от общего случая перейду к частному: так как картинок там в статьях все равно нет, то в качестве контента для записи в базу буду брать все абзацы (заключенные между <p></p>). То есть foreach-ем надо пройтись по div[class=post] div[!class] p. div[!class] нужен для того, чтобы взять именно див с контентом, а не с заголовком, у которого атрибут class присутствует.

Следующее, на что надо обратить внимание, это на всякие символы, которые желательно заменить перед вставкой в базу. Есть одинарные символы, а есть целые последовательности. Например, WP автоматически заменяет кавычки. После скачивания страницы вы увидите вместо кавычек непонятные символы. Чтобы это предотвратить, смотрим в коде символы. Я это, как всегда, делаю FAR-ом. После нажатия F3 и F4 находим по тексту ключевое слово, возле которого обнаружились "кракозябры" и смотрим последовательность.

Для открывающей кавычки это будет E2 80 9C (или в десятичных кодах - 226 128 156)
для закрывающей — E2 80 9D (226 128 157)
для — — E2 80 93 (226 128 147)



Их всех можно заменить на то, что вам угодно: или на escape-последовательности или на какие-нибудь простые символы.
Также можно заодним избавиться от лишних тегов (в частности - от ссылок).

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

Что касается перемещения по страницам, тут можно опять же пойти двумя путями:
1. Искать, есть ли ссылка на следующую страницу. И если есть — переходить.
2. Переходить в любом случае, используя шаблон. Если на той странице ничего нет - останавливаться и переходить к следующей категории.
Первый вариант, безусловно, правильнее. Особенно его "правильность" ощущается на ресурсозатратах при парсинге больших каталогов с сотнями категорий. Но в этом примере я упрощу себе работу и сделаю вторым способом.

$i=0;
while ($i < count($cat_array)) {
$cat_link = $cat_array[$i];
$i++;
$need_next_page = true;
$page_cnt = 1;
while ($need_next_page) {
if ($page_cnt > 1) $url= $cat_link.'/page/'.$page_cnt.'/';
else $url=$cat_link;
$cat_html = file_get_html($url);
$art_cnt=0; // счетчик статей на странице
foreach($cat_html->find('div[class=post] h3 a') as $article_a) {
$article_name = $article_a->innertext;
$article_link = $article_a->href;
$article_content = parse_article($article_link);
// добавляем в базу
$ins_q = 'insert into article (rubrika_n, title, link, content) '.
'values ('.$i.',\''.addslashes(getTextFromHTML($article_name)).'\',\''.addslashes($article_link).'\',\''.addslashes(getTextFromHTML($article_content)).'\')';
mysql_query($ins_q);
$art_cnt++;
sleep(1); // поставлю секундную задержку на всякий пожарный
}
$cat_html->clear();

// выводится по 20 статей на страницу. Если статей на странице меньше 20,
// то дальше можно не смотреть
if ($art_cnt < 20) $need_next_page = false;
else $page_cnt++; // инкрементируем счетчик страниц
}
}


parse_article (подробнее - в исходниках):

function parse_article ($article_link) {
$arc_html = file_get_html($article_link);
$content = '';
foreach ( $arc_html->find('div[class=post] div[!class] p') as $p) {
$content = $content. $p->innertext."<br>";
}
$arc_html->clear();
return $content;
}


$cat_array - временный массив, в который на первом этапе записала все ссылки на категории, чтобы лишний раз не обращаться к базе данных. Сделала цикл с while, а не foreach по привычке, чтобы в случае форсмажора можно было начать не с первой категории, а с любой, с какой захочу.

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

В качестве приятного бонуса выкладываю скачанную таким образом базу статей из того каталога (1,883 штук в 189 категориях) в разделе Халява. Каталог фришный, ничьих прав мы таким образом не нарушим.
___

Буду благодарна за ретвит! (Постепенно апгрейжу свой бложик, приделала вот кнопку от tweetmeme :) ).
___

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

Статьи схожей тематики:



8 комментариев:

  1. Очень хороша подборка скриптов и примеров, спасибо. Я подписался на ваш блог=) Но у меня возникла проблема при написании своего парсера. Мне удалось запарсить сайт с 3 уровнем вложенности категорий. Они то меня болше всего и интересуют как самые последние. На последнем шаге я пытаюсь парсить в этой каждой категории(а их окола 200 штук) посты(а их в каждой категории окола 30 на странице). из-за чего у меня растет физическая память и в конечном итоге вылетает денвер.
    вот кусок кода где по моим соображениям идет нагрузка:

    foreach ( $tmp as $key => $value ) {
    $tidy->parseFile($site_url.$tmp[$key]['url'], $config, 'latin1');
    $tidy->cleanRepair();
    $str = $tidy->value;
    $html = str_get_html($str);
    foreach ( $html->find('div[class="CIAContainer"] div[align="left"]') as $div) {

    где $tmp - массив с категориями последнего уровня:

    Array = (
    [id последней категории] = array(
    [url последней категории] => ....
    [id родителя категории] => ....
    [link последней категории] => .....
    [title последней категории] => ...
    )
    [1] = array(
    ....
    )
    ....
    }

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

    ОтветитьУдалить
  2. Я не знаю, как снять нагрузку и вообще почему она у вас возникает. Никогда с таким не сталкивалась. А вы не пробовали запустить парсер на сервере? За денвером вообще замечена тормознутость...

    ОтветитьУдалить
  3. Вот и у меня на втором уровне вложенности память закончилась.
    Перенес скрипт на другой хостинг - все работает. Дело оказалось в настройках сервера, сколько памяти выделяется для пользовательских скриптов. Мой хостинг "7,99WMZ в год + имя в подарок" жаден 64Mb. Решить проблему можно двумя способами:
    1. Увеличить объем доступной памяти в настройках сервера (гугль в помощь, информации валом)
    2. Добавить в начало скрипта строку
    ini_set("memory_limit","128M");
    128 спасло мой парсер, но ИМХО многовато кушает. Вордпресс чудно на том же хостинге крутится.

    ОтветитьУдалить
  4. Доктор, я к Вам и вот по какому вопросу...
    Есть ли у библиотеки Simple HTML DOM Parser ограничение на уровень вложенности тегов? У меня создалось впечатление что глубже 3-го уровня искать она не хочет :( Хотя в документации никаких упоминаний об этом не нашлось. Бьюсь все выходные, безрезультатно. Но ввиду отсутствия программерского экспириенса не исключаю кривизну рук.

    ОтветитьУдалить
  5. El Coyot, я уровни не считала, но тоже сталкивалась, что не искалось, если задать сложный запрос. Поэтому обходила эту фигню тем, что разбивала процесс парсинга на части: сначала искала большие блоки, а потом уже в этих блоках искала то, что надо.

    ОтветитьУдалить
  6. Сделал тупой тестовый файлик с кучей вложенных дивов - всё чудненько находится. Моя проблема судя по всему в том, что внутри моего div галерейка. Что-то а jQuery похоже.
    echo $html->GetElementbyID("allGalleryContainer")->outertext; выдает только пустой div. А в нем по идее еще 3 штуки вложено. Как победить - ума не приложу :(
    Если вдруг сталкивались с чем-то подобным, буду крайне признателен за совет.

    ОтветитьУдалить
  7. Я не сталкивалась. Будем надеяться, что прочитают те, кто сталкивались :) Мне тоже интересно :)

    ОтветитьУдалить
  8. Разобрался сам :) Все очень просто, не смотря на то что сложно )
    Вот такое присвоение значения переменной
    $html = file_get_html($url)
    не равнозначно загрузке того же урл в браузер. А содержимое моего дива дописывает небольшой яваскриптик, отрабатывающий при загрузке страницы.
    Нужные мне данные, src картинок, оказались в тексте скрипта (теперь думаю как их от туда выдрать, без регулярных выражений похоже не обойтись). Минус - если вдруг скрипт будет во внешнем файле, найти откуда ноги растут сложно.

    ОтветитьУдалить

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

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

Поделиться