野路子码农系列(1) 建立Web API

新工做正式开始了2天,因为客户暂时还没交接数据过来,暂时无事可作。恰逢政佬给某超市作的商品图像识别的项目客户催收了,老板要求赶忙搞个API,因而我就想我来试试吧。html

提及API,我实际上是一窍不通的,我对API的印象还停留在函数调包传参数,或者是用秘钥从网站提供的服务接数据这种层面。这两种好像都不太适合咱们这项目。我想了一下这个项目的应用流程大体是这样的:python

用户经过某个网页上传他们要识别的图片,图片被传送到服务器上,经过服务器上的算法进行识别,识别出来的结果再返回到网页上显示给用户。算法

如此这般,那我岂不是还要写网页啥的……彻底不会啊,是否是还要发布一个客户端的网页?还要在服务器上部署算法……瞬间我就懵圈了。此时正好同事在讨论flask,我想flask的葫芦应该仍是挺多的,要不就想办法依着画个瓢吧。flask

又是一阵搜索以后,我发现帮助最大的仍是flask的文档。http://docs.jinkan.org/docs/flask/quickstart.html 里面的例子很是实用,很快我就在“文件上传”那一节的例子中找到了现成的上传图片的方法。稍做修改以下:api

 1 import os
 2 from flask import Flask, request, render_template, redirect, url_for
 3 from werkzeug import secure_filename
 4 from datetime import datetime # 导入必要的库,datetime用来加时间戳
 5 import pandas as pd
 6 
 7 UPLOAD_FOLDER = './api/uploads' # 设置服务器上存放图片的路径
 8 ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif']) # 限定上传文件的类型
 9 
10 app = Flask(__name__)
11 app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # 绑定路径
12 app.config['MAX_CONTENT_LENGTH'] = 16 * 4096 * 4096 # 限定上传文件的最大尺寸,16M,像素为4096×4096
13 
14 def allowed_file(filename):
15     return '.' in filename and \
16            filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS # 检查文件是否知足以前设定的限定类型
17 
18 @app.route('/', methods=['GET', 'POST']) # 设定上传图片的页面
19 def upload_file():
20     if request.method == 'POST':
21         file = request.files['file']
22         if file and allowed_file(file.filename):
23             filename = datetime.now().strftime("%Y%m%d%H%M%S") + secure_filename(file.filename) # 在文件名以前加上时间戳,以保证不出现同名文件
24             file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) # 保存用户上传的文件
25             
26             return redirect(url_for('get_results')) # 调用get_results这个函数,返回一个重定向的页面
27     return '''
28     <!doctype html>
29     <title>Upload new File</title>
30     <h1>Upload new File</h1>
31     <form action="" method=post enctype=multipart/form-data>
32       <p><input type=file name=file>
33          <input type=submit value=Upload>
34     </form>
35     ''' # 这部分是个HTML的代码,用于上传图片
36     
37 @app.route('/results/')
38 def get_results():
39 
40     rd = pd.DataFrame()
41     rd['name'] = ['hehe', 'haha', 'oops']
42     rd['pct'] = [85, 75, 98]
43     rd['notes'] = ['!!!', '***', '???'] # 一个测试用的DataFrame
44     test_res = rd.to_dict('records') # 将DataFrame转换成字典
45 
46     return render_template('res.html', res=test_res) # 渲染模板,传入参数test_res
47 
48 
49 if __name__ == "__main__":
50     # 将host设置为0.0.0.0,端口8383,则外网用户也能够访问到这个服务
51     app.run(host="0.0.0.0", port=8383, debug=True)

 

稍微解释一下其中的几个步骤:浏览器

首先,经过这坨代码,咱们如今的流程变成了:安全

在代码中,咱们经过变量ALLOWED_EXTENSIONS限制上传的文件类型,上传图片就是上述格式,上传视频能够是avi、mp4,上传文本则是txt等。咱们再经过函数allowed_file来判断用户上传的文件是否符合格式要求,若是符合,函数返回True,不然返回False。这样一来,若是用户上传了不符合要求的文件就不会有任何反应。服务器

