Author Topic: XML and Python (dom, sax)  (Read 921 times)

Ahmed Youssef

  • Helping Freak
  • Administrator
  • Active Member
  • *****
  • Posts: 242
    • View Profile
    • WWW
    • Email
XML and Python (dom, sax)
« on: October 26, 2008, 03:42:49 AM »
XMLing with Python


ملفات ال xml من اهم الملفات اللى بنتعامل معاها بصورة شبه يومية وبايثون من انسب الحلول للتعامل معاها..
فى اكتر من باكيج للتعامل مع ال Markups
http://docs.python.org/lib/markup.html


على فرض عندنا ملف كالتالى
Code: [Select]
Code: [Select]
<?xml version="1.0"?> 

<!DOCTYPE books SYSTEM "books.dtd">
<?xml-stylesheet type="text/xsl" href="books.xsl"?>


<books>
    <book id="1">
        <name>Introduction to Python</name>
        <author>Ahmed Youssef</author>
        <price>80</price>
    </book>
    <book id="2">
        <name>Introduction to Java</name>
        <author>Wael Muhammed</author>
        <price>130</price>
    </book>
    <book id="3">
        <name>Introduction to Ruby</name>
        <author>Ahmed Youssef</author>
        <price>70</price>
    </book>
    <book id="4">
<name>Introduction to Linux Programming</name>
<author>Ahmed Mostafa</author>
<price>90</price>
    </book>
</books>

فى root وهى ال books tag 
وليها ابناء كل واحد بإسم book
كل  book tag ليه attributes ؟ ايوة كل book لية id معين
داخل كل book بيشمل name, author, price tags لإسم الكتاب والكاتب والسعر

من الملف دا عايزين نجيب اسم كل كتاب ومجموع السعر بتاعهم


1- minidom
فى implementation خفيفة لDOM بإسم minidom هنستدعيها كالتالى
Code: [Select]
import xml.dom.minidom as md #(parse, parseString..)

فى عندنا دالتين مهمين وهم parse, parseString
parse للتعامل مع file
parseString للتعامل مع string
والإتنين هيدولك ريترن ب document object

ال node object
هو يعتبر الأب لكل العناصر ملف ال xml وليه ميثودز/صفات مهمة

Code: [Select]
nodeType 

بتعبر عن النوع هل هى text node, element, comment,document,.. etc

Code: [Select]
parentNode

رفرنس للأب (ماعدا ال document root) ولل attrs هتكون ديما None

Code: [Select]
previousSibling
ال node السابقة للnode الحالية إلا اذا كانت هى الأولى

Code: [Select]
nextSibling

ال node التالية إلا اذا كانت ال node الحالية هى الأخيرة

Code: [Select]
childNodes
جميع ال nodes اللى داخل ال node الحالية

Code: [Select]
hasChildNodes()

هل فى nodes داخلها ؟

Code: [Select]
firstChild

اول ابن

Code: [Select]
lastChild

اخر ابن

Code: [Select]
hasAttributes()

هل فيها attributes ؟

Code: [Select]
appendChild(child)

إضافة ابن جديد

Code: [Select]
insertBefore(child, befored)
بتضيف child قبل ال befored وفى حال عدم وجوده بيتم إضافته فى النهاية

Code: [Select]
removeChild(child)
حذف ابن child

Code: [Select]
normalize()

ربط ال text nodes المتقاربة
ال document object بيعبر عن الملف وليه ميثودز/صفات مهمة زى
Code: [Select]
documentElement # used as a property

ودى بتعبر عن ال root element وفى مثالنا هنا هى books
Code: [Select]
getElementsByTagName(tagName) #tagName

بتدور على tagName معين فى كل الأبناء وابنائهم وهكذا وتديلك ريترن ب element object
Code: [Select]
createElement(tagName) 

لإنشاء tag جديد

Code: [Select]
createComment(comment)
لإنشاء تعليق داخلى

Code: [Select]
createAttribute(attr)

لإنشاء صفة attribute

ملحوظة فى بعض الميثودز بنفس الإسم ولكن اخرها NS ودى لربطها مع namespace ما وبتاخد nsURI كمعامل ليها.

