ジニ不純度の算出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]],
        [],[],[]}}}

Windows用 VirtualBox 4.2.14 で Vagrant を使用する

はじめに

Windows用 VirtualBox-4.2.14-86644 を Windows7(64bit)にインストールしたところ、新たに vagrant init した Box の vagrant up 時にエラーが発生するようになりました。(ちなみに VirtualBox-4.2.12-84980 や VirtualBox-4.2.16-86992 では正常に動作します)

環境は以下の通りです。

  • Windows用 VirtualBox-4.2.14-86644
  • Vagrant 1.2.2
vagrant up 時のエラー内容
> vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'sample0'...
There was an error while executing `VBoxManage`, a CLI used by Vagrant
for controlling VirtualBox. The command and stderr is shown below.

Command: ["import", "C:/vagrant/boxes/sample0/virtualbox/box.ovf"]

Stderr: 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Interpreting C:\vagrant\boxes\sample0\virtualbox\box.ovf...
OK.
0%...
Progress object failure: RPC_S_SERVER_UNAVAILABLE 0x800706BA

更に、上記 box.ovf ファイルを VBoxManage コマンドで import してみても同様のエラーが発生しますので、.ovf ファイルの import に何らかの障害があると思われます。 (vagrant は内部的に VBoxManage コマンドを使っています)

VBoxManage import 時のエラー内容
> VBoxManage.exe import box.ovf
・・・
Progress object failure: RPC_S_SERVER_UNAVAILABLE 0x800706BA

回避策

とりあえず VirtualBox-4.2.16-86992 へバージョンアップするのが妥当だと思いますが、Windows用 VirtualBox-4.2.14-86644 で Vagrant を使う方法が無いわけではありません。

実は、VBoxManage による .ovf ファイルの import は失敗しますが .ova ファイルの import は成功します。

ここで .ova ファイルの内容は下記の通りです。

  xxx.ova は xxx.ovf と xxx-disk1.vmdk を tar 化したもの

つまり、下記の 2点を実施すれば Windows用 VirtualBox-4.2.14-86644 上で一応 Vagrant が使えるようになります。

  • (1) box.ova を import するように Vagrant のソースを改造
  • (2) box.ovf と box-disk1.vmdk を tar 化して box.ova を作成

(1) box.ova を import するように Vagrant のソースを改造

Vagrant 1.2.2 では box.ovf を import するようハードコーディングされていますので、下記 import.rb ファイルを直接変更して box.ova を import するようにします。

  • %VAGRANT_HOME%\embedded\gems\gems\vagrant-1.2.2\plugins\providers\virtualbox\action\import.rb
import.rb の変更内容
・・・
#ovf_file = env[:machine].box.directory.join("box.ovf").to_s
ovf_file = env[:machine].box.directory.join("box.ova").to_s
・・・

話を簡単にするため、上記のような対応で済ませていますが、 VirtualBox のバージョンを判定して tar で box.ova を作成するような処理を実装した方が便利かもしれません。

(2) box.ovf と box-disk1.vmdk を tar 化して box.ova を作成

vagrant box add で Box を追加すると下記ディレクトリに box.ovf ファイル等が展開されていますので、box.ovf と box-disk1.vmdk を tar でアーカイブ化して box.ova を作成します。

box.ova の作成例
> tar cf box.ova box.ovf box-disk1.vmdk

ちなみに、tar コマンドは msysgit に含まれている GNU tar を使いました。

動作確認

上記作業の後、適当なディレクトリを作成し vagrant init・up してみると、一応動作すると思います。

> vagrant init sample0
・・・
> vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'sample0'...
[default] Matching MAC address for NAT networking...
[default] Setting the name of the VM...
[default] Clearing any previously set forwarded ports...
[default] Creating shared folders metadata...
[default] Clearing any previously set network interfaces...
[default] Preparing network interfaces based on configuration...
[default] Forwarding ports...
[default] -- 22 => 2222 (adapter 1)
[default] Booting VM...
[default] Waiting for VM to boot. This can take a few minutes.
[default] VM booted and ready for use!
[default] Configuring and enabling network interfaces...
[default] Mounting shared folders...
[default] -- /vagrant

