XPath を使用して XML ドキュメントに対してクエリを実行する際に知っておくべきことと避ける

 

大胆なオバサンジョ
Microsoft Corporation

2002 年 6 月 30 日

マウスと男性のベストレイドプラン

この記事のインスピレーションは、週末から生まれましたが、私が実際に取り組む予定は何もありませんでした。私の重要な他の人は、同僚とラスベガスへの祝賀旅行をすることに決めました。これは、数ヶ月前にレドモンドに引っ越して以来、最終的に私の本を開梱できるように、イケアに行き、本棚を拾う計画と非常にうまく一致しました。 2時間イケアの周りをぶらぶらした後、私はいくつかの必要な部分が在庫切れであることを知るために、私のリビングルームの配色に合った本棚が展示されているのを見つけました。 私は本棚を注文し、手ぶらで家に帰りました。 残念ながら、私はすでに家で私の本を開梱し、彼らは私のリビングルームの上に散らばっていました。 これは私の急増するライブラリをカタログ化する絶好の機会となり、もちろんXMLを使用してこのタスクを達成することにしました。

目を満たす以上

私が構築していたXMLカタログの主な目的は、私が所有している本に関する情報を格納し、クエリやさまざまな種類のプレゼンテーションを可能にするのに十分な柔軟性があり、移植可能な場所を持つことでした。 以下は、このようなドキュメントでの私の最初のパスのスニペットです。

Books.xml

<?xml version="1.0" encoding="UTF-8" ?> 
<bk:books xmlns:bk="urn:xmlns:25hoursaday-com:my-bookshelf" on-loan="yes" >
 <bk:book publisher="IDG books" on-loan="Sanjay" >
  <bk:title>XML Bible</bk:title> 
  <bk:author>Elliotte Rusty Harold</bk:author>
 </bk:book>
 <bk:book publisher="QUE">
  <bk:title>XML By Example</bk:title> 
  <bk:author>Benoit Marchal</bk:author>
 </bk:book>
</bk:books>

属性を使用 on-loan して本を貸し出したかどうかを追跡できるようにしたいと考えました。 ルート要素の 属性は on-loan 、少なくとも 1 つの書籍が出ていたことを指定し、各 book 要素の同じ属性で書籍の貸し出し先を指定します。 振り返ると、これはルート要素とその子の間の不要な結合につながるので、これは最高の設計ではなかったかもしれませんが、私と一緒に耐えてください、これは私の最初のパスにすぎませんでした。

私のシンプルなフォーマットを設計して、私はフォーマットに満足しているかどうかを確認するために、それに対していくつかの練習クエリを実行することにしました。 System.Xmlで SelectSingleNode メソッドを 使用して試した最初のクエリ 。XmlNode クラスは次のとおりです。

   //*[position() = 1]/@on-loan 

私は「ドキュメント内のすべてのノードを選択してから、最初のノードのオンローン属性を与える」という意味を意図しました。クエリは次を返しました。

   on-loan="yes" 

だから、質問に対する答えは、「私は本を出していますか?」でした yes。 しかし、私の本の1つが貸し出されたときにルート要素のローン時の値を更新できなかった場合に何が起こるかをシミュレートすると、興味深いことが起こりました。ルート要素から属性を on-loan 削除し、クエリをもう一度実行しました。 結果は次のとおりです。

   on-loan="Sanjay"

この結果は、ルート要素のいずれかの子の値になります。 バグを疑って、私もMSXMLでこれを試して、同様の結果を得ました。 さらに調査を行った結果、私のチームに関する多くの XPath 専門家とのディスカッションと 、XPath の推奨事項の詳細な読み取りが行われました。 私が発見したのは、複数の当事者によって設計された簡単ではない言語と同様に、XPathを扱うときに避けるためにいくつかの奇妙な点、特異点、不整合、そして単なる落とし穴があるということです。

略語とその意味

XPath レコメンデーションには、現在選択されているノード (コンテキスト ノードとも呼ばれます) に関連するノードを含む の数が一覧表示されます。 詳細度を下げるために、一般的に使用される特定の軸の省略形を多数指定しました。 次の表は、これらの省略形と同等の軸を示しています。

省略形
. self::node()
.. parent::node()
// /descendent-or-self::node()/
@ 属性::

また、すべての 位置ステップ またはパス式で使用される既定の軸が軸であるという child:: 事実もあります。 したがって、 /bk:books/bk:book は実際には と/child::bk:book/child::bk:book 同等ですが、入力がはるかに簡単です。

*ノード テストは、現在の軸のプリンシパル ノード タイプのすべてのノードを選択するために使用されます。 * はノード テストであり、ステップの省略形ではありません。 最後に、数値を含む述語は、コンテキスト ノードの位置がその数値と同じかどうかを確認するのと同じです。 これは、クエリ /bk:book[1] が と同等であることを意味します /bk:book[position()=1].

