美文网首页python
Python入门(二十)——Web开发

Python入门(二十)——Web开发

作者: 120c06518fa0 | 来源:发表于2018-03-28 21:56 被阅读93次

Web开发主要经历了下面几个阶段:

  • 静态Web页面:由文本编辑器直接编辑并生成静态的HTML页面,如果要修改Web页面的内容,就需要再次编辑HTML源文件,早期的互联网Web页面就是静态的。

  • CGI:由于静态Web页面无法与用户交互,比如用户填写了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了CGI(Common Gateway Interface,通用网关接口),CGI是使用多进程来服务URL请求,资源占用很严重,主要使用C/C++编写?。

  • ASP/JSP/PHP:由于Web应用特点是修改频繁,用C/C++这样的低级语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。

  • MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了MVC(Model–View–Controller)模式,来简化Web开发。ASP发展为ASP.NET,JSP和PHP也有一大堆MVC框架。

新的Web开发技术不断发展出来,例如MMVM(Model–View–ViewModel)、异步编程等。

Python比Web早诞生,作为一种解释型的脚本语言,开发效率高,非常适合用来做Web开发。Python已经有上百种Web开发框架,有很多成熟的模板技术,选择Python开发Web应用,不但开发效率高,而且运行速度快。

HTTP协议

参考:

在前面的网络编程一节中,我们其实已经知道了HTTP请求和响应的格式和数据。只不过当时是使用socket这一底层网络接口实现向新浪服务器发送和接收请求。现在我们只不过是通过浏览器(客户端)访问http://www.sina.com.cn/来看看期间发生的网络请求。打开Chrome,按F12监听网络请求。

一次HTTP请求只请求一个资源,如果请求的资源中有URL(超链接),浏览器会自动地发起请求该URL的资源,即又构造一次HTTP请求,复杂的网页往往需要包含很多次HTTP请求,这些请求并不一定是请求同一服务器,往往可能是多个服务器和服务商,比如CDN。从而达到分流、减轻服务器压力的效果。

我们看看第一个HTTP请求,即访问http://www.sina.com.cn/时发生的HTTP请求和响应。网络数据是字节流bytes(在网络编程一节可以知道)。下面的数据是bytes解码后的字符串。

请求:

GET / HTTP/1.1
Host: www.sina.com.cn
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: 【敏感内容】


响应:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 11 Mar 2018 07:18:39 GMT
Content-Type: text/html
Content-Length: 133943
Connection: keep-alive
Last-Modified: Sun, 11 Mar 2018 07:14:59 GMT
Vary: Accept-Encoding
X-Powered-By: shci_v1.03
Expires: Sun, 11 Mar 2018 07:18:56 GMT
Cache-Control: max-age=60
Content-Encoding: gzip
Age: 43
Via: http/1.1 ctc.ningbo.ha2ts4.97 (ApacheTrafficServer/6.2.1 [cMsSfW]), http/1.1 ctc.ningbo.ha2ts4.106 (ApacheTrafficServer/6.2.1 [cHs f ])
X-Via-Edge: 152075271975197eeea7aeebeee734371299a
X-Cache: HIT.106
X-Via-CDN: f=edge,s=ctc.ningbo.ha2ts4.103.nb.sinaedge.com,c=【敏感内容】;f=Edge,s=ctc.ningbo.ha2ts4.106,c=【敏感内容】

<!DOCTYPE html>
<!-- [ published at 2018-03-11 15:15:14 ] -->
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>新浪首页</title>
    <meta name="keywords" content="新浪,新浪网,SINA,sina,sina.com.cn,新浪首页,门户,资讯" />
【省略余下的网页源码内容(HTML网页)】

请求由如下部分组成:

  • 请求行:<方法> <资源路径> <协议版本>\r\n
    方法有8种:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS、CONNECT,区分大小写。
    资源路径必须以/开头,注意,如果是GET请求,Query string是放在这里:/path/to/resource/?key1=value1&key2=value2
    协议版本有目前有:HTTP/0.9、HTTP/1.0、HTTP/1.1(广泛使用)和HTTP/2(2015年5月正式发布标准)。

  • 请求头<key>: <value>\r\n,往往包含多个。在HTTP/1.1中,只有Host: <host-name>\r\n是必需,其他均可选。

  • 空行:\r\n\r\n

  • 请求体:如果是GET请求,请求体通常是空的。如果是POST请求,Query string是放在这里:key1=value1&key2=value2

