Как сравнить два xml файла между собой java

Обновлено: 07.07.2024

Я создаю серию видеоуроков для PACKT о сетевом программировании на Java. Существует целый раздел о Java NIO. Одним из примеров программы является копирование файла через необработанное сокетное соединение с клиента на сервер. Клиент читает файл с диска, а сервер сохраняет байты по мере их поступления на диск. Поскольку это демонстрационная версия, сервер и клиент работают на одном компьютере, и файл копируется из одного каталога в один и тот же каталог, но с другим именем. Доказательством того, что пудинг съест это: файлы нужно сравнивать.

Файл, который я хотел скопировать, был создан, чтобы содержать случайные байты. Передача только текстовой информации может иногда оставлять некоторую сложную ошибку в коде. Случайный файл был создан с использованием простого Java-класса:

public static void main(String[] args) throws IOException < try (FileOutputStream fos = new FileOutputStream( "sample.txt" )) <

Использовать IntelliJ для сравнения файлов довольно просто, но, поскольку файлы являются двоичными и большими, такой подход не совсем оптимален. Я решил написать короткую программу, которая будет не только сигнализировать, что файлы разные, но и где разница. Код очень прост:

public static void main(String[] args) throws IOException < BufferedInputStream fis1 = new BufferedInputStream( new FileInputStream( "sample.txt" )); BufferedInputStream fis2 = new BufferedInputStream( new FileInputStream( "sample-copy.txt" )); System.out.println( "Files are identical, you can delete one of them." ); System.out.print( "Execution time: " + (end - start)/ 1000000 + "ms" );

Время сравнения двух 160-мегабайтных файлов составляет около 6 секунд на моем Mac Book, оборудованном твердотельным накопителем, и оно значительно не улучшится, если я укажу большой, скажем, 10 МБ буфер в качестве второго аргумента для конструктора BufferedInputStream . (С другой стороны, если мы не используем BufferedInputStream тогда время примерно в десять раз больше.) Это приемлемо, но если я просто diff sample.txt sample-copy.txt из командной строки, то ответ значительно быстрее, а не 6 секунд. Это может быть много вещей, таких как время запуска Java, интерпретация кода в начале цикла while, пока JIT-компилятор не решит, что пора начинать работать. Однако я догадываюсь, что код тратит большую часть времени на чтение файла в память. Чтение байтов в буфер является сложным процессом. Это касается операционной системы, драйверов устройств, реализации JVM, и они перемещают байты из одного места в другое, и, наконец, мы сравниваем только байты, и ничего больше. Это можно сделать более простым способом. Мы можем попросить операционную систему сделать это для нас и пропустить большинство действий времени выполнения Java, файловых буферов и других проблем.

Мы можем попросить операционную систему прочитать файл в память, а затем просто извлечь байты один за другим, где они находятся. Нам не нужен буфер, который принадлежит объекту Java и использует пространство кучи. Мы можем использовать отображенные в память файлы. В конце концов, файлы с отображением в памяти используют Java NIO, и это как раз та часть той части обучающих видео, которая в настоящее время находится в стадии разработки.

В ряде проектов мне потребовалось сравнивать XML данные в тестах.

Действительно, бывает, что результат работы твоего модуля — XML данные. Если это так, то как они генерятся нужно проверять в соответствии с принципами TDD . Я же в свою очередь стараюсь их придерживаться при разработке.

Под катом я постараюсь рассказать о том, как лучше всего, по моему мнению, тестировать генерацию XML в коде. В качестве инструмента сравнения XML я использовал XmlUnit.

Мне нужно быстро и удобно сравнивать XML данные в тестах. Изобретать велосипед не хотелось, и я выбрал наиболее популярную библиотеку для этих целей. В начале я постараюсь описать список проблем, которые я решал.

С чего всё началось

Как-то работая над очередным проектом, я сломал тест коллеги. Когда стал изучать тест, то нашел там что-то типа такого:


