ジニ不純度の算出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]], [],[],[]}}}
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 を作成します。
- %VAGRANT_HOME%\boxes<Box名>\virtualbox
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) エンコードしてみたらどうなるか試してみました。
; / ? : @ = & % $ - _ . + ! * ' " ( ) , { } | \ ^ ~ [ ]
使用した言語は下記の通りです。
- 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 | - _ . ~ |
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 で実行してみました。
- JRuby 1.7.0 preview1
- kramdown 0.13.7
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 をカレントディレクトリに配置して、実行しました。
- PHP 5.4.4
- Markdown Extra 1.2.5
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 で使いました。
- Groovy 2.0.0
- MarkdownJ Core 0.4.1
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 で実行しました。
- Node.js 0.8.0
- CoffeeScript 1.3.3
- Markdown-js 0.4.0
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><blockquote></code></pre> <p> <p>For example.</p> </blockquote> </p> <h3>HTMLとの共存</h3> ・・・ <p>例:April 1<sup>st</sup>
信頼されない証明書を使った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
並列処理で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}