响应由如下部分组成:

  • 状态行:<协议版本> <HTTP状态码> <HTTP状态码描述>\r\n
    协议版本同上。
    HTTP状态码参见HTTP状态码,三位数,有3大类:1xx消息、2xx成功、3xx重定向、4xx客户端错误和5xx服务器错误。
    HTTP状态码描述参见HTTP状态码

  • 响应头<key>: <value>\r\n,往往包含多个。

  • 空行:\r\n\r\n

  • 响应体,通常是HTML网页源码、JS/CSS等资源原文。

注意,字节流传输过程中,请求和响应的一行必须以\r\n结尾!

To be continued. 😝

WSGI接口

WSGI(Python Web Server Gateway Interface,Web服务器网关接口),为Python语言定义的Web服务器Web应用程序或框架之间的一种简单而通用的接口。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。

在下面的代码中,我们实现了简单的Web应用程序,响应了来自客户端(比如浏览器)的HTTP请求。但我们仅仅是在“高层”处理HTTP请求,并没有像前面TCP一章中那样解析和处理HTTP请求报文和响应报文的具体细节(繁琐😭)。这些细节我们通过现成的HTTP服务器来实现。这些HTTP服务器必须符合上面的WSGI规范,它们处理了底层的HTTP协议报文。我们启动这样的HTTP服务器,传入我们的Web应用程序,让HTTP服务器调用我们的Web应用程序和使用里面的参数值,这样就可以完整地处理HTTP请求了。

符合WSGI规范的HTTP服务器有很多,这里展示了实现了该规范的Python HTTP服务器。Python内置了一个实现WSGI规范的HTTP服务器,wsgiref - WSGI Utilities and Reference Implementation模块就是WSGI规范的参考实现。

参考实现意思是该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。

Web应用程序是函数的栗子:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from wsgiref.simple_server import make_server
import urllib

def application(environ, start_response):
    for k, v in environ.items():
        print('%s = %s' % (k, v))

    start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
    
    kvs = urllib.parse.parse_qs(environ['QUERY_STRING'])
    name = kvs['name'][0] if 'name' in kvs.keys() and len(kvs['name']) and kvs['name'][0] else 'Tim'
    
    body = '<h1>Hello, %s!</h1>' % name
    
    return [body.encode('utf-8')]

with make_server('', 8000, application) as httpd:
    print('Serving HTTP on port 8000...')
    httpd.serve_forever()

Web应用程序是类的栗子:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from wsgiref.simple_server import make_server
import urllib

class Application(object):
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response

    def __iter__(self):
        for k, v in self.environ.items():
            print('%s = %s' % (k, v))

        self.start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])

        kvs = urllib.parse.parse_qs(self.environ['QUERY_STRING'])
        name = kvs['name'][0] if 'name' in kvs.keys() and len(kvs['name']) and kvs['name'][0] else 'Tim'
    
        body = '<h1>Hello, %s!</h1>' % name
    
        yield body.encode('utf-8')

with make_server('', 8000, Application) as httpd:
    print('Serving HTTP on port 8000...')
    httpd.serve_forever()

wsgiref.simple_server.make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler)创建Web服务器。app必须是一个WSGI application object,定义在这里,类型可以是个函数或者是个类。详细定义和用法在这里server_class定义了该函数的返回值的类型,默认是wsgiref.simple_server.WSGIServer类型。handler_class定义了处理请求的类,默认是wsgiref.simple_server.WSGIRequestHandler

上面的wsgiref.simple_server.make_server函数返回的wsgiref.simple_server.WSGIServer其实是http.server.HTTPServer的子类。http.server.HTTPServer又是socketserver.TCPServer的子类,socketserver.TCPServer又是socketserver.BaseServer的子类。socketserver.BaseServer提供了serve_forever(poll_interval=0.5)方法,该方法开启循环处理请求,直到shutdown()被调用,默认每0.5秒轮询一次是否已经关闭。

