JSON Toolkit

การแปลง JSON เป็นและจาก CSV และข้อมูลแบบตาราง

สรุปสำคัญ: CSV เป็นสองมิติ ส่วน JSON เป็นรูปต้นไม้ ส่วนที่ยากของการแปลงไม่ใช่โค้ด — แต่คือการตัดสินล่วงหน้าว่า array จะถูกแบนอย่างไรและชนิดข้อมูลจะถูกรักษาไว้อย่างไร

ผู้เริ่มต้นอ่านประมาณ 10 นาทีเผยแพร่ 2026-10-30อัปเดต 2026-10-30
ภาพหน้าปกการแปลง JSON ↔ CSV และข้อมูลแบบตาราง

สรุปสั้น: แบนด้วยคีย์แบบจุดและ array ที่แปลงเป็นสตริง JSON เมื่อคนจะอ่านไฟล์ส่งออก แยกเป็นตารางหลักพร้อมตารางที่เกี่ยวข้องเมื่อโหลดเข้าฐานข้อมูล และขยาย array เป็นแถวเพิ่มเฉพาะเมื่อทำ pivot; ตอนกู้คืน ให้แปลงตามนิยามคอลัมน์ที่ชัดเจนแทนการอนุมานทั้งชุด

การแบนต้นไม้เป็นตารางย่อมสูญเสียลำดับชั้น ส่วนการคืนตารางเป็นต้นไม้ย่อมเดาชนิดข้อมูล ทั้งสองทิศทางต้องประกาศกฎล่วงหน้า

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""]"
  • เชื่อมคีย์ของ object ที่ซ้อนด้วย .;
  • array แสดงเป็นคอลัมน์ไม่ได้ จึงใช้ JSON.stringify ลงในเซลล์โดยตรง (ซึ่งยังเป็นฟิลด์ CSV ที่ถูกกฎหมาย);
  • ข้อเสีย: เครื่องมือสเปรดชีตกรอง array ไม่ได้ ต้องแยกวิเคราะห์ซ้ำอีกครั้ง

กลยุทธ์ 2: ขยาย array เป็นหลายแถว

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

เหมาะกับความสัมพันธ์แบบหนึ่งต่อหลาย ซึ่งผู้บริโภคปลายน้ำรวมข้อมูลรายองค์ประกอบ ข้อเสียคือฟิลด์ของแถวหลักซ้ำกันและจำนวนแถวพองตัว

กลยุทธ์ 3: แยกเป็นหลายตาราง

ตารางหลักหนึ่งตารางพร้อมตารางที่เกี่ยวข้องเชื่อมด้วย id — วิธีสะอาดที่สุด แต่เกินขอบเขต "การส่งออกชิ้นเดียว" และดูเหมือนการออกแบบโมเดลข้อมูลมากกว่า

เลือกแบบไหน? ใช้กลยุทธ์ 1 สำหรับการส่งออกให้มนุษย์อ่าน กลยุทธ์ 3 สำหรับโหลดเข้าฐานข้อมูล และกลยุทธ์ 2 สำหรับทำ pivot ข้อมูล

3. CSV → JSON: ชนิดข้อมูลถูกรอนุมานอย่างไร

CSV ไม่มีชนิดข้อมูล ทุกขั้นตอนของการกู้คืนจึงเป็นการเดา:

เนื้อหาเซลล์ ความหมายที่เป็นไปได้ ความเสี่ยงเมื่อแปลงเป็นตัวเลขตรง ๆ
00123 รหัสพนักงาน รหัสไปรษณีย์ ศูนย์นำหน้าหาย → 123
1.10 หมายเลขเวอร์ชัน จำนวนเงิน กลายเป็น 1.1
1e5 ตัวระบุหรือสัญกรณ์วิทยาศาสตร์ กลายเป็น 100000
2026-10-30 วันที่หรือสตริง กำกวมเรื่องเขตเวลาและรูปแบบ
13800138000 หมายเลขโทรศัพท์ 11 หลักยังปลอดภัย แต่ ID บางแบบก็สูญเสียความแม่นยำ
"" สตริงว่างหรือ 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;
  });
}

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 ตัวคั่นที่ได้รับผลจาก locale

locale ยุโรปบางแห่งใช้เซมิโคลอน ; เป็นตัวคั่นโดยค่าเริ่มต้น เมื่อแจกจ่ายข้ามภูมิภาค ให้ตรึง CSV ให้ใช้เครื่องหมายจุลภาคและบอกให้ชัด หรือให้เวอร์ชัน TSV (คั่นด้วยแท็บ) เพิ่ม — TSV ทนต่อ locale มากกว่า

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

