Bekerja dengan CSV

Parsing CSV buatan sendiri dan dengan pustaka.

Daftar isi

Berkas CSV (comma-separated values) menyimpan data tabel sebagai teks biasa dengan kolom yang dipisahkan koma dan baris yang dipisahkan penanda baris. Topik ini membahas jebakan pemisahan sederhana dengan Split, pembuatan parser CSV buatan sendiri yang menangani tanda kutip, kemudian diakhiri pengenalan pustaka CSV siap pakai untuk kebutuhan yang lebih kompleks. Setiap contoh menulis berkas percobaannya sendiri, membacanya kembali, dan membersihkannya setelah selesai.

Bentuk berkas CSV dan jebakan pemisahan sederhana#

Satu baris CSV berisi satu baris data, dan setiap koma di dalam baris itu menandai batas kolom. Berkas berikut berisi catatan sensor suhu dengan satu nilai yang mengandung koma, yaitu 29,5 sebagai penulisan desimal, sehingga nilai tersebut dibungkus tanda kutip.

csv
waktu,suhu,kelembapan
08:00,24,70
12:00,"29,5",65
16:00,26,68

Kebiasaan lain yang sering dijumpai adalah pemisah titik koma pada berkas dari wilayah yang memakai koma sebagai tanda desimal, sehingga penanda baris dan pemisah selalu diperiksa terlebih dahulu sebelum berkas diurai. Kebutuhan paling mudah adalah memanggil metode Split(',') untuk memecah baris menjadi kolom. Pendekatan itu benar selama tidak ada koma di dalam nilai, tetapi berbisanya langsung terlihat pada baris ketiga berkas di atas, karena koma di dalam kutip seharusnya tetap menjadi bagian nilai. Contoh berikut memperlihatkan kegagalannya.

csharp Contoh: kegagalan Split pada nilai yang mengandung koma
// Contoh: kegagalan Split pada nilai yang mengandung koma
string baris = "12:00,\"29,5\",65";
string[] kolom = baris.Split(',');
Console.WriteLine($"Jumlah kolom: {kolom.Length}");
Console.WriteLine(string.Join(" / ", kolom));
text Keluaran
Jumlah kolom: 4
12:00 / "29 / 5" / 65

Seharusnya baris itu terdiri atas tiga kolom, tetapi Split menghasilkan empat potongan karena metode tersebut tidak memahami tanda kutip. Nilai ketiga pun terpecah menjadi "29 dan 5", sehingga data yang terbaca sudah tidak sesuai sumbernya. Parser CSV yang benar perlu mengetahui apakah posisi baca sedang berada di dalam tanda kutip atau tidak.

Parser CSV buatan sendiri#

Parser CSV minimal dapat dibangun dengan satu penanda keadaan (state) bernama dalamKutip yang berubah setiap kali karakter tanda kutip ditemui. Selama penanda itu bernilai true, koma diabaikan sebagai pemisah dan diperlakukan sebagai bagian nilai, sedangkan pada keadaan biasa koma menutup kolom yang sedang dibangun. Nilai tiap kolom disusun pada StringBuilder, dan kolom yang sudah lengkap disimpan ke dalam daftar. Contoh berikut menulis berkas data/sensor.csv yang sama dengan bagian sebelumnya, membacanya dengan parser tersebut, lalu menghapus berkas percobaannya.

csharp Program.cs
// Program: menulis dan membaca berkas CSV buatan sendiri
using System.Text;

string jalur = Path.Combine("data", "sensor.csv");
Directory.CreateDirectory("data");

File.WriteAllLines(jalur, new[]
{
    "waktu,suhu,kelembapan",
    "08:00,24,70",
    "12:00,\"29,5\",65",
    "16:00,26,68"
});

foreach (string[] kolom in BacaCsv(jalur))
{
    Console.WriteLine($"{kolom[0]} | {kolom[1]} | {kolom[2]}");
}

File.Delete(jalur);
Directory.Delete("data");

static IEnumerable<string[]> BacaCsv(string jalurBerkas)
{
    foreach (string baris in File.ReadLines(jalurBerkas))
    {
        yield return PisahBaris(baris);
    }
}

static string[] PisahBaris(string baris)
{
    List<string> kolom = new();
    bool dalamKutip = false;
    StringBuilder nilai = new();

    foreach (char karakter in baris)
    {
        if (karakter == '"')
        {
            dalamKutip = !dalamKutip;
        }
        else if (karakter == ',' && !dalamKutip)
        {
            kolom.Add(nilai.ToString());
            nilai.Clear();
        }
        else
        {
            nilai.Append(karakter);
        }
    }

    kolom.Add(nilai.ToString());
    return kolom.ToArray();
}
text Keluaran
waktu | suhu | kelembapan
08:00 | 24 | 70
12:00 | 29,5 | 65
16:00 | 26 | 68

Untuk menjalankan program tersebut dari folder topik, gunakan perintah berikut.

bash
# aktifkan .NET yang terpasang di sandbox
export DOTNET_ROOT=/home/kal/.dotnet PATH=/home/kal/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
# jalankan program contoh dari folder topik
dotnet run --file Program.cs
text Keluaran
waktu | suhu | kelembapan
08:00 | 24 | 70
12:00 | 29,5 | 65
16:00 | 26 | 68

Baris ketiga kini terbaca sebagai tiga kolom utuh dengan nilai 29,5 tetap menyatu. Perjalanan baris 12:00,"29,5",65 memberi gambaran kerjanya. Karakter 12:00 terkumpul hingga koma ditemui pada keadaan biasa, sehingga kolom pertama tersimpan, dan tanda kutip berikutnya membalik keadaan menjadi di dalam kutip. Koma sesudahnya diperlakukan sebagai bagian nilai, sedangkan tanda kutip penutup mengembalikan keadaan ke biasa, sehingga koma terakhir memisahkan kolom kedua dari 65. Metode ReadLines membaca berkas baris demi baris tanpa memuat seluruh isi ke memori, dan fungsi PisahBaris mengembalikan hasilnya sebagai larik string. Parser ini masih memiliki batasan yang perlu diketahui, yaitu baris CSV yang dibungkus kutip hingga mengandung penanda baris baru di dalam nilainya tidak terbaca benar, karena pembacaan baris dilakukan terlebih dahulu sebelum penguraian. Tanda kutip ganda di dalam nilai juga belum di-escape, yaitu ditulis dua kali "" untuk mewakili satu tanda kutip sesuai konvensi CSV.

Penulisan CSV mengikuti aturan kebalikannya, yaitu nilai yang mengandung koma, tanda kutip, atau penanda baris harus dibungkus tanda kutip saat ditulis. Parser di atas hanya membaca, sehingga penulisan pada contoh menyusun setiap baris secara manual dan membungkus 29,5 dengan kutip sekaligus. Apabila penulisan dan pembacaan berada di tangan yang sama, kesepakatan bentuk ini cukup dijaga di satu tempat, sedangkan pertukaran dengan pihak lain menuntut kepatuhan penuh pada aturan CSV.

Pustaka CSV siap pakai#

Parser buatan sendiri tepat untuk pembelajaran dan data yang bentuknya diketahui, sedangkan data CSV dari sumber nyata lebih aman diolah dengan pustaka. Pustaka yang paling banyak dipakai di ekosistem .NET adalah CsvHelper, yang terpasang melalui perintah dotnet add package CsvHelper pada proyek console. Kelas CsvWriter menulis baris dari objek atau dari kolom manual, sedangkan CsvReader membaca berkas baris demi baris melalui konfigurasi yang bisa diatur, misalnya pemisah titik koma untuk berkas dari wilayah yang memakainya. Metode GetRecords<T> memetakan setiap baris CSV langsung menjadi objek bertipe record atau kelas berdasarkan nama kolom pada baris kepala.

Pustaka juga menangani aturan yang belum dikerjakan parser minimal di atas, yaitu baris baru di dalam kutip, peng-escape-an tanda kutip ganda, baris kepala yang tidak konsisten, dan berkas berukuran besar melalui pembacaan streaming. Pola pemakaiannya tetap sama dengan contoh di bagian sebelumnya, yaitu berkas dibuka dengan StreamReader, kelas pustakanya membungkus pembaca tersebut, dan hasilnya dikembalikan sebagai kumpulan record. Dokumentasi resminya memuat contoh lengkap untuk pembacaan, penulisan, dan pemetaan berbasis atribut.

Ringkasan#

  • Nilai CSV yang mengandung koma dibungkus tanda kutip, sehingga pemisahan dengan Split(',') menghasilkan kolom yang salah.
  • Parser CSV minimal memakai penanda dalamKutip untuk membedakan koma pemisah dari koma di dalam nilai.
  • Parser buatan sendiri tidak menangani baris baru di dalam kutip maupun peng-escape-an tanda kutip ganda.
  • Pustaka seperti CsvHelper menangani seluruh aturan CSV dan memetakan baris menjadi objek melalui GetRecords<T>.

Referensi#