221

It's easy enough to read a CSV file into an array with Ruby but I can't find any good documentation on how to write an array into a CSV file. Can anyone tell me how to do this?

I'm using Ruby 1.9.2 if that matters.

Jason Swett
  • 43,526
  • 67
  • 220
  • 351
  • 4
    The answer you have is great, but let me urge you to not use CSV. If you don't have tabs in your data, tab-delimited files are much easier to deal with because they don't involve so much freakin' quoting and escaping and such. If you must use CSV, of course, them's the breaks. – Bill Dueber Jan 28 '11 at 01:45
  • 8
    @Bill, the CSV module neatly handles tab-delimited files as well as actual csv files. The :col_sep option lets you specify the column separator as "\t" and all's well. – tamouse Nov 13 '13 at 17:42
  • 1
    here is more Info about CSV http://docs.ruby-lang.org/en/2.1.0/CSV.html – Veeresh Honnaraddi Aug 19 '16 at 05:21
  • Using .tab files with this module is what I am doing, because opening this in Excel by accident would othrwise mess up the encoding… – MrVocabulary Jan 24 '20 at 08:03

6 Answers6

383

To a file:

require 'csv'
CSV.open("myfile.csv", "w") do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
  # ...
end

To a string:

require 'csv'
csv_string = CSV.generate do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
  # ...
end

Here's the current documentation on CSV: (until 3.1.1 just replace 1.9.2)

https://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV.html#class-CSV-label-Writing

while 3.1.2 up to latest (3.2.2 as of this edit).

https://ruby-doc.org/3.2.2/stdlibs/csv/CSV.html

Dylan Markow
  • 123,080
  • 26
  • 284
  • 201
  • 1
    @David it's the file mode. "w" means write to a file. If you don't specify this, it'll default to "rb" (read-only binary mode) and you would get an error when trying to add to your csv file. See http://www.ruby-doc.org/core-1.9.3/IO.html for a list of valid file modes in Ruby. – Dylan Markow Jul 16 '12 at 14:08
  • 18
    Gotcha. And for future users, if you want each iteration to not overwrite the previous csv file, use the "ab" option. – boulder_ruby Jul 16 '12 at 14:38
  • 1
    See this answer for Ruby File IO Modes: http://stackoverflow.com/a/3682374/224707 – Nick Sep 19 '16 at 08:41
72

If you have an array of arrays of data:

rows = [["a1", "a2", "a3"],["b1", "b2", "b3", "b4"], ["c1", "c2", "c3"]]

Then you can write this to a file with the following, which I think is much simpler:

require "csv"
File.write("ss.csv", rows.map(&:to_csv).join)
jwadsack
  • 5,708
  • 2
  • 40
  • 50
  • yeah but every time it rewrite the file , and not add the data at the end – Humayun Naseer Oct 13 '20 at 13:38
  • @HumayunNaseer I haven't needed a situation where I was appending data to a CSV but you could certainly do that using the `mode: "a"` option for `File.write`. – jwadsack Oct 15 '20 at 15:50
45

I've got this down to just one line.

rows = [['a1', 'a2', 'a3'],['b1', 'b2', 'b3', 'b4'], ['c1', 'c2', 'c3'], ... ]
csv_str = rows.inject([]) { |csv, row|  csv << CSV.generate_line(row) }.join("")
#=> "a1,a2,a3\nb1,b2,b3\nc1,c2,c3\n" 

Do all of the above and save to a csv, in one line.

File.open("ss.csv", "w") {|f| f.write(rows.inject([]) { |csv, row|  csv << CSV.generate_line(row) }.join(""))}

NOTE:

To convert an active record database to csv would be something like this I think

CSV.open(fn, 'w') do |csv|
  csv << Model.column_names
  Model.where(query).each do |m|
    csv << m.attributes.values
  end
end

Hmm @tamouse, that gist is somewhat confusing to me without reading the csv source, but generically, assuming each hash in your array has the same number of k/v pairs & that the keys are always the same, in the same order (i.e. if your data is structured), this should do the deed:

