2010年2月28日星期日

CherryPy的一个bug

web2py开发模式的web服务器是直接copy的CherryPy的代码,前段时间我们意外的发现了这段代码的一个小问题,后来查了一下,发现CherryPy也仍然存在这个问题。

[http://www.cherrypy.org/browser/trunk/cherrypy/wsgiserver/__init__.py?rev=2650#L566]
566        # Unquote the path+params (e.g. "/this%20path" -> "/this path").
567        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
568        #
569        # But note that "...a URI must be separated into its components
570        # before the escaped characters within those components can be
571        # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
572        # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path".
573        try:
574            atoms = [unquote(x) for x in quoted_slash.split(path)]
575        except ValueError, ex:
576            self.simple_response("400 Bad Request", ex.args[0])
577            return
578        path = "%2F".join(atoms)
579        self.path = path

574行中,url路径被分割后全部逆转义(unquote);在578行中,url路径被用"%2F"(即斜杠“/”)恢复。

问题就出在578行。"%2F"是一个转义了的(quote)字符,url路径也应该是被转义了的,而显然atoms列表里的所有元素都已经被逆转义而没有被再次转义。将url逆转义应该是应用程序的责任。这个bug会让应用程序在开发期间产生错觉,因为从开发服务器得到的url都是被转义过的;而在生产模式使用fcgi/mod_python部署之后应用程序得到的url就变成了没有被转义的原始url,措手不及。

2010年2月3日星期三

说说 web2py

很多人使用python进行web开发的第一个框架可能是django或者web.py,但我接触的第一个框架是web2py。在web2py上进行开发也有一年的时间了,我们还是决定切换回django。web2py有很多让我们不爽的地方,在这里进行一下不完全总结:

1、死板的url
一般情况下,web2py的url是有固定模式的,即/app_name/controller_name/function_name/arg/arg?key=val&key=val。将app、控制器和函数的名称和url直接联系起来,这是web2py的特色,但这样做最明显的问题就是url会变得很长,即使是如首页这样明显应该使用最短url的地方也是如此。web2py提供了一个routes.py来解决这个问题。在routes.py中,开发者可以像许多其他框架一样使用正则来重定向url。但这不是开倒车么?早知如此,何必当初呢。

routes.py还会带来另一个问题,因为routes.py所在的位置是web2py的目录,而非每个app的目录,也就是说routes.py是独立于app的。在进行版本控制的时候,如果开发者只对自己app进行版本控制,这个routes.py就会游离于版本控制之外了。最好的办法是将整个web2py都纳入版本控制,因为在开发的过程中你会知道,你需要自己为web2py写些补丁才能正常的工作下去。

2、url只支持某些字符
在有web2py特色的url中,如果你使用了中文的参数(args),web2py会给你两个字:Invalid Request。为啥?因为web2py对args做了严格的限制,只允许大小写字母以及几个有限的符号。对于其他符号,由于正则表达式不匹配,web2py会找不到控制层函数,于是就丢给你这么一个出错信息。url中的中文一般都是经过了转义的,即使你不去转义,浏览器也会帮你转义。比如“你好”,到了url之中就会变成“%E4%BD%A0%E5%A5%BD”,于是web2py就犯傻了。

我们修改了gluon/main.py中相关的正则表达式解决了这个问题。

3、过于独立的app
web2py的app是非常独立的,有自己的静态文件和session,不同的app之间没有预设的共享机制。这样的app划分方法可能只适合于承载多个互相独立的小站点,而对于一个含有多个相对独立的复杂模块,而模块之间又需要互相通信的项目来说,web2py的这种方式就完全不合适了。在这个问题上,web2py和django形成了鲜明的对比。

4、巨慢的启动速度
启动web2py要多久?大概5-10秒之间吧。web2py在启动的时候会import大量的库以及完成检查定时任务等工作,重启web2py是很耗时的。但是python的web开发有这么一个毛病,那就是有时开发者在修改代码之后不得不重启服务器才能看得到改动的效果,这在我所见到的所有框架中都或多或少的存在。重启django的开发服务器需要多久?web.py的呢?再回头看web2py,真是慢得跟蜗牛一样了。

5、出错页面极其简陋
在开发过程中,出错是少不了的。web2py的出错页面就是一行小字:ticket xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,然后你就跟着这个叉叉页面进行认证之后才能看得到出错内容。其实使调试页面可以通过认证访问是一个很不错的主意,我只是很不明白为什么要在开发模式下也这么干,更不用说web2py根本没有区分生产模式和开发模式的方法了。web2py的调试页面本身也相当简陋,就是把traceback打印出来,再把出错的源文件整个dump出来。而其他框架是怎样的呢?当django出错的时候,web.py出错的时候,trac出错的时候,moinmoin出错的时候,出现的页面是什么?那上面不但有traceback,还有traceback每行代码的上下文,有出错现场的所有局部和全局变量的值,还有当时的http请求和回答的内容等等。这种调试页面几乎已经成为了标准,我不明白为什么web2py视而不见。

好在将标准的调试页面迁移到web2py并不是一件很困难的事情。我们自己写了一个补丁,将这个页面从web.py中迁移了过来。

6、和python的标准模块有过节
在开发过程中我们发现我们的日志出了点小问题,日志条目包含大量的重复,而且日志似乎不是实时的,logging模块的日志切分也不正常。一开始我们以为是自己的代码问题,查来查去也没查出个所以然,于是就到web2py的邮件列表里搜索,发现有人报告了同样的问题(https://groups.google.com/group/web2py/browse_thread/thread/e20d0bd2e542aa14/a660bac70d5611a4)。比较奇葩的是创始人mdipierro竟然也不知道这个bug的根源在哪里,而且也没有任何解决的动向。帖子的楼主找到一个绕过问题的方法后大家都欢呼雀跃,然后就没人理这个问题了……

顺便说一句,web2py项目从07就开放了,09年才有人暴出logging模块的问题,难道用web2py的人都不写日志的么?

web2py还会和哪些标准模块过不去?谁知道……

7、magic(过度设计)
把事情变得傻瓜化,在用户不知情的情况下就帮他们做这做那,这是windows的哲学,我很不喜欢。很可惜,web2py就是这样的一个框架。web框架的确是应该帮开发者干一些脏活累活,但是也要弄清楚界限,否则吃力不讨好。我们在web2py中就两次遇到了这样的情况。

第一次是在构造一个文件下载的服务时,发现ie6/7下载总会出错,但是代码上没有任何问题。在StackOverflow上询问过后发现是web2py很热心的帮你设置了很多header。讨论帖请见:http://stackoverflow.com/questions/1999950/download-link-fails-in-ie

第二次是在处理url时,发现args里面的空格不是被转义成%20,而是都“自动”变成了下划线。察看web2py的源代码,发现果然是web2py搞的鬼。web2py的文档提过这事儿么?一个字都没有。

8、测试?None
我真的很羡慕django的开发者,django为他们准备好了一个非常棒的测试环境,并且在文档中给出了很多例子,帮助开发者完成代码级别的测试。而web2py呢?None!没有文档,没有例子,甚至连web2py本身,也有很多没有被测试覆盖到的地方。每个app倒是有个test目录,但怎么用?有没有best practice?web2py最官方的manual上都没说这件事儿。

9、DAL就是个笑话
现在python最好的ORM非SQLAlchemy末数了。看得出来,DAL是以SQLAlchemy为目标的,但是很可惜,画虎不成反类猫。DAL生成的数据表的migrate选项是一个很好的点子,但可惜做的不是很成功。它重度依赖自动生成的.table文件,在切换数据库时经常造成DAL无法识别明明已经存在的数据表。要说bug,DAL最严重的问题大概是会把布尔类型的字段创建为char(1),然后用'T'和'F'来表示“真”和“假”。匪夷所思是么?的确,我一开始也被雷到了,但是雷着雷着也就习惯了。

10、MVC三层互通!
这是我们最为痛恨的一点了,web2py的MVC三层竟然是通的!什么意思?这就是说,你的代码在运行的时候,不需要import,model/control/view三层的所有变量以及web2py自身的函数和变量以及request/response/session这些全局变量都会被加载到一个命名空间之中去。你自己的代码干了什么,你还是可以控制的,可以让他们不要互相重名覆盖,但是你能保证你的变量名和函数名不会和web2py的出现冲突么?import机制是白发明的么?现在全世界的开发者都知道命名空间是多么的重要,竟然还有这种脑残的设计,实在让人无语。

光凭这一点,web2py就可以去下地狱了。



当然,web2py也不是一无是处,它还是有两个优点的:

1、独立部署
web2py的发布包解压缩之后就能运行,立即就可以进行开发而无需安装,这非常方便。web2py鼓励开发者将所使用的第三方库放在modules目录中,这样在部署的时候将web2py整体打包就可以了,无需依赖其他外部环境。当然,对于其他框架,使用yolk+virtualenv也可以做到这一点。

2、自带定时任务
web2py自带了对定时任务的支持,这些定时任务代码可以访问app的model层代码,也就是可以使用model层的代码操作数据库。这样定时任务就不需要再依赖操作系统,而且也可以使用开发环境中的ORM,很方便。



Anyway,web2py的作者是个大学老师,这个框架就是拿来上课用的。如果有谁还真以为这个框架能作出什么产品级的应用,那真是天真的可以。