本文通过学习 https://programminghistorian.org/en/lessons/creating-apis-with-python-and-flask 而来,例子和数据库例子也是来自该文。
本文先介绍api的使用,然后通过几个例子一步步解说rest api 的设计。例子包括:第一个基本例子,有返回的例子,带参数查询的例子,数据库查询的例子。理解这几个例子,你就入门了。
理解api
下面是使用api的例子:
http://chroniclingamerica.loc.gov/search/pages/results/?format=json&proxtext=fire
这个返回一个json 数据集。
这里包括以下三个部分。
基本的url 部分是
http://chroniclingamerica.loc.gov
路径是:
/search/pages/results/
参数部分是:
?format=json&proxtext=fire
准备
安装好python 3 和 flask
pip install flask
建立一个目录projects,然后在这个目录下建立一个api的目录。
第一个例子
在这个目录下建立一个api.py 的python文件,内容如下:
import flask
app = flask.Flask(__name__)
app.config["DEBUG"] = True
@app.route('/', methods=['GET'])
def home():
return "<h1>Distant Reading Archive</h1><p>This site is a prototype API for distant reading of science fiction novels.</p>"
app.run()
在命令行下执行python api.py
可以看到类似下面的提示内容:
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
按照这个提示在浏览器里输入 http://127.0.0.1:5000/ 就可以看到网页:
这是显示的第一个界面。
有数据返回的例子
下面添加一点内容,增加返回一个数据表这个api。更改这个文件的代码如下:
import flask
from flask import request, jsonify
app = flask.Flask(__name__)
app.config["DEBUG"] = True
# Create some test data for our catalog in the form of a list of dictionaries.
books = [
{'id': 0,
'title': 'A Fire Upon the Deep',
'author': 'Vernor Vinge',
'first_sentence': 'The coldsleep itself was dreamless.',
'year_published': '1992'},
{'id': 1,
'title': 'The Ones Who Walk Away From Omelas',
'author': 'Ursula K. Le Guin',
'first_sentence': 'With a clamor of bells that set the swallows soaring, the Festival of Summer came to the city Omelas, bright-towered by the sea.',
'published': '1973'},
{'id': 2,
'title': 'Dhalgren',
'author': 'Samuel R. Delany',
'first_sentence': 'to wound the autumnal city.',
'published': '1975'}
]
@app.route('/', methods=['GET'])
def home():
return '''<h1>Distant Reading Archive</h1>
<p>A prototype API for distant reading of science fiction novels.</p>'''
# A route to return all of the available entries in our catalog.
@app.route('/api/v1/resources/books/all', methods=['GET'])
def api_all():
return jsonify(books)
app.run()
同样运行python api.py,当然开始要ctrl +c 关闭上个运行的代码。
这次我们浏览器输入的是:
http://127.0.0.1:5000/api/v1/resources/books/all
在浏览器中看到这三个条目的JSON输出。 Flask提供了一个jsonify函数,该函数允许将列表和字典转换为JSON格式。 在程序中,书籍条目从Python词典列表转换为JSON,然后返回给用户。
至此,已经创建了一个有效的API。 在下一部分中,将允许用户通过更具体的数据(例如条目的ID)查找图书。
有具体数据查询的例子
继续修改api.py内容为:
import flask
from flask import request, jsonify
app = flask.Flask(__name__)
app.config["DEBUG"] = True
# Create some test data for our catalog in the form of a list of dictionaries.
books = [
{'id': 0,
'title': 'A Fire Upon the Deep',
'author': 'Vernor Vinge',
'first_sentence': 'The coldsleep itself was dreamless.',
'year_published': '1992'},
{'id': 1,
'title': 'The Ones Who Walk Away From Omelas',
'author': 'Ursula K. Le Guin',
'first_sentence': 'With a clamor of bells that set the swallows soaring, the Festival of Summer came to the city Omelas, bright-towered by the sea.',
'published': '1973'},
{'id': 2,
'title': 'Dhalgren',
'author': 'Samuel R. Delany',
'first_sentence': 'to wound the autumnal city.',
'published': '1975'}
]
@app.route('/', methods=['GET'])
def home():
return '''<h1>Distant Reading Archive</h1>
<p>A prototype API for distant reading of science fiction novels.</p>'''
@app.route('/api/v1/resources/books/all', methods=['GET'])
def api_all():
return jsonify(books)
@app.route('/api/v1/resources/books', methods=['GET'])
def api_id():
# Check if an ID was provided as part of the URL.
# If ID is provided, assign it to a variable.
# If no ID is provided, display an error in the browser.
if 'id' in request.args:
id = int(request.args['id'])
else:
return "Error: No id field provided. Please specify an id."
# Create an empty list for our results
results = []
# Loop through the data and match results that fit the requested ID.
# IDs are unique, but other fields might return many results
for book in books:
if book['id'] == id:
results.append(book)
# Use the jsonify function from Flask to convert our list of
# Python dictionaries to the JSON format.
return jsonify(results)
app.run()
同样启动python api.py
在浏览器分别输入:
127.0.0.1:5000/api/v1/resources/books?id=0
127.0.0.1:5000/api/v1/resources/books?id=1
127.0.0.1:5000/api/v1/resources/books?id=2
127.0.0.1:5000/api/v1/resources/books?id=3
这些书中的每一个都应返回不同的条目,最后一个除外,后者应返回一个空列表:[],因为没有书的id值为3。(编程中的计数通常从0开始,因此id = 3 将要求不存在的第四项。)在下一部分中,我们将更详细地探讨更新的API。
理解更新的API代码
在代码中,首先使用@ app.route语法创建一个名为api_id的新函数,该语法将该函数映射到路径/ api / v1 / resources / books。 这意味着当我们访问http://127.0.0.1:5000/api/v1/resources/books时,此功能将运行。 请注意,在不提供ID的情况下访问此链接将给出错误消息提示:
return "Error: No id field provided. Please specify an id."
在函数内部,做两件事:
首先,检查提供的URL以获取ID,然后选择与该ID相匹配的图书。 必须按以下方式提供ID:?id = 0。 像这样通过URL传递的数据(在?之后)称为查询参数-是HTTP的功能,用于过滤特定类型的数据。
这部分代码确定是否存在查询参数(例如?id = 0),然后将提供的ID分配给变量。
if 'id' in request.args:
id = int(request.args['id'])
else:
return "Error: No id field provided. Please specify an id."
接下来本部分将遍历书籍测试目录,匹配给定ID的书籍,将其附加到将返回给用户的列表中:
for book in books:
if book['id'] == id:
results.append(book)
最后 return jsonify(results)
将获取结果列表返回浏览器中并呈现为JSON。
到目前为止,创建了一个实际的API。 最后,将建立一个使用数据库的更为复杂的API,但是到目前为止,我们所使用的大多数原理和模式仍然适用。
包含数据库的例子
rest API的最后一个示例从数据库中提取数据,实现错误处理,并可以按出版日期过滤书籍。 使用的数据库是SQLite,默认情况下Python支持轻量级数据库引擎。 SQLite文件通常以.db文件扩展名结尾。
在修改代码之前,请先从此位置下载示例数据库,也可以这里下载数据库(14k) ,然后使用图形用户界面将文件复制到api文件夹。 将结果返回给用户时,API的最终版本将查询该数据库。
将以下代码复制到您的文本编辑器中。 然后保存为 api_final.py
import flask
from flask import request, jsonify
import sqlite3
app = flask.Flask(__name__)
app.config["DEBUG"] = True
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
@app.route('/', methods=['GET'])
def home():
return '''<h1>Distant Reading Archive</h1>
<p>A prototype API for distant reading of science fiction novels.</p>'''
@app.route('/api/v1/resources/books/all', methods=['GET'])
def api_all():
conn = sqlite3.connect('books.db')
conn.row_factory = dict_factory
cur = conn.cursor()
all_books = cur.execute('SELECT * FROM books;').fetchall()
return jsonify(all_books)
@app.errorhandler(404)
def page_not_found(e):
return "<h1>404</h1><p>The resource could not be found.</p>", 404
@app.route('/api/v1/resources/books', methods=['GET'])
def api_filter():
query_parameters = request.args
id = query_parameters.get('id')
published = query_parameters.get('published')
author = query_parameters.get('author')
query = "SELECT * FROM books WHERE"
to_filter = []
if id:
query += ' id=? AND'
to_filter.append(id)
if published:
query += ' published=? AND'
to_filter.append(published)
if author:
query += ' author=? AND'
to_filter.append(author)
if not (id or published or author):
return page_not_found(404)
query = query[:-4] + ';'
conn = sqlite3.connect('books.db')
conn.row_factory = dict_factory
cur = conn.cursor()
results = cur.execute(query, to_filter).fetchall()
return jsonify(results)
app.run()
现在运行 python api_final.py
在浏览器中输入以下做一下测试:
http://127.0.0.1:5000/api/v1/resources/books/all
http://127.0.0.1:5000/api/v1/resources/books?author=Connie+Willis
http://127.0.0.1:5000/api/v1/resources/books?author=Connie+Willis&published=1999
http://127.0.0.1:5000/api/v1/resources/books?published=2010
理解数据库驱动的API
关系数据库允许存储和检索存储在表中的数据。 表格与电子表格类似,因为它们具有列和行-列指示数据代表什么,例如“标题”或“日期”。 行代表单个条目,可以是书籍,用户,交易或任何其他类型的实体。
使用的数据库有五列id
, published
, author
, title
和first_sentence。 每行代表一本书,该书在当年出版的标题下赢得了年度雨果奖,其文本以first_sentence列中的句子开头。
api_all函数不是使用应用程序中定义的测试数据,而是从数据库中提取数据:
def api_all():
conn = sqlite3.connect('books.db')
conn.row_factory = dict_factory
cur = conn.cursor()
all_books = cur.execute('SELECT * FROM books;').fetchall()
return jsonify(all_books)
api_all请求返回所以书的信息。
首先,使用sqlite3库连接到数据库。表示与数据库的连接的对象已绑定到conn变量。 conn.row_factory = dict_factory这一行使连接对象知道使用我们定义的dict_factory函数,该函数从数据库中将其返回为字典而不是列表,当将它们输出为JSON时,它们会更好地工作。然后,我们创建一个游标对象(cur = conn.cursor()),该对象实际上是在数据库中移动以提取数据的对象。最后,我们使用cur.execute方法执行一个SQL查询,以从数据库的books表中提取所有可用数据(*)。在我们的函数结束时,此数据以JSON的形式返回:jsonify(all_books)。注意,返回数据的另一个函数api_filter将使用类似的方法从数据库中提取数据。
page_not_found函数的目的是创建一个错误页面,供用户在遇到错误或输入未定义的路线时看到:
@app.errorhandler(404)
def page_not_found(e):
return "<h1>404</h1><p>The resource could not be found.</p>", 404
api_filter函数是对先前的api_id函数的改进,该函数根据其ID返回一本书。 此新功能允许按三个不同的字段进行过滤:id,published和author。 该函数首先获取URL中提供的所有查询参数(请记住,查询参数是URL中位于?之后的部分,例如?id = 10)。
query_parameters = request.args
然后,它提取受支持的参数id,published和author,并将它们绑定到适当的变量:
id = query_parameters.get('id')
published = query_parameters.get('published')
author = query_parameters.get('author')
接下来构建SQL查询,该查询将用于在数据库中查找所需的信息。 用于在数据库中查找数据的SQL查询采用以下形式:
SELECT <columns> FROM <table> WHERE <column=match> AND <column=match>;
为了获得正确的数据,需要构建一个类似于上面的SQL查询,并构建一个包含将要匹配的过滤器的列表。 结合使用查询和用户提供的过滤器,可以从数据库中提取正确的图书。
开始定义查询和过滤器列表:
query = "SELECT * FROM books WHERE"
to_filter = []
然后如果提供id,published或author作为查询参数,则将它们添加到查询和过滤器列表中:
if id:
query += ' id=? AND'
to_filter.append(id)
if published:
query += ' published=? AND'
to_filter.append(published)
if author:
query += ' author=? AND'
to_filter.append(author)
如果用户未提供这些查询参数,则没有任何内容可显示,将其发送到“ 404 Not Found”页面:
if not (id or published or author):
return page_not_found(404)
为了完善sql查询,删除了query尾随的AND,并加上;
query = query[:-4] + ';'
就像在api_all函数中那样连接到数据库,然后执行使用过滤器列表构建的查询:
conn = sqlite3.connect('books.db')
conn.row_factory = dict_factory
cur = conn.cursor()
results = cur.execute(query, to_filter).fetchall()
最后以json 格式返回sql执行结果给用户。