web开发
cs即客户端client和server服务器端编程;
客户端和服务器之间要使用socket,约定协议、版本(往往使用的是tcp或者udp协议),指定端口就可以通信了。
客户端、服务器端传输数据,数据需要有一定格式,这是双方约定的协议。
bs编程,即browser、server开发。
browser指浏览器,是一种特殊的客户端,支持http(s)协议,能够通过url向服务器端发起请求,等待服务端返回html等数据,并在浏览器内可视化展示。
server,支持http(s)协议,能够接受众多客户端发起的http协议请求,经过处理,将html等数据返回给浏览器。
bs分为两端开发:
客户端开发,简称前端开发。html,css,javascript等;
服务端开发,python有wsgi,django,flask,tornado等;
本质上说,bs是一种特殊的cs,即客户端必须是一种支持http协议且能够解析并渲染html的软件,服务端必须是能够接收多客户端http访问的服务器软件。
根据tcp/ip(transmission control protocol
/internet protocol)网络协议,http协议底层基于tcp协议实现。并且是应用层面的协议:
http协议是无状态协议:即客户端两次请求之间没有任何关系,服务端并不知道前后两次请求是否来自同一客户端。(主要原因是创建http协议之初,并没有像现在这样丰富的数据,当时的需求很单一)。
cookie:键值对信息,浏览器每发起一次请求时,都会把cookie信息发送给服务器端。是一种客户端和服务器端传递数据的技术,服务器通过判断这些信息,来确定请求之间是否有联系。一般来说cookie是服务端生成,返回给客户端,客户端需要保存并且在下一次连接时返回的。但是客户端可以修改cookie,因此往往会在cookie上进行不可逆加密。
url组成:
url就是地址,如www.baidu.com(schema://host[:port]/path/…/[;url-params][?query-string][#anchor]),每一个连接指向一个资源供给客户端访问。
schema,协议类型如http(s),ftp,file,mailto等。
host:port ,如百度网站等域名,通过dns域名解析成ip才能使用,如果是80端口则可以不写,解析后对返回的ip的tcp的80端口发起访问。
/path,指向服务器资源的路径,可以为逻辑地址。
[?query-string],查询字符串,以问号与前面的路径分割,后面是key-value模式,使用&符号分割。
[#anchor],锚点或者标记,作为锚点就是网页内定位用;作为标记就是网页外做链接用(类似于word的超链接)。
http消息
消息分为request,response。
request:客户端(可以是浏览器)向服务器发起的请求;
response:服务器对客户端请求的回应;
它们都是由,请求行、header消息报头、body消息正文组成。
请求
请求消息行:请求方法method 请求路径 协议版本(crlf)
get / http/1.1(这里有回车换行符)
host: www.baidu.com
connection: keep-alive
accept: text/plain, */*; q=0.01
x-requested-with: xmlhttprequest
user-agent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/76.0.3809.132 safari/537.36
sec-fetch-mode: cors
sec-fetch-site: same-origin
referer: https://www.baidu.com/
accept-encoding: gzip, deflate, br
accept-language: zh-cn,zh;q=0.9
cookie:............
请求方法分为:
get:请求获取url对应的资源,将请求(路径、请求字符串)放在header里,即第一行,body部分没有内容,即两个回车换行符后没有内容;
post:使用表格提交数据至服务器,即两个回车换行符后有内容,body不为空;
head:和get类似,不过不返回消息正文。
常见的传递信息的方式:
1、get方法使用query string,通过查询字符串在url中传递数据。
2、post方法提交数据:http://127.0.0.1:9000/xxx/yyy?id=1&name=b,使用表单提交数据,文本框input的name属性为age,weight,height等
请求消息:
post /xxx/yyy?id=1&name=b http/1.1
host: 127.0.0.1:9000
content-length: 26
(这里有两个回车换行符)
age=5&weight=90&height=170
3、url中本身包含信息:如http://127.0.0.1:9000/python/teacher/003
响应
响应消息行:协议版本 状态码 消息描述crlf
http/1.1 200 ok
bdpagetype: 2
bdqid: 0xfd4f8c6300043417
cache-control: private
connection: keep-alive
content-type: text/html;charset=utf-8
date: fri, 06 sep 2019 10:16:44 gmt
expires: fri, 06 sep 2019 10:16:44 gmt
server: bws/1.1
set-cookie: bdsvrtm=263; path=/
set-cookie: bd_home=1; path=/
set-cookie: h_ps_pssid=1420_21110_20881_29523_29519_29721_29567_29220_26350_22159; path=/; domain=.baidu.com
strict-transport-security: max-age=172800
x-ua-compatible: ie=edge,chrome=1
transfer-encoding: chunked
状态码status code:
状态码在响应头第一行。
2xx——表明请求被正常处理了
1、200 ok:请求已正常处理。
2、204 no content:请求处理成功,但没有任何资源可以返回给客户端,一般在只需要从客户端往服务器发送信息,而对客户端不需要发送新信息内容的情况下使用。
3、206 partial content:是对资源某一部分的请求,该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的get请求。响应报文中包含由content-range指定范围的实体内容。
3xx——表明浏览器需要执行某些特殊的处理以正确处理请求(浏览器做重定向,服务器端只负责发送状态码和新的url)
4、301 moved permanently:资源的uri已更新。永久性重定向,请求的资源已经被分配了新的uri,以后应使用资源现在所指的uri。
5、302 found:资源的uri已临时定位到其他位置了,姑且算你已经知道了这个情况了。临时性重定向。和301相似,但302代表的资源不是永久性移动,只是临时性性质的。换句话说,已移动的资源对应的uri将来还有可能发生改变。
6、303 see other:资源的uri已更新。该状态码表示由于请求对应的资源存在着另一个url,应使用get方法定向获取请求的资源。303状态码和302状态码有着相同的功能,但303状态码明确表示客户端应当采用get方法获取资源,这点与302状态码有区别。
当301,302,303响应状态码返回时,几乎所有的浏览器都会把post改成get,并删除请求报文内的主体,之后请求会自动再次发送。
7、304 not modified:资源已找到,但未符合条件请求。该状态码表示客户端发送附带条件的请求时(采用get方法的请求报文中包含if-match,if-modified-since,if-none-match,if-range,if-unmodified-since中任一首部)服务端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回304.。
8、307 temporary redirect:临时重定向。与302有相同的含义。
4xx——表明客户端是发生错误的原因所在。
9、400 bad request:服务器端无法理解客户端发送的请求,请求报文中可能存在语法错误。
10、401 unauthorized:该状态码表示发送的请求需要有通过http认证(basic认证,digest认证)的认证信息,需要有身份认证。
11、403 forbidden:不允许访问那个资源。该状态码表明对请求资源的访问被服务器拒绝了。(权限,未授权ip等)
12、404 not found:服务器上没有请求的资源。路径错误等。
5xx——服务器本身发生错误
13、500 internal server error:内部资源出故障了。该状态码表明服务器端在执行请求时发生了错误。也有可能是web应用存在bug或某些临时故障。
14、502 上游服务器内部资源出故障,例如nginx反向代理服务器出现问题。
15、503 service unavailable:抱歉,我现在正在忙着。该状态码表明服务器暂时处于超负载或正在停机维护,现在无法处理请求。
http协议因为基于tcp协议,是面向连接的,因此,需要三次握手和四次断开。
在http1.1之后,支持keep-alive,默认开启,一个连接打开后会保持一段时间,浏览器如果再次访问该服务器就会使用这个tcp连接,减轻服务器压力,提高效率。
二、wsgi
wsgi主要规定了服务器端和应用程序间的接口。
通过了解http和html文档,我们知道web应用就是:
1、客户端发送请求给服务器端一个http请求;
2、服务器端接受请求,并生成一个html文档;
3、服务器端向客户端发送响应,html作为响应的body;
4、客户端收到响应,从http响应消息行中提取html,并通过渲染等显示给用户。
我们将这些步骤交给服务器软件去执行,用python专注于生成html文档。因为我们不希望接触到tcp连接、http原始请求和响应格式,所以,需要一个统一的接口,让我们专心用python编写web业务。
这个接口就是wsgi:web server gateway interface。
wsgi接口定义非常简单,它只要求web开发者实现一个函数,就可以响应http请求。
wsgiref是一个wsgi参考实现库,通过帮助文档,我们可以简单的实现一个服务器框架,代码如下:
from wsgiref.validate import validator
from wsgiref.simple_server import make_server
def simple_app(environ, start_response):
status = '200 ok' # http status 状态码信息
headers = [('content-type', 'text/plain')] # http headers
start_response(status, headers)
return [b"hello world"] # 此处返回可迭代对象,示例为含有一个bytes元素的列表
validator_app = validator(simple_app)
with make_server('127.0.0.1', 9000, validator_app) as httpd:
print("listening on port 8000....")
httpd.serve_forever()
返回使用浏览器访问host:port结果如下:
通过print(environ) 我们得知,environ是一个字典,字典内容如下(我们只截取部分有用的):
# 前面一些系统信息
server_protocol : http/1.1 # 协议
server_software : wsgiserver/0.2
request_method : get # 请求方法
path_info : / # path路径
query_string : # 查询字符串
http_host : 127.0.0.1:9000 # host和port
http_connection : keep-alive # 连接属于keep-alive连接
http_accept_encoding : gzip, deflate, br # 可以接受的编码
http_accept_language : zh-cn,zh;q=0.9 # 支持的语言
由源码和上述结果分析可知:make_server先将ip和端口号绑定到wsgiserver实例中,然后将http请求信息封装在字典environ中(如请求方法,url中的path路径,查询字符串,服务名,端口号,协议,useragent信息),然后返回wsgiserver实例。其中wsgiserver继承于httpserver类,在上一级是socketserver.tcpserver类,再上一级是baseserver类。在tcpserver类中有__init__,做了地址绑定和listen。然后在server_forever时调用了simple_app。这里可以看出,wsgi是基于http,和tcp的接口。这里我们已经实现了基本的http服务。
由源码,可知start_response(self, status, headers,exc_info=none)在make_server中传入的默认参数wsgirequesthdenler的父类的父类的父类中调用的handle中,创建的serverhandler
实例的父类的父类中有定义,返回的是response header,含有三个参数。而app返回的是response body,因此必须在app返回可迭代对象之前调用。
其三个参数分别代表状态码如‘200 ok’,headers是一个元素为二元组的列表,如[(‘content-type’, ‘text/plain’)],exc_info是在错误处理的时候用,不使用validator(simple_app)可以设置exc_info参数。
由上例可知,服务器程序需要调用符合上述定义的simple_app,app处理后返回响应头和可迭代对象的正文,由服务器封装返回浏览器端。完成了一个简单的web服务器程序。
本质上:
此服务器:
- 是一个tcp服务器,监听在特定端口上;
- 支持http协议,能够将http请求报文进行解析,能够把响应数据进行http协议的报文封装并返回给客户端;
- 实现了wsgi协议。(可以参看python官方网站中的pep0333)
app: - 遵从wsgi协议
- 本身是个可调用对象(在python中可调用对象有三种,函数,类,含有call的类)
- 调用了start_response,start_response返回响应头部
- 返回包含正文的可迭代对象
接下来我们手写一个web框架
三、web框架实现过程
由上例可以看出,虽然在wsgi服务器处理http报文后会得到一个environ字典,但是用起来很不方便,我们可以尝试自己编写程序(查询字符串一般是xxx=yyy&zzz=ccc)用partition,split函数将字符串切割成去解析查询字符串,或者使用cgi模块(已过期的模块)和urllib模块中的parse.parse_qs,出来的就是查询字符串的键值对。
我们采用webob库来解析environ的数据,它可以将其封装成对象,并提供对响应进行高级封装(如装饰器)。官方文档在docs.webob.org。在linux和windows中都可以采用pip install webob安装webob库
webob库的使用
webob.request对象
可以创建request = request(environ)实例,其中request有很多方法如(只截取部分):
get:返回get字典,是一个多值字典(简单地说key不再是唯一,get、getone后进先出,getall返回同key的所有value);
post:返回request.body的数据,同上;
params:返回所有数据,参数,所有你想要的都在这里。
webob.response对象
response = response(),部分方法:
status:返回状态码
headerlist:返回headers
由response的源码可知,app可以返回设置了body和状态码的webob.response对象,本身是可调用的,并且调用后返回可迭代对象就是response.body,也可以得到之前代码的结果:
from wsgiref.validate import validator
from wsgiref.simple_server import make_server
from webob import response,request
def app(environ, start_response):
res = response()
res.body = b"hello world"
return res(environ, start_response)
validator_app = validator(app)
with make_server('127.0.0.1', 9000, validator_app) as httpd:
print("listening on port 9000....")
httpd.serve_forever()
返回 hello word
webob.dec装饰器
在webob.dec下有一个wsgify装饰器,由帮助文档可知,被wsgify装饰后只需要:
@wsgify
def app2(request):
res = response()
res.body = b"hello world"
return res
class application: # 或者封装成类,以便增强功能。
@wsgify
def __call__(self, request):
res = response()
res.body = b"hello world"
return res
即可得到之前代码的结果。装饰器可以帮你把return的值封装成response实例返回。你可以将return值设置为:response,字符串,bytes类型实例。
我们可以用一个类来代替app,增强app的功能。首先要知道一个web服务器应该有的需求如下:
不同的path路径分配到不同的函数中处理,中间可以有多个拦截器。
我们先一步一步来实现上图所述的web框架。
实现路由
路由即不同的url访问不同的资源,对于动态网页来说不同路径对应不同应用程序;静态网页则对应静态文件。因此实现路由是web框架的第一步。
我们设置四个路由:
路由 | 内容 |
---|---|
/ | hello word |
/python | hello python |
/boss | this boss’s room |
其他 | 404 |
其中404 我们采用webob.exc中的异常模块httpnotfound。
首先我们要将路由,和app分别设定类,因为各有各的功能,最好不要混在一起,不方便使用和升级。简单实现代码如下:
from wsgiref.validate import validator
from wsgiref.simple_server import make_server
from webob import request,response
from webob.dec import wsgify
from webob.exc import httpnotfound
class router:
routertables = { }
@classmethod
def register(cls, path):
def _wrapper(handler):
cls.routertables[path] = handler
return handler
return _wrapper
class application:
def __init__(self):
self.router = router()
@wsgify
def __call__(self, request: request):
try:
return self.router.routertables[request.path](request)
except:
raise httpnotfound
@router.register('/')
def indexhandler(request):
return 'hello word'
@router.register('/python')
def pythonhandler(request):
return 'hello python'
@router.register('/boss')
def indexhandler(request):
return 'this boss\'s room'
validator_app = validator(application())
with make_server('127.0.0.1', 9000, validator_app) as httpd:
print("listening on port 9000....")
httpd.serve_forever()
运行正常,到此基本实现了路由功能,但是我们还需要加入正则表达式,以更加灵活和流畅的方式实现路由匹配。
路由的正则匹配
先简单复习一下正则表达式:
模式字符串使用特殊的语法来表示一个正则表达式。
字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。
多数字母和数字前加一个反斜杠时会拥有不同的含义。
标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。
反斜杠本身需要使用反斜杠转义。
由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 r’/t’,等价于’//t’)匹配相应的特殊字符。
下表列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。
表达式 | 描述 |
---|---|
^ | 匹配字符串的开头 |
$ | 匹配字符串的末尾 |
. | 匹配任意字符,除了换行符,当re.dotall标记被指定时,则可以匹配包括换行符的任意字符。 |
[…] | 用来表示一组字符,单独列出:[amk] 匹配 ‘a’,‘m’或’k’ |
[^…] | 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。 |
re* | 匹配0个或多个的表达式。 |
re | 匹配1个或多个的表达式。 |
? | 非贪婪方式,尽可能的少,如*?, ?,??,{n,}?,{n,m}? |
re{ n,} | 精确匹配至少n个前面表达式。 |
re{ n, m} | 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 |
a| b | 匹配a或b |
(re) | 匹配括号内的表达式,也表示一个组 |
(?pre) | 匹配括号内的表达式,表示一个组,组名是name |
(?:re) | 不产生分组,如(?:w |
(?#…) | 注释. |
(?= re) | 断言,如f(?=oo),断言f右边必有oo,返回匹配的f,不返回oo |
(?<= re) | 断言,如f(?=oo),断言f左边必有oo,返回匹配的f,不返回oo |
(?! re) | 断言,必不出现,返回匹配的项 |
(? | 同上 |
\w | 匹配字母数字,包括中文,unicode字符,下划线 |
\w | 匹配非字母数字,同上 |
\s | 匹配一位任意空白字符,等价于 [\t\n\r\f]. |
\s | 匹配1位任意非空字符 |
\d | 匹配任意数字,等价于 [0-9]. |
\d | 匹配任意非数字 |
\a | 匹配字符串开始 |
\z | 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。 |
\z | 匹配字符串结束 |
\g | 匹配最后匹配完成的位置。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配"never" 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。 |
\b | 匹配非单词边界。‘er\b’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。 |
\n, \t, 等. | 匹配一个换行符。匹配一个制表符。 |
\1…\9 | 匹配第n个分组的子表达式。 |
\10 | 匹配第n个分组的子表达式,如果它经匹配。否则指的是八进制字符码的表达式。 |
方法 | 作用 |
---|---|
compile | 编译正则表达式 |
match | 从头匹配,只匹配一次,返回match对象,none |
search | 只匹配一次,返回match对象 |
fullmatch | 完全匹配,返回match对象 |
findall | 从头找,返回所有匹配的元素列表 |
finditer | 同上,返回匹配的match对象组成的迭代器 |
用正则表达式为了接下来做分级匹配铺垫。之前用的字典储存匹配路径和应用,并不适用于分级匹配,所以我们改用列表,尝试着做一些改动,加入正则表达式分组。
改动如下:
class router:
routertables = []
@classmethod
def register(cls, path):
def _wrapper(handler):
cls.routertables.append((re.compile(path), handler))
return handler
return _wrapper
class application:
def __init__(self):
self.router = router()
@wsgify
def __call__(self, request: request):
for path, handler in self.router.routertables:
if path.match(request.path):
request.groups = path.match.groups() # 或者groupdict()
request.groupdict = matcher.groupdict() # 为request动态增加属性
return handler(request)
raise httpnotfound('猜猜我在哪儿?')
@router.register(r'^/(?p\d )')
@router.register('/')
def indexhandler(request):
print(request.groupdict)
return 'hello word'
路由分组
通常我们不仅仅需要一级目录,还需要二级,甚至三级目录。在这里我们只做一级目录的映射。
因此,之前所做的注册函数已经满足不了需求,如何将一个个前缀和url联系,我们将每一个前缀都用router实例化,不同的前缀对应不同的router实例,以达到管理profix下的url的目的。做到路由类管理url,app类管理程序调用,并将方法属性化。
实现代码如下:
from wsgiref.validate import validator
from wsgiref.simple_server import make_server
from webob import request,response
from webob.dec import wsgify
from webob.exc import httpnotfound
import re
class attrdict:
def __init__(self, dic:dict):
self.__dict__.update(dic if dic else {})
def __setattr__(self, key, value):
raise httpnotfound('dont touch it')
def __repr__(self):
return '{}'.format(self.__dict__)
def __len__(self):
return len(self.__dict__)
class router:
def __init__(self, prefix):
self.prefix = prefix
self.routertables = []
def register(self, path, *methods):
def _wrapper(handler):
self.routertables.append(
(tuple(map(lambda x: x.upper(), methods)), re.compile(path), handler)
)
return handler
return _wrapper
def get(self, path):
return self.register(path, 'get')
def post(self, path):
return self.register(path, 'post')
def match(self, request: request):
if not request.path.startswith(self.prefix):
return none
for methods, path, handler in self.routertables:
if not methods or request.method in methods:
matcher = path.match(request.path) # 先match一次原地址,如果可以说明是直接用prefix访问
if matcher:
request.groups = matcher.groups()
request.groupdict = attrdict(matcher.groupdict())
return handler(request)
matcher = path.match(request.path.replace(self.prefix, '', 1)) # 再match一次去掉prefix的路径
if matcher:
request.groups = matcher.groups()
request.groupdict = attrdict(matcher.groupdict())
return handler(request)
index = router('/')
py = router('/python')
bos = router('/boss')
class application:
_router = []
@classmethod
def routers(cls, *routers):
for router in routers:
cls._router.append(router)
@wsgify
def __call__(self, request: request):
for router in self._router:
response = router.match(request)
if response:
return response
raise httpnotfound('你看不到我')
application.routers(index, py, bos)
@index.register(r'^/(?p\d )$')
@index.register(r'^/$')
def indexhandler(request):
return 'hello word'
@py.get('^/python$')
def pythonhandler(request):
return 'hello python'
@py.get('^/(?p\d ?)$')
def pymore(request):
id = request.groupdict.id if request.groupdict else ''
return 'hey {} are in python room'.format(id)
@bos.register('^/boss$', 'get')
def boss(request):
return 'this boss\'s room'
@bos.get('^/(?p\d ?)$')
def bossmore(request):
id = request.groupdict.id if request.groupdict else ''
return 'hey {} are in boss room'.format(id)
validator_app = validator(application())
with make_server('127.0.0.1', 9000, validator_app) as httpd:
print("listening on port 9000....")
httpd.serve_forever()
当然我们也可以将prefix的下属路径交给应用程序去分辨和执行。
正则表达式的化简
之前写的正则表达式不太适合于生产环境中,url需要规范,尤其是对restful风格。之前写的是/boss/12314,第一段是业务,第二段是id,从csdn博客也可以看出/mdeditor/100533324。
如何将路径规范化?
尝试按照/{name:str}/{id:int}这种方式将路径规范化。
首先要对类型进行归纳,将类型映射到正则表达式。
如下:
typepatterns = {
'str': r'[^/] ',
'word': r'/w ',
'int': r'[ -]?\d ',
'float': r'[ -]?\d \.d ',
'any': r'. '
}
typecast = {
'str': str,
'word': str,
'int': int,
'float': float,
'any': str
}
转换/student/{name:str}/{id:int}字符串,使用正则表达式
(\{([^\{\}] ):([^\{\}] )\})
search整个字符串,匹配{name:str}/{id:int},替换为目标正则表达式。首先想到的是sub函数,但是sub函数的局限性太大,无法增加需要捕获数据类型。于是我们手写一个类似于sub函数的方法。整合过后,代码如下:
class attrdict:
def __init__(self, dic: dict):
self.__dict__.update(dic if dic else {})
def __setattr__(self, key, value):
raise httpnotfound('dont touch it')
def __repr__(self):
return '{}'.format(self.__dict__)
def __len__(self):
return len(self.__dict__)
class router:
reg = re.compile(r'\{([^\{\}] ):([^\{\}] )\}')
typepatterns = {
'str': r'[^/] ',
'word': r'/w ',
'int': r'[ -]?\d ',
'float': r'[ -]?\d \.d ',
'any': r'. '
}
typecast = {
'str': str,
'word': str,
'int': int,
'float': float,
'any': str
}
def parser(self, path): # 注册的时候替换,不然无法匹配path路径
start = 0
types = {}
temp = ''
matchers = self.reg.finditer(path)
for i, matcher in enumerate(matchers):
name = matcher.group(1)
t = matcher.group(2)
types[name] = self.typecast.get(t, str) # 如果group2是空,默认为str
# 把原字符串替换,并返回回去
temp = path[start:matcher.start()]
temp = '(?p<{}>{})'.format(
name,
self.typepatterns.get(t, self.typepatterns['str'])
)
start = matcher.end()
else:
temp = path[start:]
return temp, types
def __init__(self, prefix):
self.prefix = prefix
self.routertables = []
def register(self, path, *methods):
def _wrapper(handler):
newpath, types = self.parser(path) # 在注册的时候调用parser
self.routertables.append(
(tuple(map(lambda x: x.upper(), methods)), re.compile(newpath), types, handler)
)
return handler
return _wrapper
def get(self, path):
return self.register(path, 'get')
def post(self, path):
return self.register(path, 'post')
def match(self, request: request):
if not request.path.startswith(self.prefix):
return none
for methods, path, type, handler in self.routertables:
if not methods or request.method in methods:
matcher = path.match(request.path) # 先match一次原地址,如果可以说明是直接用prefix访问
if matcher:
newdict = {}
for k,v in matcher.groupdict().items():
newdict[k] = type[k](v)
request.groups = matcher.groups()
request.groupdict = attrdict(newdict)
return handler(request)
matcher = path.match(request.path.replace(self.prefix, '', 1)) # 再match一次去掉prefix的路径
if matcher:
newdict = {}
for k, v in matcher.groupdict().items():
newdict[k] = type[k](v)
request.groups = matcher.groups()
request.groupdict = attrdict(newdict)
return handler(request)
index = router('/')
py = router('/python')
bos = router('/boss')
class application:
_router = []
@classmethod
def routers(cls, *routers):
for router in routers:
cls._router.append(router)
@wsgify
def __call__(self, request: request):
for router in self._router:
response = router.match(request)
if response:
return response
raise httpnotfound('你看不到我')
application.routers(index, py, bos)
@index.register(r'^/{id:int}')
@index.register(r'^/$')
def indexhandler(request):
return 'hello word'
@py.get('^/python$')
def pythonhandler(request):
return 'hello python'
@py.get('^/{id:int}$')
def pymore(request):
id = request.groupdict.id if request.groupdict else ''
return 'hey {} are in python room'.format(id)
@bos.register('^/boss$', 'get')
def boss(request):
return 'this boss\'s room'
@bos.get('^/{id:int}')
def bossmore(request):
id = request.groupdict.id if request.groupdict else ''
return 'hey {} are in boss room'.format(id)
validator_app = validator(application())
with make_server('127.0.0.1', 9000, validator_app) as httpd:
print("listening on port 9000....")
httpd.serve_forever()
这里一级路径访问并没有很多元素,我们并不需要使用字典。
模板、拦截器、json,打包,异常处理
目前我们依旧在框架中返回的是字符串,这些字符串被webob包装成response对象,就是http响应正文;但是此时数据和数据格式(即html)混合在一起了,是否可以将数据和格式分离?
我们可以用简单的html模板,在里面采用和刚才规范化路径的方式一样的方法,在html里写入有一定格式的字符串,将数据填充的指定位置,然后生成新的html返回。
我们采用jinja2(知识是世界的),其设计思路来自于django模板引擎。
官方网站:http://jinja.pocoo.org/docs/2.10/
安装采用pip即可。
jinja的简单使用
将模块建立如下图:
from jinja2 import environment, filesystemloader
env = environment(
# loader=packageloader('webarch', 'templates') # 包的位置,和index的包名
loader=filesystemloader('templates') # 在同一级 templates和template. 与webarch在哪儿没关系,建议使用这个。出错也是路径出问题。
)
d = {
'userlist': [
(1, 'bilibili', 10),
(3, 'li', 18),
(9, 'huang', 26)
]
}
template = env.get_template('index.html')
html = template.render(**d)
print(html)
index.html
this is title
{% for name,id,age in userlist %} # 循环输出
- {
{loop.index}} {
{id}} {
{name}} {
{age}}
{% endfor %}
在原有代码中增加:
from jinja2 import environment, filesystemloader, packageloader
def render(name, data):
env = environment(
# loader=packageloader('webarch', 'templates') # 包的位置,和index的包的名字,用绝对路径,不然容易报错
loader=filesystemloader(r'c:\users\administrator\pycharmprojects\untitled\excercise\webarch/templates') # 用绝对路径
)
template = env.get_template(name)
return template.render(**data) # html
@bos.register('^/boss$', 'get')
def boss(request):
user = {
'userlist': [
('bilibili', 1, 10),
('li', 3, 18),
('huang', 9, 26)
]
}
return render('index.html', user)
运行正常。
还差拦截器实现和json,我们先简单了解一下。拦截器可以分为:
- 请求时拦截
- 响应时拦截
前两个为拦截点不同,后两个为影响区域不同 - 全局拦截:在实例中的app中拦截
- 局部拦截:在router中拦截
可以在app或者router类中加入拦截器,或者使用mixin,我们区分一下,router拦截器是会根据实例不同而有不同的表现,适合直接加入router中,而app的拦截器适合使用mixin。
webob.response.response支持json,在response中加入关键字json=d即可。
json就是一种轻量级的数据交换格式。可以完全独立于语言,适合于网络传输,以增加传输效率。因此html常用json来进行网络传输。
我们可以通过模块化,和打包,轻松的将写好的完整代码,打包,安装。至此代码全部编写完毕。