Twee XML-bestanden vergelijken en zien wat er wijzigde

De snelste manier om twee XML-bestanden te vergelijken is beide in een side-by-side diff-tool te plakken, ze op dezelfde manier op te maken en de gemarkeerde regels te lezen. Het vergelijken is het makkelijke deel. De ruis brengt mensen in de war: herschikte attributen, witruimte tussen tags en namespace-prefixen kunnen ervoor zorgen dat twee bestanden die hetzelfde betekenen lijken alsof ze niets gemeen hebben.

Deze gids laat zien hoe je een schone, betrouwbare XML-diff krijgt. We kijken naar waarom twee gelijkwaardige documenten op papier uit elkaar lopen, welke methoden de moeite waard zijn om te kennen, en een uitgewerkt voorbeeld om te volgen. Wil je alleen de tool, dan doet onze XML-vergelijkingspagina dit allemaal in de browser.

Waarom XML-bestanden bedrieglijk moeilijk te vergelijken zijn

XML heeft een strikte grammatica (zie de XML-specificatie van het W3C), maar geeft schrijvers veel vrijheid in hoe de tekst is opgemaakt. Twee documenten kunnen exact dezelfde gegevens beschrijven en toch byte voor byte verschillen. Een gewone tekst-diff begrijpt daar niets van, dus markeert hij alles.

Hier is het kernfeit om te onthouden: in XML is de volgorde van de attributen van een element niet significant. De XML Information Set behandelt attributen als een ongeordende verzameling. Dus <user id="7" role="admin"/> en <user role="admin" id="7"/> dragen dezelfde informatie, ook al kleurt een regel-diff ze rood en groen. De volgorde van elementen daarentegen doet er meestal wél toe.

Lijkt op een wijziging, maar is dat meestal niet
Wat je ziet in de diffIs het een echte wijziging?Wat te doen
Attributen in een andere volgordeNee, attribuutvolgorde is niet significantCanonicaliseer beide kanten
2-spaties vs 4-spaties inspringingNeeMaak beide kanten op dezelfde manier op
Witruimte tussen elementenMeestal nietMaak op, of verwijder onbeduidende witruimte
<br/> vs <br></br>Nee, hetzelfde lege elementCanonicaliseer beide kanten
Een ander namespace-prefix voor dezelfde URINee, prefixen zijn willekeurige labelsVergelijk op namespace-URI, niet op prefix
Kindelementen in een andere volgordeMeestal ja, de volgorde van elementen teltOnderzoek dit, dit is waarschijnlijk echt

Die laatste rij is degene om in de gaten te houden. De attribuutvolgorde is vrij, maar de volgorde van kindelementen is in de meeste schema's deel van het document. Wil je het detail van hoe een parser dit alles ziet, dan heeft MDN een degelijke referentie over het parsen van XML met DOMParser.

Vier manieren om XML te vergelijken en wanneer je voor elke kiest

Er is geen enkele beste methode. Het hangt af van waar de bestanden staan en wat je wilt leren. Zo verhouden de gangbare opties zich tot elkaar.

MethodeBeste voorInspanningBegrijpt XML?
Met het oogPiepkleine bestanden, een of twee elementenLaagNee, jij bent de parser
Online diff-toolSnelle checks, vanaf overal plakkenLaagMet opmaak, ja
Opdrachtregel (xmllint)Bestanden op schijf, scripting, canonieke vormGemiddeldJa, met --c14n
IDE of git diffBestanden al in een repositoryLaag indien gecommitStandaard regelgebaseerd

Voor de meeste mensen wint een browser-tool op snelheid: niets te installeren, en je kunt een fragment rechtstreeks uit een configuratiebestand of een SOAP-respons plakken. De adder onder het gras is opmaakruis, die we hierna behandelen. Leef je in de terminal, dan is xmllint van libxml2 de tool om te kennen.

De snelste schone vergelijking, stap voor stap

Dit is de routine die ik gebruik wanneer iemand me twee configuratiebestanden aanreikt en vraagt "wat is er anders?". Het kost ongeveer vijftien seconden.

  1. Open de XML-vergelijkingstool.
  2. Plak het origineel links, de nieuwe versie rechts.
  3. Klik op Opmaken aan beide kanten zodat ze dezelfde inspringing delen.
  4. Zoek naar echte verschillen. Groen is toegevoegd, rood is verwijderd, en een gewijzigde waarde verschijnt als één van elk.
  5. Negeer de regels die alleen attribuutherschikking of witruimte zijn.

