פלט נתונים מממשק ה-API לייצוא נתונים לפורמט CSV

אלכסנדר לוקאס, צוות Google Analytics API – אוגוסט 2010


מבוא

במאמר הזה נסביר איך לקחת נתונים מכל שאילתה שנשלחה ל-Google Analytics Data Export API, וליצור פלט של התוצאות לפורמט CSV פופולרי. זו אחת מהמשימות הנפוצות ביותר שאנשים מבצעים עם נתוני Analytics שנשלפו מ-Data Export API, ולכן אוטומציה של התהליך היא דרך קלה לחסוך זמן רב על בסיס קבוע. בנוסף, אחרי שיהיה לכם קוד להדפסת מסמכי CSV משאילתות, תוכלו לשלב אותו בפרויקטים גדולים יותר, כמו מחוללי דוחות אוטומטיים, שירותי דואר ופונקציות 'ייצוא' למרכזי בקרה מותאמים אישית שכתבתם.

לפני שמתחילים

תוכלו להפיק את המרב ממאמר זה אם יש לכם:

סקירה כללית של התוכנית

הקוד שמתואר במאמר הזה מבצע את הפעולות הבאות:

  1. הפעלת האפשרות לבחור בזמן ריצה אם הקוד יודפס במסוף או בסטרימינג של קובץ.
  2. בהינתן אובייקט DataFeed כפרמטר, מדפיסים את הנתונים בפורמט CSV:
    • הדפסת כותרות של שורות.
    • הדפסה של שורות נתונים, כאשר כל DataEntry מהווה שורה אחת בפלט שמתקבל.
    • הפעילו כל ערך באמצעות שיטת חיטוי לקבלת פלט בטוח ל-CSV.
  3. כתוב שיטת Sanitizer שהופכת את כל קובצי ה-CSV לבטוחים.
  4. לתת לכם מחלקת Java שיכולה לקבל כל שאילתה ב-Data Export API ולהפוך אותה לקובץ CSV.

חזרה למעלה

מתן הרשאה לתקני פלט שניתנים להגדרה

הדבר הראשון שצריך לעשות הוא להגדיר מקור פלט שאפשר להגדיר עבור הכיתה להדפסה. כך, כל קוד שמשתמש בכיתה יכול להחליט אם הפלט צריך לעבור כפלט רגיל או ישירות לקובץ. כל מה שצריך לעשות כאן הוא להגדיר את השיטה getter/setter לאובייקט PrintStream. זה יהיה היעד של כל ההדפסה שתבוצע על ידי הכיתה.

private PrintStream printStream = System.out;

public PrintStream getPrintStream() {
  return printStream;
}

public void setPrintStream(PrintStream printStream) {
  this.printStream = printStream;
}

גם קל מאוד להגדיר את הפלט לקובץ. צריך רק את שם הקובץ כדי ליצור אובייקט PrintStream לקובץ הזה.

FileOutputStream fstream = new FileOutputStream(filename);
PrintStream stream = new PrintStream(fstream);
csvprinter.setPrintStream(stream);

חזרה למעלה

חזרה בין הנתונים

השורה הראשונה בקובץ ה-CSV היא השורה של שמות העמודות. כל עמודה מייצגת מאפיין או מדד מפיד הנתונים, כך שעל מנת להדפיס את השורה הראשונה, צריך לבצע את הפעולות הבאות.

  1. תופסים את הרשומה הראשונה מהפיד.
  2. חוזרים על רשימת המאפיינים באמצעות השיטה getDimensions של הרשומה הזו.
  3. מדפיסים את השם של כל מאפיין באמצעות השיטה Dimension.getName() ואחריו פסיק.
  4. חוזרים על הפעולות האלה לגבי מדדים באמצעות השיטה getMetrics(). הדפס פסיקים אחרי הכול, מלבד המדד האחרון.

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

public void printRowHeaders(DataFeed feed) {
    if(feed.getEntries().size() == 0) {
      return;
    }

    DataEntry firstEntry = feed.getEntries().get(0);

    Iterator<Dimension> dimensions = firstEntry.getDimensions().iterator();
    while (dimensions.hasNext()) {
      printStream.print(sanitizeForCsv(dimensions.next().getName()));
      printStream.print(",");
    }

    Iterator<Metric> metrics = firstEntry.getMetrics().iterator();
    while (metrics.hasNext()) {
      printStream.print(sanitizeForCsv(metrics.next().getName()));
      if (metrics.hasNext()) {
        printStream.print(",");
      }
    }
    printStream.println();
  }

הדפסת ה-"body" של קובץ ה-CSV (כל מה שמתחת לשורת שמות העמודות) דומה מאוד. יש רק שני הבדלים עיקריים. ראשית, זו לא רק הרשומה הראשונה שנבדקת. הקוד צריך לעבור בלולאה על כל הרשומות באובייקט הפיד. שנית, במקום להשתמש בשיטה getName() כדי למשוך את הערך שמיועד לחיטוי ולהדפסה, צריך להשתמש במקום זאת ב-getValue().

public void printBody(DataFeed feed) {
    if(feed.getEntries().size() == 0) {
      return;
    }

    for (DataEntry entry : feed.getEntries()) {
      printEntry(entry);
    }
  }

  public void printEntry(DataEntry entry) {
    Iterator<Dimension> dimensions = entry.getDimensions().iterator();
    while (dimensions.hasNext()) {
      printStream.print(sanitizeForCsv(dimensions.next().getValue()));
      printStream.print(",");
    }

    Iterator<Metric> metrics = entry.getMetrics().iterator();
    while (metrics.hasNext()) {
      printStream.print(sanitizeForCsv(metrics.next().getValue()));
      if (metrics.hasNext()) {
        printStream.print(",");
      }
    }
    printStream.println();
  }

