Zwei XML-Dateien vergleichen und Änderungen sehen

Der schnellste Weg, zwei XML-Dateien zu vergleichen, besteht darin, beide in ein nebeneinander angeordnetes Diff-Tool einzufügen, sie gleich zu formatieren und die hervorgehobenen Zeilen zu lesen. Das Vergleichen ist der einfache Teil. Das Rauschen verwirrt die Leute: umgeordnete Attribute, Leerzeichen zwischen Tags und Namespace-Präfixe können dazu führen, dass zwei Dateien, die dasselbe bedeuten, aussehen, als hätten sie nichts gemeinsam.

Dieser Leitfaden erklärt, wie man einen sauberen, zuverlässigen XML-Diff erhält. Wir schauen uns an, warum zwei äquivalente Dokumente auf dem Papier auseinander driften, welche Methoden es wert sind zu kennen, und ein ausgearbeitetes Beispiel zum Nachvollziehen. Wer einfach nur das Tool möchte, kann auf unserer XML-Vergleichsseite all das im Browser erledigen.

Warum XML-Dateien trügerisch schwer zu vergleichen sind

XML hat eine strenge Grammatik (siehe die XML-Spezifikation des W3C), gibt Autoren aber viel Freiheit bei der Textformatierung. Zwei Dokumente können exakt dieselben Daten beschreiben und trotzdem Byte für Byte unterschiedlich sein. Ein einfacher Textvergleich versteht nichts davon und markiert daher alles.

Hier ist das Wichtigste, das man sich merken sollte: In XML ist die Reihenfolge der Attribute eines Elements nicht signifikant. Das XML Information Set behandelt Attribute als ungeordnete Menge. Also tragen <user id="7" role="admin"/> und <user role="admin" id="7"/> dieselbe Information, auch wenn ein Zeilendiff sie rot und grün anzeigt. Die Reihenfolge der Elemente hingegen ist meist wichtig.

Sieht wie eine Änderung aus, ist es meist nicht
Was man im Diff siehtIst es eine echte Änderung?Was zu tun ist
Attribute in anderer ReihenfolgeNein, die Attributreihenfolge ist nicht signifikantBeide Seiten kanonisieren
2-Leerzeichen vs. 4-Leerzeichen-EinrückungNeinBeide Seiten gleich formatieren
Leerzeichen zwischen ElementenMeist nichtFormatieren oder unbedeutende Leerzeichen entfernen
<br/> vs. <br></br>Nein, dasselbe leere ElementBeide Seiten kanonisieren
Ein anderes Namespace-Präfix für denselben URINein, Präfixe sind beliebige BezeichnerNach Namespace-URI vergleichen, nicht nach Präfix
Kindelemente in anderer ReihenfolgeMeist ja, die Elementreihenfolge zähltUntersuchen, das ist wahrscheinlich echt

Die letzte Zeile ist die, auf die man achten muss. Die Attributreihenfolge ist frei, aber die Reihenfolge der Kindelemente ist in den meisten Schemata Teil des Dokuments. Wer das Detail dazu nachlesen möchte, wie ein Parser all das sieht, findet bei MDN eine solide Referenz zum Parsen von XML mit DOMParser.

Vier Methoden zum Vergleichen von XML und wann man zu welcher greift

Es gibt keine einzige beste Methode. Es hängt davon ab, wo die Dateien liegen und was man herausfinden möchte. So schneiden die gängigen Optionen ab.

MethodeAm besten fürAufwandVersteht XML?
Manuell durchlesenWinzige Dateien, ein oder zwei ElementeGeringNein, man ist der Parser
Online-Diff-ToolSchnelle Prüfungen, Einfügen von überallGeringMit Formatierung ja
Befehlszeile (xmllint)Dateien auf der Festplatte, Scripting, kanonische FormMittelJa, mit --c14n
IDE oder git diffDateien bereits in einem RepositoryGering bei CommitsStandardmäßig zeilenbasiert

Für die meisten Menschen gewinnt ein Browser-Tool an Geschwindigkeit: nichts zu installieren, und man kann einen Ausschnitt direkt aus einer Konfigurationsdatei oder einer SOAP-Antwort einfügen. Der Haken ist Formatierungsrauschen, das wir als nächstes behandeln. Wer viel im Terminal arbeitet, sollte xmllint von libxml2 kennen.

Der schnellste saubere Vergleich, Schritt für Schritt

