Dive-Into-Python-中文版-三-

33 阅读37分钟

Dive Into Python 中文版(三)

第八章 HTML 处理

第八章 HTML 处理

  • 8.1. 概览
  • 8.2. sgmllib.py 介绍
  • 8.3. 从 HTML 文档中提取数据
  • 8.4. BaseHTMLProcessor.py 介绍
  • 8.5. locals 和 globals
  • 8.6. 基于 dictionary 的字符串格式化
  • 8.7. 给属性值加引号
  • 8.8. dialect.py 介绍
  • 8.9. 全部放在一起
  • 8.10. 小结

8.1. 概览

8.1. 概览

我经常在 comp.lang.python 上看到关于如下的问题: “ 怎么才能从我的 HTML 文档中列出所有的 [头|图像|链接] 呢?” “怎么才能 [分析|解释|munge] 我的 HTML 文档的文本,但是又要保留标记呢?” “怎么才能一次给我所有的 HTML 标记 [增加|删除|加引号] 属性呢?” 本章将回答所有这些问题。

下面给出一个完整的,可工作的 Python 程序,它分为两部分。第一部分,BaseHTMLProcessor.py 是一个通用工具,它可以通过扫描标记和文本块来帮助您处理 HTML 文件。第二部分,dialect.py 是一个例子,演示了如何使用 BaseHTMLProcessor.py 来转化 HTML 文档,保留文本但是去掉了标记。阅读文档字符串 (doc string) 和注释来了解将要发生事情的概况。大部分内容看上去像巫术,因为任一个这些类的方法是如何调用的不是很清楚。不要紧,所有内容都会按进度被逐步地展示出来。

例 8.1. BaseHTMLProcessor.py

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

 from sgmllib import SGMLParser
import htmlentitydefs
class BaseHTMLProcessor(SGMLParser):
    def reset(self):                       
        # extend (called by SGMLParser.__init__)
        self.pieces = []
        SGMLParser.reset(self)
    def unknown_starttag(self, tag, attrs):
        # called for each start tag
        # attrs is a list of (attr, value) tuples
        # e.g. for <pre class="screen">, tag="pre", attrs=[("class", "screen")]
        # Ideally we would like to reconstruct original tag and attributes, but
        # we may end up quoting attribute values that weren't quoted in the source
        # document, or we may change the type of quotes around the attribute value
        # (single to double quotes).
        # Note that improperly embedded non-HTML code (like client-side Javascript)
        # may be parsed incorrectly by the ancestor, causing runtime script errors.
        # All non-HTML code must be enclosed in HTML comment tags (<!-- code -->)
        # to ensure that it will pass through this parser unaltered (in handle_comment).
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
    def unknown_endtag(self, tag):         
        # called for each end tag, e.g. for </pre>, tag will be "pre"
        # Reconstruct the original end tag.
        self.pieces.append("</%(tag)s>" % locals())
    def handle_charref(self, ref):         
        # called for each character reference, e.g. for "&#160;", ref will be "160"
        # Reconstruct the original character reference.
        self.pieces.append("&#%(ref)s;" % locals())
    def handle_entityref(self, ref):       
        # called for each entity reference, e.g. for "&copy;", ref will be "copy"
        # Reconstruct the original entity reference.
        self.pieces.append("&%(ref)s" % locals())
        # standard HTML entities are closed with a semicolon; other entities are not
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")
    def handle_data(self, text):           
        # called for each block of plain text, i.e. outside of any tag and
        # not containing any character or entity references
        # Store the original text verbatim.
        self.pieces.append(text)
    def handle_comment(self, text):        
        # called for each HTML comment, e.g. <!-- insert Javascript code here -->
        # Reconstruct the original comment.
        # It is especially important that the source document enclose client-side
        # code (like Javascript) within comments so it can pass through this
        # processor undisturbed; see comments in unknown_starttag for details.
        self.pieces.append("<!--%(text)s-->" % locals())
    def handle_pi(self, text):             
        # called for each processing instruction, e.g. <?instruction>
        # Reconstruct original processing instruction.
        self.pieces.append("<?%(text)s>" % locals())
    def handle_decl(self, text):
        # called for the DOCTYPE, if present, e.g.
        # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        #     "http://www.w3.org/TR/html4/loose.dtd">
        # Reconstruct original DOCTYPE
        self.pieces.append("<!%(text)s>" % locals())
    def output(self):              
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 

例 8.2. dialect.py

 import re
from BaseHTMLProcessor import BaseHTMLProcessor
class Dialectizer(BaseHTMLProcessor):
    subs = ()
    def reset(self):
        # extend (called from __init__ in ancestor)
        # Reset all data attributes
        self.verbatim = 0
        BaseHTMLProcessor.reset(self)
    def start_pre(self, attrs):            
        # called for every <pre> tag in HTML source
        # Increment verbatim mode count, then handle tag like normal
        self.verbatim += 1                 
        self.unknown_starttag("pre", attrs)
    def end_pre(self):                     
        # called for every </pre> tag in HTML source
        # Decrement verbatim mode count
        self.unknown_endtag("pre")         
        self.verbatim -= 1                 
    def handle_data(self, text):                                        
        # override
        # called for every block of text in HTML source
        # If in verbatim mode, save text unaltered;
        # otherwise process the text with a series of substitutions
        self.pieces.append(self.verbatim and text or self.process(text))
    def process(self, text):
        # called from handle_data
        # Process text block by performing series of regular expression
        # substitutions (actual substitions are defined in descendant)
        for fromPattern, toPattern in self.subs:
            text = re.sub(fromPattern, toPattern, text)
        return text
class ChefDialectizer(Dialectizer):
    """convert HTML to Swedish Chef-speak
    based on the classic chef.x, copyright (c) 1992, 1993 John Hagerman
    """
    subs = ((r'a([nu])', r'u\1'),
            (r'A([nu])', r'U\1'),
            (r'a\B', r'e'),
            (r'A\B', r'E'),
            (r'en\b', r'ee'),
            (r'\Bew', r'oo'),
            (r'\Be\b', r'e-a'),
            (r'\be', r'i'),
            (r'\bE', r'I'),
            (r'\Bf', r'ff'),
            (r'\Bir', r'ur'),
            (r'(\w*?)i(\w*?)$', r'\1ee\2'),
            (r'\bow', r'oo'),
            (r'\bo', r'oo'),
            (r'\bO', r'Oo'),
            (r'the', r'zee'),
            (r'The', r'Zee'),
            (r'th\b', r't'),
            (r'\Btion', r'shun'),
            (r'\Bu', r'oo'),
            (r'\BU', r'Oo'),
            (r'v', r'f'),
            (r'V', r'F'),
            (r'w', r'w'),
            (r'W', r'W'),
            (r'([a-z])[.]', r'\1\.  Bork Bork Bork!'))
class FuddDialectizer(Dialectizer):
    """convert HTML to Elmer Fudd-speak"""
    subs = ((r'[rl]', r'w'),
            (r'qu', r'qw'),
            (r'th\b', r'f'),
            (r'th', r'd'),
            (r'n[.]', r'n, uh-hah-hah-hah.'))
