XML ビューアー

折りたためるツリーとソースを左右に並べて表示。

入力
ツリーノードをクリックすると折りたたまれます

何か貼り付けると、ここに文書構造が表示されます。

待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

上に文書を貼り付けると、同じものが 2 通りに表示されます。左はシンタックスハイライト・行番号・折りたたみ付きのソース、右は同じ解析結果から組み立てた折りたためるツリーです。ノードは最初すべて展開されています。要素をクリックすると、その要素と配下がまとめて折りたたまれます。両方のペインは 1 回の走査から作られるので、ツリーとエラー一覧が常に同じ文書を指します。

ツリー表示が本領を発揮するのは、その文書を書いたのが自分ではないときです。ドキュメントが PDF しかないベンダーの API レスポンス、前任チームが残した 4,000 行の設定、レコード 1 件のはずが 200 件入っているかもしれないエクスポート。知りたいのはたいてい構文エラーの場所ではなく、これがどんな形をしていて、必要な値がどこにあるかです。

読んでいるあいだも検証が走り、整形式の問題は最初の 1 件で止まらず、行・列・直し方の提案つきですべて報告されます。アップロードもありません。パーサーもツリーもエディターも、すべてこのタブで動きます。

ツリーに何が出るか

各要素が 1 行です。書かれたとおりのタグ名(プレフィックス込み)に続いて、属性が name="value" の形で並びます。子を持たない要素には「空」と印が付くので、<status/> と自分で折りたたんだノードを見分けられます。テキストは要素の下の葉として現れ、200 文字で打ち切られます。ツリーは構造を見るためのものであり、40 KB の base64 を全部描画すればそれが埋もれてしまうからです。コメントはプレースホルダーの行として表示されます。

意図的に出さないものが 2 つあります。CDATA セクションと処理命令です。それらはソースペインで読んでください。ソースペインは貼り付けたままの文書を表示し、常にそちらが正です。ツリーは本物の DOM で、要素ごとに折りたためるノードが 1 つ。だから折りたたみが即座で、テキストも選択できます。同時にそれは代償でもあり、要素が数万ある文書では、XML の解析よりツリーの構築のほうに時間がかかります。

ツリーが得意なこと、苦手なこと

ツリーは「形」に関する問いに、スクロールよりずっと速く答えます。1 段下をすべて折りたためば、最上位のセクションが 1 画面に収まります。SOAP の Header を折りたためば Body がペインの先頭に来ます。コンテナの下に並ぶ繰り返し行を見れば、そのレスポンスがレコード 1 件なのか複数件なのかが分かります。それはテストでは動くのに本番でデータを取りこぼす利用側との分かれ目です。苦手なことは 3 つあり、隠すよりはっきり言うほうが役に立ちます。

  • 編集。ツリーは入力のたびに作り直されるので、書き込む先がそもそもありません。ソースペインで編集し、ツリーが組み直されるのを見てください。
  • 比較。ツリーを 2 つ並べるのは、両側の書式をまず正規化する diff より、差分を見つける手段として劣ります。
  • 抽出。「すべての明細のすべての価格」は XPath 式であって、スクロールの作業ではありません。

深い入れ子と、プレフィックスを淡く表示する理由

名前空間つきの XML が読みにくいのは、名前空間が複雑だからではありません。soap:Body や ns1:PlaceOrder のプレフィックスは足場です。その名前がどの語彙に属すかを告げるもので、いったん分かってしまえば、以降の行では雑音でしかありません。インデントが 8 段にもなると、画面の文字のかなりの割合がプレフィックスとコロンになります。

そこでエディターは、プレフィックスとコロンをローカル名より薄く描きます。すると soap:Body は「印の付いた Body」として読めます。何も隠していませんし、変えてもいません。テキストは選択でき、コピーもそのままです。例外は宣言です。xmlns:soap ではプレフィックスこそが中身であって足場ではないので、こちらは通常の濃さのままにします。束縛されていないプレフィックスの使用はエラーとして報告し、追加すべき宣言も示します。大きな文書から断片をコピーしたときによく起きます。

値を探す

リテラル文字列なら、ソースペインにカーソルを置いて Ctrl-F(Mac なら Cmd-F)を押してください。エディターの検索パネルが上部に開き、一致箇所のハイライトと置換欄が使えます。検索対象はソースなので、ツリーが省略している部分も含め、すべてのノードの全文が対象になります。

