Author Topic: XMLing with Ruby  (Read 558 times)

Ahmed Youssef

  • Helping Freak
  • Administrator
  • Active Member
  • *****
  • Posts: 242
    • View Profile
    • WWW
    • Email
XMLing with Ruby
« on: October 26, 2008, 05:44:12 PM »
XML & Ruby

هى اختصار ل eXtensible Markup Language  وهى واضحة بأنها Markup language مثل ال HTML ولكن يوجد فرق !!


XML vs HTML

الإثنان يحتويان على وسوم tags لكن الفرق أن ال HTML تستخدم tags محددة وجاهزة ولكن ال XML انت الذي تخترع فيها ال tags الخاصة بك.
فرق مهم جدا أيضا وهو أن HTML للعرض  لكن ال XML لتخزين البيانات، بكل تأكيد تستطيع أن تستخدمها فى العرض ولكن ليس هذا هدفنا من هذا الموضوع.
هناك تقنيات كثير تعتمد على ال XML ، حتى في عمل الواجهات الرسومية GUI! حيث تحتفظ المعلومات فى ملف ب xml syntax .. لن أكون مبالغا اذا قلت ان ال .NET والJava قائمين على ال xml فى أشياء كثيرة.

على فرض اننا نملك ملف كالتالى books.xml
Code: [Select]
<?xml version="1.0" encoding="UTF-8"?>

<!--
    Document   : books.xml
    Created on : April 19, 2008, 10:01 AM
    Author     : ahmed
    Description:
        Purpose of the document follows.
-->

<books>
       <book>
            <id>1</id>
            <name>Introduction to Ruby</name>
            <author>Ahmed Youssef</author>
            <price>20</price>
       </book>
       
       <book>
            <id>2</id>
            <name>Introduction to Python</name>
            <author>Ahmed Youssef</author>
            <price>40</price>
       </book>

       <book>
            <id>3</id>
            <name>Introduction to C </name>
            <author>Ahmed Mostafa</author>
            <price>70</price>
       </book>
       
      <book>
            <id>4</id>
            <name>Introduction to Perl</name>
            <author>M_SpAwN</author>
            <price>40</price>
       </book>
       
</books>



واضح أنه منظم ومقسم ل books وتضم book element وكل واحد فيهم يضم id و name و author و price
لاحظ اى وسم بهذه الصورة <start></start> يسمى عنصر element
ال XML فى ال configuration files تكون مريحة مثل هذه،  بعكس النص المجرد  فى .ini مثلا او حتى فى ملفات اعداد grub و lilo

العملية كلها تتم على عدة  خطوات:

1- أنك تعمل import ل REXML كالتالى
Code: [Select]
#STEP 1 (import rexml module)

require "rexml/document"
include REXML

2- انك تدخل على ملف ال xml كالتالى، مثلا عن طريق انشاء Document object من ال Document class الموجود فى ال unit التى حملنها:
Code: [Select]
#STEP 2 (load the xml document)
xmlDOC=Document.new(File.new("books.xml"))

تستطيع أن تستخدم ال HEREDOC string ولكن لا أحب موضوع ال Hard Coding فى السكربت او البرنامج نفسه

ما رأيك بأن نطبع اسم كل كتاب في ملف xml ؟
Code: [Select]
xmlDOC.elements.each("books/book/name") do |element|
  puts element.text
end

#Output
                Introduction to Ruby
                Introduction to Python
                Introduction to C           
                Introduction to Perl



لكل عنصر بإسم name ستتم طباعة ال text (لاحظ ان element هو صف مستقل بذاته ونحن لا نريد غير text )
اذا كتبت puts element سيظهر لك شئ كالتالي:
Code: [Select]
<name>Introduction to Ruby</name> 
<name>Introduction to Python</name>
<name>Introduction to C </name>
<name>Introduction to Perl</name>

وبنفس الفكرة إذا أردنا أسماء المؤلفين، سنقوم باستبدال name ب author

لمحة سريعة:
Code: [Select]
authors=[]
xmlDOC.elements.each("books/book/author") do |element|
  authors.push(element.text)
end

#uniq! it
authors.uniq!
p authors

#output ["Ahmed Youssef", "Ahmed Mostafa", "M_SpAwN"]


آخر لمحة وهى أننا نريد معرفة سعر الكتب الكلي 
Code: [Select]
#get sum of prices ?
sum=0

xmlDOC.elements.each("books/book/price") do |element|
 
  sum += element.text.to_i
