עבודה עם קבצי XML מתוך קוד perl

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

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

1. שליפת מידע מקבצי XML קטנים באמצעות XPath

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

<?xml version="1.0"?>
<parts>
    <title>Computer Parts</title>
    <part>
        <item>Motherboard</item>
        <manufacturer>ASUS</manufacturer>
        <model>P3B-F</model>
        <cost> 123.00</cost>
    </part>
    <part>
        <item>Video Card</item>
        <manufacturer>ATI</manufacturer>
        <model>All-in-Wonder Pro</model>
        <cost> 160.00</cost>
    </part>
    <part>
        <item>Sound Card</item>
        <manufacturer>Creative Labs</manufacturer>
        <model>Sound Blaster Live</model>
        <cost> 80.00</cost>
    </part>
    <part>
        <item> inch Monitor</item>
        <manufacturer>LG Electronics</manufacturer>
        <model> 995E</model>
        <cost> 290.00</cost>
    </part>
</parts>

לכל תוכן אלמנט שנרצה לקבל נצטרך לתאר את הנתיב עד אליו. הדרך הפשוטה ביותר לתאר נתיב ב xpath היא לשרשר את שמות הצמתים עם סימן / להפרדה ביניהם. כך הנתיב:

/parts/title

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

/parts//item

ל XPath הרבה מאוד יכולות מעבר למה שהוצג כאן ואני ממליץ למי שלא מכיר לעבור על הדוגמאות בקישור הבא:
https://msdn.microsoft.com/en-us/library/ms256086(v=vs.110).aspx

תחביר XPath מלא עם כל היכולות זמין בקישור:
http://www.w3.org/TR/xpath/

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

כדי להריץ את התוכנית מוזמנים לעבור לטאב ה Terminal ולרשום בשורת הפקודה:

perl demo1.pl

רוצים לשחק עם הדוגמא? שנו את קובץ ה XML וקוד התוכנית ונסו להריץ שוב ושוב.

2. שיפור הקוד באמצעות XML Rabbit

ככל שתעבדו עם קבצי XML מורכבים יותר גם שאילתות ה XPath יהפכו מורכבות יותר. כדי להקל על ניהול הקוד רצוי להפריד את שאילתות ה XPath המתארות את מבנה ה XML מהקוד המשתמש בהן. כך אם יהיה שינוי במבנה או שנצטרך לעבוד עם קובץ נוסף במבנה אחר נוכל לעדכן את השאילתות במקום אחד מרכזי. המודול XML Rabbit נועד לאפשר הפרדה זו בדיוק. המודול מתבסס על Moose ומאפשר לכם להגדיר שפה מותאמת לדומיין עבור קובץ ה XML שלכם. לדוגמא בקובץ שלנו שמכיל חלקי חילוף למחשב נוכל להגדיר שהמידע מורכב מפריטים ולכל פריט יהיו מאפיינים כמו שם פריט, יצרן, מודל ועלות. 

לאחר הגדרת השאילתות (אותה נראה ממש בעוד רגע) נוכל לכתוב קוד שיראה כך ולא יכלול שאילתות XPath כלל:

my $xml = My::Parts->new(file => 'parts.xml');
say "Title = ", $xml->title;

foreach my $item (@{$xml->parts}) {
    say "Item: ", $item->name;
}

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

מוזמנים להריץ באמצעות מעבר ל Terminal וכתיבת הפקודה:

perl demo2.pl

קוד התוכנית מבוסס מחלקות ומגדיר מחלקה לכל חלק בעץ ה XML שמעניין אותנו. בדוגמא שלנו תת העץ ״חלק״ מוגדר כמחלקה, וגם העץ כולו מוגדר כמחלקה נוספת. בהחלט אפשר לדמיין מבני XML מורכבים יותר בהם נצטרך יותר מחלקות כדי לתאר את כל סוגי המידע השונים.
כל מחלקה כזו מחזיקה את שאילתות ה XPath המביאות מבסיס או שורש המחלקה אל פריטי המידע השונים. כך במחלקת השורש השתמשנו בפונקציה has_xpath_value כדי לחבר בין שאילתת xpath לשם title. בזכות הקוד הזה השורה $xml->title בתוכנית הראשית יודעת באיזו שאילתת XPath להשתמש. הפונקציה has_xpath_object_list מאפשרת לחבר בין תת-עץ למחלקה נוספת. כך כל תת-עץ שמתחיל ב part יקושר למחלקה השניה שלנו  My::Parts::Part. הפונקציה מקבלת שלושה פרמטרים: השם שיחובר לשאילתה (במקרה שלנו parts), השאילתה עצמה ואובייקט חיבור שמגדיר לכל אלמנט בתוצאה לאיזו מחלקה הוא מתאים. הקוד הזה הוא מה שמאפשר להפעיל בתוכנית הראשית את הפונקציה $xml->parts ולקבל רשימה של אובייקטי My::Parts::Part. כל אחד מהם גם כן חושף פונקציות המחוברות לשאילתות XPath שיופעלו על תת העץ הרלוונטי.

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

 