class OldeDialectizer(Dialectizer):
    """convert HTML to mock Middle English"""
    subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
            (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
            (r'ick\b', r'yk'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'eea', r'e\1e'),
            (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
            (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
            (r'([aeiou])re\b', r'\1r'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
            (r'tion\b', r'cioun'),
            (r'ion\b', r'ioun'),
            (r'aid', r'ayde'),
            (r'ai', r'ey'),
            (r'ay\b', r'y'),
            (r'ay', r'ey'),
            (r'ant', r'aunt'),
            (r'ea', r'ee'),
            (r'oa', r'oo'),
            (r'ue', r'e'),
            (r'oe', r'o'),
            (r'ou', r'ow'),
            (r'ow', r'ou'),
            (r'\bhe', r'hi'),
            (r've\b', r'veth'),
            (r'se\b', r'e'),
            (r"'s\b", r'es'),
            (r'ic\b', r'ick'),
            (r'ics\b', r'icc'),
            (r'ical\b', r'ick'),
            (r'tle\b', r'til'),
            (r'll\b', r'l'),
            (r'ould\b', r'olde'),
            (r'own\b', r'oune'),
            (r'un\b', r'onne'),
            (r'rry\b', r'rye'),
            (r'est\b', r'este'),
            (r'pt\b', r'pte'),
            (r'th\b', r'the'),
            (r'ch\b', r'che'),
            (r'ss\b', r'sse'),
            (r'([wybdp])\b', r'\1e'),
            (r'([rnt])\b', r'\1\1e'),
            (r'from', r'fro'),
            (r'when', r'whan'))
def translate(url, dialectName="chef"):
    """fetch URL and translate using dialect
    dialect in ("chef", "fudd", "olde")"""
    import urllib                      
    sock = urllib.urlopen(url)         
    htmlSource = sock.read()           
    sock.close()                       
    parserName = "%sDialectizer" % dialectName.capitalize()
    parserClass = globals()[parserName]                    
    parser = parserClass()                                 
    parser.feed(htmlSource)
    parser.close()         
    return parser.output() 
def test(url):
    """test all dialects against URL"""
    for dialect in ("chef", "fudd", "olde"):
        outfile = "%s.html" % dialect
        fsock = open(outfile, "wb")
        fsock.write(translate(url, dialect))
        fsock.close()
        import webbrowser
        webbrowser.open_new(outfile)
if __name__ == "__main__":
    test("http://diveintopython.org/odbchelper_list.html") 

例 8.3. dialect.py 的输出结果

运行这个脚本会将 第 3.2 节 “List 介绍” 转换成模仿瑞典厨师用语 (mock Swedish Chef-speak) (来自 The Muppets)、模仿埃尔默唠叨者用语 (mock Elmer Fudd-speak) (来自 Bugs Bunny 卡通画) 和模仿中世纪英语 (mock Middle English) (零散地来源于乔叟的*《坎特伯雷故事集》*)。如果您查看输出页面的 HTML 源代码,您会发现所有的 HTML 标记和属性没有改动,但是在标记之间的文本被转换成模仿语言了。如果您观查得更仔细些,您会发现,实际上,仅有标题和段落被转换了;代码列表和屏幕例子没有改动。

<div class="abstract">
<p>Lists awe <span class="application">Pydon</span>'s wowkhowse datatype.
If youw onwy expewience wif wists is awways in
<span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe
in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow
<span class="application">Pydon</span> wists.</p>
</div> 

8.2. sgmllib.py 介绍

8.2. sgmllib.py 介绍

HTML 处理分成三步:将 HTML 分解成它的组成片段,对片段进行加工,接着将片段再重新合成 HTML。第一步是通过 sgmllib.py 来完成的,它是标准 Python 库的一部分。

理解本章的关键是要知道 HTML 不只是文本,更是结构化文本。这种结构来源于开始与结束标记的或多或少分级序列。通常您并不以这种方式处理 HTML ,而是以文本方式 在一个文本编辑中对其进行处理,或以可视的方式 在一个浏览器中进行浏览或页面编辑工具中进行编辑。sgmllib.py 表现出了 HTML 的结构

sgmllib.py 包含一个重要的类:SGMLParserSGMLParser 将 HTML 分解成有用的片段,比如开始标记和结束标记。在它成功地分解出某个数据为一个有用的片段后,它会根据所发现的数据,调用一个自身内部的方法。为了使用这个分析器,您需要子类化 SGMLParser 类,并且覆盖这些方法。这就是当我说它表示了 HTML 结构 的意思:HTML 的结构决定了方法调用的次序和传给每个方法的参数。

SGMLParser 将 HTML 分析成 8 类数据,然后对每一类调用单独的方法:

开始标记 (Start tag)

是开始一个块的 HTML 标记,像 &lt;html&gt;&lt;head&gt;&lt;body&gt;&lt;pre&gt; 等,或是一个独一的标记,像 &lt;br&gt;&lt;img&gt; 等。当它找到一个开始标记 tagnameSGMLParser 将查找名为 start__tagname_do__tagname_ 的方法。例如,当它找到一个 &lt;pre&gt; 标记,它将查找一个 start_predo_pre 的方法。如果找到了,SGMLParser 会使用这个标记的属性列表来调用这个方法;否则,它用这个标记的名字和属性列表来调用 unknown_starttag 方法。

结束标记 (End tag)

是结束一个块的 HTML 标记,像 &lt;/html&gt;&lt;/head&gt;&lt;/body&gt;&lt;/pre&gt; 等。当找到一个结束标记时,SGMLParser 将查找名为 end__tagname_ 的方法。如果找到,SGMLParser 调用这个方法,否则它使用标记的名字来调用 unknown_endtag

字符引用 (Character reference)

用字符的十进制或等同的十六进制来表示的转义字符,像 &#160;。当找到,SGMLParser 使用十进制或等同的十六进制字符文本来调用 handle_charref

实体引用 (Entity reference)

HTML 实体,像 &copy;。当找到,SGMLParser 使用 HTML 实体的名字来调用 handle_entityref

注释 (Comment)

HTML 注释,包括在 &lt;!-- ... --&gt;之间。当找到,SGMLParser 用注释内容来调用 handle_comment

处理指令 (Processing instruction)

HTML 处理指令,包括在 &lt;? ... &gt; 之间。当找到,SGMLParser 用处理指令内容来调用 handle_pi

声明 (Declaration)

HTML 声明,如 DOCTYPE,包括在 &lt;! ... &gt;之间。当找到,SGMLParser 用声明内容来调用 handle_decl

文本数据 (Text data)

文本块。不满足其它 7 种类别的任何东西。当找到,SGMLParser 用文本来调用 handle_data

重要 Python 2.0 存在一个 bug,即 SGMLParser 完全不能识别声明 (handle_decl 永远不会调用),这就意味着 DOCTYPE 被静静地忽略掉了。这个错误在 Python 2.1 中改正了。

sgmllib.py 所附带的一个测试套件举例说明了这一点。您可以运行 sgmllib.py,在命令行下传入一个 HTML 文件的名字,然后它会在分析标记和其它元素的同时将它们打印出来。它的实现是通过子类化 SGMLParser 类,然后定义 unknown_starttagunknown_endtaghandle_data 和其它方法来实现的。这些方法简单地打印出它们的参数。

提示 在 Windows 下的 ActivePython IDE 中,您可以在 “Run script” 对话框中指定命令行参数。用空格将多个参数分开。

例 8.4. sgmllib.py 的样例测试

下面是一个片段,来自本书的 HTML 版本的目录,toc.html。当然,您的存储路径可能与我的有所不同。 (如果您还没有下载本书的 HTML 版本,可以从 diveintopython.org/ 下载。

c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html"
 <!DOCTYPE html
  PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
      <title>Dive Into Python</title>
      <link rel="stylesheet" href="diveintopython.css" type="text/css">
... 略 ... 

通过 sgmllib.py 的测试套件来运行它,会得到如下的输出结果:

c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html"
data: '\n\n'
start tag: <html lang="en" >
data: '\n   '
start tag: <head>
data: '\n      '
start tag: <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" >
data: '\n   \n      '
start tag: <title>
data: 'Dive Into Python'
end tag: </title>
data: '\n      '
start tag: <link rel="stylesheet" href="diveintopython.css" type="text/css" >
data: '\n      '
... 略 ... 

下面是本章其它部分的路标:

  • 子类化 SGMLParser 来创建从 HTML 文档中抽取感兴趣的数据的类。
  • 子类化 SGMLParser 来创建 BaseHTMLProcessor,它覆盖了所有 8 个处理方法,然后使用它们从片段中重建原始的 HTML。
  • 子类化 BaseHTMLProcessor 来创建 Dialectizer,它增加了一些方法,专门用来处理指定的 HTML 标记,然后覆盖了 handle_data 方法,提供了用来处理 HTML 标记之间文本块的框架。
  • 子类化 Dialectizer 来创建定义了文本处理规则的类。这些规则被 Dialectizer.handle_data 使用。
  • 编写一个测试套件,它可以从 http://diveintopython.org/ 处抓取一个真正的 web 页面,然后处理它。

继续阅读本章,您还可以学习到有关 localsglobals 和基于 dictionary 的字符串格式化的内容。

8.3. 从 HTML 文档中提取数据

8.3. 从 HTML 文档中提取数据

为了从 HTML 文档中提取数据,将 SGMLParser 类进行子类化,然后对想要捕捉的标记或实体定义方法。

从 HTML 文档中提取数据的第一步是得到某个 HTML 文件。如果在您的硬盘里存放着 HTML 文件,您可以使用处理文件的函数将它读出来,但是真正有意思的是从实际的网页得到 HTML。

例 8.5. urllib 介绍

>>> import urllib                                       
>>> sock = urllib.urlopen("http://diveintopython.org/") 
>>> htmlSource = sock.read()                            
>>> sock.close()                                        
>>> print htmlSource                                    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head>
      <meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'>
   <title>Dive Into Python</title>
<link rel='stylesheet' href='diveintopython.css' type='text/css'>
<link rev='made' href='mailto:mark@diveintopython.org'>
<meta name='keywords' content='Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free'>
<meta name='description' content='a free Python tutorial for experienced programmers'>
</head>
<body bgcolor='white' text='black' link='#0000FF' vlink='#840084' alink='#0000FF'>
<table cellpadding='0' cellspacing='0' border='0' width='100%'>
<tr><td class='header' width='1%' valign='top'>diveintopython.org</td>
<td width='99%' align='right'><hr size='1' noshade></td></tr>
<tr><td class='tagline' colspan='2'>Python&nbsp;for&nbsp;experienced&nbsp;programmers</td></tr>
[...略...] 
[1]urllib 模块是标准 Python 库的一部分。它包含了一些函数,可以从基于互联网的 URL (主要指网页) 来获取信息并且真正取回数据。
[2]urllib 模块最简单的使用是提取用 urlopen 函数取回的网页的整个文本。打开一个 URL 同打开一个文件相似。urlopen 的返回值是像文件一样的对象,它具有一个文件对象一样的方法。
[3]使用由 urlopen 所返回的类文件对象所能做的最简单的事情就是 read,它可以将网页的整个 HTML 读到一个字符串中。这个对象也支持 readlines 方法,这个方法可以将文本按行放入一个列表中。
[4]当用完这个对象,要确保将它 close,就如同一个普通的文件对象。
[5]现在我们将 http://diveintopython.org/ 主页的完整的 HTML 保存在一个字符串中了,接着我们将分析它。

例 8.6. urllister.py 介绍

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

 from sgmllib import SGMLParser
class URLLister(SGMLParser):
    def reset(self):                              
        SGMLParser.reset(self)
        self.urls = []
    def start_a(self, attrs):                     
        href = [v for k, v in attrs if k=='href']  
        if href:
            self.urls.extend(href) 
[1]resetSGMLParser__init__ 方法来调用,也可以在创建一个分析器实例时手工来调用。所以如果您需要做初始化,在 reset 中去做,而不要在 __init__ 中做。这样当某人重用一个分析器实例时,可以正确地重新初始化。
[2]只要找到一个 &lt;a&gt; 标记,start_a 就会由 SGMLParser 进行调用。这个标记可以包含一个 href 属性,或者包含其它的属性,如 nametitleattrs 参数是一个 tuple 的 list,[(_attribute_, _value_), (_attribute_, _value_), ...]。或者它可以只是一个有效的 HTML 标记 &lt;a&gt; (尽管无用),这时 attrs 将是个空 list。
[3]我们可以通过一个简单的多变量 list 映射来查找这个 &lt;a&gt; 标记是否拥有一个 href 属性。
[4]k=='href' 的字符串比较是区分大小写的,但是这里是安全的。因为 SGMLParser 会在创建 attrs 时将属性名转化为小写。

例 8.7. 使用 urllister.py

>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read())         
>>> usock.close()                     
>>> parser.close()                    
>>> for url in parser.urls: print url 
toc/index.html
#download
#languages
toc/index.html
appendix/history.html
download/diveintopython-html-5.0.zip
download/diveintopython-pdf-5.0.zip
download/diveintopython-word-5.0.zip
download/diveintopython-text-5.0.zip
download/diveintopython-html-flat-5.0.zip
download/diveintopython-xml-5.0.zip
download/diveintopython-common-5.0.zip 
...略... 
[1]调用定义在 SGMLParser 中的 feed 方法,将 HTML 内容放入分析器中。 [4] 这个方法接收一个字符串,这个字符串就是 usock.read() 所返回的。
[2]像处理文件一样,一旦处理完毕,您应该 close 您的 URL 对象。
[3]您也应该 close 您的分析器对象,但出于不同的原因。feed 方法不保证对传给它的全部 HTML 进行处理,它可能会对其进行缓冲处理,等待接收更多的内容。只要没有更多的内容,就应调用 close 来刷新缓冲区,并且强制所有内容被完全处理。
[4]一旦分析器被 close,分析过程也就结束了。parser.urls 中包含了在 HTML 文档中所有的链接 URL。(如果当您读到此处发现输出结果不一样,那是因为下载了本书的更新版本。)

Footnotes

[4] 像 SGMLParser 这样的分析器,技术术语叫做消费者 (consumer)。它消费 HTML,并且拆分它。也许因此就选择了 feed 这个名字,以便同消费者 这个主题相适应。就个人来说,它让我想象在动物园看展览。里面有一个黑漆漆的兽穴,没有树,没有植物,没有任何生命的迹象。但只要您非常安静地站着,尽可能靠近着瞧,您会看到在远处的角落里有两只明眸在盯着您。但是您会安慰自已那不过是心理作用。唯一知道兽穴里并不是空无一物的方法,就是在栅栏上有一个不明显的标记,上面写着 “禁止给分析器喂食”。但也许只有我这么想,不管怎么样,这种心理想象很有意思。

8.4. BaseHTMLProcessor.py 介绍

8.4. BaseHTMLProcessor.py 介绍

SGMLParser 自身不会产生任何结果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用相应的一个方法,但是这些方法什么都不做。SGMLParser 是一个 HTML 消费者 (consumer):它接收 HTML,将其分解成小的、结构化的小块。正如您所看到的,在前一节中,您可以定义 SGMLParser 的子类,它可以捕捉特别标记和生成有用的东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser 所丢出来的所有东西的一个类,接着重建整个 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者 (producer)

BaseHTMLProcessor 子类化 SGMLParser,并且提供了全部的 8 个处理方法:unknown_starttagunknown_endtaghandle_charrefhandle_entityrefhandle_commenthandle_pihandle_declhandle_data

例 8.8. BaseHTMLProcessor 介绍

 class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        
        self.pieces = []
        SGMLParser.reset(self)
    def unknown_starttag(self, tag, attrs): 
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
    def unknown_endtag(self, tag):          
        self.pieces.append("</%(tag)s>" % locals())
    def handle_charref(self, ref):          
        self.pieces.append("&#%(ref)s;" % locals())
    def handle_entityref(self, ref):        
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")
    def handle_data(self, text):            
        self.pieces.append(text)
    def handle_comment(self, text):         
        self.pieces.append("<!--%(text)s-->" % locals())
    def handle_pi(self, text):              
        self.pieces.append("<?%(text)s>" % locals())
    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals()) 
[1]resetSGMLParser.__init__ 来调用。在调用父类方法之前将 self.pieces 初始化为空列表。self.pieces 是一个数据属性,将用来保存将要构造的 HTML 文档的片段。每个处理器方法都将重构 SGMLParser 所分析出来的 HTML,并且每个方法将生成的字符串追加到 self.pieces 之后。注意,self.pieces 是一个 list。也许您想将它定义为一个字符串,然后不停地将每个片段追加到它的后面。这样做是可以的,但是 Python 在处理 list 方面效率更高一些。 [5]
[2]因为 BaseHTMLProcessor 没有为特别标记定义方法 (如在 URLLister 中的start_a 方法), SGMLParser 将对每一个开始标记调用 unknown_starttag 方法。这个方法接收标记 (tag) 和属性的名字/值对的 list(attrs) 两参数,重新构造初始的 HTML,接着将结果追加到 self.pieces 后。 这里的字符串格式化有些陌生,我们将留到下一节再说明。
[3]重构结束标记要简单得多,只是使用标记名字,把它包在 &lt;/...&gt; 括号中。
[4]SGMLParser 找到一个字符引用时,会用原始的引用来调用 handle_charref。如果 HTML 文档包含 &#160; 这个引用,ref 将为 160。重构原始的完整的字符引用只要将 ref 包装在 &#...; 字符中间。
[5]实体引用同字符引用相似,但是没有#号。重建原始的实体引用只要将 ref 包装在 &...; 字符串中间。(实际上,一位博学的读者曾经向我指出,除些之外还稍微有些复杂。仅有某种标准的 HTML 实体以一个分号结束;其它看上去差不多的实体并不如此。幸运的是,标准 HTML 实体集已经定义在 Python 的一个叫做 htmlentitydefs 的模块中了。从而引出额外的 if 语句。)
[6]文本块则简单地不经修改地追加到 self.pieces 后。
[7]HTML 注释包装在 &lt;!--...--&gt; 字符中。
[8]处理指令包装在 &lt;?...&gt; 字符中。

重要 HTML 规范要求所有非 HTML (像客户端的 JavaScript) 必须包括在 HTML 注释中,但不是所有的页面都是这么做的 (而且所有的最新的浏览器也都容许不这样做) 。BaseHTMLProcessor 不允许这样,如果脚本嵌入得不正确,它将被当作 HTML 一样进行分析。例如,如果脚本包含了小于和等于号,SGMLParser 可能会错误地认为找到了标记和属性。SGMLParser 总是把标记名和属性名转换成小写,这样可能破坏了脚本,并且 BaseHTMLProcessor 总是用双引号来将属性封闭起来 (尽管原始的 HTML 文档可能使用单引号或没有引号) ,这样必然会破坏脚本。应该总是将您的客户端脚本放在 HTML 注释中进行保护。

例 8.9. BaseHTMLProcessor 输出结果

 def output(self):               
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 
[1]这是在 BaseHTMLProcessor 中的一个方法,它永远不会被父类 SGMLParser 所调用。因为其它的处理器方法将它们重构的 HTML 保存在 self.pieces 中,这个函数需要将所有这些片段连接成一个字符串。正如前面提到的,Python 在处理列表方面非常出色,但对于字符串处理就逊色了。所以我们只有在某人确实需要它时才创建完整的字符串。
[2]如果您愿意,也可以换成使用 string 模块的 join 方法:string.join(self.pieces, "")

进一步阅读

Footnotes

[5] Python 处理 list 比字符串快的原因是:list 是可变的,但字符串是不可变的。这就是说向 list 进行追加只是增加元素和修改索引。因为字符串在创建之后不能被修改,像 s = s + newpiece 这样的代码将会从原值和新片段的连接结果中创建一个全新的字符串,然后丢弃原来的字符串。这样就需要大量昂贵的内存管理,并且随着字符串变长,所需要的开销也在增长。所以在一个循环中执行 s = s + newpiece 非常不好。用技术术语来说,向一个 list 追加 n 个项的代价为 O(n),而向一个字符串追加 n 个项的代价是 O(n&lt;sup&gt;2&lt;/sup&gt;)

8.5. localsglobals

8.5. localsglobals

我们先偏离一下 HTML 处理的主题,讨论一下 Python 如何处理变量。Python 有两个内置的函数,localsglobals,它们提供了基于 dictionary 的访问局部和全局变量的方式。

还记得 locals 吗?您第一次是在这里看到的:

 def unknown_starttag(self, tag, attrs):
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) 

不,等等,此时您还不能理解 locals 。首先,您需要学习关于命名空间的知识。这很枯燥,但是很重要,因此要要耐心些。

Python 使用叫做名字空间的东西来记录变量的轨迹。名字空间只是一个 dictionary ,它的键字就是变量名,它的值就是那些变量的值。实际上,名字空间可以像 Python 的 dictionary 一样进行访问,一会儿我们就会看到。

在一个 Python 程序中的任何一个地方,都存在几个可用的名字空间。每个函数都有着自已的名字空间,叫做局部名字空间,它记录了函数的变量,包括函数的参数和局部定义的变量。每个模块拥有它自已的名字空间,叫做全局名字空间,它记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。还有就是内置名字空间,任何模块均可访问它,它存放着内置的函数和异常。

当一行代码要使用变量 x 的值时,Python 会到所有可用的名字空间去查找变量,按照如下顺序:

  1. 局部名字空间――特指当前函数或类的方法。如果函数定义了一个局部变量 x,或一个参数 x,Python 将使用它,然后停止搜索。
  2. 全局名字空间――特指当前的模块。如果模块定义了一个名为 x 的变量,函数或类,Python 将使用它然后停止搜索。
  3. 内置名字空间――对每个模块都是全局的。作为最后的尝试,Python 将假设 x 是内置函数或变量。

如果 Python 在这些名字空间找不到 x,它将放弃查找并引发一个 NameError 异常,同时传递 There is no variable named 'x' 这样一条信息,回到 例 3.18 “引用未赋值的变量”,您会看到一路上都有这样的信息。但是您并没有体会到 Python 在给出这样的错误之前做了多少的努力。

重要 Python 2.2 引入了一种略有不同但重要的改变,它会影响名字空间的搜索顺序:嵌套的作用域。 在 Python 2.2 版本之前,当您在一个嵌套函数或 lambda 函数中引用一个变量时,Python 会在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后在模块的名字空间。Python 2.2 将只在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后是在父函数的名字空间 中搜索,接着是模块的名字空间中搜索。Python 2.1 可 以两种方式工作,缺省地,按 Python 2.0 的方式工作。但是您可以把下面一行代码增加到您的模块头部,使您的模块工作起来像 Python 2.2 的方式:

from __future__ import nested_scopes 

您是否为此而感到困惑?不要灰心!我敢说这一点非常酷。像 Python 中的许多事情一样,名字空间在运行时直接可以访问。怎么样?不错吧,局部名字空间可以通过内置的 locals 函数来访问。全局 (模块级别) 名字空间可以通过内置的 globals 函数来访问。

例 8.10. locals 介绍

>>> def foo(arg): 
... x = 1
... print locals()
... 
>>> foo(7)        
{'arg': 7, 'x': 1}
>>> foo('bar')    
{'arg': 'bar', 'x': 1} 
[1]函数 foo 在它的局部名字空间中有两个变量:arg (它的值是被传入函数的) 和 x (它是在函数里定义的)。
[2]locals 返回一个名字/值对的 dictionary。这个 dictionary 的键字是字符串形式的变量名字,dictionary 的值是变量的实际值。所以用 7 来调用 foo,会打印出包含函数两个局部变量的 dictionary:arg (7) 和 x (1)。
[3]回想一下,Python 有动态数据类型,所以您可以非常容易地传递给 arg 一个字符串,这个函数 (和对 locals 的调用) 将仍然很好的工作。locals 可以用于所有类型的变量。

locals 对局部 (函数) 名字空间做了些什么,globals 就对全局 (模块) 名字空间做了什么。然而 globals 更令人兴奋,因为一个模块的名字空间是更令人兴奋的。[6] 模块的名字空间不仅仅包含了模块级的变量和常量,还包括了所有在模块中定义的函数和类。除此以外,它还包括了任何被导入到模块中的东西。

回想一下 from _module_ importimport _module_ 之间的不同。使用 import _module_,模块自身被导入,但是它保持着自已的名字空间,这就是为什么您需要使用模块名来访问它的函数或属性:_module_._function_ 的原因。但是使用 from _module_ import,实际上是从另一个模块中将指定的函数和属性导入到您自己的名字空间,这就是为什么您可以直接访问它们却不需要引用它们所来源的模块。使用 globals 函数,您会真切地看到这一切的发生。

例 8.11. globals 介绍

看看下面列出的在文件 BaseHTMLProcessor.py 尾部的代码块:

 if __name__ == "__main__":
    for k, v in globals().items():             
        print k, "=", v 
[1]不要被吓坏了,想想以前您已经全部都看到过了。globals 函数返回一个 dictionary,我们使用 items 方法和多变量赋值来遍历 dictionary。在这里唯一的新东西就是 globals 函数。

现在从命令行运行这个脚本,会得到下面的输出 (注意您的输出可能有略微的不同,这依赖于您的系统平台和所安装的 Python 版本):

c:\docbook\dip\py> python BaseHTMLProcessor.py 
SGMLParser = sgmllib.SGMLParser                
htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> 
BaseHTMLProcessor = __main__.BaseHTMLProcessor 
__name__ = __main__                            
... rest of output omitted for brevity... 
[1]我们使用了 from _module_ importSGMLParsersgmllib 中导入。也就是说它被直接导入到我们的模块名字空间了,就是这样。
[2]把上面的例子和 htmlentitydefs 对比一下,它是用 import 被导入的。也就是说 htmlentitydefs 模块本身被导入了名字空间,但是定义在 htmlentitydefs 之中的 entitydefs 变量却没有。
[3]这个模块只定义一个类,BaseHTMLProcessor,不错。注意这儿的值就是类本身,不是一个特别的类实例。
[4]记得 if __name__ 技巧吗?当运行一个模块时 (相对于从另外一个模块中导入而言),内置的 __name__ 是一个特殊值 __main__。因为我们是把这个模块当作脚本从命令来运行的,故 __name__ 值为 __main__,这就是为什么我们这段简单地打印 globals 的代码可以执行的原因。

注意 使用 localsglobals 函数,通过提供变量的字符串名字您可以动态地得到任何变量的值。这种方法提供了这样的功能:getattr 函数允许您通过提供函数的字符串名来动态地访问任意的函数。

localsglobals 之间有另外一个重要的区别,您应该在它困扰您之前就了解它。它无论如何都会困扰您的,但至少您还会记得曾经学习过它。

例 8.12. locals 是只读的,globals 不是

 def foo(arg):
    x = 1
    print locals()    
    locals()["x"] = 2 
    print "x=",x      
z = 7
print "z=",z
foo(3)
globals()["z"] = 8     print "z=",z 
[1]因为使用 3 来调用 foo,会打印出 {'arg': 3, 'x': 1}。这个应该没什么奇怪的。
[2]locals 是一个返回 dictionary 的函数,这里您在 dictionary 中设置了一个值。您可能认为这样会改变局部变量 x 的值为 2,但并不会。locals 实际上没有返回局部名字空间,它返回的是一个拷贝。所以对它进行改变对局部名字空间中的变量值并无影响。
[3]这样会打印出 x= 1,而不是 x= 2
[4]在有了对 locals 的经验之后,您可能认为这样不会 改变 z 的值,但是可以。由于 Python 在实现过程中内部有所区别 (关于这些区别我宁可不去研究,因为我自已还没有完全理解) ,globals 返回实际的全局名字空间,而不是一个拷贝:与 locals 的行为完全相反。所以对 globals 所返回的 dictionary 的任何的改动都会直接影响到全局变量。
[5]这样会打印出 z= 8,而不是 z= 7

Footnotes

[6] 我没有说得太多吧。

8.6. 基于 dictionary 的字符串格式化

8.6. 基于 dictionary 的字符串格式化

为什么学习 localsglobals?因为接下来就可以学习关于基于 dictionary 的字符串格式化。或许您还能记起,字符串格式化提供了一种将值插入字符串中的一种便捷的方法。值被列在一个 tuple 中,按照顺序插入到字符串中每个格式化标记所在的位置上。尽管这种做法效率高,但还不是最容易阅读的代码,特别是当插入多个值的时候。仅用眼看一遍字符串,您不能马上就明白结果是什么;您需要经常地在字符串和值的 tuple 之间进行反复查看。

有另外一种字符串格式化的形式,它使用 dictionary 而不是值的 tuple。

例 8.13. 基于 dictionary 的字符串格式化介绍

>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> "%(pwd)s" % params                                    
'secret'
>>> "%(pwd)s is not a good password for %(uid)s" % params 
'secret is not a good password for sa'
>>> "%(database)s of mind, %(database)s of body" % params 
'master of mind, master of body' 
[1]这种字符串格式化形式不用显式的值的 tuple,而是使用一个 dictionary,params。并且标记也不是在字符串中的一个简单 %s,而是包含了一个用括号包围起来的名字。这个名字是 params dictionary 中的一个键字,所以 %(pwd)s 标记被替换成相应的值 secret
[2]基于 dictionary 的字符串格式化可用于任意数量的有名的键字。每个键字必须在一个给定的 dictionary 中存在,否则这个格式化操作将失败并引发一个 KeyError 的异常。
[3]您甚至可以两次指定同一键字,每个键字出现之处将被同一个值所替换。

那么为什么您偏要使用基于 dictionary 的字符串格式化呢?的确,仅为了进行字符串格式化,就事先创建一个有键字和值的 dictionary 看上去的确有些小题大作。它的真正最大用处是当您碰巧已经有了像 locals 一样的有意义的键字和值的 dictionary 的时候。

例 8.14. BaseHTMLProcessor.py 中的基于 dictionary 的字符串格式化

 def handle_comment(self, text):        
        self.pieces.append("<!--%(text)s-->" % locals()) 
[1]使用内置的 locals 函数是最普通的基于 dictionary 的字符串格式化的应用。这就是说您可以在您的字符串 (本例中是 text,它作为一个参数传递给类方法) 中使用局部变量的名字,并且每个命名的变量将会被它的值替换。如果 text'Begin page footer',字符串格式化 "&lt;!--%(text)s--&gt;" % locals() 将得到字符串 '&lt;!--Begin page footer--&gt;'

例 8.15. 基于 dictionary 的字符串格式化的更多内容

 def unknown_starttag(self, tag, attrs):
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) 
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) 
[1]当这个模块被调用时,attrs 是一个键/值 tuple 的 list,就像一个 dictionary 的 items。这就意味着我们可以使用多变量赋值来遍历它。到现在这将是一种熟悉的模式,但是这里有很多东西,让我们分开来看:1. 假设 attrs[('href', 'index.html'), ('title', 'Go to home page')]。2. 在这个列表解析的第一轮循环中,key 将为 'href'value 将为 'index.html'。3. 字符串格式化 ' %s="%s"' % (key, value) 将生成 ' href="index.html"'。这个字符串就作为这个列表解析返回值的第一个元素。4. 在第二轮中,key 将为 'title'value 将为 'Go to home page'。5. 字符串格式化将生成 ' title="Go to home page"'。6. 这个 list 理解返回两个生成的字符串 list,并且 strattrs 将把这个 list 的两个元素连接在一起形成 ' href="index.html" title="Go to home page"'
[2]现在,使用基于 dictionary 的字符串格式化,我们将 tagstrattrs 的值插入到一个字符串中。所以,如果 tag'a',最终的结果会是 '&lt;a href="index.html" title="Go to home page"&gt;',并且这就是追加到 self.pieces 后面的东西。

