CSV parsing for Crystal, at memory speed.
The stdlib CSV module is the easy choice, but it does not hold up on large files. Reading a 1.2 GB Kaggle dataset (7.7M rows, 116M cells) with CSV.each_row takes about 20 seconds. Every cell becomes a String through a buffered IO stream, one character at a time.
csv2 is a drop-in replacement for the stdlib CSV API. It reads the file through memory mapping and materializes each cell with a single byte-span copy. The same 1.2 GB file parses in about 2.6 seconds:
| workload | stdlib CSV | csv2 |
|---|---|---|
| 1.2 GB Kaggle dataset (7.7M rows, 116M cells) | 14.5 s | 2.63 s |
| synthetic 1M-row x 10-column file | 1.39 s | 0.36 s |
Same behavior, roughly 5x less waiting on Windows, Linux and macOS. Measured with the stdlib Benchmark.ips module, parsing each file and reading every cell value.
- Faster: parse gigabyte CSVs in seconds instead of minutes.
- Drop-in: the API matches stdlib CSV, so switching is a one-line change.
- Memory-mapped I/O: the OS hands you the file's bytes directly; no manual reads.
- RFC 4180: quoted fields, embedded commas and newlines, escaped quotes, CRLF, custom separators and quote characters.
- Cross-platform: POSIX and Windows, no runtime dependencies.
require "csv2" # swap this line for "csv"
CSV2::Reader.each_row(file) do |row|
puts row # every cell is a String, like stdlib
end
CSV2::Reader.parse("a,b,c") # => [["a", "b", "c"]]
reader = CSV2::Reader.new("name,age\nJohn,20", headers: true)
reader.next
reader["name"] # => "John"
reader.row.to_h # => {"name" => "John", "age" => "20"}
CSV2::Writer.build { |w| w.row "a", "b" } # => "a,b\n"Everything you already do with CSV works the same way — parse, each_row, headers, row access by name or index, building output, but faster.
Add the dependency to your shard.yml:
dependencies:
csv2:
github: naqvis/csv2Then run shards install.
crystal specMIT
- Ali Naqvi — creator and maintainer