野路子碼農系列(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(),弱智錯誤……);再好比只能處理一個請求,不能併發,這也是以後須要解決的問題。

因此繼續再接再礪吧,野路子碼農!

相關文章
相關標籤/搜索