非同期処理で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 上で使えるようにするためのものです。
- .NET Framework 4.0 + Async CTP
ビルド例(カレントディレクトリに 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()); } }