October 25

Получаем HTML из Blazorpack

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

Я подумал: "Ага, значит данные подгружаются динамически, следовательно должен быть запрос на сервер", однако открыв DevTools я увидел WebSocket, который судя по всему получал HTML от сервера

Как это работает?

Полная схема установки соединения и согласования протоколов

Делать полный разбор протокола я не буду, поэтому кратко пройдёмся по основным этапам:

Установка соединения

После того как браузер загрузил страницу происходит POST запрос на /_blazor/negotiate, который возвращает:

  • список поддерживаемых транспортных протоколов (WebSockets, Server-Sent Events, Long Polling);
  • ID соединения (connectionId);
  • Токен доступа (connectionToken).

Браузер перебирает список протоколов, и пытается установить соединение.
Если первый вариант не удался, то пробуется следующий и так далее.

Обмен данными (BlazorPack)

В качестве протокола обмена сообщениями между сервером и браузером используется протокол SignalR, который в свою очередь использует MessagePack (бинарный формат сериализации)

Также поверх этого используется ещё 1 слой - Binary Message Format (BMF).
К данным добавляются префикс длины, что позволяет передать несколько сообщений за один раз.

Получается такая "матрёшка" из кодировок:

BMF -> SignalR -> MessagePack

Каждый слой решает свою задачу:

  • MessagePack - компактное представление данных;
  • SignalR - вызов методов и маршрутизация сообщений (Ядро BlazorPack);
  • Binary Message Format (BMF) - framing сообщений (Передача нескольких сообщений за раз)
Пример сообщения

Получаем HTML

Данный этап можно назвать самым сложным, так как Blazor не отдаёт HTML в "голом" виде , вместо этого он отдаёт представление DOM в бинарном пакете (RenderBatch) разделённым на секции:

  • UpdatedComponents (ArrayRange<Diff>);
  • ReferenceFrames (ArrayRange<Frame>);
  • DisposedComponentIds (ArrayRange<int>);
  • DisposedEventHandlerIds (ArrayRange<long>);
  • StringTable (ArrayRange<string offsets>).

В конце пакета находятся 5 чисел (int32) — это смещения каждой секции в байтах (от начала массива).

Формат DOM

Сами узлы DOM хранятся в структуре под названием RenderTreeFrame, одна структура может описывать:

  • HTML-тег;
  • текстовое значение;
  • атрибут;
  • компонент Blazor;
  • "голый" HTML-фрагмент.

Сам RenderTreeFrame выглядит так:

[Тип фрейма][Параметр 1][Параметр 2][Параметр 3][Параметр 4]

Тип фрейма определяет, что это за узел:

  • 1 - HTML-тег (Element);
  • 2 - текст (Text);
  • 3 - атрибут (Attribute);
  • 4 - Компонент Blazor (Component);
  • 5 - Регион (Служебные данные) (Region);
  • 8 - "Голый" HTML (Markup).

Таблица строк

Чтобы не дублировать текст, все строки (имена тегов, атрибуты, значения, текст)
хранятся в конце пакета в таблице строк.

Каждая запись содержит смещение на строку, а сами строки записаны как:

[LEB128-длина][UTF-8 байты строки]

Процесс восстановления HTML

  1. Берём RenderBatch полученный от SignalR
  2. Читаем 5 чисел в конце файла - получаем смещение секций
  3. Переходим в секцию ReferenceFrames - получаем все узлы дерева (RenderTreeFrame)
  4. Последовательно читаем фреймы:
    - если Element - создаём элемент;
    - если Attribute - добавляем к предыдущему тегу;
    - если Text - вставляем текстовое содержимое в предыдущий тег;
    - если Markup - вставляем HTML как есть;
    - если Region - рекурсивно обрабатываем последующие фреймы;
  5. Используем свойство subtreeLength, чтобы понять, где заканчивается элемент;
  6. Склеиваем всё в строку и получаем HTML.

P.S Реализация TS библиотеки доступна здесь.