ИИ

Анализируем 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/

Фильтры и сортировка