Python學習--20 Web開發


HTTP格式

HTTP協議是基於TCP和IP協議的。HTTP協議是一種文本協議。

每個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。

HTTP請求格式:

GET:

GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

POST:

POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

Header部分每行用\r\n換行,每行里鍵名和鍵值之間以: 分割,注意冒號后有個空格。

當遇到\r\n\r\n時,Header部分結束,后面的數據全部是Body。

HTTP響應格式:

200 OK
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

HTTP響應如果包含body,也是通過\r\n\r\n來分隔的。

請再次注意,Body的數據類型由Content-Type頭來確定,如果是網頁,Body就是文本,如果是圖片,Body就是圖片的二進制數據。

Body數據是可以被壓縮的,如果看到Content-Encoding,說明網站使用了壓縮。最常見的壓縮方式是gzip。

WSGI接口

了解了HTTP協議的格式后,我們可以理解一個Web應用的本質:
1、瀏覽器發送HTTP請求給服務器;
2、服務器接收請求后,生成HTML;
3、服務器把生成的HTML作為HTTP響應的body返回給瀏覽器;
4、瀏覽器接收到HTTP響應后,解析HTTP里body並顯示。

接受HTTP請求、解析HTTP請求、發送HTTP響應實現起來比較復雜,有專門的服務器軟件來實現,例如Nginx,Apache。我們要做的就是專注於生成HTML文檔。

Python里也提供了一個比較底層的WSGI(Web Server Gateway Interface)接口來實現TCP連接、HTTP原始請求和響應格式。實現了該接口定義的內容,就可以實現類似Nginx、Apache等服務器的功能。

WSGI接口定義要求Web開發者實現一個函數,就可以響應HTTP請求,示例:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello, web!</h1>']

這是一個簡單的文本版本的Hello, web!

上面的application()函數就是符合WSGI標准的一個HTTP處理函數,它接收兩個參數:

environ:一個包含所有HTTP請求信息的dict對象;
start_response:一個發送HTTP響應的函數。

有了WSGI,我們關心的就是如何從environ這個dict對象拿到HTTP請求信息,然后構造HTML,通過start_response()發送Header,最后返回Body。

整個application()函數本身沒有涉及到任何解析HTTP的部分,即底層代碼不需要自己編寫,只負責在更高層次上考慮如何響應請求就可以了。

但是,application()函數由誰來調用呢?因為這里的參數environstart_response我們沒法提供,返回的bytes也沒法發給瀏覽器。

application()函數必須由WSGI服務器來調用。

有很多符合WSGI規范的服務器,Python提供了一個最簡單的WSGI服務器,可以把我們的Web應用程序跑起來。這個模塊叫wsgiref,它是用純Python編寫的WSGI服務器的參考實現。所謂“參考實現”是指該實現完全符合WSGI標准,但是不考慮任何運行效率,僅供開發和測試使用。

運行WSGI服務

有了wsgiref,我們可以非常快的實現一個簡單的web服務器:

# coding: utf-8

from wsgiref.simple_server import make_server

def application(environ, start_response):
    print(environ)
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'<h1>Hello web!</h1>']

print('HTTP server is running on http://127.0.0.1:9999')

# 創建一個服務器,IP地址可以為空,端口是9999,處理函數是application:
httpd = make_server('', 9999, application)
httpd.serve_forever()

運行后訪問http://127.0.0.1:9999/,會看到:

Hello web!

擴展知識:
make_server()里第一個參數如果為空,實際等效於0.0.0.0 ,表示監聽本地所有ip地址(包括127.0.0.1)。

通過Chrome瀏覽器的控制台,我們可以查看到瀏覽器請求和服務器響應信息:

# 請求信息:
GET / HTTP/1.1
Host: 127.0.0.1:9999
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: _ga=GA1.1.948200530.1463673425

# 響應信息:
HTTP/1.0 200 OK
Date: Sun, 12 Feb 2017 05:20:31 GMT
Server: WSGIServer/0.2 CPython/3.4.3
Content-Type: text/html
Content-Length: 19

<h1>Hello web!</h1>

我們再看終端的輸出信息:

$ python user_wsgiref_server.py
HTTP server is running on http://127.0.0.1:9999
127.0.0.1 - - [12/Feb/2017 13:18:38] "GET / HTTP/1.1" 200 19
127.0.0.1 - - [12/Feb/2017 13:18:39] "GET /favicon.ico HTTP/1.1" 200 19

