JSON dan CSV

Menguraikan dan menghasilkan data JSON serta CSV menggunakan pustaka standar Ruby.

Daftar isi

Data yang dipertukarkan antarprogram hampir selalu berbentuk berkas dengan format yang disepakati, dan dua format yang paling umum adalah JSON serta CSV. Ruby menyiapkan keduanya di dalam pustaka standar melalui pustaka json dan csv, sehingga penguraian maupun penyusunan data tidak memerlukan pemasangan tambahan. Topik ini membahas JSON.generate dan JSON.parse beserta opsi symbolize_names, penulisan dan pembacaan CSV dengan CSV.open dan CSV.read, pembacaan tabel dengan headers: true, serta pertimbangan memilih di antara kedua format tersebut.

Menghasilkan dan menguraikan JSON#

Pustaka json menyediakan dua metode utama yang saling berkebalikan. JSON.generate mengubah hash atau array Ruby menjadi teks JSON, sedangkan JSON.parse menguraikan teks JSON kembali menjadi hash atau array. Struktur yang bersarang, misalnya array di dalam hash, dipertahankan apa adanya pada kedua arah. Kunci pada hash hasil penguraian berupa string secara bawaan, sehingga aksesnya menggunakan tanda kutip seperti data["lokasi"]. Arah sebaliknya berlaku sama, karena kunci simbol pada hash Ruby ditulis sebagai string pada teks JSON-nya. Untuk berkas yang dibaca manusia, JSON.pretty_generate menghasilkan teks berindentasi dengan isi yang sama.

ruby rekap-suhu-json.rb
# menyimpan rekap suhu ke JSON lalu membacanya kembali
require "json"

rekap = {
  lokasi: "Ruang Server",
  pengukuran: [24.5, 26.0, 25.5]
}

File.write("rekap.json", JSON.generate(rekap))
puts File.read("rekap.json")

data = JSON.parse(File.read("rekap.json"))
puts "Lokasi: #{data["lokasi"]}"
puts "Pengukuran pertama: #{data["pengukuran"][0]}"
text Keluaran
{"lokasi":"Ruang Server","pengukuran":[24.5,26.0,25.5]}
Lokasi: Ruang Server
Pengukuran pertama: 24.5

Berkas di atas dijalankan dengan perintah ruby rekap-suhu-json.rb. Baris pertama keluaran menunjukkan teks JSON yang tersimpan di rekap.json, yaitu satu baris ringkas tanpa spasi tambahan. Setelah diuraikan, hash hasilnya kembali memiliki kunci lokasi dan pengukuran, dan array di dalamnya tetap berupa array dengan bilangan aslinya. Hasil penguraian dapat digunakan seperti hash Ruby biasa, termasuk mengambil satu anggota array di dalamnya.

Kunci JSON dengan symbolize_names#

Opsi symbolize_names: true pada JSON.parse mengubah kunci hasil penguraian menjadi simbol (symbol), yaitu pengenal yang ditulis dengan tanda titik dua. Gaya simbol membuat akses kunci menjadi lebih ringkas dan lazim digunakan pada kode Ruby modern, sedangkan gaya string lebih dekat dengan bentuk asli teks JSON-nya. Keduanya menghasilkan data yang sama, yang berbeda hanyalah jenis kuncinya. Pemilihan gaya biasanya mengikuti gaya hash pada kode di sekitarnya, bukan isi berkas JSON-nya.

ruby JSON.parse dengan symbolize_names: true
require "json"

teks = '{"lokasi":"Ruang A","suhu":24.5}'
data = JSON.parse(teks, symbolize_names: true)
p data[:lokasi]
p data[:suhu]
text Keluaran
"Ruang A"
24.5

Penguraian dengan opsi tersebut menghasilkan hash berkunci simbol, sehingga aksesnya menggunakan data[:lokasi] alih-alih data["lokasi"]. Nilai suhu tetap berupa bilangan karena JSON menyimpan jenis datanya. Bila gaya simbol dipilih, akses dengan tanda kutip pada hash yang sama akan menghasilkan nil tanpa pesan error, sehingga gaya kunci perlu dijaga agar seragam.

