From 0cdea8d00ecb6b571a89a03cfd4b4b551c8b48d3 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Feb 2026 22:27:31 +0300 Subject: [PATCH] Add detailed README in Russian Document fetch strategy (MSXML2/WinHttp), UTF-8 decoding via ADODB.Stream, HTML parsing markers, and ChrW() Cyrillic encoding. Co-Authored-By: Claude Opus 4.6 --- README.md | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..287a65b --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +# Marathon Stats — VBA парсер live-статистики + +VBA-модуль для Excel, который загружает live-статистику спортивных событий с сайта +[Marathon Bet (BY)](https://www.marathonbet.by/su/live/45356) и выводит результаты +в отформатированную таблицу. + +## Возможности + +- Загрузка HTML-страницы через встроенные COM-объекты (без внешних зависимостей) +- Корректная обработка кириллицы (UTF-8) через `ADODB.Stream` +- Парсинг HTML по DOM-маркерам Marathon: лиги, команды, счёт, время, коэффициенты +- Профессиональное оформление Excel: цветовые схемы, группировка по лигам, замороженные панели + +## Быстрый старт + +### Из Excel + +1. Открыть Excel, нажать `Alt+F11` (редактор VBA) +2. **File → Import File** → выбрать `MarathonStats.bas` +3. Закрыть редактор, нажать `Alt+F8` → выбрать `FetchMarathonStats` → **Run** + +### Из PowerShell (автоматизация) + +```powershell +powershell.exe -ExecutionPolicy Bypass -File RunMarathon.ps1 +``` + +> **Требование:** В Excel должен быть включён доступ к объектной модели VBA: +> `File → Options → Trust Center → Trust Center Settings → Macro Settings → +> Trust access to the VBA project object model` + +## Структура файлов + +| Файл | Описание | +|---|---| +| `MarathonStats.bas` | Основной VBA-модуль (импортируется в Excel) | +| `RunMarathon.ps1` | PowerShell-скрипт для автоматического запуска | +| `MarathonStats.xlsm` | Результат — Excel-книга с данными | + +--- + +## Подробное описание кода + +### Точка входа + +```vba +Public Sub FetchMarathonStats() +``` + +Главная процедура. Последовательно выполняет: + +1. Создаёт/очищает лист `LiveStats` +2. Загружает HTML-страницу (`FetchPage`) +3. Парсит события (`ParseHTML`) +4. Записывает данные и форматирует таблицу + +--- + +### Загрузка страницы (Fetch) + +Ключевая проблема: сервер Marathon отправляет ответы в **gzip-сжатии** и разрывает +соединение с клиентами, которые не умеют его обрабатывать. Решение — двухуровневая +стратегия с двумя COM HTTP-объектами. + +#### `FetchPage(url As String) As String` + +Диспетчер — пробует два метода по очереди: + +``` +FetchPage + ├─ FetchWithXMLHTTP() ← основной + └─ FetchWithWinHTTP() ← резервный +``` + +#### Основной метод: `FetchWithXMLHTTP(url)` + +Использует **`MSXML2.XMLHTTP.6.0`** — клиентский COM-объект (на базе WinInet, как IE). +Автоматически распаковывает gzip, что критично для Marathon. + +```vba +Set http = CreateObject("MSXML2.XMLHTTP.6.0") +http.Open "GET", url, False +http.setRequestHeader "User-Agent", "Mozilla/5.0 ..." +http.setRequestHeader "Accept-Language", "ru-RU,ru;q=0.9" +http.Send +``` + +Заголовки имитируют браузер Chrome: +- **User-Agent** — без него сервер может отклонить запрос +- **Accept-Language: ru-RU** — для получения русскоязычной версии страницы + +**Декодирование UTF-8** (критически важно для кириллицы): + +```vba +Dim bodyBytes() As Byte +bodyBytes = http.responseBody ' сырые байты ответа + +Dim stream As Object +Set stream = CreateObject("ADODB.Stream") +stream.Type = 1 ' adTypeBinary +stream.Open +stream.Write bodyBytes ' записать байты +stream.Position = 0 ' перемотать в начало +stream.Type = 2 ' adTypeText +stream.Charset = "UTF-8" ' указать кодировку +FetchWithXMLHTTP = stream.ReadText ' прочитать как текст +stream.Close +``` + +Почему нельзя просто использовать `http.ResponseText`: +- VBA работает в кодировке Windows-1251 (ANSI) +- `ResponseText` может неверно интерпретировать UTF-8 байты +- `ADODB.Stream` гарантирует корректное преобразование UTF-8 → Unicode + +#### Резервный метод: `FetchWithWinHTTP(url)` + +Использует **`WinHttp.WinHttpRequest.5.1`** с заголовком `Accept-Encoding: identity`, +который просит сервер отправить **несжатый** ответ. Обходит проблему gzip на уровне +протокола. + +```vba +http.setRequestHeader "Accept-Encoding", "identity" +``` + +Декодирование UTF-8 — аналогичное через `ADODB.Stream`. + +#### Почему не работают другие подходы + +| Метод | Результат | Причина | +|---|---|---| +| `MSXML2.ServerXMLHTTP.6.0` | Соединение разорвано | Не обрабатывает gzip автоматически | +| `.NET HttpWebRequest` | Соединение разорвано | Аналогичная проблема | +| `WinHttp` без `identity` | Мусор вместо текста | Получает gzip-байты как строку | +| `InternetExplorer.Application` | Пустая страница | Контент загружается через JavaScript | + +--- + +### Парсинг HTML + +#### `ParseHTML(html As String)` + +Последовательный однопроходный сканер. Ищет ближайший из трёх маркеров и обрабатывает +его в зависимости от типа: + +``` +Маркеры в HTML Marathon: +───────────────────────────────────────────────────────── +"category-label simple-live" → название лиги (в

) +"cl-left red" → счёт матча (в
) +"data-member-link="true">" → имя команды (в ) +"data-selection-price="X"" → коэффициент ставки +"data-sport-type="Basketball" → вид спорта +"green bold nobr" → игровое время +───────────────────────────────────────────────────────── +``` + +Алгоритм на каждой итерации: +1. Находит позиции всех трёх основных маркеров через `InStr()` +2. Выбирает ближайший (минимальная позиция) +3. Извлекает данные в зависимости от типа маркера: + - **Лига** → сохраняет как текущую лигу для последующих событий + - **Счёт** → извлекает счёт и время матча + - **Команда** → читает пару команд, затем находит 2 коэффициента и сохраняет событие + +#### Извлечение счёта: `ExtractScore(html, startPos)` + +Особая обработка — нужно отсечь блок `time-description`, который идёт после счёта +в том же `
`: + +```html +
51:43 (29:17, 22:26) + Кон. +
+``` + +Функция ищет маркер `"time-description"`, затем **откатывается назад** до символа `<`, +чтобы отрезать тег `` целиком, оставив только текст счёта. + +#### Извлечение коэффициентов: `ExtractOddsPair(html, afterPos, odds1, odds2)` + +Ищет два ближайших атрибута `data-selection-price="X"` после позиции второй команды, +но **не дальше** следующего события (ограничение через `boundary`). Это предотвращает +захват коэффициентов от соседнего матча. + +--- + +### Обработка кириллицы + +В VBA-файлах (.bas) невозможно напрямую хранить кириллические литералы — при +экспорте/импорте они теряются. Поэтому все русские строки записаны через `ChrW()`: + +```vba +' "Лига" = ChrW(1051) & ChrW(1080) & ChrW(1075) & ChrW(1072) +' "Команда" = ChrW(1050) & ChrW(1086) & ChrW(1084) & ChrW(1072) & ChrW(1085) & ChrW(1076) & ChrW(1072) +' "Счёт" = ChrW(1057) & ChrW(1095) & ChrW(1105) & ChrW(1090) +' "Время" = ChrW(1042) & ChrW(1088) & ChrW(1077) & ChrW(1084) & ChrW(1103) +``` + +| Код | Символ | | Код | Символ | +|-----|--------|-|-----|--------| +| 1040–1071 | А–Я | | 1072–1103 | а–я | +| 1105 | ё | | 8212 | — (тире) | + +Название вида спорта определяется из атрибута `data-sport-type` и переводится +на русский через `Select Case`: + +```vba +Case "basketball": ' → Баскетбол +Case "football": ' → Футбол +Case "icehockey": ' → Хоккей +Case "tennis": ' → Теннис +``` + +#### `StripTags(s)` и `CleanText(s)` + +Две функции очистки текста, извлечённого из HTML: + +- **`StripTags`** — удаляет HTML-теги (`<...>`), заменяет управляющие символы + (chr 10, 13, 9) пробелами, схлопывает множественные пробелы +- **`CleanText`** — дополнительная нормализация: замена `vbCrLf`, `vbCr`, `vbLf`, + `vbTab` на пробелы с последующим схлопыванием + +Обе функции необходимы, потому что HTML Marathon содержит переносы строк и табуляции +внутри тегов со счётом и названиями лиг. + +--- + +### Выходная таблица Excel + +Лист `LiveStats` содержит: + +``` +Строка 1: Заголовок — "Marathon Bet — Баскетбол Live" +Строка 2: URL страницы +Строка 3: Дата/время загрузки +Строка 5: Шапка таблицы (закреплена) +Строка 6+: Данные + +Столбцы: + # | Лига | Команда 1 | Команда 2 | Счёт | Время | П1 | П2 +``` + +Форматирование: +- Шапка: тёмный фон, белый текст, жирный шрифт +- Чередование строк: белый / светло-серый +- Первая строка новой лиги: голубой фон, жирное название +- Счёт: крупный жирный шрифт, по центру +- Рамки: тонкие серые линии по всей таблице + +## Лицензия + +MIT