構造に関する問いなら XPath を使ってください。//order[status="failed"]/@id は、スクロール 20 回ぶんの答えを 1 手で返します。このサイトの XPath テスターは、文書から拾ったプレフィックスを使って XPath 1.0 を評価します。「式が何も返さない」という報告のほとんどは、次の 1 点で説明できます。XPath 1.0 には既定名前空間がありません。ルートが xmlns="urn:example:orders" を宣言していれば //order は何にも一致しません。//order は「名前空間なしの order 要素」を意味するからです。プレフィックスを束縛して //o:order と書くか、//*[local-name()="order"] で代用してください。

コードで文書をたどる

文書を手で読むのは、たいていそれを毎日読むコードを書くための一歩です。次のサンプルは文書をたどって構造を出力します。上のツリーのプログラム版です。どれも安全に解析します。Java と Python の既定は外部実体を解決してしまうからです。

// Print the shape of a document: element paths with a count, which is
// usually more useful than the tree itself when you are writing a consumer.
function outline(source) {
  const doc = new DOMParser().parseFromString(source, 'application/xml');
  const error = doc.querySelector('parsererror');
  if (error) throw new Error(error.textContent.trim());

  const counts = new Map();
  const walk = (el, path) => {
    const here = path + '/' + el.nodeName;
    counts.set(here, (counts.get(here) ?? 0) + 1);
    for (const child of el.children) walk(child, here);
  };
  walk(doc.documentElement, '');

  for (const [path, n] of [...counts].sort()) {
    console.log(n.toString().padStart(6), path);
  }
}

// nodeName keeps the prefix as written. For namespace-aware work use
// el.localName with el.namespaceURI, since the prefix is arbitrary and the
// URI is what identifies the vocabulary.
# iterparse reads the document as a stream and lets you drop each element
# once you are done with it, so memory stays flat on files far larger than a
# browser will open.
from defusedxml.ElementTree import iterparse
from collections import Counter

counts = Counter()
stack = []

for event, el in iterparse('document.xml', events=('start', 'end')):
    if event == 'start':
        stack.append(el.tag)
        counts['/'.join(stack)] += 1
    else:
        stack.pop()
        el.clear()          # release the subtree; this is the whole point

for path, n in sorted(counts.items()):
    print(f'{n:6d}  {path}')

# ElementTree reports tags as '{namespace-uri}local' rather than 'prefix:local'
# because the prefix is arbitrary. Split on '}' when you want the local name.
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;

public final class Outline {

    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);                 // off by default
        dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
        dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        dbf.setXIncludeAware(false);

        Document doc = dbf.newDocumentBuilder().parse(new java.io.File(args[0]));
        print(doc.getDocumentElement(), 0);
    }

    static void print(Node node, int depth) {
        if (node.getNodeType() != Node.ELEMENT_NODE) return;
        Element el = (Element) node;

        StringBuilder line = new StringBuilder("  ".repeat(depth));
        line.append(el.getNodeName());
        NamedNodeMap attrs = el.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Node a = attrs.item(i);
            line.append(' ').append(a.getNodeName())
                .append("=\"").append(a.getNodeValue()).append('"');
        }
        System.out.println(line);

        NodeList kids = el.getChildNodes();
        for (int i = 0; i < kids.getLength(); i++) print(kids.item(i), depth + 1);
    }
}
using System.Xml;
using System.Xml.Linq;

// XDocument.Load uses a reader with DtdProcessing.Prohibit by default, so
// external entities are never fetched. Construct the reader yourself when
// you want that guarantee stated in the code rather than in the docs.
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
    MaxCharactersInDocument = 20L * 1024 * 1024,
};

using var reader = XmlReader.Create("document.xml", settings);
var doc = XDocument.Load(reader);

// One line per distinct element path, with how often it occurs.
var paths = doc.Descendants()
    .GroupBy(e => string.Join(
        "/",
        e.AncestorsAndSelf().Reverse().Select(a => a.Name.LocalName)))
    .OrderBy(g => g.Key);

foreach (var group in paths)
{
    Console.WriteLine(group.Count().ToString().PadLeft(6) + "  " + group.Key);
}
# xmllint has an interactive tree browser almost nobody knows about. It gives
# you ls, cd, cat, du and pwd over the document, with XPath as the path
# syntax: the terminal equivalent of the tree on this page.
xmllint --nonet --shell document.xml
#   / > du                      show the subtree shape
#   / > cd //order[1]           navigate by XPath
#   / > ls                      list children of the current node
#   / > cat status              print a node
#   / > exit