上記の情報を考えると、元の問題のクエリに戻って、予期しない結果が発生した理由を確認できます。 //*[position() = 1]/@on-loanは、ドキュメント内のすべてのノードを選択し、選択した各ノードの最初の子の属性を取得on-loanする の省略形/descendent-or-self::node()/child::*[position() = 1]/@on-loanです。 かっこを慎重に使用すると、問題が迅速に修正され、 (//*)[position() = 1]/@on-loanの略(/descendent-or-self::node()/child::*)[position() = 1]/@on-loan である が実際に私が望んでいたものです。

問題を考え出した直後に、私は私が必要としたことを行うためのより簡単で効率的なクエリがそうであることを認識しました:

   /*/@on-loan

これは、ドキュメント内の最初のノードのみを確認する必要があるため、より優れたソリューションです。 結果の混乱を避けるために、特定のケースで省略形が何を表しているのかを考える必要がある理由を強調するもう 1 つの例を残します

省略形 フル クエリ クエリ結果
*[1] /descendent-or-self::node()/child::*[position()=1] ドキュメント内のすべてのノードの最初の子を選択します。
(//*)[1] (/descendent-or-self::node()/child::*)[position()=1] ドキュメント内の最初のノードを選択します。

数学スキルの向上

通常、リレーショナル演算子または算術演算子と文字列を含むクエリでは、直感的に反する結果が得られます。 XPath は、関係演算子または算術演算子を含む式内のすべてのオペランドを数値に変換します。 完全に数値ではない文字列は、(数値ではなく) NaN に変換されます。 次の表は、いくつかの XPath 式、暗黙的に変換される内容、および式の結果を示しています。

Expression 暗黙的な変換 結果
'5' + 7 5 + 7 12
'5' + '7' 5 + 7 12
5 + 'a' 5 + NaN NaN
'5' < 7 5 < 7 True
'5' < '7' 5 < 7 True
'5' < 'b' 5 < NaN False
'a' < 'b' NaN < NaN False
'a' > 'b' NaN > NaN False

比較演算子 (<、、=、><>=) は文字列値の辞書式比較を実行しないことに注意してください。

もう 1 つの興味深い算術的な点は、単項マイナスが定義されている (たとえば、-6 は有効な XPath 式)、単項プラスは (+6 は有効な XPath 式ではない) という点です。 さらに驚くべきことは、複数の否定を積み重ねることができ、まだ有効であることです。 したがって、------6 は、値 6 に相当する有効な XPath 式です。

XPath が 科学的/指数表記 をサポートしていないと、SQL などの一般的なクエリ言語と C++ などの一般的なプログラミング言語の両方でサポートされているため、人々がトリップする傾向があります。

ノード セットに対する算術演算とリレーショナル演算を組み合わせた式も、驚くべき結果を招く可能性があります。 ノード セットに対する算術演算では、 セット内の最初のノード の値が数値に変換され、関係演算子は ノード セット内のノードが条件を 満たしているかどうかを評価します。 次に示す XML ドキュメントは、算術演算と関係演算子によって 連想されない式につながる方法を示すために使用されます。

Numbers.xml

<Root>
 <Numbers>
  <Integer value="4" />
  <Integer value="2" />
  <Integer value="3" />
 </Numbers>
 <Numbers>
  <Integer value="2" />
  <Integer value="3" />
  <Integer value="6" />
 </Numbers>
</Root>

次の表は、算術演算の結合性の欠如を示しています。

Expression 結果 説明
Root/Numbers[Integer/@value > 4 - 1] <Numbers>

<Integer value="4" />

<Integer value="2" />

<Integer value="3" />

</Numbers>

<Numbers>

<Integer value="2" />

<Integer value="3" />

<Integer value="6" />

</Numbers>

値が <4 から 1 を超える value 属性を持つ Integer> 要素が少なくとも 1 つある<ドキュメント内のすべての Numbers> 要素を選択します。
Root/Numbers[ 1 + Integer/@value > 4] <Numbers>

<Integer value="4" />

<Integer value="2" />

<Integer value="3" />

</Numbers>

ドキュメント内のすべての Numbers> 要素を<選択します。ここで、値が 4 より大きい value 属性を持つ 1 と最初<の Integer> 要素を追加します。

XPath が代数的に結合していた場合、両方のクエリで同じ結果が返されます。

セットがセットではないのはいつですか?

ノード セットは、数学 (またはお気に入りのプログラミング言語) のセットと同じように順序付けられていないコレクションですが、多くの場合、数学的な意味ではセットとは異なる方法で扱われます。 XPath の一部の操作では、ノード セットを処理するときに 最初のセマンティクス が使用され、他の操作では 任意のセマンティクスが使用されます。 最初のセマンティクスは、その操作に対するノード セットの値がセット内の最初のノードから取得されることを意味し、セマンティクスは、ノード セットに対する操作が、セット内の任意のノードが条件を満たすかどうかに依存することを意味します。 「 数学スキルの向上 」セクションでは、任意のセマンティクスと最初のセマンティクスが使用される状況について説明します。

数学セットとは異なる XPath ノード セットのもう 1 つの特性は、XPath がサブセット、交差、対称差などのセット操作を実行するためのメカニズムを直接提供しないということです。 XSLT Programmer's Reference 2nd Edition の著者である Michael Kay は、もともと count() 関数と union 演算子の組み合わせを使用して、不足している set 演算子|を模倣する方法を発見しました。 前のセクションの XML ドキュメントに対してセット操作とその出力を実行する XSLT スタイル シートを次に示します。

Stylesheet

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" >
 
 <xsl:output method="text" />

 <xsl:variable name="a" select="/Root/Numbers[1]/Integer/@value"/> 
 <xsl:variable name="b" select="/Root/Numbers[1]/Integer/@value[. > 2]"/> 
 <xsl:variable name="c" select="/Root/Numbers[1]/Integer/@value[. = 3]"/> 

 <xsl:template match="/">
 
 SET A: { <xsl:for-each select="$a"> <xsl:value-of select="." />, </xsl:for-each> }
 SET B: { <xsl:for-each select="$b"> <xsl:value-of select="." />, </xsl:for-each> }
 SET C: { <xsl:for-each select="$c"> <xsl:value-of select="." />, </xsl:for-each> }

  a UNION b:  { <xsl:for-each select="$a | $b"> <xsl:value-of select="." 
/>, </xsl:for-each> }
  b UNION c:  { <xsl:for-each select="$b | $c"> <xsl:value-of select="." 
/>, </xsl:for-each> }
  a INTERSECTION b:  { <xsl:for-each select="$a[count(.|$b) = count($b)]"> 
<xsl:value-of select="." />, </xsl:for-each> }
  a INTERSECTION c:  { <xsl:for-each select="$a[count(.|$c) = count($c)]"> 
<xsl:value-of select="." />, </xsl:for-each> }
  a DIFFERENCE b:  { <xsl:for-each select="$a[count(.|$b) != count($b)] | 
$b[count(.|$a) != count($a)]"> <xsl:value-of select="." />, </xsl:for-each> }
  a DIFFERENCE c:  { <xsl:for-each select="$a[count(.|$c) != count($c)] | 
$c[count(.|$a) != count($a)]"> <xsl:value-of select="." />, </xsl:for-each> }
  a SUBSET OF b:  { <xsl:value-of select="count($b | $a) = count($b)"/> }
  b SUBSET OF a:  { <xsl:value-of select="count($b | $a) = count($a)"/> }
 
 </xsl:template>

</xsl:stylesheet>

OUTPUT

  SET A: { 4, 2, 3,  }
  SET B: { 4, 3,  }
  SET C: { 3,  }

  a UNION b:  { 4, 2, 3,  }
  b UNION c:  { 4, 3,  }
  a INTERSECTION b:  { 4, 3,  }
  a INTERSECTION c:  { 3,  }
  a DIFFERENCE b:  { 2,  }
  a DIFFERENCE c:  { 4, 2,  }
  a SUBSET OF b:  { false }
  b SUBSET OF a:  { true }

ノード セットと数学セットの違いの最後の点は、ノード セットが通常順序付けされる点です。 W3C XPath の推奨事項では、それらを順序なしと記述していますが、XSLT ではノード セットの順序が指定されています。

アイデンティティの危機

XPath では、ノードの ID または異なるノード セット内のノードの等価性を直接判断するためのコンストラクトはありません。 によって返されるノードが によって /bk:books/bk:books/bk:book[1]/parent::* されるノードと同じかどうかなどの比較は、直接サポートされていません。 ノード セットで 演算子を使用した = 比較では、ノード セット全体は比較されませんが、代わりに セマンティクスが 使用されます。 W3C XPath の推奨事項から:

"比較対象の両方のオブジェクトがノード セットの場合、2 つのノードの 文字列値 に対して比較を実行した結果が true になるように、最初のノード セットにノードがあり、2 番目のノード セットにノードがある場合にのみ、比較は true になります。"

このポイントをホームにするために、概要の XML カタログ形式からノード セットに対して比較操作を実行した結果を示す表を次に示します。 これらは最初の表示で矛盾した結果のように見えるので注意してください。

Expression 結果 説明
//bk:book = /bk:books/bk:book[1] TRUE 内の少なくとも 1 つのノードが、 内 //bk:book の別のノードと同じ文字列値を持ちます /bk:books/bk:book[1]?
//bk:book != /bk:books/bk:book[1] TRUE 内の少なくとも 1 つのノードが、 内 //bk:book の別のノードとは異なる文字列値を持ちますか? /bk:books/bk:book[1]?
not(//bk:book = /bk:books/bk:book[1]) false "内の少なくとも 1 つのノードが の別/bk:books/bk:book[1]?のノード//bk:book と同じ文字列値を持ちます" という質問に対する答えとは逆です。

XPath count() 関数を使用してノード ID を模倣し、同じ長さの 2 つのノード セットの交差部分がノード セットのいずれかと同じ長さであるか、シングルトン ノード セットの場合は 1 に等しいかどうかを判断できます。 たとえば、次のクエリでは、両方のノードが同じであるため、この場合は TRUE が返されます。

    count(/bk:books | /bk:books/bk:book[1]/parent::*) = 1
  

ノード ID は、XSLT の generate-id() 関数を使用して模倣することもできます。 XSLT FAQ には、 generate-id() の使用例があります

私は存在するので、私は存在する

ノードの存在をテストするための明示的なメカニズムはありませんが、ノード セットを含む多くの式で暗黙的に発生します。 存在しないノード セットは、空のノード セットとして表されます。 空のノード セットは、文字列操作と数値演算が関係する状況で、それぞれ空の文字列または NaN に暗黙的に変換されます。 この一連の暗黙的な変換により、空のノード セットが原因で発生したクエリと、実行されなかったものを特定するために、インスタンス ドキュメントを見ずにクエリを実行した場合、混乱する結果が生じる可能性があります。 空のノード セットを含むクエリの例と、これらの暗黙的な変換がそれらに与える影響を次に示します。

Expression 結果
/NonExistentNode + 5 NaN
/NonExistentNode = 5 False
/NonExistentNode != 5 False
concat(/NonExistentNode, "hello") "hello"
/Root[@nonExistentAttribute] 結果が返されない
/Root[@nonExistentAttribute < 5] 結果が返されない
/Root[@nonExistentAttribute > 5] 結果が返されない

ノードに空の文字列が含まれる可能性があるため、通常は、ノードの文字列値をチェックするのではなく、 boolean() 関数を使用してノードの存在をテストすることをお勧めします。 たとえば、FALSE を返す次のクエリは、ドキュメントに がないことを NonExistentNode 確認するための最適な方法です。

   boolean(/NonExistentNode) 

名前空間と XPath Redux

XPath で名前空間を処理するときの主な落とし穴は 、最後の列 で説明されており、ドキュメントで既定の名前空間が使用されている場合でも、式のプレフィックスと名前空間名の間にマッピングを作成する必要があります。

興味深いのは、ドキュメントに使用できる名前空間ノードが常に 1 つ以上あることです。 http://www.w3.org/1998/namespace これは XML 名前空間です。 たとえば、次のクエリを見てみましょう。

/bk:books/namespace::*

このクエリの結果は次のようになります。

urn:xmlns:25hoursaday-com:my-bookshelf
http://www.w3.org/XML/1998/namespace

返される項目は、books.xml ドキュメントのルートで使用できる名前空間ノードです。

アンタッチャブル

XML ドキュメントには、XPath に対して透過的または場合によっては見えない特定の情報があります。 XML ドキュメントの先頭にある XML 宣言 は、XPath では見えない XML コンストラクトの例です。 つまり、XPath を使用して XML ドキュメントの バージョンエンコードまたはスタンドアロンの状態 を照会する方法はありません。

CDATA セクションや解析されたエンティティなど、XML ドキュメントの解析プロセス中に置き換えられるテキストを導入するための構文コンストラクトは、XPath と同様に透過的です。 XPath は、置換テキストを通常のテキスト ノードとして扱います。

謝辞

上記の XPath の一覧には、Julia Jia、Karthik Ravindran、Martin Gudgin、Michael Brundage、Michael Rys など、多数の共同作成者が含まれています。 この記事の一部の側面は、フィリップ・ワドラとマイケル・ケイによって書かれた電子メールに触発されました。

Dare Obasanjo は Microsoft の WebData チームのメンバーであり、特に、.NET Framework、Microsoft XML Core Services (MSXML)、Microsoft Data Access Components (MDAC) の System.Xml 名前空間と System.Data 名前空間内でコンポーネントを開発しています。

この記事に関する質問やコメントは、GotDotNet の Extreme XML メッセージ ボード に自由に投稿できます。