XML 構文チェッカー

すべての構文エラーを一度に。行・列・直し方つき。

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

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

上に文書を貼り付けるか、エディターにファイルをドロップしてください。入力しながら XML 1.0 の整形式規則に照らして検査します。違反はすべて行・列・代わりに書くべき内容つきで並び、結果をクリックするとカーソルが問題の文字へ移動します。

これは、他のどの検査より先に通っていなければならない検査です。スキーマバリデーターも、XSLT プロセッサーも、SOAP スタックも、ブラウザーも、構文の壊れた文書は見ようとしません。ビルドから出てきた「premature end of data in tag」は構文の問題であり、XSD をいくら読んでも説明はつきません。

ここが違うのは、スキャナーが最初の失敗で止まらないことです。復帰して読み進めるので、1 回のパスで 4 行目の引用符のない属性、12 行目の生のアンパサンド、末尾で閉じられずに残ったタグをまとめて報告します。37 あるエラー分類にはそれぞれ独自の文言と独自の直し方があり、何でもかんでも 1 つの「Syntax Error」で片付けたりしません。アップロードもありません。ネットワークパネルを開いて、空のままなのを見てください。

「整形式」とは実際に何を指すのか

XML 1.0 は整形式制約を 12 個挙げ、残りは文法に委ねています。文書が通るか落ちるかという検査に落とし込むと、次の一覧になります。ここではその 1 行ずつをすべて適用しています。

  • ルート要素はちょうど 1 つ。その外側に置けるのはコメント、処理命令、空白だけです。
  • すべての開始タグが、名前の完全に一致する終了タグで閉じられていること。名前は大文字小文字を区別するので、</Note> は <note> を閉じません。
  • 要素は入れ子であって重なってはいけません。<b><i>x</b></i> は XML ではありません。あなたの HTML パーサーがどれだけ寛容だったとしてもです。
  • 属性値は引用符で囲まれていること。同じ要素に同じ属性を 2 回書かないこと。
  • テキスト中にも属性値中にもリテラルの < を置かないこと。参照の外に裸の & を置かないこと。テキスト中の > は合法ですが、]]> という並びは違います。
  • 定義済み実体は amp、lt、gt、quot、apos の 5 つだけ。DTD が追加を宣言している場合を除きます。&nbsp; は HTML のものであり、この 5 つには含まれません。
  • XML が許す集合の外の文字を使わないこと。-- を含むコメントを書かないこと。宣言を置くならファイルの先頭に、version、encoding、standalone の順で書くこと。
  • すべての名前空間プレフィックスが、スコープ内の xmlns 宣言で束縛されていること。この規則は XML 1.0 ではなく Namespaces in XML に由来し、無料バリデーターが最もよく飛ばす項目でもあります。大手の競合は、ファイルのどこにも束縛がないまま <ns:root> を「有効」と判定します。

すべてのエラーを一度に。他のツールが 1 件しか出さない理由

仕様は、致命的エラーのあと通常の処理を続けてはならないとプロセッサーに指示しています。DOMParser も Expat も .NET も最初の問題を報告して止まるのはそのためです。これは怠慢ではなく正しい振る舞いであり、同時に、大きな文書を直すのに 1 件につき 1 往復かかる理由でもあります。

ここでの復帰処理は意図したものです。終了タグが一致しないとき、スキャナーはその名前が開いている要素スタックの上のほうにないか探します。見つかれば、本当の間違いはその間で開いたままになっている要素のほうなので、それぞれを自分の開始行で報告し、終了タグに罪を着せません。名前の中の異常な文字はトークン全体を飲み込むので、amount$="10" はドル記号についての 1 件になり、イコールと引用符についての 5 件にはなりません。ひとつ注意があります。早い段階の構造的な間違いは、それ以降のすべてを誤りに見せます。最初の 2、3 件を直して再検査してください。報告は 200 件で打ち切り、その旨を明記します。

実際に出てくる失敗

現実の構文エラーが奇抜であることはめったにありません。時間を食うのは、パーサーが症状を名指しする一方で、原因が別の場所にあるか、そもそも目に見えないからです。

  • 宣言の前にある UTF-8 のバイト順マーク。合法で、目に見えず、「Content is not allowed in prolog」の典型的な原因です。ここでは正体不明の内容ではなく、それ自身として報告します。
  • <?xml encoding="UTF-8" version="1.0"?> と書かれた宣言や、バイト列は UTF-8 なのに windows-1252 を名乗る宣言。後者は、バイト単位のパーサーが何を読むことになるかについての警告として出します。このページに届いた時点で、ファイルはすでにデコード済みだからです。
  • &nbsp;、&mdash; ほか 50 ほどの HTML 実体。汎用の「未定義の実体」ではなく HTML のものとして名指しし、代わりに使うべき文字参照も示します。
  • 閉じたルートタグのあとに付いた PHP の notice やスタックトレース。そのメッセージが生の HTTP ボディを見るよう促すのはこのためです。
  • データベースの列から出てきた制御文字。XML 1.0 は CDATA の中も含め、どこであれこれを禁じています。
  • 束縛されていない soap:、xsi:、xlink: プレフィックス。大きな文書から断片をコピーし、xmlns 宣言を元のルートに置き去りにした結果です。