box.ova を使った Box の自作

実は box.ova を使って自作した Box を vagrant box add してみると特に問題無く成功します。

と言う事で vagrant package を使わずに box.ova を使って Box を手動で作成する手順を説明します。

1. box.ova の export

まずは VBoxManage コマンドを使って仮想マシンを box.ova ファイルへ export します。(Vagrant 用に設定済みの仮想マシンを使います)

VBoxManage export <仮想マシン名> -o box.ova
box.ova の export 例
> VBoxManage export centos6.4 -o box.ova
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Successfully exported 1 machine(s).

2. metadata.json と Vagrantfile の作成

次に、下記のような内容の metadata.json と Vagrantfile を作成します。

metadata.json ファイル
{"provider":"virtualbox"}
Vagrantfile ファイル
Vagrant::Config.run do |config|
  config.vm.base_mac = "0800111DA111"
end

include_vagrantfile = File.expand_path("../include/_Vagrantfile", __FILE__)
load include_vagrantfile if File.exist?(include_vagrantfile)

config.vm.base_mac の値は適当に付けても問題なさそうですが、 とりあえず下記コマンドで出力した macaddress1 の値を設定すればよいです。

VBoxManage showvminfo <仮想マシン名> --machinereadable
仮想マシンの MAC アドレス取得例
> VBoxManage showvminfo centos6.4 --machinereadable
・・・
macaddress1="0800111DA111"
・・・

3. tar.gz 圧縮

これまでに作成した 3 つのファイルを tar.gz 化すれば Vagrant 用の Box となります。

  • box.ova
  • metadata.json
  • Vagrantfile
Box の作成例
> tar zcf sample.box box.ova metadata.json Vagrantfile

4. Box の追加と実行

上記で作成した Box を vagrant へ add します。

Box の追加例
> vagrant box add sample sample.box
Downloading or copying the box...
Extracting box...ate: 346M/s, Estimated time remaining: --:--:--)
Successfully added box 'sample' with provider 'virtualbox'!

適当なディレクトリを作成し vagrant init・up してみると、一応動作すると思います。

Sequel + ojdbc1.4 で TIMESTAMP 変換エラー

はじめに

Sequel 3.48.0 で ojdbc14.jar (10.2.0.5) を使って TIMESTAMP 型のカラムを含むテーブルを検索したところ下記のようなエラーが発生しました。 (JRuby 1.7.4 で実行)

ただし、ojdbc5.jar・ojdbc6.jar (11.2.0.3) ではこのようなエラーは発生しません。

Sequel::InvalidValue: ArgumentError:
    no time information in "oracle.sql.TIMESTAMP@4500a0bc"

実行したのは以下のようなスクリプトです。

require 'rubygems'
require 'sequel'
require_relative 'lib/ojdbc14.jar'

DB = Sequel.connect('jdbc:oracle:thin:user1/pass1@localhost:1521/XE')

order = DB[:sample_order]

ds = order.where { |o| o.value > 200 }

ds.all.each do |r|
    p r
end

これは下記のようなテーブルを SELECT * FROM "SAMPLE_ORDER" WHERE ("VALUE" > 200) で検索しているだけです。

create table sample_order (
    order_no varchar(10) not null,
    value number(10,0) not null,
    create_date timestamp default sysdate not null,
    primary key (order_no)
)

原因

Sequel の実装が下記のようになっている事と、ojdbc14.jar における oracle.sql.TIMESTAMP クラスの toString メソッドが "oracle.sql.TIMESTAMP@4500a0bc" のような文字列を返す事が原因です。

lib/sequel/adapters/jdbc/oracle.rb (Sequel のソース)
def convert_type_oracle_timestamp(v)
    db.to_application_timestamp(v.to_string)
end