上面的app对象就是遵循PEP 3333 -- Python Web Server Gateway Interface v1.0.1规范的WSGI application objectPEP 3333PEP 333 -- Python Web Server Gateway Interface v1.0的更新版,为改进在Python 3下的可用性细微地修改了规范。

根据规范,app是个函数或者类,一般定义成函数def simple_app(environ, start_response)比较简洁,参数用法如下:

  • environ:类型必须是python内建的dict类型,不能是其子类或其他类型。包含了HTTP请求的信息,即CGI-style environment variables

  • start_response:类型是函数,格式为start_response(status, response_headers, exc_info=None)。是发送HTTP响应码和响应头的处理函数。status是HTTP响应码,类似于200 OKresponse_headerslist类型的响应头,list里面是均为str(header_name, header_value)元组类型。exc_info可选。一般来说,Content-Type等是需要发送给客户端(浏览器)的。

定义成类的app请参考上面的栗子写法。

注意,这两个参数environstart_response都是外部传进来的!即make_server创建的WSGI服务器会去调用这个app,并为其传入这2个参数。

app最终需要发送响应体,类型必须是可以迭代(iterable)的对象。如果app是函数,则可以返回types类型的list。如果app是类型,则重写类的__iter__方法,在方法内yieldbytes类型。

浏览器访问http://localhost:8000/?name=Jack,可以看到页面输出Hello, Jack!

控制台执行结果:

Serving HTTP on port 8000...
【省略本机环境变量的输出,即执行`env`命令的结果】
SERVER_NAME = TIM-MAC.local
GATEWAY_INTERFACE = CGI/1.1
SERVER_PORT = 8000
REMOTE_HOST =
CONTENT_LENGTH =
SCRIPT_NAME =
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = WSGIServer/0.2
REQUEST_METHOD = GET
PATH_INFO = /
QUERY_STRING = name=Jack
REMOTE_ADDR = 127.0.0.1
CONTENT_TYPE = text/plain
HTTP_HOST = localhost:8000
HTTP_UPGRADE_INSECURE_REQUESTS = 1
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HTTP_USER_AGENT = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4
HTTP_ACCEPT_LANGUAGE = zh-cn
HTTP_ACCEPT_ENCODING = gzip, deflate
HTTP_CONNECTION = keep-alive
wsgi.input = <_io.BufferedReader name=4>
wsgi.errors = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
wsgi.version = (1, 0)
wsgi.run_once = False
wsgi.url_scheme = http
wsgi.multithread = True
wsgi.multiprocess = False
wsgi.file_wrapper = <class 'wsgiref.util.FileWrapper'>
127.0.0.1 - - [17/Mar/2018 13:17:44] "GET /?name=Jack HTTP/1.1" 200 21

从中可以清晰地看到包含的环境变量和WSGI服务器日志的输出。

使用Web框架

处理HTTP请求就是根据请求方式environ['REQUEST_METHOD ']、请求URLenviron['PATH_INFO']和查询字符串environ['QUERY_STRING']等的不同去编写对应的处理函数。如果自己一个一个去判断这些入参的不同未免太麻烦了。我们需要上框架。

常见的Python Web框架有:

  • Django:全能型的开源框架,Model-View-Template架构模式(这里的V相当于MVC中的C,而这里的T相当于MVC中的V)。
  • Flask:轻量级开源微框架,核心简单,使用extensions扩展功能。
  • Tornado:被Facebook收购后的开源框架,轻量级的高性能的异步非阻塞IO处理模式。

这里选用Flask框架。

安装:使用pip3 install flask

(py3venv) Tim@TIM-MAC ~/project/python $ pip3 install flask
Collecting flask
  Downloading Flask-0.12.2-py2.py3-none-any.whl (83kB)
    61% |███████████████████▊            | 51kB 134kB/s eta 0:00    74% |███████████████████████▊        | 61kB 160kB/s eta     86% |███████████████████████████▋    | 71kB 172kB/s     98% |███████████████████████████████▋| 81kB 197k    100% |████████████████████████████████| 92kB 217kB/s
