Получаем 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).
К данным добавляются префикс длины, что позволяет передать несколько сообщений за один раз.
Получается такая "матрёшка" из кодировок:
Каждый слой решает свою задачу:
- 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, одна структура может описывать:
Сам RenderTreeFrame выглядит так:
[Тип фрейма][Параметр 1][Параметр 2][Параметр 3][Параметр 4]
Тип фрейма определяет, что это за узел:
- 1 - HTML-тег (
Element); - 2 - текст (
Text); - 3 - атрибут (
Attribute); - 4 - Компонент Blazor (
Component); - 5 - Регион (Служебные данные) (
Region); - 8 - "Голый" HTML (
Markup).
Таблица строк
Чтобы не дублировать текст, все строки (имена тегов, атрибуты, значения, текст)
хранятся в конце пакета в таблице строк.
Каждая запись содержит смещение на строку, а сами строки записаны как:
[LEB128-длина][UTF-8 байты строки]
Процесс восстановления HTML
- Берём
RenderBatchполученный от SignalR - Читаем 5 чисел в конце файла - получаем смещение секций
- Переходим в секцию
ReferenceFrames- получаем все узлы дерева (RenderTreeFrame) - Последовательно читаем фреймы:
- еслиElement- создаём элемент;
- еслиAttribute- добавляем к предыдущему тегу;
- еслиText- вставляем текстовое содержимое в предыдущий тег;
- еслиMarkup- вставляем HTML как есть;
- еслиRegion- рекурсивно обрабатываем последующие фреймы; - Используем свойство subtreeLength, чтобы понять, где заканчивается элемент;
- Склеиваем всё в строку и получаем HTML.
P.S Реализация TS библиотеки доступна здесь.