試しに oracle.sql.TIMESTAMP クラスの toString メソッド (JRuby 上では Java::OracleSql::TIMESTAMP の to_string) 等の結果を出力してみると下記のようになりました。

oracle_timestamp_string.rb
require_relative 'lib/ojdbc14.jar'
#require_relative 'lib/ojdbc5.jar'
#require_relative 'lib/ojdbc6.jar'

date = Java::OracleSql::TIMESTAMP.new('2013-06-07 13:20:30')

puts date.to_string
puts date.to_jdbc.to_string
puts date.timestamp_value.to_string
puts date.string_value
実行結果1 (ojdbc14.jar の場合)
> jruby oracle_timestamp_string.rb
oracle.sql.TIMESTAMP@c3e122
2013-06-07 13:20:30.0
2013-06-07 13:20:30.0
2013-6-7 13.20.30.0
実行結果2 (ojdbc5.jar、ojdbc6.jar の場合)
> jruby oracle_timestamp_string.rb
2013-06-07 13:20:30.0
2013-06-07 13:20:30.0
2013-06-07 13:20:30.0
2013-06-07 13:20:30.0

という事で、ojdbc5.jar や ojdbc6.jar を使うようにした方が良さそうです。

回避方法

とりあえず、ojdbc5.jar や ojdbc6.jar を使うのが抜本的な対策ですが。 ojdbc14.jar を使わなければならない場合は convert_type_oracle_timestamp をオープンクラスで変更すればよいと思います。(oracle.sql.TIMESTAMP の toString を変更するのもあり)

search_order.rb
require 'rubygems'
require 'sequel'
# convert_type_oracle_timestamp を変更するため下記 2行の require が必要
require 'sequel/adapters/jdbc'
require 'sequel/adapters/jdbc/oracle'
# Oracle JDBC ドライバーの require
require_relative 'lib/ojdbc14.jar'

class Sequel::JDBC::Oracle::Dataset
    # convert_type_oracle_timestamp の変更
    def convert_type_oracle_timestamp(v)
        db.to_application_timestamp(v.to_jdbc.to_string)
        # 以下でも可
        # db.to_application_timestamp(v.timestamp_value.to_string)
    end
end

DB = Sequel.connect('jdbc:oracle:thin:user1/pass1@localhost:1521/XE')

order = DB[:sample_order]

ds = order.where { |o| o.value > 200 }

ds.all.each do |r|
    p r
end

実行結果は以下のようになり、正常に処理できている事を確認できました。

実行結果
> bundle exec jruby search_order.rb
{:order_no=>"A2", :value=>300, :create_date=>2013-06-07 16:42:00 +0900}
{:order_no=>"A3", :value=>600, :create_date=>2013-06-07 16:45:00 +0900}

今回使ったソースは http://github.com/fits/try_samples/tree/master/blog/20130623/

記号文字の 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 - _ . ~

Markdown の HTML 変換 - Ruby, PHP, Groovy, Scala, Node.js

Markdown 形式の文字列を HTML 変換する処理を複数のプログラム言語で試してみました。

処理としては、標準入力から UTF-8 の Markdown 形式の文字列を取得し HTML 変換した結果を標準出力へ UTF-8 で出力しています。

ちなみに、Markdown 文字列は LOGGiX プロジェクトの日本語版サンプル markdown-sample.text を使用しました。

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

Ruby の場合

Pure Ruby な kramdown モジュールを JRuby で実行してみました。

tohtml.rb
require 'kramdown'

Encoding.default_external = 'utf-8'

puts Kramdown::Document.new($stdin.read).to_html
実行例
 jruby tohtml.rb < markdown-sample.text > ruby_result.txt
結果

特に問題なく変換されました。

更に kramdown は以下のような表組みにも対応していました。

プログラム言語 | モジュール名
---------------|-------------
PHP            |   Markdown
Ruby           |   kramdown
Groovy         |   MarkdownJ
Scala          |   knockoff
Node.js        |   markdown

HTML 変換結果は以下の通りです。

