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
<?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 كالتالى
#STEP 1 (import rexml module)
require "rexml/document"
include REXML
2- انك تدخل على ملف ال xml كالتالى، مثلا عن طريق انشاء Document object من ال Document class الموجود فى ال unit التى حملنها:
#STEP 2 (load the xml document)
xmlDOC=Document.new(File.new("books.xml"))
تستطيع أن تستخدم ال HEREDOC string ولكن لا أحب موضوع ال Hard Coding فى السكربت او البرنامج نفسه
ما رأيك بأن نطبع اسم كل كتاب في ملف xml ؟
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 سيظهر لك شئ كالتالي:
<name>Introduction to Ruby</name>
<name>Introduction to Python</name>
<name>Introduction to C </name>
<name>Introduction to Perl</name>
وبنفس الفكرة إذا أردنا أسماء المؤلفين، سنقوم باستبدال name ب author
لمحة سريعة:
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"]
آخر لمحة وهى أننا نريد معرفة سعر الكتب الكلي
#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 كالتالى
require "rexml/document"
require "rexml/streamlistener"
include REXML
2- أنشئ ال ContentHandler او ال Streamer (بيثون مأثرة شوية:] )
سنعرف ال callbacks مثلا اذا بدأ فى tag او بدأ فى البيانات الخاصة بالtag او ينهى الtag كالتالى:
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>
def tag_start(tag_name, attrs)
if (tag_name=="price") then
end
end
جيد، نحن الان لن نهتم بغير price tag فإذا دخل ال parser فى tag بإسم price فإننا نريد أن نوضح هذه المعلومة للكائن وهى أاننا نستخدم instance variable يشير إلى اننا فى ال price tag كالتالى
فتتحول إلى:
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 او لا كالتالى:
def tag_end(tag_name)
#puts "Ending #{tag_name}"
@inPrice=false
end
نأتي لأبسط شئ وهو إلى الحصول على المجموع
1- عرّف instance variable بإسم @sum وأعطيه قيمة = 0
def initialize
@inPrice=false
@sum=0
end
2- فى جزئية ال data حولها إلى integer وأضفها على ال @sum
<start attr1=val attr2=val2>
DATA </start>
كالتالى مثلا
def text(data)
if @inPrice then
@sum += data.to_i
end
end
للحصول على ال sum اعمل getter كالتالى
def get_total_sum
return @sum
end
جميل جدا، يبقى كيف نستخدم صفنا هذا؟
1- اعمل نسخة من ال BooksStreamer كالتالى:
2- مرر ملف المصدر و كائن BooksStreamer إلى Document.parse_stream كالتالي:
Document.parse_stream(File.new("books.xml"), bs)
هكذا يكون bs جاهزا حسب تطبيقاتك فى start, end, text فكل ما عليك هو انك تستدعى get_total_sum كالتالى:
puts bs.get_total_sum
#output: 170