Elixir - Phoenixで株価を表示する(後編)
前回の続き。
今回は、Volume(日別出来高ランキング)
ページを実装する。
VolumeController
引数に受けた文字列をDate
にキャストする。
その値を使いDBからデータを取得する。
ポイントは where句ではピン演算子
を使うこと。
んでそのままテンプレートに@items
として渡す。
web/controllers/volume_controller.ex
が以下。
defmodule StockScraping.VolumeController do use StockScraping.Web, :controller def index(conn, %{"date" => date}) do target_date = case Date.from_iso8601(date) do {:ok, value} -> value end items = StockScraping.YahooVolume |> where(date: ^target_date) |> Repo.all render conn, "index.html", items: items end end
VolumeView
ヘルパーは今のところ使用していないので、デフォルトのまま。
web/views/volume_view.ex
が以下。
defmodule StockScraping.VolumeView do use StockScraping.Web, :view end
VolumeTemplate
コントローラーから受け取った@items
をレンダリングする。
web/templates/volume/index.html.eex
が以下。
<h3>出来高増加率ランキング</h3> <ul> <%= for item <- @items do %> <li> <%= item.ranking %> - <%= item.name %> </li> <% end %> </ui>
これで一旦表示出来た。
この時点で月別カレンダーと日別出来高ランキングが表示されるようになった。
Elixir - Phoenixで株価を表示する(前編)
MySQLに入っている株価データを表示する。
ページ構成としてはHome
(月別カレンダー)とVolume
(日別出来高ランキング)の2つ。
今回はHomeだけ。
Routing
ルーティングでのポイントは、HomeControllerへのルートは引数有り無しのに種類用意すること。
web/router.ex
が以下。
defmodule StockScraping.Router do use StockScraping.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", StockScraping do pipe_through :browser get "/", HomeController, :index get "/:date", HomeController, :index get "/volume/:date", VolumeController, :index end end
HomeController
ここでは引数で受けた文字列をDate
にキャストしている。
引数がない場合は、現在時刻を使用する。
(キャスト方法は本当にこれであってるんだろうか…)
render conn, "index.html", items: items
とすることでテンプレートで@items
を参照可能になる。
web/controllers/home_controller.ex
の最終形が以下。
defmodule StockScraping.HomeController do use StockScraping.Web, :controller def index(conn, params) do target_date = case params do %{"date" => date} -> case Date.from_iso8601(date) do {:ok, value} -> value end _ -> now() end items = Repo.all(StockScraping.YahooVolumeDate) |> Enum.filter(fn(x) -> x.date.year == target_date.year && x.date.month == target_date.month end) render conn, "index.html", items: items end end
HomeView
ViewへはDateをstringに変換する関数を作成し、テンプレートから呼び出す。
あえてパターンマッチングで実装してみた。
web/views/home_view.ex
が以下。
defmodule StockScraping.HomeView do use StockScraping.Web, :view def convert(date) do case Timex.format(date, "{YYYY}-{0M}-{D}") do {:ok, value} -> value end end end
HomeTemplete
UIおいといてとりあえず表示させる。
ポイントはvolume_path(@conn, :index, convert(item.date))
VolumeControllerのIndexアクションへのURLをクエスパラメータ付きでレンダリングする処理。
Routingのget "/volume/:date", VolumeController, :index
でルートを追加しないと例外でる。
<h3>カレンダー</h3> <ul> <%= for item <- @items do %> <li> <a href="<%= volume_path(@conn, :index, convert(item.date)) %>"><%= item.date %></a> </li> <% end %> </ul>
これで一旦株価を更新した日付の一覧を表示できた。
Elixir - Phoenixでページを作成する
今回はページを作成していく。
Routing
Action
View
Template
を理解する。
Routing
web/router.ex
にHomeController
を追加する。
defmodule StockScraping.Router do use StockScraping.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", StockScraping do pipe_through :browser get "/", HomeController, :index end end
Action
HomeController
を追加する。
web/controllers/home_controller.ex
を新規作成。
defmodule StockScraping.HomeController do use StockScraping.Web, :controller # connはPlug.Conn構造体 def index(conn, _params) do render conn, "index.html" end end
View
Viewはヘルパー関数みたいなものらしい。
web/views/home_view.ex
を追加する。
defmodule StockScraping.HomeView do use StockScraping.Web :view def title do "株出来高増加率ランキング" end end
あえてtitle
という関数を定義している。
これでテンプレートで使用可能となる。
Template
HTMLテンプレートのこと。
web/templates/home/index.html.eex
を新規追加。
HomeView
に追加したtitle/0
を呼び出す。
<p><%= title %></p>
webサーバーを起動させる。
mix phoenix.server
http://localhost:4000 にアクセスするとtitleのみが表示される。
Elixir - Phoenixのセットアップ
このブログで書いたように、
Elixir習得のために作り始めた株価スクレイピングアプリは当初データ収集のみの想定だった。
でも思いの外気合のみで作れてしまい、まるで言語を習得できていないのでWebアプリにしようと思う。
定期的にデータをスクレイピングし、それをHTMLで表示するアプリをAWSで運用する。
ElixirでWebアプリといったら Phoenix らしい。
にしても作者完全にFF好きだな、Elixir
のPhoenix
て。
Phoenix のインストール
Hex のインストール
mix local.hex
Phoenix のインストール
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
node と npm のインストール
sudo apt-get install -y nodejs npm sudo npm cache clean sudo npm install n -g sudo n stable sudo ln -sf /usr/local/bin/node /usr/bin/node # 古いnodejs npm削除 sudo apt-get purge -y nodejs npm
inotify-tools のインストール
Linuxのみ inotify-tools
をインストールする必要があるらしい。
sudo apt-get install inotify-tools
MySQL の設定
Using MySQL · Phoenix
database
オプションをmysql
にしphoenix
プロジェクト作成。
mix phoenix.new stock_scraping --database mysql
configファイルを編集
config/dev.exs
、config/test.exs
、config/prod.secret.exs
を、
自分のMySQLの環境に合わせて修正する。
config :stock_scraping, StockScraping.Repo, adapter: Ecto.Adapters.MySQL, database: "stock_scraping", username: "ytoida", password: "", hostname: "localhost", port: 3306, pool_size: 10
Repo の作成
コンパイルして、StockScraping.Repo
を作る。
mix do deps.get, compile
mix ecto.create
Webサーバー起動
ここまできたらWebサーバーを起動できるはず。
起動してみる。
mix phoenix.server
すると、
[info] Running StockScraping.Endpoint with Cowboy using http://localhost:4000 12 Jun 17:20:46 - info: compiled 6 files into 2 files, copied 3 in 3.8 sec
と出力された。
どうやら http://localhost:4000 でアクセス出来るらしい。
Chromeからアクセスして、Welcomeページ出たらOK牧場。
MySQL(Local)をエクスポートしMySQL(AWS)にインポートする
環境が整ったので、LocalのあるMySQLのデータをAWSに移行する。
MySQL(Local)エクスポート
mysqldumpコマンドを実行すると実行ディレクトリにエクスポートファイルが作成される。
さっそくコマンドを実行。
mysqldump -u[ユーザー名] -p[パスワード] -r [バックアップファイル名] --single-transaction [データベース名]
エクスポートファイルをEC2に転送
scpコマンドで転送する。
これでルートディレクトリにエクスポートファイルが転送される。
chmod 400 ec2.pem scp -i ec2.pem [ファイル名] [EC2ユーザー]@[EC2ホスト]:~
MySQL(AWS)インポート
さきほど転送されたエクスポートファイルをインポートする。
mysql -u[ユーザー名] -p[パスワード] -h[ホスト] [データベース名] < [エクスポートファイル名]
あとはMySQLにログインして対象データベースがインポートされているか確認してみる。
AWSの無料枠内でEC2とMySQLを起動する
昨日飲み過ぎ+寝不足で今日はElixir触れなかった、こんちくしょう。
ということでAWSの設定は終わったのでそれを書く。
IAMの設定
IAM とは - AWS Identity and Access Management
Linuxでrootユーザー使いながら作業しないし、Windowsでadministratorで作業しないよねという話。
admin
グループとadmin
ユーザーを作成し、管理者権限を付与した。
作成完了。
請求アラームの設定
請求アラームの作成 - AWS 請求情報とコスト管理
12ヶ月の無料枠を使って運用するので、1$でも発生したらすぐアラートをメールに送る。
0$ < 過去6時間での請求額を閾値に設定。
EC2
この記事を参考に無料枠ないでインスタンス起動。
sil.hatenablog.com
SSH接続時のIP許可は後ほど設定する。
MySQL
この記事を参考に無料枠ないでry。
qiita.com
EC2からしかアクセス出来ないようにセキュリティグループを組むこと。
AWS CLI のセットアップ
AWS Command Line Interface のインストール - AWS Command Line Interface
pipでインストールするらしい。
sudo apt-get install -y python-pip sudo pip install awscli pip install --upgrade --user awscli
続いてConfig設定。
$ aws configure AWS Access Key ID [None]: *** AWS Secret Access Key [None]: *** Default region name [None]: ap-northeast-1 Default output format [None]: json
aws ec2 describe-instances
載せないけどちゃんと表示された。
SSH 接続
まずはec2サーバーへのSSH接続を許可する。
以下を見ながらローカルのIPアドレスからのSSH接続を許可する。
Linux インスタンス用の受信トラフィックの認可 - Amazon Elastic Compute Cloud
許可したらターミナルからSSH接続できるようになる。
ssh -i "ec2.pem" ec2-user@****
__| __|_ ) _| ( / Amazon Linux AMI ___|\___|___|
こんな出力がされたら成功。
ElixirでMySQLの株価をUpdate
前回の続き。
今度はMySQLのデータを更新してみる。
基本文法でちょっと時間かかった、やはりまずは基礎を勉強してからやるべきだったかも…。
文字列をintgerやfloatやdateに変換する
String – Elixir v1.4.4
Getting Started – timex v3.1.15
この辺みながらスクレイピングした値をパースする。
To integer
str |> String.to_integer()
** (ArgumentError) argument error (stock_scraping) lib/stock_scraping.ex:18: StockScraping.parse/1 (elixir) lib/kernel/cli.ex:76: anonymous fn/3 in Kernel.CLI.exec_fun/2
はい死亡。
よくよくみると文字列が123,456,000
のように区切り文字が入っている。
他にやり方はあるかもしれないが、何も考えずにリプレイスする。
str |> String.replace(",", "") |> String.to_integer()
無事成功。
このパイプライン演算子(|>
)は慣れると結構気持ちいいかもしれない。
To float
こちらは最初から区切り文字をリプレイス。
str |> String.replace(",", "") |> String.to_float()
** (ArgumentError) argument error (stock_scraping) lib/stock_scraping.ex:18: StockScraping.parse/1 (elixir) lib/kernel/cli.ex:76: anonymous fn/3 in Kernel.CLI.exec_fun/2
はい死亡。
文字列が小数になってないとダメらしい、どないしよ。
Float – Elixir v1.4.4
Float.parse/1
でいけそうだ。
** (ArgumentError) argument error
はい、死亡。
よくみると結果がTuple({123456000.0, ""}
)で返ってきてた。
Tupleへのアクセスはelem/2
で行うらしい。
str |> String.replace(",", "") |> String.to_float() |> elem(0)
これで無事パースできた。
To date
Timex使って特に問題なくパース出来た。
year = Enum.at(list, 0) |> String.to_integer month = Enum.at(list, 1) |> String.to_integer day = Enum.at(list, 2) |> String.to_integer date = Timex.to_date({year, month, day})
MySQLのデータを更新する
更新対象のデータを全件取得する(DBへの通信を1回で済ませるため)
この時気をつけるのは、WHERE句にはピン演算子を使うひつようがあるということ。
理由はよくわからない、後で調べる。
targets = StockScraping.YahooVolume |> where(date: ^date) |> all
上記全データから対象PKデータを取得する
ランキング1位のデータを取得。これは例。
target = targets |> Enum.filter(fn(x) -> x.ranking == 1 end) |> hd
変更したいフィールドと値でmapを作る
変更部分をmapにする感じ。
実際の処理は後述。
params = build_tuple(x, date)
Changesetを作成しUpdate
Ecto.Changeset – Ecto v2.1.4
対象データ(target)と変更map(params)を使ってchangeset
を作りupdate
。
changeset = change(target, params)
update(changeset)
これでMySQLのデータを更新できた。
現時点のまとめ
なアプリケーションが出来上がった(はず)。
次回はAWSを使ってデイリーで実行するよう自動化してみる。
最終的なビジネスロジックが書かれているコードはこちら。
近々GitHubにあげる。
defmodule StockScraping do use Timex import StockScraping.Repo import Ecto.Query import Ecto.Changeset def main(args) do HTTPoison.get!("https://info.finance.yahoo.co.jp/ranking/?kd=33&mk=2&tm=d&vl=b") |> parse end defp parse(%{status_code: 200, body: body}) do volume_elements = Floki.find(body, "tr.rankingTabledata") date_elements = Floki.find(body, "div.dtl") date = parse_date(date_elements) if StockScraping.YahooVolumeDate |> get_by(date: date) do targets = StockScraping.YahooVolume |> where(date: ^date) |> all # UPDATE MySQL update_volume(volume_elements, date, targets) else local = Timex.local() naive_datetime = NaiveDateTime.new(DateTime.to_date(local), DateTime.to_time(local)) |> elem(1) IO.inspect(naive_datetime) %StockScraping.YahooVolumeDate { date: date, add_date: naive_datetime, updt_date: naive_datetime } |> insert() # INSERT MySQL add_volume(volume_elements, date) end end defp parse_date(elements) do date_list = elements |> Enum.at(0) |> elem(2) |> Enum.at(1) |> String.replace(~r/[^0-9]/, ",") |> String.split(",", trim: true) year = Enum.at(date_list, 0) |> String.to_integer month = Enum.at(date_list, 1) |> String.to_integer day = Enum.at(date_list, 2) |> String.to_integer Timex.to_date({year, month, day}) end defp update_volume(elements, date, targets) do elements |> Enum.each(fn(x) -> params = build_map(x, date) ranking = params[:ranking] target = targets |> Enum.filter(fn(x) -> x.ranking == ranking end) |> hd changeset = change(target, params) update(changeset) end) end defp add_volume(elements, date) do entities = elements |> Enum.map(fn(x) -> build_map(x, date) end) insert_all(StockScraping.YahooVolume, entities) end defp build_map(value, date) do element = elem(value, 2) %{ date: date, ranking: get_value(element, 0) |> String.to_integer(), code: get_code(element) |> String.to_integer(), market: get_value(element, 2), name: get_value(element, 3), price: get_value(element, 5) |> String.replace(",", "") |> Float.parse() |> elem(0), volume: get_value(element, 6) |> String.replace(",", "") |> String.to_integer(), volume_average: get_value(element, 7) |> String.replace(",", "") |> String.to_integer(), volume_rate: get_value(element, 8) |> String.replace(",", "") |> Float.parse() |> elem(0), } end defp get_value(element, index) do Enum.at(element, index) |> elem(2) |> hd end defp get_code(element) do Enum.at(element, 1) |> elem(2) |> Enum.at(0) |> elem(2) |> hd end end