重要 使用 locals 来应用基于 dictionary 的字符串格式化是一种方便的作法,它可以使复杂的字符串格式化表达式更易读。但它需要花费一定的代价。在调用 locals 方面有一点性能上的问题,这是由于 locals 创建了局部名字空间的一个拷贝引起的。

8.7. 给属性值加引号

8.7. 给属性值加引号

comp.lang.python 上的一个常见问题是 “我有一些 HTML 文档,属性值没有用引号括起来,并且我想将它们全部括起来,我怎么才能实现它呢?” [7] (一般这种事情的出现是由于一个项目经理加入到一个大的项目中来,而他又抱着 HTML 是一种标记语言的教条,要求所有的页面必须能够通过 HTML 校验器的验证。而属性值没有被引号括起来是一种常见的对 HTML 规范的违反。) 不管什么原因,未括起来的属性值通过将 HTML 送进 BaseHTMLProcessor 可以容易地修复。

BaseHTMLProcessor 消费 (consume) HTML (因为它是从 SGMLParser 派生来的) 并生成等价的 HTML。但是这个 HTML 输出与输入的并不一样。标记和属性名最终会转化为小写字母,即使它们可能以大写字母开始或是大小写的混合形式。属性值将被双引号引起来,即使它们原来可能是用单引号括起来的或根本没有括起来。这就是最后我们可以受益的边际效应。

例 8.16. 给属性值加引号

>>> htmlSource = """        
... <html>
... <head>
... <title>Test page</title>
... </head>
... <body>
... <ul>
... <li><a href=index.html>Home</a></li>
... <li><a href=toc.html>Table of contents</a></li>
... <li><a href=history.html>Revision history</a></li>
... </body>
... </html>
... """
>>> from BaseHTMLProcessor import BaseHTMLProcessor
>>> parser = BaseHTMLProcessor()
>>> parser.feed(htmlSource) 
>>> print parser.output()   
<html>
<head>
<title>Test page</title>
</head>
<body>
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="toc.html">Table of contents</a></li>
<li><a href="history.html">Revision history</a></li>
</body>
</html> 
[1]请注意,在 &lt;a&gt; 标记中的 href 属性值没有被适当地括起来 (还要注意,除了文档字符串之外,我们还将三重引号用到了 doc string 之外的其它地方,并且是不会少于直接在 IDE 中的使用。它们非常有用。)
[2]装填分析器。
[3]使用定义在 BaseHTMLProcessor 中的 output 函数,我们得到单个字符串的输出,并且属性值被完全括起来了。让我们想一下这里实际上发生了多少事:SGMLParser 分析整个 HTML 文档,将其分解为一片片的标记、引用、数据等等。BaseHTMLProcessor 使用这些元素来重新构造 HTML 的片段 (如果您想查看的话它们仍然保存在 parser.pieces 中) 。最后,我们调用 parser.output,它将所有的 HTML 片段连接成一个字符串。

Footnotes

[7] 好吧,其实并不是那么普通的一个问题。在那不都是问 “我应该用何种编辑器来写 Python 代码?” (回答:Emacs) 或 “Python 比 Perl 是好还是坏?” (回答:“Perl 比 Python 差,因为人们想让它差的。” ――Larry Wall,1998 年 10 月 14 日) 但是关于 HTML 处理的问题,或者这种提法或者另一种提法,大约一个月就要出现一次,在这些问题之中,这个问题是最常见的一个。

8.8. dialect.py 介绍

8.8. dialect.py 介绍

DialectizerBaseHTMLProcessor 的简单 (和拙劣) 的派生类。它通过一系列的替换对文本块进行了处理,但是它确保在 &lt;pre&gt;...&lt;/pre&gt; 块之间的任何东西不被修改地通过。

为了处理 &lt;pre&gt; 块,我们在 Dialectizer 中定义了两个方法:start_preend_pre

例 8.17. 处理特别标记

 def start_pre(self, attrs):             
        self.verbatim += 1                  
        self.unknown_starttag("pre", attrs) 
    def end_pre(self):                      
        self.unknown_endtag("pre")          
        self.verbatim -= 1 
[1]每当 SGMLParser 在 HTML 源代码中发现一个 &lt;pre&gt; 时,都会调用 start_pre。(马上我们就会确切地看到它是如何发生的。) 这个方法使用单个参数:attrs,这个参数会包含标记的属性 (如果存在的话) 。attrs 是一个键/值 tuple 的 list,就像 unknown_starttag 中所使用的。
[2]reset 方法中,我们初始化了一个数据属性,它作为 &lt;pre&gt; 标记的一个计数器。每当我们找到一个 &lt;pre&gt; 标记,我们增加计数器的值;每当我们找到一个 &lt;/pre&gt; 标记,我们将减少计数器的值。(我们本可以把它实现为一个标志,即或把它设为 1,或重置为 0,但这样做只是为了方便,并且这样做可以处理古怪 (但有可能) 的 &lt;pre&gt; 标记嵌套的情况。) 马上我们将会看到这个计数器是多么的好用。
[3]不错,这就是我们对 &lt;pre&gt; 标记所做的唯一的特殊处理。现在我们将属性列表传给 unknown_starttag,由它来进行缺省的处理。
[4]每当 SGMLParser 找到一个 &lt;/pre&gt; 标记时,会调用 end_pre。因为结束标记不能包含属性,因此这个方法没有参数。
[5]首先我们要进行缺省处理,就像其它结束标记做的一样。
[6]其次我们将计数器减少,标记这个 &lt;pre&gt; 块已经被关闭了。

到了这个地方,有必要对 SGMLParser 更深入一层。我已经多次声明 (到目前为止您应已经把它做为信条了) ,就是 SGMLParser 查找每一个标记并且如果存在特定的方法就调用它们。例如:我们刚刚看到处理 &lt;pre&gt;&lt;/pre&gt;start_preend_pre 的定义。但这是如何发生的呢?嗯,也没什么神奇的,只不过是出色的 Python 编码。

例 8.18. SGMLParser

 def finish_starttag(self, tag, attrs):               
        try:                                            
            method = getattr(self, 'start_' + tag)       
        except AttributeError:                           
            try:                                        
                method = getattr(self, 'do_' + tag)      
            except AttributeError:                      
                self.unknown_starttag(tag, attrs)        
                return -1                               
            else:                                       
                self.handle_starttag(tag, method, attrs) 
                return 0                                
        else:                                           
            self.stack.append(tag)                      
            self.handle_starttag(tag, method, attrs)    
            return 1                                     
    def handle_starttag(self, tag, method, attrs):      
        method(attrs) 
[1]此处,SGMLParser 已经找到了一个开始标记,并且分析出属性列表。唯一要做的事情就是检查对于这个标记是否存在一个特别的处理方法,否则我们就应该求助于缺省方法 (unknown_starttag) 。
[2]SGMLParser 的 “神奇” 之处除了我们的老朋友 getattr 之外就没有什么了。您以前可能没注意到,getattr 将查找定义在一个对象的继承者中或对象自身的方法。这里对象是 self,即当前实例。所以,如果 tag'pre',这里对 getattr 的调用将会在当前实例 (它是 Dialectizer 类的一个实例) 中查找一个名为 start_pre 的方法。
[3]如果 getattr 所查找的方法在对象或它的任何继承者中不存在的话,它会引发一个 AttributeError 的异常。但没有关系,因为我们把对 getattr 的调用包装到一个 try...except 块中了,并且显式地捕捉 AttributeError 异常。
[4]因为我们没有找到一个 start_xxx 方法,在放弃之前,我们将还要查找一个 do_xxx 方法。这个可替换的命名模式一般用于单独的标记,如 &lt;br&gt;,这些标记没有相应的结束标记。但是您可以使用任何一种模式,正如您看到的,SGMLParser 对每个标记尝试两次。(您不应该对相同的标记同时定义 start_xxxdo_xxx 处理方法,因为这样的话只有 start_xxx 方法会被调用。)
[5]另一个 AttributeError 异常,它是说用 do_xxx 来调用 getattr 失败了。因为对同一个标记我们既没有找到 start_xxx 也没有找到 do_xxx 处理方法,这样我们捕捉到了异常并且求助于缺省方法:unknown_starttag
[6]记得吗?try...except 块可以有一个 else 子句,当在 try...except 块中没有异常被引发时,它将被调用。逻辑上,意味着我们确实 找到了这个标记的 do_xxx 方法,所以我们将要调用它。
[7]顺便说,不要为这些不同的返回值而担心;理论上他们有意义,但实际上它们没有任何用处。也不要担心 self.stack.append(tag) ; SGMLParser 内部会知晓您的开始标记是否有合适的结束标记与之匹配,但是它不会对这些信息做任何操作。理论上,您能使用这个模块校验您的标记是否完全匹配,但是这或许没有多大价值,并且这样的内容已经超出了本章所要讨论的范畴。现在有您更需要担心的问题。
[8]start_xxxdo_xxx 方法并不被直接调用;标记名、方法和属性被传给 handle_starttag 这个方法,以便继承者可以覆盖它,并改变全部 开始标记分发的方式。我们不需要控制这个层面,所以我们只让这个方法做它自已的事,就是用属性 list 来调用方法 (start_xxxdo_xxx) 。记住 method 是一个从 getattr 返回的函数,而函数是对象。(我知道您已经听腻了,我发誓,一旦我们停止寻找新的使用方法来为我们服务时,我就决不再提它了。) 这时,函数对象作为一个参数传入这个分发方法,这个方法反过来再调用这个函数。在这里,我们不需要知道函数是什么,叫什么名字,或是在哪时定义的;我们只需要知道用一个参数 attrs 调用它。

现在回到我们已经计划好的程序:Dialectizer。当我们跑题时,我们定义了特别的处理方法来处理 &lt;pre&gt;&lt;/pre&gt; 标记。还有一件事没有做,那就是用我们预定义的替换处理来处理文本块。为了实现它,我们需要覆盖 handle_data 方法。

例 8.19. 覆盖 handle_data 方法

 def handle_data(self, text):                                         
        self.pieces.append(self.verbatim and text or self.process(text)) 
[1]handle_data 在调用时只使用一个参数:要处理的文本。
[2]在祖先类 BaseHTMLProcessor 中,handle_data 方法只是将文本追加到输出缓冲区 self.pieces 之后。这里的逻辑稍微有点复杂。如果我们处于 &lt;pre&gt;...&lt;/pre&gt; 块的中间,self.verbatim 将是大于 0 的某个值,接着我们想要将文本不作改动地传入输出缓冲区。否则,我们将调用另一个单独的方法来进行替换处理,然后将处理结果放入输出缓冲区中。在 Python 中,这是一个一行代码,它使用了and-or 技巧。

