Skip to content

Why PyWebIO?

WangWeimin edited this page Feb 18, 2021 · 16 revisions

脚本到Web之难

在日常开发工作中,经常会遇到这样的一个问题:一个功能可以通过一个脚本(控制台程序)快速实现,但为了使用方便又希望通过Web方式提供服务。

将脚本代码转换为Web应用会面临以下几个困难:

  1. 需要编写额外的前端代码来实现界面。
    一般至少要写两个页面,一个是输入表单,另一个是结果页。如果原脚本里的输入不能整合到一个表单里(比如需要依据一些输入项的内容来决定其他一些输入项),那么还需要编写更多的表单页面。
  2. 由于Http协议的无状态性,需要在各个后端接口之间转递状态,并且需要额外的代码检查状态的合法性。
    比如,当整个业务流程需要提交多个表单时,在Web开发中一般会通过session机制或前端的<input type="hidden">机制保存用户之前输入的数据。使用Session机制需要小心地进行状态校验和清理(否则会出现诸如验证码可重复使用等漏洞),而<input type="hidden">机制也容易被恶意修改表单中保存的隐藏数据。
  3. Web应用在单次HTTP请求中,无法实现实时输出,也加大了将脚本代码转换为Web应用的难度。
    比如控制台程序若要进行一些耗时操作,可以直接阻塞在主线程中,只需要周期性打印些日志就可以,而在Web应用中,耗时操作通常需要离线完成,前端需要定时轮询来实现任务进度的实时展示。

如果你非常认同上面几点,你可以直接跳到"PyWebIO的解决方案"一节;如果你对上面几点还有疑惑,下面会通过一个具体的例子来说明。

假设现在要开发一个学生返校疫情风险审查的应用。具体业务流程是,首先学生提交基本的信息,然后判断学生所在地区是否属于高风险地区, 如果属于,还需要上传7日内的核酸检测阴性证明。最后,根据学生所在的学院为学生展示相应的入学安排。

其中,已经实现了一些函数: is_dangerous(address) 用于检测地址address是否为高风险地区;get_schedule(academy) 用于获取academy学院的入学安排信息;save()用于保存用户提交的数据。

如果使用控制台程序实现这个流程,代码逻辑可以非常清晰。以下是Python实现:

stuid = input("请输入学号")
name = input("请输入姓名")
academy = input("请输入学院")
address = input("请输入地址")
# todo: 用户输入校验

covid_test_res = None
if is_dangerous(address):
    covid_test_res = input("请提交核酸检测阴性证明")
    # todo: 用户输入校验

print("入学安排")
print(get_schedule(academy))

save(stuid, name, academy, address, covid_test_res)

注:以上代码省略了用户数据校验的逻辑

如果要将上述流程实现为Web应用,需要分别编写前端页面和后端接口。

前端需要3个页面,一个是基本信息的提交表单页(basic_info.html),一个是核酸检测提交页面(covid_test.html),最后一个是入学安排信息展示页面(schedule_info.html)。

后端需要编写两个接口,一个是接收学生信息的接口,一个是接收核酸检测的接口。两个接口的Flask实现如下所示:

from flask import Flask, request, session, render_template
app = Flask(__name__)

@app.route('/basic_info')  # 学生信息接提交口
def basic_info():
    # todo: 校验用户提交的数据
    save(request.form)

    if is_dangerous(request.form['address']):
        session['academy'] = request.form['academy']  # 保存状态
        return render_template('covid_test.html')
    
    schedule_info = get_schedule(request.form['academy'])
    
    return render_template('schedule_info.html', data=schedule_info)

@app.route('/covid_test')  # 核酸检测提交接口
def covid_test():
    if 'academy' not in session:
        return error()   # 不合法的状态
    
    # todo: 校验用户提交的数据
    save(request.form['covid_test_res'])
    schedule_info = get_schedule(session['academy'])
    return render_template('schedule_info.html', data=schedule_info)

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

首先,从直观上来看,Web服务版的代码就要比脚本版复杂,逻辑看起来也比较吃力。

在Web服务版的代码中,由于第二个接口也需要返回入学安排,而获取入学安排所需要的学院信息是在第一个接口中提交的,所以在第一个接口中需要将学院信息保存在session中。由于在技术上完全可以不请求第一个接口而直接请求第二个接口,所以在第二个接口中还需要额外校验session中是否含有学院信息。

上述Web代码还可能存在数据不一致的问题,因为数据是分别在两个接口中存储的,可能出现用户没有走完整个业务流程,导致仅储存了部分数据,而若把数据都放到最后一步进行存储,又需要在前面的接口中不断地将中间数据往后传递。

另外,假设is_dangerous(address)调用十分耗时,如果还使用上例的Flask代码,当用户提交基本信息后,页面将会一直加载中,用户体验很不友好。 而终端程序只需要在调用is_dangerous(address)前输出一些加载提示,就不至于让用户以为程序无响应。

PyWebIO的解决方案

使用PyWebIO可以快速将脚本转换成Web应用,只需要将脚本中的输入输出函数替换成PyWebIO提供的输入输出函数,就可以将脚本中的交互由终端转换为浏览器上:

from pywebio.input import input, file_upload, input_group
from pywebio.output import put_text

stu_info = input_group('基本信息', [
    input("学号", name='stuid'),
    input("姓名", name='name'),
    input("学院", name='academy'),
    input("地址", name='address'),
], validate=...)  # todo: 用户输入校验

covid_test_res = None
if is_dangerous(stu_info['address']):
    covid_test_res = file_upload("请提交核酸检测阴性证明")
    # todo: 用户输入校验

put_text("入学安排")
put_text(get_schedule(stu_info['academy']))

save(stu_info, covid_test_res)

脚本执行后,会自动打开浏览器,用户在浏览器上完成原来的交互:

PyWebIO应用演示

而将更新后的脚本代码包装到一个函数中,并传入start_server()中就可以将功能作为Web应用来提供了:

from pywebio.input import input, file_upload, input_group
from pywebio.output import put_text
from pywebio import start_server

def main():
    stu_info = input_group('基本信息', [
        input("学号", name='stuid'),
        input("姓名", name='name'),
        input("学院", name='academy'),
        input("地址", name='address'),
    ], validate=...)  # todo: 用户输入校验

    covid_test_res = None
    if is_dangerous(stu_info['address']):
        covid_test_res = file_upload("请提交核酸检测阴性证明")
        # todo: 用户输入校验

    put_text("入学安排")
    put_text(get_schedule(stu_info['academy']))

    save(stu_info, covid_test_res)

start_server(main, port=80)

使用PyWebIO编写Web应用不存在上文描述的几种问题,因为从代码编写逻辑上看,PyWebIO应用还是延续了控制台程序的编写方式,只是应用的交互媒介则由终端变成了浏览器。

也因为如此,比起终端程序,PyWebIO可以输出更多样的内容(比如图片、图表等),因此PyWebIO非常适合快速构建基于输入输出进行交互的Web应用(或基于浏览器的本地GUI应用)。另一方面,相比于在本地运行脚本,通过Web访问服务也免去了安装依赖的麻烦,所以PyWebIO也很适合写一些在线小工具。

此外,PyWebIO还提供了布局、事件绑定、协程、与Web框架集成等特性,让应用的编写更便捷。

PyWebIO 相关链接

文档:https://pywebio.readthedocs.io/ (文档里的绝大部分代码示例都有在线演示的链接)

下面是一些使用PyWebIO编写的Demo和应用:

  • 输入演示:演示PyWebIO输入模块的用法
  • 输出演示:演示PyWebIO输出模块的用法
  • 数据可视化:在PyWebIO中使用bokeh、plotly、pyecharts等库进行数据可视化
  • 聊天室:不到80行代码实现的在线聊天室
  • mtag_tool:使用PyWebIO编写的用于编辑MP3标签的应用
Clone this wiki locally