Stap drie is het grootste deel van de truc. Zodra beide documenten dezelfde inspringing gebruiken, blijft alleen over wat er echt is veranderd om te markeren. Onze diff-engine is gebouwd op Googles diff-match-patch, die eerst regel voor regel vergelijkt zodat hij snel blijft, zelfs bij lange bestanden.

Een uitgewerkt voorbeeld

Stel dat je een wijziging in een serviceconfiguratie beoordeelt. Hier is het ervoor:

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

En hier is het erna, zoals een teamgenoot het je aanreikte:

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

Gooi die in een ruwe regel-diff en de allereerste regel lijkt gewijzigd, omdat id en role van plaats zijn gewisseld. Maak beide op, vergelijk op betekenis, en het echte verhaal is kort:

Wat er echt veranderde
KnoopErvoorErnaWijziging
@roleeditoradminGewijzigd
seats35Gewijzigd
teamplatformToegevoegd
@id77Geen wijziging (alleen verplaatst)
nameAda LovelaceAda LovelaceGeen wijziging

Drie echte bewerkingen: een rolverhoging, een aantal stoelen en een nieuw team-element. De attribuutwissel was ruis. Die promotie van editor naar admin is precies het soort ding dat je wilt opmerken in een review, en het is makkelijk te missen wanneer het begraven ligt onder een regel die de diff ten onrechte markeerde.

Canoniek XML: de juiste manier om ruis te negeren

Beide kanten opmaken regelt de inspringing, maar er bestaat een standaard die precies voor dit probleem gebouwd is. Canoniek XML, gedefinieerd door het W3C in Canonical XML 1.1, herschrijft een document tot één genormaliseerde vorm: attributen gesorteerd, lege elementen uitgevouwen, witruimte in tags genormaliseerd, en standaardattributen expliciet gemaakt. Twee gelijkwaardige documenten leveren identieke canonieke uitvoer. Het is het XML-equivalent van het sorteren van JSON-sleutels.

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

Nu meldt diff alleen nog inhoud die echt is veranderd, omdat beide bestanden op dezelfde manier zijn genormaliseerd. Wil je alleen leesbare inspringing in plaats van de strikte canonieke vorm, dan maakt xmllint --format file.xml het op, wat het terminal-equivalent is van op Opmaken klikken in de browser.

Namespaces: het deel dat iedereen in verwarring brengt

Met XML-namespaces kunnen twee documenten dezelfde woordenschat gebruiken met verschillende prefix-labels. <ns1:user> gebonden aan een URI en <u:user> gebonden aan dezelfde URI zijn hetzelfde element; het prefix is slechts een lokale bijnaam. Een tekst-diff ziet ns1 tegen u en markeert een wijziging die er niet is. De oplossing is vergelijken op de namespace-URI in plaats van het prefix, wat precies is wat canonicalisatie doet. De specificatie Namespaces in XML is de referentie als je een discussie hierover wilt beslechten.

Veelvoorkomende valkuilen om op te letten

ValkuilWaarom het bijtOplossing
TekencoderingEen UTF-8- en een UTF-16-bestand kunnen dezelfde tekst bevatten maar byte voor byte verschillenNormaliseer de codering; de XML-declaratie vermeldt deze
Entiteitsreferenties&amp; en een letterlijke & kunnen beide voor hetzelfde teken voorkomenCanonicaliseer, wat entiteiten consistent oplost
CDATA vs ge-escapete tekst<![CDATA[a<b]]> en a&lt;b zijn dezelfde tekstinhoudVergelijk de geparste waarde, niet de ruwe bytes
Significante witruimteBinnen xml:space="preserve" tellen spaties en mogen ze niet worden verwijderdKnip niet blindelings; respecteer xml:space
Zelfsluitende tags<x/> en <x></x> zijn identiekCanonicaliseer zodat beide op dezelfde manier worden weergegeven

Tekst-diff vs structurele diff

