web.py指南性说明
 

    整理一下这些天研究web.py的一些经验,写一篇具有划时代意义的指南性说明~哈哈,开个玩笑,谨以此文献给所有学习web.py的同学以及Aaron Swart.

    web.py是一个开发web应用的python框架,相比于著名的Django与TurboGears,web.py更加让人感觉是用python在写网站。没有复杂的语法规则,简单的一个实现http协议的框架,不依赖其他packet,不依赖操作系统。当然也是有弊端的,框架只实现了基础的web功能,很多功能需要自己动手写,不像php那样一两个函数就搞定任务。

    正如我上句话说的,web.py十分简单,安装只需要sudo easy_install web.py即可,不到2秒中,框架已经躺在服务器里了。如果你没有安装easy_install(比如windows环境),也可以手工安装。github上下载源码,直接安装:

1 python ./setup.py install
    装好以后试一下,import web,没有抛错说明安装成功了。

 

    关于web.py的基础使用方式,一个事例文件hello world即可说明一切:

01 import web
02          
03 urls = (
04     '/(.*)''hello'
05 )
06 app = web.application(urls, globals())
07  
08 class hello:       
09     def GET(self, name):
10         if not name:
11             name = 'World'
12         return 'Hello, ' + name + '!'
13  
14 if __name__ == "__main__":
15     app.run()
    以上代码保存为app.py,然后运行:python ./app.py

 

    默认会监听8080端口作为web服务运行的端口,访问http://127.0.0.1:8080/即可看到Hello world。

    看到事例程序,urls是全局的url规则,'/(.*)'是一个正则,匹配用户访问时的url,'hello'就是处理的类名字。也就是说获得url以后与(.*)匹配,匹配上了就调用hello类处理请求。这里的正则是.*,匹配所有字符,所以用户的一切请求都会交给hello类来处理。

    根据MVC架构的思路来想,一般把这个hello类叫做控制器,controller。如果请求很多的时候,不要把所有的控制器类都放在一个文件里。我们可以使用python包,比如每个处理类一个文件,读放在action文件夹下,那么我们的urls就这么写:

1 urls = (
2     '/msg/?''action.msg.msg',
3     '/login(/quit|/)?''action.login.login',
4     '/log/?''action.log.log',
5     '/file/?''action.upload.upload',
6     '.*''action.show.show'
7 )
    action是包名字,msg是文件名,后一个msg是类名。

 

    接下来,处理好了url规则,事例文件调用了web.application创建了一个app对象,第一个参数就是urls,第二个参数是globals()的返回值。

    然后就可以app.run()了,一切从此时开始。

    不过刚才把请求交给hello类来处理,那么我们看看hello类。所有的控制器类,都可以定义两个函数,GET和POST,顾名思义,这两个函数就用来处理get和post请求。也就是说,用户对app的get请求会交给hello类的GET函数,post请求交给POST函数。

    GET(POST)函数的参数是urls中正则部分的匹配到的值。例子里的name就是(.*)的值。你访问http://localhost:8080/phithon,就能看到输出了Hello, phithon.

    再深一点,在写网站的时候,哪几个部分最重要?无非是数据库增删改查、访问控制(session)、前端(模板)。那么我一个一个来说。

    先说说SESSION吧,session是安全区分访问者的唯一的方法,其他方式用户都能够伪造,只有session是用户不能够修改的。所以,我一般会把一些重要信息记录在session中,比如用户是否登录、用户id、用户权限等。php中session就是一个全局的数组$_SESSION,在web.py中,session是web.ctx中的一个对象(关于web.ctx,请查看cookbook)。

    我们在应用运行前首先需要获得一个SESSION对象赋值给web.ctx.session:

