MSXML を操作する為の覚書。
サイト | 私的メモ |
XML Path Language (XPath) | W3C による XPath 言語解説文書を日本語訳したサイト。元の文書もそうだが読みにくい。だが XPath を利用する上で必ず目を通すべき資料。 |
XML -TECHSCORE- | XML に関する基本的な情報を大体網羅している。概要を斜め読みするのに適している。 |
@IT -XML & SOA | 実務での XML 使用に焦点を合わせている。入門ページもおススメ。 |
MSDN Online - XML Developer Center | MSXML を利用する上で外せない公式情報サイト。ここのコラム( このページの下段にリンク集あり )は読み物としても面白い。 |
たのしい XML | XML 自体の解説も豊富だが、どっちかというと XHTML 寄り。サイトの雰囲気( と絵 )で好みが分かれるかもしれない。 |
XML 1.0 | W3C による Extensible Markup Language 1.0 をを日本語に翻訳したサイト。おまけとして、W3C の文書を日本語訳したサイトのリンク集を。→W3C - W3Cの仕様書等の文書の日本語訳集 |
MSXML を使う為の基本的な準備の覚書。
MSXML を使う為には、msxml.h のインクルードが必要。XML パーサ自体は、IE や Office 等の Microsoft 製品のインストールによって更新されてゆくが、これらのバージョンに依存したくないのならば、msxml.h の使用に留め、#import による DLL のインポートは止めておいた方がいい。
ただし、DLL を再配布可能であったり、MSM 形式の MSXML パーサをアプリケーションインストーラに組み込む保障があるのなら、その限りではない。
尚、import する場合は、Visual C++ .NET で #import を XML で使用するとコンパイル エラーが発生する の問題があるので注意( このページに解決方法も含めた詳細解説有り )。
XML を操作する為に必要な、基本的オブジェクト/インターフェースは大体以下となる。
名前 | 解説 |
IXMLDOMDocument | XML を DOM( Document Object Model )として操作する為のオブジェクト。XML の構造自体をそのまま格納して編集対象とする。 |
IXMLDOMElement | XML 中の一つのエレメントを表すオブジェクト。 |
IXMLDOMNode | XML 中の一つのノードを表すオブジェクト。 |
IXMLDOMNodeList | リストとなったノードを表すオブジェクト。ノードの集合体ともいえる。 |
*Ptr | IXMLDOM... の後に Ptr と付くものは、それらのスマートポインタ。 |
XML を操作する場合は、
が基本。検索、読み書きはルートとした IXMLDOMElement のメソッドから行える。 ドキュメントのそれ自体は IXMLDOMDocument なので、読み込みと保存はこのオブジェクトを介して行う。
XML ドキュメントを新規作成する方法の覚書。 既存の XML ファイルを操作対象にする場合は、IXMLDOMDocument の load メソッドで読み込むが、新規作成の場合は、XML 文字列をドキュメントに見立てる、loadXML メソッドを利用する。
ここでいう XML 文字列とは、タグの指定などが入った XML として成立している文字列を指す。 例えば、"<?xml version="1.0" encoding="utf-8"?><root><test>aaaa</test></root>" の様に指定する。
これを利用して XML ファイルを生成する為には、以下のサンプルの様にする。
CComPtr<IXMLDOMDocument> pXmlDocument = NULL; HRESULT hr = S_OK; VARIANT_BOOL isSuccessful = VARIANT_FALSE; CComBSTR xmlString; xmlString = _T( "<?xml version=\"1.0\" encoding=\"utf-8\"?><root></root>" ); hr = pXmlDocument.CoCreateInstance( CLSID_DOMDocument ); if( S_OK == hr ) { pXmlDocument->put_async( VARIANT_FALSE ); hr = pXmlDocument->loadXML( xmlString, &isSuccessful ); if( S_OK == hr ) { pXmlDocument->save( _T( "test.xml" ) ); } }
loadXML に渡す文字列だが、XML 宣言も指定する事をおススメする。 この指定が無くとも XML の生成は可能だが、正式な XML としてメンテナンスするならば、宣言は必須なので必ず指定したい。
それと、loadXML でドキュメントを開いた場合、その文字列を基本とした XML が編集対象となるので、そのまま IXMLDOMElement にルートを取得して編集を続行出来る。
例えば以下の様な XML があったとする。
<root> <config> <variable>aaa</variable> <variable>bbb</variable> </config> </root>
この様に、同一階層、同一エレメント名が列挙されている場合、一見 XPath ではどちらか一方へのアクセスが出来ない様に見えるが、/root/config/variable[0] の様に、列挙されているエレメントへ配列添え字をつける事で、指定された要素へアクセス出来る。XML の性質上、あらゆるエレメントはユニークであり、この場合は何番目に登場するか?という点がポイントとなる。
そして、ここからが本題なのだが、添え字でアクセス出来るならば配列扱い出来ないのだろうか? まず配列として扱う場合、その総数を知る必要がある。その為には、前述の例の様な XML ならば selectNodes メソッドに /root/config/variable を指定して IXMLDOMNodeList のインスタンスを得て、get_length メソッドを呼び出す。 このメソッドは、リストの総数を得られる。
そして得られた総数をループの最大値とし、添え字を増加してゆけばそれぞれの要素にアクセス出来る。 また、
<root> <config> <variable> <test>aaaa</test> </variable> <variable> <test>bbbb</test> </variable> </config> </root>
の様な XML から aaaa と bbbb を得たいならば、variable 部分だけを配列扱いして、/root/config/variable[n]/test という XPath を用い、添え字 n を増加してインクリメンタルサーチすれば良い。
XPath とは、XML 文書内の特定箇所を指し示す構文の事。照会などにも用いられ、XML をデータベースとした場合のクエリともいえる。以下に XPath の指定方法を例を挙げながら覚書。
<?xml version="1.0" encoding="utf-8"?> <root> <config> <variable>内容</variable> </config> </root>
という XML 文書があるとして、この中から variable を指す XPath は /root/config/variable となる。 XPath はスラッシュを階層の区切り文字とし、ノード( エレメント )名が各階層名となる。
<?xml version="1.0" encoding="utf-8"?> <root> <config> <variable name="A">内容 A</variable> <variable name="B">内容 B</variable> </config> </root>
という XML 文書があるとして、この中から属性名 B を持つ variable を指す XPath は /root/config/variable[@name='B'] となる。属性名は、それを含むエレメント名の後にその情報を付与する。
エレメント名[@属性名='属性の内容']
という書式となる。
<?xml version="1.0" encoding="utf-8"?> <root> <config> <variable>内容 A</variable> <variable>内容 B</variable> <variable>内容 C</variable> </config> </root>
という XML 文書があるとして、この中から 3 番目( 内容 C を格納しているもの )の variable を指す XPath は /root/config/variable[2] となる。複数の同一名エレメントが同一階層に列挙している場合、エレメント名の後に 0 〜 N のインデックスを添え字として指定する事で、エレメントを特定出来る。
エレメント名[インデックス添え字]
という書式となる。
前述までの例は全て終端に条件を含んだ XPath だが、
<?xml version="1.0" encoding="utf-8"?> <root> <config> <variable> <variable name="A">AAAA</variable> <variable name="B">BBBB</variable> </variable> <variable> <variable name="C">CCC</variable> <variable name="D">DDD</variable> </variable> </config> </root>
という XML 文書があるとして、CCC を含む variable に到達する為の XPath としては、
の様に、複数の方法がある。途中に属性名や添え字を加える事で、多用なアクセスが可能。
動的に検索条件を行う場合、XPath に値の評価式を含める必要がある。 それは評価演算子( =,!=,<,<=,>,>= )や XPath 関数の組み合わせで表現される。
<?xml version="1.0" encoding="utf-8"?> <root> <tracks> <content id="AAAA"> <value name="Title">Test track A</value> <value name="AlbumTitle">Test Album</value> </content> <content id="BBBB"> <value name="Title">Test track B</value> <value name="AlbumTitle">Test Album</value> </content> </tracks> </root>
という XML 文書があるとして、
を得る XPath は、"/root/tracks/content[string( value[@name='Title'] )='Test track A']/@id" となる。 まず content ノードまでは普通に指定し、その添え字として、
と指定する事により、この条件を満たす子ノード value を持つ content ノードが選ばれ、末尾の /@id により、このノードの id 属性へのアクセスが行われる。
XPath の条件式指定は複数列挙する事も出来る。その場合、
/root/tracks/content[条件 1][条件 2]...[条件 n]/@id
の様に書く。 この例の場合、条件 1 〜 n までの全てに合致する content の id 属性を指す指定となる。 評価は左から右。
この仕組みは、iTunes 等に見られるスマートプレイリストの様なフィルタリングに応用出来る。
selectNode は、クエリにヒットしたノード全てが列挙されるので、例えば、
<root> <config> <variable name="a" value="config a"/> <variable name="b" value="config b"/> </config> </root>
という XML がある場合、"/root/config/variable" というクエリを指定する事で、variable を列挙出来る。 列挙したノードにアクセスする為には、このメソッドで得られた IXMLDOMNodeList の以下のメソッドを利用する
メソッド | 説明 |
get_length | 所持しているノード数を得る |
get_item | 所持しているノードから指定されたインデックスのノードを得る |
これらのメソッドにより、所持数とインデックス指定による抽出が行えるので、一般的な配列操作が行える。
selectSingleNode? は、
HRESULT selectSingleNode( BSTR queryString, IXMLDOMNode **resultNode );
と定義されている。得られる検索結果は、一つのノード( IXMLDOMNode )となる。もし複数ヒットする可能性のあるクエリを指定した場合、初めに得られたものとなる。
selectNode は、
HRESULT selectNodes( BSTR expression, IXMLDOMNodeList **resultList);
と定義されている。得られる検索結果はノードリスト( IXMLDOMNodeList )となる。
予め XML 中で一意な事が分かっているならば、selectSingleNode? が適している。 逆に、同じ名称のノードが列挙されている状態を操作したい場合などは selectNode が適している。
例えば、
<?xml version="1.0" encoding="utf-8"?> <root> <initialize name="Region"> <region name="Japan">this region is japan</region> <region name="USA">this region is USA</region> </initialize > <initialize name="Language"> <language name="Japanese">this language is japanese</region> </initialize > </root>
という XML から、"this region is japan" を取り出す為の XPath は、"/root/initialize[@name='Region']/region[@name='Japan']" となる。途中にエレメント名は重複しているが、属性名の異なるエレメントがある場合、XPath の途中の指定にその情報を含ませる事で対応する。
つまり、XPath の途中に属性名指定のあるタグを挟む必要があるのだが、その場合、途中の階層.../タグ名[@属性名1='属性値1']/...続きの階層という指定で対応する。
また、
<?xml version="1.0" encoding="utf-8"?> <root> <config name="initialize" type="Region"> <region name="Japan">this region is japan</region> <region name="USA">this region is USA</region> </config > <config name="initialize" type="Language"> <region name="Japanese">this language is japanese</region> </config > <config name="application" type="test"> <test name="test">this name is test</region> </config > </root>
という XML から、"this region is japan" を取り出す為の XPath は、"/root/config[@name='initialize'][@type='Region']/region[@name='Japan']" となる。
まずこの XML では、config というエレメントが 3 つある。config の name 属性を大別すると initialize と application の二種類で、application 側には目的の要素を持つエレメントはぶら下がってない。
次に、name が initialize となる二種類だが、これらは type 属性に差異があり、それらの内、type が Region になるものの下に目的のエレメントが存在する事が分かる。
つまり、XPath のクエリとして、途中のエレメントに二つの属性を指定した絞込みを行う必要がある。 その場合、タグ名[@属性名1='属性値1'][@属性名2='属性値2'] という指定により対応する。
あるエレメントに属性を追加する方法。
HRESULT CXmlDomIO::WriteAttribute( LPCTSTR xpath, LPCTSTR attributeName, LPCTSTR attributeValue ) { CComPtr<IXMLDOMNode> pNode = NULL; CComPtr<IXMLDOMElement> pElement = NULL; HRESULT hr = S_OK; hr = this->m_pXmlRoot->selectSingleNode( CComBSTR( xpath ), &pNode ); if( S_OK == hr ) { pElement = pNode; hr = pElement->setAttribute( CComBSTR( attributeName ), CComVariant( attributeValue ) ); } return hr; }
selectSingleNode? で発見したノードをエレメントのオブジェクトに代入、setAttribute メソッドで属性を設定している。 このサンプルメソッドの属性指定部分を可変長引数にする等で、一気に複数の属性を設定する事も可能。
XPath とは、XML ドキュメントの一部分をアドレッシングする為の言語である。と、W3C の XML Path Language に定義されている。 つまり、XPath とは XML ドキュメントに対するクエリとして利用出来る。例えば MSXML の IXMLDOMElement の selectSingleNode? は、指定された XPath に適合するノードへの参照を得られる。
MSXML のノードやエレメントには様々な検索用メソッドが提供されているが、検索対象が明確な場合、XPath による検索の方が容易であろう。パフォーマンス面の話はインサイド MSXML パフォーマンスに詳しい。
では、実際に XPath を利用する際、どの様に指定すれば良いのか? 以下の XML を例にすると、
<library xmlns="http://www.library.com/1.0" > <root> <config> <variable name="test" /> </config> </root> </library>
config エレメントを検索するクエリは、"/library/root/config" となる。その下の variable を検索するなら、一階層増やして、"/library/root/config/variable" とする。
では、
<library xmlns="http://www.library.com/1.0" > <root> <config> <variable name="test" /> <variable name="test2" /> </config> </root> </library>
の中から variable を検索する場合はどうか?単純に variable までをクエリとして指定すると、selevtSigleNode? では初めに検出されたものを返す。これは並び順の影響を受けるので、好ましくない結果だ。
通常、XML に何か設定を書く場合、あらゆるタグは「階層・エレメント名・属性情報」の面でユニークであるはずだ。例の XML では、name 属性の内容が異なっている。よってこの部分を含んだクエリを指定する事で、検索したいものを絞り込める。
そしてクエリに属性情報も含める場合は、"/library/root/config/variable[@name='test2']" の様に指定する。 このクエリの場合、上記 XML 内では <variable name="test2" /> がヒットする。
と、とても簡単な割りに効果テキメンに見えるが、デメリットもある。XPath はクエリなので、ヒットしたか否かしか管理出来ない。よって、途中経過を見る必要( 例えば終端がヒットしなくとも途中がヒットしたなら、そこに新しいノードを構築...etc )がある場合は、工夫が必要だろう。
XML が以下の様に名前空間を宣言しているとする。
<library xmlns="http://www.library.com/1.0" > <root> <config> <variable name="test" /> </config> </root> </library>
この状態で、IXMLDOMElement を作成し、例えば config ノードにそれを appendChild で追加しようとすると、
<library xmlns="http://www.library.com/1.0" > <root> <config> <variable name="test" /> <variavle xmlns="" name="test2" /></config> ←追加したエレメント </root> </library>
となってしまう。XML の構造としては矛盾していないのだが、空の名前空間属性が付与されてしまうのだ。 調査したところ、これは MSXML パーサの仕様との事。C# などの XML ライブラリでは、これを( この書き方は語弊があるが )防ぐ方法として、追加するエレメントへ明示的に名前空間を指定する事で解決出来ている。
だが、IXMLDOMElement には名前空間を指定するメソッドは用意されていない。その代わり、IXMLDOMNode は作成時に名前空間を指定出来る。では、IXMLDOMNode を appencChild すれば良いでは無いか?という話になるのだが、今度はこちらには属性を付与するメソッドが無いという問題がある。
と、万事休すの様に思えたが、調査の末、解決出来る事が判明した( これは SonicStage? 3.3 のライブラリ、XDomIO を見てひらめいた )。
IXMLDOMDocument から createNode でノードを作成する。このメソッドは名前空間を設定出来るので、指定しておく。 次に IXMLDOMElement の変数に作成されたノードを代入する。これで名前空間の付いたエレメントが生成される。 そしてエレメントに属性を追加し、appendChild すると、名前空間が同一の範囲ならば、今回の問題は生じない。
IXMLDOMElement と IXMLDOMNode が交換可能なので、相互補完して利用する、というお話。 以下はサンプル。
HRESULT CXmlDomIO::WriteElement( LPCTSTR xpath, LPCTSTR elementName, LPCTSTR attributeName, LPCTSTR attributeValue, LPCTSTR nameSpaceURI ) { CComPtr<IXMLDOMNode> pNode = NULL; CComPtr<IXMLDOMNode> pNamespaceNode = NULL; CComPtr<IXMLDOMElement> pNewElement = NULL; HRESULT hr = S_OK; hr = this->m_pXmlRoot->selectSingleNode( CComBSTR( xpath ), &pNode ); if( S_OK == hr ) { hr = this->m_pXmlDocument->createNode( CComVariant( NODE_ELEMENT ), CComBSTR( elementName ), CComBSTR( nameSpaceURI ), &pNewNode ); if( S_OK == hr ) { pNewElement = pNewNode; hr = pNewElement->setAttribute( CComBSTR( attributeName ), CComBSTR( attributeValue ) ); if( S_OK == hr ) { hr = pNode->appendChild( pNewElement, NULL ); } } } return hr; }
appendChild によってノードへエレメントを追加すると、
<root> <config> <variable name="width" value="1024"> <variable name="height" value="768"> ←追加したエレメント </config> </root>
となって欲しいのだが、
<root> <config> <variable name="width" value="1024"/> <variable name="height" value="768"/></config> ←ここに追加されてしまう </root>
となってしまう。仕様らしい。
XPath によって指定した場所へエレメントを追加する方法の覚書。
★予め m_pXmlDocument が確定しているものとする ★xpath には "/root/config" の様に目標のノード(エレメント)名を指定する HRESULT CXmlReaderWriter::AppendElement( LPCTSTR xpath, LPCTSTR elementName ) { CComPtr<IXMLDOMElement> pNewElement = NULL; CComPtr<IXMLDOMNode> pNode = NULL; HRESULT hr; hr = this->m_pXmlRoot->selectSingleNode( CComBSTR( xpath ), &pNode ); if( S_OK != hr ) return hr; hr = this->m_pXmlDocument->createElement( CComBSTR( elementName ), &pNewElement ); if( S_OK != hr ) return hr; hr = pNode->appendChild( pNewElement, NULL ); if( S_OK != hr ) return hr; this->m_pXmlDocument->save( CComVariant( this->m_strFileName ) ); }
まず createElement で追加するエレメントを作成する。 次に予め XPath により検索しておいたノードの子としてエレメントを追加する。
尚、appendChild は第一引数に IXMLDOMNode、第二引数に追加したノードへの参照( 戻り値 )を指定するが、第一引数に IXMLDOMElement を指定する事も可能なので、事前に作成したエレメントを指定する。尚、第二引数を利用しないなら NULL を指定しても問題無い。
ただし、どんどん階層を掘ってゆく場合などは、第二引数で追加されたエレメントやノードの参照を受け取っておく事により、階層 A、B... という様にどんどん掘り進む事が可能となる。
XML の XPath は、XML に対するクエリ。これを MSXML の IXMLDOMElement などが持つ selectSingleNode? に指定して、特定のノードを選択する方法は以下。すごくハマッタので覚書。
★XML の例 <?xml version="1.0" encoding="utf-8"?> <myroot> <test name="aaaa">ZZZZ</test> </myroot>
★コード - 予め pElement がルートをになっていると仮定する CComPtr<IXMLDOMNode> pNode = FALSE; HRESULT hr = pElement->selectSingleNode( L"myroot/test[@name='aaaa']", &pNode ); if( S_OK == hr ) { VARIANT result; hr = pNode->get_nodeTypedValue( &result ); if( S_OK ) { ::AfxMessageBox( CString( result.bstrVal ), MB_OK ); } }
[@属性名='属性内容']で属性を指定出来る。Microsoft のサイト情報によれば、ノードを再帰的に検索して目的のものを探すより、selectSingleNode? の方がパフォーマンスに優れる、とある。selectSingleNode? は一度の検索となるが、掘ってゆくタイプの場合、COM 呼び出しが頻繁に行われる為、重くなるのだとか。