まずはJavaScriptの文法や演算子、ビルトインオブジェクト(最初から用意されているオブジェクトのこと)を学ぼう。
ここではデータを絞り込んで加工して集計する例を1つずつ実装して学んでいく。
元となるデータは次のサービスで作成できる擬似個人情報を使用する。
チェックボックスはそのままで、生成する件数だけを1000件ぐらいに変更して「生成開始」をクリックしよう。
疑似個人情報が生成されるので、画面の一番したまでスクロールしてCSV(文字コード:UTF-8)でダウンロードしよう。
personal_infomation.csvというファイル名でダウンロードされるはず。
コマンドプロンプトを開いてpersonal_infomation.csvが置いてあるディレクトリに移動しよう。
次のコマンドを実行するとNode.jsのREPLが起動する。
(JavaScriptに限らず対話型の実行環境のことをREPLと呼ぶ。JavaにもJShellというREPLがある)
nodeこれで準備はOK。
まずは事前準備した疑似個人情報が書かれたファイルを読み込んでみよう。
Node.jsからファイル操作を行うAPIが提供されているのでそれを使う。
起動しているREPLに次のコードを打ち込んでEnterを押してみよう。
fs.readFileSync('personal_infomation.csv', 'UTF-8');コンソールにファイルの内容が表示されるはず。
REPLは実行した結果(戻り値)をコンソールに表示する。
この場合、fs.readFileSyncメソッドが読み込んだファイルの内容を戻り値として返している。
この先何度もファイルの内容を扱うので変数に代入しておこう。
REPLに次のコードを打ち込んでEnterを押してみよう。
const data = fs.readFileSync('personal_infomation.csv', 'UTF-8');実はJavaScriptには変数の種類がいくつかある。
const、let、varだ。
constは再代入不可、letは再代入可能となっている。
私の経験的に1つの変数を使いまわすとバグを生みやすい。
それを防ぐためになるべくconstを使うようにしよう。
もちろん、どうしても再代入をする必要がある場合はletを使ってよい。
しかしletを使う前に必ずconstを使えないか考えてほしい。
最後にvarについてだけど、これは使わない。
昔のJavaScriptは変数宣言をする手段はvarしかなかったけど、今はconstとletがある。
なので、昔のシステムの保守や改修をするときに見かけることもあるだろうけれど、このチュートリアルでは使わない。
| 種類 | モダン | 再代入 | スコープ | 使用 |
|---|---|---|---|---|
const |
モダン | 再代入不可 | ブロック | 推奨 |
let |
モダン | 再代入可能 | ブロック | 使ってもよい |
var |
レガシー | 再代入可能 | 関数 | 使わない |
さて、さきほどファイルの内容を変数に代入したけれど、1つの文字列になっている。
これだと少し扱いにくいので、行ごとに分けて配列形式で持つようにしよう。
文字列を分割するにはsplitメソッドが使える。
const lines = data.split('\r\n');変数の中身を単に見たい場合はREPLに変数を打ち込んでEnterを押せばよい。
試しにlinesの中身を見てみよう。
ファイルの内容を行ごとに分割してlinesに代入した。
今度はlinesを1行ずつ見て名前に「上」を含むものだけに絞り込もう。
文字列が特定の文字列を含むかどうかはindexOfを使って判定できる。
//対象文字列が見つかったら頭のインデックスを返す
'ABCDEFG'.indexOf('DEF') //3
//見つからなければ-1を返す
'ABCDEFG'.indexOf('XYZ') //-1それでは配列であるlinesの要素1つずつにindexOfを実行して絞り込むにはどうすればよいだろうか。
配列には要素を絞り込むためのfilterというメソッドが用意されている。
次のコードで名前に「上」を含む人物に絞り込める。
lines.filter(x => x.indexOf('上') !== -1);filterは関数を受け取り、絞り込んだ結果を新しい配列として返すが、この例でfilterが受け取っている関数だけ抜き出したコードはこれ。
x => x.indexOf('上') !== -1この文法はアロー関数という。
=>の左辺で引数を宣言して右辺が関数の本体になっている(ちなみに=>が矢(arrow)のように見えるからアロー関数という)。
この例では本体が1行しかないのでいくつか省略しているものがある。
まずはブロックの括弧だ。
関数本体が複数行になる場合は本体を{と}で囲む必要がある(もちろん1行しかなくてもブロックにしてもよい)。
それからreturnも省略されている。
関数本体が1行しかないときは最後に評価された式の結果が戻り値となる。
この1行で書かれたアロー関数の例にあえてブロックとreturnを書くとこうなる。
x => {
return x.indexOf('上') !== -1;
}つまりこのアロー関数は「1つの引数を取って、その引数に『上』が含まれていればtrueを、含まれていなければfalesを返す関数」というわけ。
ちなみに関数を作る方法にはfunction式というものもある。
これはvarと同じく古いJavaScriptの名残だと思ってよいだろう。
私の感覚ではfunction式がなくてもコードは書けるし、アロー関数の方がシンプルで好きだ。
一応、function式で例を書き換えたコードも示しておく。
//function式はブロックの括弧もreturnも省略できない
function (x) {
return x.indexOf('上') !== -1;
}さて、話をfilterに戻そう。
filterは配列の要素1つずつに対して、引数で渡された関数を適用して、戻り値がtrueのものだけに絞り込んだ新しい配列を返す。
filterの概念を図で表すと次のような感じ。
TODO 図を入れる。
エアコンにはフィルターが取り付けられていてゴミを取り除く役割がある。
filterもそれと似た機能で、(ゴミと呼ぶのは気が引けるけど)不要な要素を取り除く。
いくつかfilterのサンプルを示そう。
[1, 2, 3, 4].filter(x => x % 2 == 0) //[2, 4]
[true, false].filter(x => x) //[true]
['foo', 'bar', 'baz'].filter(x => x[0] === 'b') //['bar', 'baz']次に行く前に絞り込んだ結果を新たな変数に代入しておこう。
const filtered = lines.filter(x => x.indexOf('上') !== -1);今は1行のデータに名前や性別、生年月日が含まれている。 この中から性別だけを抜き出して新しい配列を作ろう。
データはCSV形式なので性別を抜き出すには,で分割して4番目の項目を取ってくればよい。
分割と抜き出しを配列が持つすべての要素に適用するにはmapメソッドを使う。
filtered.map(x => x.split(',')[3]);今回もアロー関数だけを抜き出してみよう。
x => x.split(',')[3]このアロー関数は「1つの引数を取って、その引数を,で分割して4番目の項目(配列は0オリジンなのでインデックスは3を指定)を返す関数」だ。
これをmapメソッドに渡してfiltered配列のすべての要素に適用して、戻り値からなる新しい配列を返している。
mapの概念を図で表すと次のような感じ。
TODO 図を入れる。
データベース上のカラムとJavaのエンティティクラスのプロパティを対応付けるようなことをマッピングと呼ぶ場合がある。
mapは元の配列と、戻り値として返される新しい配列の要素を関数によってマッピングする。
いくつかmapのサンプルを示そう。
[1, 2, 3, 4].map(x => x * 2) //[2, 4, 6, 8]
[true, false].map(x => x === false) //[false, true]
['foo', 'bar', 'baz'].map(x => x.toUpperCase()) //['FOO', 'BAR', 'BAZ']次に行く前に絞り込んだ結果を新たな変数に代入しておこう。
const mapped = filtered.map(x => x.split(',')[3]);性別の配列から男性・女性それぞれ何人なのか集計したい。
集計にはreduceが使えるが、コードが煩雑になってしまうのを防ぐために先回りして集計用のクラスを作っておこう。
男性(male)の人数を保持するフィールドと女性(female)の人数を保持するフィールドを持たせる。
それから'男'または'女'という文字列を受け取って人数をカウントアップするメソッドを持たせる。
このメソッドは新たなCounterインスタンスを返すイミュータブルな設計にしてある。
次のコードをREPLに打ち込んでEnterを押そう。
コード量もちょっと多いしコピペしてもよい。
class Counter {
constructor(m, f) {
this.m = m;
this.f = f;
}
add(x) {
if(x === '男'){
return new Counter(this.m + 1, this.f);
}
return new Counter(this.m, this.f + 1);
}
static empty() {
return new Counter(0, 0);
}
}クラスの宣言はclassキーワードにクラス名を続けて行う。
この例だとクラス名はCounterだ。
コンストラクタはconstructorキーワードで定義する。
例からコンストラクタを抜粋したものがこれ。
constructor(m, f) {
this.m = m;
this.f = f;
}このコンストラクタでは2つの引数mとfを受け取って、それぞれフィールドmとfに代入している。
次にメソッドだけど、この例ではaddというメソッドを定義している。
add(x) {
if(x === '男'){
return new Counter(this.m + 1, this.f);
}
return new Counter(this.m, this.f + 1);
}addメソッドは1つの引数xを受け取っている。
xが'男'ならフィールドmに+1した値とフィールドfを渡して新しいCounterインスタンスを作って返している。
xが'男'でなければ(今回はxが必ず'男'か'女'のどちらかだという前提)フィールドmとフィールドfに+1した値を渡して新しいCounterインスタンスを作って返している。
最後にstaticメソッドを定義している。
static empty() {
return new Counter(0, 0);
}メソッド定義をするときメソッド名の前にstaticキーワードを付けるとstaticメソッドとなる。
このメソッドはクラスをレシーバとして呼び出す。
//staticメソッドを呼び出す例
Counter.empty();さて、集計用のクラスも作ったし、今後こそ集計しよう。
集計もfilterやmapと同じように配列が持つメソッドを使う。
使うメソッドはreduceだ。
次のコードで男女別に人数を集計できる。
mapped.reduce((counter, x) => counter.add(x), Counter.empty())ちょっとこのメソッドはfilterやmapよりは分かりにくいけれど、少しずつ理解しよう。
reduceは2つの引数を取っている。
1つめは関数、2つめは初期値だ。
//関数
(counter, x) => counter.add(x)
//初期値
Counter.empty()初期値は男女共に0のカウンターだということは分かるよね。
分からなければemptyメソッドの定義をもう一度見てみよう。
関数は2つの引数、counterとxを取ってcounterにxをaddして取得した戻り値を、返している。
この関数はmapped配列のすべての要素に対して呼び出され、引数xには配列の要素が渡されるが、これはfilterやmapと同じ動作だ。
特徴的なのは引数counterだ。
reduceが配列の先頭の要素を処理するとき、引数counterには「reduceの第2引数で渡されたカウンター」が渡される。
reduceが2番目の要素(インデックスは1)を処理するとき、引数counterには「先頭の要素を処理した結果(つまり先頭の要素でcounter.add(x)した戻り値)」が渡される。
reduceが3番目の要素(インデックスは2)を処理するとき、引数counterには「2番目の要素(インデックスは1)を処理した結果」が渡される。
このように、reduceは初期値またはその時点での計算結果と配列の要素を受け取って、新たな計算結果を返すことを配列の要素ぶん繰り返す処理と言える。
reduceの概念を図で表すと次のような感じ。
TODO 図を入れる。
reduceの初期値・途中の計算結果(この例だとcounter)のことを一般的には「アキュムレータ」と呼ぶ。
reduceの動作は「畳み込み」と呼ばれることもある。
配列が持つ複数の要素を1つのアキュムレータに畳み込む、というと納得してもらえるかな。
まあ納得できなくても、reduceを調べると「アキュムレータ」や「畳み込み」という言葉が出てくることもある、と思っておいて。
いくつかreduceのサンプルを示そう。
[1, 2, 3, 4].reduce((acc, x) => acc + x, 0) //10
[true, false].reduce((acc, x) => acc && x, true) //false
['foo', 'bar', 'baz'].reduce((acc, x) => `${acc}${x}`, '') //'foobarbaz'ここまで終えると1000件以上の疑似個人情報から「名前に『上』を含む人物の性別ごとの人数」を得られた。
せっかくなので集計結果をファイルに書き出そう。 更に、ここまでのコードを1つのjsファイルに書いてREPLではなくてスクリプトとして実行してみよう。
次のコードをcount.jsという名前のファイルに保存しよう。
保存する場所はREPLを開いている場所、つまり疑似個人情報のCSVファイルが置かれているディレクトリだ。
const fs = require('fs');
const data = fs.readFileSync('personal_infomation.csv', 'UTF-8');
const lines = data.split('\r\n');
const filtered = lines.filter(x => x.indexOf('上') !== -1);
const mapped = filtered.map(x => x.split(',')[3]);
class Counter {
constructor(m, f) {
this.m = m;
this.f = f;
}
add(x) {
if(x === '男'){
return new Counter(this.m + 1, this.f);
}
return new Counter(this.m, this.f + 1);
}
static empty() {
return new Counter(0, 0);
}
}
const reduced = mapped.reduce((counter, x) => counter.add(x), Counter.empty());
fs.writeFileSync('count.txt', `男性:${reduced.m}人、女性:${reduced.f}人`, 'UTF-8');1行目と最終行が新しく追加されたコードになる。
//1行目のコード
const fs = require('fs');
//最終行のコード
fs.writeFileSync('count.txt', `男性:${reduced.m}人、女性:${reduced.f}人`, 'UTF-8');1行目のコードはfsモジュールを使えるようにするための宣言だ。
REPLではデフォルトで使えるようになっていたのでこのコードが不要だった。
スクリプト実行するときは必要になる。
それから最終行はファイルを書き出しているコードだ。
さあ、次のコマンドでこのスクリプトを実行しよう。
集計結果がcount.txtに書き出される。
node count.js日常でもちょっとファイルを編集したり集計したいときがあるけれど、そういったときに使えるツールの1つにJavaScriptが加わったんじゃないだろうか。
今回作った集計コードにはいくつか注意点がある。
- 疑似個人情報ファイルの内容を一度にすべて読み込んで処理をしているのでメモリに優しくはない
- 読み書きするファイル名がスクリプトにハードコーディングされているのでポータビリティは低い
次のステップとして、これらの注意点を解消するような改善をしてみるのもよさそうだ。