Python FAQ:Web开发(译)

原文: Python FAQ: Webdev

译者: youngsterxyf

Python FAQ的一部分

我只会PHP,那该怎么用Python来编写一个Web应用呢?

这是一个相当复杂的问题,甚至很容易就能写一本书来探讨Web开发与Python,以及如何关联两者,所以我很想先把这个问题放一放。但是鉴于我刚相当粗暴地捣毁了PHP,明智些,还是回答这个问题吧,宜早不宜迟。

最直接简单的回答是:不要再读了,马上使用Flask着手构建一样东西。然而,我觉得还有更好回答。

本文并非是教程。也许将来我会写一篇,但现在已经存在大量的教程了,我认为你可以阅读那些文档。相反,本文是为新手而写的Python Web开发相关事情的概览。

起步

显然,你需要安装Python。确保使用Python 2,而不是3。Python 3有一些向后不兼容的改变,并非所有的库都更新过。

安装Python库,可以考虑使用 pip 。(如果你在使用类Unix操作系统,那么也许可以通过系统包管理器安装pip,否则使用 easy_install pip ) pip 是一个小巧的Python包管理器,便于安装,删除,升级,以及检查Python库。当然,尽可能使用你的系统包管理器,不行的话就用 pip

可以使用 pip install --user ... 将Python库安装到你的home目录,但更好的方式是保持库为每个项目局部可用---这样,你就可以为一个项目升级依赖项而不会破坏其他项目的依赖。(或者破坏Python编写的系统软件。我就曾干过这种事。) virtualenv 使用单个命令就能助你创建一个独立的Python安装环境。

当然,你已计划使用源码控制,对吧?我喜欢git , 但有聊胜于无,其他的也OK的。

框架

第一个障碍是如何将你代码与浏览器相关连。PHP中,最简单的方式是安装Apache并将它指向一些文件。Python中,如同更大的PHP项目,一般需要使用Web框架。

框架多半有相似的工作流程:

  • 安装,使用 pip 这样的工具。

  • 创建一个项目骨干(skeleton)。

项目骨干的复杂性视情况而定。对于现已不被使用的Pylons,你会得到一大堆诡异的代码,还需要为新的发布版本手动升级。Flask则简单到没有骨干。复杂性适中的是Pyramid,项目骨干不过是些通用的样板文件(boilerplate),如果你是从零开始,那么最终会自己来写的这种样板文件。

  • 配置一些东西,比如数据库。

  • 启动开发服务器。

一般是一个运行你的应用的终端程序,从而不需要一个专用的HTTP服务器。当你修改了代码,开发服务器就会自动重新装载,并且能输出栈跟踪和其他调试信息。

  • 动手干吧!

那么,应该用什么框架呢?虽然有不计其数的可选项,但有一些明显最流行。

我是Pyramid的粉丝,它在极简主义与电池内置的庞然大物之间做出最佳的平衡。虽然它是从两个更老的组织良好的项目衍生而来,但在最近才成为一个竞争者。Pyramid设计良好,文档齐全,相当透明(fairly transparent)。一个简单的应用根本不需要自动生成的样板文件,允许你直接运行项目骨干,并且核心代码库扩展性非常好。有用的插件越来越多。

如果需要更快地上手, Flask则足够简单了,但是可扩展性非常良好。其设计上与众不同地做了一些相当的合理的事情,且对你没有很多强迫。

Bottle类似于Flask,但是更简单:它作为单个文件发布,没有任何依赖。是好是坏全看你自己,但这意味着Bottle中没有什么东西可以和其他框架分享的。我承认对Bottle知道的不是很多,但我曾简单地了解过(gave it a brief shot once),对它没什么大的牢骚。

