Главная дэшборда
Главная дэшборда

Вступление

Дело было вечером, делать было нечего... Я, как и многие в IT, периодически просматриваю вакансии, чтобы держать руку на пульсе рынка. И знаете, что бросается в глаза? Огромное количество позиций "Аналитик данных". Хоть это и не моя основная специализация (я больше по ML), теоретическая база у меня есть. И вот я подумал: а как бы мне сделать интересный пет-проект в этой области, чтобы и навыки прокачать, и самому не заскучать?

О! Я же уже много лет активный участник одного из сообществ на Пикабу. Почему бы не взять его в качестве "подопытного"? Не просто скачать данные, а пройти полный цикл: придумать, как их достать, где и как хранить, а в итоге — построить красивый интерактивный дашборд для анализа. Сказано — сделано.

Прежде чем мы погрузимся в технические дебри, вот результат, который можно потрогать руками (и я очень рекомендую это сделать, так будет интереснее читать дальше):

Интерактивный дашборд сообщества MLP на Streamlit Cloud

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

Разведка боем: Архитектура 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": Охота за просмотрами

Пост - Запрос с его айди -ответ в JSON, где v являются просмотрами
Пост - Запрос с его айди -ответ в JSON, где v являются просмотрами

Последняя проблема: просмотры. В 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')
Терминал во время работы. 179 месяцев ~14 лет
Терминал во время работы. 179 месяцев ~14 лет

От данных к инсайтам: Строим дашборд на 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 в будни. Ночью сообщество спит, утром и днем — работает.

  • "Золотой контент": Посты с гифками, хоть их и меньше, чем с видео, в среднем гораздо популярнее по рейтингу и просмотрам. Но безусловный король — это посты с картинками.

  • "Зал славы": В сообществе есть явные лидеры-контентмейкеры, которые создали десятки тысяч постов. Интересно, что пик их активности пришелся как раз на период после открытия сообщества.

    Да, Runoi - это я)
    Да, Runoi - это я)

Что дальше? От аналитики к Machine Learning

Этот проект — не конец, а только начало. Собранный датасет — это идеальная "песочница" для ML-экспериментов. Помимо очевидных идей, вроде создания рекомендательной системы на основе схожести тегов, или тематического моделирования (LDA) заголовков для поиска скрытых тем, у меня в планах есть кое-что поинтереснее.

Синергия проектов: Анализируем токсичность комментариев

В моем портфолио уже есть другой пет-проект: самописный NLP-классификатор, обученный на комментариях с Пикабу для определения уровня токсичности. И наш текущий датасет идеально с ним сочетается!

План действий:

  1. Расширение парсера: У нас есть ссылки на все 65 тысяч постов. Можно написать дополнительный модуль, который будет проходиться по этим ссылкам и асинхронно собирать тексты комментариев.

  2. Интеграция с ML-моделью: Собранные комментарии мы прогоняем через мой классификатор токсичности.

  3. Агрегация метрик: Для каждого поста мы можем рассчитать новые, уникальные признаки:

    • toxicity_score (средний уровень токсичности всех комментариев под постом).

    • toxic_comments_ratio (доля токсичных комментариев).

    • total_comments (общее количество комментариев, что само по себе является метрикой вовлеченности).

  4. Новый слой в дашборде: Эти метрики можно добавить в нашу базу данных и вывести в дашборд.

Какие вопросы это поможет исследовать?

  • Какие теги или темы вызывают самые "токсичные" или, наоборот, самые "ламповые" обсуждения?

  • Есть ли корреляция между рейтингом поста и уровнем токсичности в его комментариях? (Гипотеза: "хайповые", спорные темы могут собирать много плюсов, но и много негатива).

  • Как изменился средний уровень токсичности в сообществе после его открытия? (Стали ли комментарии "повеселее", как было отмечено в посте на Пикабу?).

  • Можно ли предсказать уровень токсичности в комментариях, основываясь на тегах и заголовке поста?

Это превращает наш аналитический проект в полноценное социологическое исследование онлайн-сообщества с применением NLP. Если эта тема будет интересна аудитории, с удовольствием напишу об этом отдельную статью-продолжение(а проект сделаю в любом случае).

Заключение

Этот пет-проект оказался гораздо глубже и интереснее, чем я предполагал вначале. Он позволил мне не только освежить знания в области скрапинга и анализа данных, но и пройти весь путь от сырого HTML до интерактивного продукта, который генерирует реальные инсайты.

Надеюсь, мой опыт был вам полезен. Буду рад услышать ваши идеи, критику и вопросы в комментариях!