笨方法学python习题51——从浏览器中获取输入

从浏览器中获取输入

让浏览器显示是一件很有趣的事,但如果能让用户通过表单(form)向应用程序提交文本就更有趣了。在此,我们将使用表单改进 你的web应用程序,并且将用户相关的信息保存到他们的会话(session)中

web的工作原理

关于web的工作原理,这里讲的并不完整,但相当准确,在你的程序出错时,他会帮你找到出错的原因。另外如果你理解了表单的应用,那创建表单对你来说就会更容易。

我将以一张简单的图说起,它向你展示了web请求的各个部分,以及信息传递的大致流程。


为方便讲述HTTP请求(request)的流程,我在每个线条上加了字母标签以示区别。

1. 你在浏览器中输入网址http://learnpythonthehardway.org/,然后浏览器会通过你的计算机的网络设备发送请求(线路A)
2. 你的请求被传送到互联网(线路B),然后在抵达远程服务器(线路C),然后我的服务器将接受这个请求。
3. 服务器接受请求后,我的web应用程序就去处理这个请求(线路D),然后我的python代码就会去运行index.GET这个“处理程序”(handler)
4. 在代码返回的时候,我的python服务器就会发出响应,这个响应会再通过线路D传递到你的浏览器。
5. 这个网站所在的服务器将获取由线路D发出的响应,然后通过线路C传至因特网。
6. 响应通过互联网由线路B传至你的计算机,计算机的网卡在通过线路A将响应传给你的浏览器。
7. 最后,你的浏览器显示了这个响应的内容。

