#c_sharp #регулярные_выражения #json #xpath #net_core
Здравствуйте. Ситуация: есть модель(дисериализованный файл), довольно большая. В
ней много строк и вложенных объектов. (Модель YML каталога).
[XmlRoot("yml_catalog")]
public class YMLCatalog
{
///
/// Дата и время генерации YML файла на стороне магазина
///
[Required]
[XmlAttribute("date")]
public string Date { get; set; }
///
/// Магазин
///
[Required]
[ValidateObject]
[XmlElement(ElementName = "shop")]
public Shop Shop { get; set; }
}
///
/// Точка продаж
/// https://yandex.ru/support/webmaster/goods-prices/technical-requirements.html
///
public class Shop
{
///
/// Название магазина.
///
[Required]
[XmlElement("name")]
public string Name { get; set; }
///
/// Название компании
///
[Required]
[XmlElement("company")]
public string Company { get; set; }
... и тд.
}
При валидации модели, в случае ошибки, я получаю (при помощи магии. См. краткое описание
в комментариях) путь к ошибке. Например для YML файла с ошибкой в теге price, 0го оффера,
в виде отсутствия цены, я получаю путь: shop/offers/offer['0 или 1', смотря как удобнее.
Для XPath лучше 1]/price Value: Текст ошибки. (Value - поле со значением тега, аналогично
может быть для атрибутов)
Нас интересует: shop/offers/offer[0]/price
По этому пути, мне нужно определить номер строки, где находиться этот тег. Чтобы
указать пользователю где ошибка.
Что я пробовал:
1) Сделал через регулярные выражения. Сначала последовательно нахожу индекс элементов
по пути, как получу индекс начала тега Price в файле. Затем собираю список строк в
файле и нахожу номер строки для вывода, относительно всего файла. - Это работает, но
меня не устраивает. Слишком медленно, а у меня могут быть очень большие файлы
2) Попробовал сделать через XPath. Т.е например можно легко получить элемент тега
вот так:
XmlDocument document = new XmlDocument();
document.LoadXml(text);
var node = document.SelectSingleNode("//shop/offers/offer[1]/price");// это для примера
3) Есть вариант с созданием своего дессериализатора + атрибутов валидации для него,
чтобы к каждому полю в модели приписывать в метаданных IXmlLineInfo - который в стандартном
XmlSerializer к сожалению используется только при выдаче информации об ошибке - но
это долго, а те готовые, что я нашел, не поддерживаются в net core... UPD: также в
таком случае требуется поддержка не только составных объектов, но и простых + простых
атрибутов (string, int, long и тп)
Вопрос 1: XPath как-нибудь позволяет получить кол-во элементов выше (или лучше их
список) ? Вроде комбинировать preceding и ancestor нельзя (ссылка) - Но опять же,
даже если это получиться, придется искать индекс, создавая список строк файла. - Не
очень хорошо, хотелось бы узнать номер строки ошибки другим способом
Пробовал "//shop/offers/offer[ 1 ]/price/preceding::*", но в таком случае игнорируются
все элементы выше shop, почему не знаю. (когда использовал count(...), получал 27 элементов)
- Думаю это всяко лучше только регулярных выражений
Знаете еще способ? Прошу в ответы:)
Главный вопрос: Так как лучше всего найти номер строки ошибки, имея XML и путь к тегу?
P.S Производительность решения приветствуется)
Ответы
Ответ 1
Не выдержал, решил дать свой ответ.
Во-первых, XmlSerializer выдаёт ошибку не только при ошибке синтаксиса XML (например,
незакрытый тег), но и в том случае, если тип данных не соответствует ожидаемому (например,
свойство Price имеет тип decimal, а в узле Price пусто или нечисловая строка). При
этом в выбрасываемом исключении содержится сообщение, содержащее номер и позицию ошибки:
В документе XML (4, 18) присутствует ошибка.
На мой взгляд, это уже фактически решает вопрос. Строка с ошибкой известна, можно
её исправлять.
Чуть погуглив, я легко выяснил, что для документов YML имеется схема. Скачать её
можно здесь.
При валидации по схеме будет выдаваться описание ошибки наподобие следующего:
Элемент "Price" недействителен: значение "" недействительно с точки зрения его
типа данных "http://www.w3.org/2001/XMLSchema:decimal" - Строка "" не является допустимым
значением Decimal.
Это даёт огромное удобство: пользователь легко поймёт и исправит ошибку.
Сама валидация делается в несколько строк:
var xs = new XmlSerializer(typeof(Test));
var settings = new XmlReaderSettings { ValidationType = ValidationType.Schema };
var schemas = new XmlSchemaSet();
schemas.Add(null, "schema.xsd");
settings.Schemas = schemas;
settings.ValidationEventHandler += Settings_ValidationEventHandler;
using (var xr = XmlReader.Create("test.xml", settings))
//while (xr.Read()) ;
item = (Test)xs.Deserialize(xr);
private void Settings_ValidationEventHandler(object sender, ValidationEventArgs e)
{
Console.WriteLine(e.Severity);
Console.WriteLine(e.Message);
Console.WriteLine(e.Exception.LineNumber + " " + e.Exception.LinePosition);
}
В данном коде валидация происходит в процессе чтения xml. То есть нет лишних проходов/загрузок,
всё максимально эффективно по скорости и объёму памяти.
Если нужно только валидация без десериализации, то раскомментируйте строку с while,
закомментировав следующую.
В том подходе, который используете вы сейчас, всё делается, имхо, очень неэффективно.
Десериализация - отдельно (проход по xml, потребление памяти).
Валидация - отдельно (ещё проход, ещё память).
Поиск строки с ошибкой с помощью XPath - ещё одна загрузка xml в XmlDocument/XDocument
(опять полный парсинг xml, опять тратится время и память).
Кроме того, если xml синтаксически некорректен (незакрытый тег), то он вообще не
загрузится в XmlDocument/XDocument и вы никогда не узнаете, где же ошибка...
Кроме того, вы вручную расставляете атрибуты валидации и пишете код валидации...
Лишняя работа.
Ответ 2
а через LINQ to XML нельзя сделать?
что-то вроде такого ...
XDocument xdoc = XDocument.Load(file, LoadOptions.SetLineInfo);
var node = xdoc.XPathSelectElement("//shop/offers/offer[1]/price");
var lineNumber = ((IXmlLineInfo)node).LineNumber;
Ответ 3
а нельзя добавить в сам цикл и обработку, индекс, который и будет указывать на номер
тега. Прошу прощения, вы это сами уже предложили и отвергли. И все таки почти всегда
есть главный цикл обработки, и передать место сбоя в виде текущего номера обрабатываемого
элемента, вроде как можно, ведь оно будет равняться текущему элементу цикла.
ParentNode возвращает родительский узел у текущего узла. Наверное можно обратиться
к родителю, узнать количество смежных узлов, обратиться к родителю родителя и так далее.
Можно посчитать количество переводов строки после закрывающихся тегов, не уверен
что это вообще из этой оперы, но может помочь в идентификации нужного узла и места.