當前位置:
首頁 > 知識 > 用 Python 寫一個簡單的Web框架

用 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

()


print

"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

()


print

"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

()


print

"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發行版助力數據科學
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!