Conserve is a small Common Lisp library for reading and writing RFC 4180 CSV data.
It was made because I was tired of dealing with complicated libraries with many dependencies for parsing simple CSV data file.
In the following documentation we'll read from the following example.csv:
id,name,score
1,foo,88.8
2,bar,100
3,baz,77
CSV files can be read row-by-row or all at once, from a stream or a string. If you want to read from a file it's up to you to open the file (with the appropriate external format) yourself.
To read all rows from a stream, use conserve:read-rows:
(with-open-file (f "example.csv" :direction :input)
(conserve:read-rows f))
; =>
; (("id" "name" "score")
; ("1" "foo" "88.8")
; ("2" "bar" "100")
; ("3" "baz" "77"))
Conserve does not process headers in any special way, nor does it parse values at all. Rows are returned as lists of strings — what you do with that is up to you.
(defun parse-row (row)
(destructuring-bind (id name score) row
(list (parse-integer id)
name
(parse-float:parse-float score))))
(with-open-file (f "example.csv" :direction :input)
(destructuring-bind (header . rows)
(conserve:read-rows f)
(values header (map-into rows #'parse-row rows))))
; =>
; ("id" "name" "score")
; ((1 "foo" 88.8)
; (2 "bar" 100.0)
; (3 "baz" 77.0))
Use conserve:read-row to read a single row at a time:
(with-open-file (f "example.csv" :direction :input)
(conserve:read-row f))
; =>
; ("id" "name" "score")
Note that conserve:read-row has the same interface as most Common Lisp
read-* style functions, so it can often be used in places that expect that
interface:
(iterate
(for (id name nil) :in-file "example.csv" :using #'conserve:read-row)
(finding (parse-integer id) :such-that (string= name "bar")))
; => 2
Both reading functions support reading from a string instead of a stream:
(conserve:read-row "foo,\"a,b,c\",bar")
; => ("foo" "a,b,c" "bar")
Much like reading, Conserve supports writing one or many rows at a time.
Use conserve:write-rows to write a list of rows:
(with-open-file (f "out1.csv" :direction :output)
(conserve:write-rows '(("id" "name" "score")
("1" "foo" "88.8")
("2" "bar" "100.0")
("3" "baz" "77.0"))
f))
Use conserve:write-row to write a single row at a time:
(with-open-file (f "out2.csv" :direction :output)
(conserve:write-row '("id" "name" "score") f)
(conserve:write-row '("1" "foo" "88.8") f)
(conserve:write-row '("2" "bar" "100.0") f)
(conserve:write-row '("3" "baz" "77.0") f))
Rows must be a list of strings — Conserve does not attempt to guess how you would like to serialize other objects to strings.
If nil is passed as a stream, Conserve will return the resulting CSV as
a string:
(conserve:write-row '("foo"
"some \"quoted\" field"
"comma,field")
nil)
; =>
"foo,\"some \"\"quoted\"\" field\",\"comma,field\"
"
Conserve allows one single piece of customization: the choice of delimiter to
use. You can change the delimiter by binding conserve:*delimiter*:
(let ((conserve:*delimiter* #\,))
(conserve:write-row '("a" "b") nil))
; =>
"a,b
"
(let ((conserve:*delimiter* #\|))
(conserve:write-row '("a" "b") nil))
; =>
"a|b
"
(let ((conserve:*delimiter* #\tab))
(conserve:write-row '("foo,bar" "foo|bar") nil))
; =>
"foo,bar foo|bar
"
The test suite include both hardcoded tests against particular edge cases, as
well as round-trip fuzz testing against cl-csv and fare-csv to make sure it
produces similar results. You will need to Quickload those other CSV parsers to
run the test suite (but not to use Conserve itself, of course).