Posted at
D言語Day 12

Rubyで書いたクローラーをD言語で書き直した話

More than 3 years have passed since last update.

D言語 Advent Calendar 2014の12日目の記事です。(元々の予告と内容が変わってます)

RubyConfにいた(少なくとも)3人のD言語プログラマの一人です。元々の予告通り今年からD言語使い始めた程度であまり難しいことは書けないので他言語からの移植+Dでよかった、みたいな内容で書こうと思います。


お題について

さて、もともとはC言語で書いたHTTPサーバをDで移植してみた、というのをやろうと思っていたのですが、POSIXシグナルを扱うcore.sys.posix.signalとかがガッツリCの構造体使ってたりして、システムコールをD言語で書いてどう嬉しいのかがよくわからなかった(文字列処理とかが圧倒的に楽なのは間違いない)、D言語的にいい書き方がわからなかった、ので、並列処理とプロセス実行の部分に絞って書くことにします。タイトルにある通り、Rubyで書いたクローラーをD言語で移植してみました。


コードはこんな感じ

import std.stdio, std.algorithm, std.parallelism, std.range, std.process, std.file, std.json;

void main() {
string urlFile = readText("url.json");
auto json = parseJSON(urlFile);
auto urls = json.array.map!toUrlArray;

auto results = taskPool.amap!callCurl(urls, 2);
foreach(string[] r; results){
writef("URL %s : %s, %s secs\n",r[2],r[0],r[1]);
}
}

string[] callCurl(string url){
string status, time;
auto curl = executeShell("curl -s -o /dev/null -w \"%{http_code} %{time_total}\" \"" ~ url ~"\"");
if (curl.status != 0) {
status = "0";
time = "-1";
} else {
auto output = curl.output;
auto splitOutput = output.split(" ");
status = splitOutput[0];
time = splitOutput[1];
}
return [status, time, url];
}

string toUrlArray(JSONValue j){
return j.object["url"].str;
}


url.json

[

{ "name" : "Google", "url" : "http://www.google.co.jp/"},
{ "name" : "Apple", "url" : "http://www.apple.com/"},
{ "name" : "GitHub", "url" : "https://github.com/"},
{ "name" : "D Programming Language", "url" : "http://dlang.org/"}
]


解説


何をするもの

JSONで記されたURLの一覧に対してHTTPステータスと応答時間を調べるものです。なんでこんなもの作ったかというと前の記事がヒントになるんではないでしょうか(白目)


1. JSONのパース、URL一覧の取り出し

std.jsonモジュールのparseJSONstring型からJSONValue型に変換します。JSONオブジェクトの配列が得られるので、これを.arrayで取り出してmapにかけます。map先ではキー"url"に対するstrを取るので、結果としてURLの配列が得られます。


2. URL一覧に対して並列mapでcurlを実行

std.parallelismモジュールで定義されるtaskPoolamapメソッドで並列にcallCurl関数を適用します。amapの第2引数には同時に最大何個のタスクを実行するかを指定します。現在はURL一覧が小さいので2ですが、実際は4とか8とかで走らせています。

callCurl関数の中ではexecuteShell関数でシェルを通してコマンドを呼び出します。返り値で実行ステータスと出力結果を持ったタプルが返ってくるので、出力結果からHTTPステータスコードと応答時間を取り出して配列として返します。

あとは出力するだけ。ほんとはJSONで出力したかったけどstd.jsonのJSON出力が使いにくいのでとりあえずここまで。


各種考察


並列mapの比較

並列mapにはmapamapとがあって、前者はlazyに実行され、後者はeagerに実行される、という違いがあります。今回の例でいうと、mapの場合実行が終わるごとに結果が標準出力に書き出され、amapの場合は全ての実行が終わったあとに一度に標準出力に書き出されます。

このユーズケースであればamapのほうがよさそうで、その理由としては最終的にデータをJSONとして出力する際には変換元が終端のわかっている配列であることが望ましいからです(今回最終出力は妥協してるんですけどね)。

また、amapはmap操作を行う対象のrangeがランダムアクセス可能である必要があり、mapはそうでなくても行えるので、データをストリームとして処理する場合はmapで行うことになります。


外部プロセス呼び出しの比較

外部プロセスの実行には


  • spawn

  • pipe

  • execute

の接頭語がつく3つのメソッドが、プログラムの直接実行(Process: executeだけつかない)とシェルコマンドの実行(Shell)について存在します。

spawnProcessspawnShellはプロセスを実行してプロセスIDを返します。プロセスの実行をwaitする必要があります。

pipeProcesspipeShellは上記と同様waitする必要のあるプロセス実行ですが、出力のリダイレクトを行うことができます。waitする対象は返り値のProcessPipesオブジェクトのpid、リダイレクトされたストリームはProcessPipesオブジェクトを通してアクセスできます。

executeexecuteShellはプロセスを実行しwaitまで行ってくれます。


移植での比較:プロセス実行に関して

さすがDはシステムプログラミング言語だけあって、外部プロセス実行のオプションが豊富に用意されていたので拡張もかなり容易そうです。元のRubyのクローラーでは「あまりに応答が遅い場合一定のタイムアウト時間で強制的に打ち切る」というものを用意していたのですが、「実行したプロセスのプロセスIDと標準出力を両方取る」方法に至るまでにかなり時間がかかりました(Open3を使ったのですがこれは気力があったら別記事で)。こちらの場合だとpipeShellで走らせればpidも標準出力も取れますしそれを元に待ち時間を管理してkillこともできます。

あとC言語でシステムプログラミングしたときのことを思い出すならば、scope(exit)waitできるのは死ぬほど便利だなと思いました。