ElixirのCSVライブラリnimble_csvの紹介と使用例

5 min read読了の目安(約4800字

はじめに

大量データを一括処理する際に、CSVを利用するシーンは多々あります。今回は、dashbit社が開発しているnimble_csvというライブラリを用いてCSVを処理する方法をまとめてみます。

https://github.com/dashbitco/nimble_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を例に、データの読み込み、加工の例を書いてみます。

users.csv
name,age,role
foo,20,general
bar,25,general
hoge,30,admin
huga,35,admin

CSVファイルを読み込み、フィルターして件数を取得

count.exs
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処理

import.exs
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ではデータの加工がサクサクと行えますので、データ前処理やバッチ処理等にぜひ活用してみてください🚀

この記事に贈られたバッジ