Home

Awesome

ftcsv

Run Tests and Code Coverage Coverage Status

ftcsv is a fast csv library written in pure Lua. It's been tested with LuaJIT 2.0/2.1 and Lua 5.1, 5.2, 5.3, and 5.4

It features two parsing modes, one for CSVs that can easily be loaded into memory (up to a few hundred MBs depending on the system), and another for loading files using an iterator - useful for manipulating large files or processing during load. It correctly handles most csv (and csv-like) files found in the wild, from varying line endings (Windows, Linux, and OS9), UTF-8 BOM support, and odd delimiters. There are also various options that can tweak how a file is loaded, only grabbing a few fields, renaming fields, and parsing header-less files!

Installing

You can either grab ftcsv.lua from here or install via luarocks:

luarocks install ftcsv

Parsing

There are two main parsing methods: ftcv.parse and ftcsv.parseLine. ftcsv.parse loads the entire file and parses it, while ftcsv.parseLine is an iterator that parses one line at a time.

ftcsv.parse(fileName [, options])

ftcsv.parse will load the entire csv file into memory, then parse it in one go, returning a lua table with the parsed data and a lua table containing the column headers. It has only one required parameter - the file name. A few optional parameters can be passed in via a table (examples below).

Just loading a csv file:

local ftcsv = require('ftcsv')
local zipcodes, headers = ftcsv.parse("free-zipcode-database.csv")

ftcsv.parseLine(fileName [, options])

ftcsv.parseLine will open a file and read options.bufferSize bytes of the file. bufferSize defaults to 2^16 bytes (which provides the fastest parsing on most unix-based systems), or can be specified in the options. ftcsv.parseLine is an iterator and returns one line at a time. When all the lines in the buffer are read, it will read in another bufferSize bytes of a file and repeat the process until the entire file has been read.

If specifying bufferSize there are a couple of things to remember:

Parsing through a csv file:

local ftcsv = require("ftcsv")
for index, zipcode in ftcsv.parseLine("free-zipcode-database.csv") do
    print(zipcode.Zipcode)
    print(zipcode.State)
end

Options

The options are the same for parseLine and parse, with the exception of loadFromString and bufferSize. loadFromString only works with parse and bufferSize can only be specified for parseLine.

The following are optional parameters passed in via the third argument as a table.

For all tested examples, take a look in /spec/feature_spec.lua

The options can be string together. For example if you wanted to loadFromString and not use headers, you could use the following:

ftcsv.parse("apple,banana,carrot", {loadFromString=true, headers=false})

Encoding

ftcsv.encode(inputTable [, options])

ftcsv.encode takes in a lua table and turns it into a text string that can be written to a file. You can use it to write out a file like this:

local users = {
	{name="alice", fruit="apple"},
	{name="bob", fruit="banana"},
	{name="eve", fruit="pear"}
}
local fileOutput = ftcsv.encode(users)
local file = assert(io.open("ALLUSERS.csv", "w"))
file:write(fileOutput)
file:close()

Options

Error Handling

ftcsv returns a litany of errors when passed a bad csv file or incorrect parameters. You can find a more detailed explanation of the more cryptic errors in ERRORS.md

Delimiter no longer required from 1.4.0!

Starting with version 1.4.0, the delimiter no longer required as the second argument. But don't worry, ftcsv remains backwards compatible! We check the argument types and adjust parsing as necessary. There is no intention to remove this backwards compatibility layer, so your existing code should just keep on working!

So this works just fine:

ftcsv.parse("a>b>c\r\n1,2,3", ">", {loadFromString=true})

as well as:

ftcsv.encode(users, ",")

The delimiter as the second argument will always take precedent if both are provided.

Benchmarks

We ran ftcsv against a few different csv parsers (PIL/csvutils, lua_csv, and lpeg_josh) for lua and here is what we found:

20 MB file, every field is double quoted

ParserLuaLuaJIT
PIL/csvutils1.754 +/- 0.136 SD1.012 +/- 0.112 SD
lua_csv4.191 +/- 0.128 SD2.382 +/- 0.133 SD
lpeg_josh0.996 +/- 0.149 SD0.725 +/- 0.083 SD
ftcsv1.342 +/- 0.130 SD0.301 +/- 0.099 SD

12 MB file, some fields are double quoted

ParserLuaLuaJIT
PIL/csvutils1.456 +/- 0.083 SD0.691 +/- 0.071 SD
lua_csv3.738 +/- 0.072 SD1.997 +/- 0.075 SD
lpeg_josh0.638 +/- 0.070 SD0.475 +/- 0.042 SD
ftcsv1.307 +/- 0.071 SD0.213 +/- 0.062 SD

LuaCSV was also tried, but usually errored out at odd places during parsing.

NOTE: times are measured using os.clock(), so they are in CPU seconds. Each test was run 30 times in a randomized order. The file was pre-loaded, and only the csv decoding time was measured.

Benchmarks were run under ftcsv 1.2.0

Performance

I did some basic testing and found that in lua, if you want to iterate over a string character-by-character and compare chars, string.byte performs faster than string.sub. As such, ftcsv iterates over the whole file and does byte compares to find quotes and delimiters and then generates a table from it. When using vanilla lua, it proved faster to use string.find instead of iterating character by character (which is faster in LuaJIT), so ftcsv accounts for that and will perform the fastest option that is available. If you have thoughts on how to improve performance (either big picture or specifically within the code), create a GitHub issue - I'd love to hear about it!

Contributing

Feel free to create a new issue for any bugs you've found or help you need. If you want to contribute back to the project please do the following:

  1. If it's a major change (aka more than a quick bugfix), please create an issue so we can discuss it!
  2. Fork the repo
  3. Create a new branch
  4. Push your changes to the branch
  5. Run the test suite and make sure it still works
  6. Submit a pull request
  7. Wait for review
  8. Enjoy the changes made!

Licenses