bacon.jsで関数型プログラミングをやってみる(1)
ではbacon.jsをなんとなく試してみました。
しかし、実際にはもっと複雑な処理があったり、他の(関数型プログラミングでない)ライブラリを使いたかったり等、のっぴきならない事情が多々あることでしょう。
関数型プログラミング vs オブジェクト指向 みたいな対立を煽る文章もありますが、bacon.jsではその橋渡しをするような機構がありますので、なんとなくいいとこ取りですすめていこうと思います。
今回の目標
urlのリストを元にスクレイピングし、その中身から情報を取り出す。
準備
今回の実行環境はnode.jsにスクレイピングライブラリとして、cheerio-httpcliをつかっていきたいと思います。もちろん、bacon.jsも使います。
node.jsの導入等については割愛いたします。
mkdir sample_scraping
cd sample_scraping
npm install baconjs
npm install cheerio-httpcli
これでライブラリの準備はできました。
実行するjsファイルも準備します。
//ライブラリ読み込み
var Bacon = require('baconjs');
var client = require('cheerio-httpcli');
// qiitaのadvent-calendarを対象にしてみる
var list = [
'http://qiita.com/advent-calendar/2011',
'http://qiita.com/advent-calendar/2012',
'http://qiita.com/advent-calendar/2013',
'http://qiita.com/advent-calendar/2014',
];
//baconjsの出番
var stream = Bacon.fromArray(list);
stream
.log();//とりあえずログ出力
とここまででurlのlistを元にBacon.fromArrayで準備するところまできました。ここで一旦実行結果を確認してみましょう。
node main.js
http://qiita.com/advent-calendar/2011
http://qiita.com/advent-calendar/2012
http://qiita.com/advent-calendar/2013
http://qiita.com/advent-calendar/2014
<end>
。。。はい、そのまま出力されているだけです。
ここから、このstreamに対して、mapやらなんやらの処理をしていくわけですが、webサーバへのスクレイピングは下手にやると相手のサーバを攻撃しているも同然の状態になってしまいます。説明の都合で最後になってしまいますが、適度に間隔をあけて相手サーバに過度な負荷がかからないように注意しつつ、試していきたいと思います。
データのフロー
ここで、今回のデータのフローを考え、関数を作っていきたいと思います。
今回、与えられたデータは上記のようなurlの集合のリストになります。
そこから、urlの内容を取得して、さらにその中身の情報(今回は各ページのtitleタグの中身とします)を得ることです。
- urlの集合
- urlの中身を取得
- そこから目的の情報を取得
- 表示
1のurlの集合は用意されているので、2と3の処理を関数として作っていきます。
スクレイピング
cheerio-httpcliの使い方としては、おおまかに以下のようになります。(公式のサンプルより)
var client = require('cheerio-httpcli');
// Googleで「node.js」について検索する。
client.fetch('http://www.google.com/search', { q: 'node.js' }, function (err, $, res) {
// レスポンスヘッダを参照
console.log(res.headers);
// HTMLタイトルを表示
console.log($('title').text());
// リンク一覧を表示
$('a').each(function (idx) {
console.log($(this).attr('href'));
});
});
jsによくあるcallbackで処理を記述していくタイプです。
また、レスポンス内のエレメントを取得するのにjQueryっぽくかけて非常に便利なのですが、その処理部分もcallbackでの記述になっています。
このあたりがデータを流れで処理していきたいbacon.jsにとっては鬼門になります。
でも安心してください。bacon.jsにはこれらをちゃんと扱えるような仕組みが備わっています。
それは、、、
Bacon.fromCallback
とりあえずこれがあれば、従来のcallbackベースのライブラリもFRPの世界に溶け込ますことができます。使い方は以下を参考にしてみてください。
それでは、urlからhtmlを取得する関数を作っていきます。
var fetchFromUrl = function (url) {
return Bacon.fromCallback(function (callback) {
client.fetch(url, {}, function (err, $, res) {
callback($);
});
});
};
なかなかの入れ子構造になってしまいますが、callbackをreturnのようにすることができるんです。
これで
2.urlの中身を取得
の関数になります。
[INPUT] が url で [OUTPUT] が cheerio-httpcli のコールバック関数にある $ という変数(具体的にはDOMがつまってる)です。
続いて、
3.そこから目的の情報を取得
をつくります。
var getTitle = function ($) {
return $('title').text();
};
はい、タイトルタグのtext要素を返す関数です。引数として先ほどcallbackに設定したcheerio-httpcliからきてる$が流れてきます。
そして出力へ
これで処理に必要なパーツは揃いました。あとはそれらをつなげて処理できるようにしましょう。
var stream = Bacon.fromArray(list);
stream
.flatMap(fetchFromUrl)
.map(getTitle)
.log();//とりあえずログ出力
これで用意されたurlの集合がなんやかんやでタイトルの集合になってlog()で出力される流れとなりました。
flatMap? map? と思うでしょうが、とりあえずは
関数で普通にreturnしてたらmap、callback使ったらflatMap
ということでお願いします。。
ここで、いざ実行、とすることもできるのですが、序盤にあったとおり、スクレイピングをする際は適度に間隔をあけてやる必要があります。どこかにsetTimeoutをいれてタイミングを調整、、、しなくても良いのがbacon.jsの良いところです。
データは流れだと説明しました。bacon.jsにはその流れを一時的に堰き止めて指定したタイミングで流す、という仕組みがあります。それが、
bufferingThrottle
というかっこいい名前の仕組みです。これはデータの流れの途中に差し込むことで、「流れを一時的に堰き止めて指定したタイミングで流す」ことができます。(Bacon.sequentiallyというものもあるようです)
これをつかって、各urlリクエストを1秒に1回にします。
var stream = Bacon.fromArray(list);
stream
.bufferingThrottle(1000)
.flatMap(fetchFromUrl)
.map(getTitle)
.log();//とりあえずログ出力
1行追加するだけでタイミングを変えられました。
最終的に以下のようになりました。
//ライブラリ読み込み
var Bacon = require('baconjs');
var client = require('cheerio-httpcli');
// qiitaのadvent-calendarを対象にしてみる
var list = [
'http://qiita.com/advent-calendar/2011',
'http://qiita.com/advent-calendar/2012',
'http://qiita.com/advent-calendar/2013',
'http://qiita.com/advent-calendar/2014',
];
var fetchFromUrl = function (url) {
return Bacon.fromCallback(function (callback) {
client.fetch(url, {}, function (err, $, res) {
callback($);
});
});
};
var getTitle = function ($) {
return $('title').text();
};
//baconjsの出番
var stream = Bacon.fromArray(list);
stream
.bufferingThrottle(1000)
.flatMap(fetchFromUrl)
.map(getTitle)
.log();//とりあえずログ出力
実行結果はこのようなかんじ。
node main.js
2011年のAdvent Calendar一覧 - Qiita
2012年のAdvent Calendar一覧 - Qiita
2013年のAdvent Calendar一覧 - Qiita
2014年のAdvent Calendar一覧 - Qiita
<end>
ここまでのまとめ
関数型プログラミングという新しい書き方でプログラムを組むのに、従来のcallback中心のライブラリもうまく活用して組むことができました。配列、というとforやforeachで処理したくなるところですが、データを集合と考えて纏まりのまま流していく、とすることで、途中に別の処理を挟んだりすることも比較的簡単にすることができました。
このあたりが関数型プログラミングの魅力かと思います。