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:
252
README.md
Normal file
252
README.md
Normal 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)
|
||||
```
|
||||
|
||||
| Код | Символ | | Код | Символ |
|
||||
|-----|--------|-|-----|--------|
|
||||
| 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
|
||||
Reference in New Issue
Block a user