如果我們打印environ參數信息,會看到如下值:

{
    "SERVER_SOFTWARE": "WSGIServer/0.1 Python/2.7.5",
    "SCRIPT_NAME": "",
    "REQUEST_METHOD": "GET",
    "SERVER_PROTOCOL": "HTTP/1.1",
    "HOME": "/root",
    "LANG": "en_US.UTF-8",
    "SHELL": "/bin/bash",
    "SERVER_PORT": "9999",
    "HTTP_HOST": "dev.banyar.cn:9999",
    "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
    "XDG_SESSION_ID": "64266",
    "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "wsgi.version": "0",
    "wsgi.errors": "",
    "HOSTNAME": "localhost",
    "HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.8,en;q=0.6",
    "PATH_INFO": "/",
    "USER": "root",
    "QUERY_STRING": "",
    "PATH": "/usr/local/php/bin:/usr/local/php/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin",
    "HTTP_USER_AGENT": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
    "HTTP_CONNECTION": "keep-alive",
    "SERVER_NAME": "localhost",
    "REMOTE_ADDR": "192.168.0.101",
    "wsgi.url_scheme": "http",
    "CONTENT_LENGTH": "",
    "GATEWAY_INTERFACE": "CGI/1.1",
    "CONTENT_TYPE": "text/plain",
    "REMOTE_HOST": "",
    "HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch"
}

為顯示方便,已精簡部分信息。有了環境變量信息,我們可以對程序做些修改,可以動態顯示內容:

def application(environ, start_response):
    print(environ['PATH_INFO'])
    start_response('200 OK', [('Content-Type', 'text/html')])
    body = '<h1>Hello %s!</h1>'  % (environ['PATH_INFO'][1:] or 'web' )
    return [body.encode('utf-8')]

以上使用了environ里的PATH_INFO的值。我們在瀏覽器輸入http://127.0.0.1:9999/python,瀏覽器會顯示:

Hello python!

終端的輸出信息:

$ python user_wsgiref_server.py
HTTP server is running on http://127.0.0.1:9999
/python
127.0.0.1 - - [12/Feb/2017 13:54:57] "GET /python HTTP/1.1" 200 22
/favicon.ico
127.0.0.1 - - [12/Feb/2017 13:54:58] "GET /favicon.ico HTTP/1.1" 200 27

web框架

實際項目開發中,我們不可能使用swgiref來實現服務器,因為WSGI提供的接口雖然比HTTP接口高級了不少,但和Web App的處理邏輯比,還是比較低級。我們需要使用成熟的web框架。

由於用Python開發一個Web框架十分容易,所以Python有上百個開源的Web框架。部分流行框架:

Flask:輕量級Web應用框架;
Django:全能型Web框架;
web.py:一個小巧的Web框架;
Bottle:和Flask類似的Web框架;
Tornado:Facebook的開源異步Web框架

Flask

Flask是一個使用 Python 編寫的輕量級 Web 應用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎則使用 Jinja2 。

安裝非常簡單:

pip install flask

控制台輸出:

Collecting flask
  Downloading Flask-0.12-py2.py3-none-any.whl (82kB)
    100% |████████████████████████████████| 92kB 163kB/s
Collecting itsdangerous>=0.21 (from flask)
  Downloading itsdangerous-0.24.tar.gz (46kB)
    100% |████████████████████████████████| 51kB 365kB/s
Collecting click>=2.0 (from flask)
  Downloading click-6.7-py2.py3-none-any.whl (71kB)
    100% |████████████████████████████████| 71kB 349kB/s
Collecting Jinja2>=2.4 (from flask)
  Downloading Jinja2-2.9.5-py2.py3-none-any.whl (340kB)
    100% |████████████████████████████████| 348kB 342kB/s
Collecting Werkzeug>=0.7 (from flask)
  Downloading Werkzeug-0.11.15-py2.py3-none-any.whl (307kB)
    100% |████████████████████████████████| 317kB 194kB/s
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
  Downloading MarkupSafe-0.23.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
  Running setup.py bdist_wheel for itsdangerous ... done
Successfully built itsdangerous MarkupSafe
Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.9.5 MarkupSafe-0.23 Werkzeug-0.11.15 click-6.7 flask-0.12 itsdangerous-0.24

安裝完flask會同時安裝依賴模塊:itsdangerous, click, MarkupSafe, Jinja2, Werkzeug

