Выковыривание информации из html — это скучно. Очень. Между тем, эта потребность выстреливает редко, но метко (© Суворов). Из-за этого есть спрос на готовые и короткие инструкции о том, как это сделать, чтобы не тратить время на изучение. Перед вами как раз такая.
Чтобы добавить хоть какой-то интерес скучнейшему занятию мы для примера будем парсить пользователей Хабра. А чтобы не мелочиться — ещё и реанимируем для этого экспериментальную библиотеку 11-летней давности.
Есть такой проект — htmlSQL. Старая библиотека времён Очаковских и покоренья Крыма (© Грибоедов). Она позволяет делать выборку из HTML в стиле SQL-запросов как на КДПВ, чем она мне когда-то и полюбилась.
Замечу, что результатами анализа пользователей кто-то будет возмущён, но мои намерения только самые хорошие и я вам это сейчас докажу.
Для нетерпеливых сразу основные изменения в библиотеке:
-
Я переписал её с php4, чтобы работала на php5/7/8.
-
Мелкие исправления, позволяющие парсить страницы не только с http, но и https.
-
Исправлены некоторые старые ошибки, добавлены новые :-)
-
Немного выросла скорость работы (вызовы устаревшей и медленной функции each, например, заменены на более шустрый обход foreach).
-
Чтобы хоть как-то повысить читабельность начат рефакторинг, но через два вечера внезапно остановлен.
-
Логические ошибки кое-где ещё остались. Из-за них библиотека пропускает некоторые комбинации «тег + класс».
-
Исходники прилагаются.
Статьи (и новости) я взял не все, а только на самые холиварные темы. Их оказалось меньшинство, но всё равно немало. Итог по пользователям хабра из таких статей:
-
Ценные комментарии — прежняя фишка хабра — ушли в прошлое. Котиков и маникюр в комментариях ещё не обсуждают, но исходный код и образцы конфигов из комментариев почти исчезли (по крайней мере, предназначенный для этого тег code встречается околонулевое количество раз).
-
Количество орфографических ошибок немалое, но катастрофы нет. Всё-таки Хабр пока не стал ресурсом для недоучившихся школьников.
-
Не существует единого портрета среднестатистического комментатора. Образовалось целое семейство (назовём его Habracommentator Sapiens, или просто H. s). Семейство включает в себя различные виды, например: H. s. Talker, H. s. Newscaster и — конечно же! - H. s. Politicus.
-
Семейство HabraWriter Sapiens, вопреки паническим сообщениям, не вымерло, хотя их популяция сократилась. В комментариях почти не участвуют.
-
Основные изменения кармы происходят в комментариях, а не в результате публикаций*.
*по техническим причинам гипотеза требует перепроверки, но в этот раз мы это пропустим.
Увы, труд авторов на хабре ценят мало и мы как будто стремительно превращаемся в пикабу. Но я знаю, что это поправимо.
Библиотека htmlSQL
Она появилась как эксперимент и, похоже, очень хорошо зашла некоторым пользователям. На гитхабе даже появились форки. Впрочем, и они давно померли, также как и оригинал (автор завершил эксперимент и забросил проект). Но я сделал форк и чуть-чуть изменил.
Пользоваться этим просто — чтобы спарсить какой-нибудь сайт вам нужно:
-
вызвать функцию connect(), куда передать URL сайта
-
вызвать функцию query(), куда передать SQL-подобный запрос того, что вы хотите выбрать.
Пример скажет сам за себя:
$wsql = new htmlsql();
if ($wsql->connect('url', "https://habr.com/ru/news/page4/") === false)
{
echo 'Не удалось подключиться к сайту: ' . $wsql->error . PHP_EOL;
exit;
}
if ($wsql->query('SELECT * FROM article WHERE $class=="tm-articles-list__item"') === false)
{
echo "Ошибка запроса: " . $wsql->error . PHP_EOL;
exit;
}
foreach ($wsql->fetch_array() as $row)
{
echo $row['text'] . PHP_EOL;
}
Приведённый код напечатает список всех новостей с четвёртой страницы новостного раздела хабра. Всё просто!
Обращу внимание, что функция connect() позволяет принимать на вход не только url, но также какой-нибудь локальный файл, либо — это прекрасно! — просто текстовую переменную, содержащую какой-нибудь html-код.
В каталоге с библиотекой есть 12 крошечных файлов с примерами не больше 35-45 строк.
Есть плюшки:
-
можно назначить в настройках свой User-Agent
-
можно не парсить всю html-структуру целиком, а только её кусок, указав границы «от» и «до»
-
можно ограничить работу в рамках одного тега (header или body, например)
-
никто не любит регулярные выражения, но библиотека их тоже поддерживает и даст вам возможность не любить их ещё раз.
Пару слов о коде внутри htmlSQL
Когда мне в декабре потребовалось спарсить некий сайт, я не сумел воспользоваться этой библиотекой, так как она категорически отказалась запускаться с php8. Хорошо, что кода там немного — меньше 700 строк — и я за вечер всё исправил. Код я тогда менял в целом не сильно.
Замечу, что автор, скорее всего, немец. По стилю кода он, вероятно, ещё и специалист в области немецкого жёсткого порно. Я не знаю, какие были нравы в эпоху php4 и, возможно, тогда этот код считался нормальным, но... Как по мне — это самое искромётное порево, которое я видел за последнее время. Поэтому в новогодние выходные я решил заняться рефакторингом. Обстоятельства сложились так, что уже через день-другой эту работу пришлось оставить — одна подруга срочно позвала меня есть блины и я был не в силах отказаться от излюбленного русского лакомства.
По итогу, я успел лишь поменять сигнатуру нескольких функций. Автор первоначальной версии явно любит самые пышные формы функций. Эти толстушки принимали в себя много аргументов, которые в основном передавались по ссылке. Когда их вызываешь, они в ответ, обычно, могут сделать всякое ничего не дают. Я, напротив, предпочитаю иметь дело с небольшими и стройными, поэтому заставил функции похудеть, принимать меньше параметров и что-то давать в ответ на вызов.
Также я организовал явное разделение полей и функций класса на приватные и публичные (в php4, по-моему, ещё не было ключевых слов public и private, поэтому все поля объявлялись через var). Для порядка весь исходный код в htmlsql.php приобрёл некоторую структуру: хаотично разбросанные функции упорядочены в более-менее логической последовательности по заветам Роберта Мартина. Теперь функция, вызываемая внутри другой функции, находится следом за ней и ниже, а функции, которые вызываются уже в ней — ещё ниже.
Из смешных мелочей: я не случайно отметил увлечение автора темой XXX, ведь даже в циклах со счётчиком он, обычно, использует переменную с именем $x. У менее искушённых разработчиков сложился иной обычай: счётчики называют $i или $j для вложенных циклов. Я решил не отступать от этого обычая.
Для работы библиотека использует локально установленный curl. Да, это не оговорка — консольная утилита вызывается в php через exec()! Возможно, в то время ещё не существовало расширения php-curl и иного способа попросту не было. По-хорошему так лучше не делать, но пока я не успел убрать этот атавизм. К чему я отметил эту особенность? У разных дистрибутивов путь к утилите curl может отличаться. У меня это, например, /usr/bin/curl, у кого-то может быть /usr/local/bin/curl или что-то похожее. Как вы уже поняли, даже путь к утилите захардкожен :-)
Именно в таком виде код и пребывает в настоящее время. Возможно, что я или кто-то ещё продолжит доработку, так как назвать эту библиотеку законченной нельзя. Она появилась с формулировкой «экспериментальная», такой же и осталась, хотя использовать в работе безусловно можно.
Практическая часть. Сможет ли htmlSQL взять Хабр?
Соблюдайте меры предосторожности: когда парсите, вас могут вычислить по IP забанить, поэтому работа на некоторое время встанет. Хабр блокировал меня пока что три раза.
Для выполнения работы потребуются:
-
Крайне поверхностные знания SQL (уровня select + where, а всевозможные join, group by и т. п. не нужны)
-
Более-менее ориентироваться в html-разметке (теги и классы)
-
Уметь читать PHP или любой язык с си-подобным синтаксисом
-
5 минут времени для прочтения по диагонали или 30 минут, если нужно научиться использовать примеры из статьи.
Для начала, самым прямолинейным способом переберём последние 20 страниц статей с Хабра и сохраним их:
for ($i = 1; $i <= 20 ; $i = $i + 1)
{
$url = "https://habr.com/ru/all/page$i/";
save_posts($url);
}
Функция save_posts() может быть примерно такой:
Тут скучный код
function save_posts($link)
{
echo "Обход статей на странице $link" . PHP_EOL;
$wsql = new htmlsql();
if ($wsql->connect('url', $link) === false)
{
echo 'Не удалось подключиться к сайту: ' . $wsql->error . PHP_EOL;
return false;
}
if ($wsql->query('SELECT * FROM article WHERE $class=="tm-articles-list__item"') === false)
{
echo "Ошибка запроса: " . $wsql->error . PHP_EOL;
return false;
}
$values = array();
foreach ($wsql->fetch_array() as $row) // есть ещё ->fetch_objects()
{
$text = trim($row['text']);
$posts = parse_article($text);
foreach ($posts as $article)
{
$title = $article['title'];
$url = $article['url'];
$url_to_comments = $article['comments'];
$post_id = get_post_id($url);
echo "Найдена статья $title" . PHP_EOL;
$values[] = array($post_id, $title, $url, $url_to_comments);
}
}
// тут $values можно сохранить в базу данных, например.
return true;
}
Функция ищет все теги article с css-классом «tm-articles-list__item». Именно он назначен всем статьям в ленте хабра.
Встроенная функция fetch_array() вернёт массив всех найденных статей. Это всё ещё сырой html, который дальше снова придётся парсить. Этим занимается функция parse_article(). Она может быть примерно такой:
Здесь ещё скучный код
function parse_article($text)
{
$result = parse_fragment($text, 'h2', "tm-article-snippet__title tm-article-snippet__title_h2");
if ($result === false)
{
echo "Не удалось разобрать вводную часть статьи" . PHP_EOL;
return false;
}
$posts = array();
foreach ($result as $row)
{
$introduce = trim($row['text']);
$link = parse_fragment($introduce, 'a', "tm-article-snippet__title-link");
$link = $link[0];
$post_title = parse_fragment($link['text'], 'span');
$post_title = $post_title[0];
$post_title = $post_title['text'];
$posts[] = array('url' => $link['href'], 'title' => $post_title, 'comments' => $link['href'] . 'comments/');
}
return $posts;
}
Здесь мы выбираем все теги h2 с классами «tm-article-snippet__title tm-article-snippet__title_h2», так как в них хранятся ссылка на статью и название. Обратите внимание, что мы больше не подключаемся к статье по url и разбираем с уже выцарапанный ранее html с помощью функции parse_fragment(). Она имеет примерно такой вид:
Здесь видна возможность парсинга строковой переменной
function parse_fragment($text, $tag_name, $class_name = '')
{
$wsql = new htmlsql();
if ($wsql->connect('string', $text) === false) // <<== вот это место
{
print "Не удалось начать разбор тегов $tag с классом $class_name: " . $wsql->error;
return false;
}
$sql = "SELECT * FROM $tag_name";
if (empty($class_name) === false)
{
$filter = "WHERE $class=="$class_name"";
$sql = "$sql $filter";
}
if ($wsql->query($sql) === false)
{
echo "Ошибка разбора тегов $tag с классом $class_name: " . $wsql->error;
return false;
}
return $wsql->fetch_array();
}
Собственно, схематично я показал все функции, которые спарсили хабр. По итогу мы получили:
-
«номер» статьи
-
название
-
ссылку на статью
-
ссылку на комментарии к статье
Сохранив всё это в БД спарсим некоторые метрики, чтобы понять портрет среднестатистического комментатора хабра.
Так как возможности библиотеки уже показаны, не будем останавливаться на остальном коде, а сразу укажу, что можно собрать:
-
Текст комментариев. Он всегда в тегах p без классов.
-
Список пользователей. Они живут в тегах span с классом «tm-user-info tm-comment__user-info». Внутри две ссылки: первая на профиль пользователя, вторая — на сам комментарий.
-
Карму пользователя. Она находится в профиле пользователя в теге div с классом «tm-user-card__meta» (с некоторым мусором).
-
Счётчики пользователя. Количество публикаций и комментариев пользователя расположены в одинаковых тегах span с классом «tm-tabs tm-user__tabs tm-tabs». Обратите внимание, что количество комментариев (и даже публикаций!) — это не всегда число. Количество комментариев может быть 4K, например.
Как уже говорил, я исключил из обзора все исключительно технические статьи, оставив те, где обсуждается Илон Маск, Мишустин, Роскосмос, SpaceX, НАСА, релокация, Минцифры, Байкал, Эльбрус, налоги и прочее, где чаще всего тусуются те, кто «мимокрокодил». Осталось всего 82 статьи из 400. Таким же образом выбрал новости, которых оказалось 250 из 400.
Нельзя сказать, что в этом перечне нет ничего технического, и что выборка вообще репрезентативна. Но нужно же как-то подбивать исходные данные для самых провокационных выводов? :-)
Для каждой статьи и новости был собран набор пользователей, написавших комментарии. Я покажу только обезличенные и только сводные данные. Ниже выложил сгруппированную таблицу с количеством комментариев, оставленных пользователями с кармой X и количеством публикаций Y. Результат грустный — в основном в комментариях таких статей участвуют люди, у которых вообще нет своих публикаций и почти нет кармы.
Карма |
Публикации |
Комментарии |
0 |
0 |
212 |
1 |
0 |
166 |
4 |
0 |
115 |
3 |
0 |
102 |
2 |
0 |
99 |
-1 |
0 |
54 |
-2 |
0 |
38 |
-3 |
0 |
25 |
-5 |
0 |
20 |
-4 |
0 |
20 |
-7 |
0 |
18 |
-6 |
0 |
16 |
-14 |
0 |
15 |
-10 |
0 |
14 |
-8 |
0 |
14 |
-9 |
0 |
13 |
-13 |
0 |
11 |
6 |
1 |
11 |
Как видно, пользователи с публикациями оказались лишь на восемнадцатом месте, уступая в том числе глубоко отхабренным пользователям.
Хабр — это саморегулирующееся сообщество. Инструмент саморегулирования — это карма. Чем она ниже, тем меньше у пользователя возможностей, даже если есть свои публикации.
А поменяется ли картина, если мы уберём из группировки количество публикаций? Под следующим спойлером ещё более грустная и объёмная таблица. В ней только количество комментариев, оставленных пользователями с кармой X.
Первые 50 строк выборки
Карма |
Комментарии |
0 |
228 |
1 |
180 |
4 |
132 |
3 |
108 |
2 |
106 |
-1 |
63 |
-2 |
42 |
-3 |
29 |
-6 |
24 |
-5 |
24 |
15 |
23 |
-4 |
21 |
-7 |
18 |
7 |
18 |
12 |
18 |
5 |
17 |
8 |
17 |
-14 |
16 |
-8 |
16 |
13 |
16 |
-10 |
14 |
16 |
14 |
17 |
14 |
-13 |
13 |
-12 |
13 |
-9 |
13 |
6 |
13 |
24 |
13 |
14 |
12 |
9 |
11 |
29 |
11 |
26 |
10 |
-18 |
9 |
19 |
9 |
20 |
9 |
33 |
9 |
-17 |
8 |
11 |
8 |
22 |
8 |
32 |
8 |
36 |
8 |
-16 |
7 |
-15 |
7 |
-11 |
7 |
10 |
7 |
21 |
7 |
23 |
7 |
35 |
7 |
-19 |
6 |
18 |
6 |
Как верно заметили сотрудники Хабра, карму в основном ругают отхабренные. Увы, так и есть. Но они же проводят уйму времени в комментариях и тут им карма почему-то не мешает.
Кстати, сможете найдите закономерность в таблице ниже?
А это последние 50 строк той же таблицы
Карма |
Комментарии |
78 |
1 |
80 |
1 |
87 |
1 |
91 |
1 |
92 |
1 |
93 |
1 |
96 |
1 |
98 |
1 |
102 |
1 |
110 |
1 |
111 |
1 |
120 |
1 |
121 |
1 |
125 |
1 |
127 |
1 |
128 |
1 |
130 |
1 |
131 |
1 |
133 |
1 |
143 |
1 |
144 |
1 |
156 |
1 |
165 |
1 |
180 |
1 |
181 |
1 |
182 |
1 |
189 |
1 |
193 |
1 |
194 |
1 |
200 |
1 |
204 |
1 |
210 |
1 |
228 |
1 |
229 |
1 |
245 |
1 |
283 |
1 |
303 |
1 |
308 |
1 |
317 |
1 |
322 |
1 |
343 |
1 |
363 |
1 |
381 |
1 |
403 |
1 |
545 |
1 |
567 |
1 |
647 |
1 |
676 |
1 |
867 |
1 |
1177 |
1 |
На первом месте комментаторов всё равно расположились пользователи с нулевой кармой. Далее с большим отставанием идут комментаторы с символической кармой, занимающие места со 2 по 5. Далее идут те, у кого карма от -1 до -5. Такие пользователи могут комментировать не чаще, чем 1 раз в 5 минут, что абсолютно не мешает им замыкать десятку лидеров. Те, кто имеет карму ещё ниже, могут комментировать не чаще, чем раз в сутки. Впрочем, это тоже не мешает им прочно обосноваться во второй десятке нашего рейтинга, заняв там сразу 4 строчки и написать почти 40% комментариев из этой десятки.
Большинство пользователей с высокой кармой весьма немногословны (по числу комментариев, но не всегда по их размеру).
Кстати, под новостями оставляют не так уж и мало комментариев. Например, под выбранными статьями насчиталось 3045 комментариев (~37 на статью), а у новостей — 2867 штук (~11 на каждую).
А как понять — кто живёт в комментариях? Пересекаются ли популяции комментаторов статей и новостей? Есть ли гибриды? Возможны ли скрещивания? Или, может быть, это разные виды, ареал обитания которых сильно разнится? В новостях я насчитал 744 пользователя, которые ни разу не засветились в статьях, а 646 комментаторов статей ни разу не комментировали новости. Всего же пользователей 1671, а значит существует небольшая популяция из 281 особи, которые являются гибридами первых двух. Для меня это оказалось самым неожиданным наблюдением, которое даже было перепроверено.
Какие сделаем выводы? А никакие. Помните: истинным мнением является то, которое больше нравится самому себе. Значит и выводы можно нарисовать те, которые захочется. Вы же читали книгу «Как врать с помощью статистики»?
Если результаты жизнедеятельности пользователей-комментаторов вам не нравятся — не гнобите их, а лучше покажите достойный пример, написав на Хабр отличную статью.
Здесь появились более оперативные возможности пожаловаться на неконструктивных комментаторов. Это сейчас доступно только авторам, старожилам и легендам Хабра. Неравенство? Почему этого нет у остальных? А может быть это просто справедливо? Тем более, что такую возможность можно попросить в индивидуальном порядке у Бумбурума.
Мысль вслух
Кто-то со мной не согласится, но пока я чинил библиотеку htmlSQL, мне пришла в голову идея, как можно применить парсинг сайтов в целях обучения сотрудников. Представьте ситуацию, что к вам в команду пришёл новичок, который знает только if/then/else какого-либо языка, а также теги html. У него нет никакого реального опыта в промышленной разработке, поэтому поначалу у него будут:
-
Простыня кода в одном-единственном файле
-
Захардкоженные значения
-
Магические числа на каждом шагу
Будет и много иных недостатков. Есть мнение, что недавние выпускники вузов избавляются от этого только тогда, когда сталкиваются с настоящей работой. В то же время, перечисленные мной недостатки — это почти норма в парсинге:
-
Имена классов не вынесешь в настройки, так как они чаще всего меняются вместе с самим html, а при изменении html всё равно придётся переписывать парсер заново. Из-за этого неизбежно много захардкоженных значений.
-
Выборки одноимённых тегов часто помещаются в массив, из которого нас интересуют, например, только два элемента под индексом 4 и 5. В таких условиях от магических чисел тоже трудно избавиться.
А как вы считаете?
P. S. Здесь должна была быть реклама моего телеграм-канала, но его у меня нет, поэтому её здесь не будет :-)
Вместо этого можно почитать эту или вот эту статью той же тематики, но с помощью других инструментов.
Я рассчитываю, что в комментариях к статье не будет споров о поведении комментаторов, обсуждения рейтинга и кармы. Будет прекрасно, если в комментариях будут советы и замечания по парсингу и htmlSQL. Ещё лучше, если кто-то из вида комментаторов эволюционирует в вид полезных активных авторов. В прошлом ценность хабра была в качественных и иногда весёлых статьях и комментариях по сути.
Автор: Денис Сепетов