Большинство парсеров на C# работают до тех пор, пока сайт не поменяет вёрстку. Тогда ломается селектор, и вы смотрите на клубок строк без типов, без тестов и без понятного места для правки. Laraue.Crawling ставит между вашим кодом и парсером типизированную схему: вы один раз описываете страницу C# моделями и селекторами, а библиотека заполняет модели.
Это не игрушка. На ней работал краулер нашего сервиса по недвижимости, который собрал больше 100 тысяч объявлений с двух крупных сайтов. Сервис больше не собирает новые объявления, потому что мы не нашли законного способа сделать из него продукт, но библиотека существует отдельно от него, и мы продолжаем её поддерживать.
Зачем схема, а не селекторы в коде
Обычно берут парсер (AngleSharp или HtmlAgilityPack для статики, PuppeteerSharp или Playwright для страниц с JavaScript) и пишут извлечение данных прямо в коде. Для разового скрипта это нормально. Для кода, который живёт месяцами, — больно. Схема даёт:
- Типы. Модели — обычные C# record, поэтому неверный тип обнаруживается при компиляции.
- Одно место для правки. Сайт изменился — вы меняете одну схему, а не селекторы по всему коду.
- Тесты. Схема — обычный объект: разберите сохранённый HTML-файл и проверьте модель.
- Одинаковая форма для разных парсеров. Статические и динамические схемы строят модели одним способом.
Быстрый старт
dotnet add package Laraue.Crawling.Static.AngleSharp
public record ProductPage(string Title, string Price) : ICrawlingModel;
var schema = new AngleSharpSchemaBuilder<ProductPage>()
.HasProperty(x => x.Title, "h1.title")
.HasProperty(x => x.Price, ".price")
.Build();
var parser = new AngleSharpParser(new NullLoggerFactory());
var model = await parser.RunAsync(schema, html);
Console.WriteLine(model.Title);
Вложенные объекты и списки описываются через HasObjectProperty и HasArrayProperty с вложенным построителем, поэтому страница с пользователем и списком собак превращается во вложенные record одним выражением.
Статика, динамика и XML
- Статический HTML (
Laraue.Crawling.Static.AngleSharp): быстро, без браузера, для страниц, которым JavaScript не нужен. - Страницы с JavaScript (
Laraue.Crawling.Dynamic.PuppeterSharp): настоящий headless-браузер через PuppeteerSharp. Построитель называетсяPuppeterSharpSchemaBuilder. И пакет, и класс пишутся «Puppeter», без второй «e»: именно так они названы на NuGet, поэтому копируйте имя точно. Обработчики получают дескриптор элемента PuppeteerSharp, а не строку HTML. - XML (
Laraue.Crawling.Static.Xml):XmlSchemaBuilderдля лент, карт сайта и XML-ответов.
Когда один HTML-элемент нужно разложить на несколько свойств (например, «Боб Мартин 37» на имя, фамилию и возраст), BindManually отдаёт вам элемент, и вы связываете свойства сами.
Схема из продакшена
Это часть схемы, которая читает одну страницу выдачи сайта объявлений в нашем сервисе по недвижимости. HasArrayProperty разбирает карточки, BindManually — ссылку, а Map превращает текст цены в число (весь файл):
return new PuppeterSharpSchemaBuilder<CrawlingResult>()
.HasArrayProperty(x => x.Advertisements, "article", pageBuilder =>
{
pageBuilder.HasProperty(x => x.ShortDescription, "div[data-name=Description]");
pageBuilder.BindManually(async (e, b) =>
{
var linkElement = await e.QuerySelectorAsync("div[data-name=LinkArea] a");
var href = await linkElement.GetAttributeValueAsync("href");
// ... строим идентификатор и ссылку из адреса
});
pageBuilder.HasProperty(
x => x.TotalPrice,
builder => builder
.UseSelector("span[data-mark=MainPrice]")
.Map(s => long.Parse(s.GetOnlyDigits())));
pageBuilder.HasArrayProperty(
x => x.ImageLinks,
"div[data-name=Gallery] img",
el => el!.GetAttributeValueAsync("src"));
})
.Build();
Селекторы — то, что ломается при изменении сайта, и все они лежат в одном файле.
Задачи по расписанию
В пакете Laraue.Crawling.Crawler есть BaseCrawlerJob<TModel, TLink, TState> — базовый класс для краулера, который работает как hosted service. Цикл он выполняет сам: взять следующую ссылку, разобрать её, обработать результат, подождать, повторить. Вы реализуете шаги: GetNextLinkAsync, ParseLinkAsync, AfterLinkParsedAsync, OnSessionStartAsync, OnSessionFinishAsync и GetTimeToWait.
Две вещи в нём появились из настоящего парсинга:
- Любой шаг может выбросить
SessionInterruptedException, и текущая сессия завершится корректно. Дальше задача ждёт столько, сколько вернётGetTimeToWait. - Если сайт распознал краулер, выбросите
CrawlerHasBeenDetectedException. Задача запишет это в лог и выполнит переключение состояния, которое вы приложили к исключению (например, откроет новую сессию браузера), вместо того чтобы остановиться.
В нашем сервисе на каждый сайт объявлений была одна задача, которая читала страницы выдачи от самых свежих объявлений назад. В настройках репозитория сессия запускается раз в четыре часа, а пауза между страницами случайная: от 2 до 10 секунд для одного сайта и от 35 до 45 секунд для другого.
Подойдёт ли вам
Берите, если вам нужны типизированные схемы и проверяемые селекторы: она под лицензией MIT, поддерживается и достаточно мала, чтобы прочитать её за вечер. Для быстрого скрипта в один файл используйте AngleSharp или PuppeteerSharp напрямую — библиотека лишь оборачивает их.
Пакеты
| Пакет | Назначение |
|---|---|
Laraue.Crawling.Common |
Базовые абстракции и интерфейсы |
Laraue.Crawling.Static.AngleSharp |
Статический HTML через AngleSharp |
Laraue.Crawling.Dynamic.PuppeterSharp |
Страницы с JavaScript через PuppeteerSharp |
Laraue.Crawling.Static.Xml |
XML-деревья |
Laraue.Crawling.Crawler |
BaseCrawlerJob для парсинга по расписанию |
Исходный код: github.com/win7user10/Laraue.Crawling. Сайты объявлений, для которых её писали, здесь не разбираются, потому что сервис, который её использовал, больше не собирает новые объявления; эту историю рассказывает страница проекта по недвижимости.