Node.js本のサンプルをCommon Lispで書く

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック
- 作者: クジラ飛行机
- 出版社/メーカー: ソシム
- 発売日: 2015/08/31
- メディア: 単行本
- この商品を含むブログ (2件) を見る
こんな本を買いました。タイトルを見てもピンと来なかったのですが、目次やレビューを見ると新し目の技術を広く浅く見るのに良さそうと感じた次第です。
当然サンプルコードはJavaScriptで書かれているわけですが、そのまま書き写しても面白くないので、前回記事で作った環境上を使ってCommon Lispで書いてみることにしました。Parenscriptを利用して、Common Lispで書いたコードをJavaScriptコードに変換する方針です。
結果としては、次の手順で作れるようになりました。単一ファイルのスクリプトのみ対応済みです。
init.ros
(Roswellスクリプト)でテンプレートからlispファイルを作成- プログラム本体や(Node.jsの)依存ライブラリを記述
run.ros
でJavaScriptへ変換&実行
テンプレート:skelton/skelton.lisp
次のようなテンプレートを用意しました。
<% @var name %>
などと書いているのはcl-emb(紹介記事「HTML内にコードを埋め込めるテンプレートエンジン「CL-EMB」 - Qiita」)によるコード埋め込み書式です。
大体次のような感じで使います。
- npm installしたい依存ライブラリは
*dependencies*
に書く(例.'("cheerio-httpcli" "request")
) - casperjsコマンドなど、nodeコマンド以外から実行したい場合は、
*executor*
を書き換える - メインの処理は
with-use-ps-pack
内に、その他関数などは特定の書式でトップレベルに書く
一行目でしれっとps-experiment
なるものをquickload
していますが、自作のParenscript拡張です。quicklispリポジトリには入っていないので、適切なフォルダ下(前回記事の手順であれば~/.roswell/local-projects/
下)にgit cloneしておく必要があります。
どんな感じに使えるのかは記事後半のサンプル参照。
テンプレートからlispファイルを生成:init.ros
上記のテンプレートからlispファイルを生成します。
引数処理が長々しいですが、処理としてはcl-embによる変換を噛ませている程度で、ほぼテンプレートのコピーをしているだけです。次のような感じで使います。この例であれば、part02/download-node.lisp
を編集して必要な処理を書いていきます。
$ ./init.ros <ディレクトリ名> <プログラム名> # $ (例)./init.ros part02 download-node # Create part02/download-node.lisp
一応の事故防止処置として、存在しないディレクトリを指定した場合や、既存のファイル名と被る場合はエラーにしています。
lispファイルをjsファイルに変換して実行:run.ros
編集が終わったら実行です。
長々と処理していますが、やりたいことはmain
関数の最後の数行です。
init.ros
で生成 & 手で編集したlispファイルをロードし(load
)- JavaScriptファイルをWORKディレクトリ下に書き出し(
output-js
) - 未インストールの依存npmパッケージをインストールし(
install-by-pm
) - node.jsスクリプトとして実行します(
run-shell
)
3のインストールチェックですが、直下のnode_modules
フォルダに該当する名前のフォルダがあるかどうかだけを見る超簡易判定です。npm install
自体にインストールスキップ機能がないか調べましたが、見つからなかったのでこんな感じにしました。
package.jsonにリポジトリ内で使うライブラリを全てまとめる…というのが普通のやり方のような気がしますが、たかがスクリプトなのでql:quickload
のように気軽に書きたかった感じです。
使用例
少し長めですが、関数作成とnpmモジュールの利用の2つが揃ったものとしては一番短い例を載せます。Wikipedia日本語版のハムスター*1の記事から画像をダウンロードするコードです。
ここで、ps-experiment
の目立った機能としてはdef~.ps
系マクロとwith-use-pack
マクロがあります。def~.ps
系マクロはトップレベルでParenscript用の関数やら変数やら(あと構造体)を定義するためのものです*2。
これらの定義を含めてJavaScriptコードを吐き出すのがwith-use-pack
の役割です。第一引数のリストで定義を読み込むパッケージを指定します(:this
は自パッケージのエイリアス)。スクリプトレベルだと関係ありませんが、use-package
の関係にあるパッケージは依存関係と見て再帰的に定義を読み込みます*3。なお、パッケージマネージャもどきなので名前空間を分けてくれたりはしません。
他細かい点。
--
はps:chain
の別名です。安易にこういう別名を提供するのは好きではないですが、chain
が余りにも良く出てくるので堪え切れませんでした…*4。インデントにもろに効くのに5文字は長い- ドット記法は内部的に@記法?に変換される("a.b.c" → "(ps:@ a b c)")ので安心です。JavaScriptへの変換時にドット記法に戻るため一見無駄に見えますが、
ps:with-slots
などとの兼ね合いで必要です - URLエンコードが少々曲者です。マルチバイト文字列はParenscriptがユニコードエスケープ形式に変換する(してしまう)ので、先にURLエンコードしています(
url-encode
マクロ)
最後に、生成されるコードは以下のようになります。生成後の方が短いとかいうのは野暮です。こういったスクリプトレベルでは、Lispで書けるという以上の利点は薄いかもしれません。
var CLIENT = require('cheerio-httpcli'); var REQUEST = require('request'); var FS = require('fs'); var URL = require('url'); var SAVEDIR = __dirname + '/img'; function ensureSavedir() { return !FS.existsSync(SAVEDIR) ? FS.mkdirSync(SAVEDIR) : null; }; function downloadImages() { var url = 'https://ja.wikipedia.org/wiki/' + '%E3%83%8F%E3%83%A0%E3%82%B9%E3%82%BF%E3%83%BC'; var param = { }; return CLIENT.fetch(url, param, function (err, $, res) { if (err) { console.log('error'); console.log(err); return null; }; return $('img').each(function (idx) { var src = URL.resolve(url, $(this).attr('src')); var fileName = URL.parse(src).pathname; var filePath = SAVEDIR + '/' + fileName.replace(/[^a-zA-Z0-9\.]+/g, '_'); return REQUEST(src).pipe(FS.createWriteStream(filePath)); }); }); }; ensureSavedir(); downloadImages();
こういったものは、Clojure Script擁するClojureの方が良いのかもしれないですね。そちらも調べておきたいです。
*1:本ではネコだった気がしますが気のせいです
*2:ちなみに、「def~.ps+」系のマクロを利用すると、Parenscript用定義とCommon Lisp用定義を同時にできます
*3:今は循環参照のチェックをサボっているので、循環参照させると無限ループします… → 2016/02/16 修正済み
*4:ハイフン2つでchain=鎖のイメージです。ClojureのJava関数のチェインで".."が使われているのでなぞるべきだと思ったのですが、Common Lispではエスケープが必要になるので断念…。