2016 02 05

XMLの4

仕事中、忙しいときは気分転換に、暇なときは暇潰しに、だいたい1時間に1回ぐらいは飲み物を買いに席を立ったりする。 で、自販機の前でコーヒーが出来上がるのをぼんやり待っていて、ふと 「はまだしょうこ」 が濁ると 「はまだしょうご」 になることに気がついた。 濁点一つでこの違い。 ちょっと凄い。 字面とは逆に内面は浜田翔子の方が濁ってそうだけど。

そして自席への帰り道。 カップのコーヒーを持って歩きながら 「全部濁ったら山瀬まみ」 なんて浮かんできて、自分のおっさん振りに愕然とするのだった。 そういえば、NHKの試してガッテン以外で見たことがないな、山瀬まみって。

XML

昨日の続き。

XMLのDOMからHTMLに変換するときは、DOMの全要素に順番に、そして各要素に1回だけアクセスする。 過去の経験からWSHで手軽に使えることが判っているDOMを選択したのだが、今回のような場合はSAXの方が向いているんだよな。 俺の脳内アントワネットも 「DOMが駄目ならSAXを使えばいいじゃない」 なんて言ってるし、じゃあSAXでと思ったのだが。

結論から言うと、WSHではSAXは使えない。

特定のクラスを継承したクラスを作る必要があり、これがJavaScriptではうまくいかない。 VBScriptでもだめ。 MS OfficeのVBAならできるようだが、そんなものを使う気は無いし。

でもここで終わるのはなんだか負けた気分なので、自分でSAX相当の処理を実装してみた。

function doLoad() { try { document.getElementById( "base" ).innerHTML = xmlToHtml( loadXml( getFilename() ) ).join( "" ); } catch ( e ) { alert( e ); } function getFilename() { var filename = document.getElementById( "txtFile" ).value; if ( !filename ) { throw "ERROR : filename"; } return filename; } function loadXml( filename ) { var iStrm = new ActiveXObject( "ADODB.Stream" ); var enc = loadFromFile( "Shift_JIS", -2 ).match( /\s+encoding="([^"]+)"/ ); return loadFromFile( enc ? enc[ 1 ] : "Shift_JIS", -1 ); function loadFromFile( encoding, mode ) { iStrm.Charset = encoding; iStrm.Open(); iStrm.LoadFromFile( filename ); var buf = iStrm.ReadText( mode ); iStrm.Close(); return buf; } } function xmlToHtml(xml) { var rInstruction = new RegExp( /^<\?(.+?)\?>/ ); // 1:宣言内部 var rDoctype = new RegExp( /^<!DOCTYPE\s+([^\s].+?)>/i ); // 1:宣言内部 var rComment = new RegExp( /^<!--([\s\S]*?)-->/ ); // 1:コメント var rCData = new RegExp( /^<!\[CDATA\[([\s\S]+?)\]\]>/i ); // 1:CDATA内部 var rText = new RegExp( /^(([^<]|[*s])+)/ ); // 1:テキスト全体 var rTag = new RegExp( /^<([^\?!][^>]*(\s*("[^"]*")*\s*)*?)>/ ); // 1:<>内部 var rNotBlank = new RegExp( /[^\s]/m ); var rAttributes = new RegExp( /\s+([^\s="]+)\s*=\s*"([^"]+)"/g ); // 1:属性名, 2:属性値 var htmlAry = []; var buf = xml; while ( buf !== "" ) { var len = buf.length; buf = buf.replace( rInstruction, instructionHandler ) .replace( rDoctype, doctypeHandler ) .replace( rComment, ignore ) .replace( rCData, cDataHandler ) .replace( rTag, tagHandler ) .replace( rText, textHandler ); if( len === buf.length ) { throw "ERROR : unsupported element"; } } return htmlAry; function ignore( s, s1 ) { return ""; } function instructionHandler( s, s1 ) { htmlAry.push( "<div>&lt;?" + s1 + "?&gt;</div>" ); return ""; } function doctypeHandler( s, s1 ) { htmlAry.push( "<div>&lt;!DOCTYPE " + s1 + "&gt;</div>" ); return ""; } function cDataHandler( s, s1 ) { htmlAry.push( "<div>&lt;![CDATA[<div>" + s1.replace( /</g, "&lt;" ).replace( />/g, "&gt;" ) + "</div>]]&gt;</div>" ); return ""; } function textHandler(s, s1) { if (rNotBlank.test(s1)) { htmlAry.push(s1.replace( /</g, "&lt;" ).replace( />/g, "&gt;" )); } return ""; } function tagHandler( s, s1 ) { if ( s1.charAt(0) === "/" ) { endTagHandler( s1 ); } else if( s1.charAt( s1.length - 1 ) === "/" ) { emptyTagHandler( s1 ); } else { startTagHandler( s1 ); } return ""; } function startTagHandler( s ) { htmlAry.push( "<div>&lt;" + setClassToAttribute( s ) + "&gt;" ); } function endTagHandler( s ) { htmlAry.push( "&lt;" + s + "&gt;</div>" ); } function emptyTagHandler( s ) { htmlAry.push( "<div>&lt;" + setClassToAttribute( s ) + "&gt;</div>" ); } function setClassToAttribute( s ) { if ( rAttributes.test( s ) ) { return s.replace( rAttributes, ' $1="$2"' ); } return s; } } }