3. עדכון קבצי XML קטנים

שני ממשקי התכנות העיקריים לעבודה עם קבצי XML הינם DOM ו SAX. הראשון הוא DOM והוא מבוסס על טעינת כל קובץ ה XML לזכרון במבנה עץ, והשני נקרא SAX ומבוסס על טיפול במידע אלמנט-אחר-אלמנט. טעינת המידע כולו לזכרון מאפשרת עבודה אינטואיטיבית יותר מבחינה תכנותית ולכן בקבצי XML קטנים אנו נבחר בממשק ה DOM. ממשק זה גם היה הבסיס בחלק הקודם לשאילתות XPath.

ממשק ה DOM מבוסס על מבנה של עץ בזכרון כאשר כל צומת בעץ מייצג תגית ב XML ומכל צומת ניתן להגיע לילדים (כלומר התגיות המוכלות באלמנט) או להורה, כלומר לאלמנט בו נמצאת התגית עליה אנו מדברים. האיור הבא ממחיש את הקשר בין מבנה העץ לבין תוכן קובץ ה XML:

לצורך עדכון שדות בקובץ XML אנו נטען את הקובץ במלואו לזכרון למבנה עץ, נמצא את הצמתים אותם נרצה לעדכן באמצעות שאילתת XPath או פונקציות אחרות של העץ, למשל הפונקציה getElementsByTagName שמחזירה רשימת אלמנטים לפי שם תגית. לאחר איתור הצמתים נוכל לשנות את המידע באמצעות הפונקציות השונות של XML::LibXML::Element למשל הפונקציה removeChildNodes שמוחקת את כל תוכן האלמנט, הפונקציה appendText שמוסיפה טקסט חדש או הפונקציה setAttribute שמעדכנת מאפיין של התגית. 

לאחר העדכון נשתמש בפונקציה toFile של XML::LibXML::Document שתאפשר כתיבה של המסמך המעודכן לקובץ. 

התוכנית הבאה לוקחת את הקובץ parts.xml ומעלה את המחירים של כל המוצרים ב 10%. את התוצאה נכתוב לתוך קובץ חדש בשם parts_new.xml. מוזמנים להריץ את הקוד באמצעות מעבר ל Terminal והקלדת הפקודה:

perl demo3.pl

הבחירה במודול XML::LibXML מומלצת שכן מודול זה משתמש בקוד C כדי לבצע את עבודת הפענוח והעדכון, מה שהופך אותו למהיר יותר. בנוסף מדובר על מודול וותיק ויציב עם הרבה מאוד יכולות וממשק תכנות סטנדרטי של DOM שיראה מוכר גם למי שהגיע משפות אחרות. הבעייה היחידה במודול היא התיעוד: כאן תצטרכו לזכור שהתיעוד מפוצל בין חבילות רבות. לכל מחלקה יש עמוד תיעוד משלה ולא תמיד הקשר בין המחלקות ברור. הרשימה המלאה של עמודי התיעוד של המודול נמצאת בקישור:
https://metacpan.org/release/XML-LibXML

4. שליפת מידע מקבצי XML גדולים

קבצי XML רבים גדולים מכדי להכנס לזכרון, מה שהופך את השימוש ב DOM ו XPath ללא פרקטי. ממשק תכנות שנקרא SAX מאפשר גישה שונה לעבודה עם קבצי XML שלא מחייבת החזקת המבנה כולו בזכרון. ממשק SAX מורכב משני רכיבים, הראשון הוא המפענח שעובר על המידע בקובץ שורה אחר שורה. בכל פעם שהמפענח מגיע למקטע מעניין הוא מדווח על התוצאה לרכיב המטפל, זורק את המידע מהזכרון וממשיך הלאה. 

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

perl demo4.pl

מרבית הקוד נמצא בתוך המחלקה MaxPriceFilter. מחלקה זו היא קוד המטפל — היא מקבלת אירועים ממפענח ה SAX ועליה להגדיר מה לעשות בעקבות כל אירוע. מנגנון הטיפול כולל הגדרה של פונקציית טיפול לכל אירוע שמעניין אתכם (אין צורך להגדיר פונקציות טיפול ריקות לאירועים שלא מעניינים אתכם). הפרמטר הראשון שמועבר לכל פונקציית טיפול נקרא $self. זהו פרמטר מסוג Hash Ref באמצעותו אנו מעבירים מידע בין פונקציות הטיפול. המידע היחיד שיישמר בזכרון הוא התוכן של משתנה זה, ולכן קל לשלוט בגודל הזכרון שהתוכנית לוקחת בהשוואה ל DOM.
מבחינת הקוד עצמו שימו לב שהמטפל מזהה בכל סיום אלמנט מהו האלמנט שהסתיים ולפי סוג האלמנט קורא לפונקציה הרלוונטית (שורות 38-48). בכל סיום של אלמנט item אנו שומרים את שם הפריט בצד, בסיום אלמנט cost אנו שומרים את העלות בצד ובסיום כל part נבדוק אם מצאנו את הפריט היקר ביותר עד כה ואם כן נחליף את הפריט השמור. 

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