ال Element Object بيعبر عن عنصر معين فى الملف وليه ميثودز مهمة زى
Code: [Select]
tagName  #used as a property

بتعيد الإسم المجرد لل element
Code: [Select]
getElementsByTagName*

مشابهه للموجودة بال document object
Code: [Select]
hasAttribute(attrName)

هل بيحتوى على attribute ؟
Code: [Select]
getAttribute(attrName)
بيعيدلك قيمة attribute معينة بإسم attrName
Code: [Select]
setAttribute(attrName, val)

بيربط attribute معينة attrName ليها قيمة val بالعنصر
Code: [Select]
removeAttribute(attrName)

لحذف attribute معينة attrName (مش بيرفع اى exception !)

ملحوظة فى بعض الميثودز بنفس الإسم ولكن اخرها NS ودى لربطها مع namespace ما وبتاخد nsURI كمعامل ليها.

مجموعة ال exceptions
http://docs.python.org/lib/dom-exceptions.html



طيب تمام
1- استدعى ال minidom
Code: [Select]
import xml.dom.minidom as md #(parse, parseString..)

2- انشئ ال document object سواء بإستخدام parse او parseString حسب تخزينك لملف ال xml
Code: [Select]
doc=md.parse("books.xml")


3- احصل على ال document root و اعرضه واحصل على كل tag قيمته book واطبعه
Code: [Select]
def inspectBooks(): 
    global doc
    print "Root Element: ", doc.documentElement.tagName
    books=doc.getElementsByTagName("book")
    for book in books:
if book.hasAttribute("id"): #id and it should have one!
print "ID: ",book.getAttribute("id")
for child in book.childNodes:
if child.nodeType==child.ELEMENT_NODE:
if child.tagName=="name":
        child.normalize()
        print "Book: ",child.firstChild.data

تمام ال doc هنا global variable
Code: [Select]
 global doc 


الحصول على ال document root هنا جالنا ريترن ب Element object واحنا عايزين ال tagName
doc.documentElement.tagName
نحصل على كل العناصر اللى tagName بتاعها book
Code: [Select]
    books=doc.getElementsByTagName("book") 

نعمل loop على كل عنصر فيها
Code: [Select]
    for book in books: 

اذا كان فيه id attribute (لمجرد عرض المثال)
Code: [Select]
	if book.hasAttribute("id"): #id and it should have one! 
print "ID: ",book.getAttribute("id")

طيب ولطباعة اسم الكتاب؟ لاحظ انه متخزن فى ال name tag
بسيطة جدا نعمل loop على كل الأبناء فى لا book element ونشوف النوع اذا كان ELEMENT NODE  و ال tagName بتاعه هو name نطبعه
Code: [Select]
			if child.tagName=="name": 
        child.normalize()
        print "Book: ",child.firstChild.data


ملحوظة لل nodes انواع كتير element, comment, text, .. etc

ال firstChild دا بيعبر عن ال text node اللى فى ال name tag وال data بتدى ريترن بالstring اللى جواها

<name> text node ... </name

الحصول على الثمن الكلى
Code: [Select]
def getTotalSum(): 
    global doc
    thesum=0
    prices=doc.getElementsByTagName("price")
    for price in prices:
        price.normalize()
        thesum += int(price.firstChild.data) #TO int.
    return thesum

نحصل على كل ال price elements
<price>numeric_value</price>
ونحول القيمة ل int وبس ونضيفها على ال thesum وبعد مانخلص نعمل الريترن بيها

ناتج التنفيذ ل
Code: [Select]
    inspectBooks() 
    print "Total Sum: ", getTotalSum()


Root Element:  books
ID:  1
Book:  Introduction to Python
ID:  2
Book:  Introduction to Java
ID:  3
Book:  Introduction to Ruby
ID:  4
Book:  Introduction to Linux Programming
Total Sum:  370


2- SAX
بيعتمد على ال events بمعنى انه بيديلك خبر كل مايبدأ عنصر او يبدأ ال content اللى داخله وهكذا يمكن تشوفه اعقد شوية لكن انا عن نفسى من محبى استخدامه

