• 157
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

[翻訳] 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(ビルドツール))をインストールします。

  1. Macだとbrew一発で入ります。
    $ brew install elixir

  2. Windowsのコンパイル済みバイナリのインストーラでも特に問題なく完了します。

  3. 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"

を追加するとかなりいい感じになります。ElixirHaskellPythonのようなオフサイドルール(インデント・ブロック)ではありませんが、関数呼び出し時に()を省略できたりRubyぽくdo...endでブロックが構成できるので簡易的でもいいのでインデントを調整してくれるこういうプラグインは助かります。

お題

GoqueryでWeb スクレイピングしてGithubからリリース情報を取得するをElixirに焼き直します。

実は以前Erlangに興味を持ったときにやはりWeb スクレイピングなプログラムを書きかけて…その時点では「Erlangには文字列という概念はなくすべてバイト列です(キリッ)2という状況だったので挫折していたのでそのリベンジも兼ねて。

ライブラリについて

調べてみるとElixirには既にかなりの数の素敵ライブラリ群が揃っています3
今回利用したものは
1. HTTPoison HTTP Client ライブラリ。プロキシが使える。HTTPotionではプロキシが使えなかった。
2. Floki HTMLパーザー。
3. Timex 時刻・時間関連。
です。

標準ビルドツール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らしいと思ったところ

  1. コメント行が Erlangの %% から Rubyやシェルスクリプト流の # になっている
  2. 値の取り出し方がタプル(tuple)によるパターンマッチ(HTTPoisonの結果からbodyを取得するところなど)で行われる。
  3. 文字列の扱いが楽。文字列結合は<>演算子。
  4. Erlangと同じくwhileなどによるループがない。今回の場合はEnum.each(collection, fn)による逐次実行。fnが無名変数でcollectionの要素が引数として渡される。
  5. パイプ演算子(>|)が使いやすい。計算結果を次に来る関数の第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ではプロセスによる並行動作を取り入れて高速化してみます。


  1. Erlangの分散実行環境って実はIoTに向いているのでは?と考えています。 

  2. 前にも書きましたが今はかなりいろいろ改善されています。 

  3. 言語の設計目的上、やはりサーバー向けのものが多いようです。 

  4. 最近各言語いろいろなビルドツールが標準でついてきますがプログラム本体と同じ文法で設定が書ける、という点でClojureLeiningenを思い出しました。 

  5. Leiningenproject.cljみたいなもんです。