# Non-interactive: tag counts, a rough outline of an unfamiliar document.
xmllint --nonet --format document.xml |
  grep -o '<[a-zA-Z_][^ />]*' |
  sort | uniq -c | sort -rn | head -30

# One value, straight out:
xmllint --nonet --xpath 'string(//order[1]/status)' document.xml

シェルの例にある grep のアウトラインは解析ではなく数え上げの小技です。コメントや CDATA セクションの中に現れたタグ名も平然と数えます。他のサンプルはいずれも先に木を組み立てます。それが、おおよその答えと正しい答えの違いです。

よくある質問

表示するために XML はアップロードされますか。

いいえ。パーサーもツリーもエディターも、このタブで動く JavaScript です。サーバー側の仕組みも、エディターにアクセスできる解析ツールも、サードパーティのスクリプトもありません。ネットワークタブを開いて文書を貼り付けてみてください。ページ自身のアセットが一度読み込まれ、そのあとは静かなままです。

このページではその確認が特に重要です。「中身を見る」という行為は、たいてい他人のデータに対して行うものだからです。ビューアーに貼り付ける文書は、たいてい生きた API レスポンスか本番のエクスポートで、氏名や口座番号、ヘッダーのトークンがそのまま入っています。この検索で上位に出るツールのいくつかはそれをサーバーへ送りますし、保存した文書を既定で公開にするものもあります。

ツリー表示は実際のところ何の役に立ちますか。

構造についての問いに答えるためです。どの要素が繰り返されるか、入れ子はどこまで深いか、目当ての値はどの枝にあるか、任意要素はこのレスポンスに含まれているか。どれもインデントされたテキストでは見づらく、ツリーなら一目です。

元が取れる使い方はこうです。ルートの 1 段下をすべて折りたたみ、セクション名を読み、必要なものだけ展開し、残り 3,000 行は無視する。もし問いが構文の話なら、あなたが見るべきはソースペインとエラー一覧のほうです。

ツリー上で XML を編集できますか。

できません。ツリーは入力に合わせて作り直されるので、編集の対象として残り続けるものがありません。変更は左のソースペインで行ってください。

これは意図した制限です。ツリー編集器は変更のたびに、空白・混在内容・コメントの位置・属性の順序をどう扱うか決めなければなりません。安全側に倒すと使い勝手が悪くなり、便利側に倒すと、触っていない部分を黙って書き換えてしまいます。

特定の値を見つけるには。

リテラル文字列なら、ソースペインにカーソルを置いて Ctrl-F(Mac は Cmd-F)を押します。検索パネルが上部に開き、一致箇所がハイライトされます。検索対象はツリーが省略した表示ではなく全文です。

構造に関わることなら XPath です。//order[status="failed"]/@id なら 1 手で答えが出ます。要素は確かにあるのに式が何も返さないときは、既定名前空間を疑ってください。XPath 1.0 に既定名前空間はないので、プレフィックスを束縛して //o:order と書くまで、//order は urn:example:orders の要素に一致しません。

ブラウザーがファイルを表示せず「このページには次のエラーが含まれています」と言うのはなぜですか。

それは Chrome や Firefox に内蔵された XML ビューアーが、その文書は整形式でないと告げているのです。どちらも最初のエラーで止まり、そこまでしか描画しません。メッセージは libxml2 か Expat のもので、パーサーを書いた人向けの言葉です。「Opening and ending tag mismatch」「Document is empty」といった具合に。

ここに貼り付ければ、すべての問題が行・列・提案つきで一度に出ますし、解析できた部分についてはツリーも表示されます。よくある原因は、生のアンパサンド、途中で切れたダウンロード、XML 宣言より前にあるバイト順マーク、そして XML のコンテンツタイプで返された HTML のエラーページです。

どのくらいの大きさのファイルを開けますか。

20 MB までですが、実際には解析より先にツリーが重くなります。1 MB の文書の走査はおよそ 110 ミリ秒。要素ごとに折りたためるノードを 1 つ作る作業は、数万要素ともなればそれより時間がかかります。

この上限があるのは、何もアップロードしないからです。文書もそのツリーも、どちらもこのタブのメモリーに載ります。それより大きなファイルは、手元でストリーミング処理できるものを使ってください。閲覧なら xmllint --shell、抽出なら Python の iterparse です。

関連ツール

関連する解説