Das ist die Vorgehensweise, die ich verwende, wenn mir jemand zwei Konfigurationsdateien gibt und fragt: "Was ist anders?" Es dauert etwa fünfzehn Sekunden.

  1. Das XML-Vergleichstool öffnen.
  2. Das Original links einfügen, die neue Version rechts.
  3. Auf beiden Seiten auf Formatieren klicken, damit sie die gleiche Einrückung verwenden.
  4. Nach echten Unterschieden suchen. Grün ist hinzugefügt, rot ist entfernt, und ein geänderter Wert erscheint als eines von beidem.
  5. Die Zeilen ignorieren, die nur Attribut-Umsortierung oder Leerzeichen sind.

Schritt drei ist der größte Teil des Tricks. Sobald beide Dokumente dieselbe Einrückung verwenden, bleibt nur noch hervorgehoben, was sich tatsächlich geändert hat. Unser Diff-Engine basiert auf Googles diff-match-patch, das zuerst zeilenweise vergleicht und so auch bei langen Dateien schnell bleibt.

Ein ausgearbeitetes Beispiel

Angenommen, man prüft eine Änderung an einer Service-Konfiguration. Hier ist der Vorher-Stand:

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

Und hier ist der Nachher-Stand, wie ein Teammitglied ihn übergeben hat:

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

Wirft man diese in einen rohen Zeilendiff, sieht die allererste Zeile geändert aus, weil id und role die Plätze getauscht haben. Formatiert man beide und vergleicht nach Bedeutung, ist die eigentliche Geschichte kurz:

Was sich tatsächlich geändert hat
KnotenVorherNachherÄnderung
@roleeditoradminGeändert
seats35Geändert
teamplatformHinzugefügt
@id77Keine Änderung (nur verschoben)
nameAda LovelaceAda LovelaceKeine Änderung

Drei echte Bearbeitungen: eine Rollenerhöhung, eine Sitzanzahl und ein neues Team-Element. Der Attributtausch war Rauschen. Diese Beförderung von editor zu admin ist genau die Art von Sache, die man im Review erkennen möchte, und sie ist leicht zu übersehen, wenn sie unter einer Zeile vergraben ist, die der Diff fälschlicherweise markiert hat.

Kanonisches XML: der richtige Weg, Rauschen zu ignorieren

Das Formatieren beider Seiten regelt die Einrückung, aber es gibt einen Standard, der genau für dieses Problem gebaut wurde. Kanonisches XML, vom W3C in Canonical XML 1.1 definiert, schreibt ein Dokument in eine einzige normalisierte Form um: Attribute sortiert, leere Elemente expandiert, Leerzeichen in Tags normalisiert und Standardattribute explizit gemacht. Zwei äquivalente Dokumente erzeugen identische kanonische Ausgabe. Es ist das XML-Äquivalent zum Sortieren von JSON-Schlüsseln.

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

Jetzt meldet diff nur noch Inhalt, der sich wirklich geändert hat, weil beide Dateien auf dieselbe Weise normalisiert wurden. Wenn man nur eine lesbare Einrückung statt der strengen kanonischen Form möchte, formatiert xmllint --format file.xml sie, was das Terminal-Äquivalent zum Klick auf Formatieren im Browser ist.

Namespaces: der Teil, der alle verwirrt

XML-Namespaces erlauben zwei Dokumenten, dasselbe Vokabular mit unterschiedlichen Präfix-Bezeichnern zu verwenden. <ns1:user> gebunden an einen URI und <u:user> gebunden an denselben URI sind dasselbe Element; das Präfix ist nur ein lokaler Spitzname. Ein Textdiff sieht ns1 gegen u und markiert eine Änderung, die keine ist. Die Lösung ist, nach dem Namespace-URI statt nach dem Präfix zu vergleichen, was genau das ist, was die Kanonisierung tut. Die Spezifikation Namespaces in XML ist die Referenz, falls man eine Diskussion darüber beilegen muss.

Häufige Fallstricke, auf die man achten sollte

FallstrickWarum er zuschlägtLösung
ZeichenkodierungEine UTF-8- und eine UTF-16-Datei können denselben Text enthalten, aber Byte für Byte abweichenKodierung normalisieren; die XML-Deklaration gibt sie an
Entity-Referenzen&amp; und ein literales & können beide für dasselbe Zeichen auftretenKanonisieren, was Entities konsistent auflöst
CDATA vs. maskierter Text<![CDATA[a<b]]> und a&lt;b sind derselbe TextinhaltDen geparsten Wert vergleichen, nicht die rohen Bytes
Signifikante LeerzeichenInnerhalb von xml:space="preserve" zählen Leerzeichen und dürfen nicht entfernt werdenNicht blind kürzen; xml:space respektieren
Selbstschließende Tags<x/> und <x></x> sind identischKanonisieren, damit beide gleich dargestellt werden

Textdiff vs. struktureller Diff