5. עדכון קבצי XML גדולים

העבודה עם קבצי XML גדולים מכדי להכנס לזכרון משאירה אותנו בממשק התכנות SAX. הפעם במקום לשלוף מידע אנו נרצה לכתוב מיד כל טקסט שאנו קוראים: מידע שלא מעניין אותנו נדפיס כמו שהוא ומידע שמעניין אותנו נעדכן ואז נדפיס. אחד הרעיונות היפים של SAX הוא היכולת לשלב מספר מטפלי SAX אחד אחרי השני, כלומר המפענח מייצר אירוע start_element, קוד המטפל הראשון קורא את פרטי האירוע ואז מעביר את הטיפול לקוד המטפל הבא אחריו בתור וכן הלאה.
שיטת עבודה זו מאפשרת שימוש מחדש בקוד לביצוע חלקים שונים של פענוח הקובץ או הכתיבה. כל מודול מטפל נקרא SAX Filter. דוגמא בולטת של פילטר היא המודול XML::SAX::Writer — זהו מטפל SAX שפשוט יכתוב למסך או לקובץ את כל המידע שתתנו לו. 

בדוגמא הבאה אנו נכתוב SAX Filter שיעלה ב 10% את מחירו של כל פריט. כתיבת פילטר דומה מאוד לכתיבת קוד מטפל, ובנוסף על הקוד שראינו בסעיף הקודם נצטרך בסוף כל פונקציה להפעיל את קוד המטפל הבא אחרינו בתור. שימו לב למשל לפונקציה start_element שבסופה בשורה 32 יש קריאה כזו למטפל הבא בתור. כרגיל נתחיל בהצגת הקוד ולאחר מכן הסבר קצר. את התוכנית תוכלו להריץ באמצעות מעבר ל Terminal והקלדת:

perl demo5.pl

התוכנית מתחילה ביצירת המפענח והמטפלים. תבנית העיצוב נקראת Decorator: אנו יוצרים אובייקט SAX Writer ומעבירים אותו לאובייקט IncPrice שיצרנו. כך תוצאת הפילטר IncPrice תשמש כקלט לאובייקט ה Writer שכותב את התוצאה למסך. אם תרצו לכתוב לקובץ חדש במקום יש להחליף את שורת יצירת ה Writer לשורה הבאה:

my $w = XML::SAX::Writer->new( Output => "demo5.xml");

קוד המטפל מדגים כתיבת Filter. בסוף כל פונקציה אנו קוראים לפונקציה המתאימה מהפילטר הבא אחרינו בתור (במקרה שלנו ה Writer). האתגר בדוגמא היה לוודא שהפונקציה characters של המטפל הבא בתור תקרא רק לאחר שהעלינו את המחיר. זו הסיבה שבפונקציה characters בדקנו אם האלמנט הנוכחי הוא אלמנט מסוג cost. עבור אלמנטים אלו נקרא לפונקציה characters רק אחרי סיום האלמנט ולאחר שינוי התוכן למחיר הגבוה.

6. הערות וקריאה נוספת

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

הדף Perl XML FAQ סוקר שאלות ותשובות נפוצות לעבודה עם XML מתוך פרל וסוקר בצורה מסודרת הרבה מהמודולים הקיימים ואת היחסים ביניהם.
http://perl-xml.sourceforge.net/faq/

המודול XML::Twig מציע גישה שונה וייחודית לפרל לעבודה עם קבצי XML. מודול זה מבוסס על קריאת תת-עצים כך שמצד אחד אתם מקבלים את היתרונות של עבודה עם מבנה נתונים של עץ ומצד שני לא טוענים את כל המידע לזכרון מה שמביא לצריכת זכרון נמוכה יותר. מאחר ושיטת העבודה של XML::Twig ייחודית לפרל החלטתי להשאיר אותו מחוץ לפוסט זה, אך עדיין מדובר במודול חשוב ויציב עם הרבה תיעוד. מוזמנים לקרוא ולהתרשם באתר הפרויקט:
http://xmltwig.org/xmltwig/

מודולים נוספים שלא נכנסו לפוסט זה כוללים את XML::Simple ו XML::TreeBuilder. אפשר לקרוא עליהם ועל מודולים נוספים בפוסט הלא חדש Perl XML QuickStart של קיפ המפטון בקישור:
http://www.xml.com/pub/a/2001/04/18/perlxmlqstart1.html