• בלוג
  • איך לקרוא קובץ CSV למערך של מפות ב Clojure

איך לקרוא קובץ CSV למערך של מפות ב Clojure

06/09/2020

בתיעוד על ספריית העבודה עם CSV של Clojure מצאתי את קטע הקוד הבא, שהופך קובץ CSV למערך של מפות:

(defn csv-data->maps [csv-data]
  (map zipmap
       (->> (first csv-data)
            repeat)
      (rest csv-data)))

הקטע משתמש במספר קסמים של Clojure ומשקף הרבה מהדברים היפים בשפה. בואו נפרק אותו כדי להבין איך זה עובד.

1. הפונקציה zipmap

הקסם הראשון הוא הפונקציה zipmap של קלוז'ר. את זיפ אנחנו מכירים מפייתון ומהרבה שפות אחרות; הגירסה של קלוז'ר משלבת בין zip ליצירת מילון בפונקציה אחת.

במילים אחרות הקוד הבא בשפת רובי:

%w(a b c).zip([10, 20, 30]).to_h
=> {"a"=>10, "b"=>20, "c"=>30} 

מתורגם לפונקציה הבודדת הבאה ב Clojure:

(zipmap ["a" "b" "c"] [1 2 3])
=> {"a" 1, "b" 2, "c" 3}

2. הפונקציה map

גם הפונקציה map מוכרת משפות אחרות: היא מקבלת פונקציה ואוסף אלמנטים ומפעילה את הפונקציה על כל האלמנטים באוסף. הקסם בקלוז'ר הוא שאנחנו יכולים להעביר ל map מספר אוספים, ואז map תיקח אלמנט אחד מכל אוסף ותפעיל כל פעם את הפונקציה על קבוצת אלמנטים.

קל לראות את זה דרך פונקציית פלוס:

(map + (range 5) (range 5) (range 5))
=> (0 3 6 9 12)

מה שקרה הוא ש map קיבלה שלוש רשימת של אלמנטים, בכל אחת האלמנטים 0, 1, 2, 3 ו-4. הפונקציה לקחה את האיבר הראשון מכל רשימה (כלומר את כל האפסים), חיברה את כולם וקיבלה 0. אחר כך המשיכה לאיבר השני (המספר 1), חיברה את כולם והחזירה 3. הבא בתור היה המספר 2 שכשמחברים אותו 3 פעמים מקבלים 6 וכך הלאה לקבלת כל הסידרה.

3. מבנה קובץ CSV בקלוז'ר

ספריית data.csv של קלוז'ר יודעת לקרוא קובץ CSV ולהחזיר אותו בתור רשימה של רשימות. אם קובץ ה CSV שלי נראה כך:

"Code","Access_to_basic_amenities","Occupied_private_dwellings"
"00","None of these",5979
"01","Cooking facilities",1512414
"02","Tap water that is safe to drink",1481133
"03","Kitchen sink",1513830
"04","Refrigerator",1481430
"05","Bath or shower",1514472
"06","Toilet",1515042
"07","Electricity supply",1503675
"77","Response unidentifiable",1794
"99","Not stated",132615
"TotalStated","Total stated",1529901
"TotalResponses","Total responses",10527978
"Total","Total",1664313

אז בקריאת הקובץ למבנה נתונים קלוז'רי אקבל את המבנה הבא:

[
  ["Code" "Access_to_basic_amenities" "Occupied_private_dwellings"] 
  ["00" "None of these" "5979"]
  ["01" "Cooking facilities" "1512414"]
  ["02" "Tap water that is safe to drink" "1481133"]
  ["03" "Kitchen sink" "1513830"]
  ["04" "Refrigerator" "1481430"]
  ["05" "Bath or shower" "1514472"]
  ["06" "Toilet" "1515042"]
  ["07" "Electricity supply" "1503675"]
  ["77" "Response unidentifiable" "1794"]
  ["99" "Not stated" "132615"]
  ["TotalStated" "Total stated" "1529901"]
  ["TotalResponses" "Total responses" "10527978"]
  ["Total" "Total" "1664313"]
]

4. הטריק

ועכשיו יש לנו את כל הכלים להפוך את המבנה שקיבלתי מ read-csv למבנה של מערך של אוביקטי JSON, בעזרת החיבור בין map לבין zipmap:

אנחנו נשתמש ב map כדי להעביר ל zipmap שני פרמטרים - הראשון הוא השורה הראשונה מהקובץ (שורת המפתחות), והשני יהיה שורה כלשהי מהקובץ (כל פעם שורה אחרת). זיפמפ יחבר את שני הפרמטרים כדי ליצור מפה ו map החיצוני יחזיר את כל התוצאות, כלומר את רשימת כל המפות.

עכשיו רק צריך למצוא דרך לייצר את שני האוספים שנעביר ל zipmap... בקלוז'ר אנחנו יכולים לקחת רשימה ולהוריד ממנה את האיבר הראשון עם הפונקציה rest:

(rest [10 20 30 40])
=> (20 30 40)

בנוסף אנחנו יכולים לקחת אלמנט וליצור ממנו רשימה בכל אורך עם הפונקציה repeat:

(take 10 (repeat 5))
=> (5 5 5 5 5 5 5 5 5 5)

נחזור לקוד מתחילת הפוסט:

(defn csv-data->maps [csv-data]
  (map zipmap
       (->> (first csv-data)
            repeat)
      (rest csv-data)))

לוקחים את השורה הראשונה ושולחים אותה כפרמטר ל repeat כדי לקבל סידרה אינסופית בה כל איבר הוא פשוט אותה שורה ראשונה. זה יהיה האוסף הראשון שנשלח ל map.

אחרי זה לוקחים את כל שאר השורות בקובץ (כולן מלבד הראשונה) בתור האוסף השני ל map.

הפונקציה שאני שולח ל map היא zipmap. כל פעם היא מקבלת כפרמטר ראשון את השורה הראשונה (שוב, בגלל ה repeat), וכפרמטר שני את השורה הבאה מתוך אוסף שאר השורות של ה CSV. זיפמפ מחברת את שני הפרמטרים שלה כדי ליצור מפה ו map ממשיכה להפעיל את זיפמפ על כל השורות ואוספת את התוצאות. ברגע שאוסף כל השורות ייגמר map תסיים ותחזיר לנו מערך של כל התוצאות.

הקוד הבא ממחיש איך להשתמש בפונקציה כדי לפענח קובץ CSV:

(with-open [reader (io/reader "data/csv/access-to-basic-amenities-total-responses-2018-census-csv.csv")]
  (->>
    reader
    (csv/read-csv)
    (csv-data->maps)
    (vec)))

ו-4 השורות הראשונות מהתוצאה על קובץ ה CSV מהדוגמה נראות כך:

[
  {
   "Code" "00",
   "Access_to_basic_amenities" "None of these",
   "Occupied_private_dwellings" "5979"
  }

  {
    "Code" "01",
    "Access_to_basic_amenities" "Cooking facilities",
    "Occupied_private_dwellings" "1512414"
  }

  {
    "Code" "02",
    "Access_to_basic_amenities" "Tap water that is safe to drink",
    "Occupied_private_dwellings" "1481133"
  }

  {
    "Code" "03",
    "Access_to_basic_amenities" "Kitchen sink",
    "Occupied_private_dwellings" "1513830"
  }
]