Python 项目03:万能的XML

通过 Python 对 XML 文件进行解析。

文章大纲

XML 是一种标记性语言类似于 HTML,相对于 HTML 而言它可以自定义标签。我目前对 XML 使用的不多,更多的是 YAML,但学习这个案例还是很有启发意义的,尤其是后续处理类似具有标签性格式的文件。

需要处理的 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 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>Mr. Gumby's Shouting Page</h1>
                        <p>...</p>
                </page>
                <page name="sleeping" title="Sleeping">
                        <h1>Mr. Gumby's Sleeping Page</h1>
                        <p>...</p>
                </page>
                <page name="eating" title="Eating">
                        <h1>Mr. Gumby's Eating Page</h1>
                        <p>...</p>
                </page>
        </directory>
</website>

具有多个层次结构,website page directory

对于 XML 的解析,Python 本身提供了支持,有一个 SAX 的解析器进行解析。

创建 HTML 页面

在不考虑目录的情况下,单独解析 XML 中的页面。需要完成的任务:

  • 当处理到 page 开头的,需要创建一个给定名称的文件,并写入合适的 HTML 开头内容,包含页面标题
  • 在每个 page 的结尾,将合适的 HTML 尾部写入到文件,并关闭文件
  • page 的内部,遍历所有的标签和字符不修改,直接写入到文件,但是需要判断这些内容是否在 page 里面,所以需要添加一个变量来判断,同时 HTML 的标签需要尖括号,所以需要重建它们

简单页面创建的脚本:

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>{}</title>'.format(attrs['title']))
            self.out.write('</head><body>\n')
        elif self.passthrough:
            self.out.write('<' + name)
            for key, val in attrs.items():
                self.out.write(' {}="{}"'.format(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('</{}>'.format(name))

    def characters(self, chars):
        if self.passthrough: self.out.write(chars)

parse('website.xml', PageMaker())

再次实现

以下是根据 XML 文件内容创建站点的全部实现:

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)
        os.makedirs(path, exist_ok=True)

    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(' {}="{}"'.format(key, val))
            self.out.write('>')

    def defaultEnd(self, name):
        if self.passthrough:
            self.out.write('</{}>'.format(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 <body>\n')

    def writeFooter(self):
        self.out.write('\n </body>\n</html>\n')


parse('website.xml', WebsiteConstructor('public_html'))

第一眼看过去感觉相当复杂,但其实有点第一个项目的影子。

首先 Dispatcher 类中的 dispatch 方法,它的作用是调用相关方法的,用于分派功能,不同的标签将调用不同的方法,给了一个 prefix 外加标签的名字,如果相关方法没有找到就调用默认的方法(主要是为了处理 page 内部出现的 HTML 标签,避免认为是 XML 的标签而被当作 XML 进行处理)。

同时在 Dispatcher 中重写了 ContentHandler 中的 startElementendElement 方法,来通过 dispatch 方法进行分派。

还有比较有意思的是对目录的处理,首先通过初始化将传递进来的目录放置到内部的一个类型为列表的变量中,作为根目录,一旦遇到 directory 标签,就将标签名字追加到列表中,然后使用 join 方法,将列表转换为路径进行创建。随后通过类似的方式在 directory 中根据 page 的名字创建相应的 HTML 页面,在最后遇到 /directory 时,将路径从列表中进行 pop

以上就是整个代码所做的事情,代码中实现分派器和目录的处理真的是很有启发。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理

滚动至顶部