【JavaScript】長大なテキストファイルを分割して読み込む。

数百MBのテキストファイルを読み込んでごにょごにょする JavaScript を書く必要に迫られた。

デカいファイルを一気に読み込むと当然のようにブラウザが固まるので、『ファイルの分割読み込みをしつつ読み込んだデータを逐一処理してゆく』というサンプルを探してみたところ、Stack Overflow の記事に辿り着いたのです。

そのスクリプトをコピペしてみる。

function parseFile(file, callback) {
  var fileSize = file.size;
  var chunkSize = 64 * 1024; // bytes
  var offset = 0;
  var self = this; // we need a reference to the current object
  var block = null;

  var foo = function(evt) {
    if (evt.target.error == null) {
      offset += evt.target.result.length;
      callback(evt.target.result); // callback for handling read chunk
    } else {
      console.log("Read error: " + evt.target.error);
      return;
    }
    if (offset >= fileSize) {
      console.log("Done reading file");
      return;
    }

    block(offset, chunkSize, file);
  }

  block = function(_offset, length, _file) {
    var r = new FileReader();
    var blob = _file.slice(_offset, length + _offset);
    r.onload = foo;
    r.readAsText(blob);
  }

  block(offset, chunkSize, file);
}

<input type="file" 〜 > で指定されたファイルオブジェクトをこの parseFile() に渡してやれば、内部で block() が再帰的に呼ばれて、ファイルから指定サイズだけデータを取り出し → callback() でデータを処理 → その間にファイルから次のデータが読み込まれる … という処理が行えます。

割とよくできたスクリプトで、実際に思ったような処理が行えるのだけれど、これ、実は大きな落とし穴があるのですな。処理対象がマルチバイト文字を含んだテキストファイルの場合、このスクリプト、上手く動かない。

エラーも発生しないし、何事もなく処理は終了するのだけれど、何故か結果が不正 … という、一番たちの悪いことになる。

結構長いこと悩んだ末に気がついた。

  • chunkSizeoffset の単位は "バイト"
  • ファイルを readAsText( ) で読み込んでいるので、読み込んだデータ = evt.target.result は文字列型となる。なので、そのサイズ = evt.target.result.length は "バイト数" ではなく "文字数"

読み込むテキスト内にマルチバイト文字が含まれていると、当然「読み込んだバイト数 > 読み込んだ文字数」となるので、上のスクリプトの 9行目、

      offset += evt.target.result.length;

…と、次の読み込み位置(offset)を evt.target.result.length 分だけずらす処理を行う時に誤差が生じるわけですね。これではズラし足りないので、データが重複して読み込まれる部分がでてきます。

なので、この部分はこう直す。

      offset += chunkSize;

これでOK。

カテゴリ: