del.icio.us, Yahoo! OAuth & python-oauth2 二三事
最近做两个事情:阅读《RESTful Web Services》和学习Python,正巧前者当中有需要动手写代码操练,干脆我就把书中原先用Ruby实现的代码重新用Python写一遍,一来加深对书中知识的理解,二来锻炼自己对Python的运用,一举两得。但是任何的学习都不会是轻而易举,对REST的理解和对Python的掌握还停留在初级水平,于是就出现下面这些波折。我把这些问题记录下来,方便自己以后回顾,也希望能够帮到其他人。
一事
Del.icio.us为用户使用其书签服务提供了两种方法,其一是在其网站注册,成为独立用户,其二是使用Yahoo! ID,无需重复注册。于此相应,Delicious公布的API也分为v1和v2,开发者在调用时须经过认证,如果是独立用户则要使用前者,须借助https请求和HTTP-Auth;如果是Yahoo! ID则要使用后者,须借助http请求和OAuth。
初次使用del.icio.us,弄不懂独立用户和Yahoo! ID,尝试着用Yahoo! ID登录网站,确认OK之后以为二者等同,于是用Yahoo! ID去调用其API v1,发现失败:
<!--?xml version="1.0" standalone="yes"?--> <?xml version="1.0" standalone="yes"?> <result code="invalid api" /> <!-- fe06.api.del.ac4.yahoo.net uncompressed/chunked Mon Nov 29 03:42:58 UTC 2010 --> |
顿时不解,一番google之后,从其论坛找到答案:
Depending on when you created your account (and thus how you log into delicious, you will need to use:) For “old” delicious users (who do not use a yahoo login): https://api.del.icio.us/v1/posts/all
for user with a yahoo login: http://api.del.icio.us/v2/posts/all + oauth, as per http://delicious.com/help/oauthapi
二事
什么是OAuth?和OpenID是什么关系?离开Web Applicaiton开发有一两年的时间,莫不是落伍了吧?虽然说到网站注册并不是什么麻烦的事情,但是既然有机会接触到新鲜概念,那就不要守旧了。
来自维基百科的OAuth的定义:
OAuth (开放授权) 是一个开放标准,允许用户让第三方网站访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方网站。
需要了解更多有关OAuth,可以访问它的官网:http://oauth.net。浏览了OAuth的流程发现有点儿麻烦,如果自行处理要花去不少的功夫,好在有python-oauth2可以帮助开发者节省时间,更多有关python-oauth2的内容,可以访问其网站:https://github.com/simplegeo/python-oauth2。
说到这里,就不得不提Yahoo!了,为什么呢?开发者编写的客户端如果要访问del.icio.us的数据,需要请求获得OAuth授权,那么del.icio.us又是怎样提供授权的呢?它要实现提供OAuth授权的功能,那么这个功能就是由Yahoo!来提供的。从Yahoo! Developer Network可以找到OAuth的文档。不用奇怪del.icio.us和Yahoo!的关系,留意一下网站首页的下方:a Yahoo! company。
借助python-oauth2可以非常方便地使用OAuth服务,仿效其Using the Client的示例可以很快实现从Yahoo! 获取request token。但是,问题发生了。虽然我已经在Yahoo! Developer Network注册了,拿到了Consumer Key和Consumer Secret,却在执行的时候,发现返回的响应体:oauth_problem=consumer_key_rejected
不难清楚,请求被拒绝了。这是怎么一回事,原来问题出在没有配置权限(Permission),在“Configure your Consumer Key to access”那里,应该选中“Private user data selected below ……”,同时将Delicious选中“Read/Write”。这样才算OK了。–注意,完成这些操作以后,Consumer Key和Consumer Secret会发生变化。以为这样就可以完事大吉,那就高兴太早了,执行脚本后发现一个新的错误: oauth_problem=parameter_absent&oauth_parameters_absent=oauth_callback
很明显Yahoo!认为缺少请求参数oauth_callback,在Yahoo OAuth/OpenID Guide的Get a Request Token中,它明确要求这个参数:
Yahoo! redirects Users to this URL after they authorize access to their private data. If your application does not have access to a browser, you must specify the callback as oob (out of bounds).
因为现在写的代码并不是Web Application,也就没有callback,原以为请求参数中不加入这个没有问题,看来是不行了。可是怎么添加呢?Client似乎没有相应的方法,一番苦恼之后,在python-oauth2的官网找到了同样问题的遭遇者。方法已经由该帖子给出了:
body = urllib.urlencode(dict(oauth_callback=callback_uri)) resp, content = client.request(request_token_uri, 'POST', body=body) |
三事
原以为这样就万事大吉了,不料“惊喜”总是出人意料,代码执行到了最后,突然冒出下面的错误:UnicodeEncodeError: 'ascii' codec can't encode characters in position 4-7: ordinal not in range(128)
这里我要声明一点,执行代码使用的Python为2.5版本,而从del.icio.us获取的数据包含了中文字符。依照Python Toturial里有关Unicode Strings的介绍:
To convert a Unicode string into an 8-bit string using a specific encoding, Unicode objects provide an encode() method that takes one argument, the name of the encoding. Lowercase names for encodings are preferred.
经过一番波折后,最终的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | import sys import cgi import time import urllib import urlparse import oauth2 as oauth from xml.etree import ElementTree # key and secert granted by the service provider for this consumer application consumer_key = 'WARN: YOUR APP'S KEY' consumer_secret = 'WARN: YOUR APP'S SECRET' # oauth's urls of Yahoo! request_token_url = 'https://api.login.yahoo.com/oauth/v2/get_request_token' access_token_url = 'https://api.login.yahoo.com/oauth/v2/request_auth' authorize_url = 'https://api.login.yahoo.com/oauth/v2/get_token' consumer = oauth.Consumer(consumer_key, consumer_secret) body = urllib.urlencode(dict(oauth_callback='oob')) client = oauth.Client(consumer) # Step 1: Get a request token. This is a temporary token that is used for # having the user authorize an access token and to sign the request to obtain # said access token resp, content = client.request(request_token_url, "POST", body=body) if resp['status'] != '200': raise Exception("Invalid response %s. %s" % (resp['status'], content)) request_token = dict(cgi.parse_qsl(content)) print "Request Token" print " - oauth_token = %s" % request_token['oauth_token'] print " - oauth_token_secret = %s" % request_token['oauth_token_secret'] print print "Go to the following link in your browser:" print "%s?oauth_token=%s" % (access_token_url, request_token['oauth_token']) print accepted = 'n' while accepted.lower() == 'n': accepted = raw_input('Have you authorized me? (y/n)') oauth_verifier = raw_input('What is the PIN? ') token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token.set_verifier(oauth_verifier) client = oauth.Client(consumer, token) resp, content = client.request(authorize_url, "POST") access_token = dict(cgi.parse_qsl(content)) print "Access Token" print " - oauth_token = %s" % access_token['oauth_token'] print " - oauth_token_secret = %s" % access_token['oauth_token_secret'] print print "You may now access protected resources using the access token above" print delicious_url = 'http://api.del.icio.us/v2/posts/recent' params = { 'oauth_version': '1.0', 'oauth_nonce': oauth.generate_nonce(), 'oauth_timestamp': int(time.time()) } token = oauth.Token(key=access_token['oauth_token'], secret=access_token['oauth_token_secret']) params['oauth_token'] = token.key params['oauth_consumer_key'] = consumer.key req = oauth.Request(method='GET', url=delicious_url, parameters=params) signature_method = oauth.SignatureMethod_HMAC_SHA1() req.sign_request(signature_method, consumer, token) client = oauth.Client(consumer, token) response, xml = client.request(delicious_url) doc = ElementTree.fromstring(xml) for post in doc.findall('post'): print('%s: %s' % (post.attrib['description'].encode('utf-8'), post.attrib['href'].encode('utf-8'))) |