近些年來 REST (REpresentational State Transfer) 已經變成了 web services 和 web APIs 的標配。html
在本文中我將向你展現如何簡單地使用 Python 和 Flask 框架來建立一個 RESTful 的 web service。python
六條設計規範定義了一個 REST 系統的特色:web
REST 架構的最初目的是適應萬維網的 HTTP 協議。數據庫
RESTful web services 概念的核心就是「資源」。 資源能夠用 URI 來表示。客戶端使用 HTTP 協議定義的方法來發送請求到這些 URIs,固然可能會致使這些被訪問的」資源「狀態的改變。json
HTTP 標準的方法有以下:flask
========== ===================== ==================================
HTTP 方法 行爲 示例
========== ===================== ==================================
GET 獲取資源的信息 http://example.com/api/orders
GET 獲取某個特定資源的信息 http://example.com/api/orders/123
POST 建立新資源 http://example.com/api/orders
PUT 更新資源 http://example.com/api/orders/123
DELETE 刪除資源 http://example.com/api/orders/123
========== ====================== ==================================
REST 設計不須要特定的數據格式。在請求中數據能夠以 JSON 形式, 或者有時候做爲 url 中查詢參數項。api
堅持 REST 的準則設計一個 web service 或者 API 的任務就變成一個標識資源被展現出來以及它們是怎樣受不一樣的請求方法影響的練習。數組
好比說,咱們要編寫一個待辦事項應用程序並且咱們想要爲它設計一個 web service。要作的第一件事情就是決定用什麼樣的根 URL 來訪問該服務。例如,咱們能夠經過這個來訪問:瀏覽器
http://[hostname]/todo/api/v1.0/緩存
在這裏我已經決定在 URL 中包含應用的名稱以及 API 的版本號。在 URL 中包含應用名稱有助於提供一個命名空間以便區分同一系統上的其它服務。在 URL 中包含版本號可以幫助之後的更新,若是新版本中存在新的和潛在不兼容的功能,能夠不影響依賴於較舊的功能的應用程序。
下一步驟就是選擇將由該服務暴露(展現)的資源。這是一個十分簡單地應用,咱們只有任務,所以在咱們待辦事項中惟一的資源就是任務。
咱們的任務資源將要使用 HTTP 方法以下:
========== =============================================== =============================
HTTP 方法 URL 動做
========== =============================================== ==============================
GET http://[hostname]/todo/api/v1.0/tasks 檢索任務列表
GET http://[hostname]/todo/api/v1.0/tasks/[task_id] 檢索某個任務
POST http://[hostname]/todo/api/v1.0/tasks 建立新任務
PUT http://[hostname]/todo/api/v1.0/tasks/[task_id] 更新任務
DELETE http://[hostname]/todo/api/v1.0/tasks/[task_id] 刪除任務
========== ================================================ =============================
咱們定義的任務有以下一些屬性:
目前爲止關於咱們的 web service 的設計基本完成。剩下的事情就是實現它!
若是你讀過 Flask Mega-Tutorial 系列,就會知道 Flask 是一個簡單卻十分強大的 Python web 框架。
在咱們深刻研究 web services 的細節以前,讓咱們回顧一下一個普通的 Flask Web 應用程序的結構。
我會首先假設你知道 Python 在你的平臺上工做的基本知識。 我將講解的例子是工做在一個類 Unix 操做系統。簡而言之,這意味着它們能工做在 Linux,Mac OS X 和 Windows(若是你使用Cygwin)。 若是你使用 Windows 上原生的 Python 版本的話,命令會有所不一樣。
讓咱們開始在一個虛擬環境上安裝 Flask。若是你的系統上沒有 virtualenv,你能夠從 https://pypi.python.org/pypi/virtualenv 上下載:
$ mkdir todo-api
$ cd todo-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask
既然已經安裝了 Flask,如今開始建立一個簡單地網頁應用,咱們把它放在一個叫 app.py 的文件中:
#!flask/bin/python
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "Hello, World!" if __name__ == '__main__': app.run(debug=True)
爲了運行這個程序咱們必須執行 app.py:
$ chmod a+x app.py
$ ./app.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
如今你能夠啓動你的網頁瀏覽器,輸入 http://localhost:5000 看看這個小應用程序的效果。
簡單吧?如今咱們將這個應用程序轉換成咱們的 RESTful service!
使用 Flask 構建 web services 是十分簡單地,比我在 Mega-Tutorial 中構建的完整的服務端的應用程序要簡單地多。
在 Flask 中有許多擴展來幫助咱們構建 RESTful services,可是在我看來這個任務十分簡單,沒有必要使用 Flask 擴展。
咱們 web service 的客戶端須要添加、刪除以及修改任務的服務,所以顯然咱們須要一種方式來存儲任務。最直接的方式就是創建一個小型的數據庫,可是數據庫並非本文的主體。學習在 Flask 中使用合適的數據庫,我強烈建議閱讀 Mega-Tutorial。
這裏咱們直接把任務列表存儲在內存中,所以這些任務列表只會在 web 服務器運行中工做,在結束的時候就失效。 這種方式只是適用咱們本身開發的 web 服務器,不適用於生產環境的 web 服務器, 這種狀況一個合適的數據庫的搭建是必須的。
咱們如今來實現 web service 的第一個入口:
#!flask/bin/python
from flask import Flask, jsonify app = Flask(__name__) tasks = [ { 'id': 1, 'title': u'Buy groceries', 'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 'done': False }, { 'id': 2, 'title': u'Learn Python', 'description': u'Need to find a good Python tutorial on the web', 'done': False } ] @app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': tasks}) if __name__ == '__main__': app.run(debug=True)
正如你所見,沒有多大的變化。咱們建立一個任務的內存數據庫,這裏無非就是一個字典和數組。數組中的每個元素都具備上述定義的任務的屬性。
取代了首頁,咱們如今擁有一個 get_tasks 的函數,訪問的 URI 爲 /todo/api/v1.0/tasks,而且只容許 GET 的 HTTP 方法。
這個函數的響應不是文本,咱們使用 JSON 數據格式來響應,Flask 的 jsonify 函數從咱們的數據結構中生成。
使用網頁瀏覽器來測試咱們的 web service 不是一個最好的注意,由於網頁瀏覽器上不能輕易地模擬全部的 HTTP 請求的方法。相反,咱們會使用 curl。若是你尚未安裝 curl 的話,請當即安裝它。
經過執行 app.py,啓動 web service。接着打開一個新的控制檯窗口,運行如下命令:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 294
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 04:53:53 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
]
}
咱們已經成功地調用咱們的 RESTful service 的一個函數!
如今咱們開始編寫 GET 方法請求咱們的任務資源的第二個版本。這是一個用來返回單獨一個任務的函數:
from flask import abort @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET']) def get_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) return jsonify({'task': task[0]})
第二個函數有些意思。這裏咱們獲得了 URL 中任務的 id,接着 Flask 把它轉換成 函數中的 task_id 的參數。
咱們用這個參數來搜索咱們的任務數組。若是咱們的數據庫中不存在搜索的 id,咱們將會返回一個相似 404 的錯誤,根據 HTTP 規範的意思是 「資源未找到」。
若是咱們找到相應的任務,那麼咱們只需將它用 jsonify 打包成 JSON 格式並將其發送做爲響應,就像咱們之前那樣處理整個任務集合。
調用 curl 請求的結果以下:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:50 GMT
{
"task": {
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
}
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 238
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:52 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you entered the URL manually please check your spelling and try again.</p>
當咱們請求 id #2 的資源時候,咱們獲取到了,可是當咱們請求 #3 的時候返回了 404 錯誤。有關錯誤奇怪的是返回的是 HTML 信息而不是 JSON,這是由於 Flask 按照默認方式生成 404 響應。因爲這是一個 Web service 客戶端但願咱們老是以 JSON 格式迴應,因此咱們須要改善咱們的 404 錯誤處理程序:
from flask import make_response @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404)
咱們會獲得一個友好的錯誤提示:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:36:54 GMT
{
"error": "Not found"
}
接下來就是 POST 方法,咱們用來在咱們的任務數據庫中插入一個新的任務:
from flask import request @app.route('/todo/api/v1.0/tasks', methods=['POST']) def create_task(): if not request.json or not 'title' in request.json: abort(400) task = { 'id': tasks[-1]['id'] + 1, 'title': request.json['title'], 'description': request.json.get('description', ""), 'done': False } tasks.append(task) return jsonify({'task': task}), 201
添加一個新的任務也是至關容易地。只有當請求以 JSON 格式形式,request.json 纔會有請求的數據。若是沒有數據,或者存在數據可是缺乏 title 項,咱們將會返回 400,這是表示請求無效。
接着咱們會建立一個新的任務字典,使用最後一個任務的 id + 1 做爲該任務的 id。咱們容許 description 字段缺失,而且假設 done 字段設置成 False。
咱們把新的任務添加到咱們的任務數組中,而且把新添加的任務和狀態 201 響應給客戶端。
使用以下的 curl 命令來測試這個新的函數:
$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 201 Created
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:56:21 GMT
{
"task": {
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
}
注意:若是你在 Windows 上而且運行 Cygwin 版本的 curl,上面的命令不會有任何問題。然而,若是你使用原生的 curl,命令會有些不一樣:
curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks
固然在完成這個請求後,咱們能夠獲得任務的更新列表:
$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:57:44 GMT
{
"tasks": [
{
"description": "Milk, Cheese, Pizza, Fruit, Tylenol",
"done": false,
"id": 1,
"title": "Buy groceries"
},
{
"description": "Need to find a good Python tutorial on the web",
"done": false,
"id": 2,
"title": "Learn Python"
},
{
"description": "",
"done": false,
"id": 3,
"title": "Read a book"
}
]
}
剩下的兩個函數以下所示:
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT']) def update_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) if not request.json: abort(400) if 'title' in request.json and type(request.json['title']) != unicode: abort(400) if 'description' in request.json and type(request.json['description']) is not unicode: abort(400) if 'done' in request.json and type(request.json['done']) is not bool: abort(400) task[0]['title'] = request.json.get('title', task[0]['title']) task[0]['description'] = request.json.get('description', task[0]['description']) task[0]['done'] = request.json.get('done', task[0]['done']) return jsonify({'task': task[0]}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE']) def delete_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) tasks.remove(task[0]) return jsonify({'result'