<table>
  <thead>
    <tr>
      <th>プログラム言語</th>
      <th>モジュール名</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PHP</td>
      <td>Markdown</td>
    </tr>
    ・・・
  </tbody>
</table>

PHP の場合

Markdown Extra をダウンロードし、markdown.php をカレントディレクトリに配置して、実行しました。

tohtml.php
<?php
include_once "markdown.php";

$mkStr = stream_get_contents(STDIN);

echo Markdown($mkStr);
?>
実行例
 php tohtml.php < markdown-sample.text > php_result.txt
結果

kramdown と同様、特に問題なく変換され表組みにも対応していました。
Extra の付いていない Markdown 1.0.1o の方は表組みに対応していないのでご注意ください。

Groovy の場合

Java 用のモジュール MarkdownJ を Groovy で使いました。

tohtml.groovy
@Grab('com.madgag:markdownj-core:0.4.1')
import com.petebevin.markdown.*

def mk = new MarkdownProcessor()
def enc = 'UTF-8'

System.out.withWriter enc, {w ->
    w.print mk.markdown(System.in.getText(enc))
}
実行例
 groovy tohtml.groovy < markdown-sample.text > groovy_result.txt
結果

以下のような問題が発生しました。

  • 画像の「リファレンス」スタイル箇所が a タグになった *1
・・・
<p>
「リファレンス」スタイル: !
<a title="Loggix" href="../../../theme/images/loggix-logo.png">Loggix</a>
</p>

Scala の場合

Scala は 2種類のモジュールを試してみました。Scala 2.9.2 用のモジュールが見当たらなかったため、2.9.1 用のモジュールを 2.9.2 でスクリプト実行しました。

tohtml.scala (Knockoff 版)
import scala.io._
import com.tristanhunt.knockoff.DefaultDiscounter._

val mkStr = new BufferedSource(System.in)(Codec.UTF8).mkString

val ps = new java.io.PrintStream(System.out, false, "UTF-8")

ps.println(toXHTML(knockoff(mkStr)))
実行例1 (Knockoff 版)
 scala -nc -cp knockoff_2.9.1-0.8.0-16.jar tohtml.scala < markdown-sample.text > scala_result.txt

ここで -nc オプションを指定している点にご注意ください。-nc オプションを付けないとコンパイラのプロセスがリダイレクト先のファイルを開いたままになり不都合が生じます。

結果

以下のような問題が発生しました。

  • '=' と '-' の見出しが正しく変換されなかった *2
  • 画像の「リファレンス」スタイル箇所が a タグになった
<p>Markdown Sample
=======================
</p><p>サンプルドキュメント日本語版
-----------------------
</p>
tohtml2.scala (Actuarius 版)
import scala.io._
import eu.henkelmann.actuarius.ActuariusApp

val mkStr = new BufferedSource(System.in)(Codec.UTF8).mkString

val ps = new java.io.PrintStream(System.out, false, "UTF-8")

ps.println(ActuariusApp(mkStr))
実行例2 (Actuarius 版)
 scala -nc -cp actuarius_2.9.1-0.2.3.jar tohtml2.scala < markdown-sample.text > scala_result2.txt
結果2 (Actuarius 版)

Knockoff とは別の問題が発生しました。

  • '+' と '-' のリストが正しく変換されなかった
<p>(プラス記号で記述)</p>
<p>+   モツァレラチーズ
+   パスタ
+   ワイン</p>
<p>(ハイフン(マイナス記号)で記述)</p>
<p>-   モツァレラチーズ
-   パスタ
-   ワイン</p>

なお、Scala IO を使うと標準入出力まわりの処理が以下のようになります。

ToHtml.scala
package fits.sample

import scalax.io.JavaConverters._
import com.tristanhunt.knockoff.DefaultDiscounter._

object ToHtml extends App {
    val mkStr = System.in.asUnmanagedInput.slurpString

    val html = toXHTML(knockoff(mkStr)).mkString

    System.out.asUnmanagedOutput.write(html)
}

Node.js (CoffeeScript) の場合

