Перейти к содержанию

Разбор t.me

Страницы

Страница URL Парсер
Лента канала GET https://t.me/s/{channel}[/{position}] parse_channel_page
Бесконечная прокрутка POST https://t.me/s/{channel}?before=… / ?after=… с X-Requested-With: XMLHttpRequest — возвращает JSON-строку с фрагментом HTML parse_more_page
Главная страница канала GET https://t.me/{channel} parse_preview_page

Запросы отправляются с заголовками мобильного Chrome. Редиректы не выполняются: неизвестный канал перенаправляет /s/{channel} на /{channel}, и это возвращается как 404.

Извлечение

Разбор реализован на Rust (rust/telegram-me-native/src/parse) и выполняется с отпущенным GIL: HTML один раз разбирается html5ever (соответствующий WHATWG парсер проекта Servo) и обходится CSS-селекторами. Python лишь декодирует получившийся JSON (pydantic-core) и пишет в лог пропущенные посты.

Порт на Rust побайтно воспроизводит исходный парсер на Python/selectolax: селекторы совпадают как с самим узлом, так и с его потомками, текст склеивается в точности как text() в lexbor, HTML сериализуется заново с правилами экранирования и порядком атрибутов lexbor, а теги форматирования, развёрнутые ради простого текста, остаются развёрнутыми при последующих чтениях того же поддерева — как с unwrap_tags в selectolax, работающим на месте. Результат зафиксирован снапшотами контракта.

Для каждого поста (.tgme_widget_message_wrap > .tgme_widget_message):

Поле Источник
id data-post="<channel>/<id>"
text .tgme_widget_message_text — html — это внутренняя разметка; string — текст с развёрнутыми тегами форматирования и <br> в виде \n
entities регулярные выражения по разметке текста (жирный, курсив, подчёркивание, зачёркивание, код, спойлер, хештеги, ссылки, эмодзи, кастомные эмодзи); варианты без look-ahead выполняются на DFA-движке regex, а три варианта с look-ahead дёшево проверяются и переходят на fancy-regex только при необходимости
media обёртки фото, плееры видео и видеосообщений в кружке, плееры голосовых, стикеры (варианты picture, i, video), изображения превью ссылок
poll вопрос, тип, варианты с процентами и число проголосовавших из .tgme_widget_message_poll
inline каждая .tgme_widget_message_inline_button в inline-клавиатуре
reply .tgme_widget_message_reply: автор, текст, миниатюра, целевой пост по его ссылке
preview_link .tgme_widget_message_link_preview
reacts .tgme_reaction: Telegram Stars, стандартные эмодзи (с URL спрайта), кастомные эмодзи (emoji-id)
footer time[datetime], просмотры, отметка «изменено», подпись автора
forwarded .tgme_widget_message_forwarded_from_name и _author

Устойчивость

  • Каждый пост разбирается изолированно. Если с постом что-то не так, он записывается в лог (Skipping unparsable post <channel>/<id>) и пропускается; остальная страница возвращается.
  • После разбора страница проверяется по моделям ответов. Посты, не прошедшие валидацию, отбрасываются тем же способом, так что один странный пост никогда не превращает весь ответ в ошибку.
  • 502 возвращается только тогда, когда страница в целом не имеет смысла.

Когда Telegram меняет разметку

  1. Воспроизведите проблему через uv run python scripts/record_fixtures.py <channel>, чтобы записать новую разметку.
  2. Исправьте парсер в rust/telegram-me-native/src/parse/ и запустите make rust и make native.
  3. Запустите make snapshots и просмотрите diff снапшотов — он точно показывает, какие ответы изменились.