Using command line to process CSV files (2022)
muhammadraza.me
muhammadraza.me
The frawk tool (written in Rust) also supports this.
Interestingly, Brian Kernighan is currently updating the book "The AWK Programming Language" for a second edition (I'm one of the technical reviewers), and Gawk and awk are adding a "--csv" option for this purpose. So real CSV mode is coming to an AWK near you soon!
Sure technically you can do things like having new lines on a line which cause all sorts of grief, but if you control the end to end workflow just avoid it.
I have a tool which monitors performance of loading various websites via various proxies at various points ok our network with pupetteer. That outputs a csv which I can then extract data from and make handy graphs to show manglement.
Yeah, I guess that kinda works if you're the one generating them. But you must not have too many generalised strings -- like a name field containing "Smith, John" or a book title like "The Lion, the Witch and the Wardrobe"? When I deal with CSV's those kind of things are quote common. I actually prefer TSV because you can (more easily) process them with standard AWK.
What do you output for a string field with ',' in it? Just strip the commas, or replace it with something else?
Related example is the recently discussed problems with filenames, and the many pitfalls that scripts can stumble upon when handling them.
The thing is that from the point of view of each individual tool, there isn't even an error! You ask awk to split on commas and give you the second field, it splits on commas and gives you the second field. It opened the file no problem, it didn't run out of memory writing its output to stdout... There were no errors to speak of from what it could see. The "error" is an error in the overall logic of the code.
The problem is that edge cases are not really edge cases… in certain locales. ;) Someone could be using commas as the decimal separator since that’s how they do it ’round these parts.[1][2] So it’s not like they’re storing “weird” stuff in tabular format, like HTML blog articles; they’re just storing numbers.
Of course, Excel and other software has tried to be way too clever about it with different separators depending on locale, which makes the whole pipeline complex and context-dependent.
[1] A surprising amount of countries have this convention, including my own… doesn’t make sense to me since it becomes harder to list numbers.
[2] And the number one selling point of CSV is that it is “human editable”, so of course someone is going to do that at some point in the pipeline.
<https://github.com/dbro/csvquote>
Using it with the first example command from this article would be
csvquote file.csv | awk -F, '{print $1}' | csvquote -u
By using the "-u" flag in the last step of the pipeline, all of the problematic quoted delimiters get restored.https://stackoverflow.com/questions/43477652/ignoring-comma-...
% duckdb -c "from 'test.csv'"
┌───────┬───────┬───────┐
│ A │ B │ C │
│ int64 │ int64 │ int64 │
├───────┼───────┼───────┤
│ 1 │ 2 │ 3 │
│ 4 │ 5 │ 6 │
│ 7 │ 8 │ 9 │
└───────┴───────┴───────┘
% duckdb -c "select sum(C) from 'test.csv'"
┌────────┐
│ sum(C) │
│ int128 │
├────────┤
│ 18 │
└────────┘
% duckdb -c "select sum(A + 2*B + C^2) from 'test.csv'"
┌────────────────────────────────┐
│ sum(((A + (2 * B)) + (C ^ 2))) │
│ double │
├────────────────────────────────┤
│ 168.0 │
└────────────────────────────────┘This is the description in the experimental PR (parallel CSV has since merged into the main branch)
The problem is perhaps most obvious when you consider that the value of a single cell could itself be a CSV, and this could be recursive
An unescaped cell within a CSV will break most CSV parsers in the world even with recursion handling.
CSVs are a bit of a mess. Non-trivial to parse properly, due to escaping, and the standard (such as it is) is poorly followed. E.g. sometimes people use semi-colon or pipe instead of comma.
See also: 'Why isn’t there a decent file format for tabular data?' https://news.ycombinator.com/item?id=31220841
This is also my use case where I have GBs of data in S3 as Parquet and a small CSV file that I have to join them to.
And DuckDB reads CSVs and Parquet and SQLite and others. I can join all these heterogeneous data types in a single SQL statement and have the assurance that it’ll be done correctly.
I believe clickhouse-local can do the same.
https://pandas.pydata.org/docs/reference/api/pandas.read_csv...
Parsing a CSV by hand is almost always the wrong thing to do unless you also wrote the CSV generating process and can limit the variance.
But if not, instead of working with CSVs at the string level and risk running into issues, I've found that the best practice is to farm the CSV parsing out to a battle-tested engine (I use DuckDB, but sqlite, pandas, polars, clickhouse-local or any number of engines work), and work with the parsed object instead.
Better yet, avoid CSV and use DuckDB + Parquet (paired with visidata for quick vizes) for anything nontrivial.
perl -MText::CSV=csv -e'my $aoh = csv(in=>"foo.csv",headers=>"auto",keep_headers=>\my @h); @$aoh = grep { $_->{some_header} > 0 } @$aoh; csv(in=>$aoh,out=>*STDOUT,headers=>\@h)'Not the right tool for everything, but it shines for quickly glancing at the shape of tabular data and making sense of it. Sorting, filtering, joins across files, column histograms and even column splitting/rejoining are all keystrokes away.
It groks anything even remotely table shaped like CSV, JSONL, JSON, even Excel files, it can even directly connect to databases and parse tables out of HTML.
https://jsvine.github.io/intro-to-visidata/index.html
I am a big fan of VisiData for working with data in the terminal. I also like John Kerl's miller[1] and Simon Willison's sqlite-utils [2] when I want to do queries. These are useful for working with data in simple shell scripts.
[1] https://github.com/johnkerl/miller [2] https://github.com/simonw/sqlite-utils
By giant I mean 25G gzipped files with >10^9 rows like these VCFs: https://ftp.ncbi.nlm.nih.gov/snp/latest_release/VCF/
sqlp: Run blazing-fast Polars SQL queries against several CSVs - converting queries to fast LazyFrame expressions, processing larger than memory CSV files.
to: Convert CSV files to PostgreSQL, SQLite, XLSX, Parquet and Data Package.
[1] https://github.com/jqnatividad/qsv/blob/master/src/cmd/sqlp....
[2] https://github.com/jqnatividad/qsv/blob/master/src/cmd/to.rs...
xsv is more lean while qsv tries to support every action that you might want to perform on CSV files.[2]
[1] https://github.com/jqnatividad/qsv
[2] https://github.com/jqnatividad/qsv/discussions/290#discussio...
- It works with every data format which I come across in my daily job. It support data formats like Protobuf, Avro, Cap'n Proto which regular tools don't support. Funny thing, it can even read mysql dumps.
- I can read the data stored in local or remote location like http, s3, gcs, azure and whatever location I can think of.
- I can SQL queries on the raw data and improve my SQL skills on daily basis.
[1] https://github.com/ClickHouse/ClickHouse/tree/master#readme
Whenever CSV comes up I always feel a bit sad. ASCII includes a set of four out-of-band delimiters[0] that can be used instead of silly formats like CSV that use in-band delimiters which necessitate complicated quoting rules.
You can't just treat CSV as text. It's not and woe betide you if you ever use this stuff in a script instead of using a proper CSV parsing tool. If we used the ASCII delimiters instead it would be possibly to treat it as text and stuff like this would work.
[0] https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
There are a lot of power tools for CSV. All they need to do to support this format is to support it as input and output. It should be possible for example to add support to Csvkit[2] as input/output ("format").
Just based on HN comments alone (these things always come up) there is a decent demand for this kind of format. And there are a few implementations out there. But the implementations often look more like standards or proofs of concepts rather than full-fledged implementations. (A full-fledged implementation should probably provide functions/programs to convert to and from CSV as described in RFC-4180.)
We could make this happen.
[1] https://news.ycombinator.com/item?id=31258316
[2] https://csvkit.readthedocs.io/en/latest/scripts/in2csv.html
The "arguments" against this are absurd; mainly that the resulting file is not plain text and cannot be read/modified immediately by plain text editors. This ignores the fact that plain text editors already deal with control characters because they have to decide what to do with <tab>, and makes a mountain out of a molehill when the in-band horrors of escaping delimiters create trouble every single day.
I guess you could:
1. Use unit-separator and record-separator for text
2. Use group-separator for (1)
3. Use file-separator for (2)
> The "arguments" against this are absurd; mainly that the resulting file is not plain text and cannot be read/modified immediately by plain text editors. This ignores the fact that plain text editors already deal with control characters because they have to decide what to do with <tab>, and makes a mountain out of a molehill when the in-band horrors of escaping delimiters create trouble every single day.
Could not agree more.
The only thing needed for this to work really well for text editors to treat the end of record character like a carriage return (I'm not aware of any that do).
I like using the shell (and Awk in particular!) as much as anyone, but for CSV I tend to reach for Python's standard csv module[1].
> open people.csv | where status == 'customer' | unique-by email | select surname forename email | sort-by email | save customers.json
https://www.nushell.sh/It is the most powerful (works with any formats, with remote datasets, and supports all the ClickHouse SQL) and the most performant.
Would be nice to add a seamless ability to call executable UDFs from clickhouse-local, last time I checked clickhouse-local required executables to be in a special directory (as in proper clickhouse). Instead it'd be nice to be able to reference any executable in an ad-hoc way.
When I tell them to do it in Excel by themselves, they would say Excel couldn't open a CSV larger than 1M rows...
At first, I was using sqlite through shell. I hated it so much that I built a desktop app on top of it. It wasn't slick enough with all the typing.
It is quite a joy to use, and I'd love for people to try it out: https://superintendent.app (disclaimer: I'm the creator).
It’s $40/year for those wondering.
The price is very obvious on the website.
Users can't use it for an extended period of time without paying.
You said it like I was tricking people into paying somehow.
Hate to agree, but yes right there, 100 pixels above the download link, very nice and clear site, I heartily approve. Not sure how you'll get $1b of funding for something that user friendly though -- I don't see any dark patterns at all!
> Does your workplace reimburse software's cost?
I don't need your app, but my workplace certainly doesn't. I can buy all sorts of hardware from preferred suppliers through our purchasing system, I can with a few hoops generate a one-time credit card to buy a random thing from a random person (how I pay for starlink for example), and for some specific cases I can pay myself and claim back later
However software is blocked from all this.
> Even if Superintendent.app only saves you 30 minutes per month, it's already worth the minimal price tag that we will charge after beta.
To buy software in my company would take far longer than the 6 hours that I'd save each year! This of course is entirely the fault of my company, but I wonder how many other companies have similar policies.
What I can do though is spin up a AWS ami with a software charge and nobody bats an eyelid. I'm not sure how much of a "cut" aws takes, but something to think about.
(If it really helped me I'd just eat the $40 personally and take a day off in lieu, but I've known some people put in receiptless claims to get around the policy)
One thing I'm interested in is the dark pattern you accused me of.
Users literally cannot use it beyond the trial period if they don't pay, and I don't collect credit card info before the trial.
Are you saying someone might accidentally pay for this?
Almost a breath of fresh air!
Sure it can. Just use the "Import from Text/CSV" feature instead of directly opening the CSV. https://support.microsoft.com/en-us/office/what-to-do-if-a-d...
To be honest, I'm still trying to find more a product market fit.
It feels like a travel planning app where people might need it once every few months, so it doesn't quite catch on.
On another hand, I do have a few passionate users who analyze 40+ CSVs at the same time, but I'm struggling to improve UX for these users. Building great UI is hard. But maybe this is what I need to do.
cat mtcars.csv | group_by cyl | summarise "mpg = mean(mpg)" | kable
#> | cyl| mpg|
#> |---:|--------:|
#> | 4| 26.66364|
#> | 6| 19.74286|
#> | 8| 15.10000|
https://github.com/coolbutuseless/dplyr-clipq is overdue some maintenance but I will update it with the imminent PRQL 0.9 release.
I've used pq in anger at my $dayjob and found it incredibly productive to have the full power of SQL combined with the terse and logical syntax of PRQL.
csv_to_json () { python -c 'import csv, json, sys; print(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)]))' | jq . }
It converts a csv to a json list of objects, mapping column names to values. I find it way easier to then operate on json by filtering with jq or gron, or just pasting it into other tools for post-processing. The jq at the end isn't necessary but makes for nice formatting!
Also, unless it’s the coloring you’re concerned with, you could replace the pipe to jq with pprint, a native Python library.
And it's true that it won't scale well but it still has worked every time I've wanted it to :)
python -c 'import csv, json, sys; [print(json.dumps(r)) for r in csv.DictReader(sys.stdin)]' | jq -s .
to stream the csv rows instead of reading the whole file into ram.(With credit of course, if I do so.)
ruby -rcsv -ne 'CSV($<).each { |r| puts r[0] }'
I like the seen example the dude has: '!seen[$1]++'I find it beautiful, tho :)
-rcvs
Load the (built-in) CSV module in Ruby. -e
Eval the following string as Ruby code. CSV($<)
Create CSV parser with standard input `$<` as the source. .each
Run the code in the block that follows for each row (automatically skips the CSV header): { |r| puts r[0] }
For a row, print the first/0th element. Can be simplified in recent Ruby: { puts _1[0] }I was rather skeptical on the value of having this stuff as a shell built in, but it won me over. Very convenient!
GNU datamash (https://www.gnu.org/software/datamash/) provides features like groupby, statistical operations, etc.
See also this free ebook: Data Science at the Command Line (https://jeroenjanssens.com/dsatcl/)
Some are very close to awk in spirit, like my own attempt: `pawk` (1). It will parse your csv just fine. Or tsv. Or JSON or YAML or TOML. Or Parquet, even.
SQLite, DuckDB and Clickhouse-local have been mentioned, but another very simple one, a single dependency-free binary, is https://github.com/multiprocessio/dsq
Not affiliated, just a happy user
You can get it as
https://clickhouse.com/ | sh 'cat $SPACE_SEPARATED_FILE | tr -s ' ' | tr ' ' ',' > out.csv
Allied with 'cut', it becomes easy to pull particular fields out of a text file:- 'cat $FILE_WITH_COMMAS_AND_SPACES | tr ',' ' ' | tr -s ' ' | cut -d ' ' -f1,2,17 > out.txtawk -F, '{print $1}' file.csv
Doesn't work if the data contains commas (with escapes)? If so, that might be worth spelling out.
The datasets that I work with are well small enough to fit in memory on my machine and having the full java ecosystem + clojure ergonomics is worth more to me than the performance full db tooling might offer