自作のなんちゃってSAXなので、いろいろ面倒が増えている。

まずは入力側の loadXml()

MSXMLならencodingを気にせずにメソッド一つで読み込み完了だったのだが、自分でやるならencodingを意識しなきゃいけないんだよな。

ファイル処理でよく使う Scripting.FileSystemObject は、扱えるencodingが限定されていて駄目。 最近主流のUTF-8が読めないのが致命的。 というわけで ADODB.Stream を使う。

このADODB.Streamでいろんなencodingに対応できるのはいいのだが、ファイルを開くときにencodingを指定しないといけない。 でもこれから処理しようとするファイルのencodingが何なのかは、ファイルを開いてみないと分からないというジレンマ。

幸い、XMLの場合は先頭行に

<?xml version="1.0" encoding="UTF-8"?>

などと指定されているはずなので、とりあえずシステムのデフォルトのShift_JISとして1行だけ読んでencodingを取得し、これを指定してファイル全体を読み直している。

読み直しで一気にファイル全体を読んでいるのは、設定用のXMLファイルだからメモリを圧迫するほど大きくは無いと思ったから。

って、そもそもの作る動機が、大きなXMLの設定ファイルをなんとかしたいと思ったからだったんだよな。 自分で設定値を確認するときは 「どこの馬鹿が設定ファイルをここまで肥大化させたんだよ」 なんて文句ばかりのくせに、それをプログラムにデータとして渡すときは 「このぐらい問題無い」 と思うとは。 まあ、それが人間なんだけど。

と、個人の問題を人間全体に広げて、出力側の xmlToHtml()

String.replace() は、第2引数にfunctionが指定できる。 実際に指定すると、第1引数で指定した正規表現にマッチした文字列がfunctionの引数として渡される。 サブマッチがある場合は第2引数以降に設定される。 そしてfunctionの戻り値で置換が実行される。

これを利用して、

  1. XMLの各要素があるかをXML文字列の先頭で検索し、有ればその文字列を各要素用に対応したハンドラーに渡す。
  2. ハンドラーは長さ0の文字列を返すことで、XML文字列の先頭から要素一つ分を消す。

を、XML文字列が0になるまで繰り返す。

こうすると、XMLの先頭から要素検出毎にイベントが発生してイベントハンドラーが呼び出されるかのような、ちょっとSAXっぽい動きになるのだ。

そして動作確認。

小さいファイルは問題無い。

が、大きいファイルが駄目。 処理開始してすぐに

Error: メモリが不足しています。

と言われてしまう。 DOMを使っていた版で仮想記憶がどうとか言ってきたときは、処理開始してからある程度時間が経ってエラーになったのだが、今回のは速攻終了。

処理をちょっとずつ進めながらタスクマネージャーで観察すると、xmlToHtml内のwhileループでメモリの使用量が跳ね上がっていた。

試しにhtmlAryに追加している文字列をサブマッチじゃなくて "------" みたいな固定文字列にすると、メモリ使用量上昇のカーブは少し緩くなるけど、すぐにメモリ不足で終了してしまうのは同じ。

String.replace() は実行結果の文字列を新たに作って返す。 この引数をfunctionにしたりメソッドチェーンで繋げたりがスタックを食い潰すのかもと考えて、メソッドチェーンをやめてみた。 駄目。

イベントハンドラーをクロージャーにせず、独立したオブジェクトのメソッドとし、このオブジェクト内にhtmlAryを隠蔽してみた。 駄目。

サブマッチじゃなくてマッチした文字列全体、つまり各ハンドラーに渡す第1引数のみにして、イベントハンドラーの中でサブマッチ相当を取り出すようにしてみた。 駄目。

そういったロジックの問題ではなくて、ちょっとしたコーディングミスで巧くいってないという気もするのだが、よく判らん。 うーん…