最初想爬新浪微博,从开发文档中找到了 Python 的 SDK,竟然是廖雪峰写的。链接直接指向它的 Github 仓库,看了看代码不长,于是就认真研读一下,一来学习 Python 的使用,二来也就知道新浪微博 API 的使用方法了。奈何懒癌又犯了,拖了很久没写这篇总结,竟然在某次搜索时发现了作者自己写的创建 SDK 的步骤,加深了自己的理解,就赶紧来开动写总结了。
OAuth2.0认证
新浪微博新版的 API 全部使用OAuth 2.0 进行权限管理。OAtuth2.0第一次听说是查看 Twitter 的开发文档时,当时死活不理解整个调用的过程。ruanyifeng 的这篇博文——理解 OAuth2.0 讲解的还算通俗易懂。
新浪微博的 OAuth2.0的登录验证流程如下:
- 用户在您的网站上点击“使用新浪微博登录”,您的网站将用户重定向到新浪微博的OAuth认证页,重定向链接中包含client_id参数作为您的网站ID,redirect_uri参数告诉新浪微博当用户登录成功后,将浏览器重定向到您的网站;
- 用户在新浪微博的认证页输入帐号和口令;
- 新浪微博认证成功后,将浏览器重定向到您的网站,并附上code参数;
- 您的网站通过code参数向新浪微博请求用户的access token;
- 您的网站拿到用户的access token后,用户登录完成。
OAuth的access token是提供认证服务的网站(例如新浪微博)生成的令牌,代表一个用户认证信息。在随后的API调用中,传入该access token就代表这个登录用户。
SDK中实现 API 调用的方式
weibo.py 总共有不到330行代码,所以很好奇如何通过330行代码实现了几乎整个微博 API 的调用的。
OAuth 认证过程
使用 API 前,需先设置应用相关的参数,如 APPID、APPSECRET等,然后调用 APIClient。在 APIClient 中,将参数保存:
1 2 3 4 5 |
class APIClient(object): def __init__(self, app_key, app_secret, redirect_uri): self.client_id = app_key self.client_secret = app_secret self.redirect_uri = redirect_uri |
微博的认证需要打开一个 web 页面,登录微博帐号,然后通过回调 url 将 code 参数传回,实现 OAuth 认证中的1、2、3步骤。于是需要一个获取认证 web 页面的函数
1 2 3 4 5 6 7 8 9 |
def get_authorize_url(self, redirect_uri=None, **kw): redirect = redirect_uri if redirect_uri else self.redirect_uri if not redirect: raise APIError('21305', 'Parameter absent: redirect_uri', 'OAuth2 request') response_type = kw.pop('response_type', 'code') return '%s%s?%s' % (self.auth_url, 'authorize', \ _encode_params(client_id = self.client_id, \ response_type = response_type, \ redirect_uri = redirect, **kw)) |
访问返回的页面,登录帐号,页面会重定向到我们设置的 redirect_url 上去,并附带 code 参数。将 code 参数解析出来(当然也可以拷贝出来),再次请求获取 access_token(OAuth 的第四步)
1 2 3 4 5 6 7 8 9 10 11 |
def request_access_token(self, code): resp = _post('https://api.weibo.com/oauth2/access_token', \ client_id = self.client_id, \ client_secret = self.client_secret, \ redirect_uri = self.redirect_uri, \ code = code, \ grant_type = 'authorization_code') logging.info(resp) current = int(time.time()) r = json.loads(resp) return JsonDict(access_token=r.access_token, expires=r.expires_in + current, uid=r.uid) |
_post发送 post 请求,并返回请求的响应结果(即 JSON 字符串)。通过标准库 json 实现初步解析。在 SDK 中,实现了一个 JsonDict 的类,来实现多种的访问 json 字符串内容的方式(这里用到了 python 类的一些特殊函数):
1 2 3 4 5 6 7 |
class JsonDict(dict): def __getattr__(self, attr): return self[attr] def __setattr__(self, attr, value): self[attr] = value |
实现 API 调用
(以下摘自作者原文)
考虑到Python是一个动态语言,与传统的静态语言Java、C++等不同,我们可以充分利用动态语言的特性,在不必为每个API都编写函数的情况下,允许客户端按照以下规则调用API。
首先,通过新浪微博文档可以查询到每个API的URL和参数,例如,获取当前登录用户及其所关注用户的最新微博:
1 2 3 4 5 6 7 8 9 10 11 |
URL:statuses/home_timeline HTTP请求格式:GET 请求参数: source:采用OAuth授权不需要此参数; access_token:OAuth授权的参数; since_id:可选,返回ID比since_id大的微博; max_id:可选,返回ID小于或等于max_id的微博; count:可选,单页返回的记录条数; page:可选,返回结果的页码; … |
其中,access_token对每个API都是必填参数,由APIClient内部处理,然后,将URL的“/”变为“.”,实现链式调用,最后,根据HTTP请求格式调用get()或post(),并传入**kw关键字参数。例如,我们希望能通过如下代码方式调用该API:
1 2 3 4 |
r = client.statuses.home_timeline.get(count=<span class="number">50</span>, page=<span class="number">1</span>) <span class="keyword">print</span> r <span class="comment"># {"statuses":[{"created_at":"Thu Nov 15 16:10:14 +0800 2012","id":3512660609653668,"idstr":"3512660609653668","text":"…"}]</span> |
这样,用户根据新浪微博的API文档,就可以自己构造出API调用。每个API调用返回值自动转化为JSON对象后返回。
下面,我们来实现该调用。
由于Python的动态特性,调用一个class的方法或字段实际上都是查找相应的属性,当该属性不存在时,Python会调用特殊方法__getattr__()试图获得该属性,因此,我们重写__getattr__()并返回一个_Callable对象即可。代码如下:
1 2 3 4 5 6 7 |
<span class="class"><span class="keyword">class</span> <span class="title">APIClient</span><span class="params">(object)</span>:</span> ... <span class="function"><span class="keyword">def</span> <span class="title">__getattr__</span><span class="params">(self, attr)</span>:</span> <span class="keyword">return</span> _Callable(self.access_token, attr) |
_Callable对象也重写了__getattr__()以支持动态调用,并在调用get()或post()方法时返回_Executable对象,代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
<span class="class"><span class="keyword">class</span> <span class="title">_Callable</span><span class="params">(object)</span>:</span> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, access_token, path)</span>:</span> self.access_token = access_token self.path = path <span class="function"><span class="keyword">def</span> <span class="title">__getattr__</span><span class="params">(self, attr)</span>:</span> <span class="keyword">if</span> attr==<span class="string">'get'</span> <span class="keyword">or</span> attr==<span class="string">'post'</span>: <span class="keyword">return</span> _Executable(self.access_token, attr.upper(), self.path) <span class="keyword">return</span> _Callable(self.access_token, <span class="string">'%s/%s'</span> % (self.path, attr)) |
_Executable对象虽然是class,但重写了__call__()方法,因此是一个可调用对象,可以直接实现函数调用。代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
<span class="class"><span class="keyword">class</span> <span class="title">_Executable</span><span class="params">(object)</span>:</span> <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, access_token, method, path)</span>:</span> self.access_token = access_token self.method = method self.path = path <span class="function"><span class="keyword">def</span> <span class="title">__call__</span><span class="params">(self, **kw)</span>:</span> url = <span class="string">'https://api.weibo.com/2/%s.json'</span> % self.path <span class="keyword">return</span> _http_call(url, self.method, self.access_token, **kw) |
这样,用户通过API文档,就可以自行构造出任意API调用的函数链,不但极大地简化了SDK的编写,而且调用接口简单明了。
通过access token发送微博的代码如下:
1 2 3 |
client = APIClient(_APP_ID, _APP_SECRET, _REDIRECT_URI) client.set_access_token(access_token, expires) client.statuses.update.post(status=<span class="string">u'通过Python SDK发微博'</span>) |
总结:
其实自己关注的也就是最后一部分,通过统一的方式实现对不同 API 的调用。正如作者所说,Python 是动态语言,通过对 Magic 方法类的动态变化,可以大大提高代码的重用程度。
随着版本的更新,微博也出现了一些新的 API,和上述的调用 URL 不同,因此而不通用。这就需要针对性的实现了,不过只要是 RESTful 的 API 就可以很方便的通过 HTTP 请求实现数据的交互。