パパエンジニアのポエム

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

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