Alles oben Genannte ist ein Text-Diff: schnell, visuell und perfekt für einen Menschen, der eine Änderung liest. Ein struktureller Diff geht weiter und beschreibt die Änderung im Sinne des XML-Baums: dieses Attribut hat sich geändert, jenes Kindelement wurde an diesem Pfad eingefügt. Man möchte einen strukturellen Diff, wenn ein Programm die Änderung anwenden muss oder wenn die Elementreihenfolge wirklich keine Rolle spielt und man sie ignorieren möchte. Für den täglichen Review reicht ein Textdiff zweier formatierter Dokumente völlig aus.

Verwandte Tools

XML ist selten das einzige Format, mit dem man zu tun hat. Wenn man API-Payloads vergleicht, wendet JSON-Vergleich dieselbe Idee auf JSON an. Ausgezeichnete Seiten lassen sich leichter auf der HTML-Vergleichsseite lesen, und Umgebungseinstellungen passen gut auf das Config-Vergleichstool.

Häufig gestellte Fragen

Werden XML-Dateien beim Online-Vergleich irgendwo hochgeladen?
Auf comparetext.org läuft der Diff im Browser. Die beiden XML-Dateien werden von JavaScript auf dem eigenen Computer verglichen, sodass nichts an einen Server gesendet wird, es sei denn, man klickt ausdrücklich auf Speichern oder Teilen. Das macht es sicher für Konfigurationsdateien, SOAP-Nachrichten und andere Daten, die man nicht auf einer Website einfügen möchte, die bei jedem Tastendruck hochlädt.
Warum zeigen meine zwei XML-Dateien jede Zeile als unterschiedlich an?
Fast immer ist es Formatierung, keine echten Änderungen. Eine Datei ist minimiert oder mit Tabulatoren eingerückt, die andere mit zwei Leerzeichen, oder die Attribute sind in anderer Reihenfolge. Auf beiden Seiten Formatieren klicken, damit sie dieselbe Einrückung verwenden. Danach schrumpft der Diff meist auf die wenigen Werte, die sich wirklich geändert haben. Für eine strengere Normalisierung beide Dateien zuerst mit xmllint --c14n kanonisieren.
Spielt die Attributreihenfolge beim Vergleichen von XML eine Rolle?
Nein. In XML sind die Attribute eines Elements eine ungeordnete Menge, also sind <a x="1" y="2"/> und <a y="2" x="1"/> äquivalent. Ein einfacher Textdiff weiß das nicht und markiert die Umordnung als Änderung. Kanonisches XML sortiert Attribute in eine feste Reihenfolge, sodass das Kanonisieren beider Seiten vor dem Vergleich den Fehlalarm verschwinden lässt. Die Elementreihenfolge hingegen ist meist signifikant.
Wie vergleiche ich XML und ignoriere dabei Namespace-Präfixe?
Namespace-Präfixe sind lokale Bezeichner für einen Namespace-URI, also sind ns1:user und u:user, die an denselben URI gebunden sind, dasselbe Element. Um korrekt zu vergleichen, normalisiert man nach URI statt nach Präfix. Am einfachsten kanonisiert man beide Dokumente mit xmllint --c14n, das Namespace-Bindungen konsistent umschreibt, und vergleicht dann die Ergebnisse. Ein roher Textdiff kann das nicht allein.
Kann ich große XML-Dateien vergleichen, ohne dass die Seite einfriert?
Ja, bis zu einem gewissen Punkt. Ein zeilenbasierter Diff bleibt bei Dateien mit Tausenden von Zeilen schnell, weil er zunächst ganze Zeilen vergleicht statt jedes einzelne Zeichen. Sehr große Dateien (mehrere Megabyte) lassen sich besser mit einem Befehlszeilentool wie xmllint oder git diff bearbeiten, das die Daten streamt. Für alles, was man bequem in einem Browser durchscrollen kann, ist ein Online-Diff die schnellere Option.
Was ist der Unterschied zwischen einem Textdiff und einem strukturellen Diff von XML?
Ein Textdiff vergleicht die Dateien Zeile für Zeile, genauso wie er zwei Aufsätze vergleichen würde. Ein struktureller Diff versteht den XML-Baum, also weiß er, dass ein umgeordnetes Attribut keine Änderung ist, und kann ein eingefügtes Element über seinen Pfad melden. Textdiffs sind schneller und für die meisten Reviews ausreichend, sobald beide Seiten formatiert sind. Strukturelle Diffs sind wichtig, wenn ein Programm die Änderung anwenden muss oder wenn man die Elementreihenfolge ignorieren möchte.

Bereit, es auszuprobieren? Dateien in das XML-Vergleichstool einfügen und sehen, was sich geändert hat.