LoginSignup
36
20

More than 1 year has passed since last update.

Elixir CSVデータを取り込んで表示する方法

Last updated at Posted at 2019-02-10

簡単Elixirシリーズ

~ Elixir CSVデータを取り込んで表示する方法 ~

この記事は「Elixir Advent Calendar 2022」14日目の記事です
東京にいるけどfukuokaexのYOSUKEです。

2019年にかいた記事をUPデートしました。

本題 : Elixir CSVデータを取り込んで表示する方法

fukuoka.exのpiacereさんが書かれたElixirのチュートリアルExcelから関数型言語マスター1回目:データ行の”並べ替え”と”絞り込み”
は非常に入りやすい教材なのでお勧めです。第7回まであるので、まだやっていない方は是非トライしてみてください。

このElixirのチュートリアルで、JsonAPIからデータ表示や、DBからデータ表示を体験した方向けに、だったらCSVも表示してみたい!というご要望に応えるものです。(あるのか知りませんがw)

おさらい DBアクセスモジュールを作る

Excelから関数型言語マスター3回目:WebにDBデータ表示【PostgreSQL or MySQL編】
より、DBアクセスモジュールを作るという項目があります。
CSVデータの読み込みも、最終的にはdbの読み込みと似たような考え方で実装できそうなので、これを参考にして行きましょう。

ただ、せっかくなので、今回は別の書き方で実装してみます。

defmodule Db do

  def query(sql) do
    case Ecto.Adapters.SQL.query( YourProjectName.Repo  ,sql, []) do
      {:ok, result } -> result
      {:error, _result } -> "sql error"
    end
  end

  def columns_rows(result) do
     Enum.map(result.rows, fn row -> to_map(result.columns, row) end )
  end

  defp to_map(column, row) do
    List.zip([ column, row ])
    |> Enum.into(%{})
  end

end

解説

Db.query関数では、単純に Ecto.Adapters.SQL.queryを利用すると結果が
{:ok, result } というタプル形式で返ってきます。

case 文を使って:okが返って来た時にresultだけを取得できるようにしています。
:errorが返って来た時には、"sql error"を返しています。

試しに、取得されるData構造を見てみましょう。

%Postgrex.Result{
   ["id", "name", "age", "team", "position", "inserted_at",
   "updated_at"],
  command: :select,
  connection_id: 8899,
  messages: [],
  num_rows: 2,
  rows: [
    [1, "YOSUKENAKAO.me", 40, "The Waggle", "CEO",
     ~N[2019-02-04 13:22:01.000000], ~N[2019-02-04 13:22:01.000000]],
    [2, "YOSUKENAKAO.me", 40, "fukuoka.ex", "シュミリクサー",
     ~N[2019-02-04 14:30:53.000000], ~N[2019-02-04 14:30:53.000000]]
  ]
}

構造体のDataで、columns:とrows:にそれぞれリスト形式でデータが入っている事がわかります。

この構造体のデータから、扱いやすいようにマップ型のデータに変換しているのがDb.to_map関数になります。

解説

defp to_map(column, row) do
    List.zip([ column, row ])
    |> Enum.into(%{})
end

この関数の結果作りたいdataはこちらになります。

%{
    "id" => 1,
    "inserted_at" => ~N[2019-02-04 13:22:01.000000],
    "name" => "YOSUKENAKAO.me",
    "age" => 40,
    "team" => "The Waggle",
    "position" => "CEO"
    "updated_at" => ~N[2019-02-04 13:22:01.000000]
  }

List.zip([[1,2,3],["a","b","c"]])関数はこのようなリストのデータを
[{1, "a"}, {2, "b"}, {3, "c"}]というようにタプルのデータを格納したリストデータに変換してくれます。

タプルを格納したリストデータにできると、Enum.intoを使って、マップ型のデータに変換する事ができます。
Enum.into([{1, "a"}, {2, "b"}, {3, "c"}], %{})
とすると、%{1 => "a", 2 => "b", 3 => "c"}と返ってきます。

ただ、このままだと rows:のデータは[[],[],...[]]このようなリスト構造になるので、繰り返しto_map関数に引き渡したい所です。
その願いを叶えてくれる簡単な関数がEnum.mapになります。


def columns_rows(result) do
     Enum.map(result.rows, fn row -> to_map(result.columns, row) end )
end

CSVを読み込むモジュールを作る

DBの読み込みモジュールを踏まえた上で、CSVの読み込みモジュールを作って行きましょう。と、その前に簡単にCSVデータをdecodeしてくれるモジュールが既にあるので、そちらを利用したいと思います。

mix.exs ファイルのdepsに {:csv, "~> 2.0.0"}を追記します。

mix.exs

defp deps do
    [
   :
     省略
     # {:csv, "~> 2.0.0"} 2019年時点のバージョン
     {:csv, "~> 3.0"}
    ]
