CSV

CSV #

CSV (Comma-Separated Values) adalah format data tabular yang sudah ada sejak dekade 1970-an dan masih menjadi format pertukaran data paling universal hingga hari ini — ekspor dari spreadsheet, laporan dari sistem legacy, data dari database, dan output dari berbagai tool analitik hampir semua menggunakan CSV. Ruby menyediakan library CSV yang komprehensif sebagai bagian dari standard library. Berbeda dari sekadar split string dengan koma, library ini menangani semua edge case yang membuat CSV susah diproses secara manual: nilai yang mengandung koma, nilai yang mengandung newline, quoting, encoding, dan konversi tipe data otomatis. Artikel ini membahas seluruh API CSV Ruby — dari parsing sederhana hingga pemrosesan file besar secara efisien.

Mengapa Tidak Sekadar split(",") #

Sebelum masuk ke API, penting memahami mengapa string.split(",") tidak cukup untuk CSV nyata.

require "csv"

# CSV yang terlihat sederhana tapi mengandung edge case
csv_sederhana = 'Alice,28,"Jakarta, Selatan",Developer'

# ANTI-PATTERN: split koma — salah!
csv_sederhana.split(",")
# => ["Alice", "28", "\"Jakarta", " Selatan\"", "Developer"]
# "Jakarta, Selatan" terpecah jadi dua! Kuotasi tidak ditangani!

# BENAR: gunakan CSV.parse — menangani quoting dengan benar
CSV.parse(csv_sederhana).first
# => ["Alice", "28", "Jakarta, Selatan", "Developer"]

# Edge case lain: nilai mengandung newline
csv_multiline = "Alice,\"Jalan Merdeka\nNo. 1\",Developer"
CSV.parse(csv_multiline).first
# => ["Alice", "Jalan Merdeka\nNo. 1", "Developer"]

# Edge case: nilai mengandung quote ganda (escaped dengan double-quote)
csv_quoted = 'Alice,"Dia berkata ""Halo!""",Developer'
CSV.parse(csv_quoted).first
# => ["Alice", 'Dia berkata "Halo!"', "Developer"]

Parsing CSV #

Parse dari String #

require "csv"

# Parse string multi-baris
csv_string = <<~CSV
  nama,umur,kota,aktif
  Alice,28,Jakarta,true
  Bob,35,Bandung,false
  Charlie,22,Surabaya,true
CSV

# parse tanpa opsi — mengembalikan Array of Array
baris = CSV.parse(csv_string)
# => [["nama", "umur", "kota", "aktif"],
#     ["Alice", "28", "Jakarta", "true"],
#     ["Bob", "35", "Bandung", "false"],
#     ["Charlie", "22", "Surabaya", "true"]]

# parse dengan headers: true — baris pertama jadi header
data = CSV.parse(csv_string, headers: true)
# => #<CSV::Table>

data.each do |baris|
  puts "#{baris["nama"]} dari #{baris["kota"]}"
end
# Alice dari Jakarta
# Bob dari Bandung
# Charlie dari Surabaya

# Akses kolom
data["nama"]   # => ["Alice", "Bob", "Charlie"]
data[0]        # => #<CSV::Row "nama":"Alice" "umur":"28" ...>
data[0]["nama"]     # => "Alice"
data[0][:nama]      # => nil!  key adalah String, bukan Symbol

Parse dari File #

require "csv"

# Baca semua ke memori — untuk file kecil
data = CSV.read("pengguna.csv", headers: true)
data.each { |baris| puts baris["nama"] }

# Iterasi baris — untuk file besar, lebih hemat memori
CSV.foreach("pengguna.csv", headers: true) do |baris|
  # Hanya satu baris di memori pada satu waktu
  proses(baris["nama"], baris["email"])
end

# CSV.open — kendali penuh atas file handle
CSV.open("pengguna.csv", "r", headers: true) do |csv|
  csv.each do |baris|
    next if baris["aktif"] == "false"
    puts baris.to_h
  end
end

Opsi Parsing #

Library CSV menyediakan banyak opsi untuk menyesuaikan perilaku parsing dengan format file yang berbeda-beda.

Separator dan Encoding #

require "csv"

# col_sep — separator kolom (default: ",")
CSV.parse("Alice;28;Jakarta", col_sep: ";")
# => [["Alice", "28", "Jakarta"]]

# row_sep — separator baris (default: "\n" atau "\r\n")
CSV.parse("Alice,28\r\nBob,35", row_sep: "\r\n")

