第 8 章 使用Web服务:XML-RPC、SOAP和REST

本章攻略:

  • 查询本地XML-RPC服务器
  • 编写一个多线程、多调用XML-RPC服务器
  • 运行一个支持HTTP基本认证的XML-RPC服务器
  • 使用REST从Flickr中收集一些照片信息
  • 找出亚马逊S3 Web服务支持的SOAP方法
  • 使用谷歌搜索定制信息
  • 通过商品搜索API在亚马逊中搜索图书

8.1 简介

本章介绍一些有趣的Python攻略,通过三种不同的方式使用Web服务。这三种方式是“XML远程过程调用”(XML Remote Procedure Call,简称XML-RPC)、“简单对象访问协议”(Simple Object Access Protocol,简称SOAP)和“表现层状态转化”(Representational State Transfer,简称REST)。Web服务的目的,是让两个软件组件通过精心设计的协议在网络中交互。接口是机器可读的。多种不同的协议为Web服务的使用提供了便利。

本章包含这三种常用协议的示例。XML-RPC使用HTTP作为传输媒介,使用XML格式的内容通信。实现XML-RPC的服务器等待适配的客户端调用。客户端使用不同的参数调用服务器,执行远程过程。XML-RPC较为简单,但安全性不高。SOAP有一组丰富的协议,用于增强远程过程调用。REST是一种架构风格,让Web服务变得简单。REST架构中的操作使用HTTP请求方法完成,即GETPOSTPUTDELETE。本章介绍这些Web服务协议和风格的用法,完成一些常见任务。

8.2 查询本地XML-RPC服务器

如果你经常做Web编程,可能会遇到这样的任务:从支持XML-RPC服务的网站中获取一些信息。在深入介绍XML-RPC服务之前,我们先架设一个XML-RPC服务器并和它通信。

8.2.1 准备工作

这个攻略要使用Python Supervisor程序。Supervisor广泛用于启动和管理可执行程序。Supervisor可以作为后台守护进程运行,能监控子进程,且子进程意外退出后能重启子进程。执行下面的命令即可安装Supervisor:

  1. $ pip install supervisor

8.2.2 实战演练

我们要为Supervisor创建一个配置文件。这个攻略中提供了一个示例配置,定义了一个Unix HTTP服务器套接字和一些其他参数。注意rpcinterface:supervisor部分,其中rpcinterface_factory是用来和客户端通信的。

program:8_2_ multithreaded_multicall_xmlrpc_server.py部分,我们使用Supervisor配置了一个简单的服务器程序,指定了命令和一些其他参数。

代码清单8-1a是一个简单的Supervisor配置,如下所示:

  1. [unix_http_server]
  2. file=tmpsupervisor.sock ; (the path to the socket file)
  3. chmod=0700 ; socket file mode (default 0700)
  4. [supervisord]
  5. logfile=tmpsupervisord.log
  6. loglevel=info
  7. pidfile=tmpsupervisord.pid
  8. nodaemon=true
  9. [rpcinterface:supervisor]
  10. supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
  11. [program:8_2_multithreaded_multicall_xmlrpc_server.py]
  12. command=python 8_2_multithreaded_multicall_xmlrpc_server.py ; the program (relative uses PATH, can take args)
  13. process_name=%(program_name)s ; process_name expr (default %(program_name)s)

如果在你最喜欢的编辑器中创建上述Supervisor配置文件,调用这个文件就可以运行Supervisor。

现在,我们可以编写一个XML-RPC客户端,作为Supervisor的代理,获取运行中的进程信息。

代码清单8-1b是查询XML-RPC本地服务器的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import supervisor.xmlrpc
  6. import xmlrpclib
  7. def query_supervisr(sock):
  8. transport = supervisor.xmlrpc.SupervisorTransport(None, None,
  9. 'unix://%s' %sock)
  10. proxy = xmlrpclib.ServerProxy('http://127.0.0.1',
  11. transport=transport)
  12. print "Getting info about all running processes via Supervisord..."
  13. print proxy.supervisor.getAllProcessInfo()
  14. if __name__ == '__main__':
  15. query_supervisr(sock='tmpsupervisor.sock')

运行这个Supervisor守护进程,会看到类似下面的输出:

  1. chapter8$ supervisord
  2. 2013-09-27 16:40:56,861 INFO RPC interface 'supervisor' initialized
  3. 2013-09-27 16:40:56,861 CRIT Server 'unix_http_server' running
  4. without any HTTP authentication checking
  5. 2013-09-27 16:40:56,861 INFO supervisord started with pid 27436
  6. 2013-09-27 16:40:57,864 INFO spawned:
  7. '8_2_multithreaded_multicall_xmlrpc_server.py' with pid 27439
  8. 2013-09-27 16:40:58,940 INFO success:
  9. 8_2_multithreaded_multicall_xmlrpc_server.py entered RUNNING state,
  10. process has stayed up for > than 1 seconds (startsecs)

注意,运行后启动了子进程8_2_multithreaded_multicall_xmlrpc_server.py

现在,如果运行客户端代码,它会查询Supervisor的XML-RPC服务器接口,列出运行中的进程,如下所示:

  1. $ python 8_1_query_xmlrpc_server.py
  2. Getting info about all running processes via Supervisord...
  3. [{'now': 1380296807, 'group':
  4. '8_2_multithreaded_multicall_xmlrpc_server.py', 'description': 'pid
  5. 27439, uptime 0:05:50', 'pid': 27439, 'stderr_logfile':
  6. 'tmp8_2_multithreaded_multicall_xmlrpc_server.py-stderr---
  7. supervisor-i_VmKz.log', 'stop': 0, 'statename': 'RUNNING', 'start':
  8. 1380296457, 'state': 20, 'stdout_logfile':
  9. 'tmp8_2_multithreaded_multicall_xmlrpc_server.py-stdout---
  10. supervisor-eMuJqk.log', 'logfile':
  11. 'tmp8_2_multithreaded_multicall_xmlrpc_server.py-stdout---
  12. supervisor-eMuJqk.log', 'exitstatus': 0, 'spawnerr': '', 'name':
  13. '8_2_multithreaded_multicall_xmlrpc_server.py'}]

8.2.3 原理分析

这个攻略要在后台运行Supervisor守护进程(通过rpcinterface配置)。Supervisor会启动另一个XML-RPC服务器,即8_2_ multithreaded_multicall_xmlrpc_server.py

客户端代码中定义了query_supervisr()方法,其参数是Supervisor套接字。在这个方法中,使用Unix套接字路径创建了一个SupervisorTransport实例。然后,实例化xmlrpclib模块中的ServerProxy类创建了一个XML-RPC服务器代理。传给ServerProxy类构造方法的参数是服务器地址和前面创建的transport

随后,XML-RPC服务器代理调用Supervisor中的getAllProcessInfo()方法,把子进程的信息打印出来。打印的信息包括pidstatenamedescription等。

8.3 编写一个多线程、多调用XML-RPC服务器

你可以让你的XML-RPC服务器同时接受多个调用。这意味着,多个函数调用可以只返回一个结果。而且,如果服务器支持多线程,服务器启动后还能在单个线程中执行更多的代码。此时,程序的主线程处于非阻塞模式中。

8.3.1 实战演练

我们可以定义一个ServerThread类,继承自threading.Thread类,而且还可以把这个类的一个属性设为SimpleXMLRPCServer实例。这样就能接受多个调用了。

然后,我们可以定义两个函数:一个用来启动多线程、多调用XML-RPC服务器,另一个用于创建连接服务器的客户端。

代码清单8-2是编写多线程、多调用XML-RPC服务器所需的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import argparse
  6. import xmlrpclib
  7. import threading
  8. from SimpleXMLRPCServer import SimpleXMLRPCServer
  9. # some trivial functions
  10. def add(x,y):
  11. return x+y
  12. def subtract(x, y):
  13. return x-y
  14. def multiply(x, y):
  15. return x*y
  16. def divide(x, y):
  17. return x/y
  18. class ServerThread(threading.Thread):
  19. def __init__(self, server_addr):
  20. threading.Thread.__init__(self)
  21. self.server = SimpleXMLRPCServer(server_addr)
  22. self.server.register_multicall_functions()
  23. self.server.register_function(add, 'add')
  24. self.server.register_function(subtract, 'subtract')
  25. self.server.register_function(multiply, 'multiply')
  26. self.server.register_function(divide, 'divide')
  27. def run(self):
  28. self.server.serve_forever()
  29. def run_server(host, port):
  30. # server code
  31. server_addr = (host, port)
  32. server = ServerThread(server_addr)
  33. server.start() # The server is now running
  34. print "Server thread started. Testing the server..."
  35. def run_client(host, port):
  36. # client code
  37. proxy = xmlrpclib.ServerProxy("http://%s:%s/" %(host, port))
  38. multicall = xmlrpclib.MultiCall(proxy)
  39. multicall.add(7,3)
  40. multicall.subtract(7,3)
  41. multicall.multiply(7,3)
  42. multicall.divide(7,3)
  43. result = multicall()
  44. print "7+3=%d, 7-3=%d, 7*3=%d, 7/3=%d" % tuple(result)
  45. if __name__ == '__main__':
  46. parser = argparse.ArgumentParser(description='Multithreaded multicall XMLRPC Server/Proxy')
  47. parser.add_argument('--host', action="store", dest="host", default='localhost')
  48. parser.add_argument('--port', action="store", dest="port", default=8000, type=int)
  49. # parse arguments
  50. given_args = parser.parse_args()
  51. host, port = given_args.host, given_args.port
  52. run_server(host, port)
  53. run_client(host, port)

运行这个脚本后,会看到类似下面的输出:

  1. $ python 8_2_multithreaded_multicall_xmlrpc_server.py --port=8000
  2. Server thread started. Testing the server...
  3. localhost - - [25/Sep/2013 17:38:32] "POST HTTP1.1" 200 -
  4. 7+3=10, 7-3=4, 7*3=21, 7/3=2

8.3.2 原理分析

在这个攻略中,我们定义了ServerThread类,继承自Python线程库中的Thread类。这个子类初始化时把server属性设为一个SimpleXMLRPCServer服务器实例。XML-RPC服务器的地址可以在命令行中指定。为了启用多调用功能,我们在服务器实例上调用了register_multicall_functions()方法。

然后把四个简单的函数注册到XML-RPC服务器上:add()subtract()multiply()divide()。这几个函数的作用正如其名称所示。

为了启动服务器,要把主机和端口传给run_server()函数。在这个函数中,使用前面说明的ServerThread类创建了一个服务器实例。然后在这个服务器实例上调用start()方法启动XML-RPC服务器。

再看客户端。run_client()函数同样从命令行中获取主机和端口。然后使用xmlrpclib中的ServerProxy类创建一个XML-RPC服务器代理实例。再把这个代理实例传给MultiCall类,创建一个实例multicall。现在,可以运行前面定义的四个RPC方法了,即add()subtract()multiply()divide()。最后,我们只通过一次调用(multicall())获取结果,再把结果中的元组在一行中打印出来。

8.4 运行一个支持HTTP基本认证的XML-RPC服务器

有时,你需要在XML-RPC服务器中实现认证功能。这个攻略介绍了一个简单的示例,说明如何在XML-RPC服务器中实现HTTP基本认证。

8.4.1  实战演练

我们可以定义一个SimpleXMLRPCServer类的子类,重新定义请求处理方法,以便当请求进入时,和指定的登录凭据比对。

代码清单8-3a是在XML-RPC服务器中实现HTTP基本认证的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import argparse
  6. import xmlrpclib
  7. from base64 import b64decode
  8. from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
  9. class SecureXMLRPCServer(SimpleXMLRPCServer):
  10. def __init__(self, host, port, username, password, args, *kargs):
  11. self.username = username
  12. self.password = password
  13. # authenticate method is called from inner class
  14. class VerifyingRequestHandler(SimpleXMLRPCRequestHandler):
  15. # method to override
  16. def parse_request(request):
  17. if SimpleXMLRPCRequestHandler.parse_request(request):
  18. # authenticate
  19. if self.authenticate(request.headers):
  20. return True
  21. else:
  22. # if authentication fails return 401
  23. request.send_error(401, 'Authentication failed, Try agin.')
  24. return False
  25. # initialize
  26. SimpleXMLRPCServer.__init__(self, (host, port), requestHandler=VerifyingRequestHandler, args, *kargs)
  27. def authenticate(self, headers):
  28. headers = headers.get('Authorization').split()
  29. basic, encoded = headers[0], headers[1]
  30. if basic != 'Basic':
  31. print 'Only basic authentication supported'
  32. return False
  33. secret = b64decode(encoded).split(':')
  34. username, password = secret[0], secret[1]
  35. return True if (username == self.username and password == self.password) else False
  36. def run_server(host, port, username, password):
  37. server = SecureXMLRPCServer(host, port, username, password)
  38. # simple test function
  39. def echo(msg):
  40. """Reply client in uppser case """
  41. reply = msg.upper()
  42. print "Client said: %s. So we echo that in uppercase: %s" %(msg, reply)
  43. return reply
  44. server.register_function(echo, 'echo')
  45. print "Running a HTTP auth enabled XMLRPC server on %s:%s..." %(host, port)
  46. server.serve_forever()
  47. if __name__ == '__main__':
  48. parser = argparse.ArgumentParser(description='Multithreaded multicall XMLRPC Server/Proxy')
  49. parser.add_argument('--host', action="store", dest="host", default='localhost')
  50. parser.add_argument('--port', action="store", dest="port", default=8000, type=int)
  51. parser.add_argument('--username', action="store", dest="username", default='user')
  52. parser.add_argument('--password', action="store", dest="password", default='pass')
  53. # parse arguments
  54. given_args = parser.parse_args()
  55. host, port = given_args.host, given_args.port
  56. username, password = given_args.username, given_args.password
  57. run_server(host, port, username, password)

运行这个脚本后,默认会看到如下输出:

  1. $ python 8_3a_xmlrpc_server_with_http_auth.py
  2. Running a HTTP auth enabled XMLRPC server on localhost:8000...
  3. Client said: hello server.... So we echo that in uppercase: HELLO
  4. SERVER...
  5. localhost - - [27/Sep/2013 12:08:57] "POST RPC2 HTTP1.1" 200 -

现在,我们来创建一个简单的客户端代理,使用的登录凭据和服务器一样。

代码清单8-3b是XML-RPC客户端的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import argparse
  6. import xmlrpclib
  7. def run_client(host, port, username, password):
  8. server = xmlrpclib.ServerProxy('http://%s:%s@%s:%s' %(username, password, host, port, ))
  9. msg = "hello server..."
  10. print "Sending message to server: %s " %msg
  11. print "Got reply: %s" %server.echo(msg)
  12. if __name__ == '__main__':
  13. parser = argparse.ArgumentParser(description='Multithreaded multicall XMLRPC Server/Proxy')
  14. parser.add_argument('--host', action="store", dest="host", default='localhost')
  15. parser.add_argument('--port', action="store", dest="port", default=8000, type=int)
  16. parser.add_argument('--username', action="store", dest="username", default='user')
  17. parser.add_argument('--password', action="store", dest="password", default='pass')
  18. # parse arguments
  19. given_args = parser.parse_args()
  20. host, port = given_args.host, given_args.port
  21. username, password = given_args.username, given_args.password
  22. run_client(host, port, username, password)

运行这个脚本后,会看到如下输出:

  1. $ python 8_3b_xmprpc_client.py
  2. Sending message to server: hello server...
  3. Got reply: HELLO SERVER...

8.4.2 原理分析

在服务器脚本中,继承SimpleXMLRPCServer类定义了SecureXMLRPCServer子类。在这个子类的初始化代码中,定义了用于拦截请求的VerifyingRequestHandler类,使用authenticate()方法做基本认证。

我们把HTTP请求的首部传给authenticate()方法,检查Authorization首部的值。如果值等于Basic,就使用base64标准模块中的b64decode()方法解码编码后的密码。提取出用户名和密码后,再和服务器指定的登录凭据比对。

run_server()函数中定义了一个简单的echo()子函数,将其注册到SecureXMLRPCServer实例上。

在客户端脚本中,run_client()函数的参数是服务器地址和登录凭据,再把这些参数传给ServerProxy类构造方法,创建一个实例。然后使用echo()方法发送一行消息。

8.5 使用REST从Flickr中收集一些照片信息

很多网站都通过REST API提供Web服务接口。Flickr这个著名的照片分享网站就提供了REST接口。让我们来收集一些照片信息,构建一个特殊的数据库或者开发照片相关的应用。

8.5.1 实战演练

我们需要REST URL发起HTTP请求。简单起见,这些URL硬编码在脚本中。我们可以使用第三方模块requests发起REST请求。requests模块提供了一些便利的方法,包括get()post()put()delete()

为了和Flickr的Web服务通信,你需要注册一个账户,然后获取API密钥。API密钥可写入local_settings.py文件,或者在命令行中提供。

代码清单8-4是使用REST从Flickr中收集照片信息所需的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. # Supply Flickr API key via local_settings.py
  6. import argparse
  7. import json
  8. import requests
  9. try:
  10. from local_settings import flickr_apikey
  11. except ImportError:
  12. pass
  13. def collect_photo_info(api_key, tag, max_count):
  14. """Collects some interesting info about some photos from Flickr.com for a given tag """
  15. photo_collection = []
  16. url = "http://api.flickr.com/services/rest/?method=flickr.photos.search&tags=%s&format=json&nojsoncallback=1&api_key=%s" %(tag, api_key)
  17. resp = requests.get(url)
  18. results = resp.json()
  19. count = 0
  20. for p in results['photos']['photo']:
  21. if count >= max_count:
  22. return photo_collection
  23. print 'Processing photo: "%s"' % p['title']
  24. photo = {}
  25. url = "http://api.flickr.com/services/rest/?method=flickr.photos.getInfo&photo_id=" + p['id'] + "&format=json&nojsoncallback=1&api_key=" + api_key
  26. info = requests.get(url).json()
  27. photo["flickrid"] = p['id']
  28. photo["title"] = info['photo']['title']['content']
  29. photo["description"] = info['photo']['description']['content']
  30. photo["page_url"] = info['photo']['urls']['url'][0]['content']
  31. photo["farm"] = info['photo']['farm']
  32. photo["server"] = info['photo']['server']
  33. photo["secret"] = info['photo']['secret']
  34. # comments
  35. numcomments = int(info['photo']['comments']['content'])
  36. if numcomments:
  37. #print " Now reading comments (%d)..." % numcomments
  38. url = "http://api.flickr.com/services/rest/?method=flickr.photos.comments.getList&photo_id=" + p['id'] + "&format=json&nojsoncallback=1&api_key=" + api_key
  39. comments = requests.get(url).json()
  40. photo["comment"] = []
  41. for c in comments['comments']['comment']:
  42. comment = {}
  43. comment["body"] = c['content']
  44. comment["authorid"] = c['author']
  45. comment["authorname"] = c['authorname']
  46. photo["comment"].append(comment)
  47. photocollection.append(photo)
  48. count = count + 1
  49. return photo_collection
  50. if __name__ == '__main__':
  51. parser = argparse.ArgumentParser(description='Get photo info from Flickr')
  52. parser.add_argument('--api-key', action="store", dest="api_key", default=flickr_apikey)
  53. parser.add_argument('--tag', action="store", dest="tag", default='Python')
  54. parser.add_argument('--max-count', action="store", dest="max_count", default=3, type=int)
  55. # parse arguments
  56. given_args = parser.parse_args()
  57. api_key, tag, max_count = given_args.api_key, given_args.tag, given_args.max_count
  58. photo_info = collect_photo_info(api_key, tag, max_count)
  59. for photo in photo_info:
  60. for k,v in photo.iteritems():
  61. if k == "title":
  62. print "Showiing photo info...."
  63. elif k == "comment":
  64. "\tPhoto got %s comments." %len(v)
  65. else:
  66. print "\t%s => %s" %(k,v)

Flickr API密钥可以放在local_settings.py文件中,也可在命令行中提供(通过--api-key参数)。除了API密钥,还可指定搜索标签和结果的最大数量。默认情况下,这个脚本搜索Python标签,结果限制为3个,如下面的输出所示:

  1. $ python 8_4getflickr_photo_info.py
  2. Processing photo: "legolas"
  3. Processing photo: ""The Dance of the Hunger of Kaa""
  4. Processing photo: "Rocky"
  5. description => Stimson Python
  6. Showiing photo info....
  7. farm => 8
  8. server => 7402
  9. secret => 6cbae671b5
  10. flickrid => 10054626824
  11. page_url => http://www.flickr.com/photos/102763809@N03/10054626824/
  12. description => " 'Good. Begins now the dance--the Dance of the
  13. Hunger of Kaa. Sit still and watch.'
  14. He turned twice or thrice in a big circle, weaving his head from right to left.
  15. Then he began making loops and figures of eight with his body, and soft,
  16. oozy triangles that melted into squares and five-sided figures, and
  17. coiled mounds, never resting, never hurrying, and never stopping his
  18. low humming song. It grew darker and darker, till at last the dragging,
  19. shifting coils disappeared, but they could hear the rustle of the
  20. scales."
  21. (From "Kaa's Hunting" in "The Jungle Book" (1893) by Rudyard Kipling)
  22. These old abandoned temples built around the 12th century belong to the
  23. abandoned city which inspired Kipling's Jungle Book.
  24. They are rising at the top of a mountain which dominates the jungle at
  25. 811 meters above sea level in the centre of the jungle of Bandhavgarh
  26. located in the Indian state Madhya Pradesh.
  27. Baghel King Vikramaditya Singh abandoned Bandhavgarh fort in 1617 when
  28. Rewa, at a distance of 130 km was established as a capital.
  29. Abandonment allowed wildlife development in this region.
  30. When Baghel Kings became aware of it, he declared Bandhavgarh as their
  31. hunting preserve and strictly prohibited tree cutting and wildlife
  32. hunting...
  33. Join the photographer at <a href="http://www.facebook.com/laurent.
  34. goldstein.photography" rel="nofollow">www.facebook.com/laurent.goldstein.
  35. photography</a>
  36. © All photographs are copyrighted and all rights reserved.
  37. Please do not use any photographs without permission (even for private
  38. use).
  39. The use of any work without consent of the artist is PROHIBITED and will
  40. lead automatically to consequences.
  41. Showiing photo info....
  42. farm => 6
  43. server => 5462
  44. secret => 6f9c0e7f83
  45. flickrid => 10051136944
  46. page_url => http://www.flickr.com/photos/designldg/10051136944/
  47. description => Ball Python
  48. Showiing photo info....
  49. farm => 4
  50. server => 3744
  51. secret => 529840767f
  52. flickrid => 10046353675
  53. page_url => http://www.flickr.com/photos/megzzdollphotos/10046353675/

