JSON থেকে CSV ও ট্যাবুলার ডেটা এবং উল্টো রূপান্তর
সংক্ষেপে: CSV দ্বিমাত্রিক, JSON গাছের আকারের। রূপান্তরের কঠিন অংশ কোড নয় — আগে থেকেই ঠিক করা অ্যারে কীভাবে ফ্ল্যাট হবে আর টাইপ কীভাবে সংরক্ষিত থাকবে।

মূল কথা: মানুষ এক্সপোর্ট পড়বে হলে ডটেড কি ও JSON-স্ট্রিং করা অ্যারে দিয়ে ফ্ল্যাট করুন, ডেটাবেসে লোড করলে মূল টেবিল ও সংশ্লিষ্ট টেবিলে ভাগ করুন, আর পিভটের জন্য কেবল অ্যারেকে বাড়তি সারিতে ছড়িয়ে দিন; পুনরুদ্ধারের সময় স্পষ্ট কলাম-সংজ্ঞা দিয়ে রূপান্তর করুন, সর্বজনীন অনুমান দিয়ে নয়।
গাছকে টেবিলে ফ্ল্যাট করলে সবসময় শ্রেণিবিন্যাস হারায়; টেবিলকে গাছে ফেরালে সবসময় টাইপ অনুমান করতে হয়। দুই দিকেই নিয়ম আগে থেকেই বলা দরকার।
১. 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""]"
- নেস্টেড অবজেক্টের কি
.দিয়ে জোড়া দিন; - অ্যারেকে কলাম হিসেবে প্রকাশ করা যায় না, তাই
JSON.stringifyকরে সরাসরি সেলে বসান (এটি এখনো বৈধ CSV ফিল্ড); - অসুবিধা: স্প্রেডশিট টুল অ্যারে ফিল্টার করতে পারে না, তাই আবার পার্স করতে হয়।
কৌশল ২: অ্যারেকে একাধিক সারিতে ছড়ানো
id,user.name,tag
1,张三,a
1,张三,b
এটি এক-থেকে-অনেক সম্পর্কের জন্য উপযুক্ত, যেখানে ডাউনস্ট্রিম ব্যবহারকারী প্রতি উপাদানে অ্যাগ্রিগেট করে। অসুবিধা হলো প্যারেন্ট ফিল্ড পুনরাবৃত্তি আর সারির সংখ্যা ফুলে ওঠা।
কৌশল ৩: একাধিক টেবিলে ভাগ করা
একটি মূল টেবিল আর id দিয়ে জোড়া সংশ্লিষ্ট টেবিল — সবচেয়ে পরিষ্কার, তবে এটি "একটি এক্সপোর্ট" ছাড়িয়ে যায় এবং ডেটা মডেলিংয়ের মতো দেখায়।
কোনটি? মানুষের জন্য এক্সপোর্টে কৌশল ১, ডেটাবেসে লোড করতে কৌশল ৩, আর ডেটা পিভট করতে কৌশল ২।
৩. CSV → JSON: টাইপ কীভাবে অনুমান হয়
CSV-তে টাইপ নেই, তাই পুনরুদ্ধারের প্রতিটি ধাপই অনুমান:
| সেলের বিষয়বস্তু | সম্ভাব্য অর্থ | সরাসরি সংখ্যায় রূপান্তরের ঝুঁকি |
|---|---|---|
00123 |
কর্মী আইডি, পোস্টাল কোড | শুরুর শূন্য হারায় → 123 |
1.10 |
সংস্করণ নম্বর, পরিমাণ | 1.1 হয়ে যায় |
1e5 |
আইডেন্টিফায়ার বা সায়েন্টিফিক নোটেশন | 100000 হয়ে যায় |
2026-10-30 |
তারিখ বা স্ট্রিং | টাইম জোন ও ফরম্যাটে দ্ব্যর্থতা |
13800138000 |
ফোন নম্বর | ১১ অঙ্কে এখনো নিরাপদ, তবে কিছু ID নির্ভুলতা হারায় |
"" |
খালি স্ট্রিং বা null | অর্থগত দ্ব্যর্থতা |
প্রস্তাবিত ডিফল্ট: সবকিছু স্ট্রিং হিসেবে ধরুন, আর স্কিমা বা স্পষ্ট কলাম-সংজ্ঞা ঠিক করুক কোন কলাম সংখ্যা বা বুলিয়ান হবে। "স্মার্ট অনুমান" এক্সপোর্টের সময় দারুণ, কিন্তু ইমপোর্টের সময় ঘটনার উৎস।
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;
});
}
৪. Excel-সংক্রান্ত তিনটি ফাঁদ
৪.১ এনকোডিং
BOM ছাড়া UTF-8 CSV Excel-এ খুললে চীনা লেখা এলোমেলো হয়। এক্সপোর্টের সময় BOM (\uFEFF) যোগ করা সবচেয়ে সস্তা সামঞ্জস্য সমাধান:
const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;
৪.২ লম্বা সংখ্যা সায়েন্টিফিক নোটেশনে বদলে যাওয়া
১৮ অঙ্কের জাতীয় পরিচয়পত্র বা ১৯ অঙ্কের অর্ডার নম্বর Excel-এ 1.23457E+17 দেখায়, আর মানটি নিজেই ইতিমধ্যে কাটা পড়েছে।
ফিল্ডের আগে ট্যাব বসালে বা সূত্র হিসেবে মোড়ালে (="1234...") টেক্সট ফরম্যাট টিকে থাকে, তবে সেলটি সূত্র হয়ে যায়।
নিরাপদ উপায় হলো ব্যবহারকারীকে বলুন "ইমপোর্ট ফিচার ব্যবহার করে কলামটি টেক্সট হিসেবে নির্দিষ্ট করুন", বা এক্সপোর্টের সঙ্গে একটি .xlsx দিন।
৪.৩ লোকেল দ্বারা প্রভাবিত ডিলিমিটার
কিছু ইউরোপীয় লোকেল ডিফল্টভাবে সেমিকোলন ;-কে সেপারেটর ধরে। অঞ্চলভেদে বিতরণের সময় হয় CSV-কে কমায় স্থির করে তা বলে দিন, বা TSV (ট্যাব-সেপারেটেড) সংস্করণ দিন — TSV লোকেলের প্রতি বেশি সহনশীল।
৫. বড় ফাইল হ্যান্ডল করা
এক লক্ষাধিক সারির 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) {
// একবারে একটি রেকর্ড প্রসেস করুন; মেমোরি ব্যবহার ফাইলের আকারের উপর নির্ভর করে না
}
এক্সপোর্টের দিকেও একই নিয়ম: একটি বিশাল স্ট্রিং জোড়া না লাগিয়ে যেতে যেতে জেনারেট করে স্ট্রিমে লিখুন।
৬. রাউন্ড-ট্রিপ সামঞ্জস্যের স্ব-পরীক্ষা
CSV রাউন্ড ট্রিপ (JSON → CSV → JSON) প্রায় কখনো হুবহু এক হয় না, তবে কোন ক্ষতিগ্রস্ততা গ্রহণযোগ্য তা আপনি নির্ধারণ করতে পারেন:
| যাচাই | প্রত্যাশা |
|---|---|
| ফিল্ডের সংখ্যা | হুবহু এক |
| স্ট্রিং মান | হুবহু এক (শুরুর শূন্যসহ) |
| সাংখ্যিক টাইপ | কলাম-সংজ্ঞা ঠিক করে, অনুমান নয় |
| নেস্টেড অবজেক্ট | ডটেড পথে পুনরুদ্ধারযোগ্য |
| অ্যারে | JSON স্ট্রিংয়ে সিরিয়ালাইজ করার পর দৃশ্যমান |
| খালি মান | null বা খালি স্ট্রিংয়ে নরমালাইজ — ঠিক একটিই বেছে নিন |
এই টেবিলটি টেস্টে বসানো মৌখিক সম্মতির চেয়ে অনেক বেশি কার্যকর।
৭. চেকলিস্ট
- [ ] "এক সারি = এক রেকর্ড" অর্থ স্পষ্টভাবে লেখা এবং এক্সপোর্ট ফাইলের নামে প্রতিফলিত;
- [ ] নির্ভুলতা হারাতে পারে এমন প্রতিটি কলাম স্ট্রিং হিসেবে এক্সপোর্ট হয়;
- [ ] চীনা CSV-তে BOM আছে, বা এনকোডিং স্পষ্টভাবে উল্লেখ করা;
- [ ] কমা, কোট বা নিউলাইনযুক্ত ফিল্ড সঠিকভাবে এস্কেপ করা (লাইব্রেরি ব্যবহার করুন, নিজে স্ট্রিং জোড়া লাগাবেন না);
- [ ] বড় ফাইল স্ট্রিমিং পড়া ও লেখা দিয়ে যায়;
- [ ] ইমপোর্ট কলাম-সংজ্ঞা অনুযায়ী টাইপ রূপান্তর করে, কখনো সর্বজনীন স্মার্ট অনুমান দিয়ে নয়।
সাধারণ প্রশ্ন
ফ্ল্যাট করা, সারি ছড়ানো আর টেবিল ভাগ করার মধ্যে কীভাবে বেছে নেব?
মানুষের জন্য এক্সপোর্টে ফ্ল্যাট করুন: নেস্টেড কি . দিয়ে জোড়া দিন আর অ্যারে JSON.stringify করে সেলে বসান, তবে স্প্রেডশিটে ওই অ্যারেগুলো ফিল্টার করা যাবে না। লক্ষ্য ডেটাবেস হলে মূল টেবিল আর সংশ্লিষ্ট টেবিলে ভাগ করুন। ডাউনস্ট্রিম ব্যবহারকারী প্রতি উপাদানে অ্যাগ্রিগেট করলে অ্যারেকে বাড়তি সারিতে ছড়িয়ে দিন, প্যারেন্ট ফিল্ড পুনরাবৃত্তি মেনে নিয়ে।
CSV ইমপোর্টে টাইপ স্বয়ংক্রিয়ভাবে অনুমান করা উচিত নয় কেন?
RFC 4180 ডিলিমিটার ও এস্কেপিং নির্ধারণ করে কিন্তু টাইপ নয়, তাই পুনরুদ্ধারের প্রতিটি ধাপই অনুমান: 00123 শুরুর শূন্য হারায়, 1.10 হয় 1.1, আর 1e5 হয় 100000। সবকিছু ডিফল্টভাবে স্ট্রিং ধরুন আর স্কিমা বা স্পষ্ট কলাম-সংজ্ঞা ঠিক করুক কোন কলাম সংখ্যা বা বুলিয়ান হবে।
CSV রাউন্ড ট্রিপে মূল JSON হুবহু ফিরে আসবে আশা করা উচিত?
প্রায় কখনো নয়। বরং গ্রহণযোগ্য ক্ষতি নির্ধারণ করুন: ফিল্ডের সংখ্যা এক, শুরুর শূন্যসহ স্ট্রিং মান এক, সাংখ্যিক টাইপ কলাম-সংজ্ঞা ঠিক করে, নেস্টেড অবজেক্ট ডটেড পথে পুনরুদ্ধারযোগ্য, অ্যারে সিরিয়ালাইজেশনের পর দৃশ্যমান, আর খালি মান null বা খালি স্ট্রিংয়ে নরমালাইজ — ঠিক একটি বেছে নিন।