Здесь производится генерация данных и сравнение их с оригиналом из ресурсов. Для тестов используется JUnit4. Отмечу так же, что в идеале файл оригинала должен писаться вручную разработчиком.

Чем плох данный код?

Попробую обосновать по пунктам.

1. Слишком много телодвижений, чтобы сравнить два файла

Действительно, движений много, а если сравнивать приходится часто, то этот код в том или ином виде копируется туда-сюда, что собственно часто и происходит. Что-то с этим сделать мешает отсутствие времени.

2. Нет проверки валидности сгенеренного XML-кода

Из-за того, что валидность не проверялась, другой модуль отказывался обрабатывать невалидные данные, хотя по тестам было всё нормально. Хотелось решать эту проблему на стадии отладки теста.

3. Идентичные по структуре и данным XML могут оказаться неодинаковыми из-за разного порядка тегов или атрибутов

Например, так:

Вариант 1 Меняем порядок тегов



или так:
Вариант 2 Меняем порядок атрибутов


4. Хочу в тестах XML c форматированием

В тесте, представленном в начале статьи, XML данные были представленны без пробелов и переносов строк. Почувствуйте разницу:

Вариант 3 Удаляем пробелы и переносы строк


5. Автоматически обрабатывались переносы внутри данных

Вариант 3 Перенос строк внутри данных


В рассматриваемом в начале статьи тесте, исходные данные были в одну строчку, только тегов и атрибутов было несколько больше. Посадить туда ошибку — очень просто, а исправлять так, чтобы тест проходил, — долго и мучительно.

  • воспользоваться какой-нибудь библиотекой для сравнения XML и проверки его валидности;
  • переписать все тесты так, чтобы они использовали эту библиотеку(благо их пока было ещё немного).

После недолгого поиска мой выбор пал на XmlUnit.

Основные способы подключения XmlUnit

Библиотека XmlUnit в первую очередь является расширением JUnit3.
Его основа — это класс XMLTestCase, наследник класса TestCase из JUnit3. Здесь вы можете посмотреть основные примеры использования класса.
На практике XmlUnit легко использовать и в других библиотеках тестов. Для этого есть класс Diff.

Итак, поехали

Для своих проектов я использую maven. Подключим XmlUnit как зависимость в maven. Для этого открываем pom.xml и в dependencies добавляем новую зависимость.


Открываем тест, пишем туда новое сравнение

Запускаем тест… и не работает. Ещё немного поискав в интернете, я нашел решение. Дело было в пробелах между тегами. Чтобы их не учитывать, нужно добавить предварительную настройку:

Вроде бы всё, можно радоваться результату. Однако, предположим, что мы допустили в XML ошибку. Тогда нам нужно знать, в каком именно теге проблема.
Решить её нам поможет следующий пример:

Обратите внимание на метод showXmlDiff. В нем получаем список различий и выводим его.

Что ещё можно сделать хорошего?

Литература

Ссылки, по которым я черпал информацию.

Ещё раз отмечу, что XmlUnit позволяет проверять валидность XML по DTD и XSD схемам.
Как правильно отметил Lure_of_Chaos, в схемах XSD можно требовать порядка задания элементов. Проверять это в тестах — критически важно.

UPD2: поправил последний пример проверки. Спасибо, Colwin.
На этом всё, спасибо за внимание.

Мне нужно в xml файлах сказать abc.xml и 123.xml, которые почти похожи, я имею в виду, что они имеют такое же содержимое, но второй, то есть 123.xml, имеет больше содержимого, чем предыдущий. Я хочу прочитать оба файла с помощью Java и сравнить, соответствует ли содержимое abc.xml для каждого тега содержимому 123.xml, что-то вроде сравнения объектов. Подскажите, пожалуйста, как читать XML-файл с помощью java и начать сравнение.

