Proj3万能的XML
这个项目介绍了如何用XML来表示多种数据,以及如何使用适合XML或SAX的简单API来处理XML文件。XML并不是一种具体的语言(例如HTML),它更类似于定义语言类的一种规则集合。我们仍然使用HTML中的方式来写标签,但在XML中,还可以自己定义标签名。
工作和目标
本项目解决的问题就是解析(读取和处理)XML文件,具体来讲是通过一个描述各种网页和目录的XML文件生成一个完整的网站。这个文件包括站点结构和每个页面的基本内容。
目标列举如下:
-整个网站应该用一个XML文件描述,其中包括独立网页和目录的信息。
-程序应该能创建所需的目录和网页
-应该可以轻松改变整个网站的设计,并且以新的设计为基础重新生成所有网页。
工具
Python有一些内建的XML支持,查看是否已有一个可以用的SAX语法分析器,执行下面语句:
from xml.sax import parse
parser = make_parser()
执行这条命令不引发任何异常即可以开始项目了。
准备工作
编写处理XML文件的程序前,需要设计XML格式。主要概念包括网站、目录、页面、名称、标题和内容。
-网站。不用存储有关网站本身的任何信息,所以网站就是包括所有文件和目录的顶级元素。
-目录。目录是文件和其他目录的容器。
-页面。一个网页。
-名称。目录和网页都需要名称–当目录和文件出现在文件系统和相应的URL中时,它们可以用作目录名和文件名。
-标题。每个网页都应该有标题(和文件名不同)。
-内容。每个网页都有一些内容。
简单来说,文档由一个包含directory和page元素的website元素组成,每个目录元素可以包括更多的页面和目录。directory和page元素有叫做name的特性,属性值是它们的名字。除此之外,page标签还有title特性。page元素包括XHTML代码。一个实例文件是Proj3万能的XML中的website.xml。如下:
<website>
<page name="index" title="Home Page">
<h1>Welcome to My Home Page</h1>
<p>Hi,there.My name is Alexis,and this is my home page.Here
are some of my interests:</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>Alexis's Shouting Page</h1>
<p>...</p>
</page>
<page name="sleeping" title="Sleeping">
<h1>Alexis's Sleeping Page</h1>
<p>...</p>
</page>
<page name="eating" title="Eating">
<h1>Alexis's Eating Page</h1>
<p>...</p>
</page>
</directory>
</website>
初次实现
创建简单的内容处理程序
在使用SAX进行解析时,有很多种事件类型可用,这里只用到3个:startElement(开始标签的匹配项)、endElement(关闭标签的匹配项)以及characters(字符)。使用xml.sax模块中的parse函数负责读取并且生成这些事件。要生成这些事件需要调用一些处理程序(content handler),xml.sax.handler中的ContentHandler类实现了这些处理程序,但函数具体工作需要用户重写。解析的具体原理可以参看这个链接(http://www.cnblogs.com/hongfei/p/python-xml-sax.html)。假如要解析一个<h1>
级别的标题,可以像下面这样编写一个类:
from xml.sax.handler import ContentHandler
from xml.sax import parse
class HeadlineHandler(ContentHandler):
in_headline = False
def __init__(self,headlines):
ContentHandler.__init__(self)
self.headlines = headlines
self.data = []
def startElement(self,name,attrs):
if name == 'h1':
self.in_headline = True
def endElement(self,name):
if name == 'h1':
text = ''.join(self.data)
self.data = []
self.headlines.append(text)
self.in_headline = False
def characters(self,string):
if self.in_headline:
self.data.append(string)
这种解析类都至少包括上面说的三种事件类型的处理程序。具体执行时可以像这样调用:
headlines = []
parse('website.xml',HeadlineHandler(headlines))
print 'The following <h1> elements were found:'
for h in headlines:
print h
打印出的结果如下图:
简单的过程就是当找到<h1>
标签时,调用startElement,开关打开。在结束标签之前都会调用characters收集字符串。当遇到结束标签</h1>
时,执行endElement函数,处理收集到的字符串,添加到存储题目的列表中。
创建HTML页面
像上一节的例子处理<h1>
标签那样那样,我们需要编写一个还能处理<page>
等其他标签的完整的HTML页面解析程序(暂时不考虑目录的解析)。这个简单版本的代码在工程文件夹下的pagemaker.py文件中。运行结果如下所示:
<html><head>
<title>Home Page</title>
</head><body>
<h1>Welcome to My Home Page</h1>
<p>Hi,there.My name is Alexis,and this is my home page.Here
are some of my interests:</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>
</body></html>
再次实现
调度程序的混入类
与其在标准的泛型事件处理程序(比如startElement)中编写大段的if语句,不如编写自己的具体程序(比如startPage),并且自动调用它们。一个提供有限功能的混入类(本文中是Dispatcher)和其他更具体的类(本文中是ContentHandler类)一起被继承,生成的新类WebsiteConstructor才是最终被parse使用的类。Dispatcher类中的方法dispatch()类似于项目1中的根据名称查找函数,该类还包括基本的startElement和endElement事件处理。
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,
#args是一个元组
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><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')
解释一下为什么要ensureDirectory。os.makedirs(‘foo/bar/baz’)函数会在当前目录中创建一个字符串中的目录,当该目录(指完整的../foo/bar/baz)本来就存在时,会引发一个异常。为了避免出现这个异常,需要使用os.path.isdir函数,它可以检查给定的路径是否是目录(也就是目录是否存在)。另外一个函数os.path.join可以使用正确的分隔符将数个路径连接起来例如:directory = [“C”, “pic”, “18x.jpg”],执行os.path.join(*directory) 会得到”C\\pic\\18x.jpg”。完整代码见website.py文件。
总结
本项目建立了一个简单的网站,非常简单,只有文本信息。要做更复杂的还需要深入学习,以后有机会再扩展一下这个程序。