CSV to jeden z tych formatów, którego nikt nie kocha, ale wszyscy używają. Istnieje od lat 70., nadal jest domyślnym formatem eksportu w każdej aplikacji arkusza kalkulacyjnego na świecie i z niemal całą pewnością siedzi gdzieś w Twoim pipeline danych. Format wygląda banalnie prosto — wartości oddzielone przecinkami — ale jego prawidłowe parsowanie jest zaskakująco łatwe do popełnienia błędów. Przejdźmy przez to, jak naprawdę działa, gdzie robi się nieprzyjemnie i jak sobie z tym radzić w Pythonie i JavaScript bez strzelania sobie w stopę.

Czym właściwie jest CSV

CSV oznacza Comma-Separated Values (Wartości Rozdzielone Przecinkami). Plik CSV to zwykły tekst — brak kodowania binarnego, metadanych, struktury wykraczającej poza wiersze i kolumny. Każda linia to rekord, każda wartość w linii jest oddzielona ogranicznikiem (zwykle przecinkiem), a pierwszy wiersz to zgodnie z konwencją wiersz nagłówka z nazwami kolumn. Oto jak wygląda prawdziwy plik CSV:

text
order_id,customer,product,quantity,unit_price,shipped
1001,Alice Nguyen,USB-C Hub,2,34.99,true
1002,Bob Martinez,Mechanical Keyboard,1,129.00,false
1003,Alice Nguyen,HDMI Cable,3,12.50,true
1004,Carol Smith,Webcam 1080p,1,79.95,false

To wszystko — sześć kolumn, cztery wiersze danych i nagłówek. Żadnych nawiasów kątowych, żadnych nawiasów klamrowych, żadnych wymaganych cudzysłowów (na razie). Ta prostota jest dokładnie powodem, dla którego CSV przetrwało: prawie każde narzędzie na ziemi może go otworzyć, odczytać i wyprodukować. Excel, Google Sheets, polecenie COPY PostgreSQL, pandas, read.csv() R — wszystkie obsługują CSV od razu. Kompromisem jest to, że format ma prawie żadnej struktury: brak typów, zagnieżdżenia, schematu. Każda wartość jest ciągiem znaków, dopóki nie zdecydujesz inaczej.

RFC 4180 — Standard, który nie jest naprawdę egzekwowany

Istnieje specyfikacja: RFC 4180, opublikowana w 2005 roku. Definiuje takie rzeczy jak zakończenia linii CRLF, escaping cudzysłowów podwójnych i obsługę osadzonych przecinków. Ale oto rzecz — RFC 4180 jest informacyjny, a nie standardowy. Opisuje to, co już było powszechną praktyką, a nie legisluje. Nikt nie musi go przestrzegać i w praktyce prawie nikt nie przestrzega go dokładnie.

Rezultatem jest chaos dialektów CSV. Masz pliki używające LF zamiast CRLF, pliki gdzie pierwszy wiersz może, ale nie musi być nagłówkiem, pliki z końcowym znakiem nowej linii i bez niego, pliki z UTF-8 BOM dodanym przez Excel. Artykuł Wikipedii o CSV ma solidny przegląd wszystkich rzeczy, które różnią się w praktyce. Najbezpieczniejsze podejście: nigdy nie zakładaj niczego o pliku CSV, którego sam nie produkowałeś, i zawsze używaj właściwego parsera zamiast naiwnego podziału na przecinkach.

Zasady cytowania: gdy przecinki pojawiają się wewnątrz wartości

Tu właśnie "po prostu podziel na przecinkach" zawodzi. Co się dzieje, gdy wartość zawiera przecinek? Albo nową linię? Albo cudzysłów podwójny? RFC 4180 — i większość rzeczywistych parserów — obsługuje to przez owijanie pola w cudzysłowy podwójne. Oto pełny zestaw zasad:

  • Jeśli pole zawiera przecinek, nową linię lub cudzysłów podwójny, owiń całe pole w cudzysłowy podwójne
  • Jeśli pole zawiera cudzysłów podwójny, zescapuj go przez podwojenie ("")
  • Cytowane pola mogą obejmować wiele linii — nowa linia staje się częścią wartości
  • Białe znaki wewnątrz cudzysłowów są znaczące i muszą być zachowane