rowid = 0
CSV.open(fn, 'w') do |csv|
  hsh_ary.each do |hsh|
    rowid += 1
    if rowid == 1
      csv << hsh.keys# adding header row (column labels)
    else
      csv << hsh.values
    end# of if/else inside hsh
  end# of hsh's (rows)
end# of csv open

If your data isn't structured this obviously won't work

boulder_ruby
  • 38,457
  • 9
  • 79
  • 100
  • I pulled in a CSV file using CSV.table, did some manipulations, got rid of some columns, and now I want to spool the resulting Array of Hashes out again as CSV (really tab-delimited). How to? https://gist.github.com/4647196 – tamouse Jan 27 '13 at 07:13
  • hmm...that gist is somewhat opaque, but given an array of hashes, all with the same number of k/v pairs and the same keys, in the same order... – boulder_ruby Nov 12 '13 at 04:28
  • Thanks, @boulder_ruby. That will work. The data is a census table, and that gist is rather opaque looking back at it. :) It's basically extracting certain columns from the original census table into a subset. – tamouse Nov 13 '13 at 16:46
  • 4
    You're misusing `inject` here, you really want to use `map`. Also, you don't need to pass an empty string to `join`, as this is the default. So you could shrink it even further to this: `rows.map(&CSV.method(:generate_line).join` – iGEL May 30 '16 at 12:47
  • @iGEL, you are correct but you have missed a `(` . `rows.map(&CSV.method(:generate_line)).join` – Sushant Bajracharya Aug 22 '16 at 06:36
  • 1
    Your second example is overcomplicated, as CSV library is quite powerful. `CSV.generate(headers: hsh.first&.keys) { |csv| hsh.each { |e| csv << e } }` generates an equivalent CSV. – Amadan Jan 05 '18 at 09:03
27

If anyone is interested, here are some one-liners (and a note on loss of type information in CSV):

require 'csv'

rows = [[1,2,3],[4,5]]                    # [[1, 2, 3], [4, 5]]

# To CSV string
csv = rows.map(&:to_csv).join             # "1,2,3\n4,5\n"

# ... and back, as String[][]
rows2 = csv.split("\n").map(&:parse_csv)  # [["1", "2", "3"], ["4", "5"]]

# File I/O:
filename = '/tmp/vsc.csv'

# Save to file -- answer to your question
IO.write(filename, rows.map(&:to_csv).join)

# Read from file
# rows3 = IO.read(filename).split("\n").map(&:parse_csv)
rows3 = CSV.read(filename)

rows3 == rows2   # true
rows3 == rows    # false

Note: CSV loses all type information, you can use JSON to preserve basic type information, or go to verbose (but more easily human-editable) YAML to preserve all type information -- for example, if you need date type, which would become strings in CSV & JSON.

Kanat Bolazar
  • 526
  • 5
  • 9
11

Building on @boulder_ruby's answer, this is what I'm looking for, assuming us_eco contains the CSV table as from my gist.

CSV.open('outfile.txt','wb', col_sep: "\t") do |csvfile|
  csvfile << us_eco.first.keys
  us_eco.each do |row|
    csvfile << row.values
  end
end

Updated the gist at https://gist.github.com/tamouse/4647196

tamouse
  • 2,169
  • 1
  • 19
  • 26
3

Struggling with this myself. This is my take:

https://gist.github.com/2639448:

require 'csv'

class CSV
  def CSV.unparse array
    CSV.generate do |csv|
      array.each { |i| csv << i }
    end
  end
end

CSV.unparse [ %w(your array), %w(goes here) ]
Felix Rabe
  • 4,206
  • 4
  • 25
  • 34
  • Btw, beware of multi-dimensional arrays in pry on JRuby. `[ %w(your array), %w(goes here) ]` won't look pretty. https://github.com/pry/pry/issues/568 – Felix Rabe May 08 '12 at 21:31