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')))

Leave a comment

Your comment