簡単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"}
を追記します。
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.ex
とdata_csv.ex
ファイルを作成します。
モジュール名が若干イケテナイのですが、そこはご愛嬌でw
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.take
をEnum.to_list
に変更しています。これで、全てのデータを取得してくれます。
ですが、データ形式は [ {:ok, row_data},...{} ]
一行毎に { status, data }
という形のタプルで返って来ます。このタプル形式をリストに変更したいので、tuple_to_list関数を作って、リストデータに変換します。
:error
の場合は今回は簡単にする為に、空のリストを返しています。
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
を使います。
defmodule Data_csv do
defstruct [:columns, :rows]
end
それでは、構造体にデータを追加する関数を追記しましょう。
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情報(東京都品川区)
<%
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>
これで、無事にデータを表示できました。
これを応用すれば、地図にCSVデータから取得した位置情報をマッピングする事も簡単です。
終わり
今回は、CSVデータを簡単に読み込んで取得できるようにしてみました。
Elixirって楽しそう!と思って頂ければ幸いです!
p.s
良かったら、是非 いいねをよろしくお願いします。
また、プログラミング未経験者から学べる教材開発もして行きますので、こんな内容学びたい!という要望があれば、どしどしご意見ください。
東京近辺でElixirを仕事として行きたいエンジニアの方いたら、是非是非、交流しましょう!