ElixirのCSVライブラリnimble_csvの紹介と使用例
はじめに
大量データを一括処理する際に、CSVを利用するシーンは多々あります。今回は、dashbit社が開発しているnimble_csv
というライブラリを用いてCSVを処理する方法をまとめてみます。
前置き: ライブラリの選定
2021/03/21時点でのawesome-elixirリポジトリのCSVセクションをみると、次のようになっています(star数は自分で確認して追加しました)
library | 用途 | star数 |
---|---|---|
meh/cesso | CSV handling library for Elixir. | 23 |
beatrichartz/csv | CSV Decoding and Encoding for Elixir. | 398 |
Arp-G/csv2sql | A fast and fully automated CSV to database importer. | 19 |
jimm/csvlixir | A CSV reading/writing application for Elixir. | 29 |
erpuno/ecsv | Fast libcsv-based stream parser for Elixir. | 0 |
CargoSense/ex_csv | CSV for Elixir. | 41 |
dashbitco/nimble_csv | A simple and fast CSV parsing and dumping library for Elixir. | 550 |
現時点では
のどちらかを選定しておくのがベターそうです。正直どちらでもCSVのdecode/encodeは問題なくできるのでどちらを選んでも問題なさそうですが、今回は開発も活発なnimble_csvを試してみます。
NimbleCSV
基本的な使い方としては
-
NimbleCSV.define/2
でParserを定義 - 定義したParserを用いてstring, enum, streamをparse
という流れです。
RFC4180に準拠したフォーマットのParserはあらかじめライブラリ側で定義(コード参考)されており、自分でdefineせずとも利用できます。
iex(1)> NimbleCSV.RFC4180.parse_string("name,age\nfoo,25")
[["foo", "25"]]
READMEにはこれを CSV
としてaliasする例が書いてあります。これが基本のパターンになります。
alias NimbleCSV.RFC4180, as: CSV
CSV.parse_string "name,age\njohn,27"
[["john","27"]]
,
ではなく \t
も区切り文字として扱いたいという場合は、次のように独自でParserを定義、実行できます。
iex(1)> NimbleCSV.define(MyParser, separator: [",","\t"])
{:module, MyParser,
<<70, 79, 82, 49, 0, 0, 45, 128, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 2, 247,
0, 0, 0, 67, 15, 69, 108, 105, 120, 105, 114, 46, 77, 121, 80, 97, 114, 115,
101, 114, 8, 95, 95, 105, 110, 102, 111, ...>>, :ok}
iex(2)> MyParser.parse_string("name,age\nfoo\t25")
[["foo", "25"]]
ちなみに、ExcelやNumbersといった表計算ソフト向けにBOM付きの形式で読み書きできるNimbleCSV.Spreadsheet
というParserもあらかじめ定義されています(コード参考)。
NimbleCSV.define(NimbleCSV.Spreadsheet,
separator: "\t",
escape: "\"",
encoding: {:utf16, :little},
trim_bom: true,
dump_bom: true,
moduledoc: """
A parser with spreadsheet friendly settings.
The parser uses tab as separator and double-quotes as escape, as required by
common spreadsheet software such as Excel, Numbers and OpenOffice. It's encoded
in UTF-16 little-endian with a byte-order BOM.
"""
)
うまく抽象化されていて、非常に勉強になるコードです。
使用例
次のようなCSVを例に、データの読み込み、加工の例を書いてみます。
name,age,role
foo,20,general
bar,25,general
hoge,30,admin
huga,35,admin
CSVファイルを読み込み、フィルターして件数を取得
alias NimbleCSV.RFC4180, as: CSV
"users.csv"
|> File.stream!()
|> CSV.parse_stream()
|> Stream.filter(fn [_, _, role] -> role == "admin" end)
|> Enum.count()
実行結果:
role=adminとなっているのは2レコードであり、 2
という結果が得られています。
$ mix run count.exs
2
CSVファイルを読み込み、フィルターされた結果を1トランザクションにまとめてinsert処理
alias CsvTutorial.Repo
alias Ecto.Multi
alias NimbleCSV.RFC4180, as: CSV
"users.csv"
|> File.stream!()
|> CSV.parse_stream()
|> Stream.filter(fn [_, _, role] -> role == "admin" end)
|> Enum.reduce(Multi.new(), fn [name, age, role], multi ->
Multi.insert(
multi,
"insert user #{name}",
User.changeset(%User{}, %{name: name, age: age, role: role})
)
end)
|> Repo.transaction()
|> IO.inspect()
実行結果:
roleがadminとなっているレコードのみ、1トランザクションにまとめてinsertされていることがわかります。
$ mix run import.exs
12:56:06.241 [debug] QUERY OK db=2.1ms queue=83.6ms idle=0.0ms
begin []
12:56:06.260 [debug] QUERY OK db=3.6ms
INSERT INTO "users" ("age","name","role") VALUES ($1,$2,$3) RETURNING "id" [30, "hoge", "admin"]
12:56:06.261 [debug] QUERY OK db=1.4ms
INSERT INTO "users" ("age","name","role") VALUES ($1,$2,$3) RETURNING "id" [35, "huga", "admin"]
12:56:06.265 [debug] QUERY OK db=3.8ms
commit []
{:ok,
%{
"insert user hoge" => %User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 30,
id: 3,
name: "hoge",
role: "admin"
},
"insert user huga" => %User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
age: 35,
id: 4,
name: "huga",
role: "admin"
}
}}
まとめ
nimble_csvを使ってCSVを処理する方法を書きました。Elixirではデータの加工がサクサクと行えますので、データ前処理やバッチ処理等にぜひ活用してみてください🚀
Discussion