Převod JSON na CSV a tabulková data a zpět
Shrnutí: CSV je dvourozměrné, JSON má tvar stromu. Obtížná část převodu není kód – je to rozhodnutí předem, jak se zplošťují pole a jak se zachovávají typy.

Stručně: zplošťujte pomocí tečkových klíčů a polí serializovaných do JSON řetězce, když export budou číst lidé; rozdělte data na mateřskou tabulku a související tabulky, když je načítáte do databáze; pole rozšiřte na další řádky jen pro pivoty; při obnově převádějte podle explicitních definic sloupců, nikoli globálním odvozováním.
Zploštění stromu do tabulky vždy ztratí hierarchii; obnova tabulky do stromu vždy hádá typy. Oba směry potřebují pravidla stanovená předem.
1. Co CSV vlastně specifikuje
RFC 4180 má jen několik stránek a specifikuje méně, než mnozí předpokládají:
- Každý záznam zabírá jeden řádek, pole jsou oddělena čárkami;
- Ukončovač řádku je CRLF, i když v praxi se přijímá i
\n; - Pole obsahující čárku, dvojitou uvozovku nebo zalomení řádku musí být uzavřeno v dvojitých uvozovkách;
- Dvojité uvozovky uvnitř pole se zapisují jako dvě dvojité uvozovky
""; - První řádek může být hlavička;
- Není specifikováno kódování znaků a stejně tak nejsou specifikovány typy.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"
Všimněte si pravidla 3: pole obsahující zalomení řádku jsou legální, takže naivní parser, který nejprve rozdělí podle \n a poté podle ,, bude zaručeně špatně.
2. JSON → CSV: nejprve rozhodněte strukturu
Existují pouze tři strategie pro zploštění stromové struktury do dvourozměrné tabulky:
Strategie 1: zploštění polí (nejčastější)
{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
- Klíče vnořených objektů spojte pomocí
.; - Pole nelze vyjádřit jako sloupce, takže je rovnou
JSON.stringifydo buňky (což je stále legální pole CSV); - Nevýhoda: tabulkové nástroje nemohou pole filtrovat, takže je nutné druhé parsování.
Strategie 2: rozšíření pole na více řádků
id,user.name,tag
1,张三,a
1,张三,b
Hodí se pro vztahy jedna k mnoha, kde příjemci agregují po prvcích. Nevýhodou je duplikace polí rodiče a nafouknutý počet řádků.
Strategie 3: rozdělení do více tabulek
Hlavní tabulka plus související tabulky spojené přes id – nejčistší přístup, ale přesahuje „jeden export“ a vypadá spíš jako datové modelování.
Kterou zvolit? Strategii 1 pro exporty určené lidem, strategii 3 pro načtení do databáze a strategii 2 pro pivotování dat.
3. CSV → JSON: jak se hádají typy
CSV nemá typy, takže každý krok obnovy je hádání:
| Obsah buňky | Možný význam | Riziko přímého převodu na číslo |
|---|---|---|
00123 |
ID zaměstnance, PSČ | Úvodní nuly se ztratí → 123 |
1.10 |
Číslo verze, částka | Změní se na 1.1 |
1e5 |
Identifikátor nebo vědecký zápis | Změní se na 100000 |
2026-10-30 |
Datum nebo řetězec | Nejistota v časovém pásmu a formátu |
13800138000 |
Telefonní číslo | S 11 číslicemi ještě bezpečné, ale některá ID přesnost ztratí |
"" |
Prázdný řetězec nebo null | Sémantická nejistota |
Doporučený výchozí stav: vše považujte za řetězec a nechte schéma nebo explicitní definici sloupců rozhodnout, které sloupce se stanou čísly nebo booleany. „Chytré odvozování“ je při exportu okouzlující a při importu zdrojem incidentů.
type ColumnType = 'string' | 'number' | 'boolean' | 'json';
/**
* Převede řádky CSV na objekty podle definic sloupců.
* @param rows Surové pole řádků; první řádek je hlavička
* @param columnTypes Mapování názvu sloupce na typ
* @returns Převedené pole objektů
*/
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. Tři úskalí kolem Excelu
4.1 Kódování
Když Excel otevře CSV v UTF-8 bez BOM, čínský text se změní v mojibake. Přidání BOM (\uFEFF) při exportu je nejlevnější oprava kompatibility:
const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;
4.2 Dlouhá čísla se mění na vědecký zápis
Osmnácticiferné rodné číslo nebo devatenácticiferné číslo objednávky se v Excelu zobrazí jako 1.23457E+17 a hodnota samotná už byla zkrácena.
Prefixování pole tabulátorem nebo obalení jako vzorec (="1234...") zachová textový formát, ale buňku změní na vzorec.
Bezpečnější je říci uživatelům „použijte funkci importu a určete sloupec jako text“, nebo nabídnout k exportu i .xlsx.
4.3 Oddělovače ovlivněné národním prostředím
Některá evropská národní prostředí používají jako oddělovač ve výchozím nastavení středník ;. Při distribuci napříč regiony buď CSV pevně svažte s čárkou a uveďte to, nebo nabídněte verzi TSV (oddělenou tabulátory) – TSV je k národním prostředím tolerantnější.
5. Zpracování velkých souborů
CSV se stovkami tisíc řádků nenačítejte do paměti najednou:
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) {
// Zpracovávejte jeden záznam najednou; spotřeba paměti nezávisí na velikosti souboru
}
Směr exportu funguje stejně: generujte a průběžně zapisujte do proudu, místo abyste slepovali jeden obrovský řetězec.
6. Sebekontrola konzistence obousměrného převodu
Obousměrný převod CSV (JSON → CSV → JSON) téměř nikdy není přesně shodný, ale můžete definovat, které ztráty jsou přijatelné:
| Kontrola | Očekávání |
|---|---|
| Počet polí | Shodný |
| Hodnoty řetězců | Shodné (včetně úvodních nul) |
| Číselné typy | Určené definicí sloupce, nikoli odvozováním |
| Vnořené objekty | Obnovitelné přes tečkované cesty |
| Pole | Viditelná po serializaci do JSON řetězce |
| Prázdné hodnoty | Normalizované na null nebo prázdný řetězec – vyberte přesně jednu variantu |
Zařazení této tabulky do testů je mnohem účinnější než slovní dohoda.
7. Kontrolní seznam
- [ ] Sémantika „jeden řádek je jeden záznam“ je výslovně uvedena a promítnuta do názvu exportovaného souboru;
- [ ] Každý sloupec, který může ztratit přesnost, je exportován jako řetězec;
- [ ] Čínské CSV obsahuje BOM, nebo je kódování výslovně uvedeno;
- [ ] Pole obsahující čárky, uvozovky nebo zalomení řádku jsou správně escapována (použijte knihovnu, neskládejte řetězce ručně);
- [ ] Velké soubory procházejí streamovaným čtením a zápisem;
- [ ] Import převádí typy podle definice sloupce, nikdy globálním chytrým odvozováním.
Časté dotazy
Jak se rozhodnout mezi zploštěním, rozšířením řádků a rozdělením tabulek?
Zplošťujte pro exporty určené lidem: spojte vnořené klíče pomocí . a pole serializujte pomocí JSON.stringify do buňky, za cenu nemožnosti tato pole v tabulkovém nástroji filtrovat. Rozdělte data na mateřskou tabulku plus související tabulky, když je cílem databáze. Rozšiřte pole na další řádky, když příjemci agregují po prvcích, a smiřte se s duplikovanými poli rodiče.
Proč by import CSV neměl odvozovat typy automaticky?
RFC 4180 specifikuje oddělovače a escaping, ale žádné typy, takže každý krok obnovy je hádání: 00123 ztratí úvodní nuly, 1.10 se změní na 1.1 a 1e5 na 100000. Ve výchozím nastavení považujte vše za řetězec a nechte schéma nebo explicitní definici sloupce rozhodnout, které sloupce se stanou čísly nebo booleany.
Mám očekávat, že obousměrný převod CSV přesně reprodukuje původní JSON?
Téměř nikdy. Definujte místo toho přijatelné ztráty: shodný počet polí, shodné hodnoty řetězců včetně úvodních nul, číselné typy určené definicí sloupce, vnořené objekty obnovitelné přes tečkované cesty, pole viditelná po serializaci a prázdné hodnoty normalizované na null nebo prázdný řetězec – vyberte přesně jednu variantu.