1- استدعى اللى هنستخدمه
Code: [Select]
from xml.sax import make_parser, parseString 
from xml.sax.handler import ContentHandler

ال ContentHandler دا هو المفتاح السحرى بتاعنا فيه ميثودز event handlers بيتعمل ليها استدعاء عند حدوث حدث معين
Code: [Select]
startDocument()

بيتم استدعائها مرة واحدة عند بداية الملف
Code: [Select]
endDocument()

بيتم استدعائها مرة واحدة عند نهاية الملف

Code: [Select]
startElement(name, attrs)
بيتم استدعائها عند بداية قراءة كل عنصر el
<el [attr1=val1, attr2=val2, ... attrN=valN]>CONTENT</el>

Code: [Select]
characters(content)

بيتم استدعائها عن بداية قراءة محتوى العنصر
<el [attr1=val1, attr2=val2, ... attrN=valN]>CONTENT</el>

Code: [Select]
endElement(el)

بيتم استدعائها عند نهاية قراءة عنصر el
<el [attr1=val1, attr2=val2, ... attrN=valN]>CONTENT</el>

فى بعض الميثودز بتنتهى ب NS ودى فى حالة التعامل مع namespace

الل Attributes ماهى الا mapping او dictionary مضاف ليها بعض الميثودز مثل
Code: [Select]
getLength()

للحصول على عددهم
Code: [Select]
getNames()

الحصول على اسم كل attribute

Code: [Select]
getType()
للحصول على النوع وهى عادة CDATA

Code: [Select]
getValue(attrName)
الحصول على القيمة المرافقة للattribute المسماة attrName

نرجع للمثال
1- هنستدعى الميثودز/الكلاسز المستخدمة
Code: [Select]
from xml.sax import make_parser, parseString 
from xml.sax.handler import ContentHandler

ال make_parser هتعيد لينا XML reader
parseString لقراءة ال xml من string
parse هى ميثود تبع الXML reader object بتاخد مسار ملف (نفس parse,parseString اللى تحدثنا عنهم فى minidom)

2- ملف ال xml ك string مخزن
Code: [Select]
xmldoc="""<?xml version="1.0"?> 


<books>
    <book id="1">
        <name>Introduction to Python</name>
        <author>Ahmed Youssef</author>
        <price>80</price>
    </book>
    <book id="2">
        <name>Introduction to Java</name>
        <author>Wael Muhammed</author>
        <price>130</price>
    </book>
    <book id="3">
        <name>Introduction to Ruby</name>
        <author>Ahmed Youssef</author>
        <price>70</price>
    </book>
    <book id="4">
<name>Introduction to Linux Programming</name>
<author>Ahmed Mostafa</author>
<price>90</price>
    </book>
</books>
"""

3- انشئ كلاس جديد مشتق من ال ContentHandler
Code: [Select]
class BooksHandler(ContentHandler):

ملحوظة اى ميثود مش هتعملها override مش تكتبها فى الهاندلر..
Code: [Select]
    def __init__(self): 

        self._total=0 #Sum of prices.
        self._curel=None
        self._curid=None
        self._booksInfo=[]
        self._authors=[]

ايه المتغيرات دى كلها ؟
self._total للتخزين المجموع الكلى للأسعار
self._curel لتخزين اسم العنصر اللى بيتم معالجته
self._curid لتخزين اخر id تم قرائته
self._booksInfo تخزين معلومات عن الكتاب مكونة من ال name, id
self._authors تخزين اسماء الكتاب

Code: [Select]
    def getTotal(self): 
        return self._total

    def getBooksInfo(self):
        return self._booksInfo

    def getAuthors(self):
        return self._authors

عرفنا getters للوصول للمتغيرات الداخلية
ملحوظة يفضل تستخدم properties مع lambda
Code: [Select]
    booksinfo=property(fget=lambda self: self._booksInfo) 
    authors=property(fget=lambda self: self._authors)
    total=property(fget=lambda self: self._total)


    def startDocument(self):
        #print "Starting Document."
   pa*s
لو حبيت تضيف اى رسالة او اى حاجة على هواك يتم تنفيذها عند بداية قراءة الملف
Code: [Select]
    def endDocument(self): 
        #print "Ending Document."
