Parse and Generate CSV¶
This recipe covers reading, processing, and writing CSV files — including header mode, custom delimiters, and streaming large files.
Setup¶
Read a CSV file¶
(import (scheme base) (scheme file) (kaappi csv))
;; Simple read — rows as lists of strings
(define rows
(call-with-input-file "data.csv"
(lambda (port)
(csv-read port))))
rows
;=> (("name" "age" "city") ("Alice" "30" "Berlin") ("Bob" "25" "Tokyo"))
Read with headers¶
When the first row is a header, use 'headers #t to get rows as alists:
(define records
(call-with-input-file "data.csv"
(lambda (port)
(csv-read port 'headers #t))))
records
;=> ((("name" . "Alice") ("age" . "30") ("city" . "Berlin"))
; (("name" . "Bob") ("age" . "25") ("city" . "Tokyo")))
;; Access fields by name
(cdr (assoc "name" (car records))) ;=> "Alice"
Parse a CSV string¶
(csv-read-string "a,b,c\n1,2,3\n4,5,6")
;=> (("a" "b" "c") ("1" "2" "3") ("4" "5" "6"))
(csv-read-string "name,score\nAlice,95\nBob,87" 'headers #t)
;=> ((("name" . "Alice") ("score" . "95"))
; (("name" . "Bob") ("score" . "87")))
Filter and transform rows¶
CSV values are always strings. Convert types as needed:
(import (scheme base) (scheme file) (kaappi csv))
(define data
(call-with-input-file "scores.csv"
(lambda (port) (csv-read port 'headers #t))))
;; Keep only scores above 90
(define high-scores
(filter (lambda (row)
(> (string->number (cdr (assoc "score" row))) 90))
data))
;; Extract just the names
(map (lambda (row) (cdr (assoc "name" row)))
high-scores)
Write CSV to a file¶
(define rows
'(("name" "age" "city")
("Alice" "30" "Berlin")
("Bob" "25" "Tokyo")))
(call-with-output-file "output.csv"
(lambda (port)
(csv-write rows port)))
Write CSV to a string¶
(csv-write-string
'(("name" "score")
("Alice" "95")
("Bob" "87")))
;=> "name,score\r\nAlice,95\r\nBob,87\r\n"
Tab-separated values (TSV)¶
Use 'delimiter #\tab:
(csv-read-string "name\tscore\nAlice\t95" 'delimiter #\tab)
;=> (("name" "score") ("Alice" "95"))
(csv-write-string '(("a" "b") ("1" "2")) 'delimiter #\tab)
;=> "a\tb\r\n1\t2\r\n"
Stream large files with fold¶
Process rows one at a time without loading the entire file into memory:
(import (scheme base) (scheme file) (kaappi csv))
;; Sum the "amount" column without loading all rows
(define total
(call-with-input-file "transactions.csv"
(lambda (port)
(csv-read-row port) ;; skip header row
(csv-fold port
(lambda (row acc)
(+ acc (string->number (list-ref row 2))))
0))))
(display total)
(newline)
Handle quoted fields¶
The parser handles RFC 4180 quoting automatically — fields with commas, newlines, or quotes are enclosed in double quotes:
(csv-read-string "name,bio\nAlice,\"Likes commas, really\"\nBob,\"Line 1\nLine 2\"")
;=> (("name" "bio")
; ("Alice" "Likes commas, really")
; ("Bob" "Line 1\nLine 2"))
Escaped quotes ("") inside quoted fields are also handled:
Convert CSV to JSON¶
Combine kaappi-csv and kaappi-json to convert between formats:
(import (scheme base) (scheme file)
(kaappi csv) (kaappi json))
(define records
(call-with-input-file "data.csv"
(lambda (port) (csv-read port 'headers #t))))
(call-with-output-file "data.json"
(lambda (port)
(json-write records port)))
Since header-mode rows are already alists, they serialize directly to JSON objects.
Read one row at a time¶
For custom parsing logic where fold is too rigid:
(call-with-input-file "data.csv"
(lambda (port)
(let loop ((row (csv-read-row port)))
(when row
(display row) (newline)
(loop (csv-read-row port))))))
csv-read-row returns #f at end of file.