end

puts "Total: "+ sum.to_s

#output: 170

الاسلوب الحالي يسمى أسلوب DOM وهو يعتمد على tree (حيث يخزن كل الملف فى tree structure فى الذاكرة)

يوجد أسلوب آخر، شخصيا أفضله و يوافقنا الرأي كثيرون، وهو أسلوب SAX وهو يعتمد على ال events انه يبدأ فى tag معين (فهو يتعامل مع ال tag وال attributes )
<start attr1=val attr2=val2> DATA </start>
ويدير البيانات
<start attr1=val attr2=val2> DATA </start>
وانه ينهى tag معين
<start attr1=val attr2=val2> DATA </start>

دعنا نجرب نفس المثال الخاص بالحصول على الثمن الكلى للكتب من ملف books.xml

1- اعمل load لل rexml كالتالى
Code: [Select]
require "rexml/document"
  require "rexml/streamlistener"
  include REXML
 

2- أنشئ ال ContentHandler او ال Streamer (بيثون مأثرة شوية:] )

سنعرف ال callbacks مثلا اذا بدأ فى tag او بدأ فى البيانات الخاصة بالtag او ينهى الtag كالتالى:
Code: [Select]
class BooksStreamer
    include StreamListener
   
    def initialize
      @inPrice=false
      @sum=0
    end

    def tag_start(tag_name, attrs)
      puts “Starting #{tag_name}”
      if (tag_name=="price") then
        @inPrice=true
      end
    end

    def tag_end(tag_name)
      #puts "Ending #{tag_name}"
      @inPrice=false
    end

    def text(data)
        if @inPrice then
            @sum += data.to_i
        end
    end

   
    def get_total_sum
     
      return @sum
     
    end
   
  end
 
اولا tag_start هى اول callback تستدعى لما الparser يبدأ فى tag معين ولاحظ ان ال parser سيمر على ال tag_name وخصائصه

<start attr1=val attr2=val2 ....> DATA </start>
Code: [Select]
def tag_start(tag_name, attrs)

      if (tag_name=="price") then
 
      end

    end
   

جيد، نحن الان لن نهتم بغير price tag فإذا دخل ال parser فى tag بإسم price فإننا نريد أن نوضح هذه المعلومة للكائن وهى أاننا نستخدم instance variable يشير إلى اننا فى ال price tag كالتالى     
Code: [Select]
   @inPrice=true

فتتحول إلى:
Code: [Select]
    def tag_start(tag_name, attrs)
      #puts "Starting #{tag_name}"
      if (tag_name=="price") then
        @inPrice=true
      end

    end

جميل جدا.. وبالمنطق إذا مر المفسر على وسم </price> ؟

<start attr1=val attr2=val2> DATA </start>

 فبكل تأكيد هو لن يكون في price tag فنعمل reset للمتغير الذي يشير هل الparser فى price tag او لا كالتالى:
Code: [Select]
 def tag_end(tag_name)
      #puts "Ending #{tag_name}"
      @inPrice=false
    end

نأتي لأبسط شئ وهو إلى الحصول على المجموع
1- عرّف instance variable بإسم @sum وأعطيه قيمة = 0
Code: [Select]
    def initialize
      @inPrice=false
      @sum=0
    end


2- فى جزئية ال data حولها إلى integer وأضفها على ال @sum
<start attr1=val attr2=val2> DATA </start>

كالتالى مثلا
Code: [Select]
    def text(data)
        if @inPrice then
            @sum += data.to_i
        end
    end


للحصول على ال sum اعمل getter كالتالى
Code: [Select]
    def get_total_sum
     
      return @sum
     
    end


جميل جدا، يبقى كيف نستخدم صفنا هذا؟

1- اعمل نسخة من ال BooksStreamer كالتالى:
Code: [Select]
bs=BooksStreamer.new

2- مرر ملف المصدر و كائن BooksStreamer   إلى  Document.parse_stream كالتالي:
Code: [Select]
 Document.parse_stream(File.new("books.xml"), bs)


هكذا يكون bs جاهزا حسب تطبيقاتك فى start, end, text فكل ما عليك هو انك تستدعى get_total_sum كالتالى:
Code: [Select]
 puts bs.get_total_sum

#output: 170




« Last Edit: October 29, 2008, 11:37:18 PM by Ahmed Youssef »
Logged

Life is just a chance to grow a soul. - A. Powell
Weblog: http://ahmedyoussef.wordpress.com/