status

草稿

HuangYi; 40%

TableOfContents

那么小白决定进一步探索 python web开发更高级一点的技术了,行者建议他从基础入手。

当用户在浏览器中输入网址,浏览器便找到web服务器,向它发起http请求, web服务器再找到web应用程序执行之,并把结果返回给客户端浏览器。

那么做web开发之前首先要有web服务器才行,开发阶段最好使用一个简单方便的开发服务器。 等开发调试完毕,再将代码部署到 apache、lighttpd 等成熟高效的web服务器, 放心,使用python做web开发,最后部署的阶段是很轻松的,在本章的后面便会提到。

开发服务器

那么 python2.5 就自带了一个叫做 wsgiref 模块,它提供一些专业 web 开发所需要的一些基础工具,比如一

个开发服务器:

   1 # -*- coding: utf-8 -*-
   2 from wsgiref import simple_server
   3 
   4 # 定义一个输出 hello world 和环境变量的简单web应用程序
   5 def hello_app(environ, start_response):
   6     # 输出 http 头
   7     start_response('200 OK', [('Content-type','text/plain')])
   8     # 准备输出的内容
   9     content = []
  10     content.append('Hello world')
  11     for key, value in environ.items():
  12         content.append('%s : %s' % (key, value))
  13     # 输出,根据 wsgi 协议,返回的需要是一个迭代器,返回一个 list 就可以
  14     return ['\n'.join(content)]
  15 
  16 # 构造开发服务器对象,设置绑定的地址和端口,并把 hello world 应用程序传给他
  17 server = simple_server.make_server('localhost', 8080, hello_app)
  18 # 启动开发服务器
  19 server.serve_forever()

把以上代码以utf-8编码保存成python程序,不妨命名为 main.py。然后执行上面这个程序后,打开浏览器,随

便访问一个以 http://localhost:8080 开头的网址即可看到 environ 所包含的内容。

wsgiref 带的这个开发服务器调用应用程序所使用的协议叫做 wsgi,wsgiref就是对wsgi协议的 一个参考实现,wsgi 协议内容可以参考:URL ,在这里你只需要知道,hello_app 的第一个参数environ 是包含web环境变量的字典,熟悉 cgi 的朋友可以看做是 cgi 里的环境变量, 里面包含了整个web请求有关的信息(从上面这个程序输出的内容就可以看得出来),比如请求的路径

(PATH_INFO), ,是get请求还是post请求(REQUEST_METHOD),cookie(HTTP_COOKIE),客户端的IP(REMOTE_ADDR)等等信

息。

第二个参数 start_response 是一个函数,在返回具体内容之前需要先调用它来输出 http 状态码和响应头。

web应用程序是要和用户进行交互的,意味着我们需要能够得到用户的输入,然后作出相应地 动作。 通常用户通过浏览器访问网站有两种方式,一种是 get 请求,就是直接访问一个URL,另一种是 post 请求,比如用户提交了一个表单。web应用程序可以通过 environ['REQUEST_METHOD'] 来判断是哪种类型

的请求。 get 请求的时候用户可以往URL的后面添加参数来向web应用程序传递数据, web应用程序可以通过 environ['QUERY_STRING'] 来获取这些参数,不过由于URL的长度往往是有限制的, 所以不建议使用get方法来传递的比较大的数据; post 请求的情况下用户通过填写表单来向服务器传递数据,post 提交的数据是没有大小限制的, web应用程序可以通过 environ['wsgi.input'].read() 来从 socket 中逐步读取这些数据。

显然,直接通过 environ 来处理用户输入还是有点麻烦的,小白很快就发现 python世界有一些专门的模块是可以帮忙简化这个事情的。

处理web请求和响应

小白发现有几个模块可以用来处理 web 请求和响应,webob 就是其中之一。

安装 webob 之前我们先安装 setuptools ,一个包管理的工具,到时候可以自动为我们搜索并下载想要的 软件包。 在这个页面 http://pypi.python.org/pypi/setuptools 下面,非windows平台可以下载 .egg 文件 ,然后修改权限直接执行之,或者下载源码包,解压后执行 python setup.py install , windows平台的用户也可以直接下载 exe 后缀名的可执行安装程序,下载后直接执行就好。

安装完 setuptools 后,安装 webob 就很简单了,打开一个命令行窗口,执行 easy_install webob , setuptools 能够自动找到最新的版本并自动下载安装。当然,如果嫌它单线程的下载速度有点慢,你也 可以直接到 http://pypi.python.org/pypi 搜到相应的包,手动下载, 然后加压并执行 python setup.py install ,后面的包的安装方法和这里一样。

