Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Read and write CSV records

std.csv reads a table one record at a time, so a large file never has to fit in memory. A reader sits on any byte stream and fills the same Record on every call:

import std.bytes
import std.csv
import std.io

type CsvInputError = ParseError | std.csv.CsvError | std.bytes.BytesError

def main() -> Result[(), AllocError | IoError | CsvInputError | OverflowError[i64]]:
    text = "item,price,note\r\npen,3,\"blue, fine\"\r\nbook,12,\"says \"\"read me\"\"\"\r\ncard,2,\"two\nlines\"\r\n"
    mut input = std.csv.reader(std.io.cursor(std.bytes.from_text(text)?), &std.csv.rfc4180(), 16)?
    mut record = std.csv.record()?
    input.read(&mut record)?
    print(str.repr(record.texts()?)?)?
    mut total = 0
    while input.read(&mut record)?:
        total += i64.parse(&record.text(1)?)?
        print(record.number(), record.line(), record.text(0)?, str.repr(record.text(2)?)?)?
    print(total, input.records())?
    Ok(())
["item", "price", "note"]
2 2 pen "blue, fine"
3 3 book "says \"read me\""
4 4 card "two\nlines"
17 4

read returns True while there is another record and False at the end of the input. The reader’s buffer is the 16 bytes passed to reader; records longer than that are assembled across several fills. Quoted fields may hold the delimiter, line breaks, and doubled quotes, and arrive without their quoting. The first record is not special, so the program reads the header itself.

Fields are bytes. text(index) checks one field as UTF-8 and returns a copy you own; texts() copies them all. The record itself is overwritten by the next read, which is why read takes it as &mut.

rfc4180() is the usual dialect: comma, double quote, and every record as wide as the first. A record of another width is reported, but it has been read and the reader carries on. Malformed input stops the reader for good:

import std.bytes
import std.csv
import std.io
from std.csv import CsvError

def main() -> Result[(), AllocError | IoError | CsvError | std.bytes.BytesError]:
    text = "a,b\nc\nd,e\nf,g\"h\ni,j\n"
    mut input = std.csv.reader(std.io.cursor(std.bytes.from_text(text)?), &std.csv.rfc4180(), 16)?
    mut record = std.csv.record()?
    mut more = True
    while more:
        outcome = input.read(&mut record)
        match outcome:
            case Ok(found):
                if found:
                    print(str.repr(record.texts()?)?)?
                else:
                    more = False
            case Err(cause):
                print(str.repr(&cause)?)?
                match cause:
                    case CsvError.Width(_, _, _):
                        print("skipped", str.repr(record.texts()?)?)?
                    case _:
                        more = False
    print(str.repr(input.read(&mut record))?)?
    Ok(())
["a", "b"]
std.csv.CsvError.Width(2, 2, 1)
skipped ["c"]
["d", "e"]
std.csv.CsvError.Invalid(std.csv.Fault.BareQuote, std.csv.Position(record=4, field=1, line=4, offset=13))
Result[bool, std.csv.CsvError].Err(std.csv.CsvError.Stopped)

Width(2, 2, 1) says record 2 should have two fields and has one. The Position names the record, the field index, the line, and the byte offset of the stray quote. Record i,j is never reached: after a broken quote there is no trustworthy place to resume.

A writer builds one record from fields and delivers it at end_record. texts does both for a list. Change the dialect’s public fields before creating the writer:

import std.bytes
import std.csv
import std.io
from std.csv import Newline

def main() -> Result[(), AllocError | IoError | std.bytes.BytesError | std.csv.CsvError]:
    mut dialect = std.csv.rfc4180()
    dialect.newline = Newline.Lf
    dialect.max_field = 12
    mut output = std.csv.writer(std.io.stdout(), &dialect)?
    output.texts(&["item", "note"]?)?
    output.texts(&["pen", "blue, fine"]?)?
    output.text("book")?
    output.text("says \"hi\"")?
    output.end_record()?
    print(str.repr(output.texts(&["card"]?))?)?
    print(str.repr(output.text("much too long for a field"))?)?
    raw = std.bytes.from_text("card:plain")?
    output.field(&raw[:4])?
    output.field(&raw[5:10])?
    output.end_record()?
    output.flush()?
    print(output.records())?
    Ok(())
item,note
pen,"blue, fine"
book,"says ""hi"""
Result[(), std.csv.CsvError].Err(std.csv.CsvError.Width(4, 2, 1))
Result[(), std.csv.CsvError].Err(std.csv.CsvError.Rejected(std.csv.Fault.FieldTooLong, 4, 0))
card,plain
4

Only fields that need quotes get them. The one-field record is refused by the width policy and the oversized text by max_field; neither writes anything. field adds a byte range without building a string first. Call flush when you are done: a writer does not flush or close its stream when it goes out of scope.

Limits apply to reading too. A field over max_field bytes or a record over max_record is an error at the byte that does not fit, so a file with a missing closing quote cannot consume all memory.

See the module reference for the exact grammar, every error, the borrowed data() view, and what allocates.