Pull to refresh

Десериализация огромных и ошибочных xml-файлов

Reading time4 min
Views6.5K
Некоторое время назад в одном проекте у меня стояла задача импорта данных, выгружаемых файлами xml. Откуда происходит выгрузка мне было не известно, да и не важно это. Главное что все ложилось в определенную папку. Каждый xml файл содержал один тип информации (выгружалась информация об одном и более объектов одного типа). Проект писался на C# поэтому и парсинг осуществлялся его средствами.

Честно говоря до этого я никогда не работал с xml файлами, не было такой возможности, и тут такое счастье … Естественно слово сериализация для меня было всего лишь буржуйским словом и пока не понимал как мне было бы проще с ней. В начале выборка данных была осуществлена с помощью класса XmlDocument, а для быстрого определения содержимого файлов использовался класс XmlTextReader. Все было хорошо до тех пор пока разнообразие объектов не увеличилось в разы и писать парсинг под каждый было неохота, да и хотелось чего-то универсального и менее затратного по времени написания. Вот тут сериализация стала не просто словом, а предметом пристального изучения. К моему счастью для всех xml файлов прилагался один с описанием структуры хранимых данных в формате xsd. Я это дело быстро с помощью утилиты xsd в Visual Studio преобразовал в исходник на C#, который занял несколько сот килобайт. Ну а далее как во всех примерах на множестве сайтах:

using( FileStream stream = new FileStream(NameFile) )
{
    XmlSerializer xs = new XmlSerializer(typeof(ImportObject));
    ImportObject obj = (ImportObject)xs.Deserialize(stream);
}


* This source code was highlighted with Source Code Highlighter.


Все достаточно просто и легко, в переменной obj в одно мгновение оказывается весь объект и остается только проанализировать его содержимое и разложить все по полочкам таблицам.

Но задача сразу же была усложнена тем что в xml файле передавались пустые значения (null) которые никак не описывались в структуре и естественно вся эта красота вываливалась в сообщение об ошибке преобразования. Детальное изучение xml файлов выявило что некоторые теги в которых должны были быть числа или даты содержали пустые значения (теги были в виде <Value/>). Естественно значение null ни в число ни в дату превратить невозможно, можно только заменить какими-то значениями. Первое что пришло в голову так это поправить файлик сформированный утилитой xsd, чтобы пустые значения преобразовывались в не пустые, но это будет действовать только до первого обновления структуры, а там снова по новой, что не есть хорошо. Можно было бы договориться с разработчиками в формировании структуры с учетом таких ситуаций, но попытка не удалась, т. е. я остался на сам с собой с этой проблемой. Ситуацию еще усложнило, то что импортируемые xml файлы могли быть размером … в несколько гиг! Что сразу отметало попытку загрузки содержимого в память.
Не долго думая я решил контролировать процесс чтения xml файла и в нужные места подставлять нужные данные, т. е. заменять теги вида <Value/> на 0. Как видно из выше указанного примера чтение происходит с помощью класса FileStream. Я быстренько написал своего потомка от этого класса и определил какими его функциями пользуется функция Deserialize(). Все оказалось просто – использовалась только функция Read():

class MyStream : FileStream
{
  public MyStream(string NameFile)
  {
  }

  public override int Read(byte[] array, int offset, int count)
  {
    …
  }
}


* This source code was highlighted with Source Code Highlighter.


Далее уже было техники, правда не все гладко шло – целый день написания и отладки. В своей функции Read я читал в свой буфер данные, анализировал поступающие теги и при поступлении пустых значений в определенных тегах заменял на значения по умолчанию: для чисел это были нули, для дат значение DateTime.MinValue, и после всех этих махинаций сливал все массив array. И так порцией за порцией следил за поступающими тэгами и делал нужные поправки и отдавал все это на съедение Deserialize(). Итак проблема с null значениями была решена, теперь осталось решить проблему с огромными xml файлами, которые никак не помещались в память, да и издеваться так над ней не хотелось. Но и тут не все так сложно, достаточно моему MyStream указать сколько объектов нужно прочесть и потом в Read() отсчитать нужное количество и как бы завершить чтение, т. е. сказать Deserialize(), что файл окончен, при этом не забыть запомнить где закончили. После обработки считанной порции, уже легко прочесть следующую порцию и так до тех пор пока действительно не закончится файл. Для реализации этого я указывал какой уровень тега является завершающим и после прочтения порции данных просто завершал им и теми что выше его уровнем.

Код не привожу так как он не сильно хороший, и из-за этого потерял много кармы. Поэтому не обесудьте но без него смысл метода понять можно.
После всех этих приключений я мог легко читать сколь угодно длинные xml файлы, а также для определения содержимого читал всего лишь информацию об одном объекте (не все объекты мне были нужны, и поэтому такие файлы нужно было игнорировать). Этот метод также легко позволяет показывать как далеко прошел процесс в контроле ProgressBar, для этого нужно всего лишь прочесть позицию в файле, а длина известна. Также можно легко прервать процесс чтения по нажатию клавиши.

Побочный эффект исследований

В процессе освоения метода я столкнулся с глюком в классе StreamReader, т. е. я перегрузил его функцию Read() которая передавала буфер длинною 1024 байт, и если произвести возврат меньшего количества считанных байт, то процесс чтения прекращается, хотя до конца файла еще не дошли. Получается как только StreamReader получает меньше байт чем вмещает буфер, он считает, что достигнут конец файл. Мне кажется такое поведение неверным, тем более что данные могут читаться не только из файла, а из сокетов, а там угадать количество невозможно. И в МСДН явно указано что, конец файла это когда Read() возвращает 0. Но все это не относится к функции Deserialize(), так как в процессе чтении всегда получается возвратить меньшее количество байт чем запрошено.

P.s. (poleznij sovet) Если ваша экономная лапочка вдруг внезапно перестала гореть, то достаточно выкрутить ее и постучать слегка цоколем по твердой поверхности (например стенке) и если повезет, то она еще прослужит вам некоторое время (проверено лично несколько раз).
Tags:
Hubs:
-5
Comments151

Articles