Jak porównać dwa pliki XML i zobaczyć, co się zmieniło

Najszybszy sposób porównania dwóch plików XML to wklejenie obu do narzędzia diff wyświetlającego je obok siebie, sformatowanie ich tak samo i odczytanie podświetlonych wierszy. Samo porównywanie to łatwa część. To szum wprowadza ludzi w błąd: przestawione atrybuty, białe znaki między tagami i prefiksy namespace'ów mogą sprawić, że dwa pliki znaczące to samo wyglądają, jakby nie miały ze sobą nic wspólnego.

Ten przewodnik pokazuje, jak uzyskać czysty, wiarygodny diff XML. Przyjrzymy się, dlaczego dwa równoważne dokumenty rozjeżdżają się na papierze, które metody warto znać, oraz opracowanemu przykładowi, który możesz prześledzić. Jeśli chcesz tylko narzędzia, nasza strona porównywania XML robi to wszystko w przeglądarce.

Dlaczego pliki XML są zwodniczo trudne do porównania

XML ma ścisłą gramatykę (zobacz specyfikację XML W3C), ale daje autorom dużą swobodę w rozmieszczaniu tekstu. Dwa dokumenty mogą opisywać dokładnie te same dane, a mimo to różnić się bajt po bajcie. Zwykły diff tekstowy nic z tego nie rozumie, więc oznacza wszystko.

Oto kluczowy fakt do zapamiętania: w XML kolejność atrybutów elementu nie jest znacząca. XML Information Set traktuje atrybuty jako zbiór nieuporządkowany. Zatem <user id="7" role="admin"/> i <user role="admin" id="7"/> niosą tę samą informację, nawet jeśli diff wierszowy maluje je na czerwono i zielono. Kolejność elementów z kolei zwykle ma znaczenie.

Wygląda jak zmiana, ale zwykle nią nie jest
Co widzisz w diffieCzy to prawdziwa zmiana?Co zrobić
Atrybuty w innej kolejnościNie, kolejność atrybutów nie jest znaczącaSkanonikalizuj obie strony
Wcięcie 2 vs 4 spacjeNieSformatuj obie strony tak samo
Białe znaki między elementamiZwykle nieSformatuj lub usuń nieznaczące białe znaki
<br/> vs <br></br>Nie, ten sam pusty elementSkanonikalizuj obie strony
Inny prefiks namespace'u dla tego samego URINie, prefiksy to dowolne etykietyPorównuj według URI namespace'u, nie prefiksu
Elementy potomne w innej kolejnościZwykle tak, kolejność elementów ma znaczenieZbadaj, to prawdopodobnie prawdziwe

Ten ostatni wiersz to ten, na który trzeba uważać. Kolejność atrybutów jest dowolna, ale kolejność elementów potomnych jest częścią dokumentu w większości schematów. Jeśli chcesz szczegółów na temat tego, jak parser widzi to wszystko, MDN ma solidne źródło o parsowaniu XML za pomocą DOMParser.

Cztery sposoby porównywania XML i kiedy po który sięgnąć

Nie ma jednej najlepszej metody. Zależy to od tego, gdzie są pliki i czego próbujesz się dowiedzieć. Oto jak wypadają popularne opcje.

MetodaNajlepsza doWysiłekRozumie XML?
OględzinyMaleńkie pliki, jeden lub dwa elementyNiskiNie, to ty jesteś parserem
Narzędzie diff onlineSzybkie sprawdzenia, wklejanie skądkolwiekNiskiZ formatowaniem tak
Wiersz poleceń (xmllint)Pliki na dysku, skrypty, postać kanonicznaŚredniTak, z --c14n
IDE lub git diffPliki już w repozytoriumNiski, jeśli zacommitowaneDomyślnie oparty na wierszach

Dla większości ludzi narzędzie w przeglądarce wygrywa szybkością: nic do zainstalowania, a fragment można wkleić wprost z pliku konfiguracyjnego lub odpowiedzi SOAP. Haczykiem jest szum formatowania, którym zajmiemy się dalej. Jeśli żyjesz w terminalu, xmllint z biblioteki libxml2 to narzędzie, które warto znać.

Najszybsze czyste porównanie, krok po kroku

Oto rutyna, której używam, gdy ktoś podaje mi dwa pliki konfiguracyjne i pyta „co jest inne?”. Zajmuje to około piętnastu sekund.

  1. Otwórz narzędzie porównywania XML.
  2. Wklej oryginał po lewej, nową wersję po prawej.
  3. Kliknij Formatuj po obu stronach, aby miały to samo wcięcie.
  4. Szukaj prawdziwych różnic. Zielony to dodane, czerwony to usunięte, a zmieniona wartość pojawia się jako po jednej z każdego.
  5. Ignoruj wiersze, które są jedynie przestawieniem atrybutów lub białymi znakami.