安装完成后我们可以直接在 python shell 里对 webob 所提供的功能进行试验:

   1 >>> # 导入 Request 对象
   2 >>> from webob import Request
   3 >>> environ = {}
   4 >>> # 使用 Request 来包装 environ 字典
   5 >>> req = Request(environ)

基本使用方法就是这样了,使用一个 Request 类来包装 environ ,然后通过 Request 对象的属性和方法, 对 environ 进行访问。由于只有在 web 环境下才能有一个完整的 environ 字典,为了方便在 python shell 里面进行试验,webob 还提供一种方法可以模拟一个默认的web环境,请看:

   1 >>> from webob import Request
   2 >>> req = Request.blank('http://localhost:8080/hello?name=python')
   3 >>> from pprint import pprint
   4 >>> # 自动构建的简易 web 环境
   5 >>> pprint(req.environ)
   6 {'HTTP_HOST': 'localhost:8080',
   7  'PATH_INFO': '/hello',
   8  'QUERY_STRING': 'name=python',
   9  'REQUEST_METHOD': 'GET',
  10  'SCRIPT_NAME': '',
  11  'SERVER_NAME': 'localhost',
  12  'SERVER_PORT': '8080',
  13  'SERVER_PROTOCOL': 'HTTP/1.0',
  14  'webob._parsed_query_vars': (MultiDict([('name', 'python')]), 'name=python'),
  15  'wsgi.errors': <open file '<stderr>', mode 'w' at 0x00B6F0B0>,
  16  'wsgi.input': <cStringIO.StringI object at 0x00B6A2F0>,
  17  'wsgi.multiprocess': False,
  18  'wsgi.multithread': False,
  19  'wsgi.run_once': False,
  20  'wsgi.url_scheme': 'http',
  21  'wsgi.version': (1, 0),
  22  'wsgiorg.routing_args': ((), {})}
  23 >>> # 通过 Request 的属性可以更方便地进行访问
  24 >>> req.method
  25 'GET'
  26 >>> req.host
  27 'localhost:8080'
  28 >>> req.path
  29 '/hello'
  30 >>> req.query_string
  31 'name=python'
  32 >>> # 可以通过字典的方式直接访问 get 参数
  33 >>> req.GET['name']
  34 'python'
  35 >>> # 和 post 数据,显然,这不是一个 post 请求
  36 >>> req.POST
  37 <NoVars: Not a form request>
  38 >>> # 不过我们仍然可以模拟一个 post 请求
  39 >>> req.method = 'POST'
  40 >>> # 手动设置模拟 post 的数据
  41 >>> req.body = 'name=python'
  42 >>> # 这下就可以了
  43 >>> req.POST['name']
  44 'python'
  45 >>> # 还可以很方便获取 cookie 数据
  46 >>> # 先模拟一段客户端请求带来的cookie
  47 >>> req.headers['Cookie'] = 'name=python'
  48 >>> # 同样,通过字典的方式对 cookie 进行访问
  49 >>> req.cookies
  50 {'name': 'python'}

同样,web应用程序的响应数据,比如状态码,响应头,设置 cookie 等, webob 都通过 Response 对象进行了包装:

   1 >>> from webob import Response
   2 >>> # 构造一个 Response对象
   3 >>> res = Response(status=200, body='hello world')
   4 >>> # 设置响应头
   5 >>> res.headers['content-type'] = 'text/plain'
   6 >>> # 设置 cookie
   7 >>> res.set_cookie('name', 'python', max_age=360, path='/',
   8 ...                domain='example.org', secure=True)
   9 >>> print res
  10 200 OK
  11 Content-Length: 11
  12 content-type: text/plain
  13 Set-Cookie: name=python; Domain=example.org; expires="Thu, 25-Sep-2008 14:28:07
  14 GMT"; Max-Age=360; Path=/; secure
  15 
  16 hello world