pa*s

نفس السابقة ولكن عند انتهاء القراءة
Code: [Select]
    def startElement(self, el, attrs): 

        #print "Starting ", el
        self._curel=el
if el=="book":
#get the id..
self._curid=attrs.getValue("id") #attrs["id"]


هنا هيتم الإستدعاء عند بداية قراءة كل عنصر el والصفات الخاصة بيه attrs
1- نخزن العنصر الحالى فى ال self._curel
Code: [Select]
        self._curel=el 

2- نختبر اذا كان العنصر الحالى هو book فليه attribute بإسم id فنحصل عليها ونخزنها كآخر id لآخر كتاب تم قرائته فى ال reader
Code: [Select]
	if el=="book": 
#get the id..
self._curid=attrs.getValue("id") #attrs["id"]

طبعا تقدر تحصل عليها كأنك بتتعامل مع dict مش بإستخدام .getValue ميثود

Code: [Select]
  
  def characters(self, content):
        if content.strip():

if self._curel=="price":
    #print "In Price.."
    try:
        self._total += int(content)
    except:
        pa*s
elif self._curel=="name":
    self._booksInfo +=[(content, self._curid)]
elif self._curel=="author":
    self._authors +=[content]
else:
    pa*s
 
هنا هيتم استدعائها عن قراءة المحتوى للعنصر الحالى
وبناءا على العنصر الحالى هنتعامل سواء اذا كان ثمن او اسم الكتاب او الكاتب


4- ننشئ اوبجكت من الهاندلر الجديد
Code: [Select]
    bh = BooksHandler()

 ننشئ XML reader ونباصى ليه ال handler الجديد والفايل اللى هيتعالج او نستخدم parseString ونباصى ليها الهاندلر(bh)
Code: [Select]
    p = make_parser( ) 
    p.setContentHandler(bh)
    p.parse(open("books2.xml"))


او نستخدم parseString نحدد ال string اللى هيتعالج وال هاندلر (bh)
Code: [Select]
    parseString(xmldoc, bh)

للمزيد عن
http://docs.python.org/lib/module-xml.sax.html
ومش تنسى  http://www.saxproject.org/
استخدم مين ؟
همم DOM بيتعمد على انشاء tree للملف ودا شاق جدا للملفات اللى حجمها كبير!
من ناحية اخرى SAX بيعتمد على ال events ودا اسلوب فعال جدا


3- Expat undercover

Expat مكتبة سى سريعة لمعالجة ملفات ال XML وتم عمل wrapper ليها فى بايثون

1- استدعى الموديلز اللازمة
Code: [Select]
import xml.parsers.expat as exp

2- انشئ كلاس جديد بنفس فكرة ال ContentHandler
Code: [Select]
cla*s ParsingHandler(object):

    def __init__(self, xml):
        self._curel=None
        self._curattrs=None
        self._inbook=False
        self._books=[]
        self._thesum=0

        self._p=exp.ParserCreate()
        self._p.StartElementHandler=self.__startElement
        self._p.EndElementHandler=self.__endElement
        self._p.CharacterDataHandler=self.__charsDataHandler
        self._p.Parse(xml)

لاحظ عندنا متغيرات لمتابعة العنصر الحالى والصفات الحالية ليه وهل احنا داخل ال book tag او لأ واسماء الكتاب والمجموع الكلى

القسم التانى متعلق بال parser
1- انشئ XMLParserType object بإستخدام ParserCreate
2- اربط الhandlers المختصين ببداية كل عنصر ونهايته والمحتوى بhandlers انت هتجهزهم لاحقا
3- عالج ال xml بإستخدام ال Parse method

انشئ getters
Code: [Select]
    def getTotalSum(self): 
        return self._thesum

    def getBooksInfo(self):
        return self._books

    def printBooksInfo(self):
        for book in self._books:
            print book