我们已经接近了对 Dialectizer 的全面理解。唯一缺少的一个环节是文本替换的特性。如果您知道点 Perl,您就会知道当需要复杂的文本替换时,唯一有效的解决方法就是正则表达式。在 dialect.py 文件后面的几个类中定义了一连串的正则表达式来操作 HTML 标记中的文本。我们已经学习过了正则表达式中的所有字符。我们不必重复学习正则表达式的艰难历程了,不是吗?上帝知道我反正不需要。我想现在这章您已经学得差不多了。

8.9. 全部放在一起

8.9. 全部放在一起

到了将迄今为止我们已经学过并用得不错的东西放在一起的时候了。我希望您专心些。

例 8.20. translate 函数,第一部分

 def translate(url, dialectName="chef"): 
    import urllib                       
    sock = urllib.urlopen(url)          
    htmlSource = sock.read()           
    sock.close() 
[1]这个 translate 函数有一个可选参数 dialectName,它是一个字符串,指出我们将使用的方言。一会我们就会看到它是如何使用的。
[2]嘿,等一下,在这个函数中有一个 import 语句!它在 Python 中完全合法。您已经习惯了在一个程序的前面看到 import 语句,它意味着导入的模块在程序的任何地方都是可用的。但您也可以在一个函数中导入模块,这意味着导入的模块只能在函数中使用。如果您有一个只能用在一个函数中的模块,这是一个简便的方法,使您的代码更模块化。(当发现您周末的加班已经变成了一个 800 行的艺术作品,并且决定将其分割成一打可重用的模块时,您会感谢它的。)
[3]现在我们得到了给定的 URL 源文件。

例 8.21. translate 函数,第二部分:奇妙而又奇妙

 parserName = "%sDialectizer" % dialectName.capitalize() 
    parserClass = globals()[parserName]                     
    parser = parserClass() 
[1]capitalize 是一个我们以前未曾见过的字符串方法;它只是将一个字符串的第一个字母变成大写,将其它的字母强制变成小写。再使用字符串格式化,我们就得到了一种方言的名字,并将它转化为了相应的方言变换器类的名字。如果 dialectName 是字符串 'chef'parserName 将是字符串 'ChefDialectizer'
[2]我们有了一个字符串形式 (parserName) 的类名称,还有一个 dictionary (globals()) 形式的全局名字空间。合起来后,我们可以得到以前者命名的类的引用。(回想一下,类是对象,并且它们可以像其它对象一样赋值给一个变量。) 如果 parserName 是字符串 'ChefDialectizer'parserClass 将是类 ChefDialectizer
[3]最后,我们拥有了一个类对象 (parserClass),接着我们想要生成这个类的一个实例。好,我们已经知道如何去做了:像函数一样调用类。这个类保存在一个局部变量中,但这个事实完全不会有什么影响;我们只是像函数一样调用这个局部变量,取出这个类的一个实例。如果 parserClass 是类 ChefDialectizerparser 将是类 ChefDialectizer 的一个实例。

何必这么麻烦?毕竟只有三个 Dialectizer 类;为什么不只使用一个 case 语句? (噢,在 Python 中不存在 case 语句,但为什么不只使用一组 if 语句呢?) 理由之一是:可扩展性。这个 translate 函数完全不用关心我们定义了多少个方言变换器类。设想一下,如果我们明天定义了一个新的 FooDialectizer 类,把 'foo' 作为 dialectName 传给 translatetranslate 也能工作。

甚至会更好。设想将 FooDialectizer 放进一个独立的模块中,使用 from _module_ import 将其导入。我们已经知道了,这样会将它包含在 globals() 中 ,所以不用修改 translate ,它仍然可以正确运行,尽管 FooDialectizer 位于一个独立的文件中。

现在设想一下方言的名字是从程序外面的某个地方来的,也许是从一个数据库中,或从一个表格中的用户输入的值中。您可以使用任意多的服务端 Python 脚本架构来动态地生成网页;这个函数将接收在页面请求的查询字符串中的一个 URL 和一个方言名字 (两个都是字符串) ,接着输出 “翻译” 后的网页。

最后,设想一下,使用了一种插件架构的 Dialectizer 框架。您可以将每个 Dialectizer 类放在分别放在独立的文件中,在 dialect.py 中只留下 translate 函数。假定一种统一的命名模式,这个 translate 函数能够动态地从合适的文件中导入合适的类,除了方言名字外什么都不用给出。(虽然您还没有看过动态导入,但我保证在后面的一章中会涉及到它。) 如果要加入一种新的方言,您只要在插件目录下加入一个以合适的名字命名的文件 (像 foodialect.py,它包含了 FooDialectizer 类) 。使用方言名 'foo' 来调用这个 translate 函数,将会查找 foodialect.py 模块,导入 FooDialectizer 类,这样就行了。

例 8.22. translate 函数,第三部分

 parser.feed(htmlSource) 
    parser.close()          
    return parser.output() 
[1]剩下的工作似乎会非常无聊,但实际上,feed 函数执行了全部的转换工作。我们拥有存在于单个字符串中的全部 HTML 源代码,所以我们只需要调用 feed 一次。然而,您可以按您的需要经常调用 feed,分析器将不停地进行分析。所以如果我们担心内存的使用 (或者我们已经知道了将要处理非常巨大的 HTML 页面) ,我们可以在一个循环中调用它,即我们读出一点 HTML 字节,就将其送进分析器。结果会是一样的。
[2]因为 feed 维护着一个内部缓冲区,当您完成时,应该总是调用分析器的 close 方法 (那怕您像我们做的一样,一次就全部送出) 。否则您可能会发现,输出丢掉了最后几个字节。
[3]回想一下,output 是我们在 BaseHTMLProcessor 上定义的函数,用来将所有缓冲的输出片段连接起来并且以单个字符串返回。

像这样,我们已经 “翻译” 了一个网页,除了给出一个 URL 和一种方言的名字外,什么都没有给出。

进一步阅读

  • 您可能会认为我的服务端脚本编程的想法是开玩笑。在我发现这个基于 web 的方言转换器之前,的确是这样想的。不幸的是,看不到它的源代码。

8.10. 小结

8.10. 小结

Python 向您提供了一个强大工具,sgmllib.py,可以通过将 HTML 结构转变为一种对象模型来进行处理。可以以许多不同的方式来使用这个工具。

  • 对 HTML 进行分析,搜索特别的东西
  • 摘录结果,如 URL lister
  • 在处理过程中顺便调整结构,如给属性值加引号
  • 将 HTML 转换为其它的东西,通过对文本进行处理,同时保留标记,如 Dialectizer

学过了这些例子之后,您应该无障碍地完成下面的事情:

  • 使用 locals() 和 globals() 来访问名字空间
  • 使用基于 dictionary 替换的字符串格式化

第九章 XML 处理

第九章 XML 处理

  • 9.1. 概览
  • 9.2. 包
  • 9.3. XML 解析
  • 9.4. Unicode
  • 9.5. 搜索元素
  • 9.6. 访问元素属性
  • 9.7. Segue

9.1. 概览

9.1. 概览

下面两章是关于 Python 中 XML 处理的。如果你已经对 XML 文档有了一个大概的了解,比如它是由结构化标记构成的,这些标记形成了层次模型的元素,等等这些知识都是有帮助的。如果你不明白这些,这里有很多 XML 教程能够解释这些基础知识。

如果你对 XML 不是很感兴趣,你还是应该读一下这些章节,它们涵盖了不少重要的主题,比如 Python 包、Unicode、命令行参数以及如何使用 getattr 进行方法分发。

如果你在大学里主修哲学 (而不是像计算机科学这样的实用专业),并且曾不幸地被伊曼努尔·康德的著作折磨地够呛,那么你会非常欣赏本章的样例程序。(这当然不意味着你必须修过哲学。)

处理 XML 有两种基本的方式。一种叫做 SAX (“Simple API for XML”),它的工作方式是,一次读出一点 XML 内容,然后对发现的每一个元素调用一个方法。(如果你读了 第八章 HTML 处理,这应该听起来很熟悉,因为这是 sgmllib 工作的方式。) 另一种方式叫做 DOM (“Document Object Model”),它的工作方式是,一次性读入整个 XML 文档,然后使用 Python 类创建一个内部表示形式 (以树结构进行连接)。Python 拥有这两种解析方式的标准模块,但是本章只涉及 DOM。

下面是一个完整的 Python 程序,它根据 XML 格式定义的上下文无关语法生成伪随机输出。如果你不明白是什么意思,不用担心,下面两章中将会深入检视这个程序的输入和输出。

例 9.1. kgp.py

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

"""Kant Generator for Python
Generates mock philosophy based on a context-free grammar
Usage: python kgp.py [options] [source]
Options:
  -g ..., --grammar=...   use specified grammar file or URL
  -h, --help              show this help
  -d                      show debugging information while parsing
Examples:
  kgp.py                  generates several paragraphs of Kantian philosophy
  kgp.py -g husserl.xml   generates several paragraphs of Husserl
  kpg.py "<xref id='paragraph'/>"  generates a paragraph of Kant
  kgp.py template.xml     reads from template.xml to decide what to generate
"""
from xml.dom import minidom
import random
import toolbox
import sys
import getopt
_debug = 0
class NoSourceError(Exception): pass
class KantGenerator:
    """generates mock philosophy based on a context-free grammar"""
    def __init__(self, grammar, source=None):
        self.loadGrammar(grammar)
        self.loadSource(source and source or self.getDefaultSource())
        self.refresh()
    def _load(self, source):
        """load XML input source, return parsed XML document
        - a URL of a remote XML file ("http://diveintopython.org/kant.xml")
        - a filename of a local XML file ("~/diveintopython/common/py/kant.xml")
        - standard input ("-")
        - the actual XML document, as a string
        """
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc
    def loadGrammar(self, grammar):                         
        """load context-free grammar"""                     
        self.grammar = self._load(grammar)                  
        self.refs = {}                                      
        for ref in self.grammar.getElementsByTagName("ref"):
            self.refs[ref.attributes["id"].value] = ref     
    def loadSource(self, source):
        """load source"""
        self.source = self._load(source)
    def getDefaultSource(self):
        """guess default source of the current grammar
        The default source will be one of the <ref>s that is not
        cross-referenced.  This sounds complicated but it's not.
        Example: The default source for kant.xml is
        "<xref id='section'/>", because 'section' is the one <ref>
        that is not <xref>'d anywhere in the grammar.
        In most grammars, the default source will produce the
        longest (and most interesting) output.
        """
        xrefs = {}
        for xref in self.grammar.getElementsByTagName("xref"):
            xrefs[xref.attributes["id"].value] = 1
        xrefs = xrefs.keys()
        standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
        if not standaloneXrefs:
            raise NoSourceError, "can't guess source, and no source specified"
        return '<xref id="%s"/>' % random.choice(standaloneXrefs)
    def reset(self):
        """reset parser"""
        self.pieces = []
        self.capitalizeNextWord = 0
    def refresh(self):
        """reset output buffer, re-parse entire source file, and return output
        Since parsing involves a good deal of randomness, this is an
        easy way to get new output without having to reload a grammar file
        each time.
        """
        self.reset()
        self.parse(self.source)
        return self.output()
    def output(self):
        """output generated text"""
        return "".join(self.pieces)
    def randomChildElement(self, node):
        """choose a random child element of a node
        This is a utility method used by do_xref and do_choice.
        """
        choices = [e for e in node.childNodes
                   if e.nodeType == e.ELEMENT_NODE]
        chosen = random.choice(choices)            
        if _debug:                                 
            sys.stderr.write('%s available choices: %s\n' % \
                (len(choices), [e.toxml() for e in choices]))
            sys.stderr.write('Chosen: %s\n' % chosen.toxml())
        return chosen                              
    def parse(self, node):         
        """parse a single XML node
        A parsed XML document (from minidom.parse) is a tree of nodes
        of various types.  Each node is represented by an instance of the
        corresponding Python class (Element for a tag, Text for
        text data, Document for the top-level document).  The following
        statement constructs the name of a class method based on the type
        of node we're parsing ("parse_Element" for an Element node,
        "parse_Text" for a Text node, etc.) and then calls the method.
        """
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__)
        parseMethod(node)
    def parse_Document(self, node):
        """parse the document node
        The document node by itself isn't interesting (to us), but
        its only child, node.documentElement, is: it's the root node
        of the grammar.
        """
        self.parse(node.documentElement)
    def parse_Text(self, node):    
        """parse a text node
        The text of a text node is usually added to the output buffer
        verbatim.  The one exception is that <p class='sentence'> sets
        a flag to capitalize the first letter of the next word.  If
        that flag is set, we capitalize the text and reset the flag.
        """
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)
    def parse_Element(self, node): 
        """parse an element
        An XML element corresponds to an actual tag in the source:
        <xref id='...'>, <p chance='...'>, <choice>, etc.
        Each element type is handled in its own method.  Like we did in
        parse(), we construct a method name based on the name of the
        element ("do_xref" for an <xref> tag, etc.) and
        call the method.
        """
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)
    def parse_Comment(self, node):
        """parse a comment
        The grammar can contain XML comments, but we ignore them
        """
        pass
    def do_xref(self, node):
        """handle <xref id='...'> tag
        An <xref id='...'> tag is a cross-reference to a <ref id='...'>
        tag.  <xref id='sentence'/> evaluates to a randomly chosen child of
        <ref id='sentence'>.
        """
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id]))
    def do_p(self, node):
        """handle <p> tag
        The <p> tag is the core of the grammar.  It can contain almost
        anything: freeform text, <choice> tags, <xref> tags, even other
        <p> tags.  If a "class='sentence'" attribute is found, a flag
        is set and the next word will be capitalized.  If a "chance='X'"
        attribute is found, there is an X% chance that the tag will be
        evaluated (and therefore a (100-X)% chance that it will be
        completely ignored)
        """
        keys = node.attributes.keys()
        if "class" in keys:
            if node.attributes["class"].value == "sentence":
                self.capitalizeNextWord = 1
        if "chance" in keys:
            chance = int(node.attributes["chance"].value)
            doit = (chance > random.randrange(100))
        else:
            doit = 1
        if doit:
            for child in node.childNodes: self.parse(child)
    def do_choice(self, node):
        """handle <choice> tag
        A <choice> tag contains one or more <p> tags.  One <p> tag
        is chosen at random and evaluated; the rest are ignored.
        """
        self.parse(self.randomChildElement(node))
def usage():
    print __doc__
def main(argv):                         
    grammar = "kant.xml"                
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
        usage()                         
        sys.exit(2)                     
    for opt, arg in opts:               
        if opt in ("-h", "--help"):     
            usage()                     
            sys.exit()                  
        elif opt == '-d':               
            global _debug               
            _debug = 1                  
        elif opt in ("-g", "--grammar"):
            grammar = arg               
    source = "".join(args)              
    k = KantGenerator(grammar, source)
    print k.output()
if __name__ == "__main__":
    main(sys.argv[1:]) 

例 9.2. toolbox.py

"""Miscellaneous utility functions"""
def openAnything(source):            
    """URI, filename, or string --> stream
    This function lets you define parsers that take any input source
    (URL, pathname to local or network file, or actual data as a string)
    and deal with it in a uniform manner.  Returned object is guaranteed
    to have all the basic stdio read methods (read, readline, readlines).
    Just .close() the object when you're done with it.
    Examples:
    >>> from xml.dom import minidom
    >>> sock = openAnything("http://localhost/kant.xml")
    >>> doc = minidom.parse(sock)
    >>> sock.close()
    >>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml")
    >>> doc = minidom.parse(sock)
    >>> sock.close()
    >>> sock = openAnything("<ref id='conjunction'><text>and</text><text>or</text></ref>")
    >>> doc = minidom.parse(sock)
    >>> sock.close()
    """
    if hasattr(source, "read"):
        return source
    if source == '-':
        import sys
        return sys.stdin
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)     
    except (IOError, OSError):            
        pass                              
    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)               
    except (IOError, OSError):            
        pass                              
    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source)) 

独立运行程序 kgp.py,它会解析 kant.xml 中默认的基于 XML 的语法,并以康德的风格打印出几段有哲学价值的段落来。

例 9.3. kgp.py 的样例输出

[you@localhost kgp]$ python kgp.py
 As is shown in the writings of Hume, our a priori concepts, in
