非同期処理でWebコンテンツをダウンロードする方法3 - node.js, C#

今回は、Node.js と Async CTP を使った C# で実装してみました。

  • node.js
  • C# : Async CTP

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20111030/

node.js の場合

以前は Windows で node.js を使用するのに Cygwin を使ってビルドするなど手間がかかりましたが、今では公式サイトから Windows 用の実行ファイル node.exe をダウンロードできるので、簡単に使えるようになりました。

今回の実装における注意点としては、レスポンスのエンコード設定やファイルの出力エンコードで "binary" を指定する点と URL モジュールの parse で得られた値を http.get に使うにはちょっとした加工が必要な点です。(parse ではパス部分を pathname に設定するが、http.get では path に設定しておく必要がある)

Webコンテンツダウンロード時の動作は、data のコールバックが複数回実施され、最後に end のコールバックが実施されます。

なお、標準入力を行毎に処理するのに本来であれば Readline モジュールを使えるはずですが、今回はエラーが発生して上手く動作しなかったので split と forEach で代替しました。

async_download_web.js
var http = require('http');
var url = require('url');
var fs = require('fs');
var path = require('path');

var dir = process.argv[2];

//エラーメッセージの出力
var printError = function(urlString, error) {
    console.log('failed: ' + urlString + ', ' + error.message);
}

process.stdin.resume();

process.stdin.on('data', function(urls) {
    urls.toString().trim().split('\n').forEach(function(u) {

        var trgUrl = url.parse(u);
        //http.get に必要なパラメータ設定追加
        trgUrl.path = trgUrl.pathname;

        //URL 接続
        http.get(trgUrl, function(res) {
            //バイナリ指定
            res.setEncoding('binary');
            var buf = '';

            //データダウンロード
            res.on('data', function(chunk) {
                buf += chunk;
            });

            //ダウンロード完了時の処理
            res.on('end', function() {
                var filePath = path.join(dir, path.basename(trgUrl.path));

                //ファイル出力
                fs.writeFile(filePath, buf, 'binary', function(err) {
                    if (err) {
                        printError(u, err);
                    }
                    else {
                        console.log('downloaded: ' + u + ' => ' + filePath);
                    }
                });
            });

            //接続後のエラー処理
            res.on('close', function(err) {
                if (err) {
                    printError(u, err);
                }
            });

        }).on('error', function(err) {
            printError(u, err);
        });
    });
});

C# の場合2 : Async CTP

別の PC に Microsoft Visual Studio Async CTP をインストールしてみたところ、インストールに成功したので、その PC から AsyncCtpLibrary.dll をコピーしてきて使いました。(下記サンプルなら AsyncCtpLibrary.dll があればよい)

ちなみに Async CTP は、C# 5.0 で導入される async や await 等の機能を C# 4.0 上で使えるようにするためのものです。

ビルド例(カレントディレクトリに AsyncCtpLibrary.dll を配置)
> csc /r:AsyncCtpLibrary.dll AsyncDownloadWeb.cs

Async CTP は F# の非同期ワークフローによく似ています。
非同期ワークフローの async ブロックの代わりに async を付けたメソッドやラムダ式を、let! や do! のような ! の代わりに await を使用します。
また、非同期ワークフローにおける AsyncXXX と同様に xxxAsync や xxxTaskAsync が拡張メソッドで追加されます。

下記サンプルでは、ダウンロード処理全体を async のラムダ式として定義し、DownloadDataTaskAsync でデータ取得、WriteAsync でファイルへの書き込みを非同期的に実施しています。(ちなみに async のラムダ式内で return を明示する必要はありません)

さらに、LINQ を使って全 URL に非同期ダウンロード処理を適用し、その結果(Task のリスト)を処理待ちに使っています。

AsyncDownloadWeb.cs
using System;
using System.Linq;
using System.IO;
using System.Net;
using System.Threading.Tasks;

public class AsyncDownloadWeb
{
    public static void Main(string[] args)
    {
        var urls = Console.In.ReadToEnd().Split(new string[]{Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries);

        var dir = args[0];

        //非同期ダウンロード処理
        Func<string, Task> download = async (url) => {
            var wc = new WebClient();
            var uri = new Uri(url);
            var fileName = Path.Combine(dir, Path.GetFileName(url));

            try {
                //Webコンテンツダウンロード(バイト配列で取得)
                byte[] buf = await wc.DownloadDataTaskAsync(uri);

                using (var fs = new FileStream(fileName, FileMode.Create))
                {
                    //ファイルへの書き込み
                    await fs.WriteAsync(buf, 0, buf.Length);
                }

                Console.WriteLine("download: {0} => {1}", url, fileName);
            } catch (Exception ex) {
                Console.WriteLine("failed: {0}, {1}", url, ex.Message);
            }
        };

        //処理の完了待ち
        Task.WaitAll((from url in urls select download(url)).ToArray());
    }
}

ちなみに、前回(id:fits:20111025)と同様の処理を Async CTP を使って実装すると以下のようになります。DownloadFileAsync の代わりに DownloadFileTaskAsync を使います。

AsyncDownloadWebSimple.cs
using System;
using System.Linq;
using System.IO;
using System.Net;
using System.Threading.Tasks;

public class AsyncDownloadWebSimple
{
    public static void Main(string[] args)
    {
        var urls = Console.In.ReadToEnd().Split(new string[]{Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries);

        var dir = args[0];

        //非同期ダウンロード処理
        Func<string, Task> download = async (url) => {
            var wc = new WebClient();
            var uri = new Uri(url);
            var fileName = Path.Combine(dir, Path.GetFileName(url));

            try {
                await wc.DownloadFileTaskAsync(uri, fileName);

                Console.WriteLine("download: {0} => {1}", url, fileName);
            } catch (Exception ex) {
                Console.WriteLine("failed: {0}, {1}", url, ex.Message);
            }
        };

        //処理の完了待ち
        Task.WaitAll((from url in urls select download(url)).ToArray());
    }
}