Markdown-js を npm install markdown でインストールし、CoffeeScript で実行しました。

tohtml.coffee
mk = require 'markdown'

process.stdin.resume()

process.stdin.on 'data', (data) ->
    console.log mk.markdown.toHTML data.toString()
実行例
 coffee tohtml.coffee < markdown-sample.text > coffee_result.txt
結果

以下のような問題が発生しました。

  • '=' と '-' の見出しが正しく変換されなかった
  • '<' '>' '&' などの不要なエスケープが実施されてしまった
<pre><code>&lt;blockquote&gt;</code></pre>

<p>
        &lt;p&gt;For example.&lt;/p&gt;
    &lt;/blockquote&gt;
</p>

<h3>HTMLとの共存</h3>
・・・
<p>例:April 1&lt;sup&gt;st&lt;/sup&gt;

まとめ

今回の結果をまとめると以下のようになります。

言語 モジュール 見出し = - リファレンススタイル(画像) リスト + - エスケープ 表組み
Ruby kramdown 0.13.7
PHP Markdown Extra 1.2.5
Groovy MarkdownJ Core 0.4.1 × ×
Scala Knockoff 0.8.0 × × ×
Scala Actuarius 0.2.3 × ×
Node.js (CoffeeScript) Markdown-js 0.4.0 × × ×

*1:img タグになるのが正しい

*2:それぞれ h1 と h2 タグに変換されるのが正しい

信頼されない証明書を使った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 と同様にサーバー側でエラー出力(既存の接続はリモートホストに強制的に切断されました)されます。

並列処理でWebコンテンツをダウンロードする方法 - Groovy, Scala, C#, Java, Ruby

複数のWebコンテンツ(HTMLや画像など)をダウンロードする際に 1件ずつ処理していたのでは非効率です。
というわけで、並列的にWebコンテンツをダウンロードするプログラムを Groovy, Scala, C#, Java, Ruby で実装してみました。

主な仕様は以下で、外部ライブラリを使用せずに実装しました。

  • 実行時の第1引数で出力先ディレクトリを指定
  • ダウンロード対象の URL を標準入力で指定(改行区切りで複数指定)
  • URL 内のファイル名を出力ファイル名として使用
実行例
groovy download_web.groovy destdir < urls.txt

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

Groovy の場合

Groovy 1.8 では GPars が同梱されているので、GPars による並列コレクションを使えば簡単に実装できます。
GParsExecutorsPool.withPool に渡したクロージャ内のコレクションで並列処理用のメソッド(下記の eachParallel)が使えるようになります。

  • Groovy 1.8.2
download_web.groovy
import groovyx.gpars.GParsExecutorsPool

def dir = args[0]

GParsExecutorsPool.withPool {
//並列数を固定化するなら以下のようにする
//GParsExecutorsPool.withPool(5) {

    System.in.readLines() eachParallel {u ->
        def url = new URL(u)

        try {
            def file =  new File(dir, new File(url.file).name)

            url.withInputStream {input ->
                file.bytes = input.bytes
            }

            println "downloaded: $url => $file"
        }
        catch (e) {
            println "failed: $url, $e"
        }
    }
}

Scala の場合

Scala 2.9 では並列コレクションが使えます。
コレクションに対して par メソッドを呼び出すと並列コレクション化され、後は foreach 等を実行すれば並列に処理されます。

ただし、デフォルトでは JVM が使用できるプロセッサ数※までしか並列化されないようなので、今回のような用途では並列数が少ないかもしれません。

※ scala.collection.parallel.availableProcessors で数値を参照可
   実際には java.lang.Runtime.getRuntime().availableProcessors() の値が設定されている

なお、ファイル保存処理を簡単に実装するため、JavaSE 7 で導入された java.nio.file.Files クラス等を使用しています。

  • Scala 2.9.1(JavaSE 7 依存)
download_web_scala
import scala.io.Source