現在我們來寫個簡單的登錄功能,主要是三個頁面:

  • 首頁,顯示home字樣;
  • 登錄頁,地址/login,有登錄表單;
  • 登錄后的歡迎頁面,如果登錄成功,提示歡迎語,否則提示用戶名不正確。

那么一共有3個URL:

  • GET /:首頁,返回Home;
  • GET /login:登錄頁,顯示登錄表單;
  • POST /login:處理登錄表單,顯示登錄結果。

user_flask_app.py

# 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><p><a href="/login">去登錄</a></p>'

# 登錄頁
@app.route('/login', methods=['get'])
def login():
    return '''<form action="/login" method="post">
              <p>用戶名:<input name="username"></p>
              <p>密碼:<input name="password" type="password"></p>
              <p><button type="submit">登錄</button></p>
              </form>'''

# 登錄頁處理
@app.route('/login', methods=['post'])
def do_login():
    # 從request對象讀取表單內容:
    param = request.form
    if(param['username'] == 'yjc' and param['password'] == 'yjc'):
        return '歡迎您 %s !' % param['username']
    else:
        return '用戶名或密碼不正確。'
    pass

if __name__ == '__main__':
    # run()方法參數可以都為空,使用默認值
    app.run('', 5000)

我們可以打開:http://localhost:5000/ 看效果。實際的Web App應該拿到用戶名和口令后,去數據庫查詢再比對,來判斷用戶是否能登錄成功。

通過代碼我們可以發現,Flask通過Python的裝飾器在內部自動地把URL和函數給關聯起來。

注意代碼里同一個URL/login分別有GETPOST兩種請求,可以映射到兩個處理函數中。

使用模板

Web框架讓我們從編寫底層WSGI接口拯救出來了,極大的提高了我們編寫程序的效率。

但代碼里嵌套太多的html讓整個代碼易讀性變差,使程序變得復雜。我們需要將后端代碼邏輯與前端html分離出來。這就是傳說中的MVC:Model-View-Controller,中文名“模型-視圖-控制器”。

Controller負責業務邏輯,比如檢查用戶名是否存在,取出用戶信息等等;

View負責顯示邏輯,通過簡單地替換一些變量,View最終輸出的就是用戶看到的HTML。

'Model'負責數據的獲取,如從數據庫查詢用戶信息等。Model簡單可以理解為數據。

那么就是:Model獲取數據,Controlle處理業務邏輯,View顯示數據。

現在,我們把上次直接輸出字符串作為HTML的例子用MVC模式改寫一下:

# coding: utf-8

from flask import Flask,request,render_template

app = Flask(__name__)

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

# 登錄頁
@app.route('/login', methods=['get'])
def login():
    return render_template('login.html', param = [])

# 登錄頁處理
@app.route('/login', methods=['post'])
def do_login():
    param = request.form
    if(param['username'] == 'yjc' and param['password'] == 'yjc'):
        return render_template('welcome.html', username = param['username'])
    else:
        return render_template('login.html', msg = '用戶名或密碼不正確。', param = param)
    pass

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

Flask通過render_template()函數來實現模板的渲染。和Web框架類似,Python的模板也有很多種。Flask默認支持的模板是jinja2

模板頁面:
home.html

<h1>Home</h1><p><a href="/login">去登錄</a></p>

login.html

{% if msg %}
<p style="color:red;">{{ msg }}</p>
{% endif %}
<form action="/login" method="post">
    <p>用戶名:<input name="username" value="{{ param.username }}"></p>
    <p>密碼:<input name="password" type="password"></p>
    <p><button type="submit">登錄</button></p>
</form>

welcome.html

<p>歡迎您, {{ username }} !</p>

項目目錄:

user_flask_app
    |-- templates
        |-- home.html
        |-- login.html
        |-- welcome.html
    |-- user_flask_app.py

render_template()函數第一個參數是模板名,默認是templates目錄下。后面的參數是傳給模板的變量。變量的值可以是數字、字符串、列表等等。

在Jinja2模板中,我們用{{ name }}表示一個需要替換的變量。很多時候,還需要循環、條件判斷等指令語句,在Jinja2中,用{% ... %}表示指令。

比如循環輸出頁碼:

{% for i in page_list %}
    <a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}

除了Jinja2,常見的模板還有:

Mako:用<% ... %>和${xxx}的一個模板;
Cheetah:也是用<% ... %>和${xxx}的一個模板;
Django:Django是一站式框架,內置一個用{% ... %}和{{ xxx }}的模板。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM