JSON Toolkit

تبدیل JSON به CSV و داده جدولی و بازگشت

جمع‌بندی: CSV دوبعدی است، JSON درختی. بخش دشوار تبدیل کد نیست — این است که از پیش تصمیم بگیرید آرایه‌ها چگونه تخت شوند و نوع‌ها چگونه حفظ شوند.

مقدماتیحدود 10 دقیقه مطالعهانتشار 2026-10-30به‌روزرسانی 2026-10-30
تصویر جلد تبدیل JSON ↔ CSV و داده جدولی

جمع‌بندی: وقتی انسان‌ها خروجی را می‌خوانند با کلیدهای نقطه‌ای و آرایه‌های سریال‌شده JSON تخت کنید، هنگام بارگذاری در پایگاه‌داده به جدول والد به‌همراه جدول‌های مرتبط بشکنید و آرایه‌ها را فقط برای pivot به سطرهای اضافه بگسترانید؛ هنگام بازگردانی بر اساس تعریف صریح ستون‌ها تبدیل کنید نه استنتاج سراسری.

تخت کردن درخت در جدول همیشه سلسله‌مراتب را از دست می‌دهد؛ بازگردانی جدول به درخت همیشه نوع‌ها را حدس می‌زند. هر دو جهت نیاز دارند قواعدشان از پیش بیان شود.

۱. CSV واقعاً چه چیزی را مشخص می‌کند

RFC 4180 فقط چند صفحه است و کمتر از آنچه بسیاری تصور می‌کنند مشخص می‌کند:

۱. هر رکورد یک خط را اشغال می‌کند و فیلدها با ویرگول جدا می‌شوند؛ ۲. پایان‌دهنده خط CRLF است، هرچند در عمل \n هم پذیرفته می‌شود؛ ۳. فیلدی که ویرگول، گیومه دوتایی یا خط جدید داشته باشد باید در گیومه دوتایی پیچیده شود؛ ۴. گیومه‌های دوتایی درون فیلد به‌صورت دو گیومه دوتایی "" نوشته می‌شوند؛ ۵. خط اول می‌تواند سرستون باشد؛ ۶. هیچ رمزگذاری کاراکتری مشخص نشده و نوع‌ها هم مشخص نشده‌اند.

id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

به قاعده ۳ توجه کنید: فیلدهای دارای خط جدید قانونی‌اند، پس تجزیه‌کننده‌ای ساده‌لوح که نخست بر اساس \n و سپس , تقسیم می‌کند تضمیناً اشتباه است.

۲. JSON → CSV: نخست ساختار را تصمیم بگیرید

برای تخت کردن ساختار درختی در جدول دوبعدی فقط سه استراتژی وجود دارد:

استراتژی ۱: تخت کردن فیلدها (رایج‌ترین)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • کلیدهای object تودرتو را با . به هم بچسبانید؛
  • آرایه‌ها را نمی‌توان به‌صورت ستون بیان کرد، پس آن‌ها را با JSON.stringify مستقیم در سلول بگذارید (که هنوز فیلد CSV قانونی است)؛
  • عیب: ابزارهای صفحه‌گسترده نمی‌توانند آرایه را فیلتر کنند، پس تجزیه دوباره لازم است.

استراتژی ۲: گستردن آرایه در چند سطر

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

این برای روابط یک‌به‌چند مناسب است که مصرف‌کننده پایین‌دست بر اساس هر عنصر تجمیع می‌کند. عیبش فیلدهای والد تکراری و انبوه شدن تعداد سطرهاست.

استراتژی ۳: شکستن به چند جدول

یک جدول اصلی به‌همراه جدول‌های مرتبط که با یک id به هم می‌پیوندند — تمیزترین رویکرد، اما از «یک خروجی ساده» فراتر می‌رود و بیشتر شبیه مدل‌سازی داده است.

کدام یک؟ برای خروجی انسان‌محور استراتژی ۱، برای بارگذاری در پایگاه‌داده استراتژی ۳ و برای pivot داده استراتژی ۲ را استفاده کنید.

۳. CSV → JSON: نوع‌ها چگونه حدس زده می‌شوند

CSV نوع ندارد، پس هر مرحله بازگردانی یک حدس است:

محتوای سلول معنای ممکن خطر تبدیل مستقیم به عدد
00123 شناسه کارمند، کد پستی صفرهای ابتدایی از دست می‌روند → 123
1.10 شماره نسخه، مبلغ به 1.1 تبدیل می‌شود
1e5 شناسه یا نماد علمی به 100000 تبدیل می‌شود
2026-10-30 تاریخ یا رشته ابهام در منطقه زمانی و قالب
13800138000 شماره تلفن با ۱۱ رقم هنوز امن است، اما برخی شناسه‌ها دقت را از دست می‌دهند
"" رشته خالی یا null ابهام معنایی

پیشنهاد پیش‌فرض: همه چیز را رشته بدانید و بگذارید schema یا تعریف صریح ستون تعیین کند کدام ستون‌ها عدد یا بولین می‌شوند. «استنتاج هوشمند» هنگام خروجی لذت‌بخش است و هنگام ورود منبع حادثه.

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;
  });
}

۴. سه دام پیرامون Excel

۴.۱ رمزگذاری

وقتی Excel یک CSV یوتی‌اف‌هشت بدون BOM را باز می‌کند، متن چینی به mojibake تبدیل می‌شود. افزودن BOM (\uFEFF) هنگام خروجی ارزان‌ترین رفع سازگاری است:

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

۴.۲ عددهای بلند که به نماد علمی تبدیل می‌شوند

شناسه ملی ۱۸ رقمی یا شماره سفارش ۱۹ رقمی در Excel به‌صورت 1.23457E+17 نمایش داده می‌شود و خود مقدار هم از پیش بریده شده است. گذاشتن تب در ابتدای فیلد یا پیچیدن آن به‌صورت فرمول (="1234...") قالب متنی را حفظ می‌کند، اما سلول را به فرمول تبدیل می‌کند. رویکرد امن‌تر این است که به کاربران بگویید «از قابلیت import استفاده کنند و ستون را متن مشخص کنند»، یا در کنار خروجی یک .xlsx هم عرضه کنید.

۴.۳ جداکننده‌های متأثر از locale

برخی localeهای اروپایی به‌طور پیش‌فرض از نقطه‌ویرگول ; به‌عنوان جداکننده استفاده می‌کنند. هنگام توزیع میان مناطق، یا CSV را به ویرگول تثبیت کنید و همین را بگویید، یا نسخه TSV (جداشده با تب) عرضه کنید — TSV تحمل locale بیشتری دارد.

۵. مدیریت فایل‌های بزرگ

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) {
  // Process one record at a time; memory usage is independent of file size
}

جهت خروجی هم همین‌طور کار می‌کند: به‌تدریج تولید و در جریان بنویسید، نه اینکه یک رشته غول‌آسا به هم بچسبانید.

۶. خودآزمایی سازگاری رفت‌وبرگشت

رفت‌وبرگشت CSV (JSON → CSV → JSON) تقریباً هرگز کاملاً یکسان نیست، اما می‌توانید تعریف کنید کدام تلفات قابل قبول‌اند:

بررسی انتظار
تعداد فیلد یکسان
مقدارهای رشته‌ای یکسان (شامل صفرهای ابتدایی)
نوع‌های عددی با تعریف ستون تعیین می‌شود، نه استنتاج
objectهای تودرتو از مسیرهای نقطه‌ای بازیابی‌پذیر
آرایه‌ها پس از سریال‌سازی به رشته JSON قابل مشاهده
مقدارهای خالی به null یا رشته خالی نرمال می‌شوند — دقیقاً یکی را انتخاب کنید

گذاشتن این جدول در آزمون‌ها بسیار مؤثرتر از توافق شفاهی است.

۷. چک‌لیست

  • [ ] معنای «یک سطر یک رکورد» صریح بیان و در نام فایل خروجی منعکس شده است؛
  • [ ] هر ستونی که ممکن است دقت را از دست بدهد به‌صورت رشته صادر می‌شود؛
  • [ ] CSVهای چینی BOM دارند یا رمزگذاری صریح بیان شده است؛
  • [ ] فیلدهای دارای ویرگول، گیومه یا خط جدید درست escape می‌شوند (از کتابخانه استفاده کنید، خودتان رشته‌ها را به هم نچسبانید)؛
  • [ ] فایل‌های بزرگ با خواندن و نوشتن جریانی مدیریت می‌شوند؛
  • [ ] ورودها نوع را بر اساس تعریف ستون تبدیل می‌کنند، هرگز با استنتاج هوشمند سراسری.

پرسش‌های متداول

چگونه میان تخت‌سازی، گستردن سطرها و شکستن جدول انتخاب کنم؟

برای خروجی انسان‌محور تخت کنید: کلیدهای تودرتو را با . به هم بپیوندید و آرایه‌ها را با JSON.stringify در سلول بگذارید، به قیمت اینکه نتوانید آن آرایه‌ها را در صفحه‌گسترده فیلتر کنید. وقتی هدف پایگاه‌داده است به جدول والد به‌همراه جدول‌های مرتبط بشکنید. وقتی مصرف‌کننده پایین‌دست بر اساس هر عنصر تجمیع می‌کند آرایه‌ها را به سطرهای اضافه بگسترانید و فیلدهای والد تکراری را بپذیرید.

چرا ورود CSV نباید نوع‌ها را خودکار استنتاج کند؟

RFC 4180 جداکننده‌ها و escape را مشخص می‌کند اما نوع را نه، پس هر مرحله بازگردانی یک حدس است: 00123 صفرهای ابتدایی‌اش را از دست می‌دهد، 1.10 به 1.1 و 1e5 به 100000 تبدیل می‌شود. به‌طور پیش‌فرض همه چیز را رشته بدانید و بگذارید schema یا تعریف صریح ستون تعیین کند کدام ستون‌ها عدد یا بولین می‌شوند.

آیا باید انتظار داشته باشم رفت‌وبرگشت CSV عیناً JSON اصلی را بازتولید کند؟

تقریباً هرگز. در عوض تلفات قابل قبول را تعریف کنید: تعداد فیلد یکسان، مقدارهای رشته‌ای یکسان شامل صفرهای ابتدایی، نوع‌های عددی تعیین‌شده با تعریف ستون، objectهای تودرتو بازیابی‌پذیر از مسیرهای نقطه‌ای، آرایه‌ها قابل مشاهده پس از سریال‌سازی و مقدارهای خالی نرمال‌شده به null یا رشته خالی — دقیقاً یکی را انتخاب کنید.