عرف الhandlers بتوعنا اللى اسندناهم للهاندلرز الأساسين لل self._p parser
Code: [Select]
    def __startElement(self, el, attrs): 
        print "Starting: ",el, attrs
        if el=="book":
            self._inbook=True
        self._curel=el
        self._curattrs=attrs

    def __charsDataHandler(self, data):
        if data.strip():
            if self._inbook and self._curel=="name" :
                self._books += [data]
            elif self._curel=="price" :
                self._thesum += int(data)
            else:
                pa*s

    def __endElement(self, el):
        if el=="book":
            self._inbook=False
        self._curel, self._curattrs=None,None

الإستخدام
Code: [Select]
if __name__=="__main__": 
    p=ParsingHandler(xmldoc)
    print "Total sum: ", p.getTotalSum()
    p.printBooksInfo()

لاحظ ان ال xmldoc هو string بيعبر عن ملف ال xml اللى هيتم معالجته

ناتج التنفيذ
Total sum:  370
Introduction to Python
Introduction to Java
Introduction to Ruby
Introduction to Linux Programming

مثال حقيقى لإنشاء Weather.com APIs wrapper



HTMLing with Python

على فرض انك قرأت الفصل السابق قم بتنفيذ البرنامج التالى

--سكربت يقوم بقراءة صفحة من الإنترنت ويقوم بالحصول على جميع اللينكات فيها اعتمد على النموذج التالى
Code: [Select]
#!/usr/bin/env python 
#-*- coding:utf-8 -*-

from HTMLParser import HTMLParser as HP
import urllib2 as ulib
import sys

def fetchdatafrom(url):
    return ulib.urlopen(url).read()

def as_unicode(data):
    return data.decode("cp1256").encode("utf-8")
 
   
class PageParser(HP):

def __init__(self):
self._ina=False
self._links=[]

links=lambda self: self._links

def handle_start_tag(self, tag, attrs):
pass

def handle_data(self, data):
pass

def handle_endtag(self, tag):
pass

def getlinks(url):

htmlsrc=fetchdatafrom(url)
p=PageParser()
p.feed(htmlsrc)
return p.links()

طريقة الإستخدام مشابهه لتلك مع SAX
حيث تعيد تعريف الطرق handle_starttag و handle_data و handle_endtag للتعامل مع وسوم ال HTML
الدالة fetchdatafrom تقوم بإعادة كود الصفحة اليك على صورة string
الدالة as_unciode تقوم بتحويل الcp1256 الى unicode (ربما اذا اردت ان تعالج الdata j تستطيع الإستفادة منها )
ال PageParser هو صف يشتق ال HTMLParser ويتم التعامل داخله مثلما تعاملنا مع الصفوف المشتقة ContentHandler
ولإطعامه السورس نستخدم الطريقة feed
الدالة getlinks تقوم بالحصول على الروابط من الطريقة links اللتى تعيد لنا الروابط اللتى تم قرائتها


Beautiful Soup
هى HTML/XML parser بايثونية وتعالج ايضا الملفات المكتوبة بطريقة سيئة ولاتجعلك تقلق من الإنكودينج
لمعالجة ملفات ال HTML استخدم الصف BeautifulSoup واذا اردت معالجة ملفات XML استخدم BeautifulStoneSoup

حل المطلوب السابق بإستخدام BeautifulSoup
Code: [Select]
#!bin/python

import BeautifulSoup as bs
import urllib2 as ulib

def fetchdatafrom(url):
    return ulib.urlopen(url).read() or "\n"

def getzetcodemain():
    return fetchdatafrom('http://zetcode.com')

soup=bs.BeautifulSoup(getzetcodemain())
for el in soup.findAll('a'):
    # [0][0] is href.
    print "[url=%s]%s[/url]"%(el.attrs[0][1], el.contents)


تنتظرك رحلة رائعة مع الوثائق الخاصة بيها
http://www.crummy.com/software/BeautifulSoup/documentation.html


استفيد من السكربتات السابق فى تنفيذ التالى
انشاء مفهرس للمنتديات يأخذ قسم معين كبداية وتقوم بتحديد عدد الصفحات المطلوبة ويقوم بفتحها واستخلاص اللينكات والعناوين لها




« Last Edit: October 04, 2009, 04:45:05 PM by Ahmed Youssef »
Logged

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