# quote_char — karakter quoting (default: '"')
CSV.parse("Alice|28|'Jakarta, Selatan'", col_sep: "|", quote_char: "'")
# => [["Alice", "28", "Jakarta, Selatan"]]

# Encoding — penting untuk file dari Windows atau sistem lama
CSV.read("windows_file.csv",
  headers: true,
  encoding: "Windows-1252:UTF-8"   # baca Windows-1252, konversi ke UTF-8
)

CSV.foreach("data.csv",
  headers: true,
  encoding: "UTF-8"
) do |baris|
  # ...
end

Konversi Tipe Data Otomatis #

Secara default, semua nilai CSV adalah String. Opsi converters memungkinkan konversi tipe otomatis.

require "csv"

csv_data = "Alice,28,150000.5,true,2024-01-15"

# Tanpa converter — semua String
CSV.parse(csv_data).first
# => ["Alice", "28", "150000.5", "true", "2024-01-15"]

# Dengan converter bawaan
CSV.parse(csv_data, converters: :numeric).first
# => ["Alice", 28, 150000.5, "true", "2024-01-15"]
# Integer dan Float dikonversi otomatis

CSV.parse(csv_data, converters: :all).first
# => ["Alice", 28, 150000.5, "true", 2024-01-15 00:00:00 +0000]
# Tambahan: Date dan DateTime coba dikonversi

# Converter tersedia
# :integer  — konversi ke Integer jika memungkinkan
# :float    — konversi ke Float jika memungkinkan
# :numeric  — :integer + :float
# :date     — konversi ke Date
# :date_time — konversi ke DateTime
# :all      — semua converter di atas
# Custom converter — fungsi yang menerima field dan mengembalikan nilai
bool_converter = ->(field) { field == "true" ? true : (field == "false" ? false : field) }

CSV::Converters[:boolean] = bool_converter

csv_string = "Alice,28,true\nBob,35,false"
CSV.parse(csv_string, converters: [:numeric, :boolean])
# => [["Alice", 28, true], ["Bob", 35, false]]

Header Converters #

require "csv"

csv_with_headers = "Nama Lengkap,Umur,Kota Asal\nAlice,28,Jakarta"

# header_converters — transformasi nama header
data = CSV.parse(csv_with_headers,
  headers: true,
  header_converters: :symbol
)
# Header dikonversi ke symbol: :nama_lengkap, :umur, :kota_asal?
# Sebenarnya :symbol hanya downcase dan konversi spasi jadi _

data[0][:nama_lengkap]   # => nil?  perlu cek konversi aktual

# Custom header converter
data = CSV.parse(csv_with_headers,
  headers: true,
  header_converters: ->(h) { h.downcase.gsub(" ", "_").to_sym }
)
data[0][:nama_lengkap]   # => "Alice"
data[0][:umur]           # => "28"

Generate ke String #

require "csv"

# CSV.generate — buat CSV string
csv_string = CSV.generate do |csv|
  csv << ["nama", "umur", "kota"]    # header
  csv << ["Alice", 28, "Jakarta"]
  csv << ["Bob", 35, "Bandung"]
  csv << ["Charlie", 22, "Surabaya"]
end

puts csv_string
# nama,umur,kota
# Alice,28,Jakarta
# Bob,35,Bandung
# Charlie,22,Surabaya

# Penanganan otomatis untuk nilai yang perlu di-quote
csv_string = CSV.generate do |csv|
  csv << ["nama", "alamat", "catatan"]
  csv << ["Alice", "Jalan Merdeka, No. 1", "pelanggan \"VIP\""]
end
# nama,alamat,catatan
# Alice,"Jalan Merdeka, No. 1","pelanggan ""VIP"""
require "csv"

pengguna = [
  { nama: "Alice", umur: 28, kota: "Jakarta" },
  { nama: "Bob", umur: 35, kota: "Bandung" }
]

# CSV.open dengan mode "w"
CSV.open("pengguna.csv", "w") do |csv|
  csv << ["nama", "umur", "kota"]   # header
  pengguna.each do |p|
    csv << [p[:nama], p[:umur], p[:kota]]
  end
end

# Append ke file yang sudah ada
CSV.open("pengguna.csv", "a") do |csv|
  csv << ["Charlie", 22, "Surabaya"]
end

# Menulis dengan opsi separator
CSV.open("pengguna.tsv", "w", col_sep: "\t") do |csv|
  csv << ["nama", "umur"]
  csv << ["Alice", 28]
end