Krok trzeci to większość sztuczki. Gdy oba dokumenty używają tego samego wcięcia, jedyne, co pozostaje do podświetlenia, to to, co naprawdę się zmieniło. Nasz silnik diff opiera się na diff-match-patch od Google, który najpierw porównuje wiersz po wierszu, dzięki czemu pozostaje szybki nawet przy długich plikach.

Opracowany przykład

Załóżmy, że przeglądasz zmianę w konfiguracji usługi. Oto wersja przed:

<user id="7" role="editor">
  <name>Ada Lovelace</name>
  <active>true</active>
  <seats>3</seats>
</user>

A oto wersja po, tak jak podał ci ją kolega z zespołu:

<user role="admin" id="7">
  <name>Ada Lovelace</name>
  <active>true</active>
  <seats>5</seats>
  <team>platform</team>
</user>

Wrzuć je do surowego diffa wierszowego, a sam pierwszy wiersz wygląda na zmieniony, ponieważ id i role zamieniły się miejscami. Sformatuj oba, porównaj według znaczenia, a prawdziwa historia jest krótka:

Co naprawdę się zmieniło
WęzełPrzedPoZmiana
@roleeditoradminZmodyfikowano
seats35Zmodyfikowano
teamplatformDodano
@id77Bez zmian (tylko przeniesiono)
nameAda LovelaceAda LovelaceBez zmian

Trzy prawdziwe edycje: awans roli, liczba miejsc i nowy element team. Zamiana atrybutów była szumem. Ten awans z editor na admin to dokładnie taka rzecz, którą chcesz wychwycić podczas przeglądu, a łatwo ją przeoczyć, gdy jest pogrzebana pod wierszem, który diff błędnie oznaczył.

Kanoniczny XML: właściwy sposób ignorowania szumu

Formatowanie obu stron załatwia wcięcie, ale istnieje standard stworzony dokładnie do tego problemu. Kanoniczny XML, zdefiniowany przez W3C w Canonical XML 1.1, przepisuje dokument do jednej znormalizowanej postaci: atrybuty posortowane, puste elementy rozwinięte, białe znaki w tagach znormalizowane, a atrybuty domyślne uczynione jawnymi. Dwa równoważne dokumenty dają identyczny wynik kanoniczny. To odpowiednik sortowania kluczy JSON w świecie XML.

xmllint --c14n old.xml > old.c14n.xml
xmllint --c14n new.xml > new.c14n.xml
diff old.c14n.xml new.c14n.xml

Teraz diff zgłasza tylko treść, która naprawdę się zmieniła, ponieważ oba pliki zostały znormalizowane w ten sam sposób. Jeśli chcesz tylko czytelnego wcięcia zamiast ścisłej postaci kanonicznej, xmllint --format file.xml ładnie je formatuje, co jest terminalowym odpowiednikiem kliknięcia Formatuj w przeglądarce.

Namespace'y: część, która wszystkich myli

Namespace'y XML pozwalają dwóm dokumentom używać tego samego słownictwa z różnymi etykietami prefiksów. <ns1:user> powiązany z URI i <u:user> powiązany z tym samym URI to ten sam element; prefiks to tylko lokalna ksywka. Diff tekstowy widzi ns1 kontra u i oznacza zmianę, której nie ma. Rozwiązaniem jest porównywanie według URI namespace'u zamiast prefiksu, co jest dokładnie tym, co robi kanonikalizacja. Specyfikacja Namespaces in XML to punkt odniesienia, jeśli musisz rozstrzygnąć spór na ten temat.

Częste pułapki, na które warto uważać

PułapkaDlaczego gryzieRozwiązanie
Kodowanie znakówPlik UTF-8 i UTF-16 mogą zawierać ten sam tekst, ale różnić się bajt po bajcieZnormalizuj kodowanie; deklaracja XML je podaje
Odwołania do encji&amp; i dosłowne & mogą wystąpić oba dla tego samego znakuSkanonikalizuj, co rozwiązuje encje spójnie
CDATA vs tekst z escape'ami<![CDATA[a<b]]> i a&lt;b to ta sama treść tekstowaPorównuj wartość sparsowaną, nie surowe bajty
Znaczące białe znakiWewnątrz xml:space="preserve" spacje mają znaczenie i nie wolno ich usuwaćNie przycinaj na ślepo; uszanuj xml:space
Tagi samozamykające<x/> i <x></x> są identyczneSkanonikalizuj, aby oba renderowały się tak samo