Pustaka csv menangani data tabular, yaitu data berbentuk tabel dengan baris dan kolom yang seragam. Penulisan berkas dilakukan dengan CSV.open ber-mode “w” yang menyerahkan objek penulis kepada blok, lalu setiap csv << menambahkan satu baris dari sebuah array nilai. Pembacaan sebaliknya menggunakan CSV.read, yang mengembalikan array dari array berisi seluruh baris berkas. Alternatif lain untuk penulisan adalah CSV.generate atau CSV.generate_line, yang menghasilkan teks CSV untuk kemudian ditulis dengan File.write. Nilai yang mengandung koma otomatis diberi tanda kutip oleh penulisnya, sehingga isi sel tidak mengganggu pembagian kolom.

ruby piket-csv.rb
# menulis lalu membaca daftar piket dalam format CSV
require "csv"

CSV.open("piket.csv", "w") do |csv|
  csv << ["Senin", "Ali"]
  csv << ["Selasa", "Budi"]
  csv << ["Rabu", "Citra"]
end

puts File.read("piket.csv")
CSV.read("piket.csv").each do |baris|
  puts "#{baris[0]}: #{baris[1]}"
end
text Keluaran
Senin,Ali
Selasa,Budi
Rabu,Citra
Senin: Ali
Selasa: Budi
Rabu: Citra

Berkas di atas dijalankan dengan perintah ruby piket-csv.rb. Tiga baris pertama keluaran adalah isi mentah piket.csv, yaitu nilai-nilai yang dipisahkan koma pada setiap baris. Setelah dibaca dengan CSV.read, setiap baris menjadi array dua anggota, sehingga hari dan nama petugas diambil melalui indeks 0 dan 1. Pembacaan indeks seperti ini tepat untuk tabel tanpa baris judul.

CSV dengan headers: true#

Banyak berkas CSV memiliki baris pertama berisi nama kolom, dan opsi headers: true memanfaatkan baris itu secara penuh. Dengan opsi ini, CSV.read mengembalikan tabel yang setiap barisnya berupa objek CSV::Row, dan nilai diakses melalui nama kolomnya alih-alih nomor indeks. Bentuk akses seperti ini membuat kode tidak terganggu ketika urutan kolom berubah. Objek tabelnya menampung nama kolom pada atribut headers, dan setiap baris dapat dikonversi menjadi hash melalui to_h.

ruby nilai-csv.rb
# membaca tabel nilai ujian dengan headers: true
require "csv"

File.write("nilai.csv", "nama,nilai\nAli,82\nBudi,91\nCitra,77\n")

CSV.read("nilai.csv", headers: true).each do |baris|
  puts "#{baris["nama"]} memperoleh #{baris["nilai"]}"
end
text Keluaran
Ali memperoleh 82
Budi memperoleh 91
Citra memperoleh 77

Berkas di atas dijalankan dengan perintah ruby nilai-csv.rb. Baris pertama berkas berisi nama kolom nama dan nilai, sehingga setiap baris berikutnya dapat diambil dengan baris["nama"] dan baris["nilai"]. Nilai yang diambil tetap berupa teks, dan konversi menjadi bilangan dilakukan dengan to_i atau to_f apabila nilainya akan dihitung.

Memilih JSON atau CSV#

Kedua format melayani kebutuhan yang berbeda, dan pilihannya mengikuti bentuk data yang akan disimpan. JSON unggul pada data bersarang karena hash dan array bertingkatnya dipertahankan, sedangkan CSV unggul pada tabel datar dengan kolom yang seragam. Teks JSON pada tabel yang panjang memboroskan ruang karena nama kuncinya ditulis ulang pada setiap baris, sedangkan CSV hanya menyimpan nilainya sekali per sel. Tabel berikut merangkum perbedaan yang paling menentukan.

