2016 02 06

XMLの5

双子

朝、卵焼きを作ろうと卵を割ったら、シャム双生児風だった。

いつも買っている卵はやや大きめで、そのせいか黄身が双子の確率が結構高い。 しかしこれまでに何度か遭遇した双子はどれも、二つの黄身がきっちり分離していた。 こうして半分くっついた双子は、俺的には珍しいのだが、世間一般ではどうなんだろう。

そして、この塗りの剥がれたお碗を、俺はいつまで使うのか。

XML

昨日の続き。

メモリ不足をどう回避するかを考えていたのだが、更にその先を考えてみれば、仮に String.replace() で動いたとしても遅いんだよな。 特に第2引数をfunctionにした場合は激遅い。

ということで、SAX風という基本路線はそのままに String.replace() ではなく String.substr() を使ってみることにした。

String.replace() が置換した結果を新しい文字列として作成するのに対し、 String.substr() は元の文字列の参照開始位置をずらしたものを返すので、速い上にメモリの使用効率も良い。 よく判らないメモリ不足を、これなら回避できるのではないか。

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 = match( buf, rInstruction, instructionHandler ); buf = match( buf, rDoctype, doctypeHandler ); buf = match( buf, rComment, ignore ); buf = match( buf, rCData, cDataHandler ); buf = match( buf, rTag, tagHandler ); buf = match( buf, rText, textHandler ); if( len === buf.length ) { throw "ERROR : unsupported element"; } } return htmlAry; function match( s, regexp, handler ) { var matched = s.match( regexp ); if ( matched ) { handler( matched[ 1 ] ); return s.substr( matched[ 0 ].length ); } return s; } function ignore() {} function instructionHandler( s ) { htmlAry.push( "<div>&lt;?" + s + "?&gt;</div>" ); } function doctypeHandler( s ) { htmlAry.push( "<div>&lt;!DOCTYPE " + s + "&gt;</div>" ); } function cDataHandler( s ) { htmlAry.push("<div>&lt;![CDATA[<div>" + s.replace("<", "&lt;").replace(">", "&gt;") + "</div>]]&gt;</div>"); } function textHandler( s ) { if ( rNotBlank.test( s ) ) { htmlAry.push(s.replace( "<", "&lt;" ).replace( ">", "&gt;" ) ); } } function tagHandler( s ) { if ( s.charAt( 0 ) === "/" ) { endTagHandler( s ); } else if( s.charAt( s.length - 1 ) === "/" ) { emptyTagHandler( s ); } else { startTagHandler( s ); } } 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; } } }

昨日との大きな違いは、 String.replace() を繋げていたところが match() の繰り返しになったこと。 このmatchの中で

  1. XMLの要素を表現する正規表現にマッチするか試す。
  2. サブマッチを引数にハンドラーを呼び出し、innerHTML用の文字列を作成、登録する。
  3. String.substr() でマッチした分だけを除外した残りの文字列を、次の検索用に返す。

と、SAX風の肝となる処理をしている。

そもそも String.replace() を選択したのが記述が楽だからで、だから String.substr() にしたら面倒になるかと思ったのだが、その面倒な部分を関数に隠蔽してしまえば、全体的には若干面倒って程度で大差無いな。 いや、ハンドラーの方は長さ0の文字列を返す記述が不要になったし、むしろスッキリしたかもしれない。

そして動作確認。

これが予想外に速い。 そして完遂率も高い。 SAX風処理内でのメモリ不足は発生しない。

せっかくだから、昨日の即死版はちょっと忘れて、これまでの各バージョンに約10MBのテスト用XMLファイルを読ませて処理時間を比較してみた。

load() 内の最初と最後で時刻を取得して時間差を出してみると、こんな感じ。

月.日 処理時間(秒)
02.03 40
02.04 14
02.06 7

割と良い感じで日々改善されているではないか。

表中の下の2つは、どちらも処理時間のうちの3秒ほどがinnerHTMLの文字列をHTMLのDOMに変換するのに使われている。 また、どのバージョンでも、この処理時間の後におよそ10秒かかってHTAの画面のレンダリングが完了する。

つまり今のバージョンだと、改善の余地はあと4秒分ぐらいしかなくて、しかもそこはもうボトルネックではないのだな。

ということで、予想外に難航した doLoad() はここまで。 続きは明日。