以上涉及的你应该掌握的术语,以便在谈论web应用程序时你能明白且应用它们。

  • 浏览器(browser)。你每天再用的软件。它的作用是接收你输入到地址栏的网址(如http://learnpythonthehardway.org),然后使用该信息向该网址对应的服务器提出请求。
  • 地址(address)。通常这是一个像http://learnpythonthehardway.org/一样的URL(Uniform Resource Locator,统一资源定位器),它告诉浏览器该打开哪个网站。前面的http指出了你要使用的协议,这里用的是“超文本传输协议”(Hyper-Text Transport Protocol,http)。你还可以试试ftp://ibiblio.org/,这是一个”文本传输协议”(File Transport Protocol, FTP)的例子。learnpythonthehardway.org这部分是“主机名”(hostname),也就是一个便于人阅读和记忆的地址,主机名会被分配到一串叫做“IP地址”的数字上面,这个“IP地址”就相当于网络中一台计算机的电话号码,通过这个号码可以访问这台计算机。最后,URL中还可以跟随一个“路径”,例如http://learnpythonthehardway.org/book/中的/book/,它对应的是服务器上的某个文件或者某些资源,通过访问这样的网址,你可向服务器发出请求,然后获取这些资源。网站地址还有很多别的组成部分,不过上述讲的最主要。
  • 连接(connection)。一旦浏览器知道了你用的协议(http)、你想联络的服务器及要从服务器上获得的资源,它就要去创建一个连接。这个过程中,浏览器让操作系统(operating System, OS)打开计算机的一个“端口”(port)(通常是80端口),端口准备好以后,操作系统会回传给你的程序一个类似文件的东西,它所做的事情就是通过网络传输和接收数据,让你的计算机和learnpythonthehardway.org这个网站所属的服务器之间实现数据交流。但你使用http://localhost:8080/访问自己的站点时,发生的事情其实是一样的,只不过这次你告诉了浏览器要访问的是你自己的计算机(localhost),要使用的端口不是默认的80,而是8080.你还可以直接访问http://learnpythonthehardway.org:80/,这和不输入端口效果一样,因为HTTP的默认端口本来就是80。
  • 请求(request)。浏览器通过你提供的地址建立了连接,现在它需要从远程服务器要到它或你想要的资源。如果在URL的结尾加了/book/,那你想要的就是/book/对应的文件或资源,大部分的服务器会直接为你调用/book/index.html这个文件,不过我们就假设不存在好了。浏览器为了获得服务器上的资源,它需要向服务器发送一个“请求”。这里就不讲细节了,为得到服务器上的内容。你必须先向服务器发送一个请求才行。有意思的是,“资源”不一定非要是文件。例如,当浏览器向你的应用程序提出请求时,服务器返回的其实是你的python代码生成的一些东西。
  • 服务器(server)。服务器指的是浏览器另一端连接的计算机,它知道如何回应浏览器请求的文件和资源。大部分的web服务器只要发送文件就可以了,这也是服务器流量的主要部分。不过你学的是使用python构建一个服务器,这个服务器知道如何接受请求,然后返回用python处理过的字符串。当使用这种处理方式时,其实是假装把文件发给了浏览器,而你用的都只是代码而已。就像你在习题50看到的,要构建一个“响应”其实不需要多少代码。
  • 响应(response)。这就是你的服务器回复你的请求而发回至浏览器的HTML,它里边可能有CSS、JavaScript或者图像等内容。以文件响应为例,服务器只要从磁盘读取文件,发送给浏览器就可以了,不过它还要将这些内容包在一个特别定义的“首部信息”(header)中,这样浏览器就会知道它获取的是什么类型的内容。以你的web应用程序为例,你发送的其实还是一样的东西,包括首部信息也一样,只不过这些数据是你用python代码即时生成的。

以上就是浏览器如何访问网站的最快的快速课程了。你要弄明白上述流程。有一个好的办法是,对照上面的图,将你在习题50中创建的web应用程序的内容分为几部分,让其中的各部分对应到上面的图。如果你能正确将程序的各部分对应到这张图,你就大致开始明白它的工作原理了。

表单的工作原理

熟悉“表单”最好的办法就是写一个可以接收表单数据的程序出来,然后看你可以对它做些什么。先将你的bin/app.py修改成下面的样子。

# form_test.py

import web

urls = (
    '/hello', 'Index'

)

app = web.application(urls, globals())

render = web.template.render('templates/')

class Index(object):
    def GET(self):
        form = web.input(name="Nobody")

        greeting = "Hello, %s" % form.name

        return render.index(greeting = greeting)

if __name__ == "__main__":
    app.run()

重启你的web应用程序(按Ctrl+C后重新运行),确认它已运行起来,然后使用浏览器访问http://localhost:8080/hello,这是浏览器应该会显示“I just wanted to say Hello, Nobady.”,接下来,将浏览器的地址改为http://localhost:8080/hello?name=Frank,然后你可看到页面显示为“Hello, Frank.”最后将name=Frank修改为你自己的名字,你就可以看到它对你再说“Hello”了。

研究一下你的程序里做过的修改:

1. 这里没有直接为greeting赋值,而是使用了web.input从浏览器获取数据。这个函数会将一组“键=值”的表述作为默认参数,解析你提供的URL中的?name=Frank部分,然后返回一个对象,你可以通过这个对象方便地访问到表单的值。
2. 然后通过form对象的form.name属性为greeting赋值
3. 其他内容和以前一样

URL中应该还可以包含多个参数。将本例的URL改成http://localhost:8080/hello?name=Frank&greet=Hola。然后修改代码,让它去获取form.name和form.greet,如下所示:

greeting = "%s, %s" % (form.greet, form.name)
修改完毕后,试着访问新的URL,然后将&greet=Hola部分删除,看看会得到什么样的错误信息。由于在web.input(name="Nobody")中没有为greet设定默认值,这样greet就变成了一个必须的参数,如果没有这个参数程序就会报错。现在修改一下你的程序,在web.input中未greet设一个默认值试试看。另外,你还可以设greet=None,这样可通过程序检查greet的值是否存在,然后提供一个比较好的错误信息出来。例如:

form = web.input(name="Nobody", greet=None)

if form.greet:
    greeting = "%s, %s" % (form.greet, form.name)
    return render.index(greeting = greeting)
else:
    return "ERROR: greet is required."

创建HTML表单

在URL上传递参数是可以的,不过这样看上去有些丑陋,而且不方便普通人使用。你真正需要的是一个“POST表单”,这是一种包含了<form>标签的特殊HTML文件。这种表单收集用户输入并将其传递给你的web应用程序,这和你上面实现的目的基本是一样的。

下面就快速创建一个,从中你可以看出它的工作原理。你需要创建一个新的HTML文件,叫做templates/hello_form.html:

# hello_form.html

<html>
    <head>
        <title>Sample Web Form</title>
    </head>

<body>

<h1>Fill Out This Form</h1>

<form action="/hello" method="POST">
    A Greeting: <input type="text" name="greet">
    <br/>
    Your Name: <input type="text" name="name">
    <br/>
    <input type="submit">

</form>

</body>

</html>

然后将bin/app.py改成下面样子

# post_form.py

import web

urls = (
    '/hello', 'Index'

)

app = web.application(urls, globals())

render = web.template.render('templates/')

class Index(object):
    def GET(self):

        return render.hello_form()

    def POST(self):
        form = web.input(name="Nobody", greet="Hello")
        greeting = "%s, %s" % (form.greet, form.name)

        return render.index(greeting = greeting)

if __name__=="__main__":
    app.run()

都写好以后重启web应用程序,然后通过浏览器访问它。

这次访问你会看到一个表单,它要求你输入“一个问候语句”(A Greeting)和“你的名字”(Your Name),等你输入完后点击“提交”(Submit)按钮,它就会输出一个正常的问候页面,不过这一次你的URL还是http://localhost:8080/hello,并没有包含你提交的参数。

在hello_form.html里面关键的一行是<form action="/hello" method="POST">,它告诉你浏览器以下内容。

1. 从表单中的各个栏位收集用户输入的数据。
2. 让浏览器使用一种POST类型的请求,将这些数据发送给服务器。这是另外一种浏览器请求,它会将表单栏位“隐藏”起来。
3. 将这个请求发送至/hello URL,这是由action="/hello"告诉浏览器的。

你可以看到两端<input>标签的名字(name)属性和代码中的变量是对应的,另外我们在class index中使用的不再只是GET方法,而是另一个POST方法。这个新程序的工作原理如下。

1. 浏览器访问web应用程序的/hello目录,它发送了一个GET请求,于是我们的index.GET函数就运行并返回了hello_form。
2. 你填好了浏览器的表单,然后浏览器依照<form>中的要求,将数据通过POST请求的方式发给web应用程序。
3. web应用程序运行了index.POST方法(而不是index.GET方法)来处理这个请求。
4. 这个index.POST方法完成了它正常的功能,将hello页面返回,这里并没有新的东西,只是一个新的函数名称而已。

作为练习,在templates/index.html中添加一个链接,让它指向/hello,这样你可以反复填写并提交表单查看结果。确认你可以解释清楚这个链接的工作原理,以及它是如何让你实现在templates/index.html和templates/hello_form.html之间循环跳转的,还有就是要明白你新修改过的python代码,清楚在什么情况下会运行到那一部分代码。

创建布局模板

在下一个习题中创建游戏的过程中,你需要创建很多的小HTML页面。如果你每次都写一个完整的网页,你会很快感觉到厌烦。幸运的是,你可以创建一个“布局模板”(layout template),也就是一种提供了通用的头文件和脚注的外壳模板,你可以用它将你所有的其他网页包裹起来。好程序员会尽可能减少重复动作,所以布局模板很重要。

将templates/index.html修改成下面这个样子

# index_laid_out.html

$def with (greeting)

$if greeting:
    I just wanted to say <em style="color: green; font-size: 2em;">$greeting</em>.
$else:
    <em>Hello</em>, world!

然后把templates/hello_form.html修改如下。

# hello_form_laid_out.html

<h1>Fill Out This Form</h1>

<form action="/hello" method="POST">
    A Greeting: <input type="text" name="greet">
    <br/>
    Your Name: <input type="text" name="name">
    <br/>
    <input type="submit">
</form>

上面这些修改的目的是将每一个页面顶部和底部的反复用到的“样板代码”代码剥掉。这些被剥掉的代码会被放到一个单独的templates/layout.html文件中,此后,这些反复用到的代码就由templates/layout.html来提供。

改好以后,创建一个templates/layout.html文件。

# layout.html

$def with (content)

    <html>
    <head>
        <title>Gothons Form Planet Percal #25</title>
    </head>

    <body>

    $:content

    </body>
    </html>

这个文件和普通模板文件类似,只是其他模板的内容将被传递给它,然后他会将其他模板的内容“包裹”起来。任何写在这里的内容都无需写在别的模板中了。要注意$:content的用法,这和其他模板变量有些不同。

最后一步,将render对象改成这样:

render = web.templates.render('templates/', base="layout")

这会告诉lpthw.web去使用templates/layout.html作为其他模板的基础模板。重启你的应用从程序观察一下,然后试着用各种方法修改你的布局模板,不要修改别的模板,看看输出会有什么变化。

为表单撰写自动测试代码

使用浏览器测试web应用程序很容易,只要点击刷新按钮就可以了。但如果可以写一些代码来测试我们的程序,为什么还要重复手动测试呢?你需要为你的web应用程序写一个小测试。会用到ex47中学过的一些东西,如果你不记得,回去复习。

为了让python加载bin/app.py并进行测试,需要点准备工作。首先创建一个bin/__init__.py空文件,这样python就会将bin/当做一个目录了。

我还为lpthw.web创建了一个简单的小程序,让你断言(assert)web应用程序的响应,这个函数有一个很合适的名字:assert_response。创建一个tests/tools.py文件:

#tools.py

from nose.tools import *

import re

def assert_response(resp, contains=None, matches=None, headers=None, status="200"):

    assert status in resp.status, "Expected response %r not in %r" % (status, resp.status)

    if status == "200":

        assert resp.data, "Response data is empty."

    if contains:

        assert contains in resp.data, "Response does not contain %r" % contains

    if matches:

        reg = re.compile(matches)

        assert reg.matches(resp.data), "Response does not match %r" % matches

    if headers:

        assert_equal(resp.headers, headers)

准备好这个文件以后,你就可以为bin/app.py写自动测试代码了。创建一个叫tests/app_tests.py的新文件:

# app_tests.py

from nose.tools import * 
from bin.app import app

from tests.tools import assert_response

def test_index():
    # check that we get a 404 on the / URL
    resp = app.request("/")

    assert_response(resp, status="404")

    # test our first GET request to /hello
    resp = app.request("/hello")

    assert_response(resp)

    # make sure default values work for the form
    resp = app. request("/hello", method="POST")

    assert_response(resp, contains="Nobody")

    # test that we get expected values
    data = {'name': 'Zed', 'greet': 'Hola'}
    resp = app.request("/hello", method="POST", data=data)
    assert_response(resp, contains="Zed")

最后,使用nosetests运行这个测试脚本,测试你的web应用程序。

$ nosetests
.
---------------------------------------------------------------------------
Ran 1 test in 0.059s

OK

这里我做的就是将bin/app.py这个模块中的整个web应用程序都导入进来,然后手动运行这个web应用程序。lpthw.web有一个非常简单的API来处理请求,大致如下:

app.request(localpart='/', method='GET', data=None, host='0.0.0.0:8080', headers=None, https=False)

你可将URL作为第一个参数,然后修改request的方法、form的数据及header的内容,这样,无需启动web服务器就可以使用自动测试来测试你的web应用程序了。

为验证函数的响应,你需要使用tests.tools中定义的assert_response函数,用法如下:

assert_response(resp, contains=None, matches=None, headers=None, status="200")

把调用app.request得到的响应传递给这个函数,然后将要检查的内容作为参数传递给这个函数。你可以使用contains参数来检查响应中是否包含指定的值,使用status参数可以检查指定的响应状态。这个小函数其实包含了很多信息,所以研究一下它吧。

在tests/app_tests.py自动测试脚本中,我首先确认返回了一个404 Not Found响应,因为这个URL其实是不存在的。然后我检查了/hello在GET和POST两种请求的情况下都能正常工作。

花一些时间研究一下这个最新版的web应用程序,重点研究一下自动测试的工作原理。确认你理解了将bin/app.py作为一个模块导入然后进行自动测试的流程。

附加练习

1. 阅读与HTML相关的更多资料,为你的表单设计一个更好的输出格式。你可先在纸上设计出来,然后用HTML去实现。

2. 难题:试着研究一下如何进行文件上传,通过网页上传一张图像,然后将其保存到磁盘中。

3. 更难:找到HTTP RFC 文件(讲述HTTP工作原理的技术文件),然后尽力阅读。很无趣的文档,但有时你会用到里边的一些知识。

4. 难题:找人帮你设置一个web服务器,如Apache、Nginx或thttpd。试着让服务器伺服一下你创建的.html和.css文件。如果失败了也没关系,web服务器本就有点让人失望。

5. 歇息一下,然后尝试多创建一些web应用程序。你应仔细阅读web.py(它和lpthw.web一样)中关于回话(session)的内容,这样你就明白如何保持用户的状态信息。

常见问题回答

我看到了ImportError "No module named bin.app"。

再次说明,要么是你引用路径不对,要么是没创建bin/__init__.py文件,要么是在shell中没有设置PYTHONPATH=.。记住这些解决方案!!!其经常发生。

运行模板时发生__templates__() takes no arguments (1 given)错误。

很可能忘记在模板开头放置$def with (greeting) 或者类似的变量声明。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值