第 9 章 剖析 Web
CERN 是一个很适合当作 007 老巢的粒子物理研究所,横跨了法国和瑞士的边界。幸运的是,CERN 的目标并不是统治世界,而是研究宇宙的本质。这项工作给 CERN 带来了海量的数据,对物理学家和计算机科学家来说极具挑战。
1989 年,英国科学家蒂姆 · 伯纳斯 - 李首次提出可以在 CERN 和研究机构之间传递信息。他称之为万维网,并将其架构提炼成三个非常简单的概念。
- HTTP(超文本传输协议)
规定了网络客户端和服务器 1 之间如何交换请求和响应。
- HTML(超文本标记语言)
结果的展示格式。
- URL(统一资源定位符)
唯一表示服务器和服务器上资源的方法。
1下文会出现三个名词:服务器、Web 服务器和服务端。服务器指的是物理意义上的服务器,也就是主机或者云服务器;Web 服务器指的是服务器上负责 Web 服务的软件,比如 Nginx、Apache,等等。服务端包含服务器端的所有服务,比如 Web 服务器、API,等等。——译者注
在最简单的场景中,一个 Web 客户端(我觉得伯纳斯 - 李应该是第一个使用术语浏览器的人)通过 HTTP 连接到一个 Web 服务器,请求一个 URL,收到 HTML。
他编写了第一个 Web 浏览器和第一个 Web 服务器程序,后者部署在一台 NeXT 计算机上。NeXT 计算机是由一家短命的公司研发出来的,这家公司是乔布斯在离开苹果公司期间创办的。Web 真正开始发展壮大是在 1993 年,这一年伊利诺伊大学的一群学生发布了 Mosaic Web 浏览器(支持 Windows、Macintosh 和 Unix)和 NCSA httpd 服务器程序。当我下载它们并用它们来搭建网站时,根本没有想到 Web 和互联网会迅速演变成我们日常生活的一部分。当时互联网仍然是官方并且非商业性的,全世界大概有 500 个公开的 Web 服务器(http://home.web.cern.ch/topics/birth-web)。到 1994 年底,Web 服务器的数量已经增长到了 10 000。互联网开始开放商业使用,Mosaic 的作者创立了 Netscape 来编写商用 Web 软件。从 Netscape 上市就可窥见当时互联网热潮的一斑,从那之后,Web 的爆发式增长就再也没有停止过。
几乎每种计算机语言都被用来编写过 Web 客户端和 Web 服务器程序。动态语言 Perl、PHP 和 Ruby 更是独领风骚。本章会说明为什么 Python 在 Web 相关的每个层面都是一种非常优秀的语言。Web 大致有三层。
客户端:访问远程网站。
服务端:为网站和 Web API 提供数据。
Web API 和服务:用另一种不同于可视化网页的方式来交换数据。
在本章结尾的练习中,我们还会搭建一个真正的交互式网站。
9.1 Web客户端
互联网最底层的网络传输使用的是传输控制协议 因特网协议,更常用的叫法是 TCPIP(详情参见 11.2.3 节)。这些协议会在计算机之间传输字节,但是并不关心这些字节的含义,后者由更高层的协议——用于特定目的的语法定义——来处理。HTTP 是 Web 数据交换的标准协议。
Web 是一个客户端 - 服务器系统。客户端向服务器发起请求:它会创建一个 TCP/IP 连接,通过 HTTP 发送 URL 和其他信息并接收一个响应。
响应的格式也由 HTTP 定义。其中包括请求的状态以及(如果请求成功)响应的数据和格式。
最著名的 Web 客户端是 Web 浏览器。它可以用很多种方式来发起 HTTP 请求。你可以在地址栏中输入 URL 或者点击网页上的链接来手动发起请求。通常来说,请求所返回的数据会被当作网页展示——HTML 文档、JavaScript 文件、CSS 文件和图片——但也可以是其他类型的数据,它们并不会被用于展示。
HTTP 的一个重要概念是无状态。你发起的每个 HTTP 请求都和其他请求互相独立。这可以简化基本的 Web 操作,但是会让其他的操作变得更复杂。下面列举其中一些。
- 缓存
没有改变的远程内容应该保存在 Web 客户端中,并避免重新从服务器下载。
- Session
购物网站必须记住你购物车中的商品。
- 认证
使用用户名和密码登录之后,网站应该记住你的登录状态。
可以使用 cookie 来解决无状态带来的问题。服务器可以在 cookie 中加入一些特殊的信息,这样当客户端发回 cookie 时就可以根据 cookie 内容来进行判断。
9.1.1 使用telnet
进行测试
HTTP 是基于文本的协议,所以你实际上可以自己编写协议内容来进行 Web 测试。古老的 telnet
程序可以让你连接到目标服务器和端口并输入命令。
下面以大家最常用的测试站点 Google 为例,来获取它的一些首页信息。输入:
$ telnet www.google.com 80
如果 google.com 的 80 端口有一个 Web 服务器程序(我觉得这是肯定的),telnet
会打印出一些确认信息并显示一个空白行供你输入信息:
Trying 74.125.225.177...
Connected to www.google.com.
Escape character is '^]'.
现在,向 telnet
中输入一条真实的 HTTP 命令并发送给 Google 的 Web 服务器。最常用的 HTTP 命令(当你在浏览器地址栏中输入 URL 时,实际上发送的就是这条命令)是 GET
。这会获取指定资源的内容,比如一个 HTML 文件,并返回给客户端。在我们的第一次测试中,使用的是 HTTP 命令 HEAD
,这会获取一些和资源相关的基本信息:
HEAD HTTP1.1
HEAD /
会发送 HTTP HEAD
动词(命令)来获取和首页(/
)相关的信息。再次按下回车来发送一个空行,这样远程服务器就知道你已经输入完毕,正在等待响应。你会收到一个下面这样的响应(我们用 …
省略了一些内容,这样页面会比较整洁):
HTTP/1.1 200 OK
Date: Sat, 26 Oct 2013 17:05:17 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Set-Cookie: PREF=ID=962a70e9eb3db9d9:FF=0:TM=1382807117:LM=1382807117:S=y...
expires=Mon, 26-Oct-2015 17:05:17 GMT;
path=/;
domain=.google.com
Set-Cookie: NID=67=hTvtVC7dZJmZzGktimbwVbNZxPQnaDijCz716B1L56GM9qvsqqeIGb...
expires=Sun, 27-Apr-2014 17:05:17 GMT
path=/;
domain=.google.com;
HttpOnly
P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts...
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Alternate-Protocol: 80:quic
Transfer-Encoding: chunked
这些是 HTTP 响应头和对应的值。其中一些,比如 Date
和 Content-Type
,是必要的。其他的,比如 Set-Cookie
,是用来在多次访问(稍后会讨论状态管理)之间追踪你的活动的。当你发起 HTTP HEAD
请求时,只会得到头部。如果你使用 HTTP GET
或者 POST
命令,就会收到头部和首页的数据(混合了 HTML、CSS、JavaScript 以及其他 Google 放在首页的东西)。
我不会把你扔在 telnet
里不管,输入下面的命令来关闭 telnet
:
q
9.1.2 Python的标准Web库
在 Python 2 中,Web 客户端和服务器模块结构都比较散乱。Python 3 的目标之一就是把这些模块打包成两个包(第 5 章提到过,包就是一个包含模块文件的目录)。
http
会处理所有客户端 - 服务器 HTTP 请求的具体细节:client
会处理客户端的部分server
会协助你编写 Python Web 服务器程序cookies
和cookiejar
会处理 cookie,cookie 可以在请求中存储数据
urllib
是基于http
的高层库:request
处理客户端请求response
处理服务端的响应parse
会解析 URL
下面,我们使用标准库来获取网站的内容。例子中的 URL 会返回一段随机文本,有点像幸运饼干 2:
2幸运饼干,又称签语饼、幸运签语饼、幸福饼干、占卜饼等,是一种美式亚洲风味脆饼,通常由面粉、糖、香草及奶油做成,并且里面包有类似箴言或者模棱两可预言的字条。——译者注
>>> import urllib.request as ur
>>> url = 'http://www.iheartquotes.com/api/v1/random'
>>> conn = ur.urlopen(url)
>>> print(conn)
<http.client.HTTPResponse object at 0x1006fad50>
在官方文档(https://docs.python.org/3/library/http.client.html)中,我们可以看到 conn
是一个包含许多方法的 HTTPResponse
对象,其中的 read()
方法会获取网页的数据:
>>> data = conn.read()
>>> print(data)
b'You will be surprised by a loud noise.\r\n\n[codehappy]
http://iheartquotes.com/fortune/show/20447\n'
这段 Python 代码会创建一个 TCP/IP 连接并连接到远程服务器,发起一个 HTTP 请求并接收 HTTP 响应。响应中除了网页的数据(那段随机文本)还包含很多其他信息,其中最重要的一条就是 HTTP 状态码:
>>> print(conn.status)
200
200
意味着一切正常。HTTP 状态码有许多种,可以根据第一个(百位)数字来分成五类。
- 1xx(信息)
服务器收到了请求,但是需要客户端发送一些额外的信息。
- 2xx(成功)
请求成功。除了 200 以外,其他的状态码还会包含一些特殊含义。
- 3xx(重定向)
资源位置发生改变,所以响应会返回一个新的 URL 给客户端。
- 4xx(客户端错误)
客户端发生错误,比如最出名的 404(页面不存在)。418(我是一个茶壶)是一个愚人节笑话。
- 5xx(服务端错误)
500 是最常见的错误。你可能也见到过 502(网关错误),这表示 Web 服务器程序和后端的应用服务器之间无法连接。
Web 服务器程序可以返回各种格式的数据。通常是 HTML(以及一些 CSS 和 JavaScript),但是在幸运饼干例子中返回的是纯文本。数据格式由 HTTP 响应头部中的 Content-Type
指定,它也出现在了 google.com 例子中:
>>> print(conn.getheader('Content-Type'))
text/plain
text/plain
字符串表示的是一个 MIME 类型,它的意思是纯文本。google.com 例子中返回的 MIME 类型是 text/html
。稍后你还会看到更多 MIME 类型。
出于好奇,我们来看看响应中还有什么 HTTP 头?
>>> for key, value in conn.getheaders():
... print(key, value)
...
Server nginx
Date Sat, 24 Aug 2013 22:48:39 GMT
Content-Type text/plain
Transfer-Encoding chunked
Connection close
Etag "8477e32e6d053fcfdd6750f0c9c306d6"
X-Ua-Compatible IE=Edge,chrome=1
X-Runtime 0.076496
Cache-Control max-age=0, private, must-revalidate
还记得前面的那个 telnet
例子吗?现在,Python 标准库解析了 HTTP 响应的整个头部并存储成一个字典。Date
和 Server
一眼就能看懂,其他的会难懂一些。HTTP 有许多类似 Content-Type
的标准头部,也有许多可选头部。
9.1.3 抛开标准库:requests
第 1 章最开始,我们使用标准库 urllib.request
和 json
来访问 YouTube 的 API。接着使用第三方模块 requests
重写了一个新版本。requests
的版本更短并且更易读。
在大多数情况下,使用 requests
可以让 Web 客户端开发变得更加简单。你可以阅读文档(http://docs.python-requests.org/)来获取更多信息。本节会介绍 requests
的基本用法。本书之后的内容将用它来实现 Web 客户端。
首先需要把 requests
库安装到你的 Python 环境中。打开一个终端窗口(Windows 用户可以输入 cmd
来打开),输入下面的命令让 Python 的包管理器 pip
下载最新版的 requests
包并安装:
$ pip install requests
如果遇到问题,请阅读附录 D 来学习如何安装和使用 pip
。
用 requests
来重写一遍上面的例子:
>>> import requests
>>> url = 'http://www.iheartquotes.com/api/v1/random'
>>> resp = requests.get(url)
>>> resp
<Response [200]>
>>> print(resp.text)
I know that there are people who do not love their fellow man, and I hate
people like that!
-- Tom Lehrer, Satirist and Professor
[codehappy] http://iheartquotes.com/fortune/show/21465
看起来和 urllib.request.urlopen
差不多,不过我觉得看起来更简洁一些。
9.2 Web服务端
Web 开发者已经认识到,在编写 Web 服务器和服务端程序方面 Python 是一门非常优秀的语言。因此,出现了一系列基于 Python 的 Web 框架,这导致开发者很难作出选择——对 Python 教程的作者来说更是如此。
Web 框架提供了一系列用户搭建网站的特性,已经不再是一个简单的 Web(HTTP)服务器了。你会看到许多特性:路由(URL 映射到服务端函数)、模板(HTM 加上动态内容)、调试等。
我不会介绍所有的框架,只会介绍那些相对比较简单易用并且适合开发产品级网站的框架,也会介绍如何用 Python 来处理网站的动态部分并用传统的 Web 服务器来处理静态部分。
9.2.1 最简单的Python Web服务器
可以用一行 Python 代码启动一个简单的 Web 服务器:
$ python -m http.server
这是一个非常简单的 Python HTTP 服务器。如果成功启动,会打印出下面的初始化状态信息:
Serving HTTP on 0.0.0.0 port 8000 ...
0.0.0.0
表示任意 TCP 地址。这样无论服务器的地址是什么,Web 客户端都可以访问。第 11 章会有更多 TCP 的底层细节和其他的网络协议。
现在你可以通过相对路径来请求文件,它们会被 Web 服务器返回。如果你在 Web 浏览器中输入 http://localhost:8000,会看到一个目录列表,Web 服务器会打印出下面这样的访问日志:
127.0.0.1 - - [20/Feb/2013 22:02:37] "GET HTTP1.1" 200 -
localhost
和 127.0.0.1
在 TCP 中是同义词,都表示你的本地计算机,因此即使你的计算机没有连网,也可以执行这个例子。这行输出的具体解释如下所示。
127.0.0.1
是客户端的 IP 地址第一个
"-"
是远程用户名,本例中为空第二个
"-"
是登录用户名,本例中是可选的,为空[20/Feb/2013 22:02:37]
是访问日期和时间“
GET HTTP1.1
”是发送给 Web 服务器的命令:HTTP 方法(
GET
)请求的资源(
/
,最上层目录)HTTP 版本(
HTTP/1.1
)
最后的
200
表示 Web 服务器返回的 HTTP 状态码
随便点击一个文件。如果你的浏览器可以识别它的格式(HTML、PNG、GIF、JPEG 等),就会显示出这个文件,Web 服务器也会记录这次请求。举例来说,如果当前目录下有 oreilly.png 文件,请求 http://localhost:8000/oreilly.png 会返回图 7-1 中这个令人不安的家伙,Web 服务器中会显示类似下面这样的日志:
127.0.0.1 - - [20/Feb/2013 22:03:48] "GET oreilly.png HTTP1.1" 200 -
如果同一个目录下还有其他文件,它们也会出现在列表中,你可以点击它们来下载。如果你的浏览器可以显示点击文件的格式,那你会直接在屏幕上看到文件内容;否则你的浏览器会询问你是否要下载并保存文件。
默认的端口数是 8000,你也可以指定其他的数字:
$ python -m http.server 9999
输出如下所示:
Serving HTTP on 0.0.0.0 port 9999 ...
这个 Python 特有的 Web 服务器很适合用作快速测试。在大多数终端中,你可以按下 Ctrl+C 来结束这个进程。
一定不要把这个简单的 Web 服务器用在真正的产品级网站中。Nginx 和 Apache 等传统 Web 服务器可以更快地处理静态文件。此外,这个简单的 Web 服务器不能处理动态内容,其他更高端的 Web 服务器可以接收参数并返回动态内容。
9.2.2 Web服务器网关接口
人总是会变的,现在只提供简单的文件服务已经不能满足我们了,我们想要能够动态运行程序的 Web 服务器。在 Web 发展的早期,出现了通用网关接口(CGI),客户端可以通过它来让 Web 服务器运行外部程序并返回结果。CGI 也会从 Web 服务器获取用户输入的参数并传给外部程序。然而,对于每个用户请求都需要运行一次程序,这样很难扩大用户规模,因为即使程序很小,启动时还是会有明显的等待时间。
为了避免启动延迟,人们开始把语言解释器合并到 Web 服务器中。Apache 可以通过 mod_php
模块来运行 PHP,通过 mod_perl
模块来运行 Perl,通过 mod_python
来运行 Python。这样,动态语言的代码就可以直接在持续运行的 Apache 进程中执行,不用再调用外部程序。
另一种方式是在一个独立的持续运行的程序中运行动态语言,并让它和 Web 服务器进行通信,例如 FastCGI 和 SCGI。
Web 服务器网关接口(WSGI)的定义极大地促进了 Python 在 Web 方面的发展。WSGI 是一个通用的 API,连接 Python Web 应用和 Web 服务器。本章接下来介绍的所有 Python Web 框架和 Web 服务器都使用了 WSGI。你并不需要知道 WSGI 的原理(其实也没有多少内容),但是理解其中一些概念是非常有帮助的。
9.2.3 框架
Web 服务器会处理 HTTP 和 WSGI 的具体细节,但是真正的网站是你使用框架写出的 Python 代码。因此,接下来会介绍一下什么是框架,然后再来讲解如何使用它们来创建网站。
如果你想用 Python 编写网站,有许多 Python Web 框架供你选择(或许有些过多了)。对于一个 Web 框架来说,至少要具备处理客户端请求和服务端响应的能力。框架可能会具备下面这些特性中的一种或多种。
- 路由
解析 URL 并找到对应的服务端文件或者 Python 服务器代码。
- 模板
把服务端数据合并成 HTML 页面。
- 认证和授权
处理用户名、密码和权限。
- Session
处理用户在多次请求之间需要存储的数据。
接下来会用两个框架(bottle
和 flask
)来编写一些示例代码。之后会介绍其他框架,用它们编写带数据库的网站非常方便。无论你想编写什么网站,都能找到合适的框架。
9.2.4 Bottle
Bottle(瓶子)只包含一个简单的 Python 文件,所以非常易于使用并且易于部署。Bottle 并不是 Python 标准库的一部分,所以需要使用下面的命令来安装它:
$ pip install bottle
下面的代码会运行一个测试用于 Web 服务器,如果你在浏览器中访问 http://localhost : 9999/,这个服务器会返回一行文本。把下面的代码保存为 bottle1.py:
from bottle import route, run
@route('/')
def home():
return "It isn't fancy, but it's my home page"
run(host='localhost', port=9999)
Bottle 使用 route
装饰器来关联 URL 和函数。在本例中,/
(首页)会被 home()
函数处理。输入下面的命令来用 Python 运行这个服务器脚本:
$ python bottle1.py
如果你在浏览器中访问 http://localhost:9999,会看到下面的内容:
It isn't fancy, but it's my home page
run()
函数会执行 bottle
内置的 Python 测试用 Web 服务器。你也可以使用其他 Web 服务器,但是在开发和测试时它非常有用。
把 HTML 硬编码到代码中是很不合适的,我们创建一个单独的 HTML 文件 index.html 并写入下面的内容:
My <b>new</b> and <i>improved</i> home page!!!
接着让 bottle
在首页被请求时返回这个 HTML 文件的内容。把下面的代码保存为 bottle2.py:
from bottle import route, run, static_file
@route('/')
def main():
return static_file('index.html', root='.')
run(host='localhost', port=9999)
调用 static_file()
时,我们指定的是 root
目录(在本例中是 '.'
,也就是当前目录)下的 index.html 文件。如果上一个例子的脚本还在执行,请终止它并运行这个新服务器:
$ python bottle2.py
在浏览器中访问 http://localhost:9999 时,会看到:
My new and improved home page!!!
再来看最后一个例子。它展示了如何指定 URL 中的参数并使用它们。按照惯例,这次的文件名是 bottle3.py:
from bottle import route, run, static_file
@route('')
def home():
return static_file('index.html', root='.')
@route('echo/<thing>')
def echo(thing):
return "Say hello to my little friend: %s!" % thing
run(host='localhost', port=9999)
我们定义了一个新函数 echo()
,并且在 URL 中指定了一个字符串参数。这就是例子中 @route('echo
做的事情。路由中的
表示 URL 中 echo
之后的内容都会被赋值给字符串参数 thing
,然后被传入 echo
函数。下面来看看效果,如果旧服务器还在运行就终止它,然后启动新服务器:
$ python bottle3.py
接着在浏览器中访问 http://localhost:9999echoMothra,会看到:
Say hello to my little friend: Mothra!
好的,现在请继续让 bottle3.py 运行,我们来做一个实验。刚才你在浏览器中验证了这个例子确实能够正常工作,并且看到了展示出来的页面。其实还可以让客户端库(比如 requests
)来做同样的事。把下面的代码保存为 bottle_test.py:
import requests
resp = requests.get('http://localhost:9999echoMothra')
if resp.status_code == 200 and \
resp.text == 'Say hello to my little friend: Mothra!':
print('It worked! That almost never happens!')
else:
print('Argh, got this:', resp.text)
然后运行:
$ python bottle_test.py
你会在终端中看到:
It worked! That almost never happens!
这是一个简单的单元测试。第 12 章中详细介绍了为什么要测试以及如何用 Python 编写测试。
bottle
还有许多其他的特性,例如你可以试着在调用 run()
时加上这些参数:
debug=True
,如果出现 HTTP 错误,会创建一个调试页面;reloader=True
,如果你修改了任何 Python 代码,浏览器中的页面会重新载入。
详细的文档可以在开发者网站(http://bottlepy.org/docs/dev/)上找到。
9.2.5 Flask
Bottle 是一个非常优秀的入门框架。但如果你需要更多的功能,那就试试 Flask 吧。Flask 最初只是 2010 年的一个愚人节玩笑,但是由于大家的反响非常热烈,作者 Armin Ronacher 把它变成了一个真正的框架。有趣的是,Flask 这个名字也是一个文字游戏 3。
3Flask 和 bottle 都有瓶子的意思。——译者注
Flask 和 Bottle 一样易用,同时还支持很多专业 Web 开发需要的扩展功能,比如 Facebook 认证和数据库集成。Flask 是我最喜欢的 Python Web 框架,因为它成功地做到了既好用又强大。
Flask 包中自带了 werkzeug
WSGI 库和 jinja2
模板库。你可以从终端中安装:
$ pip install flask
我们用 flask
来重写一下最后那个 bottle
例子。首先需要进行一些修改。
Flask 默认的静态文件目录是
static
,默认的静态文件 URL 由/static
开始。我们把文件夹改成'.'
(当前目录),把 URL 前缀改成''
(空),这样 URL/
可以被映射到文件 index.html。在
run()
函数中,设置debug=True
可以启用代码自动重载;bottle
把这个参数拆成了两个,debug
和reload
。
把下面的代码保存为 flask1.py:
from flask import Flask
app = Flask(__name__, static_folder='.', static_url_path='')
@app.route('/')
def home():
return app.send_static_file('index.html')
@app.route('echo<thing>')
def echo(thing):
return "Say hello to my little friend: %s" % thing
app.run(port=9999, debug=True)
然后在终端或者命令行中运行 Web 服务器:
$ python flask1.py
在浏览器中输入下面的 URL 来测试首页是否可以正常访问:
http://localhost:9999/
你能看到下面的内容(和 bottle
例子一样):
My new and improved home page!!!
试试 /echo
功能:
http://localhost:9999echoGodzilla
会看到:
Say hello to my little friend: Godzilla
把 debug
设置为 True
还有一个好处。在调用 run
时,如果代码中出现异常,Flask 会返回一个特殊的网页,其中会包含一些有用的信息,比如错误类型和错误位置。此外,你还可以使用一些命令来查看服务器程序中变量的值。
在生产环境中不要把
debug
设置为True
,否则可能会暴露出太多信息,有安全隐患。
到目前为止,Flask 的例子只是重复了我们之前用 bottle
做的事。那 Flask 能做什么 bottle
做不了的事呢? Flask 内置了 jinja2
,一个极具扩展性的模板系统。下面这个例子展示了如何在 flask
中使用 jinja2
。
创建一个名为 templates
的目录,把下面的代码存为 flask2.html:
<html>
<head>
<title>Flask2 Example</title>
</head>
<body>
Say hello to my little friend: {{ thing }}
</body>
</html>
接着在服务器程序中获取这个模板,写入我们传入的值 thing
,然后渲染成 HTML(为了节约空间,我去掉了 home()
函数)。把下面的代码存储为 flask2.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('echo<thing>')
def echo(thing):
return render_template('flask2.html', thing=thing)
app.run(port=9999, debug=True)
thing = thing
这个参数的意思是把名为 thing
的变量传入模板,它的值是变量 thing
中的字符串。
关闭 flask1.py,运行 flask2.py:
$ python flask2.py
现在输入 URL:
http://localhost:9999echoGamera
你会看到这些内容:
Say hello to my little friend: Gamera
修改一下模板内容并把它存为 flask3.html,放在 templates 目录下:
<html>
<head>
<title>Flask3 Example</title>
</head>
<body>
Say hello to my little friend: {{ thing }}.
Alas, it just destroyed {{ place }}!
</body>
</html>
你可以用很多方法把第二个参数传入 echo
的 URL。
通过URL路径传入参数
你可以把参数当作 URL 的一部分,使用这种方法可以直接扩展 URL 本身(把下面的代码存储为 flask3a.py):
from flask import Flask, render_template
app = Flask(__name__)
@app.route('echo<thing>/<place>')
def echo(thing, place):
return render_template('flask3.html', thing=thing, place=place)
app.run(port=9999, debug=True)
按照惯例,停止之前的服务器脚本并运行这个新脚本:
$ python flask3a.py
URL 看起来是这样:
http://localhost:9999echoRodan/McKeesport
你会看到:
Say hello to my little friend: Rodan. Alas, it just destroyed McKeesport!
此外,还可以用 GET
参数来传递参数(把下面的代码存储为 flask3b.py):
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('echo')
def echo():
thing = request.args.get('thing')
place = request.args.get('place')
return render_template('flask3.html', thing=thing, place=place)
app.run(port=9999, debug=True)
运行新的服务器脚本:
$ python flask3b.py
这次使用这个 URL:
http://localhost:9999/echo?thing=Gorgo&place=Wilmerding
你会看到:
Say hello to my little friend: Gorgo. Alas, it just destroyed Wilmerding!
在 URL 中使用 GET
命令时,传入的参数形式为 ?key1=val1&key2=val2&…
。
你可以使用字典的 **
操作符来向模板中一次性传入字典的多个值(把下面的代码存储为 flask3c.py):
from flask import Flask, render_template,request
app = Flask(__name__)
@app.route('echo')
def echo():
kwargs = {}
kwargs['thing'] = request.args.get('thing')
kwargs['place'] = request.args.get('place')
return render_template('flask3.html', **kwargs)
app.run(port=9999, debug=True)
**kwargs
的行为与 thing=thing
和 place=place
一样,但是在参数很多时可以少输入很多内容。
jinja2
模板语言还有很多功能,如果你用过 PHP,应该会看到许多熟悉的东西。
9.2.6 非Python的Web服务器
到目前为止,我们使用的 Web 服务器都很简单:不是标准库的 http.server
就是 Bottle 和 Flask 自带的调试用服务器。在生产环境中,你需要用更快的 Web 服务器来运行 Python。下面是常用的选择:
apache
加上mod_wsgi
模块nginx
加上uWSGI
应用服务器
两者都很不错。apache
可能是最流行的,nginx
更稳定并且占用内存更少。
1. Apache
Apache(http://httpd.apache.org/)Web 服务器中最好用的 WSGI 模块是 mod_wsgi
(https://code.google.com/p/modwsgi/)。这个模块可以在 Apache 进程中运行 Python 代码,也可以在独立进程中运行 Python 代码并和 Apache 进行通信。
如果你的系统是 Linux 或者 OS X,那你已经有 apache
了。如果是 Windows,你需要安装 apache
(http://httpd.apache.org/docs/current/platform/windows.html)。
最后,安装好你喜欢的基于 WSGI 的 Python Web 框架,这里我们使用 bottle
。之后的工作基本上都是配置 Apache,这里有很多黑魔法。
把下面的代码存储为 varwww/test/home.wsgi:
import bottle
application = bottle.default_app()
@bottle.route('/')
def home():
return "apache and wsgi, sitting in a tree"
这次不要调用 run()
,因为它会启动内置的 Python Web 服务器。我们需要给变量 application
赋值,因为 mod_wsgi
会使用这个变量来结合 Web 服务器和 Python 代码。
如果 apache
和 mod_wsgi
模块工作正常,只需要把它们连接到 Python 脚本就行。要做到这件事,需要向 apache
服务器的默认网站配置文件中加入一行,但是找到那个文件并不容易。它可能是 etcapache2/httpd.conf,也可能是 etcapache2/sites-available/default,还可能是某个人的宠物蝾螈的拉丁文名字。
假设现在你能找到那个文件,把下面这行加入控制默认网站的
中:
WSGIScriptAlias / varwww/test/home.wsgi
添加之后可能是这样的:
<VirtualHost *:80>
DocumentRoot varwww
WSGIScriptAlias / varwww/test/home.wsgi
<Directory varwww/test>
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
启动 apache
,如果你已经启动就重启,这样才能应用新的配置文件。之后,如果访问 http://localhost/,你会看到:
apache and wsgi, sitting in a tree
这样就在嵌入模式中运行了 mod_wsgi
,在这个模式下它是 apache
的一部分(在同一进程内)。
也可以用守护模式来运行,这样会产生一个或多个独立于 apache
的进程。要使用守护模式,可以向 apache
配置文件中加入两行内容:
$ WSGIDaemonProcess domain-name user=user-name group=group-name threads=25
WSGIProcessGroup domain-name
在上面的代码中,user-name
和 group-name
是操作系统的用户和用户组名称,domain-name
是你的互联网域名。最简单的 apache
配置如下所示:
<VirtualHost *:80>
DocumentRoot varwww
WSGIScriptAlias / varwww/test/home.wsgi
WSGIDaemonProcess mydomain.com user=myuser group=mygroup threads=25
WSGIProcessGroup mydomain.com
<Directory varwww/test>
Order allow,deny
Allow from all
</Directory>
</VirtualHost>
2. Nginx Web服务器
Nginx(http://nginx.org/)Web 服务器没有内嵌的 Python 模块。它通过一个独立的 WSGI 服务器(比如 uWSGI)来和 Python 程序通信。把它们结合在一起可以实现高性能并且可配置的 Python Web 开发平台。
你可以从官网(http://wiki.nginx.org/Install)安装 nginx
。此外,还需要安装 uWSGI(http://uwsgidocs.readthedocs.org/en/latest/Install.html)。uWSGI 是一个大系统,有许多需要调节的内容。可以在 http://flask.pocoo.org/docs/0.10/deploying/uwsgi/ 看到如何结合 Flask、nginx
和 uWSGI。
9.2.7 其他框架
网站和数据库就像花生酱和果冻,它们经常一起出现。小型框架,比如 bottle
和 flask
,不能直接支持数据库,尽管有一些插件可以实现。
如果你需要开发基于数据库的网站并且数据库的结构不会经常变化,那最好试试大型的 Python Web 框架。现在主流的框架有以下这些。
django
(https://www.djangoproject.com/)
是最流行的,尤其是大型网站很喜欢用它。有很多学习 django
的理由,其中最重要的就是 Python 的招聘要求中经常需要 django
的开发经验。它有 ORM 功能(8.4.6 节的“对象关系映射”部分讨论过),可以在网页中自动应用典型的数据库 CRUD 功能(创建、替换、更新和删除),就像之前在 8.4.1 节中说的一样。你也可以不用 django
自带的 ORM,可以选择 SQLAlchemy 或者直接使用 SQL 查询语句。
web2py
(http://www.web2py.com/)
和 django
功能类似,只是风格不同。
pyramid
(http://www.pylonsproject.org/)
诞生于最早的 pylons
项目,和 django
很像。
turbogears
(http://turbogears.org/)
这个框架支持 ORM、多种数据库以及多种模板语言。
wheezy.web
(http://pythonhosted.org/wheezy.web/)
这是一个比较新的框架,专为性能而生。在最近的测试中,它比其他框架都快(http://mindref.blogspot.com/2012/10/python-web-routing-benchmark.html)。
你可以使用这个在线表格(https://wiki.python.org/moin/WebFrameworks)来对比这些框架。
如果你的网站使用的是关系数据库,就可以不使用大型框架,直接用 bottle
、flask
这类框架结合关系数据库模块就行。也可以使用 SQLAlchemy 来屏蔽数据库的差异,直接写通用 SQL 代码就行。相比特定的 ORM 语法,大多数程序员更熟悉 SQL。
当然,你完全可以不使用关系数据库。如果你的数据结构差异很大——不同行的同一列差别很大——那你或许应该试试无模式数据库,比如 8.5 节讨论过的 NoSQL 数据库。我之前开发的一个网站一开始使用 NoSQL 数据库来存储数据,后来切换到一个关系数据库,然后又切换到另一个关系数据库,接着又切换到一个 NoSQL 数据库,最后又切换回了一个关系数据库。
其他Python Web服务器
下面是一些类似 apache
和 nginx
的基于 Python 的 WSGI 服务器,使用多进程和 / 或线程(参见 11.1 节)来处理并发请求:
cherrypy
(http://www.cherrypy.org/)pylons
(http://www.pylonsproject.org/)
下面是一些基于事件的服务器,只使用单线程但不会阻塞:
tornado
(http://www.tornadoweb.org)gevent
(http://gevent.org/)gunicorn
(http://gunicorn.org/)
第 11 章关于“并发”的讨论会详细介绍事件。
9.3 Web服务和自动化
我们已经看过了传统的 Web 客户端和服务器应用,它们会生成并使用 HTML 页面。然而,Web 逐渐演变出了许多非 HTML 的数据传输格式。
9.3.1 webbrowser
模块
首先来看点好玩的。在终端里启动 Python 会话并输入下面的代码:
>>> import antigravity
这会调用标准库的 webbrowser
模块并让你的浏览器显示一个 Python 入门网页 4。
4如果你因为某些原因看不到该网页,访问 xkcd(http://xkcd.com/353/)。
你也可以直接使用这个模块。下面的程序会在浏览器中打开 Python 官网的首页:
>>> import webbrowser
>>> url = 'http://www.python.org/'
>>> webbrowser.open(url)
True
下面的代码会在新窗口中打开它:
>>> webbrowser.open_new(url)
True
如果你的浏览器支持标签,下面的代码会在新标签中打开它:
>>> webbrowser.open_new_tab('http://www.python.org/')
True
webbrowser
可以完全控制你的浏览器。
9.3.2 Web API和表述性状态传递
通常来说,数据只存在于网页内。如果你想获取数据,需要在 Web 浏览器中访问网页并阅读数据。如果网站作者在你最后一次访问之后做了什么改动,数据的位置和格式就可能发生变化。
除了发布网页,你还可以通过应用编程接口(API)来提供数据。客户端通过 URL 来访问你的服务并从响应中获取状态和数据。数据并不是 HTML 网页,而是更容易被程序处理的格式,比如 JSON 和 XML(第 8 章中对这些格式有详细的介绍)。
表述性状态传递(REST)是 Roy Fielding 在他的博士论文中提出的概念。许多产品都宣称它们具备 REST 接口或者是 RESTful 接口。在具体实现上,其实就是一个 Web 接口,即定义一组可以访问 Web 服务的 URL。
RESTful 服务会用特定的方式来使用 HTTP 动词,如下所示。
HEAD
获取资源的信息,但是不包括数据。
GET
顾名思义,GET
会从服务器取回资源的数据。这是浏览器使用的标准方法。如果你在 URL 中看到一个问号(?
)之后跟着一堆参数,那就是一个 GET
请求。GET
不应该被用来创建、修改或者删除数据。
POST
这个动词会更新服务器上的数据。通常它会被用在 HTML 的表单和 Web API 中。
PUT
这个动词会创建一个新资源。
DELETE
顾名思义,这个动词会删除一些东西,就像广告中的真话一样 5 !
5意思是非常少见。——译者注
RESTful 客户端也可以使用 HTTP 请求头来请求一种或多种类型的内容,例如一个具备 REST 接口的复杂服务可能更希望输入和输出是 JSON 字符串。
9.3.3 JSON
第 1 章展示的两个 Python 例子获取了最热门的 YouTube 视频信息。第 8 章介绍了 JSON。JSON 非常适合用在 Web 客户端和服务器的数据交换中。它在基于 Web 的 API 中非常流行,比如 OpenStack。
9.3.4 抓取数据
有时候你可能想要更多信息——电影排名、股票价格或者商品供应信息——但是这些信息只存在于 HTML 页面中,包裹在广告和其他无关的信息中。
你可以使用下面的方法来手动提取信息:
(1) 在浏览器中输入 URL;
(2) 等待页面加载;
(3) 浏览页面并找到你想要的信息;
(4) 把信息记录下来;
(5) 可能要把这个过程重复应用在所有相关 URL 上。
然而,我们更希望能自动执行其中的一步或者多步。自动抓取 Web 信息的程序叫作爬行者或者爬虫(对于蜘蛛恐惧者来说毫无吸引力)。从远端服务器获取到信息之后爬虫会进行解析并寻找有用的信息。
如果你需要一个企业级的爬虫,那强烈推荐 Scrapy(http://scrapy.org/):
$ pip install scrapy
Scrapy 是一个框架,并不是类似 BeautifulSoup
的模块。它会做更多事,不过也更难设置。更多关于 Scrapy 的信息请阅读文档(http://scrapy.org)或者在线教程(http://amaral-lab.org/blog/quick-introduction-web-crawling-using-scrapy-part-)。
9.3.5 用BeautifulSoup
来抓取HTML
如果你已经拿到了一个网站的 HTML 数据并且想从中提取数据,BeautifulSoup
是一个不错的选择。解析 HTML 比想象中要难得多,因为互联网上的 HTML 在技术角度通常是不合法的:没有闭合的标签、不正确的嵌套以及其他复杂的东西。如果你自己尝试过用正则表达式(第 7 章介绍过)来解析 HTML,你一定遇到过这些麻烦事。
输入下面的命令来安装 BeautifulSoup
(千万别忘了结尾的 4
,否则 pip
会试着安装旧版并且可能会安装失败):
$ pip install beautifulsoup4
现在可以用它来尝试一下获取一个网页上的所有链接。HTML 的 a
元素表示一个链接,它的 href
属性表示链接的目标地址。下面的例子会定义函数 get_links()
并用它来完成任务。程序可以从命令行参数接收一个或多个 URL:
def get_links(url):
import requests
from bs4 import BeautifulSoup as soup
result = requests.get(url)
page = result.text
doc = soup(page)
links = [element.get('href') for element in doc.find_all('a')]
return links
if __name__ == '__main__':
import sys
for url in sys.argv[1:]:
print('Links in', url)
for num, link in enumerate(get_links(url), start=1):
print(num, link)
print()
我把这个程序存储为 links.py 并运行下面的命令:
$ python links.py http://boingboing.net
下面是一部分输出:
Links in http://boingboing.net/
1 http://boingboing.net/suggest.html
2 http://boingboing.net/category/feature/
3 http://boingboing.net/category/review/
4 http://boingboing.net/category/podcasts
5 http://boingboing.net/category/video/
6 http://bbs.boingboing.net/
7 javascript:void(0)
8 http://shop.boingboing.net/
9 http://boingboing.net/about
10 http://boingboing.net/contact
9.4 练习
(1) 如果你还没有安装 flask
,现在安装它。这样会自动安装 werkzeug
、jinja2
和其他包。
(2) 搭建一个网站框架,使用 Flask 的调试 / 代码重载来开发 Web 服务器。使用主机名 localhost
和默认端口 5000
来启动服务器。如果你电脑的 5000
端口已经被占用,使用其他端口。
(3) 添加一个 home()
函数来处理对于主页的请求,让它返回字符串 It's alive!
。
(4) 创建一个名为 home.html 的 Jinja2 模板文件,内容如下所示:
<html>
<head>
<title>It's alive!</title>
<body>
I'm of course referring to {{thing}}, which is {{height}} feet tall and {{color}}.
</body>
</html>
(5) 修改 home()
函数,让它使用 home.html 模板。给模板传入三个 GET
参数:thing
、height
和 color
。