Парсинг HTML с помощью PHP и SQL. Немного провокационный пример с анализом пользователей Хабра

в 10:22, , рубрики: html, php, sql, порно, тегиниктонечитает

Выковыривание информации из html — это скучно. Очень. Между тем, эта потребность выстреливает редко, но метко (© Суворов). Из-за этого есть спрос на готовые и короткие инструкции о том, как это сделать, чтобы не тратить время на изучение. Перед вами как раз такая.

Чтобы добавить хоть какой-то интерес скучнейшему занятию мы для примера будем парсить пользователей Хабра. А чтобы не мелочиться — ещё и реанимируем для этого экспериментальную библиотеку 11-летней давности.

Есть такой проект — htmlSQL. Старая библиотека времён Очаковских и покоренья Крыма (© Грибоедов). Она позволяет делать выборку из HTML в стиле SQL-запросов как на КДПВ, чем она мне когда-то и полюбилась.

Замечу, что результатами анализа пользователей кто-то будет возмущён, но мои намерения только самые хорошие и я вам это сейчас докажу.

Для нетерпеливых сразу основные изменения в библиотеке:

  1. Я переписал её с php4, чтобы работала на php5/7/8.

  2. Мелкие исправления, позволяющие парсить страницы не только с http, но и https.

  3. Исправлены некоторые старые ошибки, добавлены новые :-)

  4. Немного выросла скорость работы (вызовы устаревшей и медленной функции each, например, заменены на более шустрый обход foreach).

  5. Чтобы хоть как-то повысить читабельность начат рефакторинг, но через два вечера внезапно остановлен.

  6. Логические ошибки кое-где ещё остались. Из-за них библиотека пропускает некоторые комбинации «тег + класс».

  7. Исходники прилагаются.

Статьи (и новости) я взял не все, а только на самые холиварные темы. Их оказалось меньшинство, но всё равно немало. Итог по пользователям хабра из таких статей:

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

  2. Количество орфографических ошибок немалое, но катастрофы нет. Всё-таки Хабр пока не стал ресурсом для недоучившихся школьников.

  3. Не существует единого портрета среднестатистического комментатора. Образовалось целое семейство (назовём его Habracommentator Sapiens, или просто H. s). Семейство включает в себя различные виды, например: H. s. Talker, H. s. Newscaster и — конечно же! - H. s. Politicus.

  4. Семейство HabraWriter Sapiens, вопреки паническим сообщениям, не вымерло, хотя их популяция сократилась. В комментариях почти не участвуют.

  5. Основные изменения кармы происходят в комментариях, а не в результате публикаций*.

*по техническим причинам гипотеза требует перепроверки, но в этот раз мы это пропустим.

Увы, труд авторов на хабре ценят мало и мы как будто стремительно превращаемся в пикабу. Но я знаю, что это поправимо.

Библиотека htmlSQL

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

Пользоваться этим просто — чтобы спарсить какой-нибудь сайт вам нужно:

  1. вызвать функцию connect(), куда передать URL сайта

  2. вызвать функцию 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 строк.

Есть плюшки:

  1. можно назначить в настройках свой User-Agent

  2. можно не парсить всю html-структуру целиком, а только её кусок, указав границы «от» и «до»

  3. можно ограничить работу в рамках одного тега (header или body, например)

  4. никто не любит регулярные выражения, но библиотека их тоже поддерживает и даст вам возможность не любить их ещё раз.

Пару слов о коде внутри 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 забанить, поэтому работа на некоторое время встанет. Хабр блокировал меня пока что три раза.

Для выполнения работы потребуются:

  1. Крайне поверхностные знания SQL (уровня select + where, а всевозможные join, group by и т. п. не нужны)

  2. Более-менее ориентироваться в html-разметке (теги и классы)

  3. Уметь читать PHP или любой язык с си-подобным синтаксисом

  4. 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();
}

Собственно, схематично я показал все функции, которые спарсили хабр. По итогу мы получили:

  • «номер» статьи

  • название

  • ссылку на статью

  • ссылку на комментарии к статье

Сохранив всё это в БД спарсим некоторые метрики, чтобы понять портрет среднестатистического комментатора хабра.

Так как возможности библиотеки уже показаны, не будем останавливаться на остальном коде, а сразу укажу, что можно собрать:

  1. Текст комментариев. Он всегда в тегах p без классов.

  2. Список пользователей. Они живут в тегах span с классом «tm-user-info tm-comment__user-info». Внутри две ссылки: первая на профиль пользователя, вторая — на сам комментарий.

  3. Карму пользователя. Она находится в профиле пользователя в теге div с классом «tm-user-card__meta» (с некоторым мусором).

  4. Счётчики пользователя. Количество публикаций и комментариев пользователя расположены в одинаковых тегах 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. У него нет никакого реального опыта в промышленной разработке, поэтому поначалу у него будут:

  1. Простыня кода в одном-единственном файле

  2. Захардкоженные значения

  3. Магические числа на каждом шагу

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

  1. Имена классов не вынесешь в настройки, так как они чаще всего меняются вместе с самим html, а при изменении html всё равно придётся переписывать парсер заново. Из-за этого неизбежно много захардкоженных значений.

  2. Выборки одноимённых тегов часто помещаются в массив, из которого нас интересуют, например, только два элемента под индексом 4 и 5. В таких условиях от магических чисел тоже трудно избавиться.

А как вы считаете?

P. S. Здесь должна была быть реклама моего телеграм-канала, но его у меня нет, поэтому её здесь не будет :-)

Вместо этого можно почитать эту или вот эту статью той же тематики, но с помощью других инструментов.

Я рассчитываю, что в комментариях к статье не будет споров о поведении комментаторов, обсуждения рейтинга и кармы. Будет прекрасно, если в комментариях будут советы и замечания по парсингу и htmlSQL. Ещё лучше, если кто-то из вида комментаторов эволюционирует в вид полезных активных авторов. В прошлом ценность хабра была в качественных и иногда весёлых статьях и комментариях по сути.

Автор: Денис Сепетов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js