用GAE访问fanfou API,支持发图

可下载版本在这里:https://gist.github.com/789880

# -*- coding: utf-8 -*-
from django.utils import simplejson as json
from google.appengine.api import urlfetch

import urllib
import mimetypes
import base64
import random

_api_url = 'http://api.fanfou.com/'

_http_methods={
    'GET':urlfetch.GET,
    'POST':urlfetch.POST,
    'HEAD':urlfetch.HEAD,
    'PUT':urlfetch.PUT,
    'DELETE':urlfetch.DELETE
}

def _generate_boundary(length=16):
    s = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
    a = []
    for i in range(length):
        a.append(random.choice(s))
    return ''.join(a)

def _get_content_type(filename):
    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'

def _encode_multipart_formdata(fields, files=[]):
    """
    fields is a sequence of (name, value) elements for regular form fields.
    files is a sequence of (name, filename, value) elements for data to be uploaded as files
    Return (boundary, body)
    """
    boundary = _generate_boundary()
    crlf = '\r\n'

    l = []
    for k, v in fields:
        l.append('--' + boundary)
        l.append('Content-Disposition: form-data; name="%s"' % k)
        l.append('')
        l.append(str(v))
    for (k, f, v) in files:
        l.append('--' + boundary)
        l.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (k, f))
        l.append('Content-Type: %s' % _get_content_type(f))
        l.append('')
        l.append(str(v))
    l.append('--' + boundary + '--')
    l.append('')
    body = crlf.join(l)
    return boundary, body


class Fanfou:
    def __init__(self, username, password):
        self.username=username
        self.password=password
        self.http_status=0
        self.http_headers={}
        self.http_body=''

    def _fetch(self, method, url, params={}, headers={}, files=None):
        payload=None
        if method.upper() in ['POST','PUT']:
            if files and type(files) == list:
                boundary, payload = _encode_multipart_formdata(params.items(), files)
                headers['Content-Type']='multipart/form-data; boundary=%s' % boundary
            else:
                payload=urllib.urlencode(params)
        res=urlfetch.fetch(url, payload, _http_methods[method.upper()], headers)
        self.http_status=res.status_code
        self.http_headers=res.headers
        self.http_body=res.content
        return res.content

    def api_call(self, http_method, api_method, params={}, files=None):
        raw_content=self._fetch(
            http_method,
            ''.join([_api_url,api_method,'.json']),
            params,
            {'Authorization':'Basic %s' % base64.b64encode('%s:%s' % (self.username, self.password))},
            files
        )
        return json.loads(raw_content)

用法:

from fanfou import Fanfou 

fanfou=Fanfou('name','pass') 

#时间线
fanfou.api_call('GET', 'statuses/public_timeline') 

#传图 
fanfou.api_call('POST', 'photos/upload', {'status':'test...'}, files=[('photo','aaa.jpg', photo_data )])
More

使用Instagram API

这里描述的是官方公布的API,而不是通过hack得到的api。
有兴趣可以去这里注册 http://instagr.am/developer/ 。可能会有一个审核激活的过程,大约要几天的时间。
完成后会收到一封邮件,然后就能登录查看文档了。但这个文档似乎暂时并不是很丰富,因为我没找到用于上传的api。

这里主要写了一些使用上的注意事项。

1、外链图片
GET http://instagr.am/p/{shortcode}/media
它接受一个参数size,t表示缩略图 m表示中等, l表示最大,默认为m。
假设有链接 http://instagr.am/p/BSJRn/ 则可以:
<img src="http://instagr.am/p/BSJRn/media?size=m" />
效果:


2、shortcode 转换成 media-id
shortcode 就是 http://instagr.am/p/BSJRn/ 里边的 'BSJRn'
截止目前,我在那个文档中并没有找到有关如何将shortcode 转化成 media-id 的描述。但是通过一些猜测和验证,很容易就会得出正确的算法。
很简单,实际上shortcode就是一个64进制数字,解码的过程就是64进制转化成10进制的过程。
代码:

#A代表0,B代表1,..., _代表63
_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
_chars_num=len(_chars)

def deshortcode(t):
    t=str(t)
    n,i=0,len(t)-1
    for c in t:
        if c not in _chars: return 0
        n+=(_chars.index(c)*pow(_chars_num, i))
        i-=1
    return n

print deshortcode('BSJRn')
# 21533799

3、参数中的 redirect_uri 必须与注册中的 CALLBACK URL 相同
这一点是跟Twitter API 不同的地方,在注册Twitter客户端时,那个CALLBACK URL可以随便写;而Instagram 则必须跟注册时的相同,否则会硬邦邦的告诉你这样不行。
你可以点击 这里 可以注册一个 Instagram 客户端,不过你必须首先通过之前所说的那个验证才行。

4、验证过程比Twitter API简单得多
不用什么OAuth库,也不用什么参数签名(大概这就是OAuth2.0吧?)。直接通过https获得access_token,然后用这个access_token就可以做任何事情了。特别简单。文档中都有描述这里不多说了。 More

给 urlfetch 加上自动重试功能

在使用Google App Engine 的 urlfetch 请求其他服务时。时不时会遇到因目标服务出现错误(如超时、500 等)导致自己的程序出现错误。

例如 Twitter API 不稳定时,经常会返回一个500错误,如果自己的程序不加处理,一定会跟着出错。即便加入错误处理,也会给用户一个错误页面,这个体验很不好。实际上这样的错误通常只是暂时的随机出现的。只要重试几次就能获得一个满意的结果。

原理:
把urlfetch.fetch放入一个循环中。若请求成功,则返回数据;若抛出 DownloadError 异常,则开始新一轮循环。如果出错次数大于一定数值,那么意味着对方服务器差不多已经死彻底了,不想出错都不行了,就退出这个循环并抛出异常。

代码:

# xfetch.py
from google.appengine.api import urlfetch

_max_fetch_count = 5

def fetch(url, payload=None, method=urlfetch.GET,
headers={}, allow_truncated=False, follow_redirects=True,
deadline=None, validate_certificate=None):
    http_body=''
    for count in range(_max_fetch_count):
        try:
            http_body = urlfetch.fetch(url, payload, method, headers, allow_truncated, follow_redirects, deadline, validate_certificate)    
            return http_body
        except urlfetch.DownloadError, e:
            continue
    raise Exception('Max fetch count exceeded.')

注意事项:
  • _max_fetch_count*deadline 必须小于等于 request duration
  • 必须考虑到请求次数的限额,在个人负担允许范围内使用。

More

在GAE中迁移 django 0.96到1.2时遇到的模板问题

django 0.96 与 django 1.2 之间的细微差别造成很多麻烦事。


一、自动转换(autoescape)的问题

autoescape 主要是为了防止模板变量中含有标签等html元素,它会自动将html标签等元素转换实体。
例如:把
<script>
转换成
&lt;script&gt;

但是 django 0.96 没有autoescape,而django 1.2默认会自动对模板变量进行过滤。从0.96更换至1.2的时候,会发现很多不想被过滤的标签全都被过滤了,可以使用safe标记成安全字符。
如果使用了escape过滤器,则不管它出现在过滤链条的哪个位置,一律只会在链条的最后执行。并且如果变量已经标记为safe,那么escape则不会做任何事情。
例如:
{{ val|safe }}

解决办法有两种,一种是将原来模板中的escape去掉,再把不需要过滤的挨个修改成safe。另一种就是全局关闭autoescape:

{% autoescape off %}
....模板其他部分...
{% endautoescape %}


二、{% spaceless %}标签默认行为差别

在 django 0.96 中,去掉所有标签间的空白符号,仅标签间留一个空格。
在 django 1.2 中,去掉所有标签间的空白符号,标签间什么都不留。

举例:
<a href="#">This</a> <a href="#">is</a> <a href="#">mine</a>.
django 0.96 显示为:
This is mine.
django 1.2 显示成:
Thisismine.

这个解决办法很简单,不用这个标签即可,用与不用一般没什么区别。


三、{% include %}与{% base %}标签的路径问题

django 1.2 的标签中,不能使用上层路径,只能从模板根目录向下找。
在 django 0.96 中,可以这样使用:

{% include "../home.html" %}
而在 django 1.2 你必须这样:

{% include "path/to/home.html" %}

对比一下0.96与1.2的 django.template.loaders.file_system 模块,就会发现 get_template_sources 方法的文件路径结合方式不同。0.96 用的是一般的 os.path.join 方法,而 1.2 用的是 django.utils._os.safe_join 方法,后者限制不允许使用 base path 外的路径。而 include 和 extends 标签会先从base path开始寻找模板(这也许是bug?),如果路径里边有 “../” 可能会直接跳出这个 base path ,于是会触发异常:
>>> from django.utils._os import safe_join 
>>> safe_join('templates','../bbb.html') 
Traceback (most recent call last): 
File "<stdin>", line 1, in <module> 
File "C:\Python26\lib\site-packages\django\utils\_os.py", line 44, in safe_joi 
n 
raise ValueError('the joined path is located outside of the base path' 
ValueError: the joined path is located outside of the base path component 
>>> 

另外一个麻烦事就是在django 1.2中必须使用一个空app进行初始化设置才能正确找到模板。
举例,
目录结构:

project 
|--templates 
| |--outter.html
| |--bbb
| | |--in_b.html 
| |--aaa
| | |--in_a.html 
outter.html :
{% include "bbb/in_b.html"%}
in_a.html :
{% include "bbb/in_b.html"%}
main.py:

from google.appengine.dist import use_library
use_library('django', '1.2')

from django.conf import settings
#必须找个空文件(nothing.py)作为引子才能正确找到模板文件
settings.configure(INSTALLED_APPS=('nothing',))

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util, template
import os

class MainHandler(webapp.RequestHandler):
    def get(self):
        #不论settings是否设置INSTALLED_APPS=('nothing',)
        #在outter.html中总能找到bbb/in_b.html或aaa/in_b.html
        self.render('outter.html', debug=True)

        #在aaa/in_a.html中,只有在settings中设置了
        #INSTALLED_APPS=('nothing',)时
        #才能正确找到 bbb/in_b.html ,反过来也是一样。
        self.render('aaa/in_a.html', debug=True)

    def render(self, tpl, vals={},debug=False):
        directory=os.path.dirname(__file__)
        path=os.path.join(directory, os.path.join('templates',tpl))
        self.response.out.write(template.render(path, vals, debug))

def main():
    application = webapp.WSGIApplication([('/', MainHandler)],
                                         debug=True)
    util.run_wsgi_app(application)


if __name__ == '__main__':
    main()


参考:
1、https://groups.google.com/forum/#!msg/google-appengine-python/YaqfeygoiaI/RtDh9pL6XJQJ

More

django模板自动转义(autoescape)机制

用过php的人都头疼过那个magic quotes(特殊字符的转义)问题。经常需要判断哪个情况下自动了转义,那个情况下没有自动转义。不仅要防止忘记了转义,还要防止那些转义之后又被转义的情况。总之很头疼。django模板也有自动转义(html特殊字符)功能,根据文档和阅读源码,发现这个机制很有趣,它能巧妙的避免那些转义多次的问题。

一、状态机制

django把通过模板过滤器(filter)的数据分为3种状态:

1、原始态(Raw string)
即普通的str或unicode类型。如果开启了自动转义,这种数据会被自动转义;如果关闭了自动转义,那么这种数据则原样输出,不做任何改变。

2、安全态(Safe string)
这是已经做过了所有必要的转义。在它再次被修改前,是安全的,输出时不会再被转义。如果处于这个状态的数据经又过了某个过滤器,状态可能会发生改变。主要是SafeString和SafeUnicode,它们是SafeData的子类型。

3、需转义态(Strings marked as "needing escaping")
被标记为“需转义态”的数据,在输出时都会被转义。主要是EscapeString和EscapeUnicode,它们是EscapeData的子类型。

在开启自动转义的情况下,django模板数据过滤器链的尾端,都会根据数据状态进行转义,凡是处于“安全态”的数据都原样输出,凡是处于“原始态”或“需转义态”的数据都会被转义,并使之转换为“安全态”。最后只有处于“安全态”的数据才会最终将其内容输出至html文档中。在数据通过过滤器时,过滤器根据所提供的数据状态判断是否进行转义或其他操作,并根据转义或操作的结果决定是否改变数据状态。


二、内部处理流程

假设有源数据(data),要通过过滤器。
  1. 如果过滤器的 needs_autoescape 属性为True,则给该过滤器准备一个 autoescape 参数。
  2. 运行过滤器,得到一个新数据(new_data)。如果提供了autoescape参数,过滤器就可以根据autoescape的值(True/False)做必要的处理。
  3. 如果过滤器的is_safe属性为True,并且源数据(data)是SafeData实例,则将新数据(new_data)转换成“安全态”。否则,如果源数据(data)是EscapeData,则将新数据转换为“需转义态”。
  4. 返回处理好的新数据(new_data),即下一个过滤器的源数据。
  5. 进入下一个过滤器,重复步骤1-4 直至过滤器链中最后一个过滤器。
  6. 判断过滤器链条最终输出数据(final_data)的状态,如果final_data为“安全态”,则原样渲染到网页,如果final_data为其他状态,则会对final_data进行转义,并渲染到网页。

三、过滤属性的作用及用法

1、is_safe
仅当过滤器中未引入任何不安全数据时使用。但要注意如果替换或删除某些字符也会导致不安全,例如,将已经处于安全状态的数据中的链接<a>去掉了一个大于号变成<a。因此使用这个属性必须要保证真的不会对已经处于安全态的数据造成破坏。
过滤器定义:
@register.filter
def safe_filter(value):
    '%s <a href="#">x</a>' % value
    return nv
safe_filter.is_safe=True
模板:

<!--
python code:
data='<a href="http://www.google.com">Google</a>'
-->
{% autoescape on %}
<ul>
	<li>{{ data }}</li>
	<li>{{ data|safe_filter}}</li>
	<li>{{ data|safe}}</li>
	<li>{{ data|safe|safe_filter }}</li>
</ul>
{% endautoescape %}
显示为:

  • <a href="http://www.google.com">Google</a>
  • <a href="http://www.google.com">Google</a> <a href="#">x</a>
  • Google
  • Google  x

2、needs_autoescape
凡是带有这个属性的过滤器,必须提供一个名为autoescape的参数。值为True代表开启了自动转义,False代表关闭了自动转义。过滤器中一般会用conditional_escape与mark_safe等方法来辅助处理各类状况。
conditional_escape(html),根据状态转义,如果html状态为“安全态”,就直接返回html,否则就对html进行转义。
mark_safe(s),将s转换成“安全态”。

from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe

def initial_letter_filter(text, autoescape=None):
    first, other = text[0], text[1:]
    
    #如果开启了自动转义,就根据text的状态进行转义
    if autoescape:
        esc = conditional_escape
    else:
        esc = lambda x: x
    result = '<strong>%s</strong>%s' % (esc(first), esc(other))
    return mark_safe(result)
initial_letter_filter.needs_autoescape = True


参考:
  1. http://docs.djangoproject.com/en/1.2/howto/custom-template-tags/#filters-and-auto-escaping
More