end

コンソールから、mix deps.getでモジュールを追加してください。

mix deps.get

今回は、CSVモジュールを使うので、CSVのドキュメント読んで使い方を参考にしてデータを取得してみます。

参考

iex> "../test/fixtures/docs/valid.csv"
iex> |> Path.expand(__DIR__)
iex> |> File.stream!
iex> |> CSV.decode!
iex> |> Enum.take(2)
[["a","b","c"], ["d","e","f"]]

ファイルからCSV.decode!経由して、Enum.take(2)でCSVのRowデータをリスト型で2つ取得する事ができるようです。

Enum.takeの引数に数値を入れる事でその数値分を取得できるのですが、CSVデータ全部を読み込みたいので、ここを少し変えれば簡単にできそうですね。

作成した、util フォルダ([Excelから関数型言語マスター3回目:WebにDBデータ表示【PostgreSQL or MySQL編】で作成済み)の中に、csvread.exdata_csv.ex ファイルを作成します。

モジュール名が若干イケテナイのですが、そこはご愛嬌でw

csvread.ex

defmodule CsvRead do

 def stream(file_path) do
    file_path
    |> Path.expand(__DIR__)
    |> File.stream!
    |> CSV.decode
    |> Enum.to_list
    |> Enum.map(fn row -> tuple_to_list(row) end)
  end

end

解説

stream関数では、CSVモジュールの参考例を元に書いています。変えた部分は、Enum.takeEnum.to_list に変更しています。これで、全てのデータを取得してくれます。

ですが、データ形式は [ {:ok, row_data},...{} ]  一行毎に { status, data } という形のタプルで返って来ます。このタプル形式をリストに変更したいので、tuple_to_list関数を作って、リストデータに変換します。
:error の場合は今回は簡単にする為に、空のリストを返しています。

csvread.ex
defmodule CsvRead do

  省略

  defp tuple_to_list(tuple_data) do
    case tuple_data do
      {:ok, list_data} -> list_data
      {:error, _list_data } -> []
    end
  end

end

あとは、これをDbモジュールの時のようにマップ型にすれば、既に知っている方法で使えそうです。
%Postgrex.Result構造体を思い出して見ましょう。(最初の方に記載してます)
構造体の中で利用したのは、columns:rows: でした。

そこで、この2つのデータを保持する構造体を定義して、それぞれにデータを入れてあげれば良さそうです。
先ほど作成した。data_csv.ex に構造体の定義を書いて行きましょう。

構造体の定義には、defstruct を使います。

data_csv.ex

defmodule Data_csv do
  defstruct [:columns, :rows]
end

それでは、構造体にデータを追加する関数を追記しましょう。

csvread.ex

defmodule CsvRead do

  def stream(file_path) do
    file_path
    |> Path.expand(__DIR__)
    |> File.stream!
    |> CSV.decode
    |> Enum.to_list
    |> Enum.map(fn row -> tuple_to_list(row) end)
  end

  def csv_struct(head, tail) do # <- この関数を追記 
    %Data_csv{columns: head, rows: tail }
  end

  defp tuple_to_list(tuple_data) do
    case tuple_data do
      {:ok, list_data} -> list_data
      {:error, _list_data } -> []
    end
  end

end

それでは、CSVデータを読み込んで表示して行きましょう。
utf-8のCSVを用意しました。場所はプロジェクトフォルダの直下に置いています。

今回は、こちらのオープンデータを利用しました。AED情報(東京都品川区)

templates/page/index.html.eex
<%

path = "../../sample_utf8.csv"
[head | tail] = CsvRead.stream(path)
res = CsvRead.csv_struct(head, tail)
columns = res.columns
recode = Db.columns_rows(res)

%>

<table border="1">
<tr>
<%= for column <- columns do %>
<th><%= column %></th>
<% end %>
</tr>
<%= for recode_row <- recode do %>
<tr>
<%= for column <- columns do %>
<td><%= Map.get(recode_row,column) %></td>
<% end %>
</tr>
<% end %>
</table>

これで、無事にデータを表示できました。

スクリーンショット 2019-02-10 23.26.01.png

これを応用すれば、地図にCSVデータから取得した位置情報をマッピングする事も簡単です。

スクリーンショット 2019-02-10 23.26.16.png

終わり

今回は、CSVデータを簡単に読み込んで取得できるようにしてみました。
Elixirって楽しそう!と思って頂ければ幸いです!

p.s

良かったら、是非 いいねをよろしくお願いします。
また、プログラミング未経験者から学べる教材開発もして行きますので、こんな内容学びたい!という要望があれば、どしどしご意見ください。

東京近辺でElixirを仕事として行きたいエンジニアの方いたら、是非是非、交流しましょう!

36
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
20