2つのXMLファイルを比較して変更点を見つける方法
2つのXMLファイルを比較する最速の方法は、両方をサイドバイサイドのdiffツールに貼り付け、 同じ形式にフォーマットして、ハイライトされた行を読むことです。比較そのものは簡単な部分です。 人を惑わせるのはノイズです。属性の並び替え、タグ間の空白、namespaceの接頭辞によって、 同じ意味を持つ2つのファイルがまるで共通点がないように見えてしまいます。
このガイドでは、クリーンで信頼できるXMLのdiffを取得する方法を解説します。 等価な2つのドキュメントが紙の上で乖離する理由、知っておくべき手法、 そして実際に試せるワークド・エグザンプルを紹介します。ツールだけが必要な場合は、 XML比較ページでブラウザ上のすべてを処理できます。
XMLファイルの比較が意外と難しい理由
XMLは厳格な文法を持っていますが(W3CのXML仕様を参照)、 テキストのレイアウト方法については多くの自由度を書き手に与えています。2つのドキュメントが まったく同じデータを記述していても、バイト単位では異なる場合があります。プレーンテキストのdiffは そのことを理解しないため、すべての差異をフラグとして立てます。
覚えておくべき重要な事実があります。XMLでは、要素の属性の順序は意味を持ちません。
XML Information Set
は属性を順序なしの集合として扱います。そのため、
<user id="7" role="admin"/>と
<user role="admin" id="7"/>は同じ情報を持っていますが、
行ベースのdiffでは赤と緑に塗られてしまいます。一方、要素の順序は通常は意味を持ちます。
| diffで見えるもの | 本当の変更か? | 対処法 |
|---|---|---|
| 属性の順序が異なる | いいえ、属性の順序は意味を持たない | 両側を正規化する |
| 2スペースvs 4スペースのインデント | いいえ | 両側を同じ形式にフォーマットする |
| 要素間の空白 | 通常はいいえ | フォーマットするか、意味のない空白を取り除く |
<br/> vs <br></br> | いいえ、同じ空要素 | 両側を正規化する |
| 同じURIに対する異なるnamespace接頭辞 | いいえ、接頭辞は任意のラベル | 接頭辞ではなくnamespace URIで比較する |
| 子要素の順序が異なる | 通常はい、要素の順序は重要 | 調査が必要、これは本物の可能性が高い |
最後の行が注意すべきものです。属性の順序は自由ですが、子要素の順序はほとんどのスキーマで ドキュメントの一部です。パーサーがこれらをどう見ているかの詳細は、MDNに優れた DOMParserによるXMLの解析のリファレンスがあります。
XMLを比較する4つの方法とその使い分け
唯一の最良の方法はありません。ファイルがどこにあるか、何を調べたいかによって異なります。 よく使われる方法を比較します。
| 方法 | 向いている場面 | 手間 | XMLを理解するか? |
|---|---|---|---|
| 目視確認 | 小さなファイル、1〜2要素 | 低 | いいえ、あなたがパーサー |
| オンラインdiffツール | クイックチェック、どこからでも貼り付け可能 | 低 | フォーマットすれば対応 |
コマンドライン(xmllint) | ディスク上のファイル、スクリプティング、正規形 | 中 | はい、--c14nで対応 |
IDEまたはgit diff | リポジトリ内のファイル | コミット済みなら低 | デフォルトは行ベース |
ほとんどの人にとって、ブラウザツールはインストール不要で、設定ファイルやSOAPレスポンスの
断片を直接貼り付けられるため、速度面で優れています。問題はフォーマットのノイズで、次で解決します。
ターミナルで作業する場合は、libxml2の
xmllintが知っておくべきツールです。
最速のクリーン比較、ステップバイステップ
誰かが2つの設定ファイルを渡して「何が違うの?」と聞いてきたときに使うルーティンです。 約15秒で完了します。
- XML比較ツールを開く。
- 左に元のファイル、右に新しいバージョンを貼り付ける。
- 両側でフォーマットをクリックして、同じインデントを共有するようにする。
- 本物の違いを探す。緑が追加、赤が削除、変更された値はそれぞれ1つずつ表示される。
- 属性の並び替えや空白だけの行は無視する。
ステップ3がほぼすべての要点です。両方のドキュメントが同じインデントを使用すると、 ハイライトされるのは実際に変更された内容だけになります。diffエンジンはGoogleの diff-match-patch を基盤としており、最初に行単位で比較するため、長いファイルでも高速に動作します。
実例
サービス設定への変更をレビューしているとします。変更前:
<user id="7" role="editor">
<name>Ada Lovelace</name>
<active>true</active>
<seats>3</seats>
</user>
そして、チームメイトから受け取った変更後:
<user role="admin" id="7">
<name>Ada Lovelace</name>
<active>true</active>
<seats>5</seats>
<team>platform</team>
</user>
これをプレーンな行ベースdiffに入力すると、idとroleが
入れ替わったため、最初の行が変更されたように見えます。両方をフォーマットして意味で比較すると、
実際の話は短くなります:
| ノード | 変更前 | 変更後 | 変更 |
|---|---|---|---|
@role | editor | admin | 変更 |
seats | 3 | 5 | 変更 |
team | — | platform | 追加 |
@id | 7 | 7 | 変更なし(移動のみ) |
name | Ada Lovelace | Ada Lovelace | 変更なし |
本物の変更は3つ:ロールの昇格、シート数、新しいチーム要素。
属性の入れ替えはノイズでした。editorからadminへの昇格は
レビューで捉えたい変更ですが、diffが誤ってフラグした行に埋もれていると見逃しやすいです。
正規化XML:ノイズを無視する正しい方法
両側をフォーマットするとインデントは処理できますが、この問題のために作られた標準があります。 W3CがCanonical XML 1.1 で定義した正規化XMLは、ドキュメントを単一の正規化された形式に書き換えます。属性はソートされ、 空要素は展開され、タグ内の空白は正規化され、デフォルト属性は明示されます。等価な2つのドキュメントは 同一の正規化出力を生成します。これはJSONのキーをソートするXML版です。
xmllint --c14n old.xml > old.c14n.xml
xmllint --c14n new.xml > new.c14n.xml
diff old.c14n.xml new.c14n.xml
これでdiffは本当に変更された内容のみを報告します。両ファイルが同じ方法で
正規化されているためです。厳密な正規形ではなく読みやすいインデントだけが欲しい場合は、
xmllint --format file.xmlがフォーマットします。これはブラウザでフォーマットを
クリックするターミナル版です。
Namespace:誰もが混乱する部分
XMLのnamespaceにより、2つのドキュメントが異なる接頭辞ラベルで同じ語彙を使用できます。
あるURIにバインドされた<ns1:user>と、同じURIにバインドされた
<u:user>は同じ要素です。接頭辞はローカルなニックネームにすぎません。
テキストdiffはns1とuを見て、変更ではないものを変更としてフラグします。
解決策は、接頭辞ではなくnamespace URIで比較することで、これはまさに正規化が行うことです。
Namespaces in XML
の仕様は、これについて議論を決着させる必要がある場合のリファレンスです。
注意すべき一般的な落とし穴
| 落とし穴 | なぜ問題になるか | 対処法 |
|---|---|---|
| 文字エンコーディング | UTF-8ファイルとUTF-16ファイルは同じテキストを保持できるがバイト単位では異なる | エンコーディングを正規化する。XML宣言がそれを示す |
| 実体参照 | &とリテラルの&は同じ文字に対して両方現れる場合がある | 正規化し、実体を一貫して解決する |
| CDATA vs エスケープテキスト | <![CDATA[a<b]]>とa<bは同じテキスト内容 | 生のバイトではなく、解析された値を比較する |
| 意味のある空白 | xml:space="preserve"の内側では空白が重要で、取り除いてはいけない | むやみにトリムせず、xml:spaceを尊重する |
| 自己終了タグ | <x/>と<x></x>は同一 | 正規化して両方が同じように描画されるようにする |
テキストdiff vs 構造的diff
上記のすべてはテキストdiffです。高速で視覚的であり、変更を読む人間には最適です。 構造的diffはさらに進んで、XMLツリーの観点から変更を記述します。この属性が変わった、 あの子要素がこのパスに挿入された、というように。プログラムが変更を適用する必要がある場合や、 要素の順序が本当に重要でなく無視したい場合に構造的diffが必要です。日常のレビューでは、 フォーマットされた2つのドキュメントのテキストdiffで十分です。
関連ツール
XMLだけを扱うことはほとんどありません。APIのペイロードを比較するなら、 JSON比較が同じアイデアをJSONに適用します。 マークアップされたページはHTML比較ページで読みやすくなり、 環境設定はconfig比較ツールでうまく並びます。
よくある質問
- XMLファイルをオンラインで比較するとどこかにアップロードされますか?
- comparetext.orgではdiffはブラウザで実行されます。2つのXMLファイルはあなた自身のマシン上のJavaScriptによって比較されるため、明示的に保存または共有をクリックしない限り、サーバーには何も送信されません。設定ファイル、SOAPメッセージ、キーストロークのたびにアップロードするサイトに貼り付けたくないその他のデータにも安全です。
- 2つのXMLファイルがすべての行で差異があると表示されるのはなぜですか?
- ほぼ常にフォーマットの問題であり、本物の変更ではありません。一方のファイルがminifiedまたはタブでインデントされており、もう一方は2スペース、あるいは属性が異なる順序になっています。両側でフォーマットをクリックして同じインデントを使用するようにします。その後、diffは通常、本当に変更された少数の値に縮小されます。より厳密な正規化には、まず両ファイルをxmllint --c14nで正規化します。
- XMLを比較する際に属性の順序は重要ですか?
- いいえ。XMLでは要素の属性は順序なしの集合なので、
<a x="1" y="2"/>と<a y="2" x="1"/>は等価です。プレーンテキストdiffはこれを知らず、並び替えを変更としてフラグします。正規化XMLは属性を固定された順序にソートするため、比較前に両側を正規化すると誤検知が消えます。一方、要素の順序は通常は意味を持ちます。 - namespace接頭辞を無視してXMLを比較するにはどうすればよいですか?
- namespace接頭辞はnamespace URIのローカルなラベルなので、同じURIにバインドされた
ns1:userとu:userは同じ要素です。正しく比較するには、接頭辞ではなくURIで正規化します。最も簡単な方法は、両ドキュメントをxmllint --c14nで正規化することです。これはnamespaceのバインディングを一貫して書き換え、その後で結果をdiffします。生のテキストdiffは単独ではこれを実行できません。 - ページがフリーズせずに大きなXMLファイルを比較できますか?
- はい、ある程度まで。行モードdiffは、各文字ではなくまず行全体を比較するため、数千行のファイルでも高速です。非常に大きなファイル(数メガバイト)は、データをストリームするxmllintやgit diffなどのコマンドラインツールで処理する方が適しています。ブラウザで快適にスクロールできるものなら、オンラインdiffが速い選択肢です。
- XMLのテキストdiffと構造的diffの違いは何ですか?
- テキストdiffはファイルを行ごとに比較します。2つのエッセイを比較するのと同じ方法です。構造的diffはXMLツリーを理解しているため、並び替えられた属性が変更ではないことを知っており、挿入された要素をそのパスで報告できます。テキストdiffは高速で、両側がフォーマットされていればほとんどのレビューには十分です。構造的diffは、プログラムが変更を適用する必要がある場合や、要素の順序を無視したい場合に重要です。
試してみませんか?XML比較ツールにファイルを貼り付けて、何が変わったか確認しましょう。