Анализируем MLP сообщество на Пикабу или как я спарсил 65 тысяч постов с Pikabu и построил интерактивный дашборд
Вступление
Дело было вечером, делать было нечего... Я, как и многие в IT, периодически просматриваю вакансии, чтобы держать руку на пульсе рынка. И знаете, что бросается в глаза? Огромное количество позиций "Аналитик данных". Хоть это и не моя основная специализация (я больше по ML), теоретическая база у меня есть. И вот я подумал: а как бы мне сделать интересный пет-проект в этой области, чтобы и навыки прокачать, и самому не заскучать?
О! Я же уже много лет активный участник одного из сообществ на Пикабу. Почему бы не взять его в качестве "подопытного"? Не просто скачать данные, а пройти полный цикл: придумать, как их достать, где и как хранить, а в итоге — построить красивый интерактивный дашборд для анализа. Сказано — сделано.
Прежде чем мы погрузимся в технические дебри, вот результат, который можно потрогать руками (и я очень рекомендую это сделать, так будет интереснее читать дальше):
В этой статье я расскажу, какой путь прошел этот проект, с какими "стенами" и "подводными камнями" я столкнулся, и как из простого скрипта выросла целая аналитическая система. Не бойтесь, скучной теории будет минимум, только практика и живой опыт.
Разведка боем: Архитектура Pikabu и выбор инструментов
Первое, что видишь на странице сообщества — бесконечная лента. Первая мысль — Selenium. Запускаем браузер, эмулируем скролл, собираем HTML. Рабочий, но дьявольски неэффективный подход. Для сбора десятков тысяч постов Selenium — это как ехать из Москвы во Владивосток на самокате: возможно, но долго, мучительно и по пути все может сломаться. Он нужен для сложных сайтов-одностраничников, а здесь точно должно быть что-то проще.
"Эврика!" и "Стена": Пагинация и ее лимиты
Я вспомнил, что раньше на Пикабу была классическая постраничная навигация, и решил проверить, не остался ли старый механизм доступен.
Простой запрос к URL вида .../community/mlp?page=2 подтвердил гипотезу: бэкенд все еще прекрасно отдает страницы по их номеру! Хотя для пользователей теперь работает "бесконечная" лента, старый добрый ?page=X остался как рудимент, которым грех не воспользоваться. Это кардинально меняло дело! Прощай, Selenium, здравствуй, requests (а лучше — его асинхронный брат aiohttp).
Но радость была недолгой. Написав простой скрипт-переборщик, я уперся в "стену": примерно после 1620-й страницы сервер начал отдавать пустые данные. При этом на сайте гордо красовалась цифра в 67 тысяч постов, что должно было дать около 3400 страниц. Стало ясно, что простой перебор — это ловушка для наивных парсеров, и бэкенд Pikabu просто не отдает слишком "глубокие" страницы.
"Ключ к архиву": Взламываем поиск по датам
Где искать архив, если не в поиске? Внутри сообщества есть поиск с фильтром по датам. Выбираю случайный месяц, жму "Найти" и смотрю на URL:
.../search?d=6500&D=6531
Никаких 2025-10-18. Только "магические числа". Но разница 6531 - 6500 = 31 намекает, что это количество дней. Осталось найти нулевую точку отсчета. Немного python и datetime:
from datetime import date, timedelta
end_date = date(2025, 10, 18)
pikabu_epoch = end_date - timedelta(days=6500)
# Результат: 2008-01-01
Бинго! "Эпоха Пикабу" — 1 января 2008 года. Теперь мы можем сгенерировать URL для любого временного диапазона и выкачать весь архив, двигаясь от свежих постов к старым, месяц за месяцем.
"Золотой API": Охота за просмотрами
Последняя проблема: просмотры. В HTML, который отдает сервер, их нет — там стоит заглушка-загрузчик. JavaScript подгружает их отдельным запросом. Снова открываем вкладку "Network", фильтруем по Fetch/XHR и находим его: GET-запрос на https://d.pikabu.ru/counters/story/{ID_поста}. Ответ — чистый JSON со всей статистикой.
План окончательно сформирован. Мы будем парсить HTML страниц поиска, чтобы получить основную информацию, а для каждого найденного поста делать дополнительный быстрый запрос к JSON API за просмотрами.
Сердце проекта: Асинхронный ООП-парсер
Чтобы собрать 67 тысяч постов(2 тысячи Пикабу-злодей не отдал по итогу) и для каждого сделать еще один запрос, нам нужна скорость. Делать это синхронно — значит ждать ответа от сервера на каждый из ~130 000 запросов по очереди. Это I/O-bound задача, идеальный кандидат для asyncio.
Почему asyncio?
Представьте, что вы заказали пиццу. Синхронный подход — это сидеть у двери и ждать курьера, ничего не делая. Асинхронный — сделать заказ и пойти смотреть сериал, а когда курьер позвонит в дверь (событие), подойти и забрать. Наш парсер делает тысячи "заказов" данных и обрабатывает их по мере поступления "ответов", не простаивая ни секунды.
Архитектура по SOLID
Чтобы код не превратился в "лапшу", я разделил его на классы с единой зоной ответственности:
PostData (dataclass): Простая и строгая структура для хранения данных одного поста.
PostParser: "Мозг", который умеет только одно — принимать на вход HTML и возвращать список объектов PostData. Он ничего не знает о сети или базах данных.
SQLiteStorage: "Руки", которые умеют только сохранять и обновлять данные в SQLite.
PikabuScraper: "Оркестратор", который управляет всем процессом: формирует URL, делает сетевые запросы, отдает HTML парсеру, а полученные данные — хранилищу.
Пара интересных фрагментов кода:
Главный цикл сбора архива, который идет назад во времени, месяц за месяцем:
# ... внутри класса PikabuScraper ...
async def run_archive_scraper(self, start_date, end_date, ...):
# ...
# Генератор, который выдает нам периоды (месяцы) для перебора
date_periods = list(self._generate_monthly_periods(start_date, end_date))
for month_start, month_end in tqdm(date_periods, desc="Сбор по месяцам"):
d_from = self._date_to_pikabu_day(month_start)
d_to = self._date_to_pikabu_day(month_end)
page_num = 1
while True:
# Формируем URL для страницы результатов поиска
search_url = self.SEARCH_URL.format(...)
# 1. Получаем основную инфу и ID постов
story_ids = await self._scrape_page_and_get_ids(session, search_url)
if not story_ids:
break # Посты за этот месяц кончились
# 2. Асинхронно запрашиваем просмотры для всех найденных постов
views_tasks = [self._fetch_and_update_views(session, story_id) for story_id in story_ids]
await asyncio.gather(*views_tasks)
page_num += 1
await asyncio.sleep(1.0) # Будем вежливы к серверу
И, конечно, "подводный камень", на который натыкаются многие:
# ...
html_bytes = await response.read()
# Не забываем про правильную кодировку!
return html_bytes.decode('windows-1251', errors='ignore')
От данных к инсайтам: Строим дашборд на Streamlit
Собранные данные — это просто цифры в базе. Чтобы они заговорили, нужна визуализация. Для этой задачи я выбрал Streamlit. Почему? Потому что это магия. Ты пишешь простой Python-скрипт, а на выходе получаешь готовое интерактивное веб-приложение. Превратить Jupyter-ноутбук в красивый дашборд можно буквально за час.
"Фишка" проекта: Автоматический поиск точки перелома
Самый интересный инсайт, который я заметил глазами — резкий скачок просмотров в определенный момент. Это было открытие сообщества. Но как найти эту дату автоматически? Здесь на помощь приходит Data Science, а именно — Change Point Detection.
Я использовал библиотеку ruptures, которая содержит эффективные алгоритмы для поиска таких "переломов" во временных рядах.
@st.cache_data # Кэшируем результат, чтобы не считать каждый раз
def find_changepoint(df):
# Готовим временной ряд: суммарные просмотры по месяцам
views_series = df.set_index('datetime').resample('M')['views'].sum()
points = views_series.values.reshape(-1, 1)
# Ищем 1 точку перелома (n_bkps=1)
algo = rpt.Binseg(model="l2").fit(points)
result = algo.predict(n_bkps=1)
# Возвращаем дату, соответствующую найденному индексу
return views_series.index[result[0] - 1]
Теперь дашборд сам находит эту ключевую дату и позволяет сравнивать статистику "До" и "После", что выводит анализ на совершенно новый уровень.
Результаты: Что мы узнали о сообществе?
А теперь — самое интересное, те самые инсайты.
"Эффект открытия": Это было не просто благом, а настоящим взрывом. Среднее количество просмотров на пост выросло с ~200 в закрытом периоде до ~4500 в открытом. Рост на 2150%!
"Портрет брони": Анализ активности по времени подтвердил гипотезу: мы — работающие люди. Пик постов приходится на выходные (особенно воскресенье) и на вечернее время после 20:00 в будни. Ночью сообщество спит, утром и днем — работает.
"Золотой контент": Посты с гифками, хоть их и меньше, чем с видео, в среднем гораздо популярнее по рейтингу и просмотрам. Но безусловный король — это посты с картинками.
"Зал славы": В сообществе есть явные лидеры-контентмейкеры, которые создали десятки тысяч постов. Интересно, что пик их активности пришелся как раз на период после открытия сообщества.
Что дальше? От аналитики к Machine Learning
Этот проект — не конец, а только начало. Собранный датасет — это идеальная "песочница" для ML-экспериментов. Помимо очевидных идей, вроде создания рекомендательной системы на основе схожести тегов, или тематического моделирования (LDA) заголовков для поиска скрытых тем, у меня в планах есть кое-что поинтереснее.
Синергия проектов: Анализируем токсичность комментариев
В моем портфолио уже есть другой пет-проект: самописный NLP-классификатор, обученный на комментариях с Пикабу для определения уровня токсичности. И наш текущий датасет идеально с ним сочетается!
План действий:
Расширение парсера: У нас есть ссылки на все 65 тысяч постов. Можно написать дополнительный модуль, который будет проходиться по этим ссылкам и асинхронно собирать тексты комментариев.
Интеграция с ML-моделью: Собранные комментарии мы прогоняем через мой классификатор токсичности.
Агрегация метрик: Для каждого поста мы можем рассчитать новые, уникальные признаки:
toxicity_score (средний уровень токсичности всех комментариев под постом).
toxic_comments_ratio (доля токсичных комментариев).
total_comments (общее количество комментариев, что само по себе является метрикой вовлеченности).
Новый слой в дашборде: Эти метрики можно добавить в нашу базу данных и вывести в дашборд.
Какие вопросы это поможет исследовать?
Какие теги или темы вызывают самые "токсичные" или, наоборот, самые "ламповые" обсуждения?
Есть ли корреляция между рейтингом поста и уровнем токсичности в его комментариях? (Гипотеза: "хайповые", спорные темы могут собирать много плюсов, но и много негатива).
Как изменился средний уровень токсичности в сообществе после его открытия? (Стали ли комментарии "повеселее", как было отмечено в посте на Пикабу?).
Можно ли предсказать уровень токсичности в комментариях, основываясь на тегах и заголовке поста?
Это превращает наш аналитический проект в полноценное социологическое исследование онлайн-сообщества с применением NLP. Если эта тема будет интересна аудитории, с удовольствием напишу об этом отдельную статью-продолжение(а проект сделаю в любом случае).
Заключение
Этот пет-проект оказался гораздо глубже и интереснее, чем я предполагал вначале. Он позволил мне не только освежить знания в области скрапинга и анализа данных, но и пройти весь путь от сырого HTML до интерактивного продукта, который генерирует реальные инсайты.
Надеюсь, мой опыт был вам полезен. Буду рад услышать ваши идеи, критику и вопросы в комментариях!
Весь код проекта на GitHub: https://github.com/Runoi/PikabuCommunityMLP
Потрогать дашборд руками: https://mlpcommuinity.streamlit.app/