using werkzeug to build a shorly url app

werkzeug构建'url shortly'应用

参考教程: http://werkzeug.pocoo.org/docs/0.10/tutorial/

使用werkzeug构建一个快键url的应用, 有点类似于http://tinyurl.com/,在运行 服务器的时候,可以将自己指定的url存储到redis 数据库中,然后映射到一个数字上,然后通过服务器 地址加上我们的数字就可以很快键的访问这个 url,并且可以对访问的次数进行一个统计并 存到数据库中。

在构建之前要在本地搭建好redis服务器,然后 还有pip安装werkzeug, redis等自然不在话下。

0: basic wsgi

wsgi的hello world程序有点像这个样子:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!']

因为wsgi程序都是接受一个参数environ和start_response, 然后开始这个响应并返回数据。

而使用werkzeug创建的wsgi程序如下:

from werkzeug.wrappers import Response

def application(environ, start_response):
    response = Response(
        'hello world',
        mimetype='text/plain'、
    )
    return response(environ, start_response)

这里同样是在wsgi应用中创建一个响应对象并 返回调用这个函数的结果。

下面给wsgi应用一点小功能,就是能从请求中获取 一个名字name并返回hello name.

from werkzeug.wrappers import Request, Response

def application(environ, start_response):
    request = Request(environ)
    text = 'hello %s' % request.args.get(
        'name', 'world'
    )
    response = Response(
        text, mimetype='text/plain'
    )
    return response(environ, start_response)

1: creating the folders

先创建好项目的文件夹目录:

/shortly
    /static
    /templates

与其他惯例一样,static放置一些静态文件如css,js 还有图片等,而templates文件夹放置html模板文件。

2: the base structure

整个项目需要用到的Import

import os
import redis
import urlparse
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.utils import redirect
from jinja2 import Environment, FileSystemLoader

下面就是我们的Shortly类,这个就是项目的主要 wsgi应用。

class Shortly(object):

    def __init__(self, config):
        self.redis = redis.Redis(config['redis_host'], config['redis_port'])

    def dispatch_request(self, request):
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)


def create_app(redis_host='localhost', redis_port=6379, with_static=True):
    app = Shortly({
        'redis_host':       redis_host,
        'redis_port':       redis_port
    })
    if with_static:
        app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
            '/static':  os.path.join(os.path.dirname(__file__), 'static')
        })
    return app
  • 初始化函数可以接受字典参数config,设置使用的

redis数据库对象。

  • dispatch_request(request)函数顾名思义就是

分配请求,根据不同的请求返回不同的响应对象, 这里首先简单的返回'hello world'的响应

wsgi_app(self, environ, start_response)函数 就是Shortly类中主要的wsgi应用,这里作为一个 函数成员存在,可以简单理解这个函数就是一个 wsgi应用,而Shortly只是对其进行了一些包装而已。

为了方便的调用Shortly对象,给其增加了__call__ 方法,其实就是返回self.wsgi_app(),这样这个类 就可以方便的调用里面的wsgi方法了。

要运行这个应用,还需要有主函数

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple(
        'localhost',
        5000,
        app,
        # 访问网页出错可以跟踪错误信息
        use_debugger=True,
        # 修改服务器代码的时候会自动重新加载
        use_reloader=True,
    )

现在直接运行我们的shortly.py就可以开启服务器了。

$ python shortly.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader: stat() polling

访问这个地址就可以看到hello world

3: 环境

现在我们有了基本的wsgi应用,还需要处理 数据库的创建还有模板渲染路径等,可以在这个类 的初始化的时候实现

def __init__(self, config):
    self.redis = redis.Redis(
        config['redis_host'],
        config['redis_port'],
    )
    template_path = os.path.join(
        os.path.dirname(__file__),
        'templates'
    )
    self.jinja_env = Environment(
        loader=FileSystemLoader(template_path),
        autoescape=True
    )

def render_template(self, template_name, **context):
    t = self.jinja_env.get_template(template_name)
    return Response(
        t.render(context),   
        mimetype='text/plain',
    )

现在在初始化函数中加入模板的路径,还有一个 jinja环境,类型是jinja2.Environment,这个 环境对象可以自己根据模板路径来自动加载模板。

render_template函数可以返回一个经过模板渲染的 响应对象

4: routing

路由是非常重要的一部分,通过werkzeug.routing 模块里面的Map对象建立路由规则,在里面定义 Rule对象。每个规则会尝试匹配url并添加一个'endpoint',endpoint一般都是一个字符串,可以用来唯一 标识url,也可以用来翻转url。

__init__函数中添加:

