第3部分 补充/实验章节
第14章 文本处理
作为开发者,我更愿意编辑纯文本。但XML不算纯文本。
——Wesley Chun,2009年7月
(在OSCON会议上说过的话)
本章内容:
逗号分隔值(CSV);
JSON;
可扩展标记语言;
相关模块。
无论创建什么类型的应用,最终都需要处理人类可读的数据,这类数据一般都是文本。Python 标准库含有 3 个文本处理模块和包,它们可以完成这个任务:csv、json、xml。本章将依次介绍它们。
在本章末尾,我们将XML与第2章中介绍的其他客户端——服务器的知识结合起来,展示如何使用Python创建XML-RPC服务。由于这种编程方式不考虑文本处理,因此无需自行处理XML,只用进行一些数据格式转换。读者可以将最后一节作为附加材料。
14.1 逗号分隔值(CSV)
本节将介绍逗号分隔值(Comma-Separated Value,CSV)。首先简要介绍 CSV,接着通过示例介绍如何使用Python读写CSV文件,最后回顾前面的一个例子。
14.1.1 CSV简介
与专有的二进制文件格式截然不同,CSV通常用于在电子表格软件和纯文本之间交互数据。实际上,CSV都不算是一个真正的结构化数据,CSV文件内容仅仅是一些用逗号分隔的原始字符串值。不同的CSV格式有一些微妙的区别,但总体上,这些区别影响不大。大多数情况下,甚至都不需要用到专门针对CSV的模块。
听起来好像很容易解析CSV文件,是吗?可能不假思索地认为只须调用str.split(',')即可。但不能这样做,因为有些字段值可能会含有嵌套的逗号,因此需要专门用于解析和生成CSV的库,如Python的csv模块。
来看一个简短的例子,该示例获取数据,以CSV格式输出到文件中,接着将同样的数据读回。后面还会处理单个字段中就含有逗号的情形,来让示例稍微复杂一些。示例14-1展示了csvex.py,该脚本接受三元组,将对应的记录作为CSV文件写到磁盘上,接着读取并解析刚刚写入的CSV数据。
示例14-1 CSV示例,兼容Python 2和Python 3(csvex.py)
这个简单的脚本演示了将数据转成CSV格式写出,并再次读取。
接下来是另一个兼容Python 2和Python 3的示例脚本。无论使用Python 2还是Python 3,最终输出完全相同。
$ python csvex.py
* WRITING CSV DATA
* REVIEW OF SAVED DATA
Chapter 9: 'Web Clients and Servers' (featuring base64, urllib)
Chapter 10: 'Web Programming: CGI & WSGI' (featuring cgi, time, wsgiref)
Chapter 13: 'Web Services' (featuring urllib, twython)
逐行解释
第1~10行
首先导入csv模块以及distutils.log.warn(),后者作为print语句或函数的代理(print语句和函数只在单个字符串作为参数的情况下相同,使用代理后可以消除这个限制)。紧接着是数据集的导入语句。该数据集是三元组,每个元素占用一列,包括章号、章名、该章代码示例中使用的模块和包。
第12~17行
这 6 行的意思很清楚。csv.writer()函数需要一个打开的文件(或类文件)对象,返回一个writer对象。writer提供了writerow()方法,可以用来在打开的文件中逐行写入逗号分隔的数据。写入完成后,关闭该文件。
第19~25行
在这一部分,csv.reader()函数与csv.writer()相反,用于返回一个可迭代对象,可以读取该对象并解析为CSV数据的每一行。与csv.writer()类似,csv.reader()也使用一个已打开文件的句柄,返回一个reader对象。当逐行迭代数据时,CSV数据会自动解析并返回给用户(第22行)。逐行显示数据,处理完后就关闭文件。
除了csv.reader()和csv.writer()之外,csv模块还提供了csv.DictReader类和csv.DictWriter类,用于将CSV数据读进字典中(首先查找是否使用给定字段名,如果没有,就是用第一行作为键),接着将字典字段写入CSV文件中。
14.1.2 再论股票投资组合示例
在介绍另一个文本处理格式之前,先来看看这个示例。回顾第13章介绍的股票投资组合脚本,即stokc.py。这里不用str.split(','),而是在应用中使用csv模块。
另外,这里不会列出所有代码,大部分代码都与stock.py相同,所以只关注其中有改动的部分。下面是Python 2版本的完整的stock.py脚本(可以随时回顾第13章查看有关代码的逐行解释)。
!/usr/bin/env python
from time import ctime
from urllib2 import urlopen
TICKs = ('yhoo', 'dell', 'cost', 'adbe', 'intc')
URL = 'http://quote.yahoo.com/d/quotes.csv?s=%s&f=sl1c1p2'
print '\nPrices quoted as of: %s PDT\n' % ctime()
print 'TICKER', 'PRICE', 'CHANGE', '%AGE'
print '———', '——-', '———', '——'
u = urlopen(URL % ','.join(TICKs))
for row in u:
tick, price, chg, per = row.split(',')
print tick, '%.2f' % float(price), chg, per,
u.close()
修改版与原版的输出内容相似。作为比较,下面是其中一个输出结果。
Prices quoted as of: Sat Oct 29 02:06:24 2011 PDT
TICKER PRICE CHANGE %AGE
"YHOO" 16.56 -0.07 "-0.42%"
"DELL" 16.31 -0.01 "-0.06%"
"COST" 84.93 -0.29 "-0.34%"
"ADBE" 29.02 +0.68 "+2.40%"
"INTC" 24.98 -0.15 "-0.60%"
所要做的就是将stock.py中的代码复制到名为stockcsv.py新脚本中,接着进行相应的更改,使用csv模块。现在来看看不同点,重点讨论 urlopen()调用之后的代码。打开文件之后,就将文件传递给csv.reader(),如下所示。
reader = csv.reader(u)
for tick, price, chg, pct in reader:
print tick.ljust(7), ('%.2f' % round(float(price), 2)).rjust(6), \chg.rjust(6), pct.rstrip().rjust(6)
u.close()
for循环的大部分都相同,除了无须读取整行后用逗号分隔。现在使用csv模块解析数据,让用户用循环变量来指定目标字段的名称。注意,输出结果虽然相似,但并不是精确匹配。读者能找到其中的不同之处吗(除了时间戳)?下面是输出结果。
Prices quoted as of: Sun Oct 30 23:19:04 2011 PDT
TICKER PRICE CHANGE %AGE
YHOO 16.56 -0.07 -0.42%
DELL 16.31 -0.01 -0.06%
COST 84.93 -0.29 -0.34%
ADBE 29.02 +0.68 +2.40%
INTC 24.98 -0.15 -0.60%
这里的区别很小。有些字段在 str.split()版本中用引号括起来,但 csv 版本中没有。为什么会这样?回忆第13章的内容,有些值返回时带有引号,在那一章的末尾,还有一个练习要求手动去除额外的引号。
使用csv模块处理CSV数据时就不成问题了,csv模块会查找和擦除从Yahoo!服务器中获取数据时带来的引号。下面这段代码确认这些数据中含有额外的引号。
>>> from urllib2 import urlopen
>>> URL = 'http://quote.yahoo.com/d/quotes.csv?s=goog&f=sl1c1p2'
>>> u = urlopen(URL, 'r')
>>> line = u.read()
>>> u.close()
>>> line
'"GOOG",598.67,+12.36,"+2.11%"\r\n'
引号是额外的麻烦,但通过csv模块,开发者就无须处理这个问题了。由于无须额外的字符串处理流程,代码也变得更加易读。
为了改善数据管理,如果数据使用更具有层次化的方式表达就更好了。例如,如果每一行作为单个对象,而price、change、percentage作为这个对象的属性。由于每一行CSV含有4个值,若不使用第一个值作为主键或其他类似的约定,此时就没有“主键”。这种情况下JSON就可能更适合这个应用。
14.2 JSON
从JavaScript对象表示法(或JSON)这个名字就可以看出,它来自于JavaScript领域, JSON是JavsScript的子集,专门用于指定结构化的数据。其基于ECMA-262标准,与本章最后一节介绍的XML相比,JSON是轻量级的数据交换方式。JSON是以人类更易读的方式传输结构化数据。关于JSON的更多信息可以访问http://json.org。
从 Python 2.6 开始,通过标准库 json 模块正式支持了 JSON。其基本上就是外部simplejson库的集成版,其开发者还反向兼容了2.5版本。更多信息可访问http://github.com/simplejson/simplejson。
另外,json(以及simplejson)提供了与pickle和marshal类似的接口,即dump()/load()和 dumps()/loads()。除了基本参数外,这些函数还包括许多仅用于 JSON 的选项。模块还包括encoder类和decoder类,用户既可以继承,也可以直接使用。
JSON对象非常像Python的字典,如下所示,使用字典将数据转成JSON对象,接着再转换回来。
>>> dict(zip('abcde', range(5)))
{'a': 0, 'c': 2, 'b': 1, 'e': 4, 'd': 3}
>>> json.dumps(dict(zip('abcde', range(5))))
'{"a": 0, "c": 2, "b": 1, "e": 4, "d": 3}'
>>> json.loads(json.dumps(dict(zip('abcde', range(5)))))
{u'a': 0, u'c': 2, u'b': 1, u'e': 4, u'd': 3}
注意,JSON只理解Unicode字符串,所以在转换回Python字典时,上面的例子(Python 2版本)中字典的键转成Unicode字符串。如果在Python 3中运行这一行代码,就没有Unicode字符串前导操作符(即左引号前面的“u”指示符)。
>>> json.loads(json.dumps(dict(zip('abcde', range(5)))))
{'a': 0, 'c': 2, 'b': 1, 'e': 4, 'd': 3}
Python字典转化成了JSON对象。与之类似,Python列表或元组也可转成对应的JSON数组。
>>> list('abcde')
['a', 'b', 'c', 'd', 'e']
>>> json.dumps(list('abcde'))
'["a", "b", "c", "d", "e"]'
>>> json.loads(json.dumps(list('abcde')))
[u'a', u'b', u'c', u'd', u'e']
>>> # ['a', 'b', 'c', 'd', 'e'] in Python 3
>>> json.loads(json.dumps(range(5)))
[0, 1, 2, 3, 4]
Python和JSON数据类型与值之间有什么区别呢?表14-1列出了一些关键区别。
表14-1没有列出另一个细微的区别,即JSON不使用单引号,每个字符串都使用双引号分隔。另外,也没有Python程序员偶尔为了方便,在每个序列或映射元素的最后添加的额外的尾随逗号。
为了可视化其中一些区别,示例14-2显示了dict2json.py,这个脚本兼容Python 2和Python 3,它用4种方法转储字典的内容,两次作为Python字典,两次作为JSON对象。
示例14-2 Python字典转JSON示例(dict2json.py)
该脚本将Python字典转成JSON,并使用多种格式显示。
逐行解释
第1~5行
首先导入这里所需的三个函数:1)distutils.log.warn(),用来应对Python 2中print语句和Python 3中print()函数引起的差异;2)json.dumps(),用来返回一个表示Python对象的字符串;3)pprint.pprint(),用来美观地输出Python对象。
第7~22行
BOOKs数据结构是一个Python字典,表示通过ISBN标识的书籍。每本书还含有额外的信息,如书名、作者、出版年份等。这里没有使用列表这样“平坦”的数据结构。而是使用字典,因为字典可以构建具有结构化层次的属性。注意,在等价的JSON表示方法中,会移除所有额外的逗号。
第24~34行
脚本剩下的内容用于显示输出结果。第一个示例是仅仅转储的Python字典,没有什么特别内容。注意,这里同样移除了额外的逗号。这样人们在代码中使用起来就更加方便。第二个示例是相同的Python字典,但使用更美观的方式输出。
最后两个是 JSON 格式的输出。第一个是转换后普通的 JSON 转储。第二个是使用json.dumps()内置的美观的输出方式。只须传递缩进级别就可以启用这个特性。
在Python 2或3中执行这个脚本会得到以下输出。
$ python dict2json.py
RAW DICT
{'0132269937': {'edition': 2, 'year': 2007, 'title': 'Core Python Programming'}, '0137143419': {'year': 2009, 'title': 'Python Fundamentals'}, '0132356139': {'authors': ['Jeff Forcier',
'Paul Bissex', 'Wesley Chun'], 'year': 2009, 'title': 'Python Web Development with Django'}}
PRETTY_PRINTED DICT
{'0132269937': {'edition': 2,
'title': 'Core Python Programming',
'year': 2007},
'0132356139': {'authors': ['Jeff Forcier', 'Paul Bissex', 'Wesley Chun'],
'title': 'Python Web Development with Django',
'year': 2009},
'0137143419': {'title': 'Python Fundamentals', 'year': 2009}}
RAW JSON
{"0132269937": {"edition": 2, "year": 2007, "title": "Core Python
Programming"}, "0137143419": {"year": 2009, "title": "Python
Fundamentals"}, "0132356139": {"authors": ["Jeff Forcier",
"Paul Bissex", "Wesley Chun"], "year": 2009, "title": "Python Web Development with Django"}}
PRETTY_PRINTED JSON
{
"0132269937": {
"edition": 2,
"year": 2007,
"title": "Core Python Programming"
},
"0137143419": {
"year": 2009,
"title": "Python Fundamentals"
},
"0132356139": {
"authors": [
"Jeff Forcier",
"Paul Bissex",
"Wesley Chun"
],
"year": 2009,
"title": "Python Web Development with Django"
}
}
这个示例演示了从字典转换为JSON的方法。也可以将数据从列表或元组转成JSON数组。json模块还可以为其他Python数据类型提供编码和解码(decoding)类,用于与JSON互转。但这里不会一一介绍这些内容,JSON的内容非常广,本节作为入门简介无法面面俱到。
现在来看文本格式——XML,人们很少意识到XML仅仅是纯文本格式 [1]。
14.3 可扩展标记语言
本章要介绍的数据处理方面的第三个主题是可扩展标记语言(Extensible Markup Language,XML)。与前面介绍CSV的方式相同,这里首先简要介绍XML,接着通过一个教程介绍如何使用Python处理XML数据。在此之后,处理来自Google News服务的实际数据。
14.3.1 XML简介
本章最后一节将介绍XML,这是一个较老的结构化数据格式,声称是“纯文本”格式,用来表示结构化的数据。尽管XML数据是纯文本,但有充分的理由认为XML不是人类可读的。如果没有解析器的帮助,XML几乎难以辨认。但XML诞生已久,且比JSON应用得更广。当今几乎每种编程语言都有XML解析器。
XML是标准通用标记语言(Standard Generalized Markup Language,SGML)的限制版,其本身是ISO标准(ISO 8879)。XML最初诞生于1996年,万维网联盟(W3C)组建了一个团队设计XML。第1版XML规范发布于1998年,最近一次更新于2008年 [2]。可以认为XML是SGML的子集。还可以认为HTML是SGML更小的子集。
14.3.2 Python和XML
Python 最初在 1.5 版时通过 xmllib 模块支持 XML。从那时起,xmllib 最终融入到 xml包中,xml包提供了不同方式来解析和构建XML文档。
Python同时支持文档对象模型(DOM)树形结构和基于事件的简单XMLAPI(Simple API for XML,SAX)来处理XML文档。当前的SAX规范是2.0.1版,所以Python中通常称为SAX2。DOM标准比较老,几乎与XML存在的时间一样长。从Python 2.0开始就同时支持SAX和DOM。
SAX是流接口,意味着文档是通过连续的字节流一次处理一行。因此在XML文档中既不能回溯也不能执行随机访问。从这一点可以推断,这种基于事件的处理器更快且在内存操作方面更有效率。而基于树形结构的解析器将整个文档放在内存中,可以多次访问。
这里需要提醒一下,xml包中的内容根据版本不同有所差异,但至少有一个兼容SAX的XML解析器。此时这意味着用户需要手动查找并下载第三方模块或包,以满足这里的需要。幸运的是,从Python 2.3开始,标准库中自带了Expat流解析器,位于xml.parsers.expat下面。
Expat诞生于SAX之前,且不兼容SAX。但可以使用Expat创建SAX或DOM解析器。还要注意,Expat的执行效率很快。因为Expat不进行验证,意味着它不检查标记的兼容性。可以推想,进行验证的解析器由于需要额外的处理,因此速度也慢一些。
从 2.5 版本开始,Python 通过额外的 ElementTree 进一步成熟的支持XML,ElementTree是一款使用广泛、快速且符合 Python 风格的 XML 文档解析器和生成器,已经作为xml.etree.ElementTree添加到标准库中。这里将使用ElementTree处理所有原始的XML示例(还会稍微用到xml.dom.minidom),并显示使用Python的XML-RPC支持编写客户端/服务器应用。
在示例 14-3(dict2xml.py)中,使用Python 字典存储结构化的数据,使用ElementTree构建正确的XML文档,以此来表示这个数据结构,使用xml.dom.minidom来美观地输出。最后,使用多种ElementTree迭代器解析并显示其中相关的内容。
示例14-3 将Python字典转换成XML(dict2xml.py)
这个Python 2脚本将字典转换成XML,并使用多种格式显示出来。
运行该脚本,同时该脚本也能很容易移植到Python 3中,结果如下所示。
$ dict2xml.py
RAW XML
<books><book><edition>2</edition><authors>Wesley Chun</
authors><year>2006</year><title>Core Python Programming</title></
book><book><edition>1</edition><authors>Wesley Chun</
authors><year>2009</year><title>Python Fundamentals</title></
book><book><edition>1</edition><authors>Jeff Forcier, Paul Bissex,
Wesley Chun</authors><year>2009</year><title>Python Web Development
with Django</title></book></books>
PRETTY-PRINTED XML
<?xml version="1.0" ?>
<books>
<book>
<edition>
2
</edition>
<authors>
Wesley Chun
</authors>
<year>
2006
</year>
<title>
Core Python Programming
</title>
</book>
<book>
<edition>
1
</edition>
<authors>
Wesley Chun
</authors>
<year>
2009
</year>
<title>
Python Fundamentals
</title>
</book>
<book>
<edition>
1
</edition>
<authors>
Jeff Forcier, Paul Bissex, Wesley Chun
</authors>
<year>
2009
</year>
<title>
Python Web Development with Django
</title>
</book>
</books>
FLAT STRUCTURE
books - None
book - None
edition - 2
authors - Wesley Chun
year - 2006
title - Core Python Programming
book - None
edition - 1
authors - Wesley Chun
year - 2009
title - Python Fundamentals
book - None
edition - 1
authors - Jeff Forcier, Paul Bissex, Wesley Chun
year - 2009
title - Python Web Development with Django
TITLES ONLY
Core Python Programming
Python Fundamentals
Python Web Development with Django
逐行解释
第1~21行
该脚本的前几行与前一节介绍的 dict2json.py 非常相似。改动包括导入 ElementTree 和minidom。读者知道如何让代码同时在Python 2和3中工作,所以这里进行了简化,只针对Python 2版本的解决方案。
最后,最微妙的区别在于“author”字段不是使用类似dict2json.py中的列表,而是单个冒号分隔的字符串。这个改动并不是必需的,然而读者依然可以继续使用列表。
做这个改动是为了简化数据处理。关键点之一是很明显它位于第29行。另一个区别是在JSON 示例中,如果没有提供,就不会设置默认作者名。这很容易通过冒号来检查,如果数据值是字符串或列表则无须额外的检查。
第23~29行
这里是脚本正式开始工作的地方。首先创建顶层对象,即books,接着将所有其他内容添加到该节点下。对于每一本书,都添加一个book子节点,如果上面的原字典没有提供作者和版本,则使用提供的默认值。接着遍历所有键值对,将这些内容作为其他子节点添加到每个book中。
第31~45行
最后一段代码的作用是将数据用其他几种格式转储,包括原始XML、美观输出的XML (用到MiniDOM),遍历所有节点作为一个大的平坦结构。最后,演示了在XML文档中进行简单搜索。
14.3.3 XML实战
前面展示了许多关于创建和解析 XML 文档的示例,大多数应用会使用后面一种方法。所以来看另一个简短的例子,它解析数据来生成有用的信息。
在示例14-4中,goognewsrss.py从Google News服务中获取“Top Stories”源(feed),并提取前5个(默认情况下)新闻故事的标题,作为实际新闻的链接。在这种方案中, goognewsrss.topnews()是一个生成器,因为其中含有一个很明显的yield表达式。这意味着生成器用迭代的方式生成(title, link)对。查看代码,确认能否了解其中的工作方式和输出结果(这里没有显示出来)。在示例代码后面会解释原因。
示例14-4 解析实际的XML流(goognewsrss.py)
这个脚本兼容Python 2和3,显示排名靠前的新闻(默认为5个),以及Google News服务中对应的链接。
执行代码前,要确认阅读过 Google News 的服务条款(ToS),参 见 http://news.google.com/intl/en_us/terms_google_news.html。其中说明了使用该 Google 服务的要求。关键是这一段,“本服务的内容只能用于个人用途(即非商业用途),不能复制、重新生成、变更、修改、创建衍生作品,或公开显示任何内容。”
由于本书是面向公众出版的,这意味着本书不能粘贴示例程序执行后的结果,也不能遮挡住实际的输出结果,因为这形同修改内容。但读者可以私下执行程序并查看结果。
在执行结果中,会看到排名最靠前的5条新闻标题和链接组成的二元组。注意,由于这是实时的服务,内容在不断更改。不同时间运行这个程序会得到不同的结果。
逐行解释
第1~22行
是的,有些纯粹主义者会发现这里的代码有点丑,杂乱的导入语句让代码难以阅读,这一点我表示赞同。但在实际中,如果需要让生产环境中的代码支持不同版本的语言,特别是这里需要支持Python 3,必须要使用这些“ifdef”类型的语句。先放下这些,来看实际导入了哪些内容。
首先需要一个,带有文件接口的大字符串缓冲区。换句话说,这是一个位于内存中的大字符串,同时支持文件接口(即支持write()这样的文件方法)。这就是StringIO类。网络传来的数据一般是ASCII或纯字节,而不是Unicode。所以如果需要在Python 3中运行,需要用io.BytesIO类作为StringIO。
如果使用 Python 2,就不会涉及 Unicode,所以先尝试能否使用更快的 C 编译的cStringIO.StringIO类。如果这个类不可用,则使用原先的StringIO.StringIO类。
接下来,需要这个类能够较好地利用内存。因此会使用内置函数 zip()对应的迭代器版——itertools.izip()。如果itertools模块中含有izip(),则表明位于Python 2中。此时将其导入为zip()。否则,在Python 3中,因为移除了旧的zip(),并将izip()重命名为zip(),此时应该忽略掉 ImportError。注意,修改既没有使用 zip()也没有使用 izip()。更多信息,参考稍后的黑客园地。
最后一个特殊的地方是 Python 2 的 urllib2 模块,在 Python 3 中该模块合并到了urllib.request子模块中。后者提供了所需的urlopen()函数。
最后,使用 ElementTree 以及用于美化输出的 pprint.pprint()函数。在这个例子中,程序的输出通常用来显示警告消息,所以使用disutils.log.warn()显示输出。
第24~28行
应用程序在这里获取数据。首先打开一个链接,从Google News服务器请求XML格式的RSS输出。读取整个源,将其直接写入内存中等价于文件的StringIO里。
请求的主题是头条新闻,它用topic=h键值对表示。其他选项包括:ir表示焦点新闻,w表示全球新闻,n表示美国新闻,b表示商业新闻,tc表示科技新闻,e表示娱乐新闻,s表示体育新闻,snc表示科技新闻,m表示健康新闻。
获取完成后关闭文件的Web链接,将这个文件类型对象传递给ElementTree.parse()函数,该函数解析XML文档,返回ElementTree类的一个实例。注意,这里可以自行实例化,因为在这个例子中,调用ElementTree.parse(f)等价于ElementTree.ElementTree(file=f)。最后,关闭这个内存中的文件。
第30~50行
topnews()函数为调用者整理输出结果。这里只想返回正确格式化的新闻项,所以先创建两个元素的列表(但当作元组使用)。元组的第一个元素是标题,第二个是链接。只有同时有这两项时才会返回迭代数据项。否则,如果请求达到新闻数目上限(默认为5条)则退出,未达上限则直接重置这个二元组。
对于第一个标题需要特殊处理,因为这并不是真正的新闻标题,而是新闻类型的标题。在这里,因为请求的是头条,所以“title”字段获取的不是新闻的标题,而是“category”标题,以及从“TopStories”中提取出作为内容的字符串。需要忽略掉这些。
该脚本最后两行代码输出由topnews()生成的二元组。
XML 所能做的不仅是文本处理。下一节就明显与 XML 有关,但实际上又没有用到XML。XML 是基本的砖瓦,提供在线服务的开发者可以在高层次的客户端/服务器计算上编码。为了简化这些任务,无须创建让客户端能够调用函数的服务,更具体一些,即远程过程调用(Remote Procedure Call,RPC)。
核心提示:(黑客园地):将topnews()缩减为一行较长的Python代码
可以将topnews()的代码缩减为一行嵌套的代码.
topnews = lambda count=5: [(x.text, y.text) for x, y in zip
(tree.getiterator('title'), tree.getiterator('link')) if not
x.text.startswith('Top Stories')][:count]
希望这没有看坏读者的眼睛。其中的秘密之处在于 ElementTree.getiterator()函数,以及假设所有新闻数据已经正确格式化了。在原来的标准版topnews()中,既没有使用zip(),也没有使用itertools.izip(),但这里使用zip()将标题和对应的链接组合起来。
14.3.4 *使用XML-RPC的客户端-服务器服务
XML-RPC 创建于 20 世纪 90 年代末,让开发者能够通过超文本传输协议(HyperText Transfer Protocal,HTTP)作为传输机制,以此来创建远程过程调用,而XML文档作为载体。
XML文档同时包含,RPC的名称,以及任何用于执行的参数。XML-RPC导致了SOAP的出现,当然没有SOAP那么复杂。由于JSON比XML更加易读,因此也有一个JSON-RPC,包括SOAP版本的SOAPjr[3]。
Python 对 XML-RPC 的支持来自于 3 个包:客户端的 xmlrpclib,以及服务器端的SimpleXMLRPCServer 和 DocXMLRPCServer。很自然,在 Python 3.x 中这三个包重组为xmlrpc.client和xmlrpc.server。
示例14-5显示的是xmlrpcsrvr.py,这是针对Python 2的脚本,它包括简单的XML-RPC服务以及诸多RPC调用。这里首先列出代码,接着介绍RPC提供的每个服务。
示例14-5 XML-RPC服务器代码(xmlrpcsrvr.py)
这是一个XML-RPC服务器示例,其中含有多个RPC函数。
逐行解释
第1~8行
这里含有多条导入语句。首先是最重要的 SimpleXMLRPCServer,其后是一些辅助导入语句,它们提供这里所需的服务。甚至在第13章的Yahoo! 股票报价服务器和Twitter代码中也用到过这些服务。
首先导入所需的标准库模块/包,然后导入用户级别的模块,即用于与 Twitter 服务交互的twapi。导入语句的顺序遵循最佳实践准则,即首先是标准库,接着是第三方库,最后是用户定义的库。
第10~11行
导入完所需的包以后,SimpleXMLRPCServer使用给定的主机名或IP地址和端口号来创建服务。在这个例子中,仅仅使用 localhost 或 127.0.0.1。其后代码注册了通常会用到的XML-RPC内省函数。
这些函数允许客户端通过查询服务器,来确定服务器的能力。它们帮助客户端了解服务器支持哪些方法、它如何调用特定的RPC,以及是否有某个特定RPC的文档。system.listMethods、system.methodSignature、system.methodHelp这些调用就用来解决这些问题。
下面这个链接中含有这些内省函数的规范。
http:// scripts.incutio.com/xmlrpc/introspection.html
而下面这个链接中有关于如何显式实现这些函数的示例。
http://www.doughellmann.com/PyMOTW/SimpleXMLRPCServer/#introspection-api。
第13~16行
这4行代码通过RPC来提供一些标准算术函数。包括内置函数pow()和从operator模块中获取的其他 5 个算术函数。server.register_function()函数仅仅让这些函数可以在 RPC 客户端请求中使用。
第18~26行
接下来需要添加到服务中的函数与时间相关。这些函数位于SpecialServices()类中。函数在类的外部还是内部没有实际区别,这里仅仅演示类中的三个函数,以及前面的算术函数。这三个函数分别为:now_int(),该函数用秒为单位表示从1970年1月1日到现在的时间;now_str(),该函数用UNIX格式的时间戳表示本地时区的当前时间;timestamp()函数,该函数接受一个字符串,返回该字符串并在前面追加时间戳。
第28~40行
这里直接复制第13章的代码,首先复制与Yahoo! 报价服务器交互的代码。stock()函数获取公司代号,接着获取最新的报价、最新变动、涨跌幅,以及最近一次交易的日期和时间。forex()函数与之类似,但处理的是汇率。
并不是一定要使用第13章的代码,所以如果还没有阅读第13章,可以跳过对这些函数的实现,因为学习XML-RPC概念并不一定需要这些内容。
第42~53行
最后两个需要注册的RPC是status()和tweet()函数,二者来自第13章中的Twitter代码,需要用到Twython 库。status()函数获取当前用户的当前状态,tweet()用来为用户更新状态。在这段代码的最后一行,通过register_instance()函数注册SpecialServices类中的所有函数。
第55~59行
最后5行用于启动服务(通过一个无限循环),并检测用户是否想退出(按Ctrl+C组合键)。
既然有了一个服务器,如果没有客户端代码使用这个服务器的功能,那又有什么用呢?在示例14-6中,将会看到一个可能的客户端应用,xmlrpcclnt.py。很自然,这个程序可以在任何能通过相应的主机/端口地址对访问服务器的计算机上运行。
示例14-6 Python 2版本的XML-RPC客户端代码(xmlrpcclnt.py)
这是一个调用XML-RPC服务器的可能客户端。
这里没有太多客户端的成分,但还是来看一下。
逐行解释
第1~6行
为了连接XML-RPC服务器,需要Python 2中的xmlrpclib模块。前面提到,在Python3中需要使用xmlrpc.client。另外还用到了math模块中的π常量。在实际代码的第一行,连接到XML-RPC服务器,将主机/端口对作为URL传递进去。
第7~16行
剩下的代码用来向XMLRPC服务器发送RPC请求,最终获取所需的结果。这个客户端唯一没有测试的函数是tweet(),这一部分将作为练习留给读者。做这么多的调用看起来有些多余,的确很多余,所以在本章末尾会看到一个练习 [4]来解决这个问题。
服务器启动后,就可以运行客户端并看到一些输出(读者的输出会与下面的有所不同)。
$ python xmlrpcclnt.py
Current time in seconds after epoch: 1322167988.29
Current time as a string: Thu Nov 24 12:53:08 2011
Area of circle of radius 5: 78.5398163397
Latest Google stock price: 570.11 (-9.89 / -1.71%) as of 11/23/2011 at 4:00pm
Latest foreign exchange rate from USD to EUR: 0.7491 as of 11/24/2011 at 3:51pm
Latest foreign exchange rate from EUR to USD: 1.3349 as of 11/24/2011 at 3:51pm
Latest Twitter status: @KatEller same to you!!! :-) we need a
celebration meal…this coming monday or friday? have a great
thanksgiving!!
尽管本章已到末尾,但仅仅接触到XML-RPC和JSON-RPC编程的皮毛。若想了解更多相关内容,建议通过 DocXMLRPCServer 类来了解自归档的 XML-RPC 服务器,以及从XML-RPC服务器能获取的不同类型的数据结构(参见xmlrpclib/xmlrpc.client文档)。
14.4 参考文献
网上有许多关于本章内容的文档。下面列出部分值得推荐的资源。
http://docs.python.org/library/csv
http://simplejson.readthedocs.org/en/latest/
http://pypi.python.org/pypi/simplejson
http://github.com/simplejson/simplejson
http://docs.python.org/library/json
http://en.wikipedia.org/wiki/JSON
http://en.wikipedia.org/wiki/XML
http://docs.python.org/library/xmlrpclib
http://docs.python.org/library/simplexmlrpcserver
http://docs.python.org/library/docxmlrpcserver
http://en.wikipedia.org/wiki/Expat_(XML)
http://en.wikipedia.org/wiki/Xml-rpc
http://scripts.incutio.com/xmlrpc/introspection.html
http://en.wikipedia.org/wiki/JSON-RPC
http://www.doughellmann.com/PyMOTW/
SimpleXMLRPCServer/#introspection-api
对于想进一步了解的读者,建议阅读Text Processing in Python(Addison-Wesley,2003)一书,这是Python文本处理方面的经典。还有另一本书,名为Python 2.6 Text Processing(Packt, 2010)。不 要管书名中的2.6,其中介绍的内容可用于大多数当前的Python版本中。
14.5 相关模块
与文本处理相关的模块见表14-2。
表14-2 与文本处理相关的模块
① Python 2.3中新增。
② Python 2.6中新增。
③ Python 2.0中新增。
14.6 练习
CSV
14-1 CSV。什么是CSV格式?它适合什么类型的应用?
14-2 CSV与str.split()。找到一些示例数据,在这些数据中str.split(',')无法满足需要,只能使用csv模块。
14-3 CSV与str.split()。在第13章的练习13-16中,需要让stock.py的输出更加灵活,让所有列尽量对齐。除了不同股票代号的长度差异,不同股票的价格和涨跌幅也在变动。更新练习13-16,用csv.reader()替换掉str.split()。
14-4 另一种CSV格式。除了逗号之外,还有另一种分隔符。例如,兼容POSIX的密码文件使用冒号分割,Outlook中的电子邮件地址使用分号分隔。创建可以读取或写入这些使用其他分隔符的函数。
JSON
14-5 JSON。JSON格式与Python中的字典和列表在语法上有什么区别?
14-6 JSON数组。dict2json.py示例只演示了将Python字段转换成JSON对象。创建一个相似的脚本,命名为lort2json.py,将列表或元组转换成JSON数组。
14-7 后向兼容。使用Python 2.5或更老版本运行示例14-2中的dict2json.py会出现下面的错误。
$ python2.5 dict2json.py
Traceback (most recent call last):
File "dict2json.py", line 12, in <module>from json import dumps
ImportError: No module named json
a)为了让该脚本在老版本Python中运行,需要做哪些工作?
b)修改dict2json.py的代码,导入相关老版本Python(如2.4和2.5)中对应的JSON功能,使其能在2.6和之后的版本中正常工作。
14-8 JSON。向第 13章从Yahoo! Finance服务中获取股票报价的stock.py示例中添加新代码,让其返回 JSON 字符串,用层次化的数据结构格式表示股票数据,而不是直接转储到屏幕上。
14-9 JSON和类型/类。编写脚本,对任意类型的Python对象编码和解码,如数值、类、实例等。
XML和XML-RPC
14-10 Web编程。改进 goognewsrss.py脚本,让其能输出格式化的HTML,HTML中含有锚/链接,这些锚/链接直接连接到用于浏览器渲染且无外链的.html文件。这些链接应当是正确的,用户单击后可以进入对应的Web页面。
14-11 健壮性。在 xmlrpcsrvr.py中,添加对>、>=、<、<=、==、!=操作的支持,以及真除(true division)和地板除(floor division)。
14-12 Twitter。在xmlrpcclnt.py中,没有测试SpecialServices.tweet()方法。在脚本中添加对这个方法的测试。
14-13 CGIXMLRPCRequestHandler。默 认 情 况 下,SimpleXMLRPCServer 使 用SimpleXMLRPCRequestHandler处理类。这个处理程序与CGIXMLRPCRequestHandler有什么区别?创建一个使用CGIXMLRPCRequestHandler的新服务器。
14-14 DocXMLRPCServer。了解自归档的XML-RPC服务器,回答以下问题。
a)SimpleXMLRPCServer 与 DocXMLRPCServer 对象之间有什么区别?除此之外,(网络)底层方面又有什么区别?
b)将本章中标准的XML-RPC客户端与服务器转换成自归档形式。
c)将上一练习中的CGI版本转换成使用DocCGIXMLRPCRequestHandler类。
14-15 XML-RPC多调用。在xmlrpcclnt.py中,每调用一次都会向服务器发送一个请求。如果客户端向服务器发送多个调用,但只向服务器发送一个服务请求,这样做能提高性能。研究 register_multicall_functions()函数,接着将这些功能添加到服务器中。最后修改客户端来使用多调用。
14-16 XML和XML-RPC。本章中XML-RPC的内容在哪些方面与XML有关?最后一节的内容与本章其他部分有明显差异,这是如何结合到一起来的?
14-17 JSON-RPC与XML-RPC。什么是JSON-RPC?其与XML-RPC有什么关系?
14-18 JSON-RPC。将 XML-RPC 客户端和服务器代码移植到等价的 jsonrpcsrvr.py 和jsonrpcclnt.py中。