הקוד הזה מחלק את הפיד לרשומות, ואת הרשומות לערכים שיש להדפיס כפלט. אבל איך נהפוך את הערכים האלה לידידותיים ל-CSV? מה קורה במקרה שבקובץ 'ערכים מופרדים בפסיקים' יש פסיק? הערכים האלה חייבים לעבור ניקיון.

חזרה למעלה

כיצד לבצע חיטוי של נתונים לצורך תאימות ל-CSV

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

למרבה הצער, הפורמט הפשוט הזה מאפשר להטעות בקלות את הנתונים הבעייתיים. מה קורה אם יש פסיק בתוך הערך? מה קורה אם באחד מהערכים יש מעברי שורה? מה צריך לקרות כשיש רווחים בין פסיקים וערכים? אפשר להתחשב בכל המצבים האלה באמצעות כמה כללים פשוטים.

  • אם המחרוזת מכילה תו של מירכאות כפולות, תו יציאה ממנו באמצעות תו שני במירכאות כפולות.
  • אם יש פסיק במחרוזת, עוטפים את כל המחרוזת במירכאות כפולות (אלא אם כבר יש לכם).
  • אם יש מעבר שורה במחרוזת, עטוף את כל המחרוזת במירכאות כפולות (אלא אם כבר עשית זאת).
  • אם המחרוזת מתחילה או מסתיימת ברווח לבן כלשהו, עוטפים את כל המחרוזת במירכאות כפולות (אלא אם כבר יש לכם).

יכול להיות קצת מסובך להמחיש איך הערכים אמורים להיראות בשלב הזה, אז הנה כמה דוגמאות. חשוב לזכור שכל דוגמה מייצגת ערך יחיד, ולכן המערכת תסמן בתו מילוט (escape) אותה. לשם הבהרה, רווחים יוצגו כתו _.

לפני אחרי
ללא שינוי ללא שינוי
מירכאות כפולות אקראיות מירכאות כפולות אקראיות
פסיק,מופרד "פסיק,מופרד"
שני
שורות
"שתי
שורות"
_רווח, ופסיק "_רווח ופסיק"
"ציטוט מוביל, פסיק """ציטוט מוביל, פסיק"
_רווח, פסיק
שורה שנייה ומירכאות כפולות"
"_space, פסיק
שורה שנייה ומירכאות כפולות""

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

private String sanitizeForCsv(String cellData) {
  StringBuilder resultBuilder = new StringBuilder(cellData);

  // Look for doublequotes, escape as necessary.
  int lastIndex = 0;
  while (resultBuilder.indexOf("\"", lastIndex) >= 0) {
    int quoteIndex = resultBuilder.indexOf("\"", lastIndex);
    resultBuilder.replace(quoteIndex, quoteIndex + 1, "\"\"");
    lastIndex = quoteIndex + 2;
  }

  char firstChar = cellData.charAt(0);
  char lastChar = cellData.charAt(cellData.length() - 1);

  if (cellData.contains(",") || // Check for commas
      cellData.contains("\n") ||  // Check for line breaks
      Character.isWhitespace(firstChar) || // Check for leading whitespace.
      Character.isWhitespace(lastChar)) { // Check for trailing whitespace
      resultBuilder.insert(0, "\"").append("\""); // Wrap in doublequotes.
  }
    return resultBuilder.toString();
}

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

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

שימו לב שהנתונים שלמעלה משתמשים באובייקט StringBuilder, ואף פעם לא מבצעים מניפולציה ישירה על מחרוזת גולמית. הסיבה לכך היא שהפרמטר StringBuilder מאפשר לשנות את המחרוזת באופן חופשי בלי ליצור עותקים זמניים בזיכרון. מכיוון שמחרוזות ב-Java לא ניתנות לשינוי, כל שינוי קל שתבצעו ייצור מחרוזת חדשה לגמרי. כשמחפשים נתונים בגיליון אלקטרוני, הדברים האלה יכולים להצטבר במהירות.

מספר השורות x ערכים בכל שורה x שינויים בערך = סה"כ מחרוזות חדשות שנוצרו
10,000 10 3 300,000

חזרה למעלה

מה השלב הבא?

עכשיו, לאחר שקיבלת פטיש מוזהב, נשאר רק לצאת לחפש ציפורניים. הנה כמה רעיונות שיעזרו לכם להתחיל.

  • מומלץ לעיין בקוד המקור של האפליקציה לדוגמה, שמשתמש במחלקה הזו כדי להדפיס קובץ CSV על סמך שאילתה לדוגמה. הוא לוקח את שם קובץ הפלט כפרמטר של שורת פקודה, ומדפיס באופן רגיל כברירת מחדל. השתמשו בו כנקודת התחלה, בנו משהו מדהים!
  • CSV הוא רק אחד מהפורמטים הפופולריים הרבים. אפשר לשנות את המחלקה לפלט לפורמט אחר, כמו TSV, YAML, JSON או XML.
  • כתיבת אפליקציה שיוצרת קובצי CSV ושולחת אותם בסיום התהליך. דיווח חודשי אוטומטי בקלות!
  • כתוב אפליקציה שמאפשרת להזין שאילתות באופן אינטראקטיבי, וכך ליצור ממשק עשיר להתעמקות בנתונים.