Alles hierboven is een tekst-diff: snel, visueel en perfect voor een persoon die een wijziging leest. Een structurele diff gaat verder en beschrijft de wijziging in termen van de XML-boom: dit attribuut is veranderd, dat kindelement is op dit pad ingevoegd. Je wilt een structurele diff wanneer een programma de wijziging moet toepassen of wanneer de volgorde van elementen er echt niet toe doet en je die wilt negeren. Voor dagelijkse review is een tekst-diff van twee opgemaakte documenten ruim voldoende.

Gerelateerde tools

XML is zelden het enige formaat waarmee je te maken hebt. Vergelijk je API-payloads, dan past JSON vergelijken hetzelfde idee toe op JSON. Pagina's met opmaak zijn makkelijker te lezen op de HTML-vergelijkingspagina, en omgevingsinstellingen lijnen mooi uit in de config-vergelijkingstool.

Veelgestelde vragen

Worden XML-bestanden bij online vergelijken ergens geüpload?
Op comparetext.org draait de diff in je browser. De twee XML-bestanden worden door JavaScript op je eigen machine vergeleken, dus er wordt niets naar een server gestuurd tenzij je expliciet op Opslaan of Delen klikt. Dat maakt het veilig voor configuratiebestanden, SOAP-berichten en andere gegevens die je niet zou willen plakken in een site die bij elke toetsaanslag uploadt.
Waarom tonen mijn twee XML-bestanden elke regel als verschillend?
Bijna altijd is het opmaak, geen echte wijzigingen. Het ene bestand is geminificeerd of met tabs ingesprongen, het andere met twee spaties, of de attributen staan in een andere volgorde. Klik op Opmaken aan beide kanten zodat ze dezelfde inspringing gebruiken. Daarna krimpt de diff meestal tot de handvol waarden die echt veranderd zijn. Voor een striktere normalisatie canonicaliseer je beide bestanden eerst met xmllint --c14n.
Doet de attribuutvolgorde ertoe bij het vergelijken van XML?
Nee. In XML zijn de attributen van een element een ongeordende verzameling, dus <a x="1" y="2"/> en <a y="2" x="1"/> zijn gelijkwaardig. Een gewone tekst-diff weet dit niet en markeert de herschikking als een wijziging. Canoniek XML sorteert attributen in een vaste volgorde, dus beide kanten canonicaliseren vóór het vergelijken laat de valse melding verdwijnen. De volgorde van elementen is daarentegen meestal wél significant.
Hoe vergelijk ik XML terwijl ik namespace-prefixen negeer?
Namespace-prefixen zijn lokale labels voor een namespace-URI, dus ns1:user en u:user die aan dezelfde URI zijn gebonden zijn hetzelfde element. Om correct te vergelijken normaliseer je op URI in plaats van op prefix. De eenvoudigste manier is beide documenten canonicaliseren met xmllint --c14n, wat namespace-bindingen consistent herschrijft, en daarna de resultaten diffen. Een ruwe tekst-diff kan dit niet zelfstandig.
Kan ik grote XML-bestanden vergelijken zonder dat de pagina vastloopt?
Ja, tot op zekere hoogte. Een regelmodus-diff blijft snel op bestanden met duizenden regels omdat hij eerst hele regels vergelijkt in plaats van elk teken. Heel grote bestanden (meerdere megabytes) kun je beter verwerken met een opdrachtregeltool zoals xmllint of git diff, dat de gegevens streamt. Voor alles wat je comfortabel kunt scrollen in een browser is een online diff de snellere optie.
Wat is het verschil tussen een tekst-diff en een structurele diff van XML?
Een tekst-diff vergelijkt de bestanden regel voor regel, net zoals hij twee opstellen zou vergelijken. Een structurele diff begrijpt de XML-boom, dus hij weet dat een herschikt attribuut geen wijziging is en kan een ingevoegd element via zijn pad melden. Tekst-diffs zijn sneller en goed genoeg voor de meeste reviews zodra beide kanten opgemaakt zijn. Structurele diffs doen ertoe wanneer een programma de wijziging moet toepassen of wanneer je de volgorde van elementen wilt negeren.

Klaar om het te proberen? Plak je bestanden in de XML-vergelijkingstool en zie wat er veranderde.