Collecting Jinja2>=2.4 (from flask)
  Downloading Jinja2-2.10-py2.py3-none-any.whl (126kB)
    56% |██████████████████▏             | 71kB 1.2MB/s eta 0:00:    64% |████████████████████▊           | 81kB 539kB/s eta 0:0    72% |███████████████████████▍        | 92kB 605kB/s eta     81% |██████████████████████████      | 102kB 627kB/s e    89% |████████████████████████████▌   | 112kB 627kB/    97% |███████████████████████████████▏| 122kB 637    100% |████████████████████████████████| 133kB 595kB/s
Collecting Werkzeug>=0.7 (from flask)
  Using cached Werkzeug-0.14.1-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask)
  Downloading itsdangerous-0.24.tar.gz (46kB)
    66% |█████████████████████▏          | 30kB 7.0MB/s eta 0:    88% |████████████████████████████▏   | 40kB 4.2MB/s    100% |████████████████████████████████| 51kB 3.4MB/s
Collecting click>=2.0 (from flask)
  Using cached click-6.7-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
  Downloading MarkupSafe-1.0.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
  Running setup.py bdist_wheel for itsdangerous ... done
  Stored in directory: /Users/Tim/Library/Caches/pip/wheels/fc/a8/66/24d655233c757e178d45dea2de22a04c6d92766abfb741129a
  Running setup.py bdist_wheel for MarkupSafe ... done
  Stored in directory: /Users/Tim/Library/Caches/pip/wheels/88/a7/30/e39a54a87bcbe25308fa3ca64e8ddc75d9b3e5afa21ee32d57
Successfully built itsdangerous MarkupSafe
Installing collected packages: MarkupSafe, Jinja2, Werkzeug, itsdangerous, click, flask
Successfully installed Jinja2-2.10 MarkupSafe-1.0 Werkzeug-0.14.1 click-6.7 flask-0.12.2 itsdangerous-0.24
You are using pip version 9.0.1, however version 9.0.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
    return '<h1>Home</h1>'

@app.route('/signin', methods=['GET'])
def signin_form():
    return '''<form action="/signin" method="post">
                  <p><input name="username" /></p>
                  <p><input name="password" type="password" /></p>
                  <p><button type="submit">Sign In</button></p>
              </form>'''

@app.route('/signin', methods=['POST'])
def signin():
    if request.form['username'] == 'admin' and request.form['password'] == 'password':
        return '<h3>Hello, admin!</h3>'
    return '<h3>Incorrect username or password.</h3>'

if __name__ == '__main__':
    app.run('localhost', 5000)

可以看到,我们输入响应体时,直接以str类型输出,再也不用encode('utf-8')进行显式地编码了,十分方便。

flask.Flask(import_name, static_path=None, static_url_path=None, static_folder='static', template_folder='templates', instance_path=None, instance_relative_config=False)创建一个符合WSGI application的Flask对象,import_name用于指定哪个模块文件或者包是属于该Web应用程序。对于单文件通常是__name__,对于package,通常是包名。static_url_path表示静态文件夹在URL中显示的名字,默认为第四个参数即静态文件夹的名字。static_folder表示存放静态文件的文件夹名,默认为static,且位于应用程序根目录。template_folder表示存放模板文件的文件夹名,默认为templates,且位于应用程序根目录。

Flask的路由十分优雅😁。使用route(rule, **options)装饰器把函数绑定到URL上。等价于add_url_rule(*args, **kwargs)url是URL字符串。options额外选项可以指定类型为字符串列表的methods等。对于带参数的URL绑定和其他用法,详见URL路由注册

run(host=None, port=None, debug=None, **options)用于在本地开发环境运行应用程序。host默认是'127.0.0.1'port默认是5000debug默认为False,如果debug为True,则会自动加载源文件变化以及在出现异常时显示调试器。

要获取表单里面的值,使用Flask提供的全局对象flask.request,来获得当前进入请求的数据(保证在多线程下始终获得当前请求的请求数据)。该全局对象是一个代理,代理了flask.Request(environ, populate_request=True, shallow=False)这一请求对象。也就是说最终是通过该全局对象访问被代理对象的各种属性。比如通过flask.request.form访问表单数据。