reference to ends, abstract from all content of knowledge; in the study
of space, the discipline of human reason, in accordance with the
principles of philosophy, is the clue to the discovery of the
Transcendental Deduction.  The transcendental aesthetic, in all
theoretical sciences, occupies part of the sphere of human reason
concerning the existence of our ideas in general; still, the
never-ending regress in the series of empirical conditions constitutes
the whole content for the transcendental unity of apperception.  What
we have alone been able to show is that, even as this relates to the
architectonic of human reason, the Ideal may not contradict itself, but
it is still possible that it may be in contradictions with the
employment of the pure employment of our hypothetical judgements, but
natural causes (and I assert that this is the case) prove the validity
of the discipline of pure reason.  As we have already seen, time (and
it is obvious that this is true) proves the validity of time, and the
architectonic of human reason, in the full sense of these terms,
abstracts from all content of knowledge.  I assert, in the case of the
discipline of practical reason, that the Antinomies are just as
necessary as natural causes, since knowledge of the phenomena is a
posteriori.
    The discipline of human reason, as I have elsewhere shown, is by
its very nature contradictory, but our ideas exclude the possibility of
the Antinomies.  We can deduce that, on the contrary, the pure
employment of philosophy, on the contrary, is by its very nature
contradictory, but our sense perceptions are a representation of, in
the case of space, metaphysics.  The thing in itself is a
representation of philosophy.  Applied logic is the clue to the
discovery of natural causes.  However, what we have alone been able to
show is that our ideas, in other words, should only be used as a canon
for the Ideal, because of our necessary ignorance of the conditions.
[...snip...] 

这当然是胡言乱语。噢,不完全是胡言乱语。它在句法和语法上都是正确的 (尽管非常罗嗦――康德可不是你们所说的踩得到点上的那种人)。其中一些实际上是正确的 (或者至少康德可能会认同的事情),其中一些则明显是错误的,大部分只是语无伦次。但所有内容都符合康德的风格。

让我重复一遍,如果你现在或曾经主修哲学专业,这会非常、非常有趣。

有趣之处在于,这个程序中没有一点内容是属于康德的。所有的内容都来自于上下文无关语法文件 kant.xml。如果你要程序使用不同的语法文件 (可以在命令行中指定),输出信息将完全不同。

例 9.4. kgp.py 的简单输出

[you@localhost kgp]$ python kgp.py -g binary.xml
00101001
[you@localhost kgp]$ python kgp.py -g binary.xml
10110100 

在本章后面的内容中,你将近距离地观察语法文件的结构。现在,你只要知道语法文件定义了输出信息的结构,而 kgp.py 程序读取语法规则并随机确定哪些单词插入哪里。

9.2. 包

9.2. 包

实际上解析一个 XML 文档是很简单的:只要一行代码。但是,在你接触那行代码前,需要暂时岔开一下,讨论一下包。

例 9.5. 载入一个 XML 文档 (偷瞥一下)

>>> from xml.dom import minidom 
>>> xmldoc = minidom.parse('~/diveintopython/common/py/kgp/binary.xml') 
[1]这个语法你之前没有见过。它看上去很像我们熟知的 from _module_ import,但是"." 使得它好像不只是普通的 import 那么简单。事实上,xml 称为包,domxml 中嵌套的包,而 minidomxml.dom 中的模块。

听起来挺复杂的,其实不是。看一下确切的实现可能会有帮助。包不过是模块的目录;嵌套包是子目录。一个包 (或一个嵌套包) 中的模块也只是 .py 文件罢了,永远都是,只是它们是在一个子目录中,而不是在你的 Python 安装环境的主 lib/ 目录下。

例 9.6. 包的文件布局

Python21/           Python 安装根目录 (可执行文件的所在地)
|
+--lib/             库目录 (标准库模块的所在地)
   |
   +-- xml/         xml 包 (实际上目录中还有其它东西)
       |
       +--sax/      xml.sax 包 (也只是一个目录)
       |
       +--dom/      xml.dom 包 (包含 minidom.py)
       |
       +--parsers/  xml.parsers 包 (内部使用) 

所以你说 from xml.dom import minidom,Python 认为它的意思是“在 xml 目录中查找 dom 目录,然后在这个目录 中查找 minidom 模块,接着导入它并以 minidom 命名 ”。但是 Python 更聪明;你不仅可以导入包含在一个包中的所有模块,还可以从包的模块中有选择地导入指定的类或者函数。语法都是一样的; Python 会根据包的布局理解你的意思,然后自动进行正确的导入。

例 9.7. 包也是模块

>>> from xml.dom import minidom         
>>> minidom
<module 'xml.dom.minidom' from 'C:\Python21\lib\xml\dom\minidom.pyc'>
>>> minidom.Element
<class xml.dom.minidom.Element at 01095744>
>>> from xml.dom.minidom import Element 
>>> Element
<class xml.dom.minidom.Element at 01095744>
>>> minidom.Element
<class xml.dom.minidom.Element at 01095744>
>>> from xml import dom                 
>>> dom
<module 'xml.dom' from 'C:\Python21\lib\xml\dom\__init__.pyc'>
>>> import xml                          
>>> xml
<module 'xml' from 'C:\Python21\lib\xml\__init__.pyc'> 
[1]这里你正从一个嵌套包 (xml.dom)中导入一个模块 (minidom)。结果就是 minidom 被导入到了你 (程序) 的命名空间中了。要引用 minidom 模块中的类 (比如 Element),你必须在它们的类名前面加上模块名。
[2]这里你正从一个来自嵌套包 (xml.dom) 的模块 (minidom) 中导入一个类 (Element)。结果就是 Element 直接导入到了你 (程序) 的命名空间中。注意,这样做并不会干扰以前的导入;现在 Element 类可以用两种方式引用了 (但其实是同一个类)。
[3]这里你正在导入 dom 包 (xml 的一个嵌套包),并将其作为一个模块。一个包的任何层次都可以视为一个模块,一会儿就会看到。它甚至可以拥有自己的属性和方法,就像你在前面看到过的模块。
[4]这里你正在将根层次的 xml 包作为一个模块导入。

那么如何才能导入一个包 (它不过是磁盘上的一个目录) 并使其成为一个模块 (它总是在磁盘上的一个文件) 呢?答案就是神奇的 __init__.py 文件。你明白了吧,包不只是目录,它们是包含一个特殊文件 __init__.py 的目录。这个文件定义了包的属性和方法。例如,xml.dom 包含了 Node 类,它在xml/dom/__init__.py中有所定义。当你将一个包作为模块导入 (比如从 xml 导入 dom) 的时候,实际上导入了它的 __init__.py 文件。

注意 一个包是一个其中带有特殊文件 __init__.py 的目录。__init__.py 文件定义了包的属性和方法。其实它可以什么也不定义;可以只是一个空文件,但是必须要存在。如果 __init__.py 不存在,这个目录就仅仅是一个目录,而不是一个包,它就不能被导入或者包含其它的模块和嵌套包。

那为什么非得用包呢?嗯,它们提供了在逻辑上将相关模块归为一组的方法。不使用其中带有 saxdomxml 包,作者也可以选择将所有的 sax 功能放入 xmlsax.py中,并将所有的 dom 功能放入 xmldom.py中,或者干脆将所有东西放入单个模块中。但是这样可能不实用 (写到这儿时,XML 包已经超过了 3000 行代码) 并且很难管理 (独立的源文件意味着多个人可以同时在不同的地方进行开发)。

如果你发现自己正在用 Python 编写一个大型的子系统 (或者,很有可能,当你意识到你的小型子系统已经成长为一个大型子系统时),你应该花费些时间设计一个好的包架构。它是 Python 所擅长的事情之一,所以应该好好利用它。

9.3. XML 解析

9.3. XML 解析

正如我说的,实际解析一个 XML 文档是非常简单的:只要一行代码。从这里出发到哪儿去就是你自己的事了。

例 9.8. 载入一个 XML 文档 (这次是真的)

>>> from xml.dom import minidom                                          
>>> xmldoc = minidom.parse('~/diveintopython/common/py/kgp/binary.xml')  
>>> xmldoc                                                               
<xml.dom.minidom.Document instance at 010BE87C>
>>> print xmldoc.toxml()                                                 
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar> 
[1]正如在上一节看到的,该语句从 xml.dom 包中导入 minidom 模块。
[2]这就是进行所有工作的一行代码:minidom.parse 接收一个参数并返回 XML 文档解析后的表示形式。这个参数可以是很多东西;在本例中,它只是我本地磁盘上一个 XML 文档的文件名。(你需要将路径改为指向下载的例子所在的目录。) 但是你也可以传入一个文件对象,或甚至是一个类文件对象。这样你就可以在本章后面好好利用这一灵活性了。
[3]minidom.parse 返回的对象是一个 Document 对象,它是 Node 类的一个子对象。这个 Document 对象是联锁的 Python 对象的一个复杂树状结构的根层次,这些 Python 对象完整表示了传给 minidom.parse 的 XML 文档。
[4]toxmlNode 类的一个方法 (因此可以在从 minidom.parse 中得到的 Document 对象上使用)。toxml 打印出了 Node 表示的 XML。对于 Document 节点,这样就会打印出整个 XML 文档。

现在内存中已经有了一个 XML 文档了,你可以开始遍历它了。

例 9.9. 获取子节点

>>> xmldoc.childNodes    
[<DOM Element: grammar at 17538908>]
>>> xmldoc.childNodes[0] 
<DOM Element: grammar at 17538908>
>>> xmldoc.firstChild    
<DOM Element: grammar at 17538908> 
[1]每个 Node 都有一个 childNodes 属性,它是一个 Node 对象的列表。一个 Document 只有一个子节点,即 XML 文档的根元素 (在本例中,是 grammar 元素)。
[2]为了得到第一个 (在本例中,只有一个) 子节点,只要使用正规的列表语法。回想一下,其实这里没有发生什么特别的;这只是一个由正规 Python 对象构成的正规 Python 列表。
[3]鉴于获取某个节点的第一个子节点是有用而且常见的行为,所以 Node 类有一个 firstChild 属性,它和childNodes[0]具有相同的语义。(还有一个 lastChild 属性,它和childNodes[-1]具有相同的语义。)

例 9.10. toxml 用于任何节点

>>> grammarNode = xmldoc.firstChild
>>> print grammarNode.toxml() 
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar> 
[1]由于 toxml 方法是定义在 Node 类中的,所以对任何 XML 节点都是可用的,不仅仅是 Document 元素。

例 9.11. 子节点可以是文本

>>> grammarNode.childNodes                  
[<DOM Text node "\n">, <DOM Element: ref at 17533332>, \
<DOM Text node "\n">, <DOM Element: ref at 17549660>, <DOM Text node "\n">]
>>> print grammarNode.firstChild.toxml()    

>>> print grammarNode.childNodes[1].toxml() 
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
>>> print grammarNode.childNodes[3].toxml() 
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
>>> print grammarNode.lastChild.toxml() 
[1]查看 binary.xml 中的 XML ,你可能会认为 grammar 只有两个子节点,即两个 ref 元素。但是你忘记了一些东西:硬回车!在'&lt;grammar&gt;'之后,第一个'&lt;ref&gt;'之前是一个硬回车,并且这个文本算作 grammar 元素的一个子节点。类似地,在每个'&lt;/ref&gt;'之后都有一个硬回车;它们都被当作子节点。所以grammar.childNodes实际上是一个有 5 个对象的列表:3 个 Text 对象和两个 Element 对象。
[2]第一个子节点是一个 Text 对象,它表示在'&lt;grammar&gt;'标记之后、第一个'&lt;ref&gt;'标记之后的硬回车。
[3]第二个子节点是一个 Element 对象,表示了第一个 ref 元素。
[4]第四个子节点是一个 Element 对象,表示了第二个 ref 元素。
[5]最后一个子节点是一个 Text 对象,表示了在'&lt;/ref&gt;'结束标记之后、'&lt;/grammar&gt;' 结束标记之前的硬回车。

例 9.12. 把文本挖出来

>>> grammarNode
<DOM Element: grammar at 19167148>
>>> refNode = grammarNode.childNodes[1] 
>>> refNode
<DOM Element: ref at 17987740>
>>> refNode.childNodes                  
[<DOM Text node "\n">, <DOM Text node "  ">, <DOM Element: p at 19315844>, \
<DOM Text node "\n">, <DOM Text node "  ">, \
<DOM Element: p at 19462036>, <DOM Text node "\n">]
>>> pNode = refNode.childNodes[2]
>>> pNode
<DOM Element: p at 19315844>
>>> print pNode.toxml()                 
<p>0</p>
>>> pNode.firstChild                    
<DOM Text node "0">
>>> pNode.firstChild.data               
u'0' 
[1]正如你在前面的例子中看到的,第一个 ref 元素是 grammarNode.childNodes[1],因为 childNodes[0] 是一个代表硬回车的 Text 节点。
[2]ref 元素有它自己的子节点集合,一个表示硬回车,另一个表示空格,一个表示 p 元素,诸如此类。
[3]你甚至可以在这里使用 toxml 方法,尽管它深深嵌套在文档中。
[4]p 元素只有一个子节点 (在这个例子中无法看出,但是如果你不信,可以看看pNode.childNodes),而且它是表示单字符'0'的一个 Text 节点。
[5]Text 节点的 .data 属性可以向你提供文本节点真正代表的字符串。但是字符串前面的'u'是什么意思呢?答案将自己专门有一部分来论述。

9.4. Unicode

9.4. Unicode

Unicode 是一个系统,用来表示世界上所有不同语言的字符。当 Python 解析一个 XML 文档时,所有的数据都是以 unicode 的形式保存在内存中的。

一会儿你就会了解,但首先,先看一些背景知识。

历史注解. 在 unicode 之前,对于每一种语言都存在独立的字符编码系统,每个系统都使用相同的数字(0-255)来表示这种语言的字符。一些语言 (像俄语) 对于如何表示相同的字符还有几种有冲突的标准;另一些语言 (像日语) 拥有太多的字符,需要多个字符集。在系统之间进行文档交流是困难的,因为对于一台计算机来说,没有方法可以识别出文档的作者使用了哪种编码模式;计算机看到的只是数字,并且这些数字可以表示不同的东西。接着考虑到试图将这些 (采用不同编码的) 文档存放到同一个地方 (比如在同一个数据库表中);你需要在每段文本的旁边保存字符的编码,并且确保在传递文本的同时将编码也进行传递。接着考虑多语言文档,即在同一文档中使用了不同语言的字符。(比较有代表性的是使用转义符来进行模式切换;噗,我们处于俄语 koi8-r 模式,所以字符 241 表示这个;噗,现在我们处于 Mac 希腊语模式,所以字符 241 表示其它什么。等等。) 这些就是 unicode 被设计出来要解决的问题。

为了解决这些问题,unicode 用一个 2 字节数字表示每个字符,从 0 到 65535。[8] 每个 2 字节数字表示至少在一种世界语言中使用的一个唯一字符。(在多种语言中都使用的字符具有相同的数字码。) 这样就确保每个字符一个数字,并且每个数字一个字符。Unicode 数据永远不会模棱两可。

当然,仍然还存在着所有那些遗留的编码系统的情况。例如,7 位 ASCII,它可以将英文字符存诸为从 0 到 127 的数值。(65 是大写字母 “A”,97 是小写字母 “a”,等等。) 英语有着非常简单的字母表,所以它可以完全用 7 位 ASCII 来表示。像法语、西班牙语和德语之类的西欧语言都使用叫做 ISO-8859-1 的编码系统 (也叫做“latin-1”),它使用 7 位 ASCII 字符表示从 0 到 127 的数字,但接着扩展到了 128-255 的范围来表示像 n 上带有一个波浪线(241),和 u 上带有两个点(252)的字符。Unicode 在 0 到 127 上使用了同 7 位 ASCII 码一样的字符表,在 128 到 255 上同 ISO-8859-1 一样,接着使用剩余的数字,256 到 65535,扩展到表示其它语言的字符。

在处理 unicode 数据时,在某些地方你可能需要将数据转换回这些遗留编码系统之一。例如,为了同其它一些计算机系统集成,这些系统期望它的数据使用一种特定的单字节编码模式,或将数据打印输出到一个不识别 unicode 的终端或打印机。或将数据保存到一个明确指定编码模式的 XML 文档中。

在了解这个注解之后,让我们回到 Python 上来。

从 2.0 版开始,Python 整个语言都已经支持 unicode。XML 包使用 unicode 来保存所有解析了的 XML 数据,而且你可以在任何地方使用 unicode。

例 9.13. unicode 介绍

