【プログラミング入門の入門】テキストを1行ずつ処理してみよう

javascript

プログラミング Node.js 1行ずつ読み込み

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は207日目になります。

第5回目はNode.jsでテキストファイルの入出力を行いました。

リキくんは10万行のテキストを一行ずつ調べて、
特定のワードが含まれるものだけを抜き出したファイルを作りたいとトモカズくんにプログラミングを教えてもらっています。

前回ではテキストファイル丸ごと読み込んでしまっていたので、
今回もfsモジュールと新たなreadlineモジュールを使って一行ずつ処理するやり方を覚えていきます。

それではプログラミングの入門の入門最終回目始めましょう。

  1. 一行ずつ読み込んで表示してみよう
  2. 後は配列の数だけ書き込めばOK
  3. プログラムが完成した!

一行ずつ読み込んで表示してみよう

前回でテキストファイルの読み込みができたから今回は一行ずつ読み込んで行こうか。

おう!頼むぜ!

それじゃあ早速コードを書いていくよ。

var fs = require("fs");
var readline = require("readline");

var stream = fs.createReadStream("test.txt", "utf8");

var reader = readline.createInterface({ input: stream });
reader.on("line", (data) => {
  console.log(data);
});

おいトモカズ、なんか見知らぬ「readline」ってのが読み込まれているぞ!なんだこれ!

いいところに気付いたね。この「readline」こそが今回の主役なんだ。名前からして行で読み込もうとしてるでしょ。

た、たしかに。リードラインってそのまんまだな。

プログラミングにおいてわかりやすい名前を付けるのってとても大事なんだ。これに由来のないオリジナル名なんて付けられたら何に使うのかわからなくなるでしょ?
変数や関数の名前に関しても名前を付けるのは結構重要なファクターなんだ。

確かに名前は大事だな。自分でも後から見たらわからなくなりそうだ。

今回はちょっと見た目は難易度が高いけど、やっていることはすごくシンプルだから順を追って説明するね。ソースコードにコメントを付けてみたよ。

// fsモジュールを変数「fs」に読み込む
var fs = require("fs");
// readlineモジュールを変数「readline」に読み込む
var readline = require("readline");

// stream変数を作り、fsモジュールの「createReadStream」メソッドを使ってテキストファイルをストリーム形式で読み込む
var stream = fs.createReadStream("test.txt", "utf8");

// reader変数を作り、readlineモジュールの「createInterface」メソッドに先ほど作ったストリーム情報を渡す
var reader = readline.createInterface({ input: stream });

// reader変数のonメソッドで1行ずつconsole.logを実行し行末まで繰り返す
reader.on("line", (data) => {
  console.log(data);
});

テキストファイルを読み込むところまではなんとなくわかるけどよ、その後がよくわからんのだが。

ストリームやインターフェイスなどは結構専門知識よりだから今はそこまで詳しくしらなくてもいいかな。
前にも言ったけどこうやったらこう動くって覚えておいた方が良いよ。
ぶっちゃけるとボクも完全に理解しているわけじゃないからうまく噛み砕いて説明できないんだけどね。

たぶん説明されても理解出来そうにないからそうするよ。

プログラミングのコツとしては、こういうやり方をすればこう動くんだ。でもこうしたいときはどうすればいいんだろ?って疑問を持つようになったら、
そのモジュールやメソッドについて調べてみるんだ。そしてやりたいことが出来るようなら同じモジュール内のメソッドを呼び出せばいいし、
無かったら自分で作るか別のモジュールを使って組み合わせるかってやるのがベストかな。

無ければ自分で作る……難しそうだけど言葉の響きがカッコいいな!

世の中の凄いものは「無いから作る」「コレ凄いんだけどもう1歩つっこんだものがあればいいのに」という発想によって生み出されることが多いんだ。

ストリーミングとは

それじゃあ説明の続きをするね。リキくん、ストリーミングって聞いたことない?

あぁ、動画とか音楽とかによくストリーミング再生って書いてるのみたことあるぞ。そういえば特に深く考えたことなかったな。

簡単に言うと基本的には動画とか画像は全部読み込んでから動かすんだけど、ストリームは読み込み終わった順に見せて、
見せている間に読み込むってイメージかな。
youtubeのライブ動画とかで放送者と視聴者の間でラグがあるのをイメージしてもらえたらわかりやすいかな。