import java.io.File
import java.net.URL
import java.nio.file.{Paths, Files}
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.toArray.par.foreach {u =>
    val url = new URL(u)
    val filePath = Paths.get(dir, new File(url.getFile()).getName())

    try {
        using (url.openStream()) {stream =>
            Files.copy(stream, filePath, REPLACE_EXISTING)
        }

        printf("downloaded: %s => %s\n", url, filePath)
    } catch {
        case e: Exception => printf("failed: %s, %s\n", url, e)
    }
}

C# の場合

.NET Framework 4 では並列タスクが使えます。
Parallel.ForEach にコレクションとその処理内容を渡せば並列化されます。

DownloadWeb.cs
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;

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

        var dir = args[0];

        Parallel.ForEach(urls, (u) => {
            var url = new Uri(u);

            try {
                var filePath = Path.Combine(dir, Path.GetFileName(url.LocalPath));
                new WebClient().DownloadFile(url, filePath);

                Console.WriteLine("downloaded: {0} => {1}", url, filePath);
            }
            catch (Exception e) {
                Console.WriteLine("failed: {0}, {1}", url, e);
            }
        });
    }
}

Java の場合

Java の場合、今のところ並列コレクション等の仕組みが用意されていないようなので Concurrency Utilities を使って実装しました。
ファイルの保存処理には JavaSE 7 で導入された java.nio.file.Files クラス等を使用しています。

下記では URL クラスの代わりに URI を使っていますが、特に深い理由は無く Scala のサンプルと同様に URL クラスを使っても問題ありません。


なお、Paths.get() の引数に URI を渡せますが、現バージョンでは "http://・・・" から作成した URI を渡す事はできませんでした。(java.nio.file.FileSystemNotFoundException: Provider "http" not installed となる)

  • JavaSE 7
DownloadWeb.java
import java.io.*;
import java.net.URI;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

public class DownloadWeb {
    public static void main(String[] args) throws Exception {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        ExecutorService exec = Executors.newCachedThreadPool();
        //並列数を固定化するなら以下のようにする
        //ExecutorService exec = Executors.newFixedThreadPool(5);

        final String dir = args[0];
        String url = null;

        while ((url = reader.readLine()) != null) {
            final URI uri = URI.create(url);
            final Path filePath = Paths.get(dir, new File(uri.getPath()).getName());

            exec.submit(new Runnable() {
                @Override
                public void run() {
                    try (InputStream in = uri.toURL().openStream()) {
                        Files.copy(in, filePath, StandardCopyOption.REPLACE_EXISTING);
                        System.out.printf("downloaded: %s => %s\n", uri, filePath);
                    } catch (Exception e) {
                        System.out.printf("failed: %s, %s\n", uri, e);
                    }
                }
            });
        }
        //ダウンロード終了まで待機
        exec.shutdown();
    }
}

Ruby の場合

Ruby の場合も今のところ並列コレクション等の仕組みが用意されていないみたいなので Queue と Thread を使って実装してみました。

スレッド数を固定化しているので、Java で Executors.newFixedThreadPool(数値) を使ったケースや Groovy で GParsExecutorsPool.withPool(数値) {・・・} を使ったケースと同様の処理になると思います。

download_web.rb
require "thread"
require "uri"
require "net/http"

#並列数(スレッド数)
poolSize = 5

dir = ARGV[0]

q = Queue.new
#キューに URL を設定
$stdin.readlines.each {|l| q.push(l.chomp)}

threads = []
poolSize.times do
    threads << Thread.start(q) do |tq|
        #キューが空になるまでループ
        while not q.empty?
            #キューから URL 取り出し
            u = q.pop(true)

            begin
                url = URI.parse(u)
                filePath = File.join(dir, File.basename(url.path))

                res = Net::HTTP.get_response(url)
                open(filePath, 'wb') {|f| f.puts res.body}

                puts "downloaded: #{url} => #{filePath}"
            rescue => e
                puts "failed: #{url}, #{e}"
            end
        end
    end
end

#ダウンロード終了まで待機
threads.each {|t| t.join}