JSON Toolkit

המרת JSON ל-CSV ולנתונים טבלאיים ובחזרה

שורה תחתונה: CSV הוא דו-ממדי, JSON הוא בצורת עץ. החלק הקשה בהמרה אינו הקוד — אלא ההחלטה מראש כיצד המערכים יישטחו וכיצד הטיפוסים יישמרו.

מתחילכ-10 דקות קריאהפורסם 2026-10-30עודכן 2026-10-30
תמונה עבור המרת JSON ↔ CSV ונתונים טבלאיים

שורה תחתונה: שטחו עם מפתחות נקודתיים ומערכים שעברו JSON.stringify כשבני אדם יקראו את הייצוא, פצלו לטבלת אב בתוספת טבלאות קשורות כשטוענים למסד נתונים, והרחיבו מערכים לשורות נוספות רק בשביל pivots; בשחזור, המירו לפי הגדרות עמודה מפורשות במקום הסקה גלובלית.

שטיחת עץ לטבלה תמיד מאבדת היררכיה; שחזור טבלה לעץ תמיד מנחש טיפוסים. שני הכיוונים צריכים שהכללים שלהם יוצהרו מראש.

1. מה CSV באמת מגדיר

RFC 4180 אורך רק עמודים בודדים ומגדיר פחות ממה שאנשים רבים מניחים:

  1. כל רשומה תופסת שורה אחת, כשהשדות מופרדים בפסיקים;
  2. מפריד השורה הוא CRLF, אם כי בפועל \n מתקבל גם הוא;
  3. שדה המכיל פסיק, גרש כפול או שורה חדשה חייב להיות עטוף בגרשיים כפולים;
  4. גרשיים כפולים בתוך שדה נכתבים כשני גרשיים כפולים "";
  5. השורה הראשונה רשאית להיות כותרת;
  6. לא מצוין קידוד תווים, וגם לא טיפוסים.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

שימו לב לכלל 3: שדות המכילים שורות חדשות הם חוקיים, ולכן מפענח נאיבי שמפצל לפי \n ואז לפי , מובטח להיות שגוי.

2. JSON → CSV: החליטו קודם על המבנה

יש רק שלוש אסטרטגיות לשטיחת מבנה עץ לטבלה דו-ממדית:

אסטרטגיה 1: שטיחת שדות (הנפוצה ביותר)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • חברו מפתחות של אובייקטים מקוננים עם .;
  • מערכים אינם ניתנים לביטוי כעמודות, ולכן בצעו להם JSON.stringify ישר לתוך התא (שעדיין מהווה שדה CSV חוקי);
  • חסרון: כלי גיליון אלקטרוני אינם יכולים לסנן את המערך, ולכן נדרש פענוח שני.

אסטרטגיה 2: הרחבת המערך למספר שורות

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

זה מתאים ליחסי אחד-לרבים שבהם צרכנים במורד הזרם מקבצים לפי איבר. החסרון הוא שדות אב מוכפלים ומספר שורות שמתנפח.

אסטרטגיה 3: פיצול למספר טבלאות

טבלת ראשית בתוספת טבלאות קשורות המחוברות באמצעות id — הגישה הנקייה ביותר, אבל היא חורגת מ"ייצוא יחיד" ונראית יותר כמו מידול נתונים.

איזו משלושתן? השתמשו באסטרטגיה 1 לייצוא המיועד לבני אדם, באסטרטגיה 3 לטעינה למסד נתונים, ובאסטרטגיה 2 ליצירת pivots מנתונים.

3. CSV → JSON: איך מנחשים טיפוסים

ל-CSV אין טיפוסים, ולכן כל שלב בשחזור הוא ניחוש:

תוכן התא משמעות אפשרית סיכון בהמרה ישירה למספר
00123 מזהה עובד, מיקוד אפסים מובילים אובדים → 123
1.10 מספר גרסה, סכום הופך ל-1.1
1e5 מזהה או סימון מדעי הופך ל-100000
2026-10-30 תאריך או מחרוזת עמימות באזור זמן ובפורמט
13800138000 מספר טלפון עדיין בטוח ב-11 ספרות, אבל מזהים מסוימים כן מאבדים דיוק
"" מחרוזת ריקה או null עמימות סמנטית

ברירת מחדל מוצעת: התייחסו הכול כאל מחרוזת, ותנו ל-schema או להגדרת עמודה מפורשת להחליט אילו עמודות יהפכו למספרים או לבוליאניים. "הסקה חכמה" היא תענוג בייצוא ומקור לאירועים בייבוא.

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

/**
 * ממיר שורות CSV לאובייקטים לפי הגדרות העמודות.
 * @param rows מערך השורות הגולמי; השורה הראשונה היא הכותרת
 * @param columnTypes מיפוי משם עמודה לטיפוס
 * @returns מערך האובייקטים שהומר
 */
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. שלוש מלכודות סביב Excel

4.1 קידוד

כש-Excel פותח CSV ב-UTF-8 ללא BOM, טקסט סיני הופך למתועתק. הוספת BOM (\uFEFF) בייצוא היא תיקון התאימות הזול ביותר:

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

4.2 מספרים ארוכים הופכים לסימון מדעי

תעודת זהות בת 18 ספרות או מספר הזמנה בן 19 ספרות מוצגים ב-Excel כ-1.23457E+17, והערך עצמו כבר נחתך. הוספת טאב בתחילת השדה או עטיפתו כנוסחה (="1234...") משמרות עיצוב טקסט, אבל הופכות את התא לנוסחה. הגישה הבטוחה יותר היא לומר למשתמשים "השתמשו ביכולת הייבוא והגדירו את העמודה כטקסט", או להציע קובץ .xlsx לצד הייצוא.

4.3 מפרידים שמושפעים מהאזור

באזורים אירופיים מסוימים ברירת המחדל היא פסיק-נקודה ; כמפריד. כשמפיצים בין אזורים, או הצמידו את ה-CSV לפסיקים ואמרו זאת, או הציעו גרסת TSV (מופרדת בטאבים) — TSV סובלנית יותר לאזורים.

5. טיפול בקבצים גדולים

אל תטענו קובצי CSV עם יותר ממאה אלף שורות לזיכרון בבת אחת:

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) {
  // עיבוד רשומה אחת בכל פעם; השימוש בזיכרון אינו תלוי בגודל הקובץ
}

כיוון הייצוא עובד באותו אופן: צרו וכתבו לזרם תוך כדי, במקום לחבר מחרוזת ענקית אחת.

6. בדיקת עקביות עצמית של סבב הלוך ושוב

סבב CSV (JSON → CSV → JSON) כמעט לעולם אינו זהה לחלוטין, אבל אתם יכולים להגדיר אילו אובדנים מקובלים:

בדיקה ציפייה
מספר שדות זהה
ערכי מחרוזת זהים (כולל אפסים מובילים)
טיפוסים מספריים נקבעים לפי הגדרת העמודה, לא לפי הסקה
אובייקטים מקוננים ניתנים לשחזור דרך נתיבים נקודתיים
מערכים גלויים אחרי סריאליזציה למחרוזת JSON
ערכים ריקים מנורמלים ל-null או למחרוזת הריקה — בחרו בדיוק באחד

הכנסת הטבלה הזו לבדיקות יעילה הרבה יותר מהסכמה בעל-פה.

7. רשימת בדיקה

  • [ ] סמנטיקת "שורה אחת היא רשומה אחת" מוצהרת במפורש ומשתקפת בשם קובץ הייצוא;
  • [ ] כל עמודה שעלולה לאבד דיוק מיוצאת כמחרוזת;
  • [ ] קובצי CSV בסינית כוללים BOM, או שהקידוד מוצהר במפורש;
  • [ ] שדות המכילים פסיקים, גרשיים או שורות חדשות עוברים escape נכון (השתמשו בספרייה, אל תחברו מחרוזות בעצמכם);
  • [ ] קבצים גדולים עוברים קריאה וכתיבה בזרימה;
  • [ ] ייבוא ממיר טיפוסים לפי הגדרת עמודה, לעולם לא דרך הסקה חכמה גלובלית.

שאלות נפוצות

איך בוחרים בין שטיחה, הרחבת שורות ופיצול טבלאות?

שטחו בייצוא המיועד לבני אדם: חברו מפתחות מקוננים עם . ובצעו JSON.stringify למערכים בתוך התא, במחיר של חוסר יכולת לסנן את המערכים בגיליון. פצלו לטבלת אב בתוספת טבלאות קשורות כשהמטרה היא מסד נתונים. הרחיבו מערכים לשורות נוספות כשצרכנים במורד הזרם מקבצים לפי איבר, וקבלו שדות אב מוכפלים.

למה ייבוא CSV לא צריך להסיק טיפוסים אוטומטית?

RFC 4180 מגדיר מפרידים ו-escape אבל לא טיפוסים, ולכן כל שלב שחזור הוא ניחוש: 00123 מאבד את האפסים המובילים, 1.10 הופך ל-1.1, ו-1e5 הופך ל-100000. התייחסו הכול כאל מחרוזת כברירת מחדל ותנו ל-schema או להגדרת עמודה מפורשת להחליט אילו עמודות יהפכו למספרים או לבוליאניים.

האם לצפות שסבב CSV ישחזר את ה-JSON המקורי בדיוק?

כמעט לעולם לא. הגדירו במקום זאת את האובדנים המקובלים: מספר שדות זהה, ערכי מחרוזת זהים כולל אפסים מובילים, טיפוסים מספריים שנקבעים לפי הגדרת העמודה, אובייקטים מקוננים ניתנים לשחזור דרך נתיבים נקודתיים, מערכים גלויים אחרי סריאליזציה, וערכים ריקים מנורמלים ל-null או למחרוזת הריקה — בחרו בדיוק באחד.