-
Notifications
You must be signed in to change notification settings - Fork 0
Parsers
This page covers the two public parsers, LogsHtmlParser and LogsFilterCatalogParser, which turn a page's HTML
into typed models without performing any HTTP request. It documents their signatures, the markup each one needs
to find, and what happens when that markup is absent.
Every public operation of the library follows the same pipeline: the client builds a URI, the data source returns
a raw HTML string, and a static parser converts that string into records. The parsers occupy the last stage only.
They are pure static functions over a string — no HTTP client, no cookies, no authentication, no state — so a
caller who obtains the HTML by other means (an own HttpClient, a browser export, an on-disk capture, a fixture
in a test) can parse it with exactly the same code path the library uses internally.
Two consequences worth knowing:
- Nothing in the parsers can fail for network reasons; the only failures are "the expected markup is not there".
-
LogsParserClientadds nothing on top of them.GetLogsAsyncisLogsHtmlParser.ParseLogsapplied to the HTML the data source returned, and the same holds for the other three methods.
| Client method | Parser it calls |
|---|---|
GetLogsAsync |
LogsHtmlParser.ParseLogs |
GetAdminActivityAsync |
LogsHtmlParser.ParseAdminActivity |
GetTopOperationsAsync |
LogsHtmlParser.ParseTopOperations |
GetLogsFilterCatalogAsync |
LogsFilterCatalogParser.Parse |
public static partial class LogsHtmlParserNamespace: LogsParser (the root namespace — the file lives in Parsing/, but the folder is not the namespace).
All three methods validate their argument first and throw before touching the markup.
| Condition | Exception |
|---|---|
html is null
|
ArgumentNullException (derives from ArgumentException) |
html is empty or whitespace only |
ArgumentException |
public static LogsPage ParseLogs(string html)Parses a log listing page into a LogsPage.
| Markup it looks for | Maps to |
|---|---|
Показано с N по M из T (arbitrary tags between the words are tolerated) |
LogsPage.MetaInfo — LogPageMetaInfo(Start, End, Total)
|
| the page layout (navbar, server select) | LogsPage.Account |
the first <tbody> on the page |
the row source for LogsPage.Entries
|
| cell 1 of a row | LogEntry.Timestamp |
| cell 2 of a row |
LogEntry.Text, LogEntry.Html, LogEntry.RevealedValues
|
| cell 3 of a row |
LogEntry.Sender / LogEntry.Target — Money, Bank, Donate and AdditionalInfo
|
| cell 4 of a row |
LogParticipant.LastIp and LogParticipant.RegistrationIp
|
Details of the row mapping:
- Rows with fewer than two
<td>cells are skipped silently. - The action cell keeps its raw inner HTML in
LogEntry.Html.LogEntry.Textis the same cell with the blocks the site hides behind an eye toggle (class="app__hidden") removed, soTextmirrors what the page actually shows. Each hidden block becomes aLogRevealedValuewhoseLabelis the block'sdata-titleattribute (empty string when the attribute is missing) and whoseTextkeeps the block's line structure. - In the data cell, the
I:andII:markers select the sender and the target respectively. A hidden information block is attributed to the marker it follows, not paired by index, so a row that carries only aII:participant leavesSendernull. An information block yields aLogAdditionalInfoonly when it contains at least ten<code>values; otherwise it is dropped. - When the data cell has no
I:/II:markers at all, the first hidden block is read as the sender's information and the second as the target's. - In the IP cell, each
table-ipelement needs one<code>(I:orII:) and at least two links; the first link is the last IP, the second the registration IP. -
SenderandTargetarenullunless at least one of money, additional information or last IP was found.
Missing markup does not throw:
| Situation | Result |
|---|---|
no <tbody>
|
LogsPage with an empty Entries collection; MetaInfo and Account are still filled if present |
| no pagination line |
MetaInfo is null
|
| no account markup |
Account is null
|
rows with fewer than two <td> cells |
skipped, the remaining rows are returned |
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}");
}
}Throws
| Exception | When |
|---|---|
ArgumentNullException |
html is null
|
ArgumentException |
html is empty or whitespace |
FormatException |
a row's timestamp or a numeric cell cannot be parsed — values are read with DateTime.Parse, int.Parse, long.Parse under CultureInfo.InvariantCulture
|
public static AdminActivityReport ParseAdminActivity(string html)Parses the administrator activity report into an AdminActivityReport.
| Markup it requires | Maps to |
|---|---|
<input name="min_period" value="…"> and <input name="max_period" value="…">
|
AdminActivityReport.Period (From, To) |
the first <tbody> on the page |
AdminActivityReport.Entries |
app__hidden blocks inside that <tbody>
|
AdminActivityEntry.Details — the per-day breakdown |
Details of the mapping:
- The hidden blocks are removed from the table before the rows are read, and the n-th hidden block is matched to the n-th remaining row.
- A row needs at least ten
<td>cells, otherwise it is skipped. - Inside a hidden block, the first row is treated as a header and skipped; every following row needs at least
seven
<th>cells to become anAdminActivityDay. -
AdminActivityEntry.TotalOnlineis the sum of the days'Onlinevalues, so it isTimeSpan.Zerowhen a row carries no detail block. -
AdminActivityMetaInfois computed, not scraped:AdminCountis the number of entries,PeriodDaysis the day difference of the period clamped at zero,TotalReportsandTotalBansare the sums over the entries.
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}");
}Throws
| Exception | When |
|---|---|
ArgumentNullException |
html is null
|
ArgumentException |
html is empty or whitespace |
HtmlParsingException |
either period input is missing — message Admin activity period was not found.
|
HtmlParsingException |
no <tbody> — message Admin activity table was not found.
|
FormatException |
a period value, date or numeric cell cannot be parsed |
public static TopOperationsReport ParseTopOperations(string html)Parses the top operations report into a TopOperationsReport.
| Markup it requires | Maps to |
|---|---|
Данные за: yyyy-MM-dd |
TopOperationsReport.Date |
the first <tbody> on the page |
TopOperationsReport.Entries |
Details of the mapping:
- The HTML is decoded before the date is matched, so an entity-escaped label is still found.
- A row needs at least six
<td>cells, otherwise it is skipped. - Cells 3 and 4 are IP addresses. A value that
IPAddress.TryParserejects is logged as a warning and replaced withIPAddress.Loopbackrather than throwing. -
TopOperationsMetaInfois computed from the entries:PlayerCount, the sum ofTotalTransactionsand theulongsum ofSum.
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}");
}Throws
| Exception | When |
|---|---|
ArgumentNullException |
html is null
|
ArgumentException |
html is empty or whitespace |
HtmlParsingException |
the date label is missing — message Top operations date was not found.
|
HtmlParsingException |
no <tbody> — message Top operations table was not found.
|
FormatException |
the date or a numeric cell cannot be parsed |
ParseLogs degrades, ParseAdminActivity and ParseTopOperations throw. This is deliberate, not an oversight.
A log listing legitimately comes back with nothing in it — a filter that matched no events produces a page with
no rows — so an empty LogsPage is a valid answer, and the pagination meta info and account context are still
returned when the page carries them. The two reports, by contrast, always have a period (or a date) and a table
when they render at all; their absence means the response was not the page that was asked for, and
HtmlParsingException says so instead of returning a report of zero administrators.
public static partial class LogsFilterCatalogParserNamespace: LogsParser.Parsing.
public static LogsFilterCatalog Parse(string html)Parses the filter catalogue out of any page that renders the search form — in the library's own pipeline this is
the site root /. Returns a LogsFilterCatalog.
| Markup it looks for | Maps to |
|---|---|
<select name="type[]"> options |
LogsFilterCatalog.Filters — one LogsFilterDefinition per option |
an option's value attribute |
LogsFilterDefinition.Code |
| an option's text | LogsFilterDefinition.Name |
<div class="… js_component_filter_item …" data-filter-type="CODE"> blocks |
LogsFilterDefinition.AdditionalParameters |
the <label> inside such a block |
LogsFilterAdditionalParameter.Label |
the name of the <input>, <select> or <textarea> inside such a block |
LogsFilterAdditionalParameter.QueryKey (the dynamic[n] key) |
| the page layout (navbar, server select) | LogsFilterCatalog.Account |
Details of the mapping:
- Options with an empty or missing
valueare dropped, so the placeholder option of the real form does not become a filter. - Dynamic parameter blocks are grouped by their
data-filter-typeand deduplicated byQueryKeywithStringComparer.Ordinal. - A filter with no matching block gets an empty
AdditionalParameterscollection, nevernull. - When the
type[]select is absent the catalogue comes back with an emptyFilterscollection — this method never throwsHtmlParsingException.Accountis still filled if the layout is present.
The QueryKey values are exactly the keys accepted by LogsQuery.AdditionalParameters, which is why the
catalogue is the supported way to discover them — see Requests and URI Builder.
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}");Throws
| Exception | When |
|---|---|
ArgumentNullException |
html is null
|
ArgumentException |
html is empty or whitespace |
LogsPage.Account and LogsFilterCatalog.Account are both LogsAccount? and both are filled by an internal
account parser that reads the layout every authenticated page shares: the nickname link in the navbar, the
badges next to it, and the server_number select. That is why a log request and a catalogue request return the
same account context without a dedicated "who am I" call.
| Layout element | Maps to |
|---|---|
| the navbar dropdown link | LogsAccount.Nickname |
| badge spans in the right-hand navbar list |
LogsAccount.Badges — deduplicated, ordinal |
<select name="server_number"> options |
LogsAccount.AvailableServers |
an option's numeric value
|
LogsAccountServer.Id |
| an option's text |
LogsAccountServer.DisplayName; Name is the same text with a leading [id] prefix stripped |
the selected attribute |
LogsAccountServer.IsSelected |
When none of the three is found — no nickname, no badges, no servers — Account is null. An unauthenticated
page therefore yields null rather than an empty LogsAccount. The account parser itself is not part of the
public 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)"}");Any string works — a response body fetched with a plain HttpClient, a fixture embedded in a test, a page saved
by a browser. Nothing in the parsers requires the HTML to have come from the library's transport.
using LogsParser;
using LogsParser.Models;
using LogsParser.Parsing;
using var http = new HttpClient { BaseAddress = new Uri("https://arizonarp.logsparser.info") };
// A session cookie obtained elsewhere; the parsers themselves neither read nor set cookies.
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);If the goal is a full session — login, two-factor confirmation, the anti-DDoS challenge, retries and rate
limiting — use LogsParserClient over
LogsParserHttpDataSource instead of hand-rolling the requests. The parsers stay
available for the cases where the HTML is already in hand.
LogsHtmlParser writes to the library's logging facade under the category LogsHtmlParser: Debug for the
per-call result counts, Trace for skipped rows and extracted hidden blocks, Warning when a required label is
missing or an IP address falls back to loopback. LogsFilterCatalogParser does not log. See Logging
for how to attach a factory.
Everything in this section is internal detail, not public API, and may change in any release.
- Parsing is hand-rolled regex over an internal
HtmlFragmentReader. It provides depth-aware balanced-block extraction (finding the matching close tag by tracking nesting depth rather than by a lazy.*?), plus text normalisation that strips tags and collapses whitespace, and a multiline variant that preserves line breaks inside revealed values. - Row and cell patterns are deliberately loose: a row is matched up to the next
<tr>or the end of the table section, so a missing</tr>— which the real site emits — does not swallow the rest of the table. - Several Russian literals are load-bearing because they mirror the real site's markup:
Показано с … по … изfor the pagination meta info andДанные за:for the top operations date. So are the class and attribute namesapp__hidden,data-title,table-ip,js_component_filter_item,data-filter-type, and the form control namestype[]andserver_number. - The pagination pattern allows arbitrary markup between the keywords and the numbers, because the site wraps the
counts in
<strong>tags and HTML decoding does not remove tags. - Some patterns are order-sensitive by construction: a dynamic parameter block is only recognised when
classprecedesdata-filter-typeon the<div>and the<label>precedes the control. - New regexes live in
[GeneratedRegex]-annotatedprivate static partial Regexmembers, which is why the parser classes are declaredpartial. - No third-party HTML parser is used. That is a deliberate design decision, and it is the reason the package
carries only two dependencies,
Microsoft.Extensions.DependencyInjectionandMicrosoft.Extensions.Logging.Abstractions.
- Models — every record the parsers return
- LogsParserClient — the facade that fetches the HTML and calls these parsers
- Requests and URI Builder — building the URIs whose responses are parsed here
- Transport and Authentication — how the HTML is obtained
-
Exceptions —
HtmlParsingExceptionand the rest of the hierarchy - Logging — parser log categories and levels
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
- Внедрение зависимостей
- Логирование
- Исключения