動画を見てて途中で読み込みが止まるのは読み込みが追いついていないって感じか?

そうだね。ニコニコ動画とかyoutubeというか動画サイトは基本的にその仕組みだよ。今回のテキストの場合は動画や画像と違って一瞬で読み込んじゃうからプログラム側で合図が無いと次を読み込まないようにしているって考えてもらえればわかりやすいかもしれないね。

なるほど、ストリーミングについてはなんとなくわかったぜ!

ファイルを読み込んでストリーミング機に渡す

次に読み込んだファイルをストリーム形式にして1つの塊にするんだ。ストリーム形式はちょっと特殊でストリーム処理をする役割が必要で、
それが「readline.createInterface」という命令だよ。このメソッドにさっき作ったストリーム形式を渡しているんだ。

input:streamの「input:」ってなんだ?

key:valueタイプのオブジェクトなんだけど、今はちょっと説明がしづらいかな。
簡単に言うと「createInterface」が用意してくれている「input」っていう入れ物にさっき作ったstreamを入れるって意味だよ。
ここのinputは変数と同じようなものだと思ってくれればいいかな。

よくわからんが変数みたいなもんだな!

例えて言うならば、streamがゲームソフトでreaderがゲーム機で、createInterfaceがゲームソフトに書かれてる内容を機械にわかりやすく0と1に変換して伝えるための翻訳機でinput:がゲーム機のソフトを差し込む場所ってところかな。

なるほど!わかりやすいな!ってことは次にあるreaderはゲーム機だから、onはさしずめ電源を入れるってことか!?

そうそう。そのイメージだよ。

そのあとのdata〜ってのはよくわからんな。
中に入っているconsole.log変数を表示するものってのはわかる。
そしてdataを指定している…そういうことか!dataは変数だ!そして今回やっていることは一行ずつ表示するということだから…
ってあれ?

どうしたの?リキくん

いやな、俺の予想ではループするたびにこの「data」変数に一行ずつ取り出してんのかなって思ったんだがループしてないんだよ。ループってforとたしか…whileって奴を使うんだよな?

そういうことか、ごめんよちゃんと説明してなかったね。
実はこのonの後にある「”line”」ってのは行で読み込めって命令を与えていて、その後はリキくんの予想通り「data」って変数にループするたびに中身を入れ替えていく処理をするよ。そしてその中括弧の中に入れると自動的にループ処理にしてくれるんだ。

な!自動的にループになっているだと!?なんでだ!?

見えないところでforやWhile(厳密にはforやwhileではない繰り返し処理)がreadlineモジュールに記載されていて、渡したstreamを一行ずつ取り出してdata変数に入れてくれているんだよ。そしてファイルの最後に到達したらループを抜けろっていう命令が与えられているんだ

そういうことか…でも俺の考えは正しかったってことか!

うん、単にこういう動きをするっていう知識がなかっただけで基本的な考えはリキくんの考えで正解だよ。最初の頃に比べてすごい成長ぶりだね。

だろ!やっぱ実践派だな俺は!

後は配列の数だけ書き込めばOK

1つずつ抜き出すことが出来れば、あとはそれを配列に格納していけば1つの塊として完成するね。

配列って変数みたいに入れていいのか?

それもありだけど、JavaScriptだと配列の大きさが可変式だから最初は配列の大きさ指定無しで宣言するんだ。

でもどうやって入れていくんだ?

配列には「push」メソッドっていうものがあって、配列に指定した要素を追加していくんだ。

// 空配列を宣言
var ary = [];

// 何かてきとうなテキストを用意
var item0 = "テキストだよ";
var item1 = "もうひとつはいるよ";

// この時点でary = ["テキストだよ"]と同じ状態になる
ary.push(item0);
console.log(item0[0]);

// この時点でary = ["テキストだよ", "もうひとつはいるよ"]と同じ状態になる
ary.push(item1);
console.log(item0[1]);

こんな感じに配列は増やしていけるんだ。

便利だな!ってことはループの中でpushをしていけばいいってことか?

ご名答!後は配列が出来たらそれを元に今度は準備した配列を元にループさせて追記書き込みをしていくよ。

追記でファイルに書き込んでいくには

いよいよラストか……

リキくんのやりたいことの基本的なことはこれで最後になるね。あとはどの条件でテキストを保存するか見送りにするかは条件次第だからまた今度ね。

