http://chaoxz2005.blog.163.com/blog/static/15036542012863405266/
http://www.dajo.com.cn/a/boke/python/2013/1125/146.html
这里我们将会创建一个仿制TinyURL的应用,将URLs存储到一个redis实例。为了这个应用,我们将会使用的库包括,用于模板的Jinja 2、用于数据库层的redis和用于WSGI层的Werkzeug。
你可以使用pip安装需要的库:
- $pipinstallJinja2redis
你还需要确保在本地机器上正在运行着一个redis服务器。如果你在使用OS X,你可以使用brew来安装它:
- $brewinstallredis
如果你使用ubuntu或debian,你可以使用apt-get:
- $sudoapt-getinstallredis
redis是针对UNIX系统开发的,且从未真正地为在Windows上运行进行设计。然而为了开发目的,非官方的移植版本也可以很好地工作。你可以从github获得这些库。
[译者:可能还需要添加redis的python支持,在终端执行sudo easy_install redis即可。]
介绍Shortly
本教程中我们将会一起来使用Werkzeug创建一个简单的URL简化服务。请记住Werkzeug不是框架,而是一个可以创建你自己的框架或应用的、非常灵活的库。这里我们使用的方法只是诸多可用方法的一种。
数据存储方面,我们这里将会使用redis来代替关系型数据库,以保持其简洁性。这正是redis擅长的工作类型。
最终结果会如下图这样:
第0步:WSGI基础介绍
Werkzeug是一个WSGI功能库。WSGI本身是一个用来确保你的web应用能够与webserver进行对话,更重要的是,确保web应用之间能够一起配合工作的协议或约定。
在没有Werkzeug帮助下,用WSGI实现的一个基本“Hello World”应用看起来是这样的:
- defapplication(environ,start_response):
- start_response(‘200OK’,[(‘Content-Type’,‘text/plain’)])
- return[‘HelloWorld!’]
WSGI应用是你可以调用、传递一个environ字典和一个start_response函数的东西。environ包含所有的传入信息,start_response函数可以用来指示response的开始。使用Werkzeug之后,你将不再需要直接处理被提交上来的请求(request)和应答(response)对象。
请求数据获取environ对象,并允许你以一种良好的方式访问environ中的数据。response对象本身也是一个WSGI应用,提供了很多友好的创建response的方法。
下面的代码演示了如何编写带有response对象的应用:
- fromwerkzeug.wrappersimportResponse
- defapplication(environ,start_response):
- response=Response(‘HelloWorld!’,mimetype=‘text/plain’)
- returnresponse(environ,start_response)
下面是一个可以查看URL中查询字符串的扩展版本(不同之处在于,它查找URL中的name参数的值,并替换单词”World”):
- fromwerkzeug.wrappersimportRequest,Response
- defapplication(environ,start_response):
- request=Request(environ)
- text=‘Hello%s!’%request.args.get(‘name’,‘World’)
- response=Response(text,mimetype=‘text/plain’)
- returnresponse(environ,start_response)
以上就是所有关于WSGI的你需知的内容。
第1步:创建文件夹
在开始之前,先创建本应用需要的目录结构:
- /shortly
- /static
- /templates
shortly文件夹不是一个python包,而仅仅用于放置我们的文件。在这个文件夹中,我们将会使用接下来的步骤直接放置我们的主模块。在static中的文件允许此应用的用户通过HTTP访问,这是存放css和javascript文件的地方。在templates中我们将会创建Jinja2版本的模板代码,在将来教程中创建的模板会保存在此目录之中。
第2步:基本架构
下面直入主题,创建我们应用的一个模块。先在shortly中创建shortly.py文件。在开始时需要一系列导入语句,我会在这里加入所有的import,甚至包括那些当前没有立即使用的,这样可以使得代码更加清晰。
- importos
- importredis
- importurlparse
- fromwerkzeug.wrappersimportRequest,Response
- fromwerkzeug.routingimportMap,Rule
- fromwerkzeug.exceptionsimportHTTPException,NotFound
- fromwerkzeug.wsgiimportSharedDataMiddleware
- fromwerkzeug.utilsimportredirect
- fromjinja2importEnvironment,FileSystemLoader
接下来可以创建我们应用的基本架构,以及一个创建它的新实例的函数。我们也可以选择带有一个WSGI中间件,并在web上导出所有static中的文件。
- classShortly(object):
- def__init__(self,config):
- self.redis=redis.Redis(config[‘redis_host’],config[‘redis_port’])
- defdispatch_request(self,request):
- returnResponse(‘HelloWorld!’)
- defwsgi_app(self,environ,start_response):
- request=Request(environ)
- response=self.dispatch_request(request)
- returnresponse(environ,start_response)
- def__call__(self,environ,start_response):
- returnself.wsgi_app(environ,start_response)
- defcreate_app(redis_host=‘localhost’,redis_port=6379,with_static=True):
- app=Shortly({
- ‘redis_host’:redis_host,
- ‘redis_port’:redis_port
- })
- ifwith_static:
- app.wsgi_app=SharedDataMiddleware(app.wsgi_app,{
- ‘/static’:os.path.join(os.path.dirname(__file__),‘static’)
- })
- returnapp
最后,我们可以添加一段启动本地开发服务器的代码,其中包括一个自动代码reloader和一个debugger:
- if__name__==‘__main__’:
- fromwerkzeug.servingimportrun_simple
- app=create_app()
- run_simple(‘127.0.0.1’,5000,app,use_debugger=True,use_reloader=True)
这里的基本思想是,Shortly类是一个真实的WSGI应用。__call__函数直接分派至wsgi_app。这样便可以像在create_app函数中所作的那样,通过包装wsgi_app的方法来应用中间件。实际的wsgi_app方法接下来创建一个Request对象,并且调用dispatch_request,然后这个方法必须返回一个再次由WSGI应用评估的Response对象。正如你看到的:下面全是乌龟[译注:原文turtles all the way down,这是对由“不动的推动者”悖论提出的宇宙学中无限退化问题的一个诙谐的表述,表示循环往复的意思]。我们创建的Shortly类,以及Werkzeug中的任何请求对象,共同实现了WSGI接口。这样做的效果之一就是你甚至可以从dispatch_request方法中返回另一个WSGI应用。
create_app工厂方法可以用来创建我们的应用的一个新的实例。不仅仅会向应用传递一些如配置信息之类的参数,还可以选择增加一个导出静态文件的WSGI中间件。该方法甚至可以在我们没有设置服务器来提供static中的文件时,对这些文件进行访问。这对于开发来说是非常有帮助的。
插曲:运行应用
现在你应该能够使用python运行这个文件,并可以看到你本地机器上的一个服务:
- $pythonshortly.py
- *Runningonhttp://127.0.0.1:5000/
- *Restartingwithreloader:stat()polling
它还会告诉你reloader被激活。它将会一些相关技术来检查是否有哪个磁盘上的文件被修改,并自动重新开始。
现在访问URL将会看到”Hello World!”。
第3步:环境
现在已经拥有了基础的应用类,我们可以让构造函数做一些有用的工作,并在其中提供一些方便使用的帮助函数。我们需要渲染模板并连接到redis,因此下面对这个类进行一些扩展:
- 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)
- defrender_template(self,template_name,**context):
- t=self.jinja_env.get_template(template_name)
- returnResponse(t.render(context),mimetype=’text/html’)
第4步:路由
接下来是路由。路由是匹配并解析URL为我们可用的形式的过程。Werkzeug提供了一个灵活的内嵌路由系统,我们可以用它完成这项工作。它工作的方式是,你创建一个Map实例,并增加一些Rule对象。每个规则包含一个用来尝试针对一个endpoint匹配URL的模式模板。endpoint通常是一个字符串,可以用来唯一识别这个URL。我们还可以用它对URL做自动反转,不过这不是我们在本教程要做的工作。
将下面的代码添加至构造函数:
- self.url_map=Map([
- Rule(‘/’,endpoint=‘new_url’),
- Rule(‘/<short_id>’,endpoint=‘follow_short_link’),
- Rule(‘/<short_id>+’,endpoint=’short_link_details’)
- ])
这里我们创建了一个带有三个规则的URL map。“/”表示URL空间的根,这里我们将仅仅分派一个实现了创建一个新URL的逻辑的函数。然后的一个规则连接短链接到目标URL,另一个带有同样的规则,只是在短链接之后增加了一个加号(+),将其连接到短链接的细节信息。
所以,我们如何从endpoint找到一个函数呢?这取决于你自己。我们将在此教程中使用的方法是会调用类本身的一个on_加上endpoint的函数。这里是具体的实现:
- defdispatch_request(self,request):
- adapter=self.url_map.bind_to_environ(request.environ)
- try:
- endpoint,values=adapter.match()
- returngetattr(self,‘on_’+endpoint)(request,**values)
- exceptHTTPException,e:
- returne
我们讲URL map绑定到当前环境,并获得一个URLAdapter。该适配器可以用来匹配请求,但也可以反转URL。其匹配方法将会返回endpoint和一个URL中值的字典。例如对于follow_short_link规则来说,它拥有一个变量部分称为short_id。当我们访问http://localhost:5000/foo时,我们将会得到其伴随的值:
- endpoint=‘follow_short_link’
- values={‘short_id’:u’foo’}
如果它没能匹配任何东西,则会唤起一个NotFound异常,这是一个HTTPException。所有的HTTP异常本身也都是WSGI应用,她们渲染一个默认的错误页面。因此我们仅需捕获所有这些信息,然后返回错误本身。
如果所有工作正常,我们便调用函数on_ + endpoint,并将请求作为参数传递给它,就好像所有的URL参数作为关键词参数,并返回那个函数返回的应答对象一般。
第5步:第一个视图
让我们开始第一个视图:对于新的URL的视图:
- defon_new_url(self,request):
- error=None
- url=‘’
- ifrequest.method=‘POST’:
- url=request.form[‘url’]
- ifnotis_valid_url(url):
- error=‘PleaseenteravalidURL’
- else:
- short_id=self.insert_url(url)
- returnredirect(‘/%s+’%short_id)
- returnself.render_template(‘new_url.html’,error=error,url=url)
这里的逻辑应当非常易于理解。基本上是我们检查请求的方法是一个POST,这是我们验证URL,并在数据库中增加一个新的入口,然后重定位到细节页面。这意味着我们需要编写一个函数和一个帮助方法。对于URL验证,下面的方法就足够了:
- defis_valid_url(url):
- parts=urlparse.urlparse(url)
- returnparts.schemein(‘http’,‘https’)
为了插入URL,我们所需做的就是在类中增加下面的一个小方法:
- definsert_url(self,url):
- short_id=self.redis.get(‘reverse-url:’+url)
- ifshort_idisnotNone:
- returnshort_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)
- returnshort_id
reverse-url:加上URL会存储short id。如果URL已经被提交了,这肯定不会是None,而是我们需要的short id,我们便可以仅仅返回这个值。否则我们增加last-url-id键,并将其转换为基于base36的形式。然后我们存储这个链接和redis中的反转入口。下面是转换到base 36的函数:
- defbase36_encode(number):
- assertnumber>=0,‘positiveintegerrequired’
- ifnumber==0:
- return‘0’
- base36=[]
- whilenumber!=0:
- number,i=divmod(number,36)
- base36.append(‘0123456789abcdefghijklmnopqrstuvwxyz’[i])
- return‘’.join(reversed(base36))
这样只要添加模板,这个视图就可以工作了。我们会在将来创建这个模板,现在先编写其他视图,然后在完成模板工作。
第6步:重定向视图
重定向视图比较简单。所有要做的工作就是在redis中查找链接后重定向。在此之外,我们还增加了一个计数器,这样就可以知道这些链接被点击的情况。
- defon_follow_short_link(self,request,short_id):
- link_target=self.redis.get(‘url-target:’+short_id)
- iflink_targetisNone:
- raiseNotFound()
- self.redis.incr(‘click-count:’+short_id)
- returnredirect(link_target)
在URL不存在的情况下,我们会手工唤起一个NotFound异常,这将会冒泡到dispatch_request函数,并被转换为一个默认的404应答。
第7步:细节视图
链接细节视图非常类似,仅仅是对一个模板的再次渲染。在查询目标之外,我们还询问redis该链接被点击的次数,如果这个键尚不存在则默认是0:
- defon_short_link_details(self,request,short_id):
- link_target=self.redis.get(‘url-target:’+short_id)
- iflink_targetisNone:
- raiseNotFound()
- click_count=int(self.redis.get(‘click-count:’+short_id)or0)
- returnself.render_template(‘short_link_details.html’,
- link_target=link_target,
- short_id=short_id,
- click_count=click_count
- )
请注意redis通常使用字符串,因此你必须手动将click count转变成int。
第8步:模板
下面是所有的模板。只需将它们放置在templates文件夹。Jinja2支持模板继承,因此我们首先要做的是创建一个布局模板,在其中使用块(block)作为占位符。我们还需要设置Jinja2从而可以自动的从HTML规则中分离字符串,这样就不必在这上面花费时间。这样可以防止XSS攻击和渲染错误。
layout.html
- <!doctypehtml>
- <title>{%blocktitle%}{%endblock%}|shortly</title>
- <linkrel=stylesheethref=/static/style.csstype=text/css>
- <divclass=box>
- <h1><ahref=/>shortly</a></h1>
- <pclass=tagline>ShortlyisaURLshortenerwrittenwithWerkzeug{%blockbody%}{%endblock%}
- </div>
new_url.html
- {%extends"layout.html"%}
- {%blocktitle%}CreateNewShortURL{%endblock%}
- {%blockbody%}
- <h2>SubmitURL</h2>
- <formaction=""method=post>
- {%iferror%}
- <pclass=error><string>Error:</strong>{{error}}
- {%endif%}
- <p>URL:
- <inputtype=textname=urlvalue="{{url}}"class=urlinput>
- <inputtype=submitvalue="Shorten">
- </form>
- {%endblock%}
short_link_details.html
- {%extends"layout.html"%}
- {%blocktitle%}Detailsabout/{{short_id}}{%endblock%}
- {%blockbody%}
- <h2><ahref="/{{short_id}}">/{{short_id}}</a></h2>
- <dl>
- <dt>Fulllink
- <ddclass=link><div>{{link_target}}</div>
- <dt>Clickcount:
- <dd>{{click_count}}
- </dl>
- {%endblock%}