[翻訳] Elixir - 次に来る大物Web言語を翻訳していてElixirに興味を持ったので試しに簡単なプログラム…とはいえHello, Worldよりはマシなものを書いてみました。またもWebスクレイピングです。
まずは並行動作なし、逐次実行のところまで。つまりElixir/Erlangの最大のメリットのひとつは使っていません。
その2で並行処理を取り入れて改良予定です。
→続きを書きました Elixirで試しに何か書いてみる(その2)
そのまた続き
Elixirで試しに何か書いてみる(その3) - Elixirのアプリケーション
Elixirで試しに何か書いてみる(その4) - Taskを使って簡単にする
Elixirで試しに何か書いてみる(その5) - 失敗したらやり直す
もご覧ください(しぶとく更新し続けています)。
[2015/6/5 追記:GitHubのHTML仕様変更とriot.jsのURL変更に対応して修正しました]
[2015/8/29 追記:shibacowさんのコメントによりソースを修正しました]
準備
Elixirのインストール
まずこちらのElixir本家サイトにしたがってElixir(とErlang及びmix(ビルドツール))をインストールします。
-
Macだとbrew一発で入ります。
$ brew install elixir
-
Windowsのコンパイル済みバイナリのインストーラでも特に問題なく完了します。
-
Ubuntuはインストールの指示にもありますが、標準のレポジトリのErlangが古いのでレポジトリを追加する必要があります。その後は
sudo apt-get update
sudo apt-get install elixir
でインストールが完了します。
例によってBeagle Bone Black(Ubuntu 14.04)でもやってみました。こちらはレポジトリにコンパイル済みのものはないのでソースからビルドします。時間は相当かかりますがotp(Erlang)もElixirも問題なくビルドできました。Beagle Bone Blackよりリッチな環境のRaspberryPi2でなら楽勝でしょう1。
vimプラグインを入れよう
NeoBundleを使っている人は
NeoBundle "elixir-lang/vim-elixir"
を追加するとかなりいい感じになります。ElixirはHaskellやPythonのようなオフサイドルール(インデント・ブロック)ではありませんが、関数呼び出し時に()を省略できたりRubyぽくdo...endでブロックが構成できるので簡易的でもいいのでインデントを調整してくれるこういうプラグインは助かります。
お題
GoqueryでWeb スクレイピングしてGithubからリリース情報を取得するをElixirに焼き直します。
実は以前Erlangに興味を持ったときにやはりWeb スクレイピングなプログラムを書きかけて…その時点では「Erlangには文字列という概念はなくすべてバイト列です(キリッ)」2という状況だったので挫折していたのでそのリベンジも兼ねて。
ライブラリについて
調べてみるとElixirには既にかなりの数の素敵ライブラリ群が揃っています3。
今回利用したものは
標準ビルドツールmix
Elixirは最近の他の言語でもよくあるように標準ビルドツールmix4が提供されています。
ライブラリの管理などもmixからhex(パッケージ管理ツール)を叩くことで実行されるのでちょっとしたプログラムを書こうとしたらmixの利用は必須です。
ソースツリーを作る
ソースツリーを置きたいディレクトリで
$ mix new vercheckex
のようにnewコマンドを実行してください。
tamori@bbb1:~$ mix new vercheckex
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/vercheck.ex
* creating test
* creating test/test_helper.exs
* creating test/vercheck_test.exs
Your mix project was created successfully.
You can use mix to compile it, test it, and more:
cd vercheckex
mix test
Run `mix help` for more commands.
指示通りmix test
を実行して問題なくテストが終了すればOKです。
依存ライブラリの指定 (mix.exs)
今回利用するライブラリ(HTTPoison, Floki, Timex)をmix.exsに追加します。
githubのライブラリのREADME.mdに
def deps do
[{:httpoison, "~> 0.6"}]
end
のような指定のしかたが書いてあるのでその通りmix.exs5のdepsのところに追加します。
必要なものを全部設定したらまず依存性の解決です。
$ mix deps.get
これで必要なライブラリがgithubからダウンロードされ、ライブラリが展開されます。ただし、初回はパッケージ管理ツールhexがインストールされていなので、
Could not find hex, which is needed to build dependency :httpoison
Shall I Install hex? [Y/n]
と聞いてきます。Yを選択すると自動的にhexがインストールされて次に進みます(親切設計)。
本体ソース (vercheckex.ex)
全ソースで約70行です。短く書けました。
機能仕様についてはGoqueryでWeb スクレイピングしてGithubからリリース情報を取得するを見てください。
defmodule VercheckEx do
# requireで使用するライブラリを指定
require HTTPoison
require Floki
require Timex
use Timex
def fetch_content(url, type) do
ret = HTTPoison.get!( url ) # urlで指定されるページのデータを取得
%HTTPoison.Response{status_code: 200, body: body} = ret
# HTML bodyを取得する
# HTMLパーザー Flokiで処理
# 名前、リリース日時を取得
{_,_,n} = Floki.find(body, "[itemprop=title]") |> List.first
{_, date} = Floki.find(body, "time") |> Floki.attribute("datetime")
|> List.first
|> Timex.DateFormat.parse("{ISOz}")
if(type == :type1) do # バージョン番号を取得
{_,_,x} = Floki.find(body, ".tag-name span") |> List.first
else
{_,_,x} = Floki.find(body, ".css-truncate-target span") |> List.first
end
#UTC時刻をJSTに変更
date |> Timex.Date.Convert.to_erlang_datetime
|> Timex.Date.from "Asia/Tokyo"
{hd(n),hd(x),date} # 戻り値はタプル
end
def put_a_formatted_line(val) do # 1行出力
{title, ver, date} = val
l = title
if String.length(title) < 8 do
l = l <> "\t"
end
l = l <> "\t" <> ver
if String.length(ver) < 8 do
l = l <> "\t"
end
l = l <> "\t" <> Timex.DateFormat.format!(date, "%Y.%m.%d", :strftime)
now = Timex.Date.now("JST")
diff = Timex.Date.diff( date, now, :days) # リリースから今日までの日数
if diff < 14 do # 14日以内なら警告する。以前の仕事が2週間スプリントだった名残り。
l = l <> "\t<<<<< updated at " <> Integer.to_string(diff) <> " day(s) ago."
end
IO.puts(l)
end
end
urls = [
{"https://github.com/jquery/jquery/releases", :type1},
{"https://github.com/angular/angular/releases", :type1},
{"https://github.com/facebook/react/releases", :type2},
{"https://github.com/PuerkitoBio/goquery/releases", :type1},
{"https://github.com/revel/revel/releases", :type2},
{"https://github.com/lhorie/mithril.js/releases", :type1},
{"https://github.com/riot/riot/releases", :type1},
{"https://github.com/atom/atom/releases", :type2},
{"https://github.com/Microsoft/TypeScript/releases", :type2},
{"https://github.com/docker/docker/releases", :type1},
{"https://github.com/JuliaLang/julia/releases", :type2},
{"https://github.com/nim-lang/Nim/releases", :type1},
{"https://github.com/elixir-lang/elixir/releases", :type2},
{"https://github.com/philss/floki/releases", :type1},
{"https://github.com/takscape/elixir-array/releases", :type2},
]
# 逐次呼出し→結果出力 HTTPoison.start
Enum.each(urls, fn(i) ->
{u,t} = i
res = VercheckEx.fetch_content(u,t)
VercheckEx.put_a_formatted_line res
end)
Rubyなどで「クラスを1個定義してそこに全機能入れて、スクリプトのメインから呼び出す」ぐらいの感じで書いてみました。
あんまり解説を読まずに書いちゃったのでElixirの流儀に適ってないかもしれません。もっとこうしたほうがいいというのがあれば教えてください。
Elixirらしいと思ったところ
- コメント行が Erlangの %% から Rubyやシェルスクリプト流の # になっている
- 値の取り出し方がタプル(tuple)によるパターンマッチ(HTTPoisonの結果からbodyを取得するところなど)で行われる。
- 文字列の扱いが楽。文字列結合は**<>**演算子。
- Erlangと同じくwhileなどによるループがない。今回の場合はEnum.each(collection, fn)による逐次実行。fnが無名変数でcollectionの要素が引数として渡される。
- パイプ演算子(>|)が使いやすい。計算結果を次に来る関数の第1引数として渡しているのだがメソッドチェーンぽく直感的に読める(処理Aの結果を処理Bに渡してさらに処理Cに渡す…)
ビルドと実行
$ mix run vercheckex
でビルドと実行が開始されます。もし初めてElixir/Erlangを入れた場合はErlangのビルドツールrebarも入っていないでしょうから以下のようにメッセージが出ます。
Could not find rebar, which is needed to build dependency :idna
I can install a local copy which is just used by mix
Shall I install rebar? [Yn]
Yを選択して進めてください。
しばらくライブラリのビルドなどで時間がかかりますが…(初回のみ)…
うまく動いたら以下のように結果が出力されます。
jquery 1.11.3 2015.04.29
angular 2.0.0-alpha.24 2015.05.20 <<<<< updated at 1 day(s) ago.
react v0.13.2 2015.05.09 <<<<< updated at 12 day(s) ago.
goquery v0.3.2 2013.09.08
revel v0.12.0 2015.03.26
mithril.js v0.1.34 2015.05.01
riotjs v2.1.0 2015.05.20 <<<<< updated at 1 day(s) ago.
atom v0.200.0 2015.05.20 <<<<< updated at 1 day(s) ago.
TypeScript v1.5.0-beta 2015.04.28
docker v1.6.2 2015.05.14 <<<<< updated at 7 day(s) ago.
julia v0.3.8 2015.05.01
Nim v0.11.2 2015.05.05
elixir v1.0.4 2015.04.08
floki v0.2.0 2015.05.04
elixir-array 1.0.1 2014.09.23
でも順番にページの情報を取りに行ってるのでめちゃくちゃ遅いです!!
次回、その2ではプロセスによる並行動作を取り入れて高速化してみます。