text
order_id,customer,notes,unit_price
1005,David Lee,"Wants gift wrapping, express shipping",45.00
1006,Emma Brown,"Said: ""please handle with care""",89.99
1007,Frank Wu,"Address:
123 Main St
Apt 4B",15.50

W tym przykładzie: notes Davida zawiera przecinek, więc jest cytowane. Notatka Emmy zawiera cudzysłowy podwójne, więc są podwojone wewnątrz zewnętrznych cudzysłowów. Adres Franka obejmuje wiele linii — podziały linii są częścią wartości. Naiwny line.split(',') w dowolnym języku kompletnie zniszczy wszystkie trzy z nich. Dlatego potrzebujesz prawdziwego parsera.

Złota zasada parsowania CSV: Nigdy nie dziel ręcznie na przecinkach. Zawsze używaj dedykowanej biblioteki parsowania CSV. Obsługuje cytowanie, sekwencje escape, pola wieloliniowe i kodowanie — żadna z tych rzeczy nie może być poprawnie wykonana przez prosty split.

Warianty ograniczników: tabulatory, średniki, pipe

Pomimo nazwy, CSV nie musi używać przecinków. Kilka powszechnych wariantów używa różnych ograniczników i spotkasz je wszystkie w praktyce:

  • TSV (Tab-Separated Values) — używa \t jako ogranicznika. Powszechny w bioinformatyce (wyjście BLAST, pliki VCF), eksportach baz danych i wszędzie tam, gdzie wartości często zawierają przecinki
  • Oddzielone średnikami — domyślne w Excelu dla lokalizacji, gdzie przecinek jest separatorem dziesiętnym (Niemcy, Francja, większość UE). Jeśli kiedykolwiek otworzyłeś plik CSV od europejskiego kolegi i uzyskałeś jedną gigantyczną kolumnę, właśnie dlatego
  • Oddzielone pipe — używa |. Powszechne w starszych eksportach danych bankowych i ubezpieczeniowych, gdzie tabulatory mogą być usuwane przez systemy mainframe
  • Stała szerokość — technicznie nie CSV, ale często zaliczany do tej samej kategorii. Kolumny są wypełniane do stałych szerokości zamiast być rozdzielane

Większość parserów CSV pozwala jawnie określić ogranicznik. Gdy piszesz plik CSV, który inni będą używać, udokumentuj swój ogranicznik. Gdy czytasz taki, którego nie produkowałeś, sprawdź pierwsze kilka linii przed założeniem. Narzędzie CSV Formatter może pomóc w inspekcji i reformatowaniu plików z niestandardowymi ogranicznikami.

Parsowanie CSV w Pythonie

Standardowa biblioteka Pythona zawiera moduł csv i jest naprawdę dobra. Dwie klasy, których będziesz używać najczęściej, to csv.reader dla dostępu do wierszy jako list i csv.DictReader dla dostępu do wierszy jako słowników (co jest prawie zawsze tym, czego chcesz).

python
import csv

# csv.reader — each row is a list of strings
with open('orders.csv', 'r', encoding='utf-8', newline='') as f:
    reader = csv.reader(f)
    header = next(reader)          # consume the header row
    for row in reader:
        order_id, customer, product, quantity, price, shipped = row
        print(f"Order {order_id}: {quantity}x {product} for {customer}")

# csv.DictReader — each row is an OrderedDict keyed by the header
with open('orders.csv', 'r', encoding='utf-8', newline='') as f:
    reader = csv.DictReader(f)
    for row in reader:
        if row['shipped'] == 'false':
            print(f"Pending: {row['order_id']} — {row['customer']}")

Dwie rzeczy, które należy zawsze dołączyć: encoding='utf-8' (lub cokolwiek plik faktycznie używa) i newline=''. Ten drugi jest krytyczny — moduł csv sam obsługuje nowe linie i potrzebuje surowych bajtów. Bez newline='' możesz uzyskać dodatkowe puste wiersze w systemie Windows.

Pisanie jest równie proste z csv.writer:

python
import csv

orders = [
    {'order_id': 1008, 'customer': 'Grace Kim', 'product': 'Laptop Stand', 'quantity': 1, 'unit_price': 49.99, 'shipped': False},
    {'order_id': 1009, 'customer': 'Henry Park', 'product': 'USB-C Hub', 'quantity': 2, 'unit_price': 34.99, 'shipped': True},
]

with open('new_orders.csv', 'w', encoding='utf-8', newline='') as f:
    fieldnames = ['order_id', 'customer', 'product', 'quantity', 'unit_price', 'shipped']
    writer = csv.DictWriter(f, fieldnames=fieldnames)

    writer.writeheader()
    writer.writerows(orders)

# To use a different delimiter (e.g. tab-separated)
with open('new_orders.tsv', 'w', encoding='utf-8', newline='') as f:
    writer = csv.writer(f, delimiter='\t')
    writer.writerow(['order_id', 'customer', 'product'])
    writer.writerow([1008, 'Grace Kim', 'Laptop Stand'])

Moduł csv automatycznie zajmuje się całym cytowaniem — jeśli pole zawiera przecinek lub nową linię, owija je w cudzysłowy podwójne bez konieczności myślenia o tym. To właśnie sens używania biblioteki.

Parsowanie CSV w JavaScript / Node.js

JavaScript nie ma wbudowanego parsera CSV. Pokusa jest zrobić coś takiego:

js
// ❌ Don't do this — breaks immediately on quoted fields
const rows = csvText.split('\n').map(line => line.split(','));

To zawodzi natychmiast, gdy jakakolwiek wartość zawiera przecinek, nową linię w cudzysłowach lub zescapowany cudzysłów podwójny. Do czegokolwiek prawdziwego, użyj biblioteki. PapaParse to podstawowe narzędzie — jest szybkie, obsługuje wszystkie przypadki brzegowe, działa zarówno w przeglądarce, jak i Node.js, i obsługuje przesyłanie strumieniowe dla dużych plików.

js
import Papa from 'papaparse';
import fs from 'fs';

// Parse a CSV string
const csvText = fs.readFileSync('orders.csv', 'utf8');

const result = Papa.parse(csvText, {
  header: true,          // first row becomes object keys
  skipEmptyLines: true,  // ignore blank lines at end of file
  dynamicTyping: true,   // converts "true"/"false" to booleans, numbers to numbers
});

console.log(result.data);
// [
//   { order_id: 1001, customer: 'Alice Nguyen', product: 'USB-C Hub', quantity: 2, unit_price: 34.99, shipped: true },
//   { order_id: 1002, customer: 'Bob Martinez', product: 'Mechanical Keyboard', quantity: 1, unit_price: 129, shipped: false },
//   ...
// ]

// Check for parse errors
if (result.errors.length > 0) {
  console.error('Parse errors:', result.errors);
}

// Generate CSV from an array of objects
const orders = [
  { order_id: 1008, customer: 'Grace Kim', product: 'Laptop Stand', quantity: 1, unit_price: 49.99 },
  { order_id: 1009, customer: 'Henry Park', product: 'USB-C Hub', quantity: 2, unit_price: 34.99 },
];

const csvOutput = Papa.unparse(orders);
fs.writeFileSync('export.csv', csvOutput, 'utf8');

Opcja dynamicTyping jest przydatna, ale warto o niej wiedzieć — automatycznie konwertuje takie rzeczy jak "34.99" na liczbę. To zazwyczaj to, czego chcesz, ale może Cię zaskoczyć, jeśli pole takie jak order_id jest liczbą w CSV, ale chciałeś go jako ciąg. Wyłącz, jeśli potrzebujesz ścisłego wyjścia ciągów.

Do szybkich konwersji między CSV a innymi formatami narzędzie CSV do JSON obsługuje najczęstsze przypadki w przeglądarce bez żadnego kodu — przydatne do jednorazowych transformacji danych. Jest też CSV do XML, jeśli musisz zasilić dane do systemu opartego na XML.

Typowe pułapki, na które na pewno trafisz

