-
Notifications
You must be signed in to change notification settings - Fork 0
Parsers RU
Эта страница описывает два публичных парсера, LogsHtmlParser и LogsFilterCatalogParser, которые превращают
HTML страницы в типизированные модели без единого HTTP-запроса. Здесь приведены их сигнатуры, разметка, которую
каждый из них ищет, и поведение при её отсутствии.
Любая публичная операция библиотеки проходит по одному и тому же конвейеру: клиент строит URI, источник данных
возвращает сырую HTML-строку, статический парсер превращает эту строку в записи. Парсеры занимают только
последнюю стадию. Это чистые статические функции над string — без HTTP-клиента, без cookie, без аутентификации
и без состояния, поэтому тот, кто получил HTML любым другим способом (собственным HttpClient, выгрузкой из
браузера, сохранённым на диск снимком, фикстурой в тесте), может разобрать его тем же кодом, который библиотека
использует внутри.
Два следствия, о которых стоит знать:
- Ничто в парсерах не может сломаться по сетевым причинам; единственный вид отказа — «ожидаемой разметки нет».
-
LogsParserClientне добавляет к ним ничего.GetLogsAsync— этоLogsHtmlParser.ParseLogs, применённый к HTML, который вернул источник данных; то же верно и для остальных трёх методов.
| Метод клиента | Вызываемый парсер |
|---|---|
GetLogsAsync |
LogsHtmlParser.ParseLogs |
GetAdminActivityAsync |
LogsHtmlParser.ParseAdminActivity |
GetTopOperationsAsync |
LogsHtmlParser.ParseTopOperations |
GetLogsFilterCatalogAsync |
LogsFilterCatalogParser.Parse |
public static partial class LogsHtmlParserПространство имён: LogsParser (корневое — файл лежит в Parsing/, но папка не равна пространству имён).
Все три метода сначала проверяют аргумент и выбрасывают исключение до обращения к разметке.
| Условие | Исключение |
|---|---|
html равен null
|
ArgumentNullException (наследник ArgumentException) |
html пуст или состоит только из пробельных символов |
ArgumentException |
public static LogsPage ParseLogs(string html)Разбирает страницу со списком логов в LogsPage.
| Искомая разметка | Во что превращается |
|---|---|
Показано с N по M из T (произвольные теги между словами допускаются) |
LogsPage.MetaInfo — LogPageMetaInfo(Start, End, Total)
|
| общий макет страницы (навбар, выбор сервера) | LogsPage.Account |
первый <tbody> на странице |
источник строк для LogsPage.Entries
|
| ячейка 1 строки | LogEntry.Timestamp |
| ячейка 2 строки |
LogEntry.Text, LogEntry.Html, LogEntry.RevealedValues
|
| ячейка 3 строки |
LogEntry.Sender / LogEntry.Target — Money, Bank, Donate и AdditionalInfo
|
| ячейка 4 строки |
LogParticipant.LastIp и LogParticipant.RegistrationIp
|
Подробности разбора строки:
- Строки, в которых меньше двух ячеек
<td>, пропускаются молча. - Ячейка действия сохраняет свой сырой внутренний HTML в
LogEntry.Html.LogEntry.Text— та же ячейка, но с удалёнными блоками, которые сайт прячет за «глазом» (class="app__hidden"), поэтомуTextсоответствует тому, что страница действительно показывает. Каждый скрытый блок становитсяLogRevealedValue, у которогоLabel— это атрибутdata-titleблока (пустая строка, если атрибута нет), аTextсохраняет разбиение блока на строки. - В ячейке данных маркеры
I:иII:определяют отправителя и получателя соответственно. Скрытый информационный блок относится к тому маркеру, за которым он следует, а не сопоставляется по индексу, поэтому строка только с участникомII:оставляетSenderравнымnull. Информационный блок превращается вLogAdditionalInfoтолько если содержит не меньше десяти значений<code>; иначе он отбрасывается. - Если в ячейке данных маркеров
I:/II:нет вовсе, первый скрытый блок читается как информация отправителя, а второй — как информация получателя. - В ячейке с IP каждому элементу
table-ipнужны один<code>(I:илиII:) и минимум две ссылки: первая ссылка — последний IP, вторая — регистрационный. -
SenderиTargetравныnull, пока не найдено хотя бы одно из трёх: деньги, дополнительная информация или последний IP.
Отсутствие разметки не приводит к исключению:
| Ситуация | Результат |
|---|---|
нет <tbody>
|
LogsPage с пустой коллекцией Entries; MetaInfo и Account всё равно заполняются, если присутствуют |
| нет строки постраничной навигации |
MetaInfo равен null
|
| нет разметки аккаунта |
Account равен null
|
строки, в которых меньше двух ячеек <td>
|
пропускаются, остальные строки возвращаются |
using LogsParser;
using LogsParser.Models;
LogsPage page = LogsHtmlParser.ParseLogs(html);
Console.WriteLine($"{page.Entries.Count} entries of {page.MetaInfo?.Total} total");
foreach (LogEntry entry in page.Entries)
{
Console.WriteLine($"{entry.Timestamp:u} {entry.Text}");
foreach (LogRevealedValue value in entry.RevealedValues)
{
Console.WriteLine($" [{value.Label}] {value.Text}");
}
if (entry.Sender is { } sender)
{
Console.WriteLine($" sender: {sender.Money} / {sender.Bank} / {sender.Donate} from {sender.LastIp}");
}
}Исключения
| Исключение | Когда |
|---|---|
ArgumentNullException |
html равен null
|
ArgumentException |
html пуст или состоит из пробельных символов |
FormatException |
не удалось разобрать отметку времени строки или числовую ячейку — значения читаются через DateTime.Parse, int.Parse, long.Parse с CultureInfo.InvariantCulture
|
public static AdminActivityReport ParseAdminActivity(string html)Разбирает отчёт об активности администраторов в AdminActivityReport.
| Обязательная разметка | Во что превращается |
|---|---|
<input name="min_period" value="…"> и <input name="max_period" value="…">
|
AdminActivityReport.Period (From, To) |
первый <tbody> на странице |
AdminActivityReport.Entries |
блоки app__hidden внутри этого <tbody>
|
AdminActivityEntry.Details — разбивка по дням |
Подробности разбора:
- Скрытые блоки удаляются из таблицы до чтения строк, после чего n-й скрытый блок сопоставляется с n-й оставшейся строкой.
- Строке нужно минимум десять ячеек
<td>, иначе она пропускается. - Внутри скрытого блока первая строка считается заголовком и пропускается; каждой следующей строке нужно минимум
семь ячеек
<th>, чтобы статьAdminActivityDay. -
AdminActivityEntry.TotalOnline— сумма значенийOnlineпо дням, поэтому она равнаTimeSpan.Zero, если у строки нет блока с деталями. -
AdminActivityMetaInfoвычисляется, а не берётся из разметки:AdminCount— количество записей,PeriodDays— разница периода в днях, ограниченная снизу нулём,TotalReportsиTotalBans— суммы по записям.
using LogsParser;
using LogsParser.Exceptions;
using LogsParser.Models;
try
{
AdminActivityReport report = LogsHtmlParser.ParseAdminActivity(html);
Console.WriteLine($"{report.Period.From:yyyy-MM-dd} — {report.Period.To:yyyy-MM-dd}");
Console.WriteLine($"{report.MetaInfo.AdminCount} admins, {report.MetaInfo.TotalBans} bans");
foreach (AdminActivityEntry entry in report.Entries)
{
Console.WriteLine($"{entry.Nickname} [{entry.Id}] online {entry.TotalOnline} over {entry.Details.Count} days");
}
}
catch (HtmlParsingException ex)
{
Console.Error.WriteLine($"unexpected markup: {ex.Message}");
}Исключения
| Исключение | Когда |
|---|---|
ArgumentNullException |
html равен null
|
ArgumentException |
html пуст или состоит из пробельных символов |
HtmlParsingException |
отсутствует один из инпутов периода — сообщение Admin activity period was not found.
|
HtmlParsingException |
нет <tbody> — сообщение Admin activity table was not found.
|
FormatException |
не удалось разобрать значение периода, дату или числовую ячейку |
public static TopOperationsReport ParseTopOperations(string html)Разбирает отчёт по топу операций в TopOperationsReport.
| Обязательная разметка | Во что превращается |
|---|---|
Данные за: yyyy-MM-dd |
TopOperationsReport.Date |
первый <tbody> на странице |
TopOperationsReport.Entries |
Подробности разбора:
- HTML декодируется перед поиском даты, поэтому подпись, экранированная HTML-сущностями, всё равно находится.
- Строке нужно минимум шесть ячеек
<td>, иначе она пропускается. - Ячейки 3 и 4 — IP-адреса. Значение, которое
IPAddress.TryParseотвергает, записывается в лог как предупреждение и заменяется наIPAddress.Loopback, а не приводит к исключению. -
TopOperationsMetaInfoвычисляется по записям:PlayerCount, суммаTotalTransactionsи суммаSumтипаulong.
using LogsParser;
using LogsParser.Exceptions;
using LogsParser.Models;
try
{
TopOperationsReport report = LogsHtmlParser.ParseTopOperations(html);
Console.WriteLine($"{report.Date:yyyy-MM-dd}: {report.MetaInfo.PlayerCount} players, {report.MetaInfo.TotalSum} total");
foreach (TopOperationsEntry entry in report.Entries)
{
Console.WriteLine($"{entry.Nickname} [{entry.Id}] {entry.TotalTransactions} ops, {entry.Sum} from {entry.Ip}");
}
}
catch (HtmlParsingException ex)
{
Console.Error.WriteLine($"unexpected markup: {ex.Message}");
}Исключения
| Исключение | Когда |
|---|---|
ArgumentNullException |
html равен null
|
ArgumentException |
html пуст или состоит из пробельных символов |
HtmlParsingException |
отсутствует подпись с датой — сообщение Top operations date was not found.
|
HtmlParsingException |
нет <tbody> — сообщение Top operations table was not found.
|
FormatException |
не удалось разобрать дату или числовую ячейку |
ParseLogs деградирует, ParseAdminActivity и ParseTopOperations выбрасывают исключение. Это сделано
намеренно, а не по недосмотру.
Список логов вполне законно может прийти пустым — фильтр, не совпавший ни с одним событием, даёт страницу без
строк, — поэтому пустая LogsPage является корректным ответом, а информация о постраничной навигации и контекст
аккаунта всё равно возвращаются, если страница их содержит. У двух отчётов, наоборот, всегда есть период (или
дата) и таблица, раз они вообще отрисовались; их отсутствие означает, что ответ — не та страница, которую
запрашивали, и HtmlParsingException сообщает об этом вместо отчёта с нулём администраторов.
public static partial class LogsFilterCatalogParserПространство имён: LogsParser.Parsing.
public static LogsFilterCatalog Parse(string html)Разбирает каталог фильтров с любой страницы, отрисовывающей форму поиска — в собственном конвейере библиотеки
это корень сайта /. Возвращает LogsFilterCatalog.
| Искомая разметка | Во что превращается |
|---|---|
опции <select name="type[]">
|
LogsFilterCatalog.Filters — по одному LogsFilterDefinition на опцию |
атрибут value опции |
LogsFilterDefinition.Code |
| текст опции | LogsFilterDefinition.Name |
блоки <div class="… js_component_filter_item …" data-filter-type="CODE">
|
LogsFilterDefinition.AdditionalParameters |
<label> внутри такого блока |
LogsFilterAdditionalParameter.Label |
атрибут name у <input>, <select> или <textarea> внутри такого блока |
LogsFilterAdditionalParameter.QueryKey (ключ dynamic[n]) |
| общий макет страницы (навбар, выбор сервера) | LogsFilterCatalog.Account |
Подробности разбора:
- Опции с пустым или отсутствующим
valueотбрасываются, поэтому опция-заполнитель реальной формы не становится фильтром. - Блоки динамических параметров группируются по
data-filter-typeи дедуплицируются поQueryKeyс помощьюStringComparer.Ordinal. - Фильтр, для которого блока не нашлось, получает пустую коллекцию
AdditionalParameters, но никогда неnull. - Если селект
type[]отсутствует, каталог возвращается с пустой коллекциейFilters— этот метод никогда не выбрасываетHtmlParsingException.Accountпри этом всё равно заполняется, если макет присутствует.
Значения QueryKey — это ровно те ключи, которые принимает LogsQuery.AdditionalParameters, поэтому каталог и
является штатным способом их узнать; см. Запросы и построитель URI.
using LogsParser.Models;
using LogsParser.Parsing;
LogsFilterCatalog catalog = LogsFilterCatalogParser.Parse(html);
foreach (LogsFilterDefinition filter in catalog.Filters)
{
Console.WriteLine($"{filter.Code} — {filter.Name}");
foreach (LogsFilterAdditionalParameter parameter in filter.AdditionalParameters)
{
Console.WriteLine($" {parameter.QueryKey}: {parameter.Label}");
}
}
Console.WriteLine($"signed in as {catalog.Account?.Nickname}");Исключения
| Исключение | Когда |
|---|---|
ArgumentNullException |
html равен null
|
ArgumentException |
html пуст или состоит из пробельных символов |
LogsPage.Account и LogsFilterCatalog.Account имеют тип LogsAccount?, и оба заполняются внутренним
парсером аккаунта, который читает макет, общий для всех аутентифицированных страниц: ссылку с ником в навбаре,
бейджи рядом с ней и селект server_number. Именно поэтому запрос логов и запрос каталога возвращают один и тот
же контекст аккаунта без отдельного вызова «кто я».
| Элемент макета | Во что превращается |
|---|---|
| ссылка выпадающего меню в навбаре | LogsAccount.Nickname |
| span-бейджи в правом списке навбара |
LogsAccount.Badges — с дедупликацией, порядковое сравнение |
опции <select name="server_number">
|
LogsAccount.AvailableServers |
числовой value опции |
LogsAccountServer.Id |
| текст опции |
LogsAccountServer.DisplayName; Name — тот же текст без ведущего префикса [id]
|
атрибут selected
|
LogsAccountServer.IsSelected |
Если не найдено ни одного из трёх — ни ника, ни бейджей, ни серверов, — Account равен null. Поэтому
неаутентифицированная страница даёт null, а не пустой LogsAccount. Сам парсер аккаунта не входит в публичный
API.
using LogsParser;
using LogsParser.Models;
string html = await File.ReadAllTextAsync(@"C:\captures\logs-2026-07-26.html");
LogsPage page = LogsHtmlParser.ParseLogs(html);
Console.WriteLine($"{page.Entries.Count} entries, account {page.Account?.Nickname ?? "(anonymous)"}");Подходит любая строка — тело ответа, полученное обычным HttpClient, фикстура в тесте, сохранённая браузером
страница. Ничто в парсерах не требует, чтобы HTML пришёл через транспорт библиотеки.
using LogsParser;
using LogsParser.Models;
using LogsParser.Parsing;
using var http = new HttpClient { BaseAddress = new Uri("https://arizonarp.logsparser.info") };
// Сессионная cookie, полученная другим способом; сами парсеры cookie не читают и не устанавливают.
http.DefaultRequestHeaders.Add("Cookie", "arizonarp_session=…");
string root = await http.GetStringAsync("/");
LogsFilterCatalog catalog = LogsFilterCatalogParser.Parse(root);
string listing = await http.GetStringAsync("/?server_number=201&type%5B%5D=warn");
LogsPage page = LogsHtmlParser.ParseLogs(listing);Если нужна полноценная сессия — вход, подтверждение двухфакторной аутентификации, обход anti-DDoS, повторные
попытки и учёт лимитов — используйте LogsParserClient поверх
LogsParserHttpDataSource вместо самостоятельных запросов. Парсеры остаются
доступны для случаев, когда HTML уже есть на руках.
LogsHtmlParser пишет в фасад логирования библиотеки под категорией LogsHtmlParser: Debug — итоговые
счётчики по вызову, Trace — пропущенные строки и извлечённые скрытые блоки, Warning — отсутствие
обязательной подписи или откат IP-адреса на loopback. LogsFilterCatalogParser не логирует. О подключении
фабрики логирования см. Логирование.
Всё в этом разделе — внутренние детали, а не публичный API; они могут измениться в любом релизе.
- Разбор выполняется самописными регулярными выражениями поверх внутреннего
HtmlFragmentReader. Он даёт извлечение сбалансированных блоков с учётом вложенности (парный закрывающий тег ищется по глубине вложенности, а не ленивым.*?), нормализацию текста со снятием тегов и схлопыванием пробелов, а также многострочный вариант нормализации, сохраняющий переносы строк внутри раскрываемых значений. - Шаблоны строк и ячеек намеренно «нестрогие»: строка матчится до следующего
<tr>или до конца секции таблицы, поэтому отсутствующий</tr>— который реальный сайт действительно отдаёт — не поглощает остаток таблицы. - Ряд русских литералов является несущей конструкцией, так как повторяет разметку реального сайта:
Показано с … по … издля информации о постраничной навигации иДанные за:для даты топа операций. То же относится к именам классов и атрибутовapp__hidden,data-title,table-ip,js_component_filter_item,data-filter-typeи к именам элементов формыtype[]иserver_number. - Шаблон постраничной навигации допускает произвольную разметку между ключевыми словами и числами, потому что
сайт оборачивает счётчики в теги
<strong>, а декодирование HTML теги не удаляет. - Некоторые шаблоны по построению чувствительны к порядку: блок динамического параметра распознаётся только
тогда, когда
classидёт доdata-filter-typeв<div>, а<label>— до элемента ввода. - Новые регулярные выражения оформляются как
private static partial Regexс атрибутом[GeneratedRegex], из-за чего классы парсеров объявленыpartial. - Сторонний HTML-парсер не используется. Это осознанное проектное решение, и именно поэтому у пакета всего две
зависимости:
Microsoft.Extensions.DependencyInjectionиMicrosoft.Extensions.Logging.Abstractions.
- Модели — все записи, которые возвращают парсеры
- LogsParserClient — фасад, который получает HTML и вызывает эти парсеры
- Запросы и построитель URI — построение URI, ответы на которые разбираются здесь
- Транспорт и аутентификация — как добывается HTML
-
Исключения —
HtmlParsingExceptionи остальная иерархия - Логирование — категории и уровни логирования парсеров
LogsParser · CC BY-NC 4.0 — non-commercial use only / только некоммерческое использование
English
- Home
- Getting Started
- LogsParserClient
- Requests and URI Builder
- Models
- Parsers
- Transport and Authentication
- Cookie Storage
- Dependency Injection
- Logging
- Exceptions
Русский
- Главная
- Начало работы
- LogsParserClient
- Запросы и построитель URI
- Модели
- Парсеры
- Транспорт и авторизация
- Хранение cookies
- Внедрение зависимостей
- Логирование
- Исключения