Edited at

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

縁あって、東京ですけど、fukuoka.ex でElixirサイコーと叫んでるYOSUKENAKAO.meです。

普段は、ハッカソン等のイベントの運営をしたり、新規事業が中々うまく進まないといった悩みに対して、バックキャスティング思考のプロセスを導入する事で、社員とチームの生産性を高める教育をし、その後、企業の文化にあったオリジナル教材を開発して定着化させる事の支援をしています。

現在は、世界で活躍できるエンジニアの育成を目指して事業を仕込み中です。

仲間募集中なので、是非、今の日本に一石を投じたい方はDMください。

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"}
]
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を仕事として行きたいエンジニアの方いたら、是非是非、交流しましょう!