8.5.2 原理分析

这个攻略演示了如何使用Flickr的REST API和它交互。在这个示例中,collect_photo_info()函数有三个参数:Flickr API密钥、搜索标签和想要得到的搜索结果数量。

我们构建的第一个URL用于搜索照片。注意,在这个URL中,method参数的值是flickr.photos.search,希望得到的结果格式是JSON。

第一次调用get()方法得到的结果存储在变量resp中,然后在这个变量上调用json()方法,将其转换成JSON格式。然后在一个循环中读取['photos']['photo']中的JSON数据。得到的信息保存在photo_collection列表中。在这个列表中,每张照片的信息由一个字典表示。字典的键从前面的JSON格式响应中提取,照片的信息从另一个GET请求中获取。

注意,若想获取照片的评论,要再发起一个GET请求,从JSON格式响应中的['comments']['comment']元素获取。最后,把这些评论插入一个列表中,再添加到照片对应的字典里。

__main__块中,我们从photo_collection列表中读取各字典,打印各张照片的一些有用信息。

8.6 找出亚马逊S3 Web服务支持的SOAP方法

如果你需要与实现了简单对象访问协议(SOAP)的Web服务交互,可以参考这个攻略。

8.6.1 准备工作

在这个攻略中我们可以使用第三方库SOAPpy,执行下面的命令即可安装这个库:

  1. $ pip install SOAPpy

8.6.2 实战演练

我们要创建一个代理对象,在调用服务器上的方法之前,先找出支持哪些方法。

在这个攻略中,我们要和亚马逊S3存储服务交互。我们已经有了Web服务API的测试URL。你需要有API密钥才能执行这个简单的任务。

代码清单8-5是搜索亚马逊S3 Web服务支持的SOAP方法所需的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program requires Python 2.7 or any later version
  4. import SOAPpy
  5. TEST_URL = 'http://s3.amazonaws.com/ec2-downloads/2009-04-04.ec2.wsdl'
  6. def list_soap_methods(url):
  7. proxy = SOAPpy.WSDL.Proxy(url)
  8. print '%d methods in WSDL:' % len(proxy.methods) + '\n'
  9. for key in proxy.methods.keys():
  10. print "Key Name: %s" %key
  11. print "Key Details:"
  12. for k,v in proxy.methods[key].__dict__.iteritems():
  13. print "%s ==> %s" %(k,v)
  14. break
  15. if __name__ == '__mani__':
  16. list_soap_methods(TEST_URL)