前回やった書き込みをループの度にやるのはダメなのか?

試しにやってみてよ。すぐにダメだって気付くからさ。

(リキくん試し中)

……ダメだ

でしょ?

最後の1行しか書き込まれてなかった……どういうことだトモカズ!

前回教えたのはファイルを新規に書き込む方法なんだ。だから毎回新しい同じ名前のファイルを作って保存しているような感じだよ。

ってことは続けて書き込む方法があるってことか!?

そう。ファイルがない場合は新規作成して、ファイルが存在すれば末端から書き込んでいくって寸法さ。

どうやるんだ!

まぁそう焦んないでよ。それじゃあ読み込みのコードの続きに書いていくよ。

// text用の配列を準備
var texts = [];

// fsモジュールを変数「fs」に読み込む
var fs = require("fs");
// readlineモジュールを変数「readline」に読み込む
var readline = require("readline");

// stream変数を作り、fsモジュールの「createReadStream」メソッドを使ってテキストファイルをストリーム形式で読み込む
var stream = fs.createReadStream("test.txt", "utf8");

// reader変数を作り、readlineモジュールの「createInterface」メソッドに先ほど作ったストリーム情報を渡す
var reader = readline.createInterface({ input: stream });

// reader変数のonメソッドで1行ずつconsole.logを実行し行末まで繰り返す
reader.on("line", (data) => {
  texts.push(data);
});

// texts配列の長さ分ループする
for (i=0; i<texts.length; i++) {
  // output.txtに追記していく(ファイルが無ければ新規作成)
  fs.appendFile("output.txt",texts[i]);
}

まず、テキスト用の配列を作って、console.logだったところでpushを使って配列の要素を増やしていき、最後に書き込みって感じか!……うおお!?

ど、どうしたのリキくん?変な声を上げて。

今冷静になって客観視したら俺、プログラムが読めるようになってるじゃねぇか!すげぇなおい!

よ、よかったね。パッと見は難しいけどよく見たら単純なことしかしてないでしょ?もっと深くみたらものすごく難しいことをしているんだけど、
JavaScriptはそういうのを取っ払って難しいことを考えずに使えるようになっていることが多いんだ。

俺、プログラミングのこと勘違いしてたよ……正直楽しいぜ!

それはよかった。でもこのプログラム、欠陥品なんだ。どこかわかる?

え?どこがだ?

実行してみればわかるよ。

……あれ?output.txtが出てこないぞ!どうなってんだ!?

リキくんの処理の考え方は間違ってないよ。ただこのプログラムは配列を作る前に先に「for」の部分が動いてしまっているんだ。

つまり……どういうことだってばよ……?

配列に追加する時にconsole.logをして中身を確認して、forの手前にもconsole.logを入れてみるとよくわかるよ。

forの手前の方が先に表示されてるだとー!?ってことはtexts配列の要素は0!そりゃ動かないぜ!なぜなんだ!

実はストリーム処理に秘密があるんだ。ここでは詳しくは言わないけど「非同期処理」で動いているから先にforの方に処理が言っちゃうってことなんだよ。

ぜんぜんいみわからへん……

うーん、非同期処理についてはちょっと難易度が高いかな。とりあえずストリーム中は他の処理も同時に動いているって思ってくれればいいよ。

俺にはまだ早いってか!悔しいぜ!

仕方ないよ。ボクだって非同期の概念を理解するのに結構時間がかかったんだ。そんなすぐに理解されたらボクの方悔しくなるよ。

トモカズでも時間がかかったのか……そりゃ無理ってもんだな(笑)

実はストリームが終わったら処理を開始するって方法もあるんだ。fsの場合はendだけどreadlineの場合はcloseだから気を付けてね。

// text用の配列を準備
var texts = [];

// fsモジュールを変数「fs」に読み込む
var fs = require("fs");
// readlineモジュールを変数「readline」に読み込む
var readline = require("readline");

// stream変数を作り、fsモジュールの「createReadStream」メソッドを使ってテキストファイルをストリーム形式で読み込む
var stream = fs.createReadStream("test.txt", "utf8");

// reader変数を作り、readlineモジュールの「createInterface」メソッドに先ほど作ったストリーム情報を渡す
var reader = readline.createInterface({ input: stream });