def Shortly(object):
    def __init__(...):
        ...
        self.url_map = Map([
            Rule('/', endpoint='new_url'),
            Rule('/<short_id>, endpoint='follow_short_link'),
            Rule('/<short_id>+, endpoint='short_link_details'),
        ])

这里的url处理其实和webpy也是类似的,第一个rule 说明'/'地址用new_url类来处理,即创建新url的界面 ,'/<short_id>'用'follow_short_link'来处理,意思 就是连接到这个id对应的url的网页,如果后面加上'+' 就是第三个rule,用'short_link_details'来处理, 意思是显示这个url的一些详情,包括这个url的 访问次数等。注意这里访问的地址在第一个字符'/' 的前面其实都要加上服务器的地址。

有了这些url的映射关系,还要在dispatch的时候 根据这些url来返回不同的响应。假设我们对于每个 endpoint,采用'on_' + endpoint的函数来处理, 这样我们只需要在dispatch的时候对不同的url采用 不同的函数来处理就行。

def dispatch_request(self, request):
    adapter = self.url_map.bind_to_environ(
        request.environ
    )
    try:
        endpoint, values = adapter.match()
        return getattr(self, 'on_' + endpoint)(
            request, **values
        )
    except HTTPException, e:
        return e

前面我们已经在self.url_map这个Map类型的对象 中定义好url的映射规则,现在用bind_to_environ() 方法来获取用户访问的地址等,绑定好之后就可以 调用match()方法来获取用户所访问的url对应的 endpoint和values了。values是所捕获的url中的一些参数,比如访问http://localhost:5000/foo的时候 获取到的数据如下:

endpoint = 'follow_short_link'
values = {'short_id': u'foo'}

我们自己定义好'on_' _ endpoint函数之后将endpoint 和values参数传入就可以自己来决定怎么处理

5: first view

现在先定义on_new_url函数怎么处理,首先这个 view有get和post两种请求情况,如果是get情况则 返回一个创建新url的界面给用户,如果是post请求 说明是用户已经提交数据,则更新数据库并且对 用户做重定向。

class Shortly(...):
    ...
    def on_new_url(self, request):
        error = None
        url = ''
        if request.method == 'POST':
            url = request.form['url']
            if not is_valid_url(url):
                error = 'please enter a valid url.'
            else:
                short_id = self.insert_url(url)
                return redirect('/%s+' % short_id)
        return self.render_template(
            'new_url.html',
            error=error,
            url=url,
        )

这里需要辅助函数is_valid_url()来判断一个用户 输入的url是否是合法的url。上面函数中,对于post请求,则在表单中提取用户输入的url,然后判断,如果 合法,则调用辅助函数insert_url(url)将其存到 数据库中,并且重定向到这个id下的detail界面。 如果是非法输入或者get请求,则都用new_url.html 模板来渲染并返回。

一些辅助函数:

def is_valid_url(url):
    parts = urlparse.urlparse(url)
    return parts.scheme in ('http', 'https')

def insert_url(self, url):
    short_id = self.redis.get('reverse-url:' + url)
    if shord_id is not None:
        return short_id  # 这个url的id已经存在
    # 最后一个id+1之后作为新的url的id
    url_num = self.redis.incr('last-url-id')
    short_id = base36_encode(url_num)
    self.redis.set(
        'url-target:' + short_id, url
    )
    self.redis.set(
        'reverse-url:' + url, short_id
    )
    return short_id

insert函数中,每个url要添加两项数据,一个是 url到id的映射,一个是id到url的映射即reverse. 在存储id的时候,这里用了一个自定义的辅助函数 base36_encode()对id的数字进行了编码。

def base36_encode(number):
    """
    >>> base36_encode(0)
    '0'
    >>> base36_encode(35)
    'z'
    """
    assert number >= 0, 'positive integer required'
    if number == 0:
        return '0'
    base36 = []
    while number != 0:
        number, i = divmod(number, 36)
        base36.append('0123456789abcdefghijklmnopqrstuvwxyz'[i])
    return ''.join(reversed(base36))

6: redirect view

def on_follow_short_link(self, request, short_id):
    link_target = self.redis.get(
        'url-target:' + short_id
    )
    if link_target is None:
        raise NotFound()
    self.redis.incr('click-count' + short_id)
    return redirect(link_target)

注意到这里如果数据库没有找到这个id则raise NotFound(),这样在dispatch函数中就会返回一个404响应。

7: detail view

def on_short_link_details(self, request, short_id):
    link_target = self.redis.get('url-target:' + short_id)
    if link_target is None:
        raise NotFound()
    click_count = int(self.redis.get('click-count:' + short_id) or 0)
    return self.render_template('short_link_details.html',
        link_target=link_target,
        short_id=short_id,
        click_count=click_count
    )

处理方式和上一个类似,如果找不到该id就抛出错误。 找到的话则查询它被访问的次数并在最后返回。

到此为止整个应用就基本完成,最后就剩下一些 html模板文件和样式表了。对于上面查询数据库没有 找到id抛出的异常,可以在dispatch方法中对exception做一些处理,比如用404.html来进行渲染并返回。

posted @ 2015-07-31 16:08  Jolin123  阅读(254)  评论(0编辑  收藏  举报