运行这个脚本后,会打印出支持“Web服务定义语言”(Web Services Definition Language,简称WSDL)的可用方法总数,以及随意一个方法的详情,如下所示:

  1. $ python 8_5_search_amazonaws_with_SOAP.py
  2. homefaruq/env/lib/python2.7/site-packages/wstools/XMLSchema.py:1280:
  3. UserWarning: annotation is ignored
  4. warnings.warn('annotation is ignored')
  5. 43 methods in WSDL:
  6. Key Name: ReleaseAddress
  7. Key Details:
  8. encodingStyle ==> None
  9. style ==> document
  10. methodName ==> ReleaseAddress
  11. retval ==> None
  12. soapAction ==> ReleaseAddress
  13. namespace ==> None
  14. use ==> literal
  15. location ==> https://ec2.amazonaws.com/
  16. inparams ==> [<wstools.WSDLTools.ParameterInfo instance at 0x8fb9d0c>]
  17. outheaders ==> []
  18. inheaders ==> []
  19. transport ==> http://schemas.xmlsoap.org/soap/http
  20. outparams ==> [<wstools.WSDLTools.ParameterInfo instance at 0x8fb9d2c>]

8.6.3 原理分析

在这个脚本中,定义了一个名为list_soap_methods()的方法,其参数是一个URL。然后调用SOAPpy模块中的WSDL.Proxy()方法创建一个SOAP代理对象。可用的SOAP方法可通过这个代理对象的methods属性获取。

然后遍历代理对象methods属性中的键,获取各方法对应的键名。在for循环中打印出某个具体SOAP方法的详情,包括键名和键的详情。

8.7 使用谷歌搜索定制信息

对很多人来说,每天都要使用谷歌搜索信息。我们来试试使用谷歌搜索一些信息。

8.7.1 准备工作

这个攻略要使用一个第三方Python库requests,可以使用pip安装,如下面的命令所示:

  1. $ pip install requests

8.7.2 实战演练

谷歌的搜索API很复杂,你要注册一个账户,再按照指定的方式获取API密钥。简单起见,我们使用谷歌以前的简单异步JavaScript(AJAX)API,搜索Python相关的图书信息。

代码清单8-6是使用谷歌搜索定制信息所需的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program requires Python 2.7 or any later version.
  4. # It may run on any other version with/without modifications.
  5. import argparse
  6. import json
  7. import urllib
  8. import requests
  9. BASE_URL = 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0'
  10. def get_search_url(query):
  11. return "%s&%s" %(BASE_URL, query)
  12. def search_info(tag):
  13. query = urllib.urlencode({'q': tag})
  14. url = get_search_url(query)
  15. response = requests.get(url)
  16. results = response.json()
  17. data = results['responseData']
  18. print 'Found total results: %s' % data['cursor']['estimatedResultCount']
  19. hits = data['results']
  20. print 'Found top %d hits:' % len(hits)
  21. for h in hits:
  22. print ' ', h['url']
  23. print 'More results available from %s' % data['cursor']['moreResultsUrl']
  24. if __name__ == '__main__':
  25. parser = argparse.ArgumentParser(description='Search info from Google')
  26. parser.add_argument('--tag', action="store", dest="tag", default='Python books')
  27. # parse arguments
  28. given_args = parser.parse_args()
  29. search_info(given_args.tag)

运行这个脚本时,如果在--tag参数中指定了搜索关键字,它会在谷歌中搜索,并打印出结果总数和点击量最多的四个网址,如下所示:

  1. $ python 8_6_search_products_from_Google.py
  2. Found total results: 12300000
  3. Found top 4 hits:
  4. https://wiki.python.org/moin/PythonBooks
  5. http://www.amazon.com/Python-Languages-Tools-Programming-Books/b%3Fie%3DUTF8%26node%3D285856
  6. http://pythonbooks.revolunet.com/
  7. http://readwrite.com/2011/03/25/python-is-an-increasingly-popu
  8. More results available from
  9. http://www.google.com/search?oe=utf8&ie=utf8&source=uds&start=0&hl=en&q=Python+books

8.7.3 原理分析

在这个攻略中,我们定义了一个简短的函数get_search_url(),使用BASE_URL常量和查询字符串组成搜索URL。

执行搜索的函数search_info()接收一个参数,即搜索关键字,然后构建查询字符串。requests库的作用是提供get()方法,得到的响应被转换成JSON格式。

我们从JSON格式数据的responseData键中提取搜索结果。预期的结果和点击量从相应的键中获取。然后把点击量排在前四位的URL打印出来。

8.8 通过商品搜索API在亚马逊中搜索图书

如果你想在亚马逊中搜索商品,并把它们添加到自己的网站或应用中,可以参考这个攻略。我们要介绍如何在亚马逊中搜索图书。

8.8.1 准备工作