Я бы выбрал XMLUnit. Он предоставляет следующие возможности:

  • различия между двумя частями XML
  • Результат преобразования фрагмента XML с использованием XSLT
  • Оценка выражения XPath на фрагменте XML
  • Действительность куска XML
  • Отдельные узлы в фрагменте XML, предоставляемые DOM Traversal

Это немного излишне, но если у вашего XML есть схема, вы можете преобразовать ее в метамодель EMF, а затем использовать EMF Compare для сравнения.

Правильный подход зависит от двух факторов:

(а) насколько вы хотите контролировать процесс сравнения? Например, нужно ли вам контролировать, значимы ли пробелы, следует ли игнорировать комментарии, следует ли игнорировать префиксы пространств имен, следует ли игнорировать избыточные объявления пространств имен, следует ли игнорировать объявление XML?

(б) какой ответ вы хотите? (i) логическое значение: одинаковые / разные, (ii) список различий, пригодных для обработки человеком, (iii) список различий, пригодных для обработки приложением.

Я использую два метода: (а) конвертирую оба файла в Canonical XML и затем сравниваю строки. Это дает очень мало контроля и дает только логический результат. (b) сравните два дерева с помощью функции XPath 2.0 deep-equal () или расширенной саксонской версии saxon: deep-equal (). Версия Saxon дает больше контроля над тем, как выполняется сравнение, и более подробный отчет об обнаруженных различиях (для чтения человеком, а не для использования в приложениях).

Если вы хотите написать код Java, вы, конечно, можете реализовать свою собственную логику сравнения - например, вы можете найти реализацию XPath deep-equal с открытым исходным кодом и изменить ее в соответствии со своими требованиями. Это всего лишь сотня или около того строк кода.

Что ж, если вы просто хотите сравнить и отобразить, вы можете использовать Guiffy

Это хороший инструмент. Если вы хотите выполнить обработку в бэкэнде, вы должны использовать парсер DOM, загрузить оба файла в 2 объекта DOM и сравнить атрибут по атрибуту.

В общем, если вы знаете, что у вас есть два файла с идентичной структурой, но немного различающимся и неупорядоченным содержимым, вам придется «прочитать» файлы, чтобы сравнить содержимое.

Если у вас есть XML-схема для ваших XML-файлов, вы можете использовать JAXB для создания набора классов, которые будут представлять конкретная модель DOM, которая определяется вашей схемой XML. Преимущество этого подхода заключается в том, что вам не придется анализировать XML-файл с помощью общих функций для элементов и атрибутов, а скорее с помощью фактических полей, которые имеют смысл для вашей проблемы.

Конечно, чтобы иметь возможность обнаруживать наличие одной и той же записи в обоих файлах, вам придется «сопоставить» их через какое-то общее поле (например, некоторый идентификатор).

Чтобы помочь вам с процессом обнаружения дубликатов, вы можете использовать некоторую соответствующую структуру данных из коллекций Java, например Set (или одна из его производных)

Надеюсь, это поможет.

Я бы использовал JAXB для создания объектов Java из файлов XML, а затем сравнивал файлы Java. Они сделали бы обращение намного проще.

Вывод программы сравнения двух списков

Кому из нас в своей работе хоть раз не приходилось сталкиваться с задачей сравнения двух таблиц, списков, файлов и проч. Согласитесь, что эта задача, даже при небольших объемах сравниваемых данных, не из легких. Бухгалтеры обычно называют этот муторный процесс - «крыжить» списки. Однажды автор поставил себе задачу написать достаточно простую программу для сравнения электронных списков, используя бурно развивающиеся в настоящее время технологии XML. Речь пойдет о простом способе преобразования документов XML в объекты Java и обратно с помощью технологии Castor.

Листинг 1. Структура xml-файла для хранения списка

<?xml version = "1.0" encoding = "UTF-8" ?>
<recordslist >
<record >
<key > Ключ </key >
<value1 > Параметр1 </value1 >
<value2 > Параметр2 </value2 >
</record >
<record >
.
</record >
.
.
.
</recordslist >

