JSON Toolkit

JSON ל-CSV: מאין מגיעה כותרת העמודות ומדוע הערכים משתנים

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

מתקדםכ-6 דקות קריאהפורסם 2026-11-04עודכן 2026-11-04
אסטרטגיות כותרת ונאמנות ערכים בהמרת JSON ל-CSV

כשהמרה מ-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) אוהבים נקודות.

שני תרופות:

  1. החליפו את המפריד. sep="_" או מפריד מותאם — לאחר בדיקה שהמפתחות המקוריים לעולם לא מכילים אותו.
  2. החליפו את סימון הנתיבים. 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 של pandas עושה זאת ישירות:

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):

1. null, היעדרות ומחרוזת ריקה הופכים כולם לתאים ריקים. קלט [{"a":null},{"b":""},{"c":1}] — שלושת התאים יוצאים ריקים. ל-CSV אין שלושה מצבים, רק "יש תווים" ו"אין תווים". אחרי סבב-הלוך כל שלושתם נקראים ""; הסמנטיקה המקורית אינה ניתנת לשחזור.

2. אובייקטים ומערכים מקוננים הופכים למחרוזות JSON. המרכאות מוכפלות לפי RFC 4180 ({""name"":""张三""}), ובהדבקת התא לכל מפענח JSON שהוא הערך חוזר — לכן JSON בתא שומר על נאמנות.

3. מספר שלם בן 20 ספרות מאבד דיוק כבר בפענוח — והאשמה אינה ב-CSV. קלט "id": 9007199254740993; ב-CSV כבר 9007199254740992. JSON.parse ב-JavaScript משתמש ב-doubles של IEEE 754: האובדן מתרחש בזמן הפענוח, לפני כל ממיר CSV שהוא. Python json.load שומר עליו במלואו. שדות מסוג מזהה (מזהי snowflake, מספרי הזמנה) צריכים להגיע כמחרוזות כבר מהיצרן. עוד על דיוק מספרי במדוע מספרים גדולים מאבדים דיוק.

4. אפסים מובילים במחרוזות בטוחים בשכבת הטקסט של ה-CSV. "phone": "00123" נשאר 00123 ב-CSV, כי זו מחרוזת מההתחלה עד הסוף ושום דבר לא משפשף אותה. מה שמשפשף אפסים הוא השלב המאוחר יותר שבו אקסל מתייחס לתא כמספר — ראו ארבע דרכים להמיר JSON לאקסל.

5. שדות עם פסיקים, מרכאות או ירידות שורה. 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 של papaparse לא מסיר כפילויות, ואקסל בפתיחה ממזג או משליך כפילויות בשקט. מסיבה זו זה ברשימת הבדיקה.

האם שדות הורה משוכפלים אחרי פיצוץ הם באג?

לא — זה עיצוב. שורה מפוצצת היא שורת פירוט, ושורת פירוט חייבת להיות מתחברת מחדש לרשומת ההורה לפי מפתח. צברו במורד הזרם לפי מפתח ההורה; אם הגודל מדאיג, ייצאו קובץ פירוט וטבלת master נפרדת.

כיצד לבדוק שההמרה שלי לא איבדה שום שדה?

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