JSON به CSV: سرصفحه از کجا میآید و چرا مقادیر تغییر میکنند
جمعبندی: نه آزمایش روی یک مبدل واقعی: راهبرد «کلیدهای رکورد اول» بیسروصدا فیلد گم میکند، null و غیبت و رشتهٔ خالی همه به خانههای خالی تبدیل میشوند، عدد صحیح ۲۰ رقمی همانجا در JSON.parse دقتش را از دست میدهد، و نقطه در یک کلید دو مسیر متفاوت را در یک ستون به برخورد میاندازد. سرصفحه چگونه ساخته میشود، کدام مقادیر تغییر شکل مییابند و چگونه بسته به مصرف پاییندستی راهبرد انتخاب کنید.

وقتی تبدیل JSON به CSV داده از دست میدهد، تقریباً هرگز در مرحلهٔ «تبدیل» رخ نمیدهد. از دست رفتن در دو پرسشی است که هیچکس نپرسید: چه کسی سرصفحه را تعیین کرد، و آیا هر خانه همچنان بهعنوان همان مقدار قبلیاش قابل تشخیص است؟
یک فیلد در سی ثانیه ناپدید میشود
خیلی از مبدلها — از جمله خط لولهٔ معمول jq — سرصفحه را فقط از رکورد اول میگیرند. پیامدها را در پایتون بازسازی کنید:
data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys()) # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# ایمیل رکورد دوم بدون خطا یا هشدار ناپدید میشود
کلید email در سرصفحه نیست: حتی حق تبدیلشدن به یک خانهٔ خالی را هم ندارد — ستون هرگز ساخته نشده و مقدار بیسروصدا دور انداخته میشود. اصلاح آن اتحاد کلیدها است: همهٔ رکوردها را بپیمایید و برای هر کلیدی که جایی ظاهر میشود یک ستون بسازید. همان دادهها از اتحاد میگذرند و سرصفحهٔ id,name/email میسازند، با خانهٔ خالی جایی که name غایب است — و هیچچیز از دست نمیرود.
مبدل مستقیم صفحهٔ /convert/csv ما دقیقاً همین راهبرد را بهکار میبرد. با ورودی [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}] این خروجی را میدهد:
id,name,email
1,A,
2,,b@x.com
کلیدها به ترتیب نخستین ظهور مرتب میشوند و فیلدهای غایب به خانههای خالی تبدیل میشوند. jsonoperations و jsonbeam هر دو این را «اصلاح باگ کلاسیک» مینامند — اما «از اتحاد استفاده میکند» فقط نیمی از ماجراست. اتحاد بهای خودش را دارد؛ آن بخش بعدی است.
سه راه ساخت سرصفحه — و اینکه هر یک برای چه کسی مناسب است
| راهبرد سرصفحه | چگونه ساخته میشود | نیرو | بهای آن |
|---|---|---|---|
| کلیدهای رکورد اول | فقط رکورد اول خوانده میشود | ستون کم، پایدار، یک خط jq | فیلدهای تازهٔ رکوردهای بعدی بیسروصدا گم میشوند |
| اتحاد کلیدها | همهٔ رکوردها خوانده میشوند | هیچ فیلدی از دست نمیرود | ستونهای پراکنده و خانههای خالی فراوان وقتی شکلها متفاوتاند |
| فهرست صریح ستونها | دستی یا از روی اسکیمای تعریفشده | کاملاً کنترلپذیر، بازتولیدپذیر | نگهداری میخواهد؛ فیلدهای تازه به بهروزرسانی دستی نیاز دارند |
معیار انتخاب «کدامیک پیچیدهتر است» نیست، بلکه این است که مصرفکننده با این CSV چه میکند:
- همکاری که فقط نگاهی میاندازد: کلیدهای رکورد اول کافی است؛ شکل رکوردها معمولاً یکنواخت است.
- گزارش یا خط لولهٔ داده: اتحاد کلیدها حداقل شرط است — خانهٔ خالی خودش اطلاعات است.
- اکسپورت خودکار دورهای: فهرست صریح ستونها. رانش شکل (فیلد تازه از بالادست، تغییر نام کلید) باید بلند و آشکار شکست بخورد، نه اینکه بیسروصدا جدولی تولید کند که کسی متوجه نادرستیاش نشود.
گزینهٔ سوم بیش از همه مغفول مانده. در اتوماسیون «تبدیل موفق بود» و «تبدیل درست است» دو گزارهٔ متفاوتاند — و فهرست صریح ستونها تنها سازوکاری است که این دو را جدا میکند.
وقتی کلید نقطه دارد، مسیرهای تختسازی برخورد میکنند
تختسازی با نقطهگذاری {"a": {"b": 2}} را به ستون a.b تبدیل میکند. اما اگر کلید خودش نقطه داشته باشد؟ با pandas آزمودهایم:
import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# فقط یک ستون باقی میماند: ['a.b'] — دو کلید کاملاً متفاوت روی یک مسیر برخورد میکنند
کلید تحتاللفظی a.b و مسیر تودرتو a → b نام ستون یکسانی میسازند، یکی دیگری را بازنویسی میکند و داده بیسروصدا خراب میشود. مثال ساختهشده است، اما با دادههای واقعی هم پیش میآید: نام متریکهای مانیتورینگ (cpu.usage) و فیلدهای لاگ (error.type) عاشق نقطهاند.
دو درمان:
- جداکننده را عوض کنید.
sep="_"یا جداکنندهٔ دلخواه — البته پس از بررسی اینکه کلیدهای اصلی هرگز آن را نداشته باشند. - نشانگذاری مسیر را عوض کنید. JSON Pointer از
/بهعنوان جداکننده استفاده میکند (/a/b)؛ مبدل اکسل toolvore همین راه را میرود. او هم همان طبقهٔ ابهام را دارد — کلید با/باید~1نوشته شود — اما قواعد گریز در RFC 6901 استاندارد شدهاند، نه اینکه هر ابزار برای خودش اختراع کند.
هر چه انتخاب میکنید، قانون را در مستندات اکسپورت بنویسید. سرصفحهٔ CSV تنها «اسکیمایی» است که این دادهها با خود میبرند؛ خواننده قواعد تختسازی شما را هیچجای دیگر نمییابد.
آرایهها: سه راهبرد، هر یک چیز متفاوتی از دست میدهد
آرایهها موانع اصلی JSON به CSV هستند: یک خانه نمیتواند چند مقدار داشته باشد. سه راهبرد با زیانهای آزمودهشده در عمل:
| راهبرد | شکل خروجی | چه چیزی از دست میرود | مناسب برای |
|---|---|---|---|
| JSON در خانه | آرایه بهصورت رشتهٔ JSON داخل خانه | ابزارهای جدولی نمیتوانند عناصر را فیلتر یا تجمیع کنند | اول وفاداری، بازبینی دستی |
| ستونهای نمایهای | tags.0, tags.1, هر یک یک ستون |
تعداد ستونها با بلندترین آرایه منفجر میشود؛ خانههای خالی فراوان | آرایههای کوچک با طول ثابت |
| ردیفهای منفجرشده | یک ردیف به ازای هر عنصر، فیلدهای والد تکرار میشوند | تورم ردیف؛ رفتار آرایههای خالی باید جداگانه تعیین شود | تحلیل جزئیات، تجمیع بهازای هر عنصر |
مبدلهای /convert/csv و /convert/table ما JSON در خانه را بهکار میبرند. ورودی [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] این را میدهد:
orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"
وقتی ردیفهای منفجرشده لازم دارید، record_path پانداس مستقیماً انجامش میدهد:
orders = [{"orderId": "A001", "total": 99,
"items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
meta=["orderId", "total"], sep=".")
# دو ردیف جزئیات؛ orderId و total در هر دو تکرار شدهاند
در انفجار سه تله هست که باید پیشاپیش دربارهشان تصمیم بگیرید: منفجرکردن چند آرایه با هم ضرب دکارتی میسازد؛ آرایههای خالی بهطور پیشفرض هیچ ردیفی نمیدهند (فهرست مشتریانتان آدمها را گم میکند)؛ و فیلدهای والد تکرارشده هنگام تجمیع بر پایهٔ orderId در پاییندستی حذف تکرار میخواهند. راهنمای convertcsvonline «اول سطح جزئیات را تعیین کنید» را دقیقاً به همین دلیل گام اول گذاشته — وقتی سطح جزئیات ثابت شد، هر سه پرسش یک پاسخ دارند.
آزمودهشده: مقادیری که تغییر یافته از تبدیل بیرون میآیند
آزمون وفاداری روی مبدل واقعی ما (Node v22.13.1 + papaparse، همان پیادهسازیِ /convert/csv):
۱. null و غیبت و رشتهٔ خالی همه به خانههای خالی تبدیل میشوند. ورودی [{"a":null},{"b":""},{"c":1}] — هر سه خانه خالی بیرون میآیند. CSV سه حالت ندارد، فقط «نویسه هست» و «نویسه نیست». پس از یک رفتوبرگشت هر سه "" خوانده میشوند؛ معنای اصلی بازیافتناپذیر است.
۲. آبجکتها و آرایههای تودرتو رشتهٔ JSON میشوند. کوتیشنها طبق RFC 4180 دوبرابر میشوند ({""name"":""张三""}) و با چسباندن خانه به هر پارسر JSONای، مقدار برمیگردد — به همین دلیل JSON در خانه وفادار است.
۳. عدد صحیح ۲۰ رقمی همانجا در پارس دقت را از دست میدهد — و تقصیر CSV نیست. ورودی "id": 9007199254740993؛ در CSV از قبل 9007199254740992 آمده. JSON.parse در جاوااسکریپت از double بر مبنای IEEE 754 استفاده میکند: از دست رفتن هنگام پارس است، پیش از هر مبدل CSVای. json.load پایتون آن را کامل نگه میدارد. فیلدهای شناسهای (شناسهٔ اسنوفلیک، شماره سفارش) باید از سمت تولیدکننده بهصورت رشته بیایند. بیشتر دربارهٔ دقت عددی در چرا اعداد بزرگ دقتشان را از دست میدهند.
۴. صفرهای آغازین رشتهها در لایهٔ متنی CSV ایمناند. "phone": "00123" در CSV همان 00123 میماند، چون از آغاز تا پایان رشته است و چیزی به آن خط نمیزند. صفرها را گام بعدی میخراشد، جایی که اکسل خانه را عدد تلقی میکند — ببینید چهار راه تبدیل JSON به اکسل.
۵. فیلدهای دارای ویرگول، کوتیشن یا خط جدید. papaparse تابع RFC 4180 است: خانه در کوتیشن، کوتیشنهای داخلی دوبرابر. "He said \"hi\", ok" بهصورت "He said ""hi"", ok" بیرون میآید و رفتوبرگشت را بیآسیب میگذراند. سریالایزرهای دستساز که از کتابخانه میانبر میزنند تقریباً همیشه دقیقاً همینجا شکست میخورند.
رفتوبرگشت: کلید استنتاج چه تغییر میدهد
CSV → JSON باید برای هر خانه نوعی را حدس بزند. مبدل ما استنتاج نوع را بهطور پیشفرض خاموش میکند و همه را رشته برمیگرداند:
[{ "id": "9007199254740992", "phone": "00123" }]
با فعالبودن typeInference، id عدد میشود (دقت از پیش از دست رفته) در حالی که phone رشته میماند — قواعد استنتاج شکلهای با صفر آغازین مثل 010 را که اعداد معتبر نیستند مستثنا میکنند، پس رشته بهشانسی زنده میماند.
این عدم تقارن را به خاطر بسپارید: استنتاج خاموش در بدترین حالت نوع از شما میگیرد؛ استنتاج روشن ممکن است داده بگیرد. ستونهایی که به عدد نیاز دارند باید با تعاریف صریح ستون تبدیل شوند، نه به استنتاج سراسری سپرده شوند. پیادهسازی rowsToRecords در مقالهٔ پیشین ما JSON، CSV و دادههای جدولی دقیقاً همین اصل در قالب کد است.
انتخاب راهبرد بر اساس مصرفکننده: جدول تصمیم
| چه کسی CSV را میگیرد | راهبرد سرصفحه | نحوهٔ برخورد با آرایه | انواع |
|---|---|---|---|
| همکاری که یکبار بازش میکند | اتحاد کلیدها | JSON در خانه | بدون استنتاج |
| اکسل / گزارشسازی | اتحاد کلیدها | JSON در خانه، یا ستونهای نمایهای در طول ثابت | ستونهای شناسه اجباراً متن |
| درونریزی به پایگاه داده | فهرست صریح ستونها | جدولهای جداگانهٔ master/detail، یا انفجار | تعاریف صریح ستون |
| BI / تجمیع | فهرست صریح ستونها | ردیفهای منفجرشده، سطح جزئیات = کوچکترین واحد تحلیل | تعاریف صریح ستون |
| خط لولهٔ خودکار | فهرست صریح ستونها | JSON در خانه، انفجار در پاییندستی | استنتاج خاموش، تبدیل ستونبهستون |
قانون مشترک: هرچه نزدیکتر به انسان، بیشتر میتوان سهل گرفت؛ هرچه نزدیکتر به ماشین، بیشتر باید همهچیز صریح باشد.
چکلیستی برای کپیبرداری
- [ ] سرصفحه از اتحاد کامل کلیدها میآید یا فهرست صریح؟ اگر نه، میتوانید ثابت کنید هیچ رکوردی فیلد از دست نداده؟
- [ ] کلیدها میتوانند نقطه یا اسلش داشته باشند؟ برخورد با جداکنندهٔ تختسازی بررسی شده؟
- [ ] راهبرد آرایه مستند شده؟ رفتار آرایههای خالی تعیین شده؟
- [ ] برای
null، غیبت و رشتهٔ خالی: مصرفکننده برای هر یک چه میخواند؟ یک قاعده انتخاب و تثبیت کنید. - [ ] ستونهای شناسهای (شناسهها، شماره سفارش، تلفن) سر تا سر بهصورت رشته سفر میکنند؟
- [ ] پس از انفجار، تعداد ردیفها برابر جمع طول آرایههاست؟ بهصورت نمونهای بررسی شده؟
- [ ] فیلدهای نمونه با ویرگول، کوتیشن و خط جدید رفتوبرگشت را بیآسیط طی میکنند؟
- [ ] استنتاج سراسری نوع در بازدرونریزی خاموش است، با تعاریف صریح برای ستونهای عددی؟
فقط وقتی هر هشت پاسخ روشن دارند، این CSV «درست» است — نه صرفاً «با موفقیت تبدیلشده».
FAQ
چرا مبدل بهطور پیشفرض آبجکتهای تودرتو را به ستون تخت نمیکند؟
تختسازی تعداد ستونها را از کنترل خارج میکند: تودرتویی عمیق نامهایی مثل user.address.geo.lat میسازد و در ترکیب با اتحاد کلیدها، یک رکورد با شکل متفاوت کافی است تا انبوهی ستون اضافه شود. رشتههای JSON در خانهها وفاداری و تعداد ستون پایدار را نگه میدارند؛ هر وقت لازم شد، صریحاً تخت کنید.
اگر سرصفحه نام ستون تکراری داشته باشد چه باید؟
برخورد دو کلید متفاوت پس از تختسازی (مثل a.b در برابر a→b تودرتو) باید در بالادست حل شود: جداکننده را عوض کنید یا به JSON Pointer بروید. برای تکرار درون یک آبجکت، مبدلها معمولاً آخرین مقدار را نگه میدارند؛ تکرار در سرصفحه به پیادهسازی بستگی دارد — unparse پاپاپارس حذف تکرار نمیکند و اکسل هنگام بازکردن تکرارها را بیسروصدا ادغام یا دور میریزد. به همین دلیل در چکلیست است.
فیلدهای والدِ تکرارشده پس از انفجار باگ هستند؟
نه — این طراحی است. ردیف منفجرشده یعنی ردیف جزئیات، و ردیف جزئیات باید با کلید بتواند به رکورد والد وصل شود. در پاییندستی با کلید والد تجمیع کنید؛ اگر حجم نگرانکننده است، فایل جزئیات و یک جدول master جداگانه اکسپورت کنید.
چطور مطمئن شوم تبدیل من هیچ فیلدی از دست نداده؟
ممیزی کلید انجام دهید: اتحاد همهٔ کلیدهای JSON مبدأ را حساب کنید، سپس مجموعهٔ سرصفحههای ستون CSV، و تفاضل بگیرید. تفاضل ناخالی یعنی فیلد گمشده. در خطوط لولهٔ دستهای آن را بهصورت assert در تستها کدنویسی کنید: هر جابهجایی مبدل که شروع به گمکردن فیلد کند خودکار شکست میخورد.