这个攻略要用到一个第三方Python库bottlenose。这个库可使用pip安装,如下面的命令所示:

  1. $ pip install bottlenose

首先,要把亚马逊账户的访问密钥、私钥和联盟ID保存在文件local_settings.py中。本书附带的源码提供了一个示例设置文件。你也可以修改下面这个脚本,把设置写进去。

8.8.2 实战演练

我们可以使用bottlenose库实现亚马逊商品搜索API。

代码清单8-7是使用商品搜索API在亚马逊中搜索图书所需的代码,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 8
  3. # This program requires Python 2.7 or any later version
  4. import argparse
  5. import bottlenose
  6. from xml.dom import minidom as xml
  7. try:
  8. from local_settings import amazon_account
  9. except ImportError:
  10. pass
  11. ACCESS_KEY = amazon_account['access_key']
  12. SECRET_KEY = amazon_account['secret_key']
  13. AFFILIATE_ID = amazon_account['affiliate_id']
  14. def search_for_books(tag, index):
  15. """Search Amazon for Books """
  16. amazon = bottlenose.Amazon(ACCESS_KEY, SECRET_KEY, AFFILIATE_ID)
  17. results = amazon.ItemSearch(
  18. SearchIndex = index,
  19. Sort = "relevancerank",
  20. Keywords = tag
  21. )
  22. parsed_result = xml.parseString(results)
  23. all_items = []
  24. attrs = ['Title','Author', 'URL']
  25. for item in parsed_result.getElementsByTagName('Item'):
  26. parse_item = {}
  27. for attr in attrs:
  28. parse_item[attr] = ""
  29. try:
  30. parse_item[attr] = item.getElementsByTagName(attr)[0].childNodes[0].data
  31. except:
  32. pass
  33. all_items.append(parse_item)
  34. return all_items
  35. if __name__ == '__main__':
  36. parser = argparse.ArgumentParser(description='Search info from Amazon')
  37. parser.add_argument('--tag', action="store", dest="tag", default='Python')
  38. parser.add_argument('--index', action="store", dest="index", default='Books')
  39. # parse arguments
  40. given_args = parser.parse_args()
  41. books = search_for_books(given_args.tag, given_args.index)
  42. for book in books:
  43. for k,v in book.iteritems():
  44. print "%s: %s" %(k,v)
  45. print "-" * 80

运行这个脚本时如果提供了搜索关键字和目录,会看到一些类似下面的输出:

  1. $ python 8_7_search_amazon_for_books.py --tag=Python --index=Books
  2. URL: http://www.amazon.com/Python-In-Day-Basics-Coding/dp/tech-data/1
  3. 490475575%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%26tag%3D7052-6929-
  4. 7878%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D1490475575
  5. Author: Richard Wagstaff
  6. Title: Python In A Day: Learn The Basics, Learn It Quick, Start Coding
  7. Fast (In A Day Books) (Volume 1)
  8. -------------------------------------------------------------------------
  9. -------
  10. URL: http://www.amazon.com/Learning-Python-Mark-Lutz/dp/tech-data/1449355
  11. 730%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%26tag%3D7052-6929-7878%26link
  12. Code%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D1449355730
  13. Author: Mark Lutz
  14. Title: Learning Python
  15. -------------------------------------------------------------------------
  16. -------
  17. URL: http://www.amazon.com/Python-Programming-Introduction-Computer-
  18. Science/dp/tech-data/1590282418%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%2
  19. 6tag%3D7052-6929-7878%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%
  20. 26creativeASIN%3D1590282418
  21. Author: John Zelle
  22. Title: Python Programming: An Introduction to Computer Science 2nd
  23. Edition
  24. ---------------------------------------------------------------------
  25. -----------

8.8.3 原理分析

这个攻略使用第三方库bottlenose中的Amazon类创建了一个对象,使用商品搜索API在亚马逊中进行搜索。这些操作在顶层函数search_for_books()中完成。在这个对象上调用ItemSearch()方法时,分别把search_for_books()函数的参数传给SearchIndexKeywords键。搜索结果使用relevancerank方式排序。

搜索结果使用xml模块中的minidom接口处理,这个接口提供了一个有用的parseString()方法,得到的结果是解析后的树状数据结构。在这个数据结构上调用getElementsByTagName()方法能获取全部搜索结果。然后遍历搜索结果,把属性存入parse_item字典中。最后,把所有解析后的搜索结果都保存到all_items列表中,返回给用户。