app是咱们的一个Flask实例,咱们经过app.config['XXX']这样的语句来设定app的一些参数,好比上传文件的最大尺寸,上传后文件保存的路径等等。此后咱们经过装饰器@app.route('XXX‘)来设定每一个函数对应的网页路径。好比,在这个例子中,upload_file函数前面的装饰器是@app.route('/', methods=['GET', 'POST']),则表明upload_file对应的页面就是http://127.0.0.1:8383/ (本地状况下,ip为127.0.0.1)。而get_results函数前面的装饰器是@app.route('/results/'),也就表示get_results的显示页面为http://127.0.0.1:8383/results/ 。并发

至于文件名的路径,secure_filename函数用来保证文件名路径的安全性,以避免给服务器形成意外损害。此外,咱们在文件名以前还加了个时间戳,这主要是考虑到若是用户上传了多个同名文件的话会出现互相覆盖的状况,有了时间戳就能够区分开来。app

上传图片完成后,咱们将页面重定向到get_results所对应的页面,这里咱们写了个模板res.html。你能够把模板当成是一张须要你填写的表,别人把表格的样式什么的全弄好了,你只要把数据填进相应的位置(也就是传入参数)就能够了。模板内容以下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Results</title>
 6 </head>
 7 <ul>
 8 {% for sth in res %}
 9     <li>{% for iid, val in sth.items() %}
10     <a href="#">{{ iid }}:{{ val }}</a>
11     {% endfor %}</li>
12 {% endfor %}
13 </ul>
14 </html>

模板写了两层循环,第一层从列表res中取元素,res中的每一个元素是一个字典;第二层是从字典中取信息,表如今网页上。

最后,咱们将python文件随便起个名字,好比upload.py,咱们在相同的目录下新建一个templates文件夹,而后把咱们的模板res.html放在那个文件夹下。而后咱们打开命令行,经过一下命令启动

python upload.py

顺利的话咱们会看到以下文字:

随后咱们打开浏览器,由于咱们是在本机运行,因此ip地址就是127.0.0.1,端口按照python代码中设置的是8383,所以咱们在地址栏输入http://127.0.0.1:8383/,出现以下页面:

咱们随便选一张图片以后,按下Upload按钮,就会跳转到页面http://127.0.0.1:8383/results/

这个就是咱们DataFrame中设定的内容。

 

这么看来一切都很顺利了!但问题又来了,因为upload_file和get_results这两个函数是分开的,按照咱们以前的流程,算法从upload_file读取图片并运行以后,要将结果传入get_results,这之中就有传参的问题。而我毕竟是野路子码农,怎么都无法解决传参的问题,老是报错。因而政佬说咱们把算法的结果写成csv导出吧,在另外一个函数中在读取csv文件。好吧,那还有没有别的曲线救国的办法?

固然有啦!咱们其实能够直接放弃重定向到get_results这一步,把最终要渲染的HTML模板直接也写在upload_file函数里。

 1 import os
 2 from flask import Flask, request, render_template_string  # 注意,此次咱们导入了render_template_string
 3 from werkzeug import secure_filename
 4 from datetime import datetime
 5 import pandas as pd
 6 
 7 UPLOAD_FOLDER = './api/uploads'
 8 ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])
 9 
10 app = Flask(__name__)
11 app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
12 app.config['MAX_CONTENT_LENGTH'] = 16 * 4096 * 4096
13 
14 def allowed_file(filename):
15     return '.' in filename and \
16            filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
17 
18 @app.route('/', methods=['GET', 'POST'])
19 def upload_file():
20     if request.method == 'POST':
21         file = request.files['file']
22         if file and allowed_file(file.filename):
23             filename = datetime.now().strftime("%Y%m%d%H%M%S") + secure_filename(file.filename)
24             file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
25             
26             # add your function here!
27             
28             rd = pd.DataFrame() 
29             rd['name'] = ['hehe', 'haha', 'oops']
30             rd['pct'] = [filename[8:10], filename[10:12], filename[12:14]]
31             rd['notes'] = ['!!!', '***', '???'] # 一个测试用的DataFrame
32     
33             rd = rd.to_dict('records') # 将DataFrame转换成字典
34 
35             return render_template_string('''
36                                           <!DOCTYPE html>
37                                           <html lang="en">
38                                           <head>
39                                           <meta charset="UTF-8">
40                                           <title>Results</title>
41                                           </head>
42                                           <ul>
43                                           {% for sth in res %}
44                                           <li>{% for iid, val in sth.items() %}
45                                           <a href="#">{{ iid }}:{{ val }}</a>
46                                            {% endfor %}</li>
47                                           {% endfor %}
48                                           </ul>
49                                           <input type="button" name="Submit" value="Return" onclick ="location.href='/'"/>
50                                           </html>
51                                           ''', res = rd) # 咱们把原来的模板res.html直接写在这里了,把rd传入模板
52         
53     return '''
54     <!doctype html>
55     <title>Upload new File</title>
56     <h1>Upload new File</h1>
57     <form action="" method=post enctype=multipart/form-data>
58       <p><input type=file name=file>
59          <input type=submit value=Upload>
60     </form>
61     '''
62 
63 if __name__ == "__main__":
64     # 将host设置为0.0.0.0,则外网用户也能够访问到这个服务
65     app.run(host="0.0.0.0", port=8383, debug=True)

这样一来咱们把两个函数合并成了一个函数,也就不存在了传参的问题,政佬能够在函数里直接调用他的算法,而返回的结果则经过render_template_string渲染模板并传入参数,直接获得了最终的页面。

咱们还在模板中加了一句:

<input type="button" name="Submit" value="Return" onclick ="location.href='/'"/>

这句语句就是添加一个Return按钮,按下以后连接的位置是'/',也就是http://127.0.0.1:8383/。咱们运行一下来看看最终效果:

上传界面别无二致:

咱们注意一下跳转后的界面,因为如今没有重定向了,因此网址没有任何变化。咱们再点击Return就又回到了上传界面。政佬在AWS上部署以后,把服务器的ip发给用户,用户经过这个ip和8383端口就能接入这个服务了,这下就大功告成啦!

 

经过此次折腾,我也算是从0开始学习了一下Web API的建立过程,能够说至关有成就感了。不过这个简陋的玩意儿还有挺多问题的,好比上传.JPG(大写)就会识别不出(filename没加.lower(),弱智错误……);再好比只能处理一个请求,不能并发,这也是以后须要解决的问题。

因此继续再接再砺吧,野路子码农!

相关文章
相关标签/搜索