01 web.config.session_parameters['cookie_name'= 'py_pytalk_sid'
02 web.config.session_parameters['cookie_domain'= None
03 web.config.session_parameters['timeout'= 3600
04 web.config.session_parameters['ignore_expiry'= True
05 web.config.session_parameters['ignore_change_ip'= True
06 web.config.session_parameters['secret_key'= '3u12m8xXo0is'
07 web.config.session_parameters['expired_message'= 'Session expired'
08 session = web.session.Session(app, web.session.DiskStore('data/sessions'), initializer={'login'False})
09 def session_hook():
10     web.ctx.session = session
11 app.add_processor(web.loadhook(session_hook))

    众所周知session是用cookie来传递的,所以cookie_name指这个cookie的名字。timeout指session过期时间,秒为单位。secret_key是salt,加密session id。其他一些设置的意义如下:

cookie_name - name of the cookie used to store the session id
cookie_domain - domain for the cookie used to store the session id
timeout - number of second of inactivity that is allowed before the session expires
ignore_expiry - if True, the session timeout is ignored
ignore_change_ip - if False, the session is only valid when it is accessed from the same ip address that created the session
secret_key - salt used in session id hash generation
expired_message - message displayed when the session expires
    

    我就不再赘述了。通过web.session.Session初始化一个session对象,DiskStore是储存session文件的地址,initializer是session对象初始化内容。最后将新建的这个session对象赋值给web.ctx.session,以后就能够直接调用web.ctx.session来访问session了。

    注意,这些设置请在app.run()函数调用前设置好,然后调用app.run()执行程序。以后我们的web.ctx.session的使用就和php中的$_SESSION数组一样了。比如用户登录以后,设置web.ctx.session.uname = '用户名'。访客访问时,判断web.ctx.session.login == True。

    关于访问控制,还有一个小技巧。通常一个网站有后台,后台的页面也不止一个,这时候访问控制就是一个大麻烦。

    如果后台页面很多,我们不可能在每个页面对应的类中判断web.ctx.session.login是否为真,来判断管理员是否登录。所以我们可以让所有后台页面对应的类都继承一个admin类,然后在admin类的构造函数里加入判断代码:

1 class admin:
2     def __init__(self):
3         if not web.ctx.session.login:
4             raise web.seeother('/login')
    如果web.ctx.session.login的值非真,就用raise语句抛出一个错误,并跳转到/login页面去登录。如果不停止运行的话,即使调用seeother,但后面的内容还是会被执行,造成了安全隐患。但这里不能用return,return没任何效果,也不能用sys.exit,否则就直接退出整个网站的运行了。raise是一个好方法,可以完美保证后面的代码不被执行。

 

 

    然后说到数据库。数据库是一个比较容易出漏洞的操作(sql注入),但解决sql注入的方法又是极为简单的(可以说是所有漏洞里最好解决的),那就是参数化查询。web.py提供了一个类似参数化查询的方式,基本可以满足我们日常使用数据库。

    首先我们创建一个数据库对象,并连接数据库:

1 database = 'db/pytalk.db3'
2 db = web.database(dbn = 'sqlite', db = database)
    我使用的sqlite数据库,如果是mysql,方法类似具体看文档。

 

    这个db就是sql对象,我们以后就调用db.query来执行sql语句:

1 res = db.query("SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search"vars = {
2     'i'100,
3     'search''test'
4 })
    用$xxx来占位,然后用一个字典对象来传入数据。这样web.py内部会自动将相应的占位符用具体的数据替代。不知道大家注意没有,$search外面是没打引号的,也就是说web.py会自动帮我加入引号,这也是为什么它能够防范sql注入,因为它能自动处理引号和转义字符。

 

    这样执行sql语句以后,我就再也不用担心注入的问题了,从用户那里获取的数据我直接像这样插入数据库,不用过滤,不用像PHP一样调用addslashes处理了。

    query返回值是一个iterbetter对象,这个对象是一个迭代器,但不像列表,它内部维护着一个指向当前元素的指针,这个指针只会往后走。也就是说我调用了一次res[0],下次就必须调用res[1],再访问res[0]就会抛出错误。

    所以我们可以简单地将这个值转换成一个列表,之后就方便多了:

1 res = list(db.query("SELECT * FROM `log` WHERE `sort` = $i AND `keyword` = $search"vars = {
2     'i'100,
3     'search''test'
4 }))
    这个列表就是sql语句执行结果,len(res)就是行数,列表中的元素是字典,字典的键是列名,字典的值是列值。

 

 

    最后说一下模板引擎,也就是关于前端的各种问题。

1 $def with (data)
2 Hello $data['name']!
    $def定义一个变量。一般这个变量是一个字典,这样我们把所有模板中可能用到的值都放进字典中,作为一个变量传入。

 

    我在这里定义了两个函数,很大程度上简略了模板操作:

1 def assign(self,key,value = ''):
2     if type(key) == dict:
3         self.tplData = dict(self.tplData,**key)
4     else:
5         self.tplData[key] = value
6  
7 def display(self, tplName):
8     self.tplData['render'= web.template.render('templates'globals = self.globalsTplFuncs)
9     return getattr(self.tplData['render'], tplName)(self.tplData)
    self.tplData是包含所有变量的字典。我们使用assign来定义模板变量,定义完所有模板变量以后,调用display显示模板。tplName就是模板的名字(模板文件后缀是html),templates是模板文件地址。

 

    globalsTplFuncs是模板函数,比较简单的模板是用不上的。

    关于安全性,我强调一点。在模板引擎中,web.py是默认会转义<>"'&等xss字符的,也就是说输出$data['name']的时候会转换这个值再输出。如果你不想自动转义,就在$后面加个冒号,$:data['name']就不会转义了。

    什么情况下不想转义,就是模板存在包含的时候(如果转义的话你包含的文件就不是html了)。比如我写一个网站,网站的header一般是不会变的,所以我们最好在模板中创建一个header.html,然后其他模板文件包含之。这就就不用每写一个页面都写html头了。

    那么怎么实现包含?看到我之前那个display函数的第一行,就是将web.template.render方法赋值给了tplData['render']变量。然后我们在模板中就能够直接调用$:tplData['render'].header(tplData)来包含header.html,并将tplData传入。

    具体代码可以参考我后面给的一个app。

 

    说了这么多,可能有的同学还有不少疑惑。其实千万文字不如几行代码,我把我自己做的一个项目开源出来,相信有什么疑惑的方面,在代码中也能迅速找到解答。如果涉及到代码以外的东西,我们可以私下交流。

    已在在线IRC聊天室的小程序。