この記事を書いた理由は、私たちが Geonode であり、データ収集を行う人々にプロキシを販売しているからです。そして、そのデータの多くは、サイトマップ、RSS や Atom フィード、SOAP レスポンス、製品カタログなど、XML 形式で届きます。 率直に言えば、構文解析の失敗がネットワークの問題であることはほぼありません。 XMLパーサーでエラーが発生した場合は、コードを変更する前に、受信したデータの最初の200文字を出力してみてください。10回中9回はHTMLのエラーページであり、パーサーはXMLが送信されなかったことを正確に報告しているのです。
ブラウザ内:DOMParser
組み込み機能であり、依存関係はありません。
const parser = new DOMParser();
const doc = parser.parseFromString(xmlString, "application/xml");
const titles = doc.querySelectorAll("item > title");
titles.forEach(t => console.log(t.textContent));
MDNのドキュメントによると、許容されるMIMEタイプはtext/html
、text/xml
、application/xml
、application/xhtml+xml
、およびimage/svg+xml
です。この関数は、Document
を返します。このには、「指定されたmimeType
と一致するcontentType
プロパティ」が含まれており、要求内容に応じてHTMLDocument
またはXMLDocument
のいずれかになります。
XMLの場合はapplication/xml
を使用してください。text/html
を指定するとHTMLパーサーが呼び出されますが、このパーサーは寛容すぎて、ドキュメントを黙って変更してしまうことがあります。つまり、XMLの大文字小文字の区別を無視し、XMLで禁止されている要素も平気で受け入れてしまいます。
解析が完了すると、DOMが得られます。DOMのトラバーサルに関する既知の知識はすべて適用されます:querySelector
、querySelectorAll
、getElementsByTagName
、children
、textContent
。
エラーの落とし穴:例外がスローされない
初めて遭遇した誰もが引っかかってしまう挙動。
DOMParser に不正なXMLを送信しても、例外は発生しません。MDNには「返されるXMLDocumentには、解析エラーを記述した<parsererror>ノードが含まれる」と明記されており、エラーは「ブラウザのJavaScriptコンソールにも報告される場合がある」とされています。
したがって、以下のチェックを行う必要があります:
const doc = parser.parseFromString(xmlString, "application/xml");
const errorNode = doc.querySelector("parsererror");
if (errorNode) {
throw new Error(`XML parse failed: ${errorNode.textContent}`);
}
このチェックを行わないと、不正な形式のドキュメントによって、本来データがあるべき場所にエラーメッセージを含む Document オブジェクトが生成されます。その後の querySelectorAll は何も返さず、その症状は構文解析の失敗ではなく、セレクタの問題のように見えてしまいます。
次のように1回ラップしてください:
function parseXml(text) {
const doc = new DOMParser().parseFromString(text, "application/xml");
const err = doc.querySelector("parsererror");
if (err) {
throw new Error(
`XML parse failed: ${err.textContent.trim()}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
return doc;
}
text.slice(0, 200)の部分が時間を節約してくれます。パースエラーは入力が無効であることを示し、最初の200文字はそれがHTMLエラーページであることを示しています。
Node.js:ライブラリの選択
Node.jsには組み込みのXMLパーサーがないため、これは依存関係の決定事項となります。週間ダウンロード数は、その普及状況のおおよその目安となります。以下はnpmレジストリからのデータで、2026年9月時点のものです。
| ライブラリ | 週間ダウンロード数 | スタイル |
|---|---|---|
sax |
| 約88.9M | ストリーミング、イベント駆動型 |
| fast-xml-parser
| 約85.0M | XMLからプレーンなJavaScriptオブジェクトへ |
| @xmldom/xmldom
| 約48.7M | Node用のDOM実装 |
| xml2js
| 約44.5M | XMLからオブジェクトへの変換、コールバックおよびPromise API |
| xpath
| 約12.2M | XPathクエリ、xmldomと組み合わせて使用 |
**fast-xml-parser
** は、ほとんどの作業において実用的なデフォルトの選択肢です。これはXMLを通常のJavaScriptオブジェクトに変換するため、DOMメソッドではなくドット表記で操作できます:
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
const obj = parser.parse(xmlString);
console.log(obj.rss.channel.item[0].title);
理解しておくべきトレードオフ:オブジェクトへの変換には、ある特定の点で情報損失が生じます。1回だけ出現する要素はオブジェクトになりますが、同じ要素が2回出現すると配列になります。したがって、channel.item
は、アイテムが3つあるフィードでは配列になりますが、アイテムが1つのフィードではオブジェクトになります。そして、配列を前提としたコードは、アイテムが1つの場合に動作しなくなります。 ほとんどのライブラリには、名前付き要素に対して常に配列を生成するオプションが用意されています。反復処理を行う要素については、問題が発生する前にこのオプションを有効にしておくことをお勧めします。
**@xmldom/xmldom
** は、Node.js で実際の DOM を提供します。これは、同じコードを両方の環境で動作させたい場合や、XPath が必要な場合に重要です。これを xpath
パッケージと組み合わせて使用してください:
import { DOMParser } from "@xmldom/xmldom";
import xpath from "xpath";
const doc = new DOMParser().parseFromString(xmlString, "text/xml");
const titles = xpath.select("//item/title/text()", doc);
**sax
** は、読み込み中にイベントを発行するストリーミングパーサーです。メモリに収まりきらないほど巨大なドキュメント(数ギガバイト規模のサイトマップインデックスや、大量のカタログエクスポートなど)に対して、DOM アプローチでは対応できない場合に最適なソリューションです。
**xml2js
** は長年にわたり確立され、広く利用されています。そのAPIは少々時代遅れですが、十分に実用可能であり、これを使用した既存のコードも数多く存在します。
名前空間:実際のドキュメントを台無しにする要因
正常に動作していたセレクタが突然何も見つからなくなる、最も一般的な理由です。
多くの実際のXMLフォーマットでは、名前空間が宣言されています:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc></url>
</urlset>
このxmlns
により、すべての要素がデフォルトの名前空間に配置されます。名前空間を意識したパーサーでは、<loc>
は単にloc
とはならず、sitemaps名前空間内のloc
となり、単なるgetElementsByTagName("loc")
では何も見つからない可能性があります。
これに対処する3つの方法(正しさの順に並べたもの)があります。
名前空間を意識したメソッドを使用する:
const NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
const locs = doc.getElementsByTagNameNS(NS, "loc");
名前空間を問わない場合は、ワイルドカード名前空間を使用する:
const locs = doc.getElementsByTagNameNS("*", "loc");
ライブラリを設定して名前空間を無視する。 ほとんどのオブジェクトマッピングライブラリには、名前空間のプレフィックスを削除するオプションがあり、これにより単純な loc
形式のキーが生成されます。これは便利ですが、たまたまローカル名が同じである2つの本質的に異なる要素を、警告なしにマージしてしまいます。サイトマップでは許容されますが、語彙が混在するドキュメントでは危険です。
なお、querySelector
とgetElementsByTagNameNS
では、この点において挙動が異なります。CSSセレクタには独自の名前空間構文がありますが、これは扱いにくく、ほとんど使用されません。そのため、名前空間を持つXMLの場合は、NS
メソッドやXPathの方が信頼性が高いです。
JavaScript における XPath
ブラウザでは document.evaluate を通じて、Node では xpath パッケージ(xmldom)を通じて利用可能です。
const result = doc.evaluate(
"//item/title/text()",
doc,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < result.snapshotLength; i++) {
console.log(result.snapshotItem(i).nodeValue);
}
この API は記述が煩雑なため、多くの人は一度ラップしてしまえば、その後は忘れてしまうほどです。それでも手間をかける価値があるのは、XPath なら CSS セレクタでは表現できないこと――テキストコンテンツによるマッチング、祖先要素への移動、兄弟要素を基準とした位置論理――を表現できるからです。
2つの制約があります。ブラウザはXPath 1.0を実装しています。したがって、matches()、lower-case()、ends-with()は使用できません。また、名前空間にはリゾルバ関数(第3引数)が必要であり、これはプレフィックスを名前空間URIにマッピングします。nullを渡す方法は、名前空間のないドキュメントでのみ機能するため、実際のフィードのほとんどは対象外となります。
ブラウザで名前空間を持つドキュメントの場合:
const resolver = prefix => ({ sm: "http://www.sitemaps.org/schemas/sitemap/0.9" }[prefix] || null);
const result = doc.evaluate("//sm:loc/text()", doc, resolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
XPath 1.0 にはデフォルトの名前空間という概念がないため、ドキュメントが何を使用しているかに関係なく、プレフィックス(ここでは sm)を自分で指定する必要があることに注意してください。
XMLからJSONへの変換、そして失われるもの
実際に多くの人が求めているのはこれですが、この変換がロスレスではないことを理解しておく価値があります。
XMLとJSONはデータモデルが異なります。XMLには属性、要素、テキストノード、コメント、処理命令、名前空間、および順序があります。JSONにはオブジェクト、配列、文字列、数値、ブール値、およびnullがあります。この変換において、4つの要素が完全には保持されません。
属性と子要素の違い。 <item id="1"><name>x</name></item> には属性と子要素がありますが、JSONでは両者の区別がありません。ライブラリは、属性キーにプレフィックスを付けることでこれを処理します(一般的に @ または $)。これは設定が必要で、設定後は覚えておく必要があります:
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
// { item: { "@id": "1", name: "x" } }
繰り返される要素は一貫性なく配列になる。 上記で触れたが、この分野で最も一般的なバグであるため繰り返す価値がある:1回出現するとオブジェクトになり、2回出現すると配列になる。反復処理を行う予定のすべての要素について、ライブラリの「always array」オプションを設定すること。
混合コンテンツには明確な表現形式がありません。 <p>Hello <b>world</b>!</p> では、テキストと要素が混在しています。オブジェクトに変換されると、テキストの断片や子要素に対する相対的な位置を表現することが困難になり、ほとんどのライブラリではテキストを連結するか、一部を省略してしまいます。 XMLに文章のマークアップが含まれている場合、オブジェクトへの変換は誤ったアプローチです。DOMのままで保持してください。
順序は保証されません。 JSONオブジェクトのキーにはデータモデル上で定義された順序がないため、要素の順序に意味があるドキュメントでは、その情報が失われます。配列は順序を保持しますが、名前が異なる兄弟要素は順序を保持しません。
また、特に指定しない限り、すべてが文字列になります。 XMLには型がないため、<price>42.50</price>はテキストとして扱われます。ほとんどのライブラリは数値への型変換機能を提供しており、これは便利ですが、先頭にゼロが付いた製品コードを数値に変換したり、バージョン文字列を浮動小数点数に変換したりしてしまいます。数量ではなく識別子であるものについては、型変換を無効にしてください。
実用的な指針:オブジェクト変換は、要素がレコードやフィールドである「データ型」のXML(フィード、カタログ、設定、APIレスポンスなど)に適しています。マークアップが文章に埋め込まれており、構造自体が意味を持つ「ドキュメント型」のXMLについては、DOMを使用してください。
セキュリティ:XXEとインジェクション
2つの異なるリスクですが、いずれも現実的な脅威です。
XML外部エンティティ処理。 XMLでは、ローカルファイルやネットワークURLなどの外部リソースを参照するエンティティを宣言することができます。 これらを解決するパーサーを悪用すれば、サーバーからファイルを読み取らせたり、攻撃者に代わってリクエストを行わせたりすることが可能です。これは古典的であり、現在もなお一般的な脆弱性の種類です。
対策としては、使用するパーサーを問わず、外部エンティティおよび DTD の処理を無効にすることです。 ブラウザのDOMParserは外部エンティティを解決しないため、ブラウザの場合はデフォルトで安全です。Nodeライブラリによって挙動が異なります。そのため、安易に想定するのではなく、確認することが重要です。Nodeで信頼できないソースからのXMLを解析する場合は、リリース前にパーサーのエンティティ処理を確認してください。
DOMへの再挿入時のインジェクション。 MDNは、parseFromStringが「『インジェクションシンク』であり、入力が攻撃者からのものである場合、XSS攻撃の潜在的なベクトルとなる」と警告しています。 ここには重要なニュアンスがあります。text/html を使用する場合、「<script>要素は実行不可としてマークされ、イベントハンドラは呼び出されません」が、解析されたドキュメントが「後に可視DOMに挿入されると、スクリプトは実行されてしまいます」。
つまり、解析自体は安全ですが、その結果を挿入することは安全ではありません。 MDNの推奨事項は、文字列ではなくTrustedHTMLオブジェクトを渡すこと、CSPのrequire-trusted-types-forディレクティブを通じて信頼できる型を強制すること、そしてTrustedTypePolicyを通じてDOMPurifyなどのライブラリでサニタイズを行うことです。
簡単なルールは、サニタイズを行わずに解析済みの信頼できないマークアップをライブDOMに挿入してはならないこと、そしてテキストのみが必要な場合はinnerHTMLではなくtextContentを優先することです。
実用的なパターン
RSS または Atom フィードの解析:
const doc = parseXml(await res.text());
const items = [...doc.getElementsByTagNameNS("*", "item")].map(item => ({
title: item.getElementsByTagNameNS("*", "title")[0]?.textContent?.trim(),
link: item.getElementsByTagNameNS("*", "link")[0]?.textContent?.trim(),
date: item.getElementsByTagNameNS("*", "pubDate")[0]?.textContent?.trim(),
}));
ワイルドカード名空間を使用することで、分岐処理を行わずに RSS と Atom の両方を処理でき、オプションのチェーン処理により、フィールドが欠落しているフィード(ほとんどのフィードがこれに該当します)にも対応できます。
インデックスファイルを含むサイトマップの解析:
const doc = parseXml(xml);
const isIndex = doc.documentElement.localName === "sitemapindex";
const locs = [...doc.getElementsByTagNameNS("*", "loc")].map(n => n.textContent.trim());
// if isIndex, these are sitemap URLs to fetch; otherwise they are page URLs
両者とも<loc>
要素を含み、違いはラッパーのみであるため、document要素のlocalName
属性を確認するのが、両者を区別する確実な方法です。
大規模なドキュメントの処理 — DOMを構築するのではなく、ストリーミングパーサーを使用します:
import sax from "sax";
const stream = sax.createStream(true, { trim: true });
let current = null;
stream.on("opentag", node => { if (node.name === "loc") current = ""; });
stream.on("text", t => { if (current !== null) current += t; });
stream.on("closetag", name => { if (name === "loc") { emit(current); current = null; } });
ドキュメントのサイズに関係なくメモリ使用量は一定ですが、その代わりに小さなステートマシンを記述する必要があります。
よくある質問
JavaScriptでXMLを解析するにはどうすればよいですか?
ブラウザでは、組み込みのDOMParserを使用します。new DOMParser().parseFromString(xml, "application/xml") を実行すると、DOMメソッドで操作可能なDocumentが返されます。Node.jsには組み込みのパーサーがないため、別途インストールする必要があります。オブジェクト変換用にはfast-xml-parser、本格的なDOM用には@xmldom/xmldom があります。
なぜDOMParserは不正なXMLに対して例外をスローしないのですか?
設計上の仕様です。例外の代わりに、返されるドキュメントには、エラー内容を記述した<parsererror>ノードが含まれます。doc.querySelector("parsererror")を使用して明示的にチェックする必要があります。そうしないと、不正な形式のドキュメントに対して、クエリ結果が空になるだけで、エラーは黙ってスルーされてしまいます。
Node.js には組み込みの XML パーサーはありますか?
いいえ。JSON とは異なり、XML には依存関係が必要です。広く使用されている選択肢としては、オブジェクトへの変換には fast-xml-parser や xml2js、DOM 実装には @xmldom/xmldom、非常に大きなドキュメントのストリーミングには sax などがあります。
XML セレクタが何も見つからないのはなぜですか?
通常は名前空間が原因です。xmlns と宣言されたドキュメントでは、すべての要素がその名前空間に属するため、単純な getElementsByTagName では一致しない場合があります。名前空間 URI を含む getElementsByTagNameNS を使用するか、ワイルドカードとして "*" を使用するか、ライブラリを設定して名前空間を無視するようにしてください。
JavaScript で XML に対して XPath を使用するにはどうすればよいですか?
ブラウザでは、document.evaluate に XPathResult タイプを指定します。これは、一度ラップするだけで十分なほど詳細な出力を得られます。Node.js では、xpath パッケージと @xmldom/xmldom を組み合わせて使用します。なお、ブラウザは XPath 1.0 のみを実装しており、名前空間付きのドキュメントには、プレフィックスを URI にマッピングするリゾルバ関数が必要となります。
JavaScript での XML 解析はセキュリティリスクになりますか?
2つのリスクがあります。 XML外部エンティティの処理により、パーサーがローカルファイルを読み込んだり、リクエストを発行したりする可能性があります。ブラウザのDOMParserは外部エンティティを解決しませんが、Nodeライブラリによって挙動が異なるため、確認が必要です。また、解析済みの信頼できないマークアップをライブDOMに挿入するとスクリプトが実行される可能性があるため、挿入前にサニタイズを行ってください。
非常に大きなXMLファイルを解析するにはどうすればよいですか?
sax のようなストリーミングパーサーを使用してください。これは、メモリ内にドキュメントを構築するのではなく、読み込みながらイベントを発行します。これにより、現在の位置を追跡するための小さなステートマシンを記述する必要はありますが、一定のメモリ使用量で任意のサイズのファイルを処理できます。
解析したXMLが、場合によっては配列になり、場合によってはオブジェクトになるのはなぜですか?
これは、オブジェクトマッピングライブラリが、要素が繰り返される場合にのみ配列を生成するためです。3つの項目を含むフィードでは配列が返されますが、同じフィードでも項目が1つの場合はオブジェクトが返されます。ほとんどのライブラリには、名前付き要素に対して常に配列を生成するオプションがあります。反復処理を行う場合は、このオプションを有効にしてください。
まとめ
こうしたことのほとんどは環境によって決まります。ブラウザでは DOMParser が利用でき、依存関係はありません。一方、Node.js ではライブラリを選択することになり、その選択が、その後のコードの書き方を左右することになります。
人々が時間を浪費する原因のほとんどは、次の2つの挙動によるものです。DOMParserは、例外ではなくparsererrorノードでエラーを報告するため、形式不備のドキュメントでは結果が空になり、セレクタの問題のように見えてしまいます。そのノードを確認し、入力の最初の200文字をログに記録してください。通常、それはHTMLのエラーページだからです。 また、名前空間は、まさに解析を最も必要とするドキュメント(サイトマップ、フィード、SOAPレスポンスなど)において、単純なタグ名の検索を黙って妨げてしまいます。これらすべてが名前空間を宣言しているからです。
さらに、ドキュメントのサイズに合わせてツールを選択してください。 通常のドキュメントにはオブジェクトマッピングを、XPathやブラウザとサーバー間のコード共有が必要な場合には実際のDOMを、ファイルが大きすぎて一括処理できない場合にはストリーミングパーサーを使用してください。また、ソースが信頼できない場合は、リリース前にNodeパーサーの外部エンティティ処理を確認してください。これは20年にわたり脆弱性の原因となっており、現在もなお問題となっています。
