第 10 章. 脚本和流

10.1. 抽象输入源

Python 最大的优势之一是其动态绑定,而动态绑定的一种强大用途是 类文件对象

许多需要输入源的函数可以简单地接收文件名,打开文件进行读取,读取文件,并在完成后关闭文件。但它们并没有这样做。相反,它们接收一个 类文件对象

在最简单的情况下,类文件对象 是指任何具有 read 方法的对象,该方法带有一个可选的 size 参数,并返回一个字符串。当不带 size 参数调用时,它将从输入源读取所有内容,并将所有数据作为单个字符串返回。当使用 size 参数调用时,它将从输入源读取该数量的数据并返回该数量的数据;当再次调用时,它将从上次停止的地方继续读取并返回下一块数据。

这就是 从真实文件读取 的工作原理;区别在于您不局限于真实文件。输入源可以是任何东西:磁盘上的文件、网页,甚至是硬编码的字符串。只要您将类文件对象传递给函数,并且该函数只调用对象的 read 方法,该函数就可以处理任何类型的输入源,而无需针对每种类型编写特定的代码。

如果您想知道这与 XML 处理有什么关系,minidom.parse 就是这样一个可以接收类文件对象的函数。

示例 10.1. 从文件解析 XML

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    1
>>> xmldoc = minidom.parse(fsock) 2
>>> fsock.close()                 3
>>> print xmldoc.toxml()          4
<?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 足够聪明,可以 做正确的事情™。但请注意,从 Internet 直接解析 XML 文档是多么相似——而且容易。

示例 10.2. 从 URL 解析 XML

>>> import urllib
>>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') 1
>>> xmldoc = minidom.parse(usock)                              2
>>> usock.close()                                              3
>>> print xmldoc.toxml()                                       4
<?xml version="1.0" ?>
<rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<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 接收一个网页 URL 并返回一个类文件对象。最重要的是,此对象有一个 read 方法,该方法返回网页的 HTML 源代码。
2 现在,将类文件对象传递给 minidom.parse,它将服从地调用对象的 read 方法并解析 read 方法返回的 XML 数据。这个 XML 数据现在直接来自网页这一事实完全无关紧要。minidom.parse 不知道网页,也不关心网页;它只知道类文件对象。
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) 1
>>> 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)   1
>>> ssock.read()                          2
"<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read()                          3
''
>>> ssock.seek(0)                         4
>>> ssock.read(15)                        5
'<grammar><ref i'
>>> ssock.read(15)
"d='bit'><p>0</p"
>>> ssock.read()
'><p>1</p></ref></grammar>'
>>> ssock.close()                         6
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) 1
>>> 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 来解析存储在网页、本地文件或硬编码字符串中的 XML 文档。对于网页,使用 urlopen 获取类文件对象;对于本地文件,使用 open;对于字符串,使用 StringIO。现在让我们更进一步,概括 这些 差异。

示例 10.6. openAnything


def openAnything(source):                  1
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      2
    except (IOError, OSError):            
        pass                              

    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)                3
    except (IOError, OSError):            
        pass                              

    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source))  4
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(作为 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