Introduction of Ruby CSV

內容是Web Tech Topic #9 分享所做的整理


什麼是 CSV

CSV 全名為 Comma-Separated Values,就是以“逗點”分隔的純文本,是一種通用的、相對簡單的文件格式。範例:

Tony,27,22000,Hi
David,30,50000,Hello

你可以在 Microsoft 的 Excel 中打開這個檔案,就會發現到這些被逗點分隔的資料整齊地排列在每個欄位內,事實上,你也可以在 Excel 中編輯資料表,最後存檔的時候選擇用 csv 的格式輸出。


常見的格式問題

1. 每個欄位都會以字串的方式儲存
2. 如果要存放的資料內容有逗點,用雙引號(’’)保護,可以避免被電腦誤認是分隔符號。
3. 如果要存放的資料內容有雙引號,那…就再用一個雙引號保護它。

範例:

Tony,27,22000,”Hi, my name is Tony”
David,30,50000,”say “”Hello””, my friend.”

常見的使用時機

輸出報表: 這裡指的報表,泛指為非 CRUD 的操作。通常為給定特定的條件所查詢尋到的多筆資料。例如:熱門文章,商城中符合搜尋條件限制的商品清單等等。
資料庫備份: 資料庫在搬移的時候,通常可以拆成兩個部分,資料庫欄位格式 (schema) 的sql檔與留存資料內容的 csv 檔,所以基本上 csv 就是份資料表,每一行可以被視作一筆的資料。


Ruby 如何讀取 CSV

Ruby 本身有內建的 library 可以讀取,只需要宣告 require “csv” 就可以了,是不是相當方便呢?

讀取的方式可以分為兩種,一次性讀取或是分行讀取。一次性讀取會將整份 csv 檔案寫入記憶體,而分行讀取則是讀取單行資料進記憶體處理,先進先出。所以如果要讀取的 csv 檔案非常大,或是記憶體容量有限的情形下,就應該要避免使用一次性讀取而改用分行讀取,避免爆炸。

Ruby 會將整份 csv 當作一個陣列 (array),第一行就是這個陣列的第一個元素,以此類推直到最後一行資料。而 Ruby 又會將每一行資料當作一個陣列,也就是每筆資料的第一個欄位就是第一個元素,以此類推直到最後一個欄位。由此可知,整份 csv 就是個二維陣列 (two-dimensional array)。

另外,內建的 parse method 可以將字串 (string) 轉成 csv。

require ‘csv’
# 一次性讀取
csv = CSV.read(“workers.csv”)
=> [[“Tony”, “27”, “22000”, “Hi, my name is Tony”],
[“David”, “30”, “50000”, “say \”Hello\”, my friend.”]]
# 分行讀取
CSV.foreach(“workers.csv”) do |row|
puts row.inspect
end
=>[“Tony”, “27”, “22000”, “Hi, my name is Tony”]
=>[“David”, “30”, “50000”, “say \”Hello\”, my friend.”]
# 利用parse,將字串轉成csv
string = “sky,30,50000,\”Time to run!\”\nabner,26,50000,\”amazing!\””
parsed_csv = CSV.parse(string)
=> [[“sky”, “30”, “100000”, “Time to run!”],
[“abner”, “26”, “100000”, “amazing!”]]

常見的options

col_sep: string

使用 column separation option 可以讓我們處理非”逗號”分隔的資料表,用自行定義的符號作為分隔符號。

csv = CSV.read(“workers.ssv”, col_sep: “;”)

headers: boolean

使用 headers option 可以讓 ruby 將第一筆資料視為欄位的標題。另外用法在接下來繼續介紹。

csv = CSV.read(“workers.csv”, headers: true)

常見的操作

那我們就來練習操作一下吧,下方的 workers.csv 記錄了每個員工的姓名、年齡跟月薪。範例:

Tony,27,22000
Danny,30,50000
Bobby,31,44000
Willy,22,55000

練習一:算出平均月薪。

require "csv"
workers = CSV.read("workers.csv")
count = workers.size
total = workers.inject(0) {|sum, worker| sum += worker[2].to_i}
average_salary = total/count

練習二:幫大家調薪30%,輸出成另一張資料表。

require "csv"
new_csv = CSV.open("new.csv", "w")
CSV.foreach("workers.csv") do |worker|
worker[2].to_i * 1.3
new_csv << worker
end

經過簡單的兩個練習,相信大家已經發現其實讀取後就是在操作陣列而已。


讀取欄位標題差很多

現在我們來觀察一下,讀取欄位標題狀態的 csv 有什麼不同。範例:

name,age,salary
Tony,27,22000
Danny,30,50000

在沒有讀取欄位標題狀態之下,Ruby 會將整份文件變成二維陣列,而欄位標題視為第一筆資料。範例:

require 'csv'
csv_without_headers = CSV.read("workers.csv")
p csv_without_headers.class
=> Array
p csv_without_headers[0]
=> ["name", "age", "salary"]

在有讀取欄位標題狀態之下,Ruby 會將第一筆資料變成欄位標題,而整份文件變成一個 CSV::Table 的類別物件。

require 'csv'
csv_with_headers = CSV.read("workers.csv", headers: true)
p csv_with_headers.class
=> CSV::Table
p csv_with_headers[0]
=> #<CSV::Row "name":"Tony" "age":"27" "salary":"22000">

甚至在分行讀取的操作中,每一行的資料也不再是陣列,而是 CSV::Row。而欄位標題已經不再被視為是資料的一部分。

require 'csv'
CSV.foreach("workers.csv", headers: true) do |row|
p row.class
end
=> CSV::Row
=> CSV::Row

變成 CSV 有什麼特別的嗎?難道操作陣列不好嗎?

Array 是一個有順序性的資料結構,可以按照已知順序的 index 取得該值,對比之下,Hash 就是沒有順序性的容器,所以依靠自己定義的 key 去存取 value。Ruby 的 CSV 有點類似陣列跟雜湊 (hash) 的混合體,CSV::Table仍然保持了整份資料表中一筆筆資料的順序性,但也可以由 CSV::Row 操作自行定義的欄位標題取得該欄位的內容,有點類似 array of hashes。

練習三:取得第一個員工的姓名與第二個員工的年齡。

require "csv"
csv = CSV.read("workers.csv", headers:true)
p csv[0]["name"]
=> "Tony"
p csv[1]["age"]
=> "30"

練習四:刪去第一個員工的資料與整份資料表的salary欄位。

require "csv"
csv = CSV.read("workers.csv", headers:true)
csv.delete(0)
p csv
=>name,age,salary
Danny,30,50000
csv.delete("salary")
p csv
=>name,age
Tony,27
Danny,30

練習五:選出年齡大於28的員工。

require "csv"
workers = CSV.read("workers.csv", headers: true)
old_workers = workers.select do |worker|
worker["age"].to_i > 28
end`
p old_workers.inspect
=>[#<CSV::Row "name":"Danny" "age":"30" "salary":"50000">]

常見的Gems

  • smarter_csv: 可以直接處理 Mongoid 或 ActiveRecord 的資料,支援Resque 或 Sidekiq 等非同步處理。
  • csv_shaper: 提供了漂亮的 DSL,可以像用 Jbuilder 方式在 view 中寫 csv,如此就可以避免把組合 csv 的邏輯寫在model 或是 controller中。
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.