此时可以访问http://localhost:5000/http://localhost:5000/signin测试效果。

控制台执行结果:

 * Running on http://localhost:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [17/Mar/2018 16:21:41] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2018 16:21:44] "GET /signin HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2018 16:21:48] "POST /signin HTTP/1.1" 200 -
127.0.0.1 - - [17/Mar/2018 16:21:53] "POST /signin HTTP/1.1" 200 -

使用模板引擎

在上一节中,使用(多行)字符串直接在.py文件编写HTML,对于大篇幅网页显然不现实,需要使用模板来渲染网页。即采用MVC模式,C是绑定到URL的处理函数,V是模板文件即包含变量占位符的HTML网页。M则是处理函数在渲染模板时传入的变量,大部分Web框架都支持在渲染模板时传入关键字参数,接着在框架内部统一组装成一个dict作为Model传给模板。

Web开发中的MVC模式.png

常见的Python模板引擎有:

  • Jinja2:优雅的全功能的支持Unicode的模板引擎。使用{% ... %}{{ xxx }}
  • Maco:轻量级的模板引擎。使用<% ... %>${ xxx }
  • Cheetah:使用<% ... %>${ xxx }
  • Django:一站式框架。使用{% ... %}{{ xxx }}

我们使用Jinja2。不仅是因为功能齐全,也因为运行 Flask 本身仍然需要 Jinja2 依赖 ,这对启用富扩展是必要的,扩展可以依赖 Jinja2 存在。

安装Flask默认已经了Jinja2,如果要独立安装,使用pip3 install Jinja2

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request
from flask import render_template

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
    return render_template('home.html')

@app.route('/signin', methods=['GET'])
def signin_form():
    return render_template('form.html')

@app.route('/signin', methods=['POST'])
def signin():
    username = request.form['username']
    password = request.form['password']
    if username == 'admin' and password == 'password':
        return render_template('signin-ok.html', username=username)
    return render_template('form.html', message='Incorrect username or password', username=username)

if __name__ == '__main__':
    app.run('localhost', 5000)

flask.render_template(template_name_or_list, **context)用来渲染模板。template_name_or_list表示模板文件或模板文件列表,如果是列表则只渲染列表中第一个存在的模板文件。模板文件是从模板文件夹中查找。context就是模板文件中需要用到的变量(Model)。

由于app = Flask(__name__)没有显式指定模板文件夹路径,即默认为/templates/。于是模板文件必须放在应用程序根目录下面的templates文件夹中。

(py3venv) Tim@TIM-MAC ~/project/python/templates $ touch home.html
(py3venv) Tim@TIM-MAC ~/project/python/templates $ touch form.html
(py3venv) Tim@TIM-MAC ~/project/python/templates $ touch signin-ok.html
应用程序目录结构.png

home.html文件内容如下:

<html>
<head>
    <title>Home</title>
</head>
<body>
    <h1 style="font-style:italic">Home</h1>
</body>
</html>

form.html文件内容如下:

<html>
<head>
  <title>Please Sign In</title>
</head>
<body>
    {% if message %}
    <p style="color:red">{{ message }}</p>
    {% endif %}
    <form action="/signin" method="post">
        <legend>Please sign in:</legend>
        <p><input name="username" placeholder="Username" value="{{ username }}" /></p>
        <p><input name="password" placeholder="Password" type="password" /></p>
        <p><button type="submit">Sign In</button></p>
    </form>
</body>
</html>

signin-ok.html文件内容如下:

<html>
<head>
    <title>Welcome, {{ username }}</title>
</head>
<body>
    <p>Welcome, {{ username }}!</p>
</body>
</html>

此时再去访问http://localhost:5000/http://localhost:5000/signin测试效果。

Jinja2模板引擎功能齐全,比如还可以使用{% ... %}完成判断和循环等功能,从而控制DOM元素的输出。

相关文章

网友评论

    本文标题:Python入门(二十)——Web开发

    本文链接:https://www.haomeiwen.com/subject/ubnsnxtx.html