パパエンジニアのポエム

奥さんと娘ちゃんへの愛が止まらない

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

これでSSH接続可能。
インスタンス情報を表示してみる。

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のデータを更新できた。

現時点のまとめ

  • 株価サイトをスクレイピングして
  • 更新日付を見てデータがなければMySQLにINSERT
  • 更新日付を見てデータがあればMySQLにUPDATE

なアプリケーションが出来上がった(はず)。
次回は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

ElixirでMySQLに株価をInsert

前回の続き。
今回はO/RマッパーであるEctoを使い、MySQLにデータを保存する。

Ectoをセットアップする

mix.exsのdepsに下記を追加しライブラリをダウンロードする。

{:ecto, "~> 2.1.4"},
{:mariaex, "~> 0.8.2"},

リポジトリを作成する

rオプションでリポジトリ名をつけてコマンドを実行する。

mix ecto.gen.repo -r StockScraping.Repo

すると、config/config.exslib/stock_scraping/repo.exが作成される。

config.exsを設定する

config.exsの各フィールドにMySQLの接続情報を入れる。
最後の行は自動生成されないので追記する。

use Mix.Config

config :stock_scraping, StockScraping.Repo,
  adapter: Ecto.Adapters.MySQL,
  database: "stock_scraping",
  username: "ytoida",
  password: "",
  hostname: "localhost",
  port: 3306

config :stock_scraping, ecto_repos: [StockScraping.Repo]

Supervisorの設定

SupervisorにStockScraping.Repoを監視させる。
Supervisorについてはよくわかってないので、いつかまとめたもの書く。
lib/stock_scraping/application.exのchildrenにworkerを追加する。

defmodule StockScraping.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(StockScraping.Repo, []),
    ]

    opts = [strategy: :one_for_one, name: StockScraping.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

これで、StockScraping.Repoプロセスが死んでもすぐに再起動してくれるはず。
ここまでがEctoのテンプレ設定っぽい。

Schemaを作成する

Ecto.Schema – Ecto v2.1.4
Ecto.Schemaを、lib/stock_scraping/[テーブル名].exで作成する。
この時@primary_keyを設定することを忘れない。{:id, :id, autogenerate: true}がデフォルトなので要注意。

defmodule StockScraping.YahooVolumeDate do
  use Ecto.Schema

  @primary_key {:date, :naive_datetime, autogenerate: false}
  schema "yahoo_volume_date" do
    field :add_date, :naive_datetime
    field :updt_date, :naive_datetime
  end
end

ここまででMySQLに接続出来るはず。

MySQLからテストデータをSelectしてみる

コマンドラインからMySQLのデータが取得出来るか確認する。

StockScraping.YahooVolumeDate |> StockScraping.Repo.all

f:id:yuki-toida:20170606133300p:plain
とれてる。

MySQLにInsertしてみる

流れとしては、対象サイトの更新日時を取得してDateにコンバート。
そのままPKにしてInsertする、以下抜粋。

if StockScraping.YahooVolumeDate |> get_by(date: date) do
  # データが存在している場合はUpdate(後日実装)
else
  # データが存在していない場合はInsert
  naive_datetime = Timex.to_naive_datetime(Timex.local())
  row = %StockScraping.YahooVolumeDate
  {
    date: date,
    add_date: naive_datetime,
    updt_date: naive_datetime
  }
  insert(row)
end

これで、MySQLへの接続とSelect/Insertは実装出来た。
次回はUpdateの処理と、AWSの無料枠を使いスケジューリングしたい。

Elixirで株価をスクレイピングする

開発ブログに再挑戦。
Elixir習得のため、株価スクレイピングアプリをElixirで書いてAWSで運用するまでを一旦の目標にする。

開発環境を構築する

OS無しノートPCにUbuntu入れた。スペック低いけど、プログラミングかブラウジングしかしないので問題ない。

Elixirをインストールする

公式見ながらコマンド実行。
Installing Elixir - Elixir

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt-get update
sudo apt-get install esl-erlang
sudo apt-get install elixir

これでスッとインストール出来るはず。

Mixを使いプロジェクトを作成する

Mixという便利なプロジェクト管理ツールを使い、Supervisor作成オプション付きでプロジェクトを作成する。

mix new stock_scraping --sup

ライブラリの依存関係を解決する

mix.exsに使用するライブラリを追記する。
使用するライブラリは以下。

最終的にコマンドラインで実行するためescriptを使う。
escriptの実行モジュール記載を忘れないこと。
実際のmix.exsこちら(ファイル名を表示する方法が分からん)。

defmodule StockScraping.Mixfile do
  use Mix.Project

  def project do
    [app: :stock_scraping,
     version: "0.1.0",
     elixir: "~> 1.4",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     escript: [main_module: StockScraping],
     deps: deps()]
  end

  def application do
    [extra_applications: [:logger],
     mod: {StockScraping.Application, []}]
  end

  defp deps do
    [
      {:timex, "~> 3.1"},
      {:httpoison, "~> 0.11.2"},
      {:floki, "~> 0.17.0"},
      {:certifi, "1.1.0", override: true},
      {:idna, "4.0.0", override: true},
      {:tzdata, "0.1.8", override: true},
    ]
  end
end

コマンドを実行しライブラリをダウンロードする。

mix deps.get

これでプロジェクトのセットアップは完了。

ロジックを実装して株価をスクレイピングする

escriptで実行されるStockScrapingモジュールを実装し、実際に株価をとってくる。
関数型言語書いたことないし、Elixirの文法もよくわからない中、気合で書いたので相当汚いはず笑。
現状その汚さすら理解できない状態のstock_scraping.exこちら。
(ここはこう書けド素人!!!みたいな意見貰えるとありがたい)

defmodule StockScraping do
  use Timex

  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
    parse_datetime(body)
    parse_volume(body)
  end

  defp parse_datetime(body) do
    date_list = Floki.find(body, "div.dtl")
    |> 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
    updttime = Timex.to_datetime({year, month, day}, "Asia/Tokyo")
  end

  defp parse_volume(body) do
    Floki.find(body, "tr.rankingTabledata")
    |> Enum.each(fn(x) ->
      element = elem(x, 2)
      rank = get_value(element, 0)
      code = get_code(element)
      market = get_value(element, 2)
      name = get_value(element, 3)
      price = get_value(element, 5)
      volume = get_value(element, 6)
      volume_average = get_value(element, 7)
      volume_rate = get_value(element, 8)
      end
    )
  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

escriptを使ってビルドする

ビルドコマンド実行。

mix escript.build

通ればルートにstock_scrapingというEarlangVM上で実行可能なバイナリファイルが生成される。

escriptを使ってバイナリファイルを実行する

バイナリを実行する。

./stock_scraping

f:id:yuki-toida:20170605134816p:plain

出力された、プログラムがちゃんと動くと嬉しい。
これで一旦値がとれるところまではいけたので、次回MySQLにこいつを突っ込む。