Замечания по структуре xml-файла: кроме стандартного заголовка файл имеет обязательный корневой элемент
<recordslist >
Элемент
<recordslist >
может иметь сколько угодно элементов
<record > ,
которые описывают одну строку (запись) списка. Соответственно, элементы выделенные открывающимися/закрывающимися тегами
<key > </key > ,
<value1 > </value1 > и
<value2 > </value2 >
- это поля соответствующие Ключу, Параметру1 и Параметру2, которые описаны в нашей структуре.
Подготовить подобные списки можно по-разному. Автор это делал напрямую путем выгрузки из базы с помощью SQL-запроса и несложной программы. Покажем, для примера, как получить такой xml-файл из таблицы Excel. Нам поможет макрос, выгружающий выделенные три столбца таблицы Excel в файл нужной структуры (см. Листинг 2).

Листинг 2. Макрос для выгрузки столбцов Excel в xml-файл

Итак, создадим подходящий каталог для нашего проекта и в нем – файл Record.java, – класс, описанный в этом файле, будет описывать одну строку (запись) нашего списка (см. Листинг 3).

Листинг 3. Файл Record.java

public class Record <
private String key ;
private String value1 ;
private String value2 ;
public Record ( ) < >;
public Record ( String key, String value1, String value2 ) <
this . key = key ;
this . value1 = value1 ;
this . value2 = value2 ;
>
public void setKey ( String key ) <
this . key = key ;
>
public String getKey ( ) <
return key ;
>
public void setValue1 ( String value1 ) <
this . value1 = value1 ;
>
public String getValue1 ( ) <
return value1 ;
>
public void setValue2 ( String value2 ) <
this . value2 = value2 ;
>
public String getValue2 ( ) <
return value2 ;
>
>

В качестве пояснений к этому классу скажем, что в нем определены три строковых поля – key, value1, value2, которые соответствуют структуре нашего списка и, главное, структуре xml-файла, конструктор по умолчанию и конструктор, принимающий три параметра (для удобства). А также три метода get-set с одноименными названиями полей – это уже требование спецификации Castor.
Следующий класс, который нам понадобится – RecordsList (см. Листинг 4).

Листинг 4. Файл RecordsList.java

public class RecordsList <
private ArrayList < Record > records ;
public RecordsList ( ) < >;
public RecordsList ( Record record ) <
this . records = new ArrayList < Record > ( ) ;
records. add ( record ) ;
>
public void setRecords ( ArrayList < Record > records ) <
this . records = records ;
>
public ArrayList < Record > getRecords ( ) <
return records ;
>
public void addRecord ( Record record ) <
records. add ( record ) ;
>
public void deleteRecord ( int index ) <
records. remove ( index ) ;
>
>

RecordList содержит коллекцию объектов Record (ArrayList) и методы добавления/удаления записей.
Далее класс, реализующий демаршалинг – ListMapUnmarshaller (см. Листинг 5):

Листинг 5. файл ListMapUnmarshaller.java

public class ListMapUnmarshaller <
private RecordsList lst ;
ListMapUnmarshaller ( ) < >;
public RecordsList readFile ( String xml_file ) <
Mapping mapping = new Mapping ( ) ;
try <
mapping. loadMapping ( "list-map.xml" ) ;
FileReader reader = new FileReader ( xml_file ) ;
Unmarshaller unm = new Unmarshaller ( RecordsList. class ) ;
unm. setMapping ( mapping ) ;
lst = ( RecordsList ) unm. unmarshal ( reader ) ;
>
catch ( Exception e ) <
System . err . println ( e. getMessage ( ) ) ;
e. printStackTrace ( System . err ) ;
>
return lst ;
>
>

Некоторые пояснения для этого класса. Основная функция readFile(). Здесь буквально в трех строках кода:

Unmarshaller unm = new Unmarshaller ( RecordsList. class ) ;
unm. setMapping ( mapping ) ;
lst = ( RecordsList ) unm. unmarshal ( reader ) ;

сосредоточен весь процесс преобразования xml-файла в наш класс RecordsList, и мы можем уже сравнивать коллекции, состоящие из экземпляров класса Record, сортировать эти коллекции и прочее.
Отдельно поговорим о строке

В файле list-map.xml, который, как Вы наверное уже успели заметить, тоже имеет xml-структуру, содержится нужное для Castor описание полей наших классов. Приведем листинг этого файла, который называется mapping-файлом (см. Листинг 6). Отметим важность наличия mapping-файла для данной технологии (см. [2]).

Листинг 6. Файл list-map.xml

<?xml version = "1.0" ?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "<a href="http://castor.org/mapping.dtd">
<mapping >
<class "> http://castor.org/mapping.dtd">
<mapping >
<class </a > name="si.xml.castor.comparator.RecordsList">
<map-to xml = "recordslist" />
<field name = "Records"
type = "si.xml.castor.comparator.Record"
collection = "arraylist" >
<bind-xml name = "record" />
</field >
</class >
<class name = "si.xml.castor.comparator.Record" >
<field name = "Key" type = "java.lang.String" >
<bind-xml name = "key" node = "element" />
</field >
<field name = "Value1" type = "java.lang.String" >
<bind-xml name = "value1" node = "element" />
</field >
<field name = "Value2" type = "java.lang.String" >
<bind-xml name = "value2" node = "element" />
</field >
</class >
</mapping >

Нужно просто положить этот файл в каталог проекта.
Настало время привести главный файл проекта с функцией main(). Назовем его с3f.java (от англ. compare three fields). См. Листинг 7.

Листинг 7. Программа сравнения двух списков – файл c3f.java

public class c3f <
private RecordsList rlst ;
private RecordsList rlst2 ;
static Comparator < Record > keyorder = new Comparator < Record > ( ) <
public int compare ( Record o1, Record o2 ) <
return o1. getKey ( ) . compareTo ( o2. getKey ( ) ) ;
>
> ;

private ArrayList < Record > sortByKey ( ArrayList < Record > lst ) <
Collections . sort ( lst, keyorder ) ;
return lst ;
>
c3f ( ) < >;
c3f ( String xml_file_1, String xml_file_2 ) <
rlst = new ListMapUnmarshaller ( ) . readFile ( xml_file_1 ) ;
rlst2 = new ListMapUnmarshaller ( ) . readFile ( xml_file_2 ) ;
>
private int compareTwoLists ( ) <
//возвращает 1 - если есть ошибки, 0 - если ошибок нет
ArrayList records = rlst. getRecords ( ) ;
ArrayList records2 = rlst2. getRecords ( ) ;
TreeMap erTM = new TreeMap ( ) ;
//счетчик ошибок
int ern = 0 ;
//сортируем списки
records = sortByKey ( records ) ;
records2 = sortByKey ( records2 ) ;
//проверяем первый список на двойные значения ключа
String keyPrev = "" , keyNext ;
erTM. put ( ern, "Проверяем 1-й список" ) ;
for ( Iterator i = records. iterator ( ) ; i. hasNext ( ) ; ) <
Record record = ( Record ) i. next ( ) ;
keyNext = record. getKey ( ) ;
//если есть двойные значения ключа в первом списке - ошибка
if ( keyNext. equals ( keyPrev ) ) <
ern ++;
erTM. put ( ern, record. getKey ( ) + " - двойное значение ключа в первом списке" ) ;
>
keyPrev = keyNext ;
>
//первый список проверяем на наличие ключей во втором
//списке и равенство Параметра1 и Параметра2
//вспомогательная переменная
boolean eq1 ;
for ( Iterator r = records. iterator ( ) ; r. hasNext ( ) ; ) <
eq1 = false ;
Record record = ( Record ) r. next ( ) ;
for ( Iterator k = records2. iterator ( ) ; k. hasNext ( ) ; ) <
Record record2 = ( Record ) k. next ( ) ;
if ( record2. getKey ( ) . equals ( record. getKey ( ) ) ) <
eq1 = true ;
if ( ! record2. getValue1 ( ) . equals ( record. getValue1 ( ) ) ) <
ern ++;
erTM. put ( ern, record. getKey ( ) + " (" + record. getValue1 ( ) + ", " + record. getValue2 ( ) + ") - для одного ключа отличие первого Параметра во втором списке" ) ;
>
if ( ! record2. getValue2 ( ) . equals ( record. getValue2 ( ) ) ) <
ern ++;
erTM. put ( ern, record. getKey ( ) + " (" + record. getValue1 ( ) + ", " + record. getValue2 ( ) + ") - для одного ключа отличие второго Параметра во втором списке" ) ;
>
>
>
if ( ! eq1 ) <
ern ++;
erTM. put ( ern, record. getKey ( ) + " - ключ есть в первом и нет во втором списке" ) ;
>
>
//проверяем второй список на двойные значения ключа
//код аналогичен приведенному выше для 1-го списка
.
//второй список проверяем на наличие ключей в первом списке
//код аналогичен приведенному выше для 1-го списка
.
//если есть ошибки - сохраняем в файл
if ( ern > 1 ) <
saveErrors2file ( erTM ) ;
return 1 ;
> else return 0 ;
>
public void saveErrors2file ( TreeMap err_map ) <
try <
PrintWriter out = new PrintWriter ( new FileWriter ( "errors.log" ) ) ;
Set < Map . Entry > entries = err_map. entrySet ( ) ;
for ( Map. Entry entry : entries ) <
out. println ( entry. getValue ( ) ) ;
>
out. close ( ) ;
>
catch ( IOException exc ) < >
>
public static void main ( String [ ] args ) <
if ( args. length > 0 ) <
if ( args. length == 2 ) <
c3f lf = new c3f ( args [ 0 ] , args [ 1 ] ) ;
if ( lf. compareTwoLists ( ) == 1 ) <
System . out . println ( "There are errors!See errors.log file" ) ;
>
else <
System . out . println ( "Lists are equals!" ) ;
>
>
>
else <
System . out . println ( "must to have two parameters!" ) ;
System . exit ( 0 ) ;
>
>
>

Метод sortByKey нужен для сортировки коллекций. Конструктор с двумя строковыми параметрами преобразует xml-файлы в экземпляр класса RecordsList. Основной метод compareTwoLists() сравнивает две коллекции по описанному выше алгоритму, метод saveErrors2file сохраняет ошибки в файл. В остальном, приведенный код несложен для понимания и обильно снабжен комментариями. Автор использовал простейшие алгоритмы сравнения, желая показать простоту использования XML технологий для решения практических задач IT. Отметим также, что нашу программу можно дополнить, при необходимости, сравнением Параметра1 и Параметра2 не только на равенство, но и на возрастание-убывание (если это цифровые показатели), да и увеличить число Параметров не составит труда. Это зависит от конкретной задачи. Достаточно будет добавить в метод compareTwoLists() соответствующий код.

Осталось откомпилировать наши java-файлы и посмотреть что получится. Для компиляции проекта использовалась машина с ОС Ubuntu 11.04. Несколько слов о подключении jar-файлов Castor к проекту. Для системы Ubuntu в файл .profile из домашнего каталога пользователя достаточно дописать строки (см. Листинг 8).

Итак, в нашей статье мы научились использовать связывание с данными XML с помощью технологии Castor для прикладной задачи сравнения списков. Отметим, что язык XML прочно обосновался в информационных технологиях именно благодаря развитым средствам языков программирования и технологий от сторонних производителей для работы с этим форматом.

Читайте также: