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 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:27:31 +03:00
parent e48bca907d
commit 0cdea8d00e

252
README.md Normal file
View File

@@ -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" → название лиги (в <h2>)
"cl-left red" → счёт матча (в <div>)
"data-member-link="true">" → имя команды (в <span>)
"data-selection-price="X"" → коэффициент ставки
"data-sport-type="Basketball" → вид спорта
"green bold nobr" → игровое время
─────────────────────────────────────────────────────────
```
Алгоритм на каждой итерации:
1. Находит позиции всех трёх основных маркеров через `InStr()`
2. Выбирает ближайший (минимальная позиция)
3. Извлекает данные в зависимости от типа маркера:
- **Лига** → сохраняет как текущую лигу для последующих событий
- **Счёт** → извлекает счёт и время матча
- **Команда** → читает пару команд, затем находит 2 коэффициента и сохраняет событие
#### Извлечение счёта: `ExtractScore(html, startPos)`
Особая обработка — нужно отсечь блок `time-description`, который идёт после счёта
в том же `<div>`:
```html
<div class="cl-left red">51:43 (29:17, 22:26)
<span class="time-description">Кон.</span>
</div>
```
Функция ищет маркер `"time-description"`, затем **откатывается назад** до символа `<`,
чтобы отрезать тег `<span>` целиком, оставив только текст счёта.
#### Извлечение коэффициентов: `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)
```
| Код | Символ | | Код | Символ |
|-----|--------|-|-----|--------|
| 10401071 | А–Я | | 10721103 | а–я |
| 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