ทิศทางส่งออกก็เช่นกัน: สร้างและเขียนลงสตรีมไปเรื่อย ๆ แทนการต่อเป็นสตริงยักษ์ก้อนเดียว

6. ตรวจความสอดคล้องของการไป-กลับด้วยตัวเอง

การไป-กลับของ CSV (JSON → CSV → JSON) แทบไม่เคยเหมือนเดิมทุกประการ แต่คุณกำหนดได้ว่าความสูญเสียแบบไหนยอมรับได้:

สิ่งที่ตรวจ สิ่งที่คาดหวัง
จำนวนฟิลด์ เหมือนกัน
ค่าสตริง เหมือนกัน (รวมศูนย์นำหน้า)
ชนิดข้อมูลตัวเลข กำหนดด้วยนิยามคอลัมน์ ไม่ใช่การอนุมาน
Object ที่ซ้อน กู้คืนได้ผ่านเส้นทางแบบจุด
Array มองเห็นได้หลังแปลงเป็นสตริง JSON
ค่าว่าง ทำให้เป็น null หรือสตริงว่าง — เลือกอย่างเดียวเท่านั้น

การใส่ตารางนี้ลงในเทสต์ได้ผลกว่าการตกลงด้วยวาจามาก

7. รายการตรวจสอบ

  • [ ] ความหมาย "หนึ่งแถวคือหนึ่งเรกคอร์ด" ถูกระบุชัดเจนและสะท้อนในชื่อไฟล์ส่งออก;
  • [ ] ทุกคอลัมน์ที่อาจสูญเสียความแม่นยำถูกส่งออกเป็นสตริง;
  • [ ] CSV ภาษาจีนมี BOM หรือระบุการเข้ารหัสไว้ชัดเจน;
  • [ ] ฟิลด์ที่มีเครื่องหมายจุลภาค เครื่องหมายคำพูด หรือการขึ้นบรรทัดใหม่ถูก escape ถูกต้อง (ใช้ไลบรารี อย่าต่อสตริงเอง);
  • [ ] ไฟล์ใหญ่ผ่านการอ่านและเขียนแบบสตรีมมิ่ง;
  • [ ] การนำเข้าแปลงชนิดข้อมูลตามนิยามคอลัมน์ ไม่ใช่การอนุมานอัจฉริยะทั้งชุด

คำถามที่พบบ่อย

จะเลือกระหว่างการแบน การขยายแถว และการแยกตารางอย่างไร

แบนสำหรับการส่งออกให้มนุษย์อ่าน: เชื่อมคีย์ที่ซ้อนด้วย . และ JSON.stringify array ลงในเซลล์ โดยยอมแลกกับการกรอง array เหล่านั้นในสเปรดชีตไม่ได้ แยกเป็นตารางหลักพร้อมตารางที่เกี่ยวข้องเมื่อปลายทางเป็นฐานข้อมูล ขยาย array เป็นแถวเพิ่มเมื่อผู้บริโภคปลายน้ำรวมข้อมูลรายองค์ประกอบ โดยยอมรับฟิลด์ของแถวหลักซ้ำกัน

ทำไมการนำเข้า CSV ไม่ควรอนุมานชนิดข้อมูลอัตโนมัติ

RFC 4180 กำหนดตัวคั่นและการ escape แต่ไม่กำหนดชนิดข้อมูล ทุกขั้นตอนกู้คืนจึงเป็นการเดา: 00123 ทำศูนย์นำหน้าหาย, 1.10 กลายเป็น 1.1 และ 1e5 กลายเป็น 100000 ให้ถือทุกอย่างเป็นสตริงโดยค่าเริ่มต้น แล้วให้ schema หรือนิยามคอลัมน์ที่ชัดเจนตัดสินว่าคอลัมน์ใดเป็นตัวเลขหรือบูลีน

ควรคาดหวังให้การไป-กลับ CSV ได้ JSON เดิมเป๊ะหรือไม่

แทบไม่เคย ให้กำหนดความสูญเสียที่ยอมรับได้แทน: จำนวนฟิลด์เท่ากัน ค่าสตริงเท่ากันรวมศูนย์นำหน้า ชนิดข้อมูลตัวเลขกำหนดด้วยนิยามคอลัมน์ object ที่ซ้อนกู้คืนได้ผ่านเส้นทางแบบจุด array มองเห็นได้หลังแปลงเป็นสตริง และค่าว่างถูกทำให้เป็น null หรือสตริงว่าง — เลือกอย่างเดียวเท่านั้น