// reader変数のonメソッドで1行ずつconsole.logを実行し行末まで繰り返す
reader.on("line", (data) => {
  texts.push(data);
});

reader.on('close', function () {
  // texts配列の長さ分ループする
  for (i=0; i<texts.length; i++) {
    // output.txtに追記していく(ファイルが無ければ新規作成)
    fs.appendFile("output.txt",texts[i]);
  }
});

このコードで書き込めるようになるよ。試してみて。

よし、これでようやく完成だな!?……っておい!なんか改行無しでファイルに書き込まれているぞ!これだと1行ずつ処理してる意味がないじゃないか!

そりゃそうだよ。改行コードを入れていないからね。

改行コードぉ?

リキくん、文章を改行するときってどうやる?

どうって……エンターキーだろ?

それは人間の場合だよね?パソコンはキーボードのエンターキー押せないし、例え内部エンターキーと同じ処理をしたとしても命令されてないから押さないよ?

ぐっ……確かに命令してないな……でもそれぐらいやってくれよなぁ

それは期待するだけ無駄だよ。パソコンは完璧なる指示待ちマシーンだからね。

これだから最近の若いやつは……

何年寄りくさいこと言ってんの……それにリキくんもその若者側でしょ。

で、その改行コードってのは何なんだよ?

プログラミングでの改行コードは大体の言語では「\n」で表すんだ。時々違う時もあるけど大体これだよ。環境によってはバックスラッシュ+nじゃなくて「半角の円マーク+n」ってなってるかもね。

それを行末に入れればいいってことか?

なぜ\nが改行なんだって考えても無駄だからね。意味はちゃんとあるけど基本的にはそういう決まりなんだ。とりあえずやってみてよ。ちなみに文字列同士の結合は+で出来るよ。

// text用の配列を準備
var texts = [];

// fsモジュールを変数「fs」に読み込む
var fs = require("fs");
// readlineモジュールを変数「readline」に読み込む
var readline = require("readline");

// stream変数を作り、fsモジュールの「createReadStream」メソッドを使ってテキストファイルをストリーム形式で読み込む
var stream = fs.createReadStream("test.txt", "utf8");

// reader変数を作り、readlineモジュールの「createInterface」メソッドに先ほど作ったストリーム情報を渡す
var reader = readline.createInterface({ input: stream });

// reader変数のonメソッドで1行ずつconsole.logを実行し行末まで繰り返す
reader.on("line", (data) => {
  texts.push(data);
});

reader.on('close', function () {
  // texts配列の長さ分ループする
  for (i=0; i<texts.length; i++) {
    // output.txtに追記していく(ファイルが無ければ新規作成)
    fs.appendFile("output.txt",texts[i]+"\n");
  }
});

おぉ、できたぞ!ちゃんと改行されてる!

これでリキくんのやりたいことは全部そろったね。あとは自分の好きなように改変していけばいいよ。

数日後……

プログラムが完成した!

よートモカズ!

やあリキくん。どうだった?

あぁ、完璧だったぜ!ちょっと組み方がわからなくてブサイクなコードになっちまったがちゃんと動いたぜ!
しかも先輩にもこの短期間でよくできたなって褒められたぜ!

それはよかった。非効率な書き方になるのは仕方ないよ。理解したら後からきれいにすればいいんだし。それにプログラムの書き方に正解がないのもプログラミングの醍醐味の一つだよ。

ありがとな!マジで助かったぜ!……ただ

ただ?

速攻で仕事が終わったからメチャクチャ仕事量増やされちまったよ……

ハハ……そりゃ会社はお金払ってるから遊ばせるわけにはいかないよ(笑)
仕事いっぱいこなせば信頼されて給料があがるかもしれないよ?

そうだな……またプログラム書いて一瞬で終わらせてやるか!

その意気だよ!またわからないことがあったら聞いてよ。答えられる範囲では答えるからさ。

あぁ、とりあえず自力でやってみるが分からなくなったらよろしく頼むぜ!

これでリキくんのやりたいことは出来るようになりました。

プログラミングの入門の入門はココまでです。

後半はちょっと難易度が高い非同期処理などもありましたが、
プログラミングをやっていれば必ずぶち当たる壁です。

次回はもうちょっと難易度を上げたプログラミング講座を作りますので、
よければ見てやってください。

それでは良きプログラミングライフを。