構文検査が止まる場所

このページが答えるのは 1 つの問いだけです。構文が合法かどうか。<invoice> が <line> を含んでよいかとか、必須の要素が欠けていないかは、決して問いません。それにはスキーマが要るので、XSD バリデーターと DTD バリデーターの担当です。もしどこかのシステムがあなたの文書を「invalid」と呼んだなら、それはスキーマ検査を実行したのであり、ここで緑になってもその説明にはなりません。

明記しておくべき制限が 2 つ。内部 DTD サブセットは実体宣言のためだけに読むので、その中の要素宣言や属性リスト宣言は読み飛ばすだけで強制はしません。外部 DTD は報告はしても決して取得しません。サイズは 2,000 万文字、入れ子は 512 段、実体展開は 1,000 万が上限です。すべてこのタブで動いているからです。

コードで XML の構文を検査する

XML を最もよく扱う言語での同じ検査を、外部実体と実体展開に対して安全な書き方で示します。複数のエラーを報告するものがどれほど少ないかに注目してください。それはライブラリではなく仕様がそう言っているからです。

// DOMParser never throws. Given broken input it returns a document whose
// root is <parsererror>, which is why so much code silently accepts XML
// that no other parser would take.
function checkXml(source) {
  const doc = new DOMParser().parseFromString(source, 'application/xml');
  const error = doc.querySelector('parsererror');
  if (!error) return { wellFormed: true, message: null };

  // The text is browser-specific. Firefox includes a line and column,
  // Chrome's wording differs, and neither exposes them as fields.
  return { wellFormed: false, message: error.textContent.trim() };
}

// Browsers do not resolve external entities, so XXE is not reachable here.
// Internal entity expansion is, so cap the input length before parsing:
//   if (source.length > 20_000_000) throw new Error('too large');
# lxml is the only common Python parser that keeps going after a syntax
# error and hands back the whole list rather than the first entry.
from lxml import etree

parser = etree.XMLParser(
    recover=True,            # keep parsing so error_log fills up
    resolve_entities=False,  # do not expand entities
    no_network=True,         # never fetch an external DTD
    load_dtd=False,
    huge_tree=False,         # keep libxml2's depth and expansion limits
)
etree.fromstring(source.encode('utf-8'), parser)

for entry in parser.error_log:
    print(f"line {entry.line}, column {entry.column}: {entry.message}")
if not parser.error_log:
    print('well-formed')

# Without recover=True, fromstring raises etree.XMLSyntaxError and
# e.position gives a (line, column) tuple for the first failure only.
# For untrusted input with the standard library, use defusedxml.
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Drop the next line only if your documents legitimately carry a DOCTYPE.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

var reader = factory.newSAXParser().getXMLReader();
reader.setErrorHandler(new DefaultHandler() {
    @Override public void warning(SAXParseException e) { print("warning", e); }
    @Override public void error(SAXParseException e) { print("error", e); }
    @Override public void fatalError(SAXParseException e) throws SAXParseException {
        print("fatal", e);
        throw e;  // the specification requires processing to stop here
    }
    private void print(String kind, SAXParseException e) {
        System.err.printf("%s at line %d, column %d: %s%n",
            kind, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
    }
});
reader.parse(new InputSource(new java.io.StringReader(source)));
using System.Xml;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // no DTD, so no entity expansion
    XmlResolver = null,                      // never fetch anything
    MaxCharactersFromEntities = 1024 * 1024,
    MaxCharactersInDocument = 20L * 1024 * 1024,
    ConformanceLevel = ConformanceLevel.Document,
};

try
{
    using var reader = XmlReader.Create(new StringReader(source), settings);
    while (reader.Read()) { }   // pull every node; syntax errors surface here
    Console.WriteLine("well-formed");
}
catch (XmlException e)
{
    // LinePosition is the column, 1-based. Reading stops at this point.
    Console.Error.WriteLine(
        $"line {e.LineNumber}, column {e.LinePosition}: {e.Message}");
}
<?php
// libxml recovers internally, so libxml_get_errors() is the closest a PHP
// script gets to a full list rather than a first failure.
libxml_use_internal_errors(true);

$doc = new DOMDocument();
$doc->loadXML($source, LIBXML_NONET);   // NONET blocks external fetches

$errors = libxml_get_errors();
libxml_clear_errors();

foreach ($errors as $e) {
    $kind = $e->level === LIBXML_ERR_FATAL ? 'fatal' : 'error';
    fprintf(
        STDERR,
        "%s at line %d, column %d: %s\n",
        $kind,
        $e->line,
        $e->column,
        trim($e->message)
    );
}
exit($errors ? 1 : 0);
# xmllint ships with libxml2 and is already installed on most machines.
# --nonet stops it fetching a DTD the document points at.
xmllint --noout --nonet document.xml

