5.7. 提取输入源

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

许多需要输入源的函数可以简单地接收一个文件名,以读方式打开文件,读出它,处理完后关毕它。但这些函数不是这样的。相反,它们接收一个类文件对象

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

上面就是从真正文件中读取是如何工作的;区别就在于我们没有把自已局限于真正的文件。输入源可以是任何东西:磁盘上的文件,网页,甚至可以是一个硬编码的字符串。只要我们将一个类文件对象传给函数,只需要调用对象的 read 方法,就可以处理任何类型的输入源,而不需要处理每种类型的特别代码。

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

例 5.25. 从文件中分析 XML

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    1
>>> xmldoc = minidom.parse(fsock) 2
>>> fsock.close()                 3
>>> print xmldoc
<?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 不会替你做这件事。

哦,所有这些看上去象是在浪费大量的时间。毕竟我们已经看到过 minidom.parse 可以只接收文件名,并自动执行所有打开和关闭不再用的文件。不错,如果你知道正要分析一个本地文件,你是可以传递文件名,且 minidom.parse 可以足够灵活地“做正确的事”。但请注意,使用类文件对象直接从Internet来分析XML文档是多么相似和容易呀。

例 5.26. 从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>

[...略...]
1

正如我们在前一章所看到的,urlopen 接收一个web页面的URL并返回一个类文件对象。最重要的是,这个对象有一个 read 方法,它可以返回web页面的HTML源代码。

2

现在我们将类文件对象传给 minidom.parse,它顺从地调用对象的 read 方法并对 read 方法所返回的XML数据进行分析。这与XML数据现在是直接从一个web页面来的事实毫不相干。minidom.parse 不了解web页面,同时也不关心;它只知道类文件对象。

3

一理你处理完毕,确保将 urlopen 提供给你的类文件对象关闭了。

4

随便说一句,这个URL是真实的,它真是一个XML。它是位于Slashdot站点上的当前标题的XML表示,一条技术新闻和站点一瞥。

例 5.27. 从字符串中分析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

OK,所以我们可以使用 minidom.parse 函数来分析本地文件和远端URL,但对于分析字符串,我们使用…一个不同的函数。这就是说,如果你想能够从文件,URL,或字符串接收输入,我们需要特别的逻辑来检查它是否是一个字符串,从而调用 parseString 函数。多不让人满意。

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

例 5.28. 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 将返回你还未读出的字符串的剩余部分。所有这些严格地按文件对象的方式工作,这就是术语类文件对象的来历。

例 5.29. 从字符串中分析XML(类文件对象方式)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) 1
>>> 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。现在让我们更深一步同时也把这些不同之处归纳一下。

例 5.30. 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                         

    # assume source is string
    import StringIO                  
    return StringIO.StringIO(source)  4
1

openAnything 函数接收一个参数,source,并返回一个类对象。source 是某种类型的字符串;它可以是一个URL(如 'http://slashdot.org/slashdot.rdf'),一个本地文件的完整或部分路径名(如 'binary.xml'),或一个将要被分析的包含实际XML数据的字符串。

2

首先,我们查看 source 是否是一个URL。我们通过强制力来完成判断:我们试着把它作为URL进行打开,并静静地把由于打开非URL的东西时产生的错误忽略掉。感觉上这样做非常好,如果 urllib 在以后还会支持新的URL类型,不用重新编码我们也可以支持它们。

3

如果 urllib 向我们呼喊并告诉我们那个 source 不是一个有效的URL,我们则假定它是一个磁盘文件的路径,并试着打开它。再一次,我们不做任何特别的事情来检查 source 是否是一个有效的文件名(总之在不同的平台上,判断文件名有效性的规则变化很大,那么不管怎样做我们都可能会判断错)。相反,我们只盲目地打开文件,并静静地俘获任何错误。

4

到了这个地方,我们需要假定 source 是一个其中有硬编码的字符串(因为没有什么可以判断的了),所以我们使用 StringIO 来从中生成一个类文件对象并将其返回。

现在我们可以使用这个 openAnything 函数同 minidom.parse 联系起来生成一个函数,它接收一个一个不知从哪来的XML文档,source (或者为一个URL,或一个本地文件,或在一个字符串中的硬编码XML文档)并分析它。

例 5.31. 在 kgp.py 中使用 openAnything

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