JSON Toolkit

Konwersja JSON do i z CSV oraz danych tabelarycznych

Podsumowanie: CSV jest dwuwymiarowy, JSON ma kształt drzewa. Trudna część konwersji to nie kod, ale ustalenie z góry, jak spłaszczać tablice i jak zachować typy.

Początkujący~10 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu o konwersji JSON ↔ CSV i danych tabelarycznych

Najważniejsze: spłaszczaj kluczami z kropkami i tablicami serializowanymi do JSON, gdy eksport ma czytać człowiek; rozdzielaj na tabelę nadrzędną i powiązane, gdy wczytujesz do bazy; rozwijaj tablice w dodatkowe wiersze tylko dla tabel przestawnych; przy przywracaniu konwertuj według jawnych definicji kolumn, a nie globalnego wnioskowania.

Spłaszczenie drzewa do tabeli zawsze gubi hierarchię; przywrócenie tabeli do drzewa zawsze zgaduje typy. W obu kierunkach reguły trzeba ustalić z góry.

1. Co CSV naprawdę określa

RFC 4180 liczy tylko kilka stron i określa mniej, niż wielu osobom się wydaje:

  1. Każdy rekord zajmuje jeden wiersz, a pola są rozdzielane przecinkami;
  2. Terminatorem wiersza jest CRLF, choć w praktyce akceptowany jest też \n;
  3. Pole zawierające przecinek, cudzysłów lub nową linię musi być ujęte w podwójne cudzysłowy;
  4. Podwójne cudzysłowy wewnątrz pola zapisuje się jako dwa cudzysłowy "";
  5. Pierwszy wiersz może być nagłówkiem;
  6. Nie określa kodowania znaków ani typów.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

Zwróć uwagę na regułę 3: pola zawierające nowe linie są legalne, więc naiwny parser, który dzieli po \n, a potem po ,, z pewnością zawiedzie.

2. JSON → CSV: najpierw zdecyduj o strukturze

Istnieją tylko trzy strategie spłaszczenia struktury drzewa do tabeli dwuwymiarowej:

Strategia 1: spłaszczenie pól (najczęstsza)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • Łącz klucze zagnieżdżonych obiektów kropką .;
  • Tablic nie da się wyrazić jako kolumn, więc serializuj je JSON.stringify prosto do komórki (co nadal jest legalnym polem CSV);
  • Wada: arkusze nie potrafią filtrować tablicy, więc wymagane jest ponowne parsowanie.

Strategia 2: rozwinięcie tablicy w wiele wierszy

id,user.name,tag
1,张三,a
1,张三,b

Pasuje do relacji jeden-do-wielu, w których odbiorcy niższego szczebla agregują per element. Wadą są zduplikowane pola nadrzędne i rosnąca liczba wierszy.

Strategia 3: podział na wiele tabel

Tabela główna plus powiązane tabele połączone identyfikatorem — najczystsze podejście, ale wykracza poza „pojedynczy eksport” i wygląda bardziej jak modelowanie danych.

Którą wybrać? Strategię 1 dla eksportów przeznaczonych dla ludzi, strategię 3 do wczytania do bazy, a strategię 2 do tworzenia tabel przestawnych.

3. CSV → JSON: jak zgadywane są typy

CSV nie ma typów, więc każdy etap przywracania jest zgadywaniem:

Treść komórki Możliwe znaczenie Ryzyko bezpośredniej konwersji na liczbę
00123 Identyfikator pracownika, kod pocztowy Utrata zer wiodących → 123
1.10 Numer wersji, kwota Staje się 1.1
1e5 Identyfikator lub notacja naukowa Staje się 100000
2026-10-30 Data lub łańcuch Niejednoznaczność strefy czasowej i formatu
13800138000 Numer telefonu Wciąż bezpieczny przy 11 cyfrach, ale niektóre identyfikatory tracą precyzję
"" Pusty łańcuch lub null Niejednoznaczność semantyczna

Sugerowane domyślne ustawienie: traktuj wszystko jako łańcuch i pozwól schematowi albo jawnej definicji kolumn zdecydować, które kolumny stają się liczbami lub wartościami logicznymi. „Inteligentne wnioskowanie” cieszy przy eksporcie i jest źródłem incydentów przy imporcie.

type ColumnType = 'string' | 'number' | 'boolean' | 'json';

/**
 * Converts CSV rows into objects according to the column definitions.
 * @param rows The raw row array; the first row is the header
 * @param columnTypes Mapping from column name to type
 * @returns The converted array of objects
 */
function rowsToRecords(
  rows: string[][],
  columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
  const [headerRow, ...dataRows] = rows;

  return dataRows.map((dataRow) => {
    const record: Record<string, unknown> = {};

    headerRow.forEach((columnName, columnIndex) => {
      const rawValue = dataRow[columnIndex] ?? '';
      const columnType = columnTypes[columnName] ?? 'string';

      if (columnType === 'number') {
        const numericValue = Number(rawValue);
        record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
        return;
      }
      if (columnType === 'boolean') {
        record[columnName] = rawValue === 'true' || rawValue === '1';
        return;
      }
      if (columnType === 'json') {
        try {
          record[columnName] = JSON.parse(rawValue);
        } catch {
          record[columnName] = rawValue;
        }
        return;
      }
      record[columnName] = rawValue;
    });

    return record;
  });
}

4. Trzy pułapki związane z Excelem

4.1 Kodowanie

Gdy Excel otwiera CSV UTF-8 bez BOM, chiński tekst zamienia się w krzaczki. Dodanie BOM (\uFEFF) przy eksporcie jest najtańszą poprawką zgodności:

const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;

4.2 Długie liczby zamieniające się w notację naukową

18-cyfrowy numer dowodu lub 19-cyfrowy numer zamówienia pojawia się w Excelu jako 1.23457E+17, a sama wartość została już obcięta. Poprzedzenie pola tabulatorem lub opakowanie w formułę (="1234...") zachowuje formatowanie tekstowe, ale zamienia komórkę w formułę. Bezpieczniejszym podejściem jest poinformowanie użytkowników, aby „użyli funkcji importu i wskazali kolumnę jako tekst”, albo udostępnienie .xlsx obok eksportu.

4.3 Separatory zależne od ustawień regionalnych

Niektóre ustawienia europejskie domyślnie używają średnika ; jako separatora. Przy dystrybucji między regionami albo ustal CSV na przecinki i powiedz to wprost, albo udostępnij wersję TSV (rozdzielaną tabulatorami) — TSV jest bardziej tolerancyjny regionalnie.

5. Obsługa dużych plików

Nie wczytuj do pamięci naraz CSV z ponad setką tysięcy wierszy:

import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';

const parser = createReadStream('orders.csv').pipe(
  parse({ columns: true, skipEmptyLines: true, bom: true }),
);

for await (const record of parser) {
  // Process one record at a time; memory usage is independent of file size
}

Kierunek eksportu działa tak samo: generuj i zapisuj do strumienia na bieżąco, zamiast sklejać jeden wielki łańcuch.

6. Samokontrola spójności w obie strony

Pełny obieg CSV (JSON → CSV → JSON) prawie nigdy nie jest dokładnie identyczny, ale możesz zdefiniować, które straty są akceptowalne:

Kontrola Oczekiwanie
Liczba pól Identyczna
Wartości łańcuchowe Identyczne (w tym zera wiodące)
Typy numeryczne Określone przez definicję kolumny, a nie przez wnioskowanie
Zagnieżdżone obiekty Odzyskiwalne przez ścieżki z kropkami
Tablice Widoczne po serializacji do łańcucha JSON
Puste wartości Znormalizowane do null lub pustego łańcucha — wybierz dokładnie jedną opcję

Umieszczenie tej tabeli w testach jest znacznie skuteczniejsze niż ustalenia słowne.

7. Lista kontrolna

  • [ ] Semantyka „jeden wiersz to jeden rekord” jest określona jawnie i widoczna w nazwie pliku eksportu;
  • [ ] Każda kolumna, która może utracić precyzję, jest eksportowana jako łańcuch;
  • [ ] Chińskie pliki CSV zawierają BOM albo kodowanie jest określone jawnie;
  • [ ] Pola zawierające przecinki, cudzysłowy lub nowe linie są poprawnie escape'owane (użyj biblioteki, nie sklejaj łańcuchów ręcznie);
  • [ ] Duże pliki przechodzą przez strumieniowy odczyt i zapis;
  • [ ] Import konwertuje typy według definicji kolumn, nigdy przez globalne inteligentne wnioskowanie.

FAQ

Jak wybrać między spłaszczeniem, rozwijaniem wierszy a podziałem tabel?

Spłaszczaj przy eksportach dla ludzi: łącz zagnieżdżone klucze kropką i serializuj tablice do komórki przez JSON.stringify, kosztem braku filtrowania tych tablic w arkuszu. Podziel na tabelę nadrzędną plus powiązane, gdy celem jest baza danych. Rozwiń tablice w dodatkowe wiersze, gdy odbiorcy niższego szczebla agregują per element, akceptując zduplikowane pola nadrzędne.

Dlaczego import CSV nie powinien wnioskować typów automatycznie?

RFC 4180 określa separatory i ucieczkę, ale nie typy, więc każdy etap przywracania jest zgadywaniem: 00123 traci zera wiodące, 1.10 staje się 1.1, a 1e5 staje się 100000. Traktuj wszystko domyślnie jako łańcuch i pozwól schematowi lub jawnej definicji kolumn zdecydować, które kolumny stają się liczbami lub wartościami logicznymi.

Czy pełny obieg CSV powinien odtwarzać oryginalny JSON dokładnie?

Prawie nigdy. Zdefiniuj zamiast tego akceptowalne straty: identyczna liczba pól, identyczne wartości łańcuchowe z zerami wiodącymi, typy numeryczne ustalone przez definicję kolumny, zagnieżdżone obiekty odzyskiwalne przez ścieżki z kropkami, tablice widoczne po serializacji oraz puste wartości znormalizowane do null lub pustego łańcucha — wybierz dokładnie jedną opcję.