Install the CSV library
mainAdd {:csv, "~> 3.2"} to your dependencies in mix.exs to use the library in your Elixir project.
defp deps do
[{:csv, "~> 3.2"}]
endrepository·main·Indexed 19 days ago
https://github.com/beatrichartz/csvAn RFC 4180 compliant, composable CSV parsing and encoding library for Elixir designed for high-performance data pipelines. It supports streaming of bytes or lines, providing functions like CSV.decode/1 for resilient parsing and CSV.encode/1 for transforming tables into CSV streams. The library includes a strict mode via CSV.decode!/1, customizable separators and escape characters, and a CSV.Encode protocol for custom data types.
Add {:csv, "~> 3.2"} to your dependencies in mix.exs to use the library in your Elixir project.
defp deps do
[{:csv, "~> 3.2"}]
endThe 3.x version streamlines the API and uses binary matching. Key breaking changes include:
:num_workers and :worker_work_ratio options.CSV now expects line breaks. If you previously split strings manually, pass the string as a single-item list: ["a,b,c\nd,e,f"] |> CSV.decode().StrayQuoteError is now StrayEscapeCharacterError.:strip_fields with :field_transform (e.g., field_transform: &String.trim/1).:validate_row_length now defaults to false. Set to true for 2.x behavior.:escape_formulas is now :unescape_formulas for decode and decode!.:replace is removed. Use :field_transform to handle incorrect encoding manually.:escape_max_lines now defaults to 10 (previously 1000).For large files, performance is best when streaming with :read_ahead in byte mode. While 1000 bytes is a common default, you should tune this based on your environment.
File.stream!("data.csv", [read_ahead: 100_000], 1000) |> CSV.decode()Use CSV.encode/1 to transform a table (a two-dimensional list) into a stream of lines ready for output (e.g., writing to a file or IO).
# Writing a table to a file
table_data |> CSV.encode |> Enum.each(&IO.write(file, &1))You can implement the CSV.Encode protocol to define how custom data types should be encoded into CSV format.
defimpl CSV.Encode, for: MyData do
def encode(%MyData{has: fun}, env \ []) do
"so much #{fun}" |> CSV.Encode.encode(env)
end
endUse CSV.decode!/1 for strict mode. This returns a two-dimensional list (a list of lists) and will raise an exception immediately upon encountering the first error, aborting the operation.
You can use the unredact_exceptions: true option to ensure the source data is included in any exceptions thrown.
# Strict mode returns a list of lists and raises on error
File.stream!("data.csv") |> CSV.decode!
# Unredact source data in exceptions
File.stream!("data.csv") |> CSV.decode!(unredact_exceptions: true)Use CSV.decode/1 to transform a stream of bytes or lines into a stream of row tuples.
In normal mode, it returns a stream of {:ok, [fields]} or {:error, "message"} tuples. It is designed to be resilient by reparsing lines after an unterminated escape sequence to ensure all correctly formatted rows are captured.
Common usage patterns include:
# Decode file line by line
File.stream!("data.csv")
|> CSV.decode()
# Decode a UTF-16 file with BOM
File.stream!([:trim_bom, encoding: {:utf16, :little}])
|> CSV.decode()
# Decode file in chunks of 1000 bytes
File.stream!("data.csv", [], 1000)
|> CSV.decode()
# Decode a csv formatted string
["long,csv,string\nwith,multiple,lines"]
|> CSV.decode()
# Decode a list of arbitrarily chunked csv data
["list,", "of,arbitrarily", "\nchun", "ked,csv,data\n"]
|> CSV.decode()When calling CSV.decode/2 or CSV.decode!/2, you can pass an options keyword list to customize parsing:
separator: Specify a custom separator (e.g., separator: ?;).escape_character: Specify a custom escape character (e.g., escape_character: ?@).field_transform: A function applied to each field during parsing (e.g., field_transform: &String.trim/1).unescape_formulas: Boolean to unescape formulas that have been escaped.redact_errors: Boolean to redact source data in error tuples produced by decode/1.# Example: custom separator and field transformation
stream |> CSV.decode(separator: ?;, field_transform: &String.trim/1)
# Example: unescaping formulas
stream |> CSV.decode(unescape_formulas: true)
# Example: redacting errors
stream |> CSV.decode(redact_errors: true)Customize the encoding process using CSV.encode/2:
separator: Specify a custom separator (e.g., separator: ?;).escape_character: Specify a custom escape character (e.g., escape_character: ?@).headers:headers: ["z", "a"].headers: [a: "x", b: "y"].# Using a semicolon separator
your_data |> CSV.encode(separator: ?;)
# Encoding maps with specific header positions
[%{"a" => "value!"}] |> CSV.encode(headers: ["z", "a"])
# Output: ["z,a\r\n", ",value!\r\n"]
# Encoding maps with custom header names via keyword list
[%{a: "value!"}] |> CSV.encode(headers: [a: "x", b: "y"])
# Output: ["x,y\r\n", "value!,\r\n"]You can override the default comma separator and CRLF delimiter by passing :separator (as a codepoint) and :delimiter (as a string) to the encode/2 function.
[["a\nb", "\tc"], ["de", "\tf\""]]
|> CSV.Encoding.Encoder.encode(separator: ?\t, delimiter: "\n")
|> Enum.take(2)
# => ["\"a\nb\"\t\"\tc\"\n", "de\t\"\tf\"\"\"\n"]When your input stream consists of maps, set headers: true to automatically extract keys from the first map to create a header row, followed by the values of each map as data rows.
[%{"a" => 1, "b" => 2}, %{"a" => 3, "b" => 4}]
|> CSV.Encoding.Encoder.encode(headers: true)
|> Enum.to_list()
# => ["a,b\r\n", "1,2\r\n", "3,4\r\n"]To support custom data types in CSV generation, implement the CSV.Encode protocol. The encode/2 function receives the data to be encoded and an env keyword list containing the current encoding context (such as the separator and delimiter).
By default, the protocol falls back to Any, which converts the data to a string and then uses the BitString encoding implementation.
defimpl CSV.Encode, for: MyCustomType do
def encode(data, env \ []) do
# Your custom encoding logic here
# env can contain :separator, :escape_character, :delimiter, etc.
end
end