>>> s = u'Dive in'            
>>> s
u'Dive in'
>>> print s                   
Dive in 
[1]为了创建一个 unicode 字符串而不是通常的 ASCII 字符串,要在字符串前面加上字母 “u”。注意这个特殊的字符串没有任何非 ASCII 的字符。这样很好;unicode 是 ASCII 的一个超集 (一个非常大的超集),所以任何正常的 ASCII 都可以以 unicode 形式保存起来。
[2]在打印字符串时,Python 试图将字符串转换为你的默认编码,通常是 ASCII 。(过会儿有更详细的说明。) 因为组成这个 unicode 字符串的字符都是 ASCII 字符,打印结果与打印正常的 ASCII 字符串是一样的;转换是无缝的,而且如果你没有注意到 s 是一个 unicode 字符串的话,你永远也不会注意到两者之间的差别。

例 9.14. 存储非 ASCII 字符

>>> s = u'La Pe\xf1a'         
>>> print s                   
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> print s.encode('latin-1') 
La Pe?a 
[1]unicode 真正的优势,理所当然的是它保存非 ASCII 字符的能力,例如西班牙语的 “?”(n 上带有一个波浪线)。用来表示波浪线 n 的 unicode 字符编码是十六进制的 0xf1 (十进制的 241),你可以像这样输入:\xf1
[2]还记得我说过 print 函数会尝试将 unicode 字符串转换为 ASCII 从而打印它吗?嗯,在这里将不会起作用,因为你的 unicode 字符串包含非 ASCII 字符,所以 Python 会引发 UnicodeError 异常。
[3]这儿就是将 unicode 转换为其它编码模式起作用的地方。s 是一个 unicode 字符串,但 print 只能打印正常的字符串。为了解决这个问题,我们调用 encode 方法 (它可以用于每个 unicode 字符串) 将 unicode 字符串转换为指定编码模式的正常字符串。我们向此函数传入一个参数。在本例中,我们使用 latin-1 (也叫 iso-8859-1),它包括带波浪线的 n (然而缺省的 ASCII 编码模式不包括,因为它只包含数值从 0 到 127 的字符)。

还记得我说过:需要从一个 unicode 得到一个正常字符串时,Python 通常默认将 unicode 转换成 ASCII 吗?嗯,这个默认编码模式是一个可以定制的选项。

例 9.15. sitecustomize.py

# sitecustomize.py 
# this file can be anywhere in your Python path,
# but it usually goes in ${pythondir}/lib/site-packages/
import sys
sys.setdefaultencoding('iso-8859-1') 
[1]sitecustomize.py 是一个特殊的脚本;Python 会在启动的时候导入它,所以在其中的任何代码都将自动运行。就像注解中提到的那样,它可以放在任何地方 (只要 import 能够找到它),但是通常它位于 Python 的lib 目录的 site-packages 目录中。
[2]嗯,setdefaultencoding 函数设置默认编码。Python 会在任何需要将 unicode 字符串自动转换为正规字符串的地方,使用这个编码模式。

例 9.16. 设置默认编码的效果

>>> import sys
>>> sys.getdefaultencoding() 
'iso-8859-1'
>>> s = u'La Pe\xf1a'
>>> print s                  
La Pe?a 
[1]这个例子假设你已经按前一个例子中的改动对 sitecustomize.py 文件做了修改,并且已经重启了 Python。如果你的默认编码还是 'ascii',可能你就没有正确设置 sitecustomize.py 文件,或者是没有重新启动 Python。默认的编码只能在 Python 启动的时候改变;之后就不能改变了。(由于一些我们现在不会仔细研究的古怪的编程技巧,你甚至不能在 Python 启动之后调用 sys.setdefaultencoding 函数。仔细研究 site.py,并搜索 “setdefaultencoding” 去发现为什么吧。)
[2]现在默认的编码模式已经包含了你在字符串中使用的所有字符,Python 对字符串的自动强制转换和打印就不存在问题了。

例 9.17. 指定.py文件的编码

如果你打算在你的 Python 代码中保存非 ASCII 字符串,你需要在每个文件的顶端加入编码声明来指定每个 .py 文件的编码。这个声明定义了 .py 文件的编码为 UTF-8:

#!/usr/bin/env python
# -*- coding: UTF-8 -*- 

现在,想想 XML 中的编码应该是怎样的呢?不错,每一个 XML 文档都有指定的编码。重复一下,ISO-8859-1 是西欧语言存放数据的流行编码方式。KOI8-R 是俄语流行的编码方式。编码――如果指定了的话――都在 XML 文档的首部。

例 9.18. russiansample.xml

 <?xml version="1.0" encoding="koi8-r"?>  <preface>
<title>Предисловие</title>  </preface> 
[1]这是从一个真实的俄语 XML 文档中提取出来的示例;它就是这本书俄语翻译版的一部分。注意,编码 koi8-r 是在首部指定的。
[2]这些是古代斯拉夫语的字符,就我所知,它们用来拼写俄语单词“Preface”。如果你在一个正常文本编辑器中打开这个文件,这些字符非常像乱码,因为它们使用了 koi8-r 编码模式进行编码,但是却以 iso-8859-1 编码模式进行显示。

例 9.19. 解析 russiansample.xml

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('russiansample.xml') 
>>> title = xmldoc.getElementsByTagName('title')[0].firstChild.data
>>> title                                       
u'\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435'
>>> print title                                 
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> convertedtitle = title.encode('koi8-r')     
>>> convertedtitle
'\xf0\xd2\xc5\xc4\xc9\xd3\xcc\xcf\xd7\xc9\xc5'
>>> print convertedtitle                        
Предисловие 
[1]我假设在这里你将前一个例子以 russiansample.xml 为名保存在当前目录中。也出于完整性的考虑,我假设你已经删除了 sitecustomize.py 文件,将缺省编码改回到 'ascii',或至少将 setdefaultencoding 一行注释起来了。
[2]注意 title 标记 (现在在 title 变量中,上面那一长串 Python 函数我们暂且跳过,下一节再解释)――在 XML 文档的 title 元素中的文本数据是以 unicode 保存的。
[3]直接打印 title 是不可能的,因为这个 unicode 字符串包含了非 ASCII 字符,所以 Python 不能把它转换为 ASCII,因为它无法理解。
[4]但是,你能够显式地将它转换为 koi8-r,在本例中,我们得到一个 (正常,非 unicode) 单字节字符的字符串 (f0, d2, c5,等等),它是初始 unicode 字符串中字符 koi8-r 编码的版本。
[5]打印 koi8-r 编码的字符串有可能会在你的屏幕上显示为乱码,因为你的 Python IDE 将这些字符作为 iso-8859-1 的编码进行解析,而不是 koi8-r 编码。但是,至少它们能打印。 (并且,如果你仔细看,当在一个不支持 unicode 的文本编辑器中打开最初的 XML 文档时,会看到相同的乱码。Python 在解析 XML 文档时,将它从 koi8-r 转换到了 unicode,你只不过是将它转换回来。)

总结一下,如果你以前从没有看到过 unicode,倒是有些唬人,但是在 Python 处理 unicode 数据真是非常容易。如果你的 XML 文档都是 7 位的 ASCII (像本章中的例子),你差不多永远都不用考虑 unicode。Python 在进行解析时会将 XML 文档中的 ASCII 数据转换为 unicode,在任何需要的时候强制转换回为 ASCII,你甚至永远都不用注意。但是如果你要处理其它语言的数据,Python 已经准备好了。

进一步阅读

  • Unicode.org 是 unicode 标准的主页,包含了一个简要的技术简介
  • Unicode 教程有更多关于如何使用 Python unicode 函数的例子,包括甚至在并不真的需要时如何将 unicode 强制转换为 ASCII。
  • PEP 263 涉及了何时、如何在你的.py文件中定义字符编码的更多细节。

Footnotes

[8] 这一点,很不幸仍然 过分简单了。现在 unicode 已经扩展用来处理古老的汉字、韩文和日文文本,它们有太多不同的字符,以至于 2 字节的 unicode 系统不能全部表示。但当前 Python 不支持超出范围的编码,并且我不知道是否有正在计划进行解决的项目。对不起,你已经到了我经验的极限了。

9.5. 搜索元素

9.5. 搜索元素

通过一步步访问每一个节点的方式遍历 XML 文档可能很乏味。如果你正在寻找些特别的东西,又恰恰它们深深埋入了你的 XML 文档,有个捷径让你可以快速找到它:getElementsByTagName

在这部分,将使用 binary.xml 语法文件,它的内容如下:

例 9.20. binary.xml

<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar> 

它有两个 ref'bit' (位) 和 'byte' (字节)。一个 bit'0' 或者 '1',而一个 byte 是 8 个 bit

例 9.21. getElementsByTagName 介绍

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('binary.xml')
>>> reflist = xmldoc.getElementsByTagName('ref') 
>>> reflist
[<DOM Element: ref at 136138108>, <DOM Element: ref at 136144292>]
>>> print reflist[0].toxml()
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
>>> print reflist[1].toxml()
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref> 
[1]getElementsByTagName 接收一个参数,即要找的元素的名称。它返回一个 Element 对象的列表,列表中的对象都是有指定名称的 XML 元素。在本例中,你能找到两个 ref 元素。

例 9.22. 每个元素都是可搜索的

>>> firstref = reflist[0]                      
>>> print firstref.toxml()
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
>>> plist = firstref.getElementsByTagName("p") 
>>> plist
[<DOM Element: p at 136140116>, <DOM Element: p at 136142172>]
>>> print plist[0].toxml()                     
<p>0</p>
>>> print plist[1].toxml()
<p>1</p> 
[1]继续前面的例子,在 reflist 中的第一个对象是 'bit' ref元素。
[2]你可以在这个 Element 上使用相同的 getElementsByTagName 方法来寻找所有在'bit' ref 元素中的&lt;p&gt;元素。
[3]和前面一样,getElementsByTagName 方法返回一个找到元素的列表。在本例中,你有两个元素,每“位”各占一个。

例 9.23. 搜索实际上是递归的

>>> plist = xmldoc.getElementsByTagName("p") 
>>> plist
[<DOM Element: p at 136140116>, <DOM Element: p at 136142172>, <DOM Element: p at 136146124>]
>>> plist[0].toxml()                         
'<p>0</p>'
>>> plist[1].toxml()
'<p>1</p>'
>>> plist[2].toxml()                         
'<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>' 
[1]仔细注意这个例子和前面例子之间的不同。前面,你是在 firstref 中搜索 p 元素,但是这里你是在 xmldoc 中搜索 p 元素,xmldoc 是代表了整个 XML 文档的根层对象。这样就会 找到嵌套在 ref 元素 (它嵌套在根 grammar 元素中) 中的 p 元素。
[2]前两个 p 元素在第一个 ref 内 ('bit' ref)。
[3]后一个 p 元素在第二个 ref 中 ('byte' ref)。

9.6. 访问元素属性

9.6. 访问元素属性

XML 元素可以有一个或者多个属性,只要已经解析了一个 XML 文档,访问它们就太简单了。

在这部分中,将使用 binary.xml 语法文件,你在上一节中已经看到过了。

注意 这部分由于某个含义重叠的术语可能让人有点糊涂。在一个 XML 文档中,元素可以有属性,而 Python 对象也有属性。当你解析一个 XML 文档时,你得到了一组 Python 对象,它们代表 XML 文档中的所有片段,同时有些 Python 对象代表 XML 元素的属性。但是表示 (XML) 属性的 (Python) 对象也有 (Python) 属性,它们用于访问对象表示的 (XML) 属性。我告诉过你它让人糊涂。我会公开提出关于如何更明显地区分这些不同的建议。

例 9.24. 访问元素属性

>>> xmldoc = minidom.parse('binary.xml')
>>> reflist = xmldoc.getElementsByTagName('ref')
>>> bitref = reflist[0]
>>> print bitref.toxml()
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
>>> bitref.attributes          
<xml.dom.minidom.NamedNodeMap instance at 0x81e0c9c>
>>> bitref.attributes.keys()    
[u'id']
>>> bitref.attributes.values() 
[<xml.dom.minidom.Attr instance at 0x81d5044>]
>>> bitref.attributes["id"]    
<xml.dom.minidom.Attr instance at 0x81d5044> 
[1]每个 Element 对象都有一个 attributes 属性,它是一个 NamedNodeMap 对象。听上去挺吓人的,其实不然,因为 NamedNodeMap 是一个行为像字典的对象,所以你已经知道怎么使用它了。
[2]NamedNodeMap 视为一个字典,你可以通过 attributes.keys() 获得属性名称的一个列表。这个元素只有一个属性,'id'
[3]属性名称,像其它 XML 文档中的文本一样,都是以 unicode 保存的。
[4]再次将 NamedNodeMap 视为一个字典,你可以通过 attributes.values() 获取属性值的一个列表。这些值本身是 Attr 类型的对象。你将在下一个例子中看到如何获取对象的有用信息。
[5]仍然把 NamedNodeMap 视为一个字典,你可以通过常用的字典语法和名称访问单个的属性。(那些非常认真的读者将已经知道 NamedNodeMap 类是如何实现这一技巧的:通过定义一个 __getitem__ 专用方法。其它的读者可能乐意接受这一事实:他们不需要理解它是如何工作的就可以有效地使用它。)

例 9.25. 访问单个属性

>>> a = bitref.attributes["id"]
>>> a
<xml.dom.minidom.Attr instance at 0x81d5044>
>>> a.name  
u'id'
>>> a.value 
u'bit' 
[1]Attr 对象完整代表了单个 XML 元素的单个 XML 属性。属性的名称 (与你在 bitref.attributes NamedNodeMap 伪字典中寻找的对象同名) 保存在a.name中。
[2]这个 XML 属性的真实文本值保存在 a.value 中。

注意 类似于字典,一个 XML 元素的属性没有顺序。属性可以以某种顺序偶然 列在最初的 XML 文档中,而在 XML 文档解析为 Python 对象时,Attr 对象以某种顺序偶然 列出,这些顺序都是任意的,没有任何特别的含义。你应该总是使用名称来访问单个属性,就像字典的键一样。

9.7. Segue [9]

9.7. Segue [9]

以上就是 XML 的核心内容。下一章将使用相同的示例程序,但是焦点在于能使程序更加灵活的其它方面:使用输入流处理,使用 getattr 进行方法分发,并使用命令行标识允许用户重新配置程序而无需修改代码。

在进入下一章前,你应该没有困难的完成这些事情:

  • 使用 minidom 解析 XML 文档,搜索已解析文档,并以任意顺序访问元素属性和元素子元素
  • 将复杂的库组织为包
  • 将 unicode 字符串转换为不同的字符编码

Footnotes

[9] “Segue”是音乐术语,意为“继续演奏”。

第十章 脚本和流

第十章 脚本和流

  • 10.1. 抽象输入源
  • 10.2. 标准输入、输出和错误
  • 10.3. 查询缓冲节点
  • 10.4. 查找节点的直接子节点
  • 10.5. 根据节点类型创建不同的处理器
  • 10.6. 处理命令行参数
  • 10.7. 全部放在一起
  • 10.8. 小结

10.1. 抽象输入源

10.1. 抽象输入源

Python 的最强大力量之一是它的动态绑定,而动态绑定最强大的用法之一是类文件(file-like)对象

许多需要输入源的函数可以只接收一个文件名,并以读方式打开文件,读取文件,处理完成后关闭它。其实它们不是这样的,而是接收一个类文件对象

在最简单的例子中,类文件对象 是任意一个带有 read 方法的对象,这个方法带有一个可选的 size 参数,并返回一个字符串。调用时如果没有 size 参数,它从输入源中读取所有东西并将所有数据作为单个字符串返回;调用时如果指定了 size 参数,它将从输入源中读取 size 大小的数据并返回这些数据;再次调用的时候,它从余下的地方开始并返回下一块数据。

这就是从真实文件读取数据的工作方式;区别在于你不用把自己局限于真实的文件。输入源可以是任何东西:磁盘上的文件,甚至是一个硬编码的字符串。只要你将一个类文件对象传递给函数,函数只是调用对象的 read 方法,就可以处理任何类型的输入源,而不需要为处理每种类型分别编码。

你可能会纳闷,这和 XML 处理有什么关系。其实 minidom.parse 就是一个可以接收类文件对象的函数。

例 10.1. 从文件中解析 XML

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    
>>> xmldoc = minidom.parse(fsock) 
>>> fsock.close()                 
>>> print xmldoc.toxml()          
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar> 
[1]首先,你要打开一个磁盘上的文件。这会提供给你一个文件对象。
[2]将文件对象传递给 minidom.parse,它调用 fsockread 方法并从磁盘上的文件读取 XML 文档。
[3]确保处理完文件后调用 close 方法。minidom.parse不会替你做这件事。
[4]在返回的 XML 文档上调用 toxml() 方法,打印出整个文档的内容。