Iterasi Efisien untuk File Besar #

Untuk file CSV yang besar (ratusan MB hingga beberapa GB), penting untuk tidak memuat semua data ke memori sekaligus.

require "csv"

# ANTI-PATTERN: baca semua ke memori
semua_data = CSV.read("data_besar.csv", headers: true)
semua_data.each { |baris| proses(baris) }
# Untuk file 1GB, ini bisa menghabiskan RAM puluhan GB!

# BENAR: iterasi baris per baris
CSV.foreach("data_besar.csv", headers: true) do |baris|
  proses(baris)
  # Hanya satu baris di memori pada satu waktu
end

# Memproses dalam batch — berguna untuk bulk database insert
def proses_dalam_batch(file_path, ukuran_batch: 1000)
  batch = []

  CSV.foreach(file_path, headers: true) do |baris|
    batch << baris.to_h

    if batch.size >= ukuran_batch
      simpan_ke_database(batch)
      batch.clear
    end
  end

  # Jangan lupa sisa batch terakhir
  simpan_ke_database(batch) unless batch.empty?
end

proses_dalam_batch("transaksi.csv", ukuran_batch: 500)
# Streaming dengan Enumerator untuk kontrol lebih
def csv_enumerator(path, **opsi)
  Enumerator.new do |y|
    CSV.foreach(path, **opsi) { |baris| y << baris }
  end
end

# Sekarang bisa menggunakan lazy enumeration
csv_enumerator("besar.csv", headers: true)
  .lazy
  .select { |baris| baris["aktif"] == "true" }
  .map { |baris| { id: baris["id"].to_i, nama: baris["nama"] } }
  .first(100)   # hanya proses sampai 100 baris aktif ditemukan

CSV::Table dan CSV::Row #

Ketika parsing dengan headers: true, hasilnya bukan Array biasa tapi CSV::Table berisi CSV::Row.

require "csv"

csv_string = "nama,umur,kota\nAlice,28,Jakarta\nBob,35,Bandung"
table = CSV.parse(csv_string, headers: true)

# CSV::Table — seperti Hash multidimensi
table.class     # => CSV::Table
table.headers   # => ["nama", "umur", "kota"]
table.size      # => 2  (jumlah baris data, tidak termasuk header)

# Akses kolom (mode by_col)
table["nama"]   # => ["Alice", "Bob"]
table["umur"]   # => ["28", "35"]

# Akses baris (mode by_row)
table[0]        # => #<CSV::Row "nama":"Alice" "umur":"28" "kota":"Jakarta">
table[1]["kota"]  # => "Bandung"

# CSV::Row — seperti Hash tapi terurut
baris = table[0]
baris.class         # => CSV::Row
baris["nama"]       # => "Alice"
baris.to_h          # => {"nama"=>"Alice", "umur"=>"28", "kota"=>"Jakarta"}
baris.fields        # => ["Alice", "28", "Jakarta"]  (hanya values)
baris.headers       # => ["nama", "umur", "kota"]

# Modifikasi
baris["umur"] = "29"
baris.to_h          # => {"nama"=>"Alice", "umur"=>"29", "kota"=>"Jakarta"}

# Iterasi CSV::Table
table.each do |baris|
  puts "#{baris["nama"]}: #{baris["kota"]}"
end

# Konversi ke Array of Hash
array_of_hash = table.map(&:to_h)
# => [{"nama"=>"Alice", "umur"=>"28", "kota"=>"Jakarta"},
#     {"nama"=>"Bob", "umur"=>"35", "kota"=>"Bandung"}]

Integrasi dengan Kelas Custom #

Pola umum di aplikasi nyata adalah mengkonversi baris CSV langsung ke objek domain.

require "csv"

class Produk
  attr_reader :id, :nama, :harga, :stok, :kategori

  def initialize(attrs)
    @id       = attrs["id"].to_i
    @nama     = attrs["nama"]
    @harga    = attrs["harga"].to_f
    @stok     = attrs["stok"].to_i
    @kategori = attrs["kategori"]
  end

  def tersedia?
    @stok > 0
  end

  def diskon(persen)
    @harga * (1 - persen / 100.0)
  end

  def to_csv_row
    [@id, @nama, @harga, @stok, @kategori]
  end

  def self.from_csv(path)
    CSV.foreach(path, headers: true).map { |baris| new(baris) }
  end

  def self.ekspor_csv(produk_list, path)
    CSV.open(path, "w") do |csv|
      csv << ["id", "nama", "harga", "stok", "kategori"]
      produk_list.each { |p| csv << p.to_csv_row }
    end
  end
