SonarAnalyzer.CSharp でサイクロマティック複雑度を算出

C# ソースファイルのサイクロマティック複雑度(循環的複雑度)を算出するサンプルを SonarC# (SonarAnalyzer.CSharp)API を利用して作ってみました。

今回、使用した環境は以下の通りです。

ソースは 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# で実装

LINQGroupBy メソッドを使えば要素毎にグルーピングした 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 ではグルーピング処理等は用意されていないようなので自前で実装しました。 (今回は 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エンコードしてみたらどうなるか試してみました。

; / ? : @ = & % $ - _ . + ! * ' " ( ) , { } | \ ^ ~ [ ]

使用した言語は下記の通りです。

ソースは 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
  • (1) Uri.EscapeUriString()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ はエンコードされない
  • (2) HttpUtility.UrlEncode()
    • 半角スペースが + になる
    • - _ . ! * ( ) はエンコードされない
    • アルファベットが小文字

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
  • (1) escape()
    • 半角スペースが %20 になる
    • / @ - _ . + * がエンコードされない
  • (2) encodeURI()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ がエンコードされない
  • (3) encodeURIComponent()
    • 半角スペースが %20 になる
    • - _ . ! * ' ( ) ~ がエンコードされない

Ruby の場合

Ruby では下記 API が使えます。

  • 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
  • (1) URI.escape(), encode()
    • 半角スペースが %20 になる
    • ; / ? : @ = & $ - _ . + ! * ' ( ) , ~ [ ] がエンコードされない
  • (2) CGI.escape()

Python の場合

Python では下記 API が使えます。

  • 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 の場合

PHP では下記 API が使えます。

  • urlencode()
  • rawurlencode()
url_encode.php
<?php
$str = ';/?:@=&% $-_.+!*\'"(),{}|\\^~[]';

// (1)
echo urlencode($str), "\n";
// (2)
echo rawurlencode($str);
?>
実行結果
> php url_encode.php
%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
%3B%2F%3F%3A%40%3D%26%25%20%24-_.%2B%21%2A%27%22%28%29%2C%7B%7D%7C%5C%5E~%5B%5D
  • (1) urlencode()
  • (2) rawurlencode()
    • 半角スペースが %20 になる
    • - _ . ~ がエンコードされない

まとめ

まとめると以下のようになりました。
こうしてみると微妙な違いが結構ありますね。

言語環境 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(this IObservable source, Action onNext)" 拡張メソッドを利用し Console.WriteLine を AnonymousObserver として 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 上で使えるようにするためのものです。

ビルド例(カレントディレクトリに 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)と同様の非同期ダウンロード処理を JavaC# で実装し、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 で設定した処理結果は特に使っていません)

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 サーバープログラム作成

まずは、クライアントからの処理を受け付けるサーバープログラムを作成し、起動しておきます。

サーバープログラムは HTTPSBasic認証・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]
    ]
}
サーバープログラム実行例
> jruby https_server.rb

今回はサーバープログラムを JRuby 1.6.4 (JavaSE 7) や Ruby 1.8.7 で実行しています。
Ruby 1.9.2(RubyInstaller for Windows)で実行した場合では、Java 系のクライアントからの接続に失敗するようなので注意が必要です。

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 ヘッダーで指定する方法をとっています。

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 クラスを使えば簡単に実装できます。

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);
        }
    }
}

なお、上記を実行すると結果は一応返って来ますがサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。(解決策は不明。サーバーの実行環境が JRubyRuby の違いに関わらず発生する)

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

なお、Java と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。