哦,所有这些看上去像是在浪费大量的时间。毕竟,你已经看到,minidom.parse 可以只接收文件名,并自动执行所有打开文件和关闭无用文件的行为。不错,如果你知道正要解析的是一个本地文件,你可以传递文件名而且 minidom.parse 可以足够聪明地做正确的事情 (Do The Right Thing?[10]),这一切都不会有问题。但是请注意,使用类文件,会使分析直接从 Internet 上来的 XML 文档变得多么相似和容易!

例 10.2. 解析来自 URL 的 XML

>>> import urllib
>>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') 
>>> xmldoc = minidom.parse(usock)                              
>>> usock.close()                                              
>>> print xmldoc.toxml()                                       
<?xml version="1.0" ?>
<rdf:RDF 
 >
<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>
<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>
<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>
[...snip...] 
[1]正如在前一章中所看到的,urlopen 接收一个 web 页面的 URL 作为参数并返回一个类文件对象。最重要的是,这个对象有一个 read 方法,它可以返回 web 页面的 HTML 源代码。
[2]现在把类文件对象传递给 minidom.parse,它顺从地调用对象的 read 方法并解析 read 方法返回的 XML 数据。这与 XML 数据现在直接来源于 web 页面的事实毫不相干。minidom.parse 并不知道 web 页面,它也不关心 web 页面;它只知道类文件对象。
[3]到这里已经处理完毕了,确保将 urlopen 提供给你的类文件对象关闭。
[4]顺便提一句,这个 URL 是真实的,它真的是一个 XML。它是 Slashdot 站点 (一个技术新闻和随笔站点) 上当前新闻提要的 XML 表示。

例 10.3. 解析字符串 XML (容易但不灵活的方式)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> xmldoc = minidom.parseString(contents) 
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar> 
[1]minidom 有一个方法,parseString,它接收一个字符串形式的完整 XML 文档作为参数并解析这个参数。如果你已经将整个 XML 文档放入一个字符串,你可以使用它代替 minidom.parse

好吧,所以你可以使用 minidom.parse 函数来解析本地文件和远端 URL,但对于解析字符串,你使用……另一个函数。这就是说,如果你要从文件、URL 或者字符串接收输入,就需要特别的逻辑来判断参数是否是字符串,然后调用 parseString。多不让人满意。

如果有一个方法可以把字符串转换成类文件对象,那么你只要这个对象传递给 minidom.parse 就可以了。事实上,有一个模块专门设计用来做这件事:StringIO

例 10.4. StringIO 介绍

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> import StringIO
>>> ssock = StringIO.StringIO(contents)   
>>> ssock.read()                          
"<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read()                          
''
>>> ssock.seek(0)                         
>>> ssock.read(15)                        
'<grammar><ref i'
>>> ssock.read(15)
"d='bit'><p>0</p"
>>> ssock.read()
'><p>1</p></ref></grammar>'
>>> ssock.close() 
[1]StringIO 模块只包含了一个类,也叫 StringIO,它允许你将一个字符串转换为一个类文件对象。 StringIO 类在创建实例时接收字符串作为参数。
[2]现在你有了一个类文件对象,你可用它做类文件的所有事情。比如 read 可以返回原始字符串。
[3]再次调用 read 返回空字符串。真实文件对象的工作方式也是这样的;一旦你读取了整个文件,如果不显式定位到文件的开始位置,就不可能读取到任何其他数据。StringIO 对象以相同的方式进行工作。
[4]使用 StringIO 对象的 seek 方法,你可以显式地定位到字符串的开始位置,就像在文件中定位一样。
[5]将一个 size 参数传递给 read 方法,你还可以以块的形式读取字符串。
[6]任何时候,read 都将返回字符串的未读部分。所有这些严格地按文件对象的方式工作;这就是术语类文件对象 的来历。

例 10.5. 解析字符串 XML (类文件对象方式)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) 
>>> ssock.close()
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar> 
[1]现在你可以把类文件对象 (实际是一个 StringIO) 传递给 minidom.parse,它将调用对象的 read 方法并高兴地开始解析,绝不会知道它的输入源自一个硬编码的字符串。

那么现在你知道了如何使用同一个函数,minidom.parse,来解析一个保存在 web 页面上、本地文件中或硬编码字符串中的 XML 文档。对于一个 web 页面,使用 urlopen 得到类文件对象;对于本地文件,使用 open;对于字符串,使用 StringIO。现在让我们进一步并归纳一下这些 不同。

例 10.6. openAnything

 def openAnything(source):                  
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      
    except (IOError, OSError):            
        pass                              
    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)                
    except (IOError, OSError):            
        pass                              
    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source)) 
[1]openAnything 函数接受单个参数,source,并返回类文件对象。source 是某种类型的字符串;它可能是一个 URL (例如 'http://slashdot.org/slashdot.rdf'),一个本地文件的完整或者部分路径名 (例如 'binary.xml'),或者是一个包含了待解析 XML 数据的字符串。
[2]首先,检查 source 是否是一个 URL。这里通过强制方式进行:尝试把它当作一个 URL 打开并静静地忽略打开非 URL 引起的错误。这样做非常好,因为如果 urllib 将来支持更多的 URL 类型,不用重新编码就可以支持它们。如果 urllib 能够打开 source,那么 return 可以立刻把你踢出函数,下面的 try 语句将不会执行。
[3]另一方面,如果 urllib 向你呼喊并告诉你 source 不是一个有效的 URL,你假设它是一个磁盘文件的路径并尝试打开它。再一次,你不用做任何特别的事来检查 source 是否是一个有效的文件名 (在不同的平台上,判断文件名有效性的规则变化很大,因此不管怎样做都可能会判断错)。反而,只要盲目地打开文件并静静地捕获任何错误就可以了。
[4]到这里,你需要假设 source 是一个其中有硬编码数据的字符串 (因为没有别的可以判断的了),所以你可以使用 StringIO 从中创建一个类文件对象并将它返回。(实际上,由于使用了 str 函数,所以 source 没有必要一定是字符串;它可以是任何对象,你可以使用它的字符串表示形式,只要定义了它的 __str__ 专用方法。)

现在你可以使用这个 openAnything 函数联合 minidom.parse 构造一个函数,接收一个指向 XML 文档的 source,而且无需知道这个 source 的含义 (可以是一个 URL 或是一个本地文件名,或是一个硬编码 XML 文档的字符串形式),然后解析它。

例 10.7. 在 kgp.py 中使用 openAnything

 class KantGenerator:
    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc 

Footnotes

[10] 这是一部著名的电影。――译注

10.2. 标准输入、输出和错误

10.2. 标准输入、输出和错误

UNIX 用户已经对标准输入、标准输出和标准错误的概念非常熟悉了。这一节是为其他不熟悉的人准备的。

标准输入和标准错误 (通常缩写为 stdoutstderr) 是内建在每一个 UNIX 系统中的管道。当你 print 某些东西时,结果前往 stdout 管道;当你的程序崩溃并打印出调试信息 (例如 Python 中的 traceback (错误跟踪)) 的时候,信息前往 stderr 管道。通常这两个管道只与你正在工作的终端窗口相联,所以当一个程序打印时,你可以看到输出,而当一个程序崩溃时,你可以看到调试信息。(如果你正在一个基于窗口的 Python IDE 上工作,stdoutstderr 缺省为你的“交互窗口”。)

例 10.8. stdoutstderr 介绍

>>> for i in range(3):
... print 'Dive in'             
Dive in
Dive in
Dive in
>>> import sys
>>> for i in range(3):
... sys.stdout.write('Dive in') 
Dive inDive inDive in
>>> for i in range(3):
... sys.stderr.write('Dive in') 
Dive inDive inDive in 
[1]正如在例 6.9 “简单计数”中看到的,你可以使用 Python 内置的 range 函数来构造简单的计数循环,即重复某物一定的次数。
[2]stdout 是一个类文件对象;调用它的 write 函数可以打印出你给定的任何字符串。实际上,这就是 print 函数真正做的事情;它在你打印的字符串后面加上一个硬回车,然后调用 sys.stdout.write 函数。
[3]在最简单的例子中,stdoutstderr 把它们的输出发送到相同的地方:Python IDE (如果你在一个 IDE 中的话),或者终端 (如果你从命令行运行 Python 的话)。和 stdout 一样,stderr 并不为你添加硬回车;如果需要,要自己加上。

stdoutstderr 都是类文件对象,就像在第 10.1 节 “抽象输入源”中讨论的一样,但是它们都是只写的。它们都没有 read 方法,只有 write 方法。然而,它们仍然是类文件对象,因此你可以将其它任何 (类) 文件对象赋值给它们来重定向其输出。

例 10.9. 重定向输出

[you@localhost kgp]$ python stdout.py
Dive in
[you@localhost kgp]$ cat out.log
This message will be logged instead of displayed 

(在 Windows 上,你要使用 type 来代替 cat 显示文件的内容。)

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

#stdout.py
import sys
print 'Dive in'                                          
saveout = sys.stdout                                     
fsock = open('out.log', 'w')                             
sys.stdout = fsock                                        print 'This message will be logged instead of displayed' 
sys.stdout = saveout                                     
fsock.close() 
[1]打印输出到 IDE “交互窗口” (或终端,如果从命令行运行脚本的话)。
[2]始终在重定向前保存 stdout,这样的话之后你还可以将其设回正常。
[3]打开一个新文件用于写入。如果文件不存在,将会被创建。如果文件存在,将被覆盖。
[4]所有后续的输出都会被重定向到刚才打开的新文件上。
[5]这样只会将输出结果“打印”到日志文件中;在 IDE 窗口中或在屏幕上不会看到输出结果。
[6]在我们将 stdout 搞乱之前,让我们把它设回原来的方式。
[7]关闭日志文件。

重定向 stderr 以完全相同的方式进行,只要把 sys.stdout 改为 sys.stderr

例 10.10. 重定向错误信息

[you@localhost kgp]$ python stderr.py
[you@localhost kgp]$ cat error.log
Traceback (most recent line last):
  File "stderr.py", line 5, in ?
    raise Exception, 'this error will be logged'
Exception: this error will be logged 

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

#stderr.py
import sys
fsock = open('error.log', 'w')               
sys.stderr = fsock                            raise Exception, 'this error will be logged' 
[1]打开你要存储调试信息的日志文件。
[2]将新打开的日志文件的文件对象赋值给 stderr 以重定向标准错误。
[3]引发一个异常。从屏幕输出上可以注意到这个行为没有 在屏幕上打印出任何东西。所有正常的跟踪信息已经写进 error.log
[4]还要注意你既没有显式关闭日志文件,也没有将 stderr 设回最初的值。这样挺好,因为一旦程序崩溃 (由于引发的异常),Python 将替我们清理并关闭文件,因此永远不恢复 stderr 不会造成什么影响。然而对于 stdout,恢复初始值相对更为重要――你可能会在后面再次操作标准输出。

向标准错误写入错误信息是很常见的,所以有一种较快的语法可以立刻导出信息。

例 10.11. 打印到 stderr

>>> print 'entering function'
entering function
>>> import sys
>>> print >> sys.stderr, 'entering function' 
entering function 
[1]print 语句的快捷语法可以用于写入任何打开的文件 (或者是类文件对象)。在这里,你可以将单个 print 语句重定向到 stderr 而且不用影响后面的 print 语句。

另一方面,标准输入是一个只读文件对象,它表示从前一个程序到这个程序的数据流。这个对于老的 Mac OS 用户和 Windows 用户可能不太容易理解,除非你受到过 MS-DOS 命令行的影响。在 MS-DOS 命令行中,你可以使用一行指令构造一个命令的链,使得一个程序的输出就可以成为下一个程序的输入。第一个程序只是简单地输出到标准输出上 (程序本身没有做任何特别的重定向,只是执行了普通的 print 语句等),然后,下一个程序从标准输入中读取,操作系统就把一个程序的输出连接到一个程序的输入。

例 10.12. 链接命令

[you@localhost kgp]$ python kgp.py -g binary.xml         
01100111
[you@localhost kgp]$ cat binary.xml                      
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
[you@localhost kgp]$ cat binary.xml | python kgp.py -g -  
10110001 
[1]正如你在第 9.1 节 “概览”中看到的,该命令将只打印一个随机的八位字符串,其中只有 0 或者 1
[2]该处只是简单地打印出整个 binary.xml 文档的内容。(Windows 用户应该用 type 代替 cat。)
[3]该处打印 binary.xml 的内容,但是“&#124;”字符,称为“管道”符,说明内容不会打印到屏幕上;它们会成为下一个命令的标准输入,在这个例子中是你调用的 Python 脚本。
[4]为了不用指定一个文件 (例如 binary.xml),你需要指定“-”,它会使得你的脚本从标准输入载入脚本,而不是从磁盘上的特定文件。 (下一个例子更多地说明了这是如何实现的)。所以效果和第一种语法是一样的,在那里你要直接指定语法文件,但是想想这里的扩展性。让我们把 cat binary.xml 换成别的什么东西――例如运行一个脚本动态生成语法――然后通过管道将它导入你的脚本。它可以来源于任何地方:数据库,或者是生成语法的元脚本,或者其他。你根本不需要修改你的 kgp.py 脚本就可以并入这个功能。你要做的仅仅是从标准输入取得一个语法文件,然后你就可以将其他的逻辑分离出来,放到另一程序中去了。

那么脚本是如何“知道”在语法文件是“-”时从标准输入读取? 其实不神奇;它只是代码。

例 10.13. 在 kgp.py 中从标准输入读取

 def openAnything(source):
    if source == "-":    
        import sys
        return sys.stdin
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib
    try:
[... snip ...] 
[1]这是 toolbox.py 中的 openAnything 函数,以前在第 10.1 节 “抽象输入源”中你已经检视过了。所有你要做的就是在函数的开始加入 3 行代码来检测源是否是“-”;如果是,返回 sys.stdin。就这么简单!记住,stdin 是一个拥有 read 方法的类文件对象,所以其它的代码 (在 kgp.py 中,在那里你调用了 openAnything) 一点都不需要改动。

10.3. 查询缓冲节点

10.3. 查询缓冲节点

kgp.py 使用了多种技巧,在你进行 XML 处理时,它们或许能派上用场。第一个就是,利用输入文档的结构稳定特征来构建节点缓冲。

一个语法文件定义了一系列的 ref 元素。每个 ref 包含了一个或多个 p 元素,p 元素则可以包含很多不同的东西,包括 xref。对于每个 xref,你都能找到相对应的 ref 元素 (它们具有相同的 id 属性),然后选择 ref 元素的子元素之一进行解析。(在下一部分中你将看到是如何进行这种随机选择的。)

语法的构建方式如下:先为最小的片段定义 ref 元素,然后使用 xref 定义“包含”第一个 ref 元素的 ref 元素,等等。然后,解析“最大的”引用并跟着 xref 跳来跳去,最后输出真实的文本。输出的文本依赖于你每次填充 xref 时所做的 (随机) 决策,所以每次的输出都是不同的。

这种方式非常灵活,但是有一个不好的地方:性能。当你找到一个 xref 并需要找到相应的 ref 元素时,会遇到一个问题。xrefid 属性,而你要找拥有相同 id 属性的 ref 元素,但是没有简单的方式做到这件事。较慢的方式是每次获取所有 ref 元素的完整列表,然后手动遍历并检视每一个 id 属性。较快的方式是只做一次,然后以字典形式构建一个缓冲。

例 10.14. loadGrammar

 def loadGrammar(self, grammar):                         
        self.grammar = self._load(grammar)                  
        self.refs = {}                                       
        for ref in self.grammar.getElementsByTagName("ref"): 
            self.refs[ref.attributes["id"].value] = ref 
[1]从创建一个空字典 self.refs 开始。
[2]正如你在第 9.5 节 “搜索元素”中看到的,getElementsByTagName 返回所有特定名称元素的一个列表。你可以很容易地得到所有 ref 元素的一个列表,然后遍历这个列表。
[3]正如你在第 9.6 节 “访问元素属性”中看到的,使用标准的字典语法,你可以通过名称来访问个别元素。所以,self.refs 字典的键将是每个 ref 元素的 id 属性值。
[4]self.refs 字典的值将是 ref 元素本身。如你在第 9.3 节 “XML 解析”中看到的,已解析 XML 文档中的每个元素、节点、注释和文本片段都是一个对象。