# --recover keeps parsing after a failure, so you see more than the first.
xmllint --noout --nonet --recover document.xml

# Check a whole tree, one file at a time:
find . -name '*.xml' -print0 | xargs -0 -n1 xmllint --noout --nonet

# Exit status: 0 well-formed, 1 unclassified, 3 well-formedness error,
# 4 validation error against a DTD or schema.

どのサンプルも何かを「オフ」にしており、それこそが本題で、定型文ではありません。外部実体の解決は Java では既定で有効ですし、Python の標準ライブラリは展開の上限をまったく設けていません。これらのフラグこそが、構文検査と「自分のサーバーに向いたファイル読み出しの足がかり」とを分けるものです。

よくある質問

何もインストールせずに XML の構文を検査するには。

このページ上部のエディターに文書を貼り付けてください。入力しながら検査が走り、押すボタンも作るアカウントもありません。.xml ファイルをドロップすることもできます。ファイルはブラウザーの File API で読み込まれ、送信されることはありません。

ターミナルからなら、xmllint が libxml2 の一部として macOS とほとんどの Linux ディストリビューションに最初から入っています。xmllint --noout --nonet yourfile.xml は構文が合法なら何も出力しません。最初のエラーで止まる点が実用上の違いです。

ここで検査すると XML はアップロードされますか。

いいえ。スキャナーはこのタブの Web Worker で動く JavaScript です。文書を送る先のエンドポイントはありませんし、エディターにアクセスできる解析ツールもエラー報告スクリプトもありません。

これは約束ではなく確認できる事実です。開発者ツールを開いてネットワークタブに切り替え、文書を貼り付けて見てください。このページのアセットが一度読み込まれ、あとは何も続きません。これが重要なのは、人が構文チェッカーに貼り付ける文書が、ベアラートークン入りの SOAP エンベロープ、SAML アサーション、接続文字列の入った設定ファイルだからです。

構文チェッカーと XML バリデーターの違いは何ですか。

この 2 つは同じ意味で使われがちで、そこから混乱が始まります。構文検査が問うのは、その文書が XML 自体の規則に従っているかどうかです。タグが閉じている、入れ子が正しい、ルートが 1 つ、属性が引用符で囲まれている、アンパサンドがエスケープされている。文書以外には何も要りません。

仕様の意味での検証が問うのは、文書がスキーマ、つまり XSD や DTD に適合しているかどうかです。どの要素がどこに現れてよいかを記した 2 つめのファイルが要ります。構文がきれいになるまで実行できないので、ミドルウェアが返す「invalid」は、構文エラーのこともスキーマ違反のこともあります。このページは前者を実行し、そう明記します。ここの XSD・DTD バリデーターが後者を実行します。こちらもアップロードはありません。

エディターは 1 件しか出さないのに、ここが 5 件出すのはなぜですか。

XML 仕様が、適合プロセッサーは致命的エラーのあと停止するよう指示しており、あなたのエディターの裏にいるパーサーはそれに従っているからです。この規則には理があります。タグが閉じていない時点で、パーサーには文書の残りが何を意図していたのか本当に分からないのです。

このスキャナーは代わりに復帰します。タグの境界で再同期し、壊れた名前トークンは丸ごと飲み込み、それを露呈させた終了タグより、閉じられていない開始タグのほうを責めます。長い一覧は、たいてい先頭付近の 1 つの構造的ミスが大半を生んでいるので、早い段階のエラーを信頼できるものとして扱い、直したら再検査してください。

XML のタグ名は大文字小文字を区別しますか。

完全に区別します。<Note> と <note> は別の要素であり、</Note> は <note> を閉じません。これは HTML から来た人がつまずく点で、HTML ではパーサーがタグ名を小文字に畳んでしまいます。手書きの XML でタグ不一致エラーが出る最も一般的な原因でもあります。

属性名にも名前空間プレフィックスにも同じことが当てはまります。xmlns:Soap と xmlns:soap は別のプレフィックスです。不一致が大文字小文字の差だけの場合、メッセージは汎用の不一致ではなくその旨を伝えます。よく引き写される「XML の構文規則」のまとめのいくつかは、XML のタグは大文字小文字を区別しないと書いていますが、それは誤りで、従えば実在のパーサーがすべて拒否する文書ができあがります。

検査できる最大のファイルサイズは。

2,000 万文字、ASCII でおよそ 20 MB です。10 MB の文書は 1 秒強、1 MB は約 110 ミリ秒で走査され、Web Worker で動くのでエディターは反応を保ちます。

それより大きいものには、手元でストリーミングパーサーを使ってください。xmllint、Python の iterparse、あるいは任意の SAX パーサーが、木全体を作らずに整形式を検査できます。

関連ツール

関連する解説

これで解決できるエラー