试完了基本功能,就试试把它加入我们的 web 应用程序吧,有个 webob, 那处理业务逻辑的代码就可以通过 Request、Response 对象来跟 web 服务器打交道, 这样我们可以把实现业务逻辑的代码分离开来:

   1 # -*- coding: utf-8 -*-
   2 from wsgiref import simple_server
   3 from webob import Request, Response
   4 
   5 # 专门实现业务的函数,现在简洁多了
   6 def hello_app(request):
   7     content = []
   8     # 获取 get 请求的参数
   9     content.append('Hello %s'%request.GET['name'])
  10     # 输出所有 environ 变量
  11     for key, value in request.environ.items():
  12         content.append('%s : %s' % (key, value))
  13 
  14     response = Response(body='\n'.join(content))
  15     response.headers['content-type'] = 'text/plain'
  16     return response
  17 
  18 # 对请求和响应进行包装
  19 def wrapper(environ, start_response):
  20     request = Request(environ)
  21     response = hello_app(request)
  22     # response 对象本身也实现了与 wsgi 服务器之间通讯的协议,
  23     # 所以可以帮我们处理与web服务器之间的交互。
  24     return response(environ, start_response)
  25 
  26 server = simple_server.make_server('localhost', 8080, wrapper)
  27 server.serve_forever()

这样,执行这个程序后,当我们访问地址 http://localhost:8080/hello?name=python 时, 就可以看到 hello python 和后面跟的一串 wsgi 的环境变量了。

模板

但是 web程序大部分时候都是要返回一个 html 页面的,而把一个复杂的 html 页面 代码直接写到python代码里面就太丑陋了,应该把它放到一个模板里去,把页面展现与 业务逻辑分离开来。

python里面的模板引擎主要有mako、genshi、jinja等。mako 主要特点在于模板里面 可以比较方便的嵌入python代码,而且执行效率一流;genshi 的特点在于基于 xml, 非常简单易懂的模板语法,对于热爱xhtml的朋友来说是很好的选择,同时也可以嵌入python 代码,实现一些复杂的展现逻辑;jinja 和 genshi 一样拥有很简单的模板语法,只是不 依赖于 xml 的格式,同样很适合设计人员直接进行模板的制作,同时也可以嵌入python 代码实现一些复杂的展现逻辑。

小白看 mako 挺顺眼的,于是就钻研了它一把。 可以到这里 http://www.makotemplates.org/download.html 找到最新版本的下载地址,下载解压, 然后执行 python setup.py install 就可以了。

下面的代码就是写的一个简单的 mako 模板文件,里面使用 python 的 for 循环来把 data 这个字典的 内容填充到一个html列表(mako模板的详细语法请参考:URL)。

<html>
  <head>
    <title>简单mako模板</title>
  </head>
  <body>
    <h5>Hello ${name}!</h5>
    <ul>
      % for key, value in data.items():
      <li>
        ${key} - ${value}
      <li>
      % endfor
    </ul>
  </body>
</html>

然后要做的就是给模板传递一个叫做 data 的字典对象了,可以编写如下三句代码实现:

   1 # -*- coding: utf-8 -*-
   2 # 导入模板对象
   3 from mako import Template
   4 # 使用模板文件名构造模板对象
   5 tmpl = Template('./simple.html')
   6 # 构造一个简单的字典填充模板,并print出来
   7 print tmpl.render(name='python', data = {'a':1, 'b':2})

执行以上程序,就可以看到通过模板输出的 html 代码了。 功能研究清楚了,小白开始试着把它整合到这个web应用中来, 这个时候小白发现,需要改动的部分主要集中在 hello_app 这个函数了, 因为核心的逻辑就是在这里进行的,小白决定进行一次小小的重构, 把 hello_app 给彻底独立出来,形成单独的模块 controller.py , 这样 main.py 就变成这样了:

   1 # -*- coding: utf-8 -*-
   2 from wsgiref import simple_server
   3 from webob import Request
   4 from controller import hello_app
   5 
   6 def wrapper(environ, start_response):
   7     request = Request(environ)
   8     response = hello_app(request)
   9     return response(environ, start_response)
  10 
  11 server = simple_server.make_server('localhost', 8080, wrapper)
  12 server.serve_forever()

而 controller.py 和模板功能整合以后成为:

   1 # -*- coding: utf-8 -*-
   2 from webob import Response
   3 from mako import Template
   4 
   5 def hello_app(request):
   6     tmpl = Template('./simple.html.html')
   7     content = tmpl.render(name=request.GET['name'], data=request.environ)
   8     return Response(body=content)

执行 python main.py ,并访问 http://localhost:8080/hello?name=python 你就可以看到通过模板输出的页

面了。

通过把 hello_app 分离开来, main.py 基本上就固定下来了,以后对业务逻辑的修改就只设计

controller.py 这个文件。

ORM

作为一个动态网站,有很多数据需要持久存储,关系数据库在这方面通常都是不二选择。 当小白向行者问道如何才能简化对数据库的访问时,行者郑重向小白推荐了 sqlalchemy 这个库。

