Home About Contact
Node.js , XML

Node.js での XML処理 fast-xml-parser いろいろ躓きポイントの整理

XMLデータをいい感じに javascript のオブジェクトに変換してくれる・・・という fast-xml-parser

実際に使ってみた結果躓いた点を書き留めます。

以前にも同じようなことしていた。 Node.js で XML文書をパース

このときは XMLのパースに xmldom を使っていた。 完全に覚えていない。

事前準備

プロジェクトディレクトリを作成して、そこに fast-xml-parser を入れておきます。

$ mkdir myxml
$ cd myxml
$ npm init -y
$ npm install fast-xml-parser --save
$ touch index.js

要素内に複数の要素があるとき

root 要素内に a, b, c 要素が出現する場合:

const { XMLParser, XMLBuilder } = require('fast-xml-parser');

const xml = `
<root>
  <a>AAA</a>
  <b>BBB</b>
  <c>CCC</c>
</root>`;

const options = {
};
const parser = new XMLParser(options);
const object = parser.parse(xml);
console.log(object);

パースオプションはまだ空にしておく。

実行してみる。

$ node index.js
{ root: { a: 'AAA', b: 'BBB', c: 'CCC' } }

これは問題ない。

もし a 要素が複数出現したらどうなるのか? つまり、処理するXMLデータが以下の場合:

const xml = `
<root>
  <a>AAA</a>
  <b>BBB</b>
  <c>CCC</c>
  <a>AAA</a>
</root>`;

実行してみる。

$ node index.js
{ root: { a: [ 'AAA', 'AAA' ], b: 'BBB', c: 'CCC' } }

二回出現した a 要素がまとまってしまった。 データとしてはこの方が都合がよい場合もあるだろうけれど、 もし、HTML的な要素だとして h2, p, p, h2, p のような場合:

const xml = `
<root>
  <h2>H2H2H2</h2>
  <p>PPP</p>
  <p>PPP</p>
  <h2>H2H2H2</h2>
  <p>PPP</p>
</root>`;

パースした結果は:

{ root: { h2: [ 'H2H2H2', 'H2H2H2' ], p: [ 'PPP', 'PPP', 'PPP' ] } }

あきらかに期待したものではない。

しかし、 preserveOrder というオプションがある。 それを使ってみる。

パースオプション他いろいろなオプションは以下を参照:

空にしていたオプションを以下のように修正:

const options = {
    preserveOrder: true
};

実行するとこうなる。

[ { root: [ [Object], [Object], [Object], [Object], [Object] ] } ]

JSON.strigify() して内容がわかるようにする:

[{"root":[{"h2":[{"#text":"H2H2H2"}]},{"p":[{"#text":"PPP"}]},{"p":[{"#text":"PPP"}]},{"h2":[{"#text":"H2H2H2"}]},{"p":[{"#text":"PPP"}]}]}]

まあ、出現順は維持できるようになった。

属性が消える

const { XMLParser, XMLBuilder } = require('fast-xml-parser');

const xml = `
<root>
  <a id="1">AAA</a>
  <b id="2">BBB</b>
  <c id="3">CCC</c>
</root>`;

const options = {
};
const parser = new XMLParser(options);
const object = parser.parse(xml);
console.log(object);

実行:

{ root: { a: 'AAA', b: 'BBB', c: 'CCC' } }

消える。

オプションで、属性を維持するように指定:

const options = {
    ignoreAttributes: false,
    attributeNamePrefix : '__',
};

実行:

{
  root: {
    a: { '#text': 'AAA', __id: '1' },
    b: { '#text': 'BBB', __id: '2' },
    c: { '#text': 'CCC', __id: '3' }
  }
}

維持できました。

ちなみに、このオブジェクトを XML に戻すには:

new XMLBuilder(options).build(object);

コード全体では:

const { XMLParser, XMLBuilder } = require('fast-xml-parser');

const xml = `
<root>
  <a id="1">AAA</a>
  <b id="2">BBB</b>
  <c id="3">CCC</c>
</root>`;

const options = {
    ignoreAttributes: false,
    attributeNamePrefix : '__',
};
const parser = new XMLParser(options);
const object = parser.parse(xml);
//console.log(object);

const xml2 = new XMLBuilder(options).build(object);
console.log(xml2);

実行:

<root><a id="1">AAA</a><b id="2">BBB</b><c id="3">CCC</c></root>

パースしたときと同じオプションを XMLBuilder に指定しないと意図しない結果になるので注意が必要です。

勝手に数値にされた

こんなXMLをパースする場合。

<root>
  <id>0123</id>
  <id>456</id>
</root>

コード全体:

const { XMLParser, XMLBuilder } = require('fast-xml-parser');

const xml = `
<root>
  <id>0123</id>
  <id>456</id>
</root>`;

const options = {
};
const parser = new XMLParser(options);
const object = parser.parse(xml);
console.log(object);

const xml2 = new XMLBuilder(options).build(object);
console.log(xml2);

実行。

{ root: { id: [ 123, 456 ] } }
<root><id>123</id><id>456</id></root>

ある種のテキストノードは数値として扱われるので、0123 が 123 に変換されました。 0123 を維持することができません。

もちろん、パースオプション numberParseOptions を指定すればこれを回避できます。

const options = {
    numberParseOptions: {
        skipLike: /^[0-9]+/
    }
};

この場合は、0,1,2,3...,9 で始める場合は数値にするのを阻止します。

このオプションをパースオプションに指定して実行。

{ root: { id: [ '0123', '456' ] } }
<root><id>0123</id><id>456</id></root>

0123 が維持できました。 (文字列として扱われています。)

P要素の途中に BR がある場合

こんなXML:

<root>
  <p>Hello,<br/>World!</p>
</root>

HTMLでありそうな構造です。

const { XMLParser, XMLBuilder } = require('fast-xml-parser');

const xml = `
<root>
  <p>Hello,<br/>World!</p>
</root>`;

const options = {
};
const parser = new XMLParser(options);
const object = parser.parse(xml);
console.log(object);

const xml2 = new XMLBuilder(options).build(object);
console.log(xml2);

実行。

{ root: { p: { br: '', '#text': 'Hello,World!' } } }
<root><p><br></br>Hello,World!</p></root>

Hello, と World! の間に改行があることを期待しているのに。 br要素が先頭に移動してしまった。

あきらかに期待した結果ではない。

これは、オプションで回避する方法がすぐわからなかった。 もちろん、このコードのようにパースしてXMLに戻すだけならば preserveOrder をオプション指定すればOKです。

const options = {
    preserveOrder: true
};

これを XMLParser, XMLBuilder のオプションに指定すれば:

[ { root: [ [Object] ] } ]
<root><p>Hello,<br></br>World!</p></root>

問題ありません。

しかし、通常わざわざXMLをパースしたということは、 そこでなにがしかの処理をして・・・ということなので。 まあ、preserveOrder した上で、頑張ればいいってことだとは思う。