用 Python 寫一個簡單的Web框架
點擊
上方藍字
,快速關注我們)
來源:RussellLuo
www.cnblogs.com/russellluo/p/3338616.html
如有好文章投稿,請點擊 → 這裡了解詳情
一、概述
二、從demo_app開始
三、WSGI中的application
四、區分URL
五、重構
1、正則匹配URL
2、DRY
3、抽象出框架
六、參考
一、
概述
在Python中,WSGI(Web Server Gateway Interface)定義了Web伺服器與Web應用(或Web框架)之間的標準介面。在WSGI的規範下,各種各樣的Web伺服器和Web框架都可以很好的交互。
由於WSGI的存在,用Python寫一個簡單的Web框架也變得非常容易。然而,同很多其他的強大軟體一樣,要實現一個功能豐富、健壯高效的Web框架並非易事;如果您打算這麼做,可能使用一個現成的Web框架(如 Django、Tornado、web.py 等)會是更合適的選擇。
本文嘗試寫一個類似web.py的Web框架。好吧,我承認我誇大其辭了:首先,web.py並不簡單;其次,本文只重點實現了 URL調度(URL dispatch)部分。
二、從demo_app開始
首先,作為一個初步體驗,我們可以藉助 wsgiref.simple_server 來搭建一個簡單無比(trivial)的Web應用:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from
wsgiref.simple_server
import
make_server
,
demo_app
httpd
=
make_server
(
""
,
8086
,
demo_app
)
sa
=
httpd
.
socket
.
getsockname
()
"http://{0}:{1}/"
.
format
(
*
sa
)
# Respond to requests until process is killed
httpd
.
serve_forever
()
運行腳本:
$
python
code
.py
http
://
0.0.0.0
:
8086
/
打開瀏覽器,輸入http://0.0.0.0:8086/後可以看到:一行」Hello world!」 和 眾多環境變數值。
三、WSGI中的application
WSGI中規定:application是一個 可調用對象(callable object),它接受 environ 和 start_response 兩個參數,並返回一個 字元串迭代對象。
其中,可調用對象 包括 函數、方法、類 或者 具有__call__方法的 實例;environ 是一個字典對象,包括CGI風格的環境變數(CGI-style environment variables)和 WSGI必需的變數(WSGI-required variables);start_response 是一個可調用對象,它接受兩個 常規參數(status,response_headers)和 一個 默認參數(exc_info);字元串迭代對象 可以是 字元串列表、生成器函數 或者 具有__iter__方法的可迭代實例。更多細節參考 Specification Details。
The Application/Framework Side 中給出了一個典型的application實現:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
def
simple_app
(
environ
,
start_response
)
:
"""Simplest possible application object"""
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
start_response
(
status
,
response_headers
)
return
[
"Hello world!
"
]
現在用simple_app來替換demo_app:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""code.py"""
from
wsgiref.simple_server
import
make_server
from
application
import
simple_app
as
app
if
__name__
==
"__main__"
:
httpd
=
make_server
(
""
,
8086
,
app
)
sa
=
httpd
.
socket
.
getsockname
()
"http://{0}:{1}/"
.
format
(
*
sa
)
# Respond to requests until process is killed
httpd
.
serve_forever
()
運行腳本code.py後,訪問http://0.0.0.0:8086/就可以看到那行熟悉的句子:Hello world!
四、區分URL
倒騰了一陣子後,您會發現不管如何改變URL中的path部分,得到的響應都是一樣的。因為simple_app只識別host+port部分。
為了對URL中的path部分進行區分處理,需要修改application.py的實現。
首先,改用 類 來實現application:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
class
my_app
:
def
__init__
(
self
,
environ
,
start_response
)
:
self
.
environ
=
environ
self
.
start
=
start_response
def
__iter__
(
self
)
:
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Hello world!
"
然後,增加對URL中path部分的區分處理:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
class
my_app
:
def
__init__
(
self
,
environ
,
start_response
)
:
self
.
environ
=
environ
self
.
start
=
start_response
def
__iter__
(
self
)
:
path
=
self
.
environ
[
"PATH_INFO"
]
if
path
==
"/"
:
return
self
.
GET_index
()
elif
path
==
"/hello"
:
return
self
.
GET_hello
()
else
:
return
self
.
notfound
()
def
GET_index
(
self
)
:
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Welcome!
"
def
GET_hello
(
self
)
:
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Hello world!
"
def
notfound
(
self
)
:
status
=
"404 Not Found"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Not Found
"
修改code.py中的from application import simple_app as app,用my_app來替換simple_app後即可體驗效果。
五、重構
上面的代碼雖然奏效,但是在編碼風格和靈活性方面有很多問題,下面逐步對其進行重構。
1、正則匹配URL
消除URL硬編碼,增加URL調度的靈活性:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import
re
##########修改點
class
my_app
:
urls
=
(
(
"/"
,
"index"
),
(
"/hello/(.*)"
,
"hello"
),
)
##########修改點
def
__init__
(
self
,
environ
,
start_response
)
:
self
.
environ
=
environ
self
.
start
=
start_response
def
__iter__
(
self
)
:
##########修改點
path
=
self
.
environ
[
"PATH_INFO"
]
method
=
self
.
environ
[
"REQUEST_METHOD"
]
for
pattern
,
name
in
self
.
urls
:
m
=
re
.
match
(
"^"
+
pattern
+
"$"
,
path
)
if
m
:
# pass the matched groups as arguments to the function
args
=
m
.
groups
()
funcname
=
method
.
upper
()
+
"_"
+
name
if
hasattr
(
self
,
funcname
)
:
func
=
getattr
(
self
,
funcname
)
return
func
(
*
args
)
return
self
.
notfound
()
def
GET_index
(
self
)
:
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Welcome!
"
def
GET_hello
(
self
,
name
)
:
##########修改點
status
=
"200 OK"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Hello %s!
"
%
name
def
notfound
(
self
)
:
status
=
"404 Not Found"
response_headers
=
[(
"Content-type"
,
"text/plain"
)]
self
.
start
(
status
,
response_headers
)
yield
"Not Found
"
2、DRY
消除GET_*方法中的重複代碼,並且允許它們返回字元串:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import
re
class
my_app
:
urls
=
(
(
"/"
,
"index"
),
(
"/hello/(.*)"
,
"hello"
),
)
def
__init__
(
self
,
environ
,
start_response
)
:
##########修改點
self
.
environ
=
environ
self
.
start
=
start_response
self
.
status
=
"200 OK"
self
.
_headers
=
[]
def
__iter__
(
self
)
:
##########修改點
result
=
self
.
delegate
()
self
.
start
(
self
.
status
,
self
.
_headers
)
# 將返回值result(字元串 或者 字元串列表)轉換為迭代對象
if
isinstance
(
result
,
basestring
)
:
return
iter
([
result
])
else
:
return
iter
(
result
)
def
delegate
(
self
)
:
##########修改點
path
=
self
.
environ
[
"PATH_INFO"
]
method
=
self
.
environ
[
"REQUEST_METHOD"
]
for
pattern
,
name
in
self
.
urls
:
m
=
re
.
match
(
"^"
+
pattern
+
"$"
,
path
)
if
m
:
# pass the matched groups as arguments to the function
args
=
m
.
groups
()
funcname
=
method
.
upper
()
+
"_"
+
name
if
hasattr
(
self
,
funcname
)
:
func
=
getattr
(
self
,
funcname
)
return
func
(
*
args
)
return
self
.
notfound
()
def
header
(
self
,
name
,
value
)
:
##########修改點
self
.
_headers
.
append
((
name
,
value
))
def
GET_index
(
self
)
:
##########修改點
self
.
header
(
"Content-type"
,
"text/plain"
)
return
"Welcome!
"
def
GET_hello
(
self
,
name
)
:
##########修改點
self
.
header
(
"Content-type"
,
"text/plain"
)
return
"Hello %s!
"
%
name
def
notfound
(
self
)
:
##########修改點
self
.
status
=
"404 Not Found"
self
.
header
(
"Content-type"
,
"text/plain"
)
return
"Not Found
"
3、抽象出框架
為了將類my_app抽象成一個獨立的框架,需要作出以下修改:
剝離出其中的具體處理細節:urls配置 和 GET_*方法(改成在多個類中實現相應的GET方法)
把方法header實現為類方法(classmethod),以方便外部作為功能函數調用
改用 具有__call__方法的 實例 來實現application
修改後的application.py(最終版本):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""application.py"""
import
re
class
my_app
:
"""my simple web framework"""
headers
=
[]
def
__init__
(
self
,
urls
=
(),
fvars
=
{})
:
self
.
_urls
=
urls
self
.
_fvars
=
fvars
def
__call__
(
self
,
environ
,
start_response
)
:
self
.
_status
=
"200 OK"
# 默認狀態OK
del
self
.
headers
[
:
]
# 清空上一次的headers
result
=
self
.
_delegate
(
environ
)
start_response
(
self
.
_status
,
self
.
headers
)
# 將返回值result(字元串 或者 字元串列表)轉換為迭代對象
if
isinstance
(
result
,
basestring
)
:
return
iter
([
result
])
else
:
return
iter
(
result
)
def
_delegate
(
self
,
environ
)
:
path
=
environ
[
"PATH_INFO"
]
method
=
environ
[
"REQUEST_METHOD"
]
for
pattern
,
name
in
self
.
_urls
:
m
=
re
.
match
(
"^"
+
pattern
+
"$"
,
path
)
if
m
:
# pass the matched groups as arguments to the function
args
=
m
.
groups
()
funcname
=
method
.
upper
()
# 方法名大寫(如GET、POST)
klass
=
self
.
_fvars
.
get
(
name
)
# 根據字元串名稱查找類對象
if
hasattr
(
klass
,
funcname
)
:
func
=
getattr
(
klass
,
funcname
)
return
func
(
klass
(),
*
args
)
return
self
.
_notfound
()
def
_notfound
(
self
)
:
self
.
_status
=
"404 Not Found"
self
.
header
(
"Content-type"
,
"text/plain"
)
return
"Not Found
"
@
classmethod
def
header
(
cls
,
name
,
value
)
:
cls
.
headers
.
append
((
name
,
value
))
對應修改後的code.py(最終版本):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""code.py"""
from
application
import
my_app
urls
=
(
(
"/"
,
"index"
),
(
"/hello/(.*)"
,
"hello"
),
)
wsgiapp
=
my_app
(
urls
,
globals
())
class
index
:
def
GET
(
self
)
:
my_app
.
header
(
"Content-type"
,
"text/plain"
)
return
"Welcome!
"
class
hello
:
def
GET
(
self
,
name
)
:
my_app
.
header
(
"Content-type"
,
"text/plain"
)
return
"Hello %s!
"
%
name
if
__name__
==
"__main__"
:
from
wsgiref.simple_server
import
make_server
httpd
=
make_server
(
""
,
8086
,
wsgiapp
)
sa
=
httpd
.
socket
.
getsockname
()
"http://{0}:{1}/"
.
format
(
*
sa
)
# Respond to requests until process is killed
httpd
.
serve_forever
()
當然,您還可以在code.py中配置更多的URL映射,並實現相應的類來對請求作出響應。
六、參考
本文主要參考了 How to write a web framework in Python(作者 anandology 是web.py代碼的兩位維護者之一,另一位則是大名鼎鼎卻英年早逝的 Aaron Swartz),在此基礎上作了一些調整和修改,並摻雜了自己的一些想法。
如果您還覺得意猶未盡,Why so many Python web frameworks? 也是一篇很好的文章,也許它會讓您對Python中Web框架的敬畏之心蕩然無存:-)
看完本文有收穫?請轉
發分享給更多人
關注「Python開發者」,提升Python技能


※如何用Python對數據進行差分
※英特爾Python發行版助力數據科學
※Python 的正則表達式彩蛋
※tomd: 用Python將HTML轉換回Markdown
※每月好書:? 深入淺出深度學習:原理剖析與Python實踐
TAG:Python |
※使用 Python 和 Pygame 模塊構建一個遊戲框架
※Python Web 應用程序 Tornado 框架簡介
※原創:用python web框架 bottle 開發網站一
※DeBug Python代碼全靠print函數?換用這個一天2K+Star的工具吧
※在 Kubernetes 上運行一個 Python 應用程序
※用Python做一個翻譯軟體
※用 Python 編寫的 Python 解釋器
※一文概述用 python 的 scikit-image 模塊進行圖像分割
※分析了 Stack Overflow、Reddit等9 個榜單,Python 第一的地位穩了!
※使用Python和Gitlab創建一個票證系統
※從零到一,使用 VS Code寫Python
※三步教你如何使用RT-Thread MicroPython搭建一個Web伺服器
※MongoDB Python官方驅動 PyMongo 的簡單封裝
※10分鐘學會用python寫遊戲!Python其實很簡單!
※使用 Redis 和 Python 構建一個共享單車的應用程序
※Python小技巧:QPython,一個在手機上運行Python的神器
※Python爬蟲框架之pyspider
※使用Python實現一個簡易Http伺服器
※API Star:一個 Python 3 的 API 框架
※iPhone X 的新解鎖技術:用 Python 編寫 Face ID!