背景
虽然是一名算法工程师,但是实际情况是不仅要懂算法,还要懂工程。算法实现后,算法效果展示,算法落地等就需要一些工程化的内容。算法工程化的一个简单方式就是将落地的算法包装成服务,供他人调用。
那么问题来了,我们需要了解一下服务相关的内容。对外提供服务的方式也有多种方式,例如基于GRPC的服务,基于Restful的api接口服务等。当然,我个人感觉使用基于restful的api接口方式是使用比较多的。于是在了解Python web的相关框架,如Django,Flask, FastAPI等,综合考虑使用Flask去做这项工作。
那么学习一项技术最好的方式是看官方文档,但是官方文档很多内容是接口,方法的使用,枯燥、无味。这里我就通常看有没有Quickstart以及Tutorial,因为这个一方面是主流方法使用的概括。一方面是一个小demo的实现,读起来能够大致了解一项技术包含哪些内容,另一方面可以了解如果使用该技术去完成一些任务。至于更详细的内容,则可以通过查阅文档进一步挖掘知识。
查看Flask文档,恰好有这两部分内容。于是我就开心的学习和记录相关内容了。当然我也不会一口吞一个胖子,很快的阅读完,一段时间学一点记录一点,毕竟自己的主要工作是算法方面的拓展和实践。
当然,后面的记录既是官方文档的翻译也是自己的总结和笔记,献丑啦。
环境准备
flask如何安装可以参考官方网站,本次学习记录主要使用的是2.1.x版本。
为了更好地接近实际工作中的开发方式,我在虚拟机上装了一个centos7系统,并配置了anaconda开发环境。按照flask 2.1.x版本要求Python的版本是3.7+,我也创建了一个python3.7的空开发环境,以便后续可以导出requirements.txt文件。我们可以按照使用venv模块去创建虚拟环境。不过还是比较习惯使用conda去创建虚拟环境,如下:
conda create -n web_flask python=3.7
然后就是在这个环境中安装flask:pip install flask==2.1
下面就是使用pycharm以远程连接的方式连接这个虚拟开发环境了。你可能会想,在自己的虚拟机上开发会不会性能有影响?我觉得这个大可不必担心,这种开发不会吃电脑太多性能。
一个最小应用程序
一个最小的Flask应用可以如下所示:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
上面代码对应的功能如下:
- 导入的Flask类,他是WSGI应用程序的一个实例。如果不清楚这个,可以参考百度词条:wsgi
- 接下来就是对Flask进行实例化。其中第一个参数就是这个应用的模块或者包的名称。
__name__
在python中的作用是:第一种情况指的是当前运行的模块,那么当前模块__name__
的值就为__main__
;第二种情况指的是该模块是使用import导入的模块,那么这个被导入模块的__name__
变量的值为该模块的文件名。这是必需的操作,以便 Flask 知道在哪里寻找资源,例如模板和静态文件。 - 当我们使用
route
这个装饰器时,就是告诉Flask去这个方法处理什么样的URL链接。 - 该函数返回我们想要在用户浏览器中显示的消息。默认内容类型为 HTML,因此字符串中的 HTML 将由浏览器呈现。
将上面的内容保存到hello.py这个文件中或者使用其他文件名。不过需要确保不是flask.py这个名字,否则会与Flask冲突。
我们使用flask
这个命令或者python -m flask
去运行这个应用。我们在linux系统hello.py对应文件路径下开始启动他,当然在此之前需要通过导出 FLASK_APP 环境变量来告诉终端要使用的应用程序:
$ export FLASK_APP=hello
$ flask run
于是就有了:
现在是运行起来了,但是只有服务器那台机器可以访问。
应用发现行为:其实flask对于应用启动是有快捷方式的,当服务程序是app.py或者wsgi.py时,就不需要设置FLASK_APP环境变量的。
程序运行起来后,本质上来说是启动了一个非常简单的内置服务器,其应对测试是没有问题的,但是不适合生产中使用。当系统中有其他程序占用了flask的默认端口5000时,启用是会出现OSError:[Errno98]
或OSError:[WinError 10013]
的错误,这时候则需要更改一下这个程序监听的端口。可以解决的一种方式是在运行命令中添加--port 其他端口号
.
当前程序还只能被当前服务器自身访问 ,为了使程序成为一个Extenally Visible Server,可以做的一种方式是在运行开始时添加--host=0.0.0.0
,如:flask run --host=0.0.0.0
。这就告诉操作系统去监控所有的公网IP了。
根据以上两个问题,汇总开启程序如下:flask run --port 5001 --host=0.0.0.0
在宿主机访问如下:
此外,如果宿主机不能访问的话,可能是虚拟机的系统防火墙的问题,需要打开对5000端口的监听。打开方式可以参见【Centos7】防火墙(firewall)常用命令总结.
Debug模式
flask run
命令还可以开启调试模式。通过开启调试模式,如果代码发生变动服务器可以自动加载,并且如果请求期间发生错误,则在浏览器中显示交互式调试器。
需要注意的是:degugger虽然好用,但是最好不要在生产环境中使用,可能会泄露系统的信息。
启动方式:
export FLASK_ENV=development
flask run
HTML转义
flask默认返回的response(响应)是HTML,当然也可以返回json格式的数据等。当返回的内容是HTML时,这时就需要考虑一个问题:injection attacks,flask页面端使用的html渲染引擎是Jinja,将会自动进行防注入攻击处理,就是进行转义。
下面是手动调用的防注入攻击的方式:escape()
。为了描述简单,后面的内容都省略了,但是我们需要知道的是,我们是处理的数据都是不可信的数据。
from markupsafe import escape
@app.route("/<name>")
def hello(name):
return f"Hello, {escape(name)}!"
例如,用户提交的name数据是<script>alert("bad")</script>
,那么escape就会将其进行转义,页面端就不会执行这行代码。
可能<name>
这种使用方式你还不理解,其就是从URL中获取参数便于服务端根据不同情况处理不同的问题,后面会介绍。
路由Routing
现代 Web 应用程序使用有意义的 URL 来帮助用户。如果页面使用有意义的 URL,他们可以记住并用于直接访问页面,用户更有可能喜欢页面并返回。
每个URL都代表一定意义,根据这些URL指定的意义返回页面是很重要的。于是在flask中,会使用route()
这个小东西去绑定一个函数和URL请求。例如:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
其实有了这,我们可以做更多!可以使部分 URL 动态化并将多个规则附加到一个函数。因为前文我们已经看到了可以获取URL中的参数。已经有点Restful接口的意思啦。
可变规则
那么如何动态地搞事情呢?我们可以通过使用 <variable_name>
标记部分来将可变部分添加到 URL中。我们的处理函数接受这个 <variable_name>
,就可以作为函数的参数。除此之外,我们可以使用转换器来指定参数的类型,例如 <converter:variable_name>
。使用案例如下:
from markupsafe import escape
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return f'User {escape(username)}'
@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return f'Post {post_id}'
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return f'Subpath {escape(subpath)}'
转换参数类型的功能,在低版本的flask是不支持的哦。那么类型转换有哪些呢?如下:
string | (默认值)接受任何不带斜线的文本 |
---|---|
int | 接受正整数 |
float | 接受正浮点值 |
path | 像字符串,但也接受斜杠 |
uuid | 接受 UUID 字符串 |
独特的URLS/重定向行为
以下两条规则在使用斜杠时有所不同。
@app.route('/projects/')
def projects():
return 'The project page'
@app.route('/about')
def about():
return 'The about page'
其中,projects这个URL的尾部有一个斜线"/",这似乎更像一个文件系统中的文件夹。如果您访问的 URL 不带斜杠 (/projects),Flask 会将您重定向到带有斜杠 (/projects/) 的规范 URL。
about 端点的规范 URL 没有尾部斜杠。它类似于文件的路径名。使用尾部斜杠 (/about/) 访问 URL 会产生 404“未找到”错误。这有助于使这些资源的 URL 保持唯一性,从而帮助搜索引擎避免将同一页面编入两次索引。
也就是说,程序中URL中加上斜杠,用户访问的URL没有斜杠时,系统会补全。而程序中URL不加斜杠,用户访问的URL有斜杠时,那么就会报错。
URL构建
要构建特定函数的 URL,可以使用url_for()
这个函数。它接受函数的名称作为其第一个参数和任意数量的关键字参数,每个关键字参数对应于 URL 规则的可变部分。未知的可变部分作为查询参数附加到 URL。
那么为什么要使用 URL 反转函数url_for()
构建 URL,而不是将它们硬编码到模板中?
- 反转通常比对 URL 进行硬编码更具描述性。
- 您可以一次性更改 URL,而无需记住手动更改硬编码的 URL。
- URL 构建透明地处理特殊字符的转义。
- 生成的路径总是绝对的,避免了浏览器中相对路径的意外行为。
- 如果您的应用程序位于 URL 根之外,例如,在 /myapplication 而不是 / 中,则 url_for() 会为您正确处理。
例如,这里我们使用test_request_context()
方法来尝试 url_for()
。 test_request_context()
告诉 Flask 表现得好像它正在处理一个请求。
from flask import url_for
@app.route('/')
def index():
return 'index'
@app.route('/login')
def login():
return 'login'
@app.route('/user/<username>')
def profile(username):
return f'{username}\'s profile'
with app.test_request_context():
print(url_for('index'))
print(url_for('login'))
print(url_for('login', next='/'))
print(url_for('profile', username='John Doe'))
访问结果:
/
/login
/login?next=/
/user/John%20Doe
HTTP协议中方法
Web 应用程序在访问 URL 时使用不同的 HTTP 方法。在使用 Flask 时,您应该熟悉 HTTP 方法。默认情况下,路由只响应 GET 请求。您可以使用 route()
装饰器的方法参数来处理不同的 HTTP 方法。
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return do_the_login()
else:
return show_the_login_form()
上面的示例将路由的所有方法保留在一个函数中,如果每个部分都使用一些公共数据,这将很有用。
您还可以将不同方法的视图分成不同的功能。 Flask 为每种常见的 HTTP 方法提供了一种使用 get()
、post()
等装饰此类路由的快捷方式。
@app.get('/login')
def login_get():
return show_the_login_form()
@app.post('/login')
def login_post():
return do_the_login()
如果 GET 存在,Flask 会自动添加对 HEAD 方法的支持并根据 HTTP RFC 处理 HEAD 请求。同样,OPTIONS 会自动为您实施。
总结
码的有点累,下次周末继续搞一波。quickstart不知不觉已过半,加油!!!