end

# Penggunaan
produk_list = Produk.from_csv("produk.csv")
tersedia = produk_list.select(&:tersedia?)
mahal = produk_list.select { |p| p.harga > 1_000_000 }

# Ekspor subset
Produk.ekspor_csv(tersedia, "produk_tersedia.csv")

Pola Transformasi Data #

CSV sering digunakan untuk transformasi data — baca dari satu format, proses, tulis ke format lain.

require "csv"
require "json"

# CSV ke JSON
def csv_ke_json(csv_path, json_path = nil)
  data = CSV.foreach(csv_path, headers: true, converters: :numeric)
    .map(&:to_h)

  if json_path
    File.write(json_path, JSON.pretty_generate(data))
  else
    JSON.generate(data)
  end
end

# JSON ke CSV
def json_ke_csv(json_path, csv_path)
  data = JSON.parse(File.read(json_path))
  return unless data.is_a?(Array) && !data.empty?

  headers = data.first.keys

  CSV.open(csv_path, "w") do |csv|
    csv << headers
    data.each { |item| csv << headers.map { |h| item[h] } }
  end
end

# Transformasi: filter dan reshape
def transform_laporan(input_path, output_path)
  CSV.open(output_path, "w") do |output|
    output << ["nama", "total_transaksi", "rata_rata"]

    data = CSV.foreach(input_path, headers: true, converters: :numeric)
      .group_by { |baris| baris["nama"] }

    data.each do |nama, baris_list|
      jumlah_list = baris_list.map { |b| b["jumlah"] }.compact
      total = jumlah_list.sum
      rata = jumlah_list.empty? ? 0 : total / jumlah_list.length.to_f
      output << [nama, total, rata.round(2)]
    end
  end
end

Menangani CSV yang Tidak Sempurna #

Data dunia nyata sering tidak sempurna — ada kolom hilang, encoding aneh, atau baris yang tidak konsisten.

require "csv"

# Penanganan baris yang bermasalah
def baca_csv_toleran(path)
  hasil = []
  error_rows = []

  CSV.foreach(path, headers: true) do |baris|
    hasil << baris.to_h
  rescue CSV::MalformedCSVError => e
    error_rows << { baris: $., error: e.message }
  end

  { data: hasil, errors: error_rows }
end

# Encoding detection dan konversi
def baca_csv_dengan_encoding(path)
  # Coba UTF-8 dulu
  CSV.read(path, headers: true, encoding: "UTF-8")
rescue Encoding::InvalidByteSequenceError
  # Fallback ke Windows-1252 (Latin-1)
  CSV.read(path, headers: true, encoding: "Windows-1252:UTF-8")
end

# strip_whitespace — bersihkan whitespace dari semua nilai
csv_data = "  nama  , umur , kota  \n  Alice  ,  28  ,  Jakarta  "
CSV.parse(csv_data,
  headers: true,
  header_converters: ->(h) { h.strip },
  converters: ->(v) { v&.strip }
)

Ringkasan #

  • Jangan split(",") untuk CSV nyata — nilai yang mengandung koma, quote, atau newline akan salah diparse; selalu gunakan library CSV untuk keandalan.
  • CSV.foreach untuk file besar — tidak memuat seluruh file ke memori; iterasi baris per baris adalah pilihan terbaik untuk file yang besar.
  • headers: true untuk data terstruktur — mengubah setiap baris menjadi CSV::Row yang bisa diakses dengan nama kolom, jauh lebih aman dari akses by-index.
  • converters: :numeric untuk konversi tipe otomatis — tanpa ini, semua nilai adalah String; gunakan :numeric untuk Integer dan Float, atau :all untuk Date juga.
  • Penanganan encoding penting — file dari Windows sering menggunakan Windows-1252; gunakan opsi encoding: "Windows-1252:UTF-8" untuk konversi saat membaca.
  • CSV.generate dengan blok untuk menulis — gunakan csv << array di dalam blok; library menangani quoting otomatis untuk nilai yang mengandung koma atau newline.
  • Proses dalam batch untuk performa — ketika memproses data ke database, kumpulkan dalam array hingga ukuran tertentu sebelum flush; ini jauh lebih cepat dari insert satu per satu.
  • to_h pada CSV::Row — konversi baris ke Hash biasa untuk kompatibilitas dengan kode yang tidak mengharapkan CSV::Row.

← Sebelumnya: JSON   Berikutnya: URI →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact