SonarAnalyzer.CSharp でサイクロマティック複雑度を算出
C# ソースファイルのサイクロマティック複雑度(循環的複雑度)を算出するサンプルを SonarC# (SonarAnalyzer.CSharp) の API を利用して作ってみました。
今回、使用した環境は以下の通りです。
- SonarC# 7.13
- .NET Core SDK 3.0 preview3
ソースは http://github.com/fits/try_samples/tree/master/blog/20190422/
準備
dotnet コマンドを使ってプロジェクトを作成します。
プロジェクトの作成
> dotnet new console
C# のソースを構文解析する必要があるので Microsoft.CodeAnalysis.CSharp
パッケージを追加します。
Microsoft.CodeAnalysis.CSharp の追加
> dotnet add package Microsoft.CodeAnalysis.CSharp
次に、SonarAnalyzer.CSharp
パッケージを追加しますが、これは IDE(VisualStudio)用パッケージのようなので、単に add package してもプロジェクトで参照できるようにはなりません。(analyzers ディレクトリへ .dll が配置されているため)
そこで、以下のように指定のディレクトリへパッケージを配置し ※、.csproj
を編集する事で対応してみました。
※ 普通に add package して .nuget/packages ディレクトリへ 配置された dll のパスを設定する方法も考えられる
SonarAnalyzer.CSharp の追加(pkg ディレクトリへ配置)
> dotnet add package SonarAnalyzer.CSharp --package-directory pkg
上記コマンドで追加された PackageReference
要素をコメントアウトし、代わりに Reference
要素を追加します。(HintPath
で SonarAnalyzer.CSharp.dll のパスを指定)
sonar_sample.csproj の編集
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" /> <!-- 以下をコメントアウト <PackageReference Include="SonarAnalyzer.CSharp" Version="7.13.0.8313"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> --> <!-- 以下を追加 --> <Reference Include="SonarAnalyzer.CSharp"> <HintPath>./pkg/sonaranalyzer.csharp/7.13.0.8313/analyzers/SonarAnalyzer.CSharp.dll</HintPath> </Reference> </ItemGroup> </Project>
実装
C# のソースコードをパースして MethodDeclarationSyntax
を取り出し、CSharpCyclomaticComplexityMetric.GetComplexity
メソッドへ渡す事でサイクロマティック複雑度を算出します。
Program.cs
using System; using System.Linq; using System.IO; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using SonarAnalyzer.Metrics.CSharp; namespace CyclomaticComplexity { class Program { static void Main(string[] args) { using (var reader = new StreamReader(args[0])) { // ソースコードのパース var tree = CSharpSyntaxTree.ParseText(reader.ReadToEnd()); var root = tree.GetCompilationUnitRoot(); // MethodDeclarationSyntax の取得 var methods = root.DescendantNodes() .OfType<MethodDeclarationSyntax>(); foreach(var m in methods) { var c = CSharpCyclomaticComplexityMetric.GetComplexity(m); Console.WriteLine("{0},{1}", m.Identifier, c.Complexity); } } } } }
実行
ビルドして実行してみます。
ビルド
> dotnet build ・・・ ビルドに成功しました。 0 個の警告 0 エラー
Program.cs の複雑度を算出してみます。
実行1
> dotnet run Program.cs Main,2
SonarC# のソースで試してみます。
実行2
> cd .. > git clone https://github.com/SonarSource/sonar-dotnet.git ・・・ > cd sonar_sample > dotnet run ../sonar-dotnet/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/Metrics/CSharpMetrics.cs GetCognitiveComplexity,1 GetCyclomaticComplexity,1 IsClass,4 IsCommentTrivia,1 IsDocumentationCommentTrivia,4 IsEndOfFile,1 IsFunction,16 IsNoneToken,1 IsStatement,28
ジニ不純度の算出2 - Ruby, C#, F#, Erlang
前回 に続き、今回は下記のようなプログラム言語でジニ不純度(ジニ係数)の算出処理を同じように実装してみました。
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20140608/
Ruby で実装
Ruby では group_by
で要素毎の Hash オブジェクトを取得できます。
(下記では {"A"=>["A", "A"], "B"=>["B", "B", "B"], "C"=>["C"]}
)
なお、Hash で map
した結果は配列になります。
(下記 list.group_by {|x| x }.map {|k, v| [k, v.size.to_f / xs.size] }
の結果は [["A", 0.33・・・], ["B", 0.5], ["C", 0.16・・・]]
)
また、combination(2)
で前回の Scala の関数と同様に 2要素の組み合わせを取得できます。
(下記では、[["A", 0.33・・・], ["B", 0.5]], [["A", 0.33・・・], ["C", 0.16・・・]], [["B", 0.5], ["C", 0.16・・・]]
)
gini.rb
#coding:utf-8 # (a) 1 - (AA + BB + CC) def giniA(xs) 1 - xs.group_by {|x| x }.inject(0) {|a, (k, v)| a + (v.size.to_f / xs.size) ** 2 } end # (b) AB × 2 + AC × 2 + BC × 2 def giniB(xs) xs.group_by {|x| x }.map {|k, v| [k, v.size.to_f / xs.size] }.combination(2).inject(0) {|a, t| a + t.first.last * t.last.last * 2} end list = ["A", "B", "B", "C", "B", "A"] puts giniA(list) puts giniB(list)
実行結果
> ruby gini.rb 0.6111111111111112 0.611111111111111
C# で実装
- .NET Framework 4.5
LINQ の GroupBy
メソッドを使えば要素毎にグルーピングした IGrouping<TKey, TSource>
のコレクションを取得できます。
要素の組み合わせも LINQ のクエリ式を使えば簡単に作成できます。(下記の combination メソッド)
gini.cs
using System; using System.Collections.Generic; using System.Linq; class Gini { public static void Main(string[] args) { var list = new List<string>() {"A", "B", "B", "C", "B", "A"}; Console.WriteLine("{0}", giniA(list)); Console.WriteLine("{0}", giniB(list)); } // (a) 1 - (AA + BB + CC) private static double giniA<K>(IEnumerable<K> xs) { return 1 - xs.GroupBy(x => x).Select(x => Math.Pow((double)x.Count() / xs.Count(), 2)).Sum(); } // (b) AB + AC + BA + BC + CA + CB private static double giniB<K>(IEnumerable<K> xs) { return combination( countBy(xs).Select(t => Tuple.Create(t.Item1, (double)t.Item2 / xs.Count()) ) ).Select(x => x.Item1.Item2 * x.Item2.Item2).Sum(); } private static IEnumerable<Tuple<K, int>> countBy<K>(IEnumerable<K> xs) { return xs.GroupBy(x => x).Select(g => Tuple.Create(g.Key, g.Count())); } // 異なる要素の組み合わせを作成 private static IEnumerable<Tuple<Tuple<K, V>, Tuple<K, V>>> combination<K, V>(IEnumerable<Tuple<K, V>> data) { return from x in data from y in data where !x.Item1.Equals(y.Item1) select Tuple.Create(x, y); } }
実行結果
> csc gini.cs > gini.exe 0.611111111111111 0.611111111111111
F# で実装
- F# 3.1
F# では Seq.countBy
で要素毎のカウント値を取得できます。
(下記では seq [("A", 2); ("B", 3); ("C", 1)]
)
要素の組み合わせは内包表記を使えば簡単に作成できます。 (下記の combinationCount)
gini.fs
let size xs = xs |> Seq.length |> float // (a) 1 - (AA + BB + CC) let giniA xs = xs |> Seq.countBy id |> Seq.sumBy (fun (k, v) -> (float v / size xs) ** 2.0) |> (-) 1.0 let combinationCount cs = [ for x in cs do for y in cs do if fst x <> fst y then yield (snd x, snd y) ] // (b) AB + AC + BA + BC + CA + CB let giniB xs = xs |> Seq.countBy id |> combinationCount |> Seq.sumBy (fun (x, y) -> (float x / size xs) * (float y / size xs)) let list = ["A"; "B"; "B"; "C"; "B"; "A";] printfn "%A" (giniA list) printfn "%A" (giniB list)
実行結果
> fsc gini.fs > gini.exe 0.6111111111 0.6111111111
Erlang で実装
- Erlang 5.10
Erlang ではグルーピング処理等は用意されていないようなので自前で実装しました。 (今回は dict モジュールを使いました)
リスト内包表記 ([<構築子> || <限定子>, ・・・]
) で使用するジェネレータ (<変数> <- <式>
) の右辺はリストになる式を指定する必要があるため、dict:to_list()
でリスト化しています。
gini.erl
-module(gini). -export([main/1]). groupBy(Xs) -> lists:foldr(fun(X, Acc) -> dict:append(X, X, Acc) end, dict:new(), Xs). countBy(Xs) -> dict:map( fun(_, V) -> length(V) end, groupBy(Xs) ). % (a) 1 - (AA + BB + CC) giniA(Xs) -> 1 - lists:sum([ math:pow(V / length(Xs), 2) || {_, V} <- dict:to_list(countBy(Xs)) ]). combinationProb(Xs) -> [ {Vx, Vy} || {Kx, Vx} <- Xs, {Ky, Vy} <- Xs, Kx /= Ky ]. % (b) AB + AC + BA + BC + CA + CB giniB(Xs) -> lists:sum([ (Vx / length(Xs)) * (Vy / length(Xs)) || {Vx, Vy} <- combinationProb(dict:to_list(countBy(Xs))) ]). main(_) -> List = ["A", "B", "B", "C", "B", "A"], io:format("~p~n", [ giniA(List) ]), io:format("~p~n", [ giniB(List) ]).
実行結果
> escript gini.erl 0.6111111111111112 0.611111111111111
なお、groupBy(List)
と countBy(List)
の結果を出力すると下記のようになりました。
groupBy(List) の出力結果
{dict,3,16,16,8,80,48, {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}, {{[], [["B","B","B","B"]], [],[],[],[], [["C","C"]], [],[],[],[],[], [["A","A","A"]], [],[],[]}}}
countBy(List) の出力結果
{dict,3,16,16,8,80,48, {[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]}, {{[], [["B"|3]], [],[],[],[], [["C"|1]], [],[],[],[],[], [["A"|2]], [],[],[]}}}
記号文字の URL エンコード - Java, .NET, JavaScript, Ruby, Python, PHP
下記のような文字をいくつかのプログラム言語の標準的な API で URL (URI) エンコードしてみたらどうなるか試してみました。
; / ? : @ = & % $ - _ . + ! * ' " ( ) , { } | \ ^ ~ [ ]
使用した言語は下記の通りです。
- Groovy (Java API)
- C# (.NET Framework)
- JavaScript
- Ruby
- Python
- PHP
ソースは http://github.com/fits/try_samples/tree/master/blog/20130425/
Java の場合
Java では下記メソッドが使えます。
- java.net.URLEncoder の encode() メソッド
今回は Groovy で実装してみました。
url_encode.groovy
str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]' println URLEncoder.encode(str)
実行結果
> groovy url_encode.groovy %3B%2F%3F%3A%40%3D%26%25+%24-_.%2B%21*%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
- 半角スペースが + になる
- - _ . * はエンコードされない
.NET の場合
.NET では下記メソッドが使えます。
- System.Uri の EscapeUriString() メソッド
- System.Web.HttpUtility の UrlEncode() メソッド
今回は C# で実装してみました。
url_encode.cs
using System; using System.Web; class UrlEncode { static void Main(string[] args) { var str = ";/?:@=&% $-_.+!*'\"(),{}|\\^~[]"; // (1) Console.WriteLine(Uri.EscapeUriString(str)); // (2) Console.WriteLine(HttpUtility.UrlEncode(str)); } }
実行結果
> csc url_encode.cs ・・・ > url_encode.exe ;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~%5B%5D %3b%2f%3f%3a%40%3d%26%25+%24-_.%2b!*%27%22()%2c%7b%7d%7c%5c%5e%7e%5b%5d
JavaScript の場合
JavaScript では下記 API が使えます。
- escape()
- encodeURI()
- encodeURIComponent()
node.js で実行してみました。
url_encode.js
var str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]'; // (1) console.log(escape(str)); // (2) console.log(encodeURI(str)); // (3) console.log(encodeURIComponent(str));
実行結果
> node url_encode.js %3B/%3F%3A@%3D%26%25%20%24-_.+%21*%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D ;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~%5B%5D %3B%2F%3F%3A%40%3D%26%25%20%24-_.%2B!*'%22()%2C%7B%7D%7C%5C%5E~%5B%5D
Ruby の場合
- URI.escape(), encode()
- CGI.escape()
url_encode.rb
require 'uri' require 'cgi' str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]' # (1) puts URI.escape(str) puts URI.encode(str) # (2) puts CGI.escape(str)
実行結果
> ruby url_encode.rb ;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~[] ;/?:@=&%25%20$-_.+!*'%22(),%7B%7D%7C%5C%5E~[] %3B%2F%3F%3A%40%3D%26%25+%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
Python の場合
- urllib.quote()
url_encode.py
import urllib str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]' print urllib.quote(str)
実行結果
> python url_encode.py %3B/%3F%3A%40%3D%26%25%20%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E%7E%5B%5D
- 半角スペースが %20 になる
- / - _ . がエンコードされない
PHP の場合
- urlencode()
- rawurlencode()
url_encode.php
<?php $str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]'; // (1) echo urlencode($str), "\n"; // (2) echo rawurlencode($str); ?>
まとめ
まとめると以下のようになりました。
こうしてみると微妙な違いが結構ありますね。
言語環境 | API | 半角スペースのエンコード | エンコードされない文字 |
---|---|---|---|
Java | URLEncoder.encode() | + | - _ . * |
.NET | Uri.EscapeUriString() | %20 | ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ |
.NET | HttpUtility.UrlEncode() | + | - _ . ! * ( ) |
JavaScript | escape() | %20 | / @ - _ . + * |
JavaScript | encodeURI() | %20 | ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ |
JavaScript | encodeURIComponent() | %20 | - _ . ! * ' ( ) ~ |
Ruby | URI.escape(), encode() | %20 | ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ [ ] |
Ruby | CGI.escape() | + | - _ . |
Python | urllib.quote() | %20 | / - _ . |
PHP | urlencode() | + | - _ . |
PHP | rawurlencode() | %20 | - _ . ~ |
Mono で Rx を使用する
Mono で Rx (Reactive Extensions) を使ってみました。
Rx は非同期やイベント処理を LINQ で実装できるようにする API で、id:fits:20130212 や id:fits:20130216 で試した Iteratee によく似ていると思います。
生産者 | 消費者 | |
---|---|---|
Iteratee | Enumerator | Iteratee |
Rx | IObservable | IObserver |
今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20130224/
Rx インストール手順(コマンドライン版 NuGet の利用)
コマンドライン版の NuGet を使って Mono で Rx をインストールするには以下のような手順を実施します。
- (1) Mono に SSL 証明書をインストール
- (2) コマンドライン版 NuGet 取得とアップデート
- (3) Rx インストール
(1) Mono に SSL 証明書をインストール
NuGet がパッケージをダウンロードできるように mozroots コマンドを使って Mono に SSL 証明書をインストールしておきます。
> mozroots --import --sync
(2) コマンドライン版 NuGet 取得とアップデート
下記 NuGet ダウンロードページから NuGet.exe Command Line をダウンロードして任意のディレクトリに保存します。
次に、mono コマンドで NuGet.exe を実行し NuGet.exe 2.2.1 へのアップデートを行います。
> mono NuGet.exe NuGet bootstrapper 1.0.0.0 Found NuGet.exe version 2.2.1. Downloading... Update complete.
(3) Rx インストール
NuGet.exe を使って Rx-Main をインストールすると、カレントディレクトリに Rx のパッケージがインストールされます。
> mono NuGet.exe install Rx-Main Attempting to resolve dependency 'Rx-Interfaces (? 2.1.30214.0)'. Attempting to resolve dependency 'Rx-Core (? 2.1.30214.0)'. Attempting to resolve dependency 'Rx-Linq (? 2.1.30214.0)'. Attempting to resolve dependency 'Rx-PlatformServices (? 2.1.30214.0)'. Successfully installed 'Rx-Interfaces 2.1.30214.0'. Successfully installed 'Rx-Core 2.1.30214.0'. Successfully installed 'Rx-Linq 2.1.30214.0'. Successfully installed 'Rx-PlatformServices 2.1.30214.0'. Successfully installed 'Rx-Main 2.1.30214.0'.
単純な Rx サンプル
以下のような単純な処理を行うサンプルを作成してみます。 *1
- (1) 1 〜 3 を出力
- (2) 1 〜 4 の偶数だけを取り出し先頭に # を付けて出力
Rx では、Subscribe メソッドを使って IObservable へ IObserver を登録します。
ObservableExtensions *2 には Subscribe 用の拡張メソッドが色々と定義されており、下記では "Subscribe
RxSample.cs
using System; using System.Linq; using System.Reactive.Linq; class RxSample { static void Main(string[] args) { // (1) 1 〜 3 を出力 Observable.Range(1, 3).Subscribe(Console.WriteLine); Console.WriteLine("-----"); // (2) 1 〜 4 の偶数だけを取り出し先頭に # を付けて出力 Observable.Range(1, 4).Where(x => x % 2 == 0).Select(x => "#" + x).Subscribe(Console.WriteLine); // 以下でも可 //(from x in Observable.Range(1, 3) where x % 2 == 0 select "#" + x).Subscribe(Console.WriteLine); } }
mcs でソースをビルドし mono で実行します。
ビルド
> mcs -r:System.Reactive.Core,System.Reactive.Linq RxSample.cs
実行
> mono RxSample.exe 1 2 3 ----- #2 #4
Rx で行単位のファイル処理
id:fits:20130216 で実装したような行単位のファイル処理を Rx で実装してみました。
Observable.Create() という IObservable オブジェクトを作成するメソッドが用意されているので、今回はこれを使用する事にします。
Observable.Create() には IObserver を引数とするラムダ式を渡し、ラムダ式内で以下のような処理を実装します。
- OnNext を実行しデータを PUSH (ファイルから 1行読んで OnNext 実行)
- 完了時に OnCompleted 実行 (ファイルの読み込み完了時に OnCompleted 実行)
- エラー発生時に OnError 実行
なお、今回はキャンセル機能を用意しないため Disposable.Empty を返しています。
RxFileSample.cs
using System; using System.IO; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; class RxFileSample { static void Main(string[] args) { // 全行出力 FromFile(args[0]).Subscribe(Console.WriteLine); Console.WriteLine("-----"); // 1行目をスキップして 2・3 行目の先頭に # を付けて出力 FromFile(args[0]).Skip(1).Take(2).Select(x => "#" + x).Subscribe(Console.WriteLine); } private static IObservable<string> FromFile(string fileName) { return Observable.Create<string>(observer => { try { using(var reader = File.OpenText(fileName)) { while (!reader.EndOfStream) { // 1行分のデータを PUSH observer.OnNext(reader.ReadLine()); } } // 完了時 observer.OnCompleted(); } catch (Exception error) { // エラー発生時 observer.OnError(error); } return Disposable.Empty; }); } }
ビルド
> mcs -r:System.Reactive.Core,System.Reactive.Linq RxFileSample.cs
実行
> mono RxFileSample.exe sample.txt Rx を使ったファイル処理の サンプル 1行毎に処理するサンプルを 実装してみました。 ----- #サンプル #
Rx で行単位の非同期ファイル処理
最後に、ファイルの 1行読み込み部分を async/await を使って非同期化してみました。
Observable.Create() へ渡すラムダ式に async を付けて 1行読み込みの箇所を await reader.ReadLineAsync() とします。
RxAsyncFileSample.cs
・・・ class RxAsyncFileSample { static void Main(string[] args) { // 全行出力 FromAsyncFile(args[0]).Subscribe(Console.WriteLine); Console.WriteLine("-----"); // 1行目をスキップして 2・3 行目の先頭に # を付けて出力 FromAsyncFile(args[0]).Skip(1).Take(2).Select(x => "#" + x).Subscribe(Console.WriteLine); } private static IObservable<string> FromAsyncFile(string fileName) { // async を付ける return Observable.Create<string>(async observer => { try { using(var reader = File.OpenText(fileName)) { while (!reader.EndOfStream) { // await と ReadLineAsync() を使用 var line = await reader.ReadLineAsync(); observer.OnNext(line); } } observer.OnCompleted(); } catch (Exception error) { observer.OnError(error); } return Disposable.Empty; }); } }
ただし、このサンプルは Mono 3.0.3 では正常に動作しません。
Mono 3.0.3 では 2行目の読み込み時に以下のようなエラーが発生し、正常に処理が続きません。
Mono 3.0.3 で実行した場合のエラー内容
System.InvalidOperationException: Operation is not valid due to the current state of the object ・・・
なお、.NET Framework SDK 4.5 でビルドし実行した場合は動作しました。 *3
ビルド(.NET Framework 4.5)
> csc /r:System.Reactive.Core.dll;System.Reactive.Linq.dll;System.Reactive.Interfaces.dll;System.Threading.Tasks.dll;System.Runtime.dll RxAsyncFileSample.cs Microsoft (R) Visual C# Compiler Version 4.0.30319.17929 for Microsoft (R) .NET Framework 4.5 Copyright (C) Microsoft Corporation. All rights reserved.
実行(.NET Framework 4.5)
> RxAsyncFileSample.exe sample.txt Rx を使ったファイル処理の サンプル 1行毎に処理するサンプルを 実装してみました。 ----- #サンプル #
*1:NuGet を実行したディレクトリにソースを作成します
*2:ソースは Observable.Extensions.cs
*3:.NET Framework 4.5 の場合はカレントディレクトリに Rx 関連のアセンブリ(.dll)を配置してビルドと実行を行いました
非同期処理で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()); } }
非同期処理でWebコンテンツをダウンロードする方法2 - Groovy, Scala, Java, C#
今回は、前回(id:fits:20111016)と同様の非同期ダウンロード処理を Java と C# で実装し、Groovy と Scala は別の実装方法を模索してみました。
使用した機能は以下の通りです。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20111025/
Groovy の場合2 : Actor (GPars)
今回は GPars の Actor を使って実装してみました。
react を多段にして処理をつなげ、例外発生時は onException で一括処理するようにしています。((1) 〜 (4) の順に非同期処理される)
- Groovy 1.8.2
async_download_web2.groovy
import groovyx.gpars.actor.* def dir = args[0] System.in.readLines() collect {u -> def download = Actors.actor { def url //例外発生時の処理 delegate.metaClass.onException = { println "failed: ${url}, ${it}" } react {urlString -> //URL接続開始 (2) url = new URL(urlString) //Actor(自分)へのメッセージ送信 (3) send url.openStream() react {stream -> //ダウンロード処理開始 (4) def file = new File(dir, new File(url.file).name) file.bytes = stream.bytes println "downloaded: ${url} => ${file}" } } } //Actor へのメッセージ送信 (1) download.send u download } each { //処理の完了待ち it.join() }
Scala の場合2 : 限定継続 + ops
前回(id:fits:20111016)の Actor 部分を ops.spawn で置き換えてみました。
- Scala 2.9.1(JavaSE 7 依存)
前回と同様、実行時に -P:continuations:enable オプション指定が必要になります。
実行例(限定継続を有効化)
scala -P:continuations:enable async_download_web2.scala destdir < urls.txt
async_download_web2.scala
import scala.concurrent.ops import scala.util.continuations._ import scala.io.Source import java.io.{InputStream, File} import java.net.URL import java.nio.file.{Paths, Files, Path} import java.nio.file.StandardCopyOption._ val dir = args(0) val using = (st: InputStream) => (block: InputStream => Unit) => try {block(st)} finally {st.close()} Source.stdin.getLines.toList.foreach {u => val url = new URL(u) reset { //URL接続処理 val stream = shift {k: (InputStream => Unit) => //非同期実行 ops.spawn { try { //継続の呼び出し:(1) から処理を継続 k(url.openStream()) } catch { case e: Exception => printf("failed: %s, %s\n", url, e) } } } //(1) //ダウンロード処理 val file = shift {k: (Path => Unit) => //非同期実行 ops.spawn { val f = new File(url.getFile()).getName() val filePath = Paths.get(dir, f) try { using (stream) {st => Files.copy(st, filePath, REPLACE_EXISTING) } //継続の呼び出し:(2) から処理を継続 k(filePath) } catch { case e: Exception => printf("failed: %s, %s\n", url, e) } } } //(2) printf("downloaded: %s => %s\n", url, file) } }
Java の場合 : Concurrency Utilities
Concurrency Utilities の Future を使って実装してみました。
処理的には前回の Groovy 版に似ていると思います。
- JavaSE 7.0u1
AsyncDownloadWeb.java
import java.io.*; import java.util.*; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import static java.nio.file.StandardCopyOption.*; public class AsyncDownloadWeb { public static void main(String[] args) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); ExecutorService exec = Executors.newCachedThreadPool(); final String dir = args[0]; String urlString = null; while ((urlString = reader.readLine()) != null) { final URI uri = URI.create(urlString); //URL接続処理の非同期実行 final Future<InputStream> stream = exec.submit(new Callable<InputStream>() { public InputStream call() throws Exception { return uri.toURL().openStream(); } }); //ダウンロード処理の非同期実行 final Future<Path> file = exec.submit(new Callable<Path>() { public Path call() throws Exception { String fileName = new File(uri.getPath()).getName(); Path filePath = Paths.get(dir, fileName); try (InputStream in = stream.get()) { Files.copy(in, filePath, REPLACE_EXISTING); } return filePath; } }); //結果出力 exec.submit(new Runnable() { public void run() { try { System.out.printf("downloaded: %s => %s\n", uri, file.get()); } catch (Exception ex) { System.out.printf("failed: %s, %s\n", uri, ex); } } }); } //全処理の完了待ち exec.shutdown(); } }
C# の場合 : TPL + EAP
最初、Async CTP を使って F# と同じような処理を C# で実装するつもりだったのですが、Async CTP がまともにインストールできなかったので*1、タスク並列ライブラリ(TPL)と従来のイベントベース非同期パターン(EAP)を組み合わせて実装してみました。
TPL の TaskCompletionSource を使うと EAP の処理を Task 化できるので、下記サンプルでは DownloadFileAsync のイベント処理を Task 化し、処理の完了待ちに使用しています。(TrySetResult で設定した処理結果は特に使っていません)
- C# 4.0 (.NET Framework 4.0)
AsyncDownloadWeb.cs
using System; using System.Collections.Generic; 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]; var taskList = new List<Task<bool>>(urls.Length); foreach (var u in urls) { var wc = new WebClient(); var uri = new Uri(u); var fileName = Path.Combine(dir, Path.GetFileName(u)); TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); //非同期ダウンロード完了時のイベント処理 wc.DownloadFileCompleted += (sender, e) => { if (e.Error != null) { //エラー発生時の処理 Console.WriteLine("failed: {0}, {1}", uri, e.Error.Message); //処理結果の設定 tcs.TrySetResult(false); } else { //成功時の処理 Console.WriteLine("downloaded: {0} => {1}", uri, fileName); //処理結果の設定 tcs.TrySetResult(true); } }; //非同期ダウンロード開始 wc.DownloadFileAsync(uri, fileName); //Task 化したオブジェクトを追加 taskList.Add(tcs.Task); } //全処理の完了待ち Task.WaitAll(taskList.ToArray()); } }
*1:インストールは一応完了するが、必要なファイルが正常にインストールされなかった
信頼されない証明書を使ったHTTPSサーバーにBasic認証でPOST - Ruby, PHP, C#, Java, Groovy
信頼されないSSL証明書(自己証明書)を使ったサイトに対して、Basic認証を行い POST するサンプルを Ruby, PHP, C#, Java, Groovy で実装してみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20111002/
サンプルは、第1引数に URL、第2引数と第3引数に Basic 認証のユーザー名とパスワード、第4引数に POST するデータを指定するようにしています。
なお、POST データを & で区切ると Windows のコマンドプロンプトなどで実行するには不都合があるので ; で区切っている点に注意。
サンプルの実行例
> jruby basic_post_novalidate_certificate.rb https://localhost:8443/ user1 pass1 mode=test;id=123 hello
HTTPS サーバープログラム作成
まずは、クライアントからの処理を受け付けるサーバープログラムを作成し、起動しておきます。
サーバープログラムは HTTPS・Basic認証・POST を処理する必要がありますので、今回は Sinatra + WEBrick で実現してみました。
実装が容易で証明書などの準備も不要なのでテスト用途にはお勧めな方法です。(証明書が未指定なら実行時に自動生成してくれる)
下記では ポート 8443 で実行、クライアントの検証を実施せず(:SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE)、ユーザー名が user1 ならBasic認証を通るように実装しています。
https_server.rb (Sinatra を使った HTTPS サーバープログラム)
require "rubygems" require "sinatra/base" require "webrick/https" require "openssl" class SampleApp < Sinatra::Base #Basic認証 use Rack::Auth::Basic do |user, pass| user == 'user1' end #POST の処理 post '/' do p params 'hello' end end #WEBrick で SSL を使用するための処理 #(実行時に自己証明書が自動生成される) Rack::Handler::WEBrick.run SampleApp, { :Port => 8443, :SSLEnable => true, #クライアントを検証しないための設定 :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, :SSLCertName => [ ["CN", WEBrick::Utils::getservername] ] }
Ruby による Web クライアント
Ruby では、use_ssl を true にした Net::HTTP オブジェクトの verify_mode に OpenSSL::SSL::VERIFY_NONE を設定すれば自己証明書を処理できるようになります。
basic_post_novalidate_certificate.rb
require 'net/https' require 'uri' url = URI.parse(ARGV[0]) user = ARGV[1] pass = ARGV[2] postData = ARGV[3] https = Net::HTTP.new(url.host, url.port) #SSLの有効化 https.use_ssl = true #SSL証明書を検証しないための設定 https.verify_mode = OpenSSL::SSL::VERIFY_NONE res = https.start do req = Net::HTTP::Post.new(url.path) #Basic認証 req.basic_auth user, pass #POSTデータの設定 req.body = postData #POST https.request(req) end #結果の出力 print res.body
PHP による Web クライアント
PHP で file_get_contents を使えば HTTPS を特に意識しなくてもよいので簡単に実装できます。(自己証明書を意識する必要も無いみたいです)
ただし、php_openssl の extension を有効化する等、file_get_contents で HTTPS を処理するための環境設定が必要かもしれません。(今回は php.ini で php_openssl の extension を有効化しました)
Basic認証に関しては URL でユーザー名とパスワードを指定する方法もありますが、今回は Authorization ヘッダーで指定する方法をとっています。
- PHP 5.3.8
basic_post_novalidate_certificate.php
<?php $url = $argv[1]; $user = $argv[2]; $pass = $argv[3]; $postData = $argv[4]; $options = array('http' => array( 'method' => 'POST', 'header' => "Authorization: Basic " . base64_encode("$user:$pass") . "\r\n" . "Content-Type: application/x-www-form-urlencoded\r\n", 'content' => $postData )); //POST $res = file_get_contents($url, false, stream_context_create($options)); //結果の出力 echo $res; ?>
C# による Web クライアント
C# では ServicePointManager.ServerCertificateValidationCallback に true を返すコールバックを設定するだけで自己証明書を処理できるようになります。
Basic認証も POST も WebClient クラスを使えば簡単に実装できます。
- .NET Framework 4.0
BasicPostNovalidateCertificate.cs
using System; using System.Net; class BasicPostNovalidateCertificate { public static void Main(string[] args) { var url = args[0]; var user = args[1]; var pass = args[2]; var postData = args[3]; //SSL 証明書を検証しないようにする設定 //(証明書を何でも受け入れるようにする) ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true; using (WebClient wc = new WebClient()) { //Basic認証 wc.Credentials = new NetworkCredential(user, pass); //POST var res = wc.UploadString(url, "POST", postData); //結果の出力 Console.Write(res); } } }
なお、上記を実行すると結果は一応返って来ますがサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。(解決策は不明。サーバーの実行環境が JRuby・Ruby の違いに関わらず発生する)
Java による Web クライアント
Java では SSLContext を何もしない X509TrustManager で初期化し、どんなホスト名でも受け入れる HostnameVerifier を設定します。
- JavaSE 7
BasicPostNovalidateCertificate.java
import java.io.*; import java.net.*; import java.security.SecureRandom; import java.security.cert.X509Certificate; import javax.net.ssl.*; public class BasicPostNovalidateCertificate { public static void main(String[] args) throws Exception { URL url = new URL(args[0]); final String user = args[1]; final String pass = args[2]; String postData = args[3]; //Basic認証 Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, pass.toCharArray()); } }); // ホスト名を検証しないようにする設定 //(openConnection する前に設定しておく必要あり) HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { public boolean verify(String host, SSLSession ses) { return true; } }); HttpsURLConnection con = (HttpsURLConnection)url.openConnection(); con.setDoOutput(true); con.setRequestMethod("POST"); //SSL 証明書を検証しないための設定 SSLContext sslctx = SSLContext.getInstance("SSL"); sslctx.init(null, new X509TrustManager[] { new X509TrustManager() { public void checkClientTrusted(X509Certificate[] arg0, String arg1) { } public void checkServerTrusted(X509Certificate[] arg0, String arg1) { } public X509Certificate[] getAcceptedIssuers() { return null; } } }, new SecureRandom()); con.setSSLSocketFactory(sslctx.getSocketFactory()); //POSTデータの出力 OutputStream os = con.getOutputStream(); PrintStream ps = new PrintStream(os); ps.print(postData); ps.close(); //結果の出力 InputStream is = con.getInputStream(); BufferedInputStream bis = new BufferedInputStream(is); int len = 0; byte[] buf = new byte[1024]; while ((len = bis.read(buf, 0, buf.length)) > -1) { System.out.write(buf, 0, len); } bis.close(); } }
なお、Java でも C# と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。
Groovy による Web クライアント
Groovy は基本的に Java 版の内容をそのまま実装しました。
- Groovy 1.8.2(JavaSE 7)
basic_post_novalidate_certificate.groovy
import java.security.SecureRandom import java.security.cert.X509Certificate import javax.net.ssl.* def url = new URL(args[0]) def user = args[1] def pass = args[2] def postData = args[3] //Basic認証(getPasswordAuthentication() をオーバーライド) Authenticator.default = { new PasswordAuthentication(user, pass.toCharArray()) } as Authenticator // ホスト名を検証しないようにする設定 //(注)openConnection する前に設定しておく必要あり HttpsURLConnection.defaultHostnameVerifier = { host, session -> true } as HostnameVerifier def con = url.openConnection() con.doOutput = true con.requestMethod = "POST" //SSL 証明書を検証しないための設定 def sslctx = SSLContext.getInstance("SSL") //X509TrustManager インターフェースの実装 def tmanager = [ checkClientTrusted: {chain, authType -> }, checkServerTrusted: {chain, authType -> }, getAcceptedIssuers: {null} ] as X509TrustManager sslctx.init(null as KeyManager[], [tmanager] as X509TrustManager[], new SecureRandom()) con.SSLSocketFactory = sslctx.socketFactory //POSTデータの出力 con.outputStream.withWriter { it.print postData } //結果の出力 print con.inputStream.text