问题描述:
问题来源于《Python基础教程》第三个实例,万能的xml。本项目要解决的常见问题是解析(读取和处理)XML文件。因为使用XML几乎能表示任何数据。更具体点来说,通过单个的XML文件生成一个完整的网站,这个文件包括站点结果和每个页面的基本内容。因为对于XML的不了解,所以,先在http://www.w3school.com.cn/x.asp补充了一点XML的背景知识。
首先考虑XML文件需要描述什么:
* 网站:不用存储有关网站本身的任何信息,所以,网站就是包括所有文件和目录的顶级元素。
* 目录:目录是文件和其他目录的容器。
* 页面:一个网页。
* 名称:目录和网页都需要名称--当目录和文件出现在文件系统和相应的URL中时,它们可以用作目录名和文件名。
* 标题:每个网页都应该有标题(和文件名不同)、
* 内容:每个网页都有一些内容。这里只用XHTL来表示内容--这样就能将它传递到最终的网页上,让浏览器对它进行解释。
简单来说,文档由一个包含数个directory和page元素的website元素组成,每个目录元素可以包括更多的页面和目录。Directory和page元素有叫做name的特性,属性值是他们的名字。除此之外,page标签还有title的特性。
用于测试的XML文件表示的简单网站(website.xml)
<website>
<page name="index" title="Home page">
<h1>Welcome to my Home page</h1>
<p>Hi, there. My name is Mr.gumby,and this is my home page,here are some of my int:</p>
<ul>
<li><a href="interests/shouting.html">Shouting</a></li>
<li><a href="interests/sleeping.html">Sleeping</a></li>
<li><a href="interests/eating.html">Eating</a></li>
</ul>
</page>
<directory name="interests">
<page name="shouting" title="Shouting">
<h1>shouting page</h1>
<p>....</p>
</page>
<page name="sleeping" title="Sleeping">
<h1>sleeping page</h1>
<p>...</p>
</page>
<page name="eating" title="Eating">
<h1>Eating page</h1>
<p>....</p>
</page>
</directory>
</website>
XML解析:
XML解析是如何工作的,这里所使用的方法叫做SAX,包括编写一组时间处理程序(就如同GUI程序设计),当解析器读XML文档的时候,就可以让它调用这些处理完成解析工作。
使用SAX进行解析时,有很多的事件类型可用,但是本项目只用了三个:元素的起始(开始标签的匹配项)、元素的结束(关闭标签的匹配项)以及纯文本(字符)。要解析XML文件,可以使用xml.sax模块的parse函数。这个函数负责处理程序会作为内容处理程序(content handler)对象的方法来实现。需要集成xml.asx.handler中的ContentHandler类,因为它实现了所有需求的事件处理程序(只不过是没有任何效果的未操作),可以再需要的时候覆盖这些函数。
创建HTML页面:
现在已经准备好要创建好原形了,首先忽略目录,专注于创建HTML页面。创建的压面要符合厦门的要求:
1、在每个page元素的开始处,使用给定的文件名打开一个新文件,写入合适的HTML首部,包括给定的标题。
2、在每个page元素的结尾处,写入HTML的页脚,然后关闭文件。
3、在page元素内部时,跳过所有标签和字符,不进行修改(将他们直接写入文件)。
4、不再page元素内部时,忽略所有的标签(比如说website或者directory)
大多数的功能实现起来都很简单,但是有两个问题比较不完全清楚:
1、不能简单的“穿过”标签(在建立HTML文件时直接写入文件),因为只有名字(可能还有一些特性)。需要自己重建这些标签(使用尖括号等)。
2、SAX本身无法告诉你当前是否正位于一个page元素内部。所以,需要自己注意这类事情。在这个项目中,程序支队是否穿过标签和文本感兴趣,所以使用一个叫做passthrough的布尔变量在进入和离开页面时进行更新。
初次实现代码如下:
from xml.sax.handler import ContentHandler
from xml.sax import parse
class PageMaker(ContentHandler):
passthrough = False
def startElement(self, name, attrs):
if name == 'page':
self.passthrough = True
self.out = open(attrs['name']+ '.html','w')
self.out.write('<html><head>\n')
self.out.write('<title>%s</title>' % attrs['title'])
self.out.write('</head><body>\n')
elif self.passthrough:
self.out.write('<' + name)
for key,val in attrs.items():
self.out.write(' %s="%s"' % (key, val))
self.out.write('>')
def endElement(self,name):
if name =='page':
self.passthrough = False
self.out.write('\n</body></html>\n')
self.out.close()
elif self.passthrough:
self.out.write('</%s>' % name)
def characters(self,chars):
if self.passthrough: self.out.write(chars)
parse('website.xml',PageMaker())
这段代码运行的结果会得到4个HTML文件:
Eating.html
Index.html
Shouting.html
Sleeping.html
其中index.html的截图如下:
再次实现:
因为SAX的机制比较底层且基本,同城会编写一个Mix-in类来处理手机字符数据,管理布尔状态变量(比如passthrough)或是纸牌时间到自己定义的时间中处理程序等这类管理细节。这里主要介绍程序的调度。
主要改变有三点:
1、调度程序的Mix-in类
这个类实现了以下几个功能:
A、当使用‘foo’这样的名字调用startElement时,它会试图寻找叫做startFoo的时间处理程序,然后利用给定的特性进行调用。
B、同样的,如果使用‘foo’调用endElement,那么它会试着调用ednFoo。
C、如果在这些地方找不到给定的时间处理程序,那么会分别调用defaultStart或者defaultEnd方法。如果连默认的处理程序都没有的话,那就什么都不做。
可能对于抽象编程这类东西了解不够深,对于这个感觉还是有点难以理解。按照书里面说的,所做的方法如下:
A、根据一个前缀(‘start’或‘end’)和一个标签名(比如‘page’)构造处理程序的方法名(比如‘startPage’)
B、使用同一的前缀,构造默认处理程序的名字(比如‘defaultStart’)
C、试着使用getattr获得处理程序,用None作为默认值
D、如果结果可以调用,那么讲一个空元组赋值给args
E、否则试着利用getattr获取默认处理程序,在使用None作为默认值。同样的,将args设定为只包括标签名的元组(以为内默认的处理程序需要)。
F、如果正使用一个起始处理程序,那么将属性添加到参数元组(args)中
G、如果处理程序可调用(或者是可用的具体处理程序,或者是可用的默认处理程序),那么,使用正确的参数进行调用。
2、实现首部、页脚和默认的处理程序
这个比较好理解,程序创建单独的方法用于编写首部和页脚。
3、对目录的支持
为了创建所需要的目录,需要os和os.path模块中的一些有用的函数,其中之一就是os.makedirs,它可以再给定的路径中创建所有需要的目录。比如os.makedirs(‘foo/bar/baz’)会在当前的目录中创建foo目录,然后在foo中创建bar目录,最后在bar目录中创建baz,如果foo目录已经存在,那么只会创建bar和baz,类似的,如果bar也存在的话那么只有baz会被创建。不过如果baz同样存在的话,就会引发一个异常。
为了避免出现这个异常,需要使用os.path.isdir函数,它可以检查给定的路径是否是目录(即目录是否存在)。另外的一个有用的函数是os.path.join,它可以使用正确的分隔符将数个路径连接起来。
4、事件处理程序
最后需要实现时间处理程序。需要四个对象--两个处理目录,两个处理页面。目录处理程序只有使用directory列表和ensureDirectory方法。
页面处理程序使用writeHeader和writeFooter方法,初次之外,他们还要设定passthrough变量(穿过XHTML)以及要打开和关闭的页面关联的文件。
网站构建函数如下(website,py):
from xml.sax.handler import ContentHandler
from xml.sax import parse
import os
class Dispatcher:
def dispatch(self,prefix,name,attrs = None):
mname = prefix +name.capitalize()
dname = 'default' +prefix.capitalize()
method = getattr(self,mname,None)
if callable(method): args = ()
else:
method = getattr(self,dname,None)
args = name,
if prefix == 'start':args +=attrs,
if callable(method): method(*args)
def startElement(self,name,attrs):
self.dispatch('start',name,attrs)
def endElement(self,name):
self.dispatch('end',name)
class WebsiteConstructor(Dispatcher,ContentHandler):
passthrough = False
def __init__(self,directory):
self.directory = [directory]
self.ensureDirectory()
def ensureDirectory(self):
path = os.path.join(*self.directory)
if not os.path.isdir(path): os.makedirs(path)
def characters(self,chars):
if self.passthrough: self.out.write(chars)
def defaultStart(self,name,attrs):
if self.passthrough:
self.out.write('<' + name)
for key,val in attrs.items():
self.out.write(' %s = "%s"' %(key,val))
self.out.write('>')
def defaultEnd(self,name):
if self.passthrough:
self.out.write('</%s>' % name)
def startDirectory(self,attrs):
self.directory.append(attrs['name'])
self.ensureDirectory()
def endDirectory(self):
self.directory.pop()
def startPage(self, attrs):
filename = os.path.join(*self.directory+[attrs['name']+'.html'])
self.out = open(filename,'w')
self.writeHeader(attrs['title'])
self.passthrough = True
def endPage(self):
self.passthrough = False
self.writeFooter()
self.out.close()
def writeHeader(self,title):
self.out.write('<html>\n <head>\n <title>')
self.out.write(title)
self.out.write('</title>\n </head>\n <body>\n')
def writeFooter(self):
self.out.write('\n </body>\n</html>\n')
parse('website.xml',WebsiteConstructor('public_html'))
实现结果是生成public_html/的文件夹,这个文件夹里面含有一个index.html的文件和interests的文件夹,interests里面放的是三个HTML文件,HTML的文件实现结果和之前一样。