Hatena::ブログ(Diary)

ホワイトぼーど このページをアンテナに追加 RSSフィード Twitter

2011-10-29

node.jsで1行ずつテキストを読み込む方法

Perlだとあんなに簡単だったのに…


ファイルから1行ずつテキストを読み込む処理も、Perlだとファイルハンドルを使って簡単にできたのですが、少なくとも現時点でのnode.jsでのファイル読み込みはバイト単位が基本で、1行単位の読み込みに特化した命令がないようです。


ちなみにPerlでいうこれをやりたいわけです

open(my $fho,">", $outfile);
open(my $fhi,"<", $infile);
while(<$fhi>){
	print $fho $_;
}
close($fhi);
close($fho);

処理時間:868ms(約0.9秒)
※全郵便番号データを読み込んで1行ずつ書きこむ処理の場合の結果


そもそもDB利用によるWebアプリケーションを前提としていて、1行ずつ読み込む場面が少ないのかもしれませんが、例えば郵便番号データを扱っているWebアプリケーションだった場合、改訂される度にCSVファイルでデータを取り込んだりする必要があります。

CSVパーサライブラリはあるので、まぁ本来はそっちを使うべきなのでしょうが、それでもやっぱり1行単位での処理は欲しかったりします。で、ちょっと調べてみました。


1行ずつ読み込む方法その(1)

var fs = require('fs');
var dat = '';
fs.readFileSync(infile).toString().split('\n').forEach(function (line) {
	dat = dat + line + '\n';
});
fs.writeFile(outfile,dat);

ファイルを全て読み込み、それを改行で分割して、さらに分割した数だけループして1行ずつ処理するという、荒業です。見たとおり、読み込み自体も同期処理を前提としていて、その時点でnode.jsを使ってる喜びが半減しますが…。

郵便番号データの読み書き処理をさせて、console.timeとprocess.memoryUsageを使って、処理時間とメモリー使用量を調べてみたところ以下のとおりでした。

処理時間:1220ms(約1.2秒)
メモリ使用量:{ rss: 56,823,808, heapTotal: 37,834,716, heapUsed: 23,613,824 }


1行ずつ読み込む方法その(2)

var lazy = require("lazy");
var ws = fs.createWriteStream(outfile);
new lazy(fs.createReadStream(infile)).lines.forEach(function(line){
	ws.write(line);
});

lazyというモジュールを使うことによって、ストリームで断続的に入手したデータを、改行ごとにリストにして、それをループさせることができます。ですが、以下のように、2分25秒もかかってしまいます。

処理時間:145065ms(2分25秒)
メモリ使用量:{ rss: 62,959,616, heapTotal: 38,773,080, heapUsed: 17,431,908 }

データ自体は問題なく書きこまれているんですけどね…。
以下のようにすることによって処理を早くできるのですが、これじゃ非同期の意味がまったくありません。

var lazy = require("lazy");
var dat='';
var rs = fs.createReadStream(infile)
new lazy(rs).lines.forEach(function(line){
	dat = dat + line + '\n';
});
rs.on('close',   function (){
	fs.writeFile(outfile,dat);
});

処理時間:1202ms(約1.2秒)
メモリ使用量:{ rss: 52,518,912, heapTotal: 42,169,216, heapUsed: 28,591,948 }


カーネルバッファにご注意


最初、(1)も書き込みの部分をcreateWriteStreamを使って書きこんでいたのですが、結果2分以上掛かってしまい、こりゃ駄目だって話で、現在のようになってます。

調べてみたところ、node.jsマニュアルの「Writable Stream」のところにこんな記述が

「文字列がカーネルバッファにフラッシュされた場合は true が返ります。
カーネルバッファがいっぱいの場合は、データが将来カーネルバッファに送られることを示すために、 false が返ります。」


ひょっとしてカーネルバッファが一杯になってるのではと思い、戻り値を出力してみたところ、こんな感じ…