sqlalchemy 是一个 ORM (对象-关系映射)库,提供python对象与关系数据库之间的映射, 可以通过 python 对象很方便地对关系数据库进行操纵,比如使用python代码描述数据库结构, 创建数据库,执行插入、查询、删除等数据库操作,非常方便。除了这些 ORM 的基本功能外, sqlalchemy 还支持Model的继承,自引用(Self-referential)、eager loading、lazy loading 等等, 还可以直接将 Model 映射到自定义的 SQL 语句。可以说既有不输于 Hibernate 的强大功能, 同时不失 Python 的优雅简洁。

在这里 http://www.sqlalchemy.org/download.html 找到最新版本的下载地址,下载解压, 然后执行 python setup.py install 进行安装。

代码胜千言,sqlalchemy 到底可以做什么,直接上代码展示下 sqlalchemy 的基本功能吧:

   1 # -*- coding: utf-8 -*-
   2 from sqlalchemy.ext.declarative import declarative_base
   3 
   4 # 创建数据库引擎,这里我们直接使用 python2.5 自带的数据库引擎:sqlite,
   5 # 直接在当前目录下建立名为 data.db 的数据库
   6 engine = create_engine('sqlite:///data.db')
   7 # sqlalchemy 中所有数据库操作都要由某个session来进行管理
   8 # 关于 session 的详细信息请参考:http://www.sqlalchemy.org/docs/05/session.html
   9 Session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
  10 Base = declarative_base()
  11 
  12 class Dictionay(Base):
  13     # python 对象对应关系数据库的表名
  14     __tablename__ = 't_dictionay'
  15     # 两个字段:
  16     key = Column('key', String(255), primary_key=True)
  17     value =  Column('value', String(255))
  18 
  19 # 创建数据库
  20 Base.metadata.create_all(engine)
  21 
  22 session = Session()
  23 for item in ['python','ruby','java']:
  24     dictionay = Dictionay(key=item, value=item.upper())
  25     session.add(dictionay)
  26 
  27 # 提交session,在这里才真正执行数据库的操作,添加三条记录到数据库
  28 session.commit()
  29 
  30 # 查询Dictionary类对应的数据
  31 for dictionay in session.query(Dictionary):
  32     print dictionay.name, dictionay.value

直接执行这个程序就可以了。 不过这是个单独的 demo,要把放到 web 应用里面,还需要先对它的结构重新组织一下。

先剥离出一个公用的 model.py 文件。

   1 # -*- coding: utf-8 -*-
   2 from sqlalchemy.ext.declarative import declarative_base
   3 
   4 engine = create_engine('sqlite:///data.db')
   5 Session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))
   6 Base = declarative_base()
   7 
   8 class Dictionay(Base):
   9     __tablename__ = 't_dictionay'
  10     key = Column('key', String(255), primary_key=True)
  11     value =  Column('value', String(255))

然后建立一个创建数据库并插入一些测试数据的单独的程序,创建数据库只需要一开始做一次就可以了。

   1 # -*- coding: utf-8 -*-
   2 # 导入公用的 model 模块
   3 from model import Base, Session, Dictionary
   4 
   5 # 创建数据库
   6 Base.metadata.create_all(engine)
   7 
   8 # 插入初始数据
   9 session = Session()
  10 for item in ['python','ruby','java']:
  11     dictionay = Dictionay(key=item, value=item.upper())
  12     session.add(dictionay)
  13 
  14 session.commit()

最后再将这个功能加入到我们的 web 程序里面来,修改 controller.py 文件:

   1 # -*- coding: utf-8 -*-
   2 from webob import Response
   3 from mako import Template
   4 # 导入公用的 model 模块
   5 from model import Session, Dictionary
   6 
   7 def hello_app(request):
   8     session = Session()
   9     # 查询到所有 Dictionary 对象
  10     dictionaries = session.Query(Dictionary)
  11     # 然后根据 Dictionary 对象的 name、value 属性把列表转换成一个字典
  12     data = dict([(dictionary.name, dictionary.value) for dictionary in dictionaries])
  13 
  14     tmpl = Template('./simple.html.html')
  15     content = tmpl.render(name=request.GET['name'], data=data)
  16     return Response(body=content)

执行 python main.py ,并访问 http://localhost:8080/hello?name=python,就可以看到从数据库中 取出来的三条测试数据了。

TODO 其他组件的介绍

现在让我们回头看一下,我们最终得到了一个什么样的程序。

TODO 成熟 web 框架介绍