AspekJSONCSV
Strukturbersarang, mendukung hash dan array bertingkattabular datar berupa baris dan kolom
Tipe nilaiangka, teks, boolean, dan null dipertahankanseluruh nilai berupa teks
Bentuk dokumensatu dokumen dengan isi campurantabel dengan kolom yang seragam
Penggunaan umumkonfigurasi dan pertukaran data antarprogramrekap nilai, daftar piket, dan ekspor tabel

Sifat nilai CSV yang seluruhnya berupa teks dapat diverifikasi langsung, termasuk nilai yang semula ditulis sebagai bilangan.

ruby Nilai CSV selalu berupa teks
require "csv"

File.write("satu-baris.csv", CSV.generate_line(["Ali", 82]))
baris = CSV.read("satu-baris.csv").first
p baris[0].class
p baris[1].class
text Keluaran
String
String

Bilangan 82 dikirim sebagai bagian baris CSV, tetapi hasil pembacaannya berupa String sebagaimana anggota lainnya. Perilaku ini berbeda dengan JSON yang mempertahankan jenisnya, sehingga data CSV selalu memerlukan konversi eksplisit sebelum dihitung. Karena itu, tabel rekap sederhana cocok disimpan sebagai CSV, sedangkan data bertingkat seperti konfigurasi lebih sesuai dalam JSON.

Kesalahan umum#

Kesalahan pertama adalah menggunakan JSON atau CSV tanpa memuat pustakanya terlebih dahulu. Nama konstanta tersebut belum ada sebelum require dijalankan, dan programnya berhenti karena NameError.

ruby NameError ketika pustaka belum dimuat
begin
  JSON.generate({ suhu: 24.5 })
rescue NameError => e
  puts "#{e.class}: #{e.message}"
end
text Keluaran
NameError: uninitialized constant JSON

Perbaikannya sederhana, yaitu menambahkan require "json" pada bagian atas berkas sebelum konstanta JSON digunakan. Syarat yang sama berlaku untuk require "csv".

Kesalahan kedua adalah menguraikan teks JSON yang tidak lengkap tanpa penanganan. JSON.parse membangkitkan JSON::ParserError untuk teks yang tidak membentuk JSON sah, misalnya kurung penutup yang hilang.

ruby JSON rusak ditangani sebagai JSON::ParserError
require "json"

begin
  JSON.parse('{"suhu": 24.5')
rescue JSON::ParserError => e
  puts e.class
end
text Keluaran
JSON::ParserError

Teks contoh kehilangan kurung kurawal penutupnya, sehingga penguraian gagal dan cabang rescue menangkapnya. Data JSON dari luar program memang perlu diperlakukan dengan penangkapan semacam ini karena bentuknya tidak dapat dijamin.

Kesalahan ketiga adalah mengakses baris CSV dengan nama kolom tanpa menggunakan headers: true. Hasil pembacaan tanpa opsi itu berupa array biasa, sehingga baris["nama"] mengembalikan nil dan program melanjutkan dengan data kosong. Gunakan headers: true ketika berkasnya memiliki baris judul, atau akses melalui indeks ketika tidak ada.

Ringkasan#

  • require "json" dan require "csv" memuat pustaka standar untuk kedua format tanpa pemasangan gem tambahan.
  • JSON.generate menghasilkan teks JSON dari hash dan array, sedangkan JSON.parse menguraikannya kembali dengan kunci string atau simbol melalui symbolize_names: true.
  • CSV.open ber-mode “w” menulis baris dengan operator <<, sedangkan CSV.read mengembalikan array dari array baris berkas.
  • headers: true menjadikan setiap baris diakses lewat nama kolom, dan seluruh nilai CSV tetap berupa teks yang perlu dikonversi sebelum dihitung.
  • JSON cocok untuk data bersarang dan pertukaran antarprogram, sedangkan CSV cocok untuk tabel datar dengan kolom seragam.

Referensi#

  • JSON — Ruby — Dokumentasi pustaka JSON standar beserta JSON.generate dan JSON.parse.
  • CSV — ruby/csv — Repositori resmi pustaka csv beserta dokumentasi penggunaannya pada README.