Diff tekstowy vs diff strukturalny

Wszystko powyżej to diff tekstowy: szybki, wizualny i idealny dla osoby czytającej zmianę. Diff strukturalny idzie dalej i opisuje zmianę w kategoriach drzewa XML: ten atrybut się zmienił, ten element potomny wstawiono w tej ścieżce. Diffa strukturalnego chcesz, gdy program musi zastosować zmianę albo gdy kolejność elementów naprawdę nie ma znaczenia i chcesz ją zignorować. Do codziennego przeglądu diff tekstowy dwóch sformatowanych dokumentów w zupełności wystarcza.

Powiązane narzędzia

XML rzadko jest jedynym formatem, z którym masz do czynienia. Jeśli porównujesz ładunki API, porównywanie JSON stosuje ten sam pomysł do JSON. Strony ze znacznikami łatwiej czyta się na stronie porównywania HTML, a ustawienia środowiska ładnie się układają w narzędziu porównywania config.

Najczęściej zadawane pytania

Czy porównywanie plików XML online przesyła je gdzieś?
Na comparetext.org diff działa w twojej przeglądarce. Dwa pliki XML są porównywane przez JavaScript na twojej własnej maszynie, więc nic nie jest wysyłane na serwer, chyba że wyraźnie klikniesz Zapisz lub Udostępnij. To czyni go bezpiecznym dla plików konfiguracyjnych, wiadomości SOAP i innych danych, których nie chciałbyś wklejać na stronę przesyłającą wszystko przy każdym naciśnięciu klawisza.
Dlaczego moje dwa pliki XML pokazują każdy wiersz jako różny?
Prawie zawsze to formatowanie, nie prawdziwe zmiany. Jeden plik jest zminifikowany lub wcięty tabulatorami, drugi dwiema spacjami, albo atrybuty są w innej kolejności. Kliknij Formatuj po obu stronach, aby używały tego samego wcięcia. Potem diff zwykle kurczy się do garstki wartości, które naprawdę się zmieniły. Dla ściślejszej normalizacji najpierw skanonikalizuj oba pliki za pomocą xmllint --c14n.
Czy kolejność atrybutów ma znaczenie przy porównywaniu XML?
Nie. W XML atrybuty elementu to zbiór nieuporządkowany, więc <a x="1" y="2"/> i <a y="2" x="1"/> są równoważne. Zwykły diff tekstowy o tym nie wie i oznaczy przestawienie jako zmianę. Kanoniczny XML sortuje atrybuty w stałej kolejności, więc skanonikalizowanie obu stron przed porównaniem sprawia, że fałszywy alarm znika. Kolejność elementów z kolei zwykle jest znacząca.
Jak porównać XML, ignorując prefiksy namespace'ów?
Prefiksy namespace'ów to lokalne etykiety dla URI namespace'u, więc ns1:user i u:user powiązane z tym samym URI to ten sam element. Aby porównać poprawnie, normalizuj według URI, a nie prefiksu. Najprostszy sposób to skanonikalizować oba dokumenty za pomocą xmllint --c14n, co spójnie przepisuje powiązania namespace'ów, a następnie zdiffować wyniki. Surowy diff tekstowy nie zrobi tego sam.
Czy mogę porównać duże pliki XML bez zawieszania strony?
Tak, do pewnego stopnia. Diff w trybie wierszowym pozostaje szybki na plikach z tysiącami wierszy, ponieważ najpierw porównuje całe wiersze zamiast każdego znaku. Bardzo duże pliki (kilka megabajtów) lepiej obsłużyć narzędziem wiersza poleceń, jak xmllint lub git diff, które strumieniuje dane. Dla wszystkiego, co da się wygodnie przewinąć w przeglądarce, diff online jest szybszą opcją.
Jaka jest różnica między diffem tekstowym a strukturalnym XML?
Diff tekstowy porównuje pliki wiersz po wierszu, tak samo jak porównałby dwa wypracowania. Diff strukturalny rozumie drzewo XML, więc wie, że przestawiony atrybut to nie zmiana, i potrafi zgłosić wstawiony element po jego ścieżce. Diffy tekstowe są szybsze i wystarczające do większości przeglądów, gdy obie strony są sformatowane. Diffy strukturalne mają znaczenie, gdy program musi zastosować zmianę lub gdy chcesz zignorować kolejność elementów.

Gotowy, żeby spróbować? Wklej swoje pliki do narzędzia porównywania XML i zobacz, co się zmieniło.