Nawet z dobrym parserem, CSV ma kilka dobrze znanych min. Oto te, które pojawiają się najczęściej:

  • Bajty BOM z Excela. Gdy Excel eksportuje CSV jako "UTF-8", często poprzedza go UTF-8 BOM (EF BB BF w hex lub \ufeff jako znak). To sprawia, że pierwsza nazwa kolumny nagłówka wygląda jak order_id zamiast order_id. W Pythonie otwórz plik z encoding='utf-8-sig' zamiast utf-8, aby usunąć go automatycznie. PapaParse obsługuje to przezroczyście.
  • Zakończenia linii CRLF vs LF. RFC 4180 określa CRLF (\r\n), ale narzędzia Unix produkują LF (\n), a stare pliki Mac używają samego CR (\r). Dlatego moduł csv Pythona potrzebuje newline='' — obsługuje wewnętrznie wszystkie trzy. Jeśli czytasz surowe bajty i dzielisz ręcznie, musisz jawnie usuwać \r.
  • Problemy z kodowaniem. CSV nie ma sposobu na zadeklarowanie własnego kodowania — w przeciwieństwie do <meta charset> HTML lub <?xml encoding="..."?> XML. Excel często zapisuje pliki w Windows-1252 (aka CP1252) zamiast UTF-8, co zniekształca znaki akcentowane. Jeśli widzisz znaki takie jak é zamiast é, masz plik UTF-8 dekodowany jako Latin-1 lub odwrotnie. Zawsze ustal kodowanie poza pasmem z tym, kto produkuje plik.
  • Liczby wyglądające jak daty. Excel cicho konwertuje wartości takie jak 1-2 lub 03/04 na daty podczas otwierania lub zapisywania. Jeśli eksportujesz kody produktów lub numery wersji, poprzedź je pojedynczym cudzysłowem w Excelu ('1-2), aby temu zapobiec — lub powiedz temu, kto produkuje plik, aby tak zrobił.
  • Końcowe przecinki. Niektóre eksportery emitują końcowy przecinek na końcu każdej linii, co tworzy fantomową pustą kolumnę. Solidny parser go ignoruje; naiwny split tworzy dodatkowy pusty element ciągu.

Jeśli masz do czynienia z plikiem, który wygląda nieprawidłowo, narzędzie CSV Validator może szybko powiedzieć, czy plik jest poprawnie uformowany i oznaczyć problemy z kodowaniem lub strukturalne przed próbą jego przetworzenia.

CSV vs JSON vs Excel — kiedy używać czego

Te trzy formaty mocno się pokrywają w praktyce, ale każdy ma wyraźny punkt mocny:

  • Używaj CSV, gdy przenosisz płaskie, tabelaryczne dane między systemami — eksporty baz danych, pipeline'y analityczne, importy arkuszy kalkulacyjnych, masowe ładowanie danych. Jest powszechnie obsługiwany, mały rozmiarowo i trywialnie porównywalny w git. Ograniczenie: jest płaski. Brak zagnieżdżenia, typów, relacji.
  • Używaj JSON, gdy dane są hierarchiczne lub schemat ma znaczenie. Zamówienie z wieloma pozycjami, plik konfiguracyjny z zagnieżdżonymi obiektami, odpowiedź API — to jest naturalnie JSON. CSV zmusiłoby Cię do denormalizacji danych lub wymyślenia własnej konwencji zagnieżdżania. Specyfikacja JSON jest czysta i jednoznaczna; format zachowuje typy (liczby, wartości logiczne, null, tablice, obiekty).
  • Używaj Excela (.xlsx), gdy wyjście jest przeznaczone dla ludzi, nie maszyn. Formatowanie, formuły, wiele arkuszy, wykresy — jeśli użytkownik biznesowy jest ostatecznym konsumentem, Excel jest często właściwym wyborem. Nigdy nie używaj go jako formatu wymiany między systemami. Specyfikacja OOXML jest ogromna, a format jest kruchy między wersjami.

Jest praktyczna heurystyka: jeśli naturalnie przeglądałbyś dane w arkuszu kalkulacyjnym, CSV jest prawdopodobnie odpowiednie. Jeśli przeglądałbyś je w drzewie lub edytorze kodu, użyj JSON. Jeśli wysyłasz to do interesariusza biznesowego, który będzie filtrować i sortować, użyj Excela.

Podsumowanie

CSV jest prosty z założenia i zwodniczo skomplikowany w praktyce. Format nie ma egzekwowanego standardu, jest dostarczany w wielu wariantach ograniczników i opiera się na zasadach cytowania, które psują każdą próbę ręcznego parsowania. Naprawą jest zawsze to samo: użyj prawdziwego parsera (wbudowany moduł csv Pythona lub PapaParse w JavaScript), zawsze jawnie określaj kodowanie i uważaj na bajty BOM z Excela. Gdy masz niezawodny parser, CSV jest naprawdę świetnym formatem — szybkim w produkcji, łatwym do inspekcji w każdym edytorze tekstu i obsługiwanym wszędzie. Do codziennej pracy z plikami CSV narzędzia CSV do JSON, CSV Formatter i CSV Validator obsługują typowe operacje bez pisania linii kodu.