只要构建了这个缓冲,无论何时你遇到一个 xref 并且需要找到具有相同 id 属性的 ref 元素,都只需在 self.refs 中查找它。

例 10.15. 使用 ref 元素缓冲

 def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id])) 

你将在下一部分探究 randomChildElement 函数。

10.4. 查找节点的直接子节点

10.4. 查找节点的直接子节点

解析 XML 文档时,另一个有用的己技巧是查找某个特定元素的所有直接子元素。例如,在语法文件中,一个 ref 元素可以有数个 p 元素,其中每一个都可以包含很多东西,包括其他的 p 元素。你只要查找作为 ref 孩子的 p 元素,不用查找其他 p 元素的孩子 p 元素。

你可能认为你只要简单地使用 getElementsByTagName 来实现这点就可以了,但是你不可以这么做。getElementsByTagName 递归搜索并返回所有找到的元素的单个列表。由于 p 元素可以包含其他的 p 元素,你不能使用 getElementsByTagName,因为它会返回你不要的嵌套 p 元素。为了只找到直接子元素,你要自己进行处理。

例 10.16. 查找直接子元素

 def randomChildElement(self, node):
        choices = [e for e in node.childNodes
                   if e.nodeType == e.ELEMENT_NODE]   
        chosen = random.choice(choices)             
        return chosen 
[1]正如你在例 9.9 “获取子节点”中看到的,childNodes 属性返回元素所有子节点的一个列表。
[2]然而,正如你在例 9.11 “子节点可以是文本”中看到的,childNodes 返回的列表包含了所有不同类型的节点,包括文本节点。这并不是你在这里要查找的。你只要元素形式的孩子。
[3]每个节点都有一个 nodeType 属性,它可以是ELEMENT_NODE, TEXT_NODE, COMMENT_NODE,或者其它值。可能值的完整列表在 xml.dom 包的 __init__.py 文件中。(关于包的介绍,参见第 9.2 节 “包”。) 但你只是对元素节点有兴趣,所以你可以过滤出一个列表,其中只包含 nodeTypeELEMENT_NODE的节点。
[4]只要拥有了一个真实元素的列表,选择任意一个都很容易。Python 有一个叫 random 的模块,它包含了好几个有用的函数。random.choice 函数接收一个任意数量条目的列表并随机返回其中的一个条目。比如,如果 ref 元素包含了多个 p 元素,那么 choices 将会是 p 元素的一个列表,而 chosen 将被赋予其中的某一个值,而这个值是随机选择的。

10.5. 根据节点类型创建不同的处理器

10.5. 根据节点类型创建不同的处理器

第三个有用的 XML 处理技巧是将你的代码基于节点类型和元素名称分散到逻辑函数中。解析后的 XML 文档是由各种类型的节点组成的,每一个都是通过 Python 对象表示的。文档本身的根层次通过一个 Document 对象表示。Document 还包含了一个或多个 Element 对象 (表示 XML 标记),其中的每一个可以包含其它的 Element 对象、Text 对象 (表示文本),或者 Comment 对象 (表示内嵌注释)。使用 Python 编写分离各个节点类型逻辑的分发器非常容易。

例 10.17. 已解析 XML 对象的类名

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('kant.xml') 
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__                   
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__          
'Document' 
[1]暂时假设 kant.xml 在当前目录中。
[2]正如你在第 9.2 节 “包”中看到的,解析 XML 文档返回的对象是一个 Document 对象,就像在 xml.dom 包的 minidom.py 中定义的一样。又如你在第 5.4 节 “类的实例化”中看到的,__class__ 是每个 Python 对象的一个内置属性。
[3]此外,__name__ 是每个 Python 类的内置属性,是一个字符串。这个字符串并不神秘;它和你在定义类时输入的类名相同。(参见第 5.3 节 “类的定义”。)

好,现在你能够得到任何给定 XML 节点的类名了 (因为每个 XML 节点都是以一个 Python 对象表示的)。你怎样才能利用这点来分离解析每个节点类型的逻辑呢?答案就是 getattr,你第一次见它是在第 4.4 节 “通过 getattr 获取对象引用”中。

例 10.18. parse,通用 XML 节点分发器

 def parse(self, node):          
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__)  
        parseMethod(node) 
[1]首先,注意你正在基于传入节点 (node 参数) 的类名构造一个较大的字符串。所以如果你传入一个 Document 节点,你就构造了字符串 'parse_Document',其它类同于此。
[2]现在你可以把这个字符串当作一个函数名称,然后通过 getattr 得到函数自身的引用。
[3]最后,你可以调用函数并将节点自身作为参数传入。下一个例子将展示每个函数的定义。

例 10.19. parse 分发器调用的函数

 def parse_Document(self, node): 
        self.parse(node.documentElement)
    def parse_Text(self, node):    
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)
    def parse_Comment(self, node): 
        pass
    def parse_Element(self, node): 
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node) 
[1]parse_Document 只会被调用一次,因为在一个 XML 文档中只有一个 Document 节点,并且在已解析 XML 的表示中只有一个 Document 对象。在此它只是起到中转作用,转而解析语法文件的根元素。
[2]parse_Text 在节点表示文本时被调用。这个函数本身做某种特殊处理,自动将句子的第一个单词进行大写处理,而不是简单地将表示的文本追加到一个列表中。
[3]parse_Comment 只有一个 pass,因为你并不关心语法文件中嵌入的注释。但是注意,你还是要定义这个函数并显式地让它不做任何事情。如果这个函数不存在,通用 parse 函数在遇到一个注释的时候会执行失败,因为它试图找到并不存在的 parse_Comment 函数。为每个节点类型定义独立的函数――甚至你不要使用的――将会使通用 parse 函数保持简单和沉默。
[4]parse_Element 方法其实本身就是一个分发器,一个基于元素的标记名称的分发器。这个基本概念是相同的:使用元素的区别 (它们的标记名称) 然后针对每一个分发到一个独立的函数。你构建了一个类似于 'do_xref' 的字符串 (对 &lt;xref&gt; 标记而言),找到这个名称的函数,并调用它。对其它的标记名称 (像&lt;p&gt;&lt;choice&gt;) 在解析语法文件的时候都可以找到类似的函数。

在这个例子中,分发函数 parseparse_Element 只是找到相同类中的其它方法。如果你进行的处理过程很复杂 (或者你有很多不同的标记名称),你可以将代码分散到独立的模块中,然后使用动态导入的方式导入每个模块并调用你需要的任何函数。动态导入将在第十六章 函数编程中介绍。

10.6. 处理命令行参数

10.6. 处理命令行参数

Python 完全支持创建在命令行运行的程序,也支持通过命令行参数和短长样式来指定各种选项。这些并非是 XML 特定的,但是这样的脚本可以充分使用命令行处理,看来是时候提一下它了。

如果不理解命令行参数如何暴露给你的 Python 程序,讨论命令行处理是很困难的,所以让我们先写个简单点的程序来看一下。

例 10.20. sys.argv 介绍

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

#argecho.py
import sys
for arg in sys.argv: 
    print arg 
[1]每个传递给程序的命令行参数都在 sys.argv 中,而它仅仅是一个列表。这里我们在独立行中打印出每个参数。

例 10.21. sys.argv 的内容

[you@localhost py]$ python argecho.py             
argecho.py
[you@localhost py]$ python argecho.py abc def     
argecho.py
abc
def
[you@localhost py]$ python argecho.py --help      
argecho.py
--help
[you@localhost py]$ python argecho.py -m kant.xml 
argecho.py
-m
kant.xml 
[1]关于 sys.argv 需要了解的第一件事情就是:它包含了你正在调用的脚本的名称。你后面会实际使用这个知识,在第十六章 函数编程中。现在不用担心。
[2]命令行参数通过空格进行分隔。在 sys.argv 列表中,每个参数都是一个独立的元素。
[3]命令行标志,像 --help,在 sys.argv 列表中还保存了它们自己的元素。
[4]为了让事情更有趣,有些命令行标志本身就接收参数。比如,这里有一个标记 (-m) 接收一个参数 (kant.xml)。标记自身和标记参数只是 sys.argv 列表中的一串元素。并没有试图将元素与其它元素进行关联;所有你得到的是一个列表。

所以正如你所看到的,你确实拥有了命令行传入的所有信息,但是接下来要实际使用它似乎不那么容易。对于只是接收单个参数或者没有标记的简单程序,你可以简单地使用 sys.argv[1] 来访问参数。这没有什么羞耻的;我一直都是这样做的。对更复杂的程序,你需要 getopt 模块。

例 10.22. getopt 介绍

 def main(argv):                         
    grammar = "kant.xml"                 
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) 
    except getopt.GetoptError:           
        usage()                          
        sys.exit(2)                     
...
if __name__ == "__main__":
    main(sys.argv[1:]) 
[1]首先,看一下例子的最后并注意你正在调用 main 函数,参数是 sys.argv[1:]。记住,sys.argv[0] 是你正在运行脚本的名称;在处理命令行时,你不用关心它,所以你可以砍掉它并传入列表的剩余部分。
[2]这里就是所有有趣处理发生的地方。getopt 模块的 getopt 函数接受三个参数:参数列表 (你从 sys.argv[1:] 得到的)、一个包含了程序所有可能接收到的单字符命令行标志,和一个等价于单字符的长命令行标志的列表。第一次看的时候,这有点混乱,下面有更多的细节解释。
[3]在解析这些命令行标志时,如果有任何事情错了,getopt 会抛出异常,你可以捕获它。你可以告诉 getopt 你明白的所有标志,那么这也意味着终端用户可以传入一些你不理解的命令行标志。
[4]和 UNIX 世界中的标准实践一样,如果脚本被传入了不能理解的标志,你要打印出正确用法的一个概要并友好地退出。注意,在这里我没有写出 usage 函数。你还是要在某个地方写一个,使它打印出合适的概要;它不是自动的。

那么你传给 getopt 函数的参数是什么呢?好的,第一个只不过是一个命令行标志和参数的原始列表 (不包括第一个元素――脚本名称,你在调用 main 函数之前就已经将它砍掉了)。第二个是脚本接收的短命令行标志的一个列表。

"hg:d"

-h

打印用法概要

-g ...

使用给定的语法文件或 URL

-d

在解析时显示调试信息

第一个标志和第三个标志是简单的独立标志;你选择是否指定它们,它们做某些事情 (打印帮助) 或者改变状态 (打开调试)。但是,第二个标志 (-g) 必须 跟随一个参数――进行读取的语法文件的名称。实际上,它可以是一个文件名或者一个 web 地址,这时还不知道 (后面会确定),但是你要知道必须要有些东西。所以,你可以通过在 getopt 函数的第二个参数的 g 后面放一个冒号,来向 getopt 说明这一点。

更复杂的是,这个脚本既接收短标志 (像 -h),也接受长标志 (像 --help),并且你要它们做相同的事。这就是 getopt 第三个参数存在的原因:它是指定长标志的一个列表,其中的长标志是和第二个参数中指定的短标志相对应的。

["help", "grammar="]

--help

打印用法概要

--grammar ...

使用给定的语法文件或 URL

这里有三点要注意:

  1. 所有命令行中的长标志以两个短划线开始,但是在调用 getopt 时,你不用包含这两个短划线。它们是能够被理解的。
  2. --grammar 标志的后面必须跟着另一个参数,就像 -g 标志一样。通过等于号标识出来:"grammar="
  3. 长标志列表比短标志列表更短一些,因为 -d 标志没有相应的长标志。这很好;只有 -d 才会打开调试。但是短标志和长标志的顺序必须是相同的,你应该先指定有长标志的短标志,然后才是剩下的短标志。

被搞昏没?让我们看一下真实的代码,看看它在上下文中是否起作用。

例 10.23. 在 kgp.py 中处理命令行参数

 def main(argv):                          
    grammar = "kant.xml"                
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
        usage()                         
        sys.exit(2)                     
    for opt, arg in opts:                
        if opt in ("-h", "--help"):      
            usage()                     
            sys.exit()                  
        elif opt == '-d':                
            global _debug               
            _debug = 1                  
        elif opt in ("-g", "--grammar"): 
            grammar = arg               
    source = "".join(args)               
    k = KantGenerator(grammar, source)
    print k.output() 
[1]grammar 变量会跟踪你正在使用的语法文件。如果你没有在命令行指定它 (使用 -g 或者 --grammar 标志定义它),在这里你将初始化它。
[2]你从 getopt 取回的 opts 变量是一个由元组 (flagargument) 组成的列表。如果标志没有带任何参数,那么 arg 只是 None。这使得遍历标志更容易了。
[3]getopt 验证命令行标志是否可接受,但是它不会在短标志和长标志之间做任何转换。如果你指定 -h 标志,opt 将会包含 "-h";如果你指定 --help 标志,opt 将会包含"--help" 标志。所以你需要检查它们两个。
[4]别忘了,-d 标志没有相应的长标志,所以你只需要检查短形式。如果你找到了它,你就可以设置一个全局变量来指示后面要打印出调试信息。(我习惯在脚本的开发过程中使用它。什么,你以为所有这些程序都是一次成功的?)
[5]如果你找到了一个语法文件,跟在 -g 或者 --grammar 标志后面,那你就要把跟在后面的参数 (arg) 保存到变量grammar 中,覆盖掉在 main 函数你初始化的默认值。
[6]就是这样。你已经遍历并处理了所有的命令行标志。这意味着所有剩下的东西都必须是命令行参数。它们由 getopt 函数的 args 变量返回。在这个例子中,你把它们当作了解析器源材料。如果没有指定命令行参数,args 将是一个空列表,而 source 将是空字符串。

10.7. 全部放在一起

10.7. 全部放在一起

你已经了解很多基础的东西。让我们回来看看所有片段是如何整合到一起的。

作为开始,这里是一个接收命令行参数的脚本,它使用 getopt 模块。

 def main(argv):                         
...
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
...
    for opt, arg in opts:               
... 

创建 KantGenerator 类的一个实例,然后将语法文件和源文件传给它,可能在命令行没有指定。

 k = KantGenerator(grammar, source) 

KantGenerator 实例自动加载语法,它是一个 XML 文件。你使用自定义的 openAnything 函数打开这个文件 (可能保存在一个本地文件中或者一个远程服务器上),然后使用内置的 minidom 解析函数将 XML 解析为一棵 Python 对象树。

 def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close() 

哦,根据这种方式,你将使用到 XML 文档结构的知识建立一个引用的小缓冲,这些引用都只是 XML 文档中的元素。

 def loadGrammar(self, grammar):                         
        for ref in self.grammar.getElementsByTagName("ref"):
            self.refs[ref.attributes["id"].value] = ref 

如果你在命令行中指定了某些源材料,你可以使用它;否则你将打开语法文件查找“顶层”引用 (没有被其它的东西引用) 并把它作为开始点。

 def getDefaultSource(self):
        xrefs = {}
        for xref in self.grammar.getElementsByTagName("xref"):
            xrefs[xref.attributes["id"].value] = 1
        xrefs = xrefs.keys()
        standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
        return '<xref id="%s"/>' % random.choice(standaloneXrefs) 

现在你打开了了源材料。它是一个 XML,你每次解析一个节点。为了让代码分离并具备更高的可维护性,你可以使用针对每个节点类型的独立处理方法。

 def parse_Element(self, node): 
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node) 

你在语法里面跳来跳去,解析每一个 p 元素的所有孩子,

 def do_p(self, node):
...
        if doit:
            for child in node.childNodes: self.parse(child) 

用任意一个孩子替换 choice 元素,

 def do_choice(self, node):
        self.parse(self.randomChildElement(node)) 

并用对应 ref 元素的任意孩子替换 xref,前面你已经进行了缓冲。

 def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id])) 

就这样一直解析,最后得到普通文本。

 def parse_Text(self, node):    
        text = node.data
...
            self.pieces.append(text) 

把结果打印出来。

 def main(argv):                         
...
    k = KantGenerator(grammar, source)
    print k.output() 

10.8. 小结

10.8. 小结

Python 带有解析和操作 XML 文档非常强大的库。minidom 接收一个 XML 文件并将其解析为 Python 对象,并提供了对任意元素的随机访问。进一步,本章展示了如何利用 Python 创建一个“真实”独立的命令行脚本,连同命令行标志、命令行参数、错误处理,甚至从前一个程序的管道接收输入的能力。

在继续下一章前,你应该无困难地完成所有这些事情:

  • 通过标准输入输出链接程序
  • 使用 getattr 定义动态分发器
  • 通过 getopt 使用命令行标志并进行验证