6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Node.jsで巨大なXMLファイルを扱う

Posted at

はじめに

ファイルサイズが数十GB程度あり、ファイルから読み込んだデータを全てメモリ上に展開するには大分無理のあるXMLファイルを扱う機会がありました。そんな時に便利なライブラリnode-xml-streamを見つけたので使い方をご紹介していきます。

今回、Node.jsのバージョンは18.12.0node-xml-streamのバージョンは1.0.2を使用しています。

node-xml-streamとは

XML/HTMLファイルを軽量かつ高速に処理するパーサーライブラリです。Node.jsの標準APIであるStreamを使用して作られています。
通常のファイル読み込みではデータを一気にメモリ上に展開してから後続の処理に進むのに対し、Streamを使用したファイル読み込みではchunkと呼ばれる単位でファイルを読み込みながら順次処理することができるため、物理メモリサイズを超えたファイルの処理も可能になります。その反面、階層構造が複雑なファイルの読み込みにはあまり適していません。理由は以降で説明します。

振る舞い

ファイルを開いたStreamとパーサーインスタンスをpipe()で接続すると、ファイルを順次読み込みながら開始タグや終了タグ等が見つかったタイミングでイベントが発火されます。これをハンドリングしてお目当てのデータを取得していくのが基本的な使い方です。

試しに、次のようなイベントを拾うだけのプログラムを作り、サンプルXMLファイルを読み込ませてイベントの発火順序を見てみましょう。

sample1.js
import { Parser } from 'node-xml-stream'
import fs from 'fs'

const parser = new Parser()

// 開始タグが見つかった
parser.on('opentag', (name, attrs) => console.log(`${name} has opened`))

// 終了タグが見つかった
parser.on('closetag', name => console.log(`${name} has closed`))

// テキストが見つかった
parser.on('text', text => console.log(text))

// 読み込みが完了した
parser.on('finish', () => console.log('finish!'))

// Streamとパーサーを接続してファイルを読み込んでいく
const stream = fs.createReadStream('./sample.xml')
stream.pipe(parser)
sample.xml
<?xml version="1.0"?>
<root>
  <parent>
    <child>foo</child>
    <child>bar</child>
  </parent>
  <parent>
    <child>hoge</child>
  </parent>
</root>
stdout
open: root
open: parent
open: child
text: foo
close: child
open: child
text: bar
close: child
close: parent
open: parent
open: child
text: hoge
close: child
close: parent
close: root
finish

実行結果を見ると分かる通り、各イベントはそれぞれ「何のタグの始まり/終わりが見つかった」「何のタグのテキストが見つかった」等、タグ単品についての情報は教えてくれますが、「親、子、兄弟にはどのようなタグを持つ」といったタグの文脈を教えてはくれません。データの取得時にはだいたい階層構造の決まった位置にあるテキストを取得したいケースがほとんどであると思うので、自前実装で見つかった開始タグと終了タグの名前や出現回数を見ながらデータを取得していく必要があります。

サンプル

タグの開始/終了をフラグ管理し、お目当ての箇所のテキストが見つかったらテキストを取得する例です。これに先程のサンプルXMLファイルを読み込ませると、期待どおりの位置のテキストが取得できていることが確認できます。

サンプルのような小さなXMLファイルではほとんどライブラリによる恩恵は受けられませんが、数GBから数十GBクラスのファイルを読み込ませても、このプログラムが使用する物理メモリ量は高々数百MB程度におさえることができます。

sample2.js
const isOpened = {
  root: false,
  parent: false,
  child: false,
}

parser.on('opentag', (name, attrs) => {
  if (!typeof(isOpened[name]) !== 'undefined') {
    isOpened[name] = true
  }
})

parser.on('closetag', name => {
  if (!typeof(isOpened[name]) !== 'undefined') {
    isOpened[name] = false
  }
})

parser.on('text', text => {
  // root > parent > childのテキストを取得する
  if (isOpened.root && isOpened.parent && isOpened.child) {
    console.log(`child: ${text}`)
  }
})

const stream = fs.createReadStream('./sample.xml')
stream.pipe(parser)
stdout
child: foo
child: bar
child: hoge

まとめ

巨大なXMLファイルを扱えるnode-xml-streamをご紹介しました。あまりこのようなユースケースは多くはないとは思いますが、日本語の記事を探しても見当たらなかったので思い切って自分で書いてみました。記載した内容に誤り等があればご指摘ください。また、もっと便利な使い方があるよ~なんてご意見等あればぜひお寄せいただけると嬉しいです。ではでは。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?