false
false
false
false
・
・
・

えらいことになってました。


書き込みを一時停止する方法


Writable Streamに書き込みを待機させる方法が無いかと調べてみましたが、これが無かったりします。ですが、実はReadable Streamには、読み込みを停止させ、復帰させるpause()とresume()があります。

そこで先ほどのlazyの出番です。

lazyを使えば、読み込みのストリームとforEachが連動するので、読み込み自体をストップさせることで書き込みを停止させることは可能になります。

でこんな感じになりました。


1行ずつ読み込む方法その(3)

var lazy = require("lazy");
var ws = fs.createWriteStream(outfile);
var rs = fs.createReadStream(infile,{bufferSize: 256 * 1024});
new lazy(rs).lines.forEach(function(line){
	var ret = ws.write(line + '\n');
	if(ret == false){
		rs.pause();
	}
});
ws.on('drain', function (){rs.resume();})

falseを感知したら、読み込みを停止するという仕様です。
「256」の数字を変えることで多少負荷が増減します。
で、結果はというと以下のとおり。

処理時間:15228ms(15秒)
メモリ使用量:{ rss: 13,123,584, heapTotal: 9,272,704, heapUsed: 3,237,448 }

おお!やった。
先ほど2分以上かかってたのがウソのように短くなりました。
(1)の1.2秒には遠く及びませんが、メモリの使用量が格段に下がったのは喜ばしいことです。

ここでもう一度、戻り値を見てみると

false
false
・
・

先ほどほどではなくなりましたが、まだありますね。
そりゃそうですよね。falseが出てから停止させてるわけですから。
ならばいっそのこと、一定文字数ごとに停止させてみてはどうかと思って作ったのが次のソース。


1行ずつ読み込む方法その(4)

var lazy = require("lazy");
var ws = fs.createWriteStream(outfile);
var rs = fs.createReadStream(infile,{bufferSize: 256 * 1024});
var a = 0;
new lazy(rs).lines.forEach(function(line){
	var dat = line + '\n';
	a = a + dat.length;
	var ret = ws.write(dat);
	if(a > 64){
		rs.pause();
	}else if(ret == false){
		rs.pause();
	}
});
ws.on('drain', function (){a=0;rs.resume();})

「64」となっている文字数、これを超えると読み込みを停止させています。
こちらも数値を変えることで多少負荷は増減し、false数を0にすることも可能です。

処理時間:15366ms(15秒)
メモリ使用量:{ rss: 13,148,160, heapTotal: 9,272,704, heapUsed: 1,905,400 }

結果は(3)とあまり変わりませんが、falseが返ってきてるのを考えると気持ち悪いので、自分的にはこちらが好みです。


結論


まぁ、結局のところ、普段は(1)とかで済むかもしれませんが、メモリの使用が限られる場面で大きなファイルを扱う場合とかでは(4)も使い道がある…かな?
非同期でできたし、なによりも勉強になりました。

あと、関係無いですが、Web漫画描いてます。
『情弱姫+iPhone
http://hime.dojin.com/
よかったらご覧下さい。


追記


読取を行単位で行うモジュールを見つけました。
byline」というやつです。
以下のような書き方ができます。

var byline = require('byline');
var stream = fs.createReadStream(infile, {encoding:'utf8'});
stream = byline.createLineStream(stream);
stream.pipe(fs.createWriteStream(outfile));

で、結果はというと以下のとおり。

処理時間:145837ms(2分26秒)
メモリ使用量:{ rss: 56913920, heapTotal: 29177692, heapUsed: 16263500 }

やっぱりpipeしようが、これもfalseが発生している模様です。
ReadableStreamとWritableStreamを直接pipeしたらfalseは発生しないんですけどね。

いずれにしても一行ずつストリームで読み込むことは可能なので、
書き込みはDBを使うようにします。
ということで、次はCSV->DBをやってみたいと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

Connection: close