另一极端上,Django是为类内容管理系统和其他富内容网站而设计的巨大怪兽。它有庞大的可插部件生态系统,内置从模板到ORM的各种东西,以及大量文档和社区资源。Django常被认为是与Ruby on Rails等价的Python框架。其缺点是让它做些它不想做的事情会很别扭。( #python 中许多更加愚钝的问题都是由于试图捣鼓Django而产生的)对于首次尝试Web开发来说,Django可能有些重量了。

还有web2py。我,额,不太了解它。据说它会在你的模块名字空间内注入变量,这是令人讨厌的,所以如果你在意那些我认为讨厌的东西就不要用它,否则就用吧,随你自己啦。

曾经有一个Apache模块 mod_python ,本质上类似于 mod_perl ,但很早就被抛弃了。请 不要 使用它。

最后,你也可以完全"手动"编写Python web代码,但那多半是一次令人沮丧的练习。不会更快,不会有什么教育意义,也不会有什么用。不要自找麻烦。

我的建议?如果你只是想折腾捣鼓,那就从Flask入手吧,随你添加东西。如果你有想法要做一个网站,并且想旗开得胜(hit the ground running),那就使用一个Pyramid脚手架(scaffold),跟随它的叙述性文档进行开发。

路由选择(Routing)

PHP是根据URL执行一整个文件,Python web应用则倾向"拥有"一整个目录结构(或者甚至是整个域(domain))。因此,将特定的代码连接到特定的代码更加灵活,这种连接通常是由一个路由选择系统来处理。

路由(routes)是包含可选占位符的URL,就像这样:

/users/{name}
/companies/{id}/products
/blog/{year:\d\d\d\d}/{month:\d\d}/{day:\d\d}/{title}

你可以把这样的一个路由绑定到一个函数,那么当你浏览到 /users/eevee ,那个函数就会被执行,占位符则以 dict(name=u'eevee') 这样的结构可供使用。

一些框架(比如Pyramid)在这一步上走得更远:不是直接将路由绑定到函数,而是给路由一个名字,然后把名字绑定到函数。需要一点额外的工作,但优点是在你的应用中只需维护一个所有页面的核心列表。你也可以一个路由名和占位符值来生成一个URL---那么,之后,你就可以只在一个地方修改一下就能改变一个URL,而不需要接触其他东西,并且开发过程中打字错误会产生一个错误信息而不是一个404页面。

语法和具体实现会有些不同,但每个框架都是使用这个系统的某个变种。有些有助于创建REST风格的路由或者其他常见模式,或者你可以很容易编写自己的模式。

请求周期

一次HTTP请求往往会执行某处的一个函数(由一个路由选择),然后给函数传递一个 request 对象参数。

request对象的确切接口依赖于特定的框架,但是它们一般都比较类似:解析过的查询数据,一些cookie,请求消息头,等等。举例来说, webob (http://www.webob.org) 的 Request 对象包含:

  • request.GETrequest.POST 是存储解析过的查询数据的"multidict"。(对于 request.GET['foo'] ,一个multidict返回的是单个值,但使用 getall() 方法则会返回所有的值)

  • request.params 是包含上面两者的一个multidict。

  • request.cookies 是一个cookie的解析字典。

  • request.headers 是一个HTTP请求消息头的字典,但是其键是大小写敏感的。

  • request.is_xhr 返回是否存在 X-Requested-with: XMLHttpRequest 消息头,以识别由jQuery这样的库设置的ajax请求。

request对象的文档一般非常齐全,因此只要浏览一下你所选择的框架的文档,挑出其中重要的内容。

当你的应用完成了一些非常酷的事情之后,你要让它返回一个响应消息。通常你能选择是否明确地构建一个 Response 对象(包括HTTP消息头和其他可手动设置的细节)还是简单返回一块HTML代码,其他所有东西都使用默认设置。很少需要你自己创建一个响应。对于像返回JSON这样的常见工作,每个框架都有某种快捷的或辅助的装饰器。

模板

组装HTML的工作一般是由模板引擎来完成的。MakoJiaja2是两个主要的竞争者。

我真的喜欢Mako。真的,真的,真的。使用它吧。它使用朴素的Python作为语法,使用起来非常自然。你甚至可以在模板里编写纯粹的Python代码块,但是你得控制住,尽可能避免这样做。:)

Jinja2也不错,但提醒你一下:Jinja2中,如果 foo 看起来像一个字典,那么 foo.bar 就会被当作 foo['bar'] 处理,反之亦然。恰巧我认为这并不是一个好主意,我曾遭遇过许多诡异的问题,都是模板系统中这种"特性"所造成的。(另外, {% %} 这种语法真的很烦人,但这有些鸡蛋里挑骨头了)。除此之外,Jinja2是一个非常可靠的库,而你肯定会做得更糟,糟得多

这两个工具速度都很快,会自动编译成Python模块,具备优秀的可调试能力(以疯狂的做法从原来的模板源码中得到栈跟踪信息),应该足够强大,让你想干啥就能干啥了。大致了解一下这两个,然后选一个就开始使用吧。如果你不知道或者无所谓用哪个,那就用Mako吧。

(注意,虽然Flask默认使用Jinja2,但使用Mako作为替代也是相当容易的。)

当然还有其他的竞争者:排名第三的可能是Genshi,但它令人极其费解,以至于主页上一开始就用了一张流程图; Djano有自己的模板引擎,千方百计想把逻辑剔除出模板(在我看来,对其是不利的);Bottle同样有自己的极其简单的模板,但是可能很快就会让你感到越来越痛苦;Pyramid的另一个内建模板引擎是Chameleon,将类似HTML的属性标签用于循环和其他逻辑,太TMD古怪了。

也许你会喜欢其中之一;我并没有都深入使用过它们。

不管你做什么,都不要使用Cheetah。 不要 使用Cheetah。它邪恶可憎。不要再提到它。

模板中的逻辑

也许你以前没用过模板,那你不可避免地会遭遇这个问题---一些复杂的表现代码是应该用Python实现,还是放在模板中实现。

这是无聊的老生常谈,但我想说:就像许多程序设计中的架构决策,归结起来就是要尽可能减少以后因为它而对自己的厌恶感。尽可能保持模板简单,如果不行的话,也不用勉强。谨记你始终可以在简单的Python模块中编写简单的Python函数,然后导入它。一个强大的模板语言对于你的问题可能内建了创造性的解决方案,所以当你在想办法的时候可以先浏览一下文档。

Unicode

Unicode很烂。这是众所周知的事实。(我在说谎。编码(encoding)处理得很烂。Unicode很伟大。这个问题比较复杂,之后我会写到。)

Python(2)有两种"字符串"类型: strunicode 。这是一个巧妙的谎言。事实是:一个 str 并不真的是一个字符串。它只是一串字节。有时恰巧看起来像一个字符串,但事实上只是一个二进制表示,就像 85 00 00 00 是数字133的常见二进制表示。一个真正的数字是 int 类型的,一个真正的字符串是 unicode 类型的。

这个问题很复杂,值得单独写一篇文章来解释(迟早我会写的),但现在可以有些快速的笔记:

  • 你的程序只需要担心真正的字符串(也就是 unicode 类型的)。字符串进入你的程序时需要解码,离开时需要编码,但是幸运的是,大多数的web框架都会为你做这事。

  • 你可以使用 u 前缀来创建一个 unicode 的字面字符串,e.g., u'foo'

  • 你可以在文件的顶部添加 from __future__ import unicode_literals 使得文件中的所有字面字符串默认为 unicode 。如果你确实需要一个 str 类型字符串,那就使用 b 前缀吧。

  • 如果你想在Python源码中使用非ASCII字符,在顶部添加 #encoding: utf8 魔术注释。(当然是假定你的源码保存为UTF-8编码的,这样做绝对更好。)

  • 永远 不要通过剥离非ASCII字符来解决Unicode问题!这是对很多人的无礼;想象一下当你尝试去使用一个网站,因为某个程序员懒得弄清楚如何处理英文字母,所以不允许你使用英文字母,你会是什么感受?

  • 实际上,对于撼动编码问题,重音字母和亚洲字符功不可没。将一些非ASCII字符的莫名其妙的话粘贴到你的网站表单中,看看会发生什么。

XSS(跨站脚本攻击)

实际上,现在的每项相关技术都内建某种形式的自动HTML转义过滤器。理念是:对于这样的一个模板:

<p>Hello, ${name}!</p>

当给定 name = '<b>' ,将安全地打印出 Hello, &lt;b&gt;! 。这意味着,大多数时候,你并不需要担心XSS。

大多数时候,如果没有别的,你必须核对所使用框架和模板引擎的文档,确保自动HTML转义过滤功能默认开启,如果不是,则开启它。(随便说一下:对于Pyramid,Django和Flask,你能轻松获得此项功能。如果你的模板文件具备一个处理HTML的扩展,Bottle则也能自动做到。)

那么,棘手的地方就是知道何时以及如何关闭它。如果你在Python代码中构建了某种复杂的HTML,且不想完全转义它,那么仅仅使得转义行为失效是个蹩脚的解决方案。任何转义失效的地方都可能发生注入。幸运的是,许多框架(至少有Pyramid和Flask)使用了markupsafe库,它能智能地帮助避免这个问题。

markupsafe提供一个单一的类, Markup ,继承自 unicode . Markup(u'Hello!') ,会产生一个行为上相当像字符串的对象。类方法 Markup.escape 工作方式相同,但会转义经过包裹的字符串中的任意HTML字符。

这里有两个鬼祟的花招。第一:一个 Markup 对象不会被转义两次。请看:

>>> s = u'<b>oh noo xss</b>'
>>> Markup.escape(s)
Markup(u'&lt;b&gt;oh noo xss&lt;/b&gt;')
>> Markup.escape(Markup.escape(s))
Markup(u'&lt;b&gt;oh noo xss&lt;/b&gt;')

因此,一旦创建了一个 Markup 对象,就可将它用于你的模板,过滤器不会管它---即使它包含HTML。

另一个把戏是, Markup 对象重载了所有string的方法,并且自动转义所有的参数。这意味着在Python里,你可以这么干:

>>> user_input = u'<script>alert("pwn");</script>'
>>> Markup(u'<p>Hello, %s!</p>') % user_input
Markup(u'<p>Hello, &lt;script&gt;alert(&#34;pwn&#34;);&lt;/script&gt;!</p>')

因此你可以相当安全地构建一些复杂的HTML代码,而不用太担心转义不够或者过分转义。

当然,这并不完美。主要问题是你需要将 Markup().join(...) 用于一些其他的 Markup 对象,而不是 ''.join(...) 。并且某些操作,比如分片(slicing),分割(splitting),以及正则表达式,有可能产生没有意义的结果。 绝对 不要试图分解一个 Markup 对象或者任何其他HTML字符串;如果实在需要的话,那就使用一个真正的解析器,比如 lxml ,但是大多数时候,你可以在将普通字符串包裹进HTML之前,对它做任何你需要的转换。

表单

我厌恶所有处理表单的库。每个单一的库。它们都把作者的疯狂命名方式强加到我的表单。我甚至不喜欢PHP使用 foo[] 作为字段名称的行为;这有多丑陋啊。

至今让我讨厌程度最低的是wtform;它强加的设计上的限制相当少,并且使用起来非常简单。它甚至内建支持配合markupsafe。主要的缺陷是要想去除那些设计上的缺陷比较困难(每个表单元素都有一个与名字相匹配的 id 属性),并且实现一种新的字段会有点复杂。

对于其他的我没法多说些什么,唉。FormEncode是一个东西。Pyramid的维护者还拥有deform 。它们都做了一些愚蠢的事情,也许确实是因为太挑剔了,我才这样烦扰。货比三家吧。

无论你做啥,都要确保你使用东西不会使得你的项目变得太大。比表单处理库更让我厌恶的一件事是手动编写验证码。

"净化(Santizing)"

关于PHP界共同趋势的笔记。

不要 "净化(sanitize)"。

这个词语本身没有什么意义。不存在某种你可以用于任意字符串并使之"安全"的方法。这种想法就是我一直遇到银行网站的联系表单告诉我不能使用 < 字符的原因;某些傻瓜企业开发者并不知道如何处理数据,所以他强迫所有数据必须简单易懂。

不要成了一个白痴。

大多数时候,"净化"是指使得用户输入安全地嵌入HTML,传递给SQL,或者作为命令行参数使用。你根本不需要改变原有数据就能做到所有这些事情。对于HTML,上面提到有一些过滤器,比如markupsafe。对于SQL,有界限参数和ORM。对于执行命令,你应该完全避免使用shell,仅仅将参数作为列表传递(参见 subprocess (http://docs.python.org/library/subprocess.html))。

这些就是语言障碍(language barrier)的所有问题:HTML,SQL,以及shell都是结构化语言,你不能把一些莫名的垃圾数据倒给它们,还希望最好的结果。你不会使用字符串连结来创建JSON,所以也别用来执行 convert (译注:这里的convert应该是用于图片格式转换的convert命令)。使用理解底层结构的工具。

这并不是说你应该从不修改或过滤用户输入,而是你应该尽可能地避免它,并且当你做的时候应该极其小心。以常见的密码为例,为什么一般都禁止在密码中使用空格或者要将密码限制为16个字符?并没有明确的理由;仅仅是做了的一件事情。

我仍然被这个问题所困扰:不让我输入 < 的那个地方还坚持要我输入16个数字的字符串作为我的信用卡号。这就很难一目了然地证实我的输入是否正确---此外,在我的信用卡上的号码中间是有空格的呀!为什么不剔除空格和连字符?

仔细思考你正在做的事情以及你正试图解决什么问题。人们会使用从右到左的Uniode字符对你的站点做一些愚蠢的事情么?你想阻止他们?没有理由要强迫每个人都使用ASCII;Unicode有类别之分,你可以仅过滤怪异类别的字符。但更好的做法是,修改你的网站,使得说希伯来语的人都能使用它。:)

调试

如果你有幸(例如,使用Pyramid),那么当你的程序崩溃时,你会有一个交互式的调试器,允许你检查程序的实时状态。你可以运行任意的Python代码,观察变量的状态,审核堆栈,以及耍着玩。

如果你不幸,也不用担心;你还可以使用werkzeug调试器。它相当易于使用;它能包裹任何WSGI应用,然后捕捉异常。(看到没有?WSGI妙得很。)

只是当你部署应用的时候,要确保关闭调试,否则就会为别人所用;"任意Python代码"意味着任何看到调试屏幕的人都可以做你能在你的电脑做的任何事情。

数据库

一罐子的蠕虫啊。这话有些武断(This is as opinionated as I'm going to get.)。

其一:你应该使用ORM。它是尝试将数据表映射到Python类,数据行映射到对象,查询映射到方法的物件。结果更加简单明了,通常更加容易理解,并且有时甚至正确率更高。

你应该使用的ORM是SQLAlchemy。Pyramid对它有内建支持;如果你使用的框架没有内置支持,SQLAlchemy这么受欢迎,框架文档肯定有说明如何与它相连。如果你在使用Django,它有自己的ORM,虽然没有SQLAlchemy这么好,但要想把Django自带的ORM替换掉非常麻烦,不值得,除非你有迫切的需求。

许多贬低ORM的人会告诉你ORM会产生糟糕的SQL。是的,糟糕的ORM确实会,但优秀的ORM,比如SQLAlchemy,对于SQL,和你理解得一样好。如果你懂SQL,SQLAlchemy会非常适合你;如果你不懂SQL,SQLAlchemy至少能帮你避免很多编写糟糕SQL的尴尬。记住你总是可以察看执行过的查询;SQLAlchemy可以把它们全部记录下来,并且各种调试工具栏会显示查询执行时间列表。(另外,留心那些执行多次的相同查询,这标示需要预先加载。)

接下来,使用事务(transaction)。希望你不要对这个有太多的顾虑;如果一个框架集成了SQLAlchemy,它很可能会为你做这事。理念是:当开始一个请求,则开始一个事务,如果发生异常,事务会自动回滚。这是你一开始就想要的行为。这是使用数据库的一半(不对,是1/4)原因。

还有一件事:既然本文中我所说的都是关于尝试新事物,那么 不要使用MySQL 。在我能想到的任何意义上,MySQL都是数据库中的PHP(the PHP of databases)。考虑一下PostgreSQL,它搭建起来并不会更难,使用起来更友好,也不会让你做在日期栏中存储字符串那样的蠢事。(在我看来,最友好的事情之一是PostgreSQL可以使用你的Unix用户帐号登录,不需要密码)。唯一的有人曾经反对使用Postgres的理由是它"不能扩展"。请放心,我还没看到这种情况的实际例子,总之,当访问者超过百万的时候,你才需要担心这个问题。

会话(Sessions)

每个框架都支持会话。看起来我们也熟悉:一个会话标识存储于cookie中,并且在后端你能魔法般地得到一个字典,可以往其中存储任意数据。随你怎么用。但尽量不要把它当作垃圾场。这事实证明数据库非常适合存储数据,你明白的。

额外的特性包括:对CSRF(译注:Cross Site Request Forgery,跨站请求伪造)保护的一等(first-class)支持,以及信息提示(Pyramid,Flask,Django)。去阅读文档吧。

提醒一句:如果你使用Beaker会话(Pyramid采用这个独立的库),繁琐的东西会逐渐积累。默认情况下是为每个会话在磁盘上创建一个文件,但是如果使用数据库支持会话,你将得到一个存储了大量会话的数据表,并且这个数据表只增不减。这是一个非常糟糕却不明显的问题,主要的修复方法是手工操作。深表遗憾。

部署

啊哈,你明白我的意思的,部署有很多种方式,应该花更多的时间来讲述,但这里我没法花那么多的时间。

如果可能,还是宁愿花些钱吧。提供服务肯定是要有代价的。如果你有自己的专用(虚拟的或者不是)机器随你摆弄,那么这是部署应用最简单的方式---手头有台服务器是件很酷的事情。你可以每月花费$20而获得一个基本的Linode,还存在更便宜的托管服务提供商(但是没那么酷了)。

Heroku也是值得选择的,其收费等级中有一个免费级,只提供一个工作进程(worker,译注:Heroku官网解释"A worker dyno is a single background process running your code and processing jobs from a queue.")(类似于最低级Linode),但是每个额外的工作进程都需要你另外支付$36/每月。(可以同时处理的请求数目正比于你持有的工作进程数目。具体需要多少工作进程则要看你的应用以及你如何运行它。)优点是你的应用会被部署得很专业。Heroku现在还支持多个应用副本。

正如他们所说,部署是个很好的问题:因为这意味着你确实构建了有用的东西。那么在我努力写一篇关于部署选项的文章之时,你赶紧去构建某个应用吧。

总结

Web是复杂的,涉及很多活动件(moving parts)。聪明的人们已经为你解决了很多问题。去捣鼓吧。

希望本文足够让你开始Web开发了!

一如既往,我并不知道自己做得怎么样,所以请你告诉怎样做得更好。

Comments