RealPython 中文系列教程(四)

原文:RealPython

协议:CC BY-NC-SA 4.0

Alexa Python 开发:构建和部署一项 Alexa 技能

原文:https://realpython.com/alexa-python-skill/

智能家庭扬声器仅仅在几年前还是一个新奇的想法。今天,它们已经成为许多人的家庭和办公室的核心部分,并且它们的采用预计只会增加。这些设备中最受欢迎的是由亚马逊 Alexa 控制的设备。在本教程中,通过部署自己的 Alexa skill,你将成为一名 Alexa Python 开发人员,这是一个用户将使用语音命令与亚马逊 Alexa 设备进行交互的应用程序。

在本教程中,您将学习:

  • Alexa 技能的主要组成部分是什么
  • 如何设置 Alexa 技能并创造意图
  • 什么是ask_sdk_coreAlexa Python 包
  • 如何使用ask_sdk_core创建你的 Alexa Python 技能的业务逻辑
  • 如何使用在线开发者控制台构建、部署和测试您的 Alexa Python 技能

免费奖励: 单击此处下载一个 Python 语音识别示例项目,该项目具有完整的源代码,您可以将其用作自己的语音识别应用程序的基础。

Alexa Python 开发入门

按照这个教程,你需要创建一个免费的 Alexa 开发者账户。在该页面上,您将采取以下步骤:

  1. 点击开始按钮。
  2. 点击后续页面上的注册按钮。
  3. 点击创建您的亚马逊账户
  4. 用所需的细节填写表格。
  5. 点击提交完成注册过程。

你还需要熟悉一些概念,比如 Python 中的列表字典,以及 JavaScript 对象符号(JSON)。如果你是 JSON 新手,那么看看用 Python 处理 JSON 数据的

我们开始吧!

Remove ads

了解 Alexa 技能

一个 Alexa Python 开发者必须熟悉许多不同的 Alexa 技能组件,但是两个最重要的组件是接口服务:

  1. 技能接口处理用户的语音输入,并将其映射到意图。
  2. 技能服务包含所有的业务逻辑,这些逻辑决定给定用户输入的响应,并将其作为 JSON 对象返回。

技能界面将是你 Alexa 技能的前端。在这里,您将定义执行特定功能的意图和调用短语。本质上,这是负责与用户互动的技能的一部分。

技能服务将是你的 Alexa 技能的后端。当用户触发特定意图时,它会将该信息作为请求发送给技能服务。这将包含要返回的业务逻辑以及有价值的信息,这些信息将最终传递给用户。

设置您的环境

是时候开始建立你的第一个 Alexa Python 技能了!登录 Alexa 开发者控制台,点击创造技能按钮开始。在下一页,输入技能名称,将会是笑话机器人:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这将是你技能的调用短语。这是一个用户开始使用你的 Alexa 技能时会说的短语。如果你愿意,以后你可以把这个换成别的。此外,请注意,Alexa 技能可以用多种语言进行交互,这可以从默认语言下拉菜单中看到。现在,只需将其设置为英语(美国)

接下来,你需要选择一个模型来增加你的技能。这些模型就像是亚马逊团队根据一些常见用例预先设计的模板,帮助你开始 Alexa Python 开发。对于本教程,您应该选择定制型号。

最后,你需要选择一种方法来托管你的 Alexa 技能的后端。该服务将包含您的应用程序的业务逻辑。

**注意:**如果你选择提供你自己的选项,那么你将不得不为你的 Alexa Python 项目托管你自己的后端。这可以是一个在你选择的平台上构建和托管的 API 。另一个选择是创建一个单独的 AWS Lambda 函数,并将其与您的 Alexa 技能相联系。你可以在他们的定价页面上了解更多关于 AWS Lambda 定价的信息。

现在,选择 Alexa 托管(Python) 作为你的 Alexa 技能的后台。这将自动在 AWS 免费层中为您提供一个托管的后端,因此您不必预先支付任何费用或立即设置一个复杂的后端。

最后,点击创造技能按钮继续。你可能会被要求在这里填写验证码,所以也要填写完整。大约一分钟后,您应该会被重定向到开发人员控制台的构建部分。

了解 Alexa 技能模型

一旦你登录到 Alexa 开发者控制台并选择或创建了一个技能,你会看到构建部分。此部分为您提供了许多选项和控件来设置技能的交互模型。这个交互模型的组件允许您定义用户将如何与您的技能进行交互。这些属性可以通过左侧面板访问,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作为一名 Alexa Python 开发者,你需要了解一些 Alexa 技能交互模型的组件。首先是调用。这是用户开始与你的 Alexa 技能互动时会说的话。例如,用户会说,“笑话机器人”,以调用您将在本教程中建立的 Alexa 技能。您可以在任何时候从调用部分对此进行更改。

另一个组件是 intent ,它代表了应用程序的核心功能。你的应用程序将有一套意图,代表你的技能可以执行什么样的动作。为了提供给定意图的上下文信息,您将使用一个槽,,它是话语短语中的一个变量。

考虑下面的例子。调用天气意图的示例话语可以是,“告诉我天气情况。”为了使该技能更有用,您可以将意图设置为“告诉我芝加哥的天气”,其中单词“芝加哥”将作为槽变量传递,这改善了用户体验。

最后,还有**插槽类型,**定义了如何处理和识别插槽中的数据。例如,亚马逊。DATE slot type 可以轻松地将表示日期的单词(如“今天”、“明天”等)转换为标准日期格式(如“2019-07-05”)。可以查看官方槽型参考页了解更多。

**注意:**要了解更多关于 Alexa 技能交互模型的信息,请查看官方文档

此时,意图面板应该是打开的。如果不是,那么你可以从左边的工具条中选择意图来打开它。您会注意到默认情况下已经设置了五个意图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

意向面板包括一个hello world ent和五个内置意向。内置的意图是提醒你考虑一些对制作用户友好的机器人很重要的常见情况。这里有一个简单的概述:

  1. 亚马逊。cancel ent让用户取消一个事务或任务。例子包括,“没关系”,“忘记它”,“退出”和“取消”,尽管还有其他的。
  2. 亚马逊。HelpIntent 提供如何使用技能的帮助。这可以用来返回一句话,作为用户如何与你的技能互动的手册。
  3. 亚马逊。停止意图允许用户退出技能。
  4. 亚马逊。NavigateHomeIntent 将用户导航到设备主屏幕(如果正在使用屏幕)并结束技能课程。

默认情况下,没有指定的示例话语来触发这些意图,所以您也必须添加它们。把它当作你作为 Alexa Python 开发者培训的一部分。您可以在官方文档中了解更多关于这些内置意图的信息。

Remove ads

查看样本意图

在本教程的后面,你将学习如何建立一个新的意图,但是现在,看一看你创造的每个新技能中的一些现有的意图是一个好主意。首先,单击hello world ent查看其属性:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以看到用户为了实现这一意图而说出的示例话语。当这个意图被调用时,这个信息被发送到你的 Alexa 技能的后端服务,然后它将执行所需的业务逻辑并返回一个响应。

在此之下,您可以选择设置对话框委托策略,这允许您委托您定义的特定对话框来实现特定目的。虽然你不会在本教程中涉及这一点,但你可以在官方文档中了解更多。

接下来,您可以选择为您打算收集的一些特定数据定义。例如,如果您要创建一个告知给定日期天气的意图,那么您将在这里有一个日期槽,它将收集日期信息并将其发送到您的后端服务。

**注意:**此外,当您在一个单独的意图中从用户那里收集大量不同的数据点,并且您想在发送给用户进行进一步处理之前提示用户时,意图确认选项会很有用。

每当您对意图进行更改时,您需要点击保存模型按钮来保存它。然后,你可以点击建立模型按钮继续测试你的 Alexa Python 技能。

知道一项技能的交互模型可以完全用一种 JSON 格式来表示是很有帮助的。要查看你的 Alexa 技能的当前结构,点击控制台左侧面板的 JSON 编辑器选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果您直接使用 JSON 编辑器进行更改,那么这些更改也会反映在开发人员控制台 UI 中。为了测试这种行为,添加一个新的意图并点击保存模型

一旦你对技能的交互模型做了所有必要的修改,你就可以打开开发者控制台的测试部分来测试你的技能。测试是成为 Alexa Python 开发者的重要一环,所以一定不要跳过这一步!单击开发人员控制台顶部导航栏中的测试按钮。默认情况下,测试将被禁用。从下拉菜单中选择开发开始测试:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,你有很多方法可以测试你的 Alexa Python 技能。让我们做一个快速测试,这样你就可以了解你的 Alexa 技能将如何对话语做出反应。

从左侧面板选择 Alexa 模拟器选项,然后输入短语,“嘿 Alexa,打开笑话机器人。”你可以通过在输入框中输入或者使用麦克风选项来完成。几秒钟后,会有一个响应返回给您:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

除了语音响应,你还可以看到发送到你的 Alexa 技能后端服务的 JSON 输入,以及接收回控制台的 JSON 输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以下是目前发生的情况:

  • JSON 输入对象由用户通过语音或文本输入的输入数据构建而成。
  • Alexa 模拟器将输入和其他相关元数据打包,并发送到后端服务。你可以在 JSON 输入框中看到这一点。
  • 后端服务接收输入的 JSON 对象,并对其进行解析以检查请求的类型。然后,它将 JSON 传递给相关的意图处理函数。
  • 意图处理器函数处理输入并收集所需的响应,该响应作为 JSON 响应发送回 Alexa 模拟器。您可以在 JSON 输出框中看到这一点。
  • Alexa 模拟器解析这个 JSON,把语音响应读回给你。

**注:**你可以在官方文档中阅读关于 Alexa 技能的 JSON 请求-响应机制。

现在你已经对 Alexa 技能的不同组成部分以及信息如何从一个部分流向另一个部分有了一个概述,是时候开始构建你的笑话机器人了!在下一节中,您将通过创建一个新的意图来测试您的 Alexa Python 开发人员技能。

Remove ads

创造新的意向

让我们从创建 JokeIntent 开始,它将从列表中向用户返回一个随机的笑话。打开你的 Alexa 开发者控制台的构建部分。然后,点击左侧面板中意图选项旁边的添加按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

选择创建自定义意图选项,将名称设置为 JokeIntent ,然后点击创建自定义意图按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,您需要添加示例话语,用户将说出这些话语来调用这个意图。这些短语可以是“给我讲个笑话”或“我想听个笑话”键入一个短语并单击加号(+)将其添加为示例话语。这应该是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

您可以添加更多的示例话语,但是现在,这些已经足够了。最后,点击窗口左上角的保存模型按钮保存这些更改。

请记住,在测试之前,您需要构建您的模型。点击构建模型按钮,重新构建你的 Alexa Python 技能的交互模型。您会在浏览器窗口的右下角看到一个进度通知。一旦构建过程成功,您应该会看到另一个弹出通知,指示构建过程的状态。

您可以查看 JokeIntent 是否被成功触发。点击开发者控制台右上角的评估模型按钮。一个小窗口将从侧面弹出,允许你检查给定的输入话语将触发什么意图。键入任何示例话语,以确保成功调用了 JokeIntent

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要摆脱评估弹出窗口,再次点击评估模型按钮。

**注意:**这里要记住的一件关键事情是,就作为示例话语短语一部分的关键字而言,该模型非常灵活。例如,以这句话为例,“这是某种玩笑吗?”甚至这个短语也会触发的玩笑。作为一名 Alexa Python 开发者,选择在你的技能中执行其他意图的概率较低的话语是很重要的。

既然您已经成功地创建了一个意图,那么是时候编写 Python 代码来处理这个意图并返回一个笑话作为响应了。

构建技能后台

现在您已经创建了一个可以被用户触发的意图,您需要在技能后端添加功能来处理这个意图并返回有用的信息。打开 Alexa 开发者控制台的代码部分开始。

**注意:**由于你在设置过程中选择了 Alexa 托管的 Python 选项,你可以在开发者控制台中编写、测试、构建和部署你的 Alexa 技能的后端。

当您打开开发人员控制台的代码部分时,您可以看到一个在线代码编辑器,其中已经为您设置了一些文件。特别是,您将在 lambda 子目录中看到以下三个文件:

  1. lambda_function.py: 这是后端服务的主入口。来自 Alexa intent 的所有请求数据都在这里接收,并且应该只从这个文件返回。
  2. requirements.txt: 这个文件包含了这个项目中使用的 Python 包的列表。如果你选择建立自己的后端服务,而不是使用亚马逊提供的服务,这就特别有用。要了解更多关于需求文件的信息,请使用需求文件查看
  3. utils.py: 这个文件包含一些 lambda 函数与亚马逊 S3 服务交互所需的实用函数。它包含了一些关于如何从 Amazon S3 存储桶中获取数据的示例代码,您可能会发现这在以后会很有用。目前,这个文件没有在lambda_function.py中使用。

现在,您将只对lambda_function.py进行修改,所以让我们仔细看看文件的结构:

 7import logging
 8import ask_sdk_core.utils as ask_utils
 9
10from ask_sdk_core.skill_builder import SkillBuilder
11from ask_sdk_core.dispatch_components import AbstractRequestHandler
12from ask_sdk_core.dispatch_components import AbstractExceptionHandler
13from ask_sdk_core.handler_input import HandlerInput
14
15from ask_sdk_model import Response
16
17logger = logging.getLogger(__name__)
18logger.setLevel(logging.INFO)
19
20
21class LaunchRequestHandler(AbstractRequestHandler):
22    """Handler for Skill Launch."""
23    def can_handle(self, handler_input):
24        # type: (HandlerInput) -> bool
25
26        return ask_utils.is_request_type("LaunchRequest")(handler_input)
27
28    def handle(self, handler_input):
29        # type: (HandlerInput) -> Response
30        speak_output = "Welcome, you can say Hello or Help. " \
31                       "Which would you like to try?"
32
33        return (
34            handler_input.response_builder
35                .speak(speak_output)
36                .ask(speak_output)
37                .response
38        )
39...

首先,导入必要的工具,这些工具在ask_sdk_core Alexa Python 包中提供。然后,您需要在lambda_function.py中执行三个主要任务来处理来自 Alexa 技能前端的请求:

  1. 创建一个意向处理程序类,它继承自AbstractRequestHandler类,具有函数can_handle()handle()。在lambda_function.py中已经定义了几个处理程序类,比如LaunchRequestHandlerHelpIntentHandler等等。这些处理 Alexa 技能的基本意图。这里需要注意的重要一点是,您需要为您定义的每个意图创建一个新的意图处理程序类。
  2. 创建一个SkillBuilder对象,,作为你的 Alexa Python 技能的切入点。这会将所有传入的请求和响应有效负载路由到您定义的意图处理程序。
  3. 将意图处理程序类作为参数传递给.add_request_handler(),以便每当接收到新请求时,它们被按顺序调用。SkillBuilder是一个单例,所以只需要它的一个实例来处理所有传入请求的路由。

这是你经历lambda_function.py的好时机。你会注意到,相同的模式被一遍又一遍地遵循,以处理可能由你的 Alexa Python 技能触发的不同意图。

现在,您已经大致了解了在后端处理一个意图需要做的各种事情,是时候编写代码来处理您在上一节中构建的 JokeIntent 了。

Remove ads

创建 JokeIntent 处理程序

因为来自ask_sdk_core Alexa Python 包的重要实用程序已经被导入,你不需要再次导入它们。如果你想更深入地了解这些,那么你可以查看一下官方文档

接下来,您将创建一个新的意图处理器,它将处理来自 JokeIntent 的请求。在下面的代码片段中,意图处理程序将简单地返回一个示例短语。这表明对 JokeIntent 的响应是从后端接收的。将以下代码添加到LaunchRequestHandler()的类定义上方的lambda_function.py:

20class JokeIntentHandler(AbstractRequestHandler):
21    def can_handle(self, handler_input):
22        return ask_utils.is_intent_name("JokeIntent")(handler_input)
23
24    def handle(self, handler_input):
25        speak_output = "Here's a sample joke for you."
26
27        return (
28            handler_input.response_builder
29                .speak(speak_output)
30                .ask(speak_output)
31                .response
32        )

让我们看看每个部分都做了什么。在第 20 行中,你为 JokeIntent 创建了一个新的 intent handler 类,它是AbstractRequestHandler类的子类。当你在前端创建一个意图时,你需要在后端创建一个意图处理器类来处理来自 Alexa 的请求。您为此编写的代码需要做两件事:

  1. JokeIntentHandler.can_handle() 识别 Alexa 发送的每个传入请求。
  2. JokeIntentHandler.handle() 返回一个适当的响应。

第 21 行你定义 .can_handle() 。它接受handler_input作为参数,这是一个包含所有输入请求信息的dict()类型的对象。然后,它使用ask_utils.is_intent_name()ask_utils.is_request_type()来检查它接收到的 JSON 输入是否可以被这个意图处理函数处理。

您使用.is_intent_name()并传入意图的名称。这返回一个谓词,它是一个函数对象,如果给定的handler_input来源于指定的意图,则返回True。如果这是真的,那么SkillBuilder对象将调用JokeIntentHandler.handle()

**注意:**如果 JokeIntent 是从 Alexa skill 前端触发的,那么它将发送一个 JSON 对象,在request的主体中包含一个键type,表示名为JokeIntent的意图作为输入被接收。

这条语句随后调用 .handle() ,你在行 24 中定义。该方法接收输入请求以及可能需要的任何其他重要信息。它包含成功处理特定意图所需的业务逻辑。在 JokeIntent 的情况下,这个方法需要将包含笑话的响应发送回 Alexa 前端。

speak_ouput变量包含将由 Alexa 技能前端向用户反馈的句子。speak(speak_output)表示 Alexa 前端将向用户播放的语音内容。ask("Question to ask...")可用来问后续问题。在这个方法中,类response_builder的一个对象将响应返回给 Alexa 技能。

**注意:**如果.handle()不存在,将返回默认响应消息(Sorry, I had trouble doing what you asked. Please try again.)。

请注意,speak_output的值现在被设置为固定响应。稍后您将更改它,从笑话列表中返回一个随机笑话。

下面是您的代码在编辑器中的样子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦创建了意图处理程序类,就需要将其作为参数传递给SkillBuilder.add_request_handler。滚动到lambda_function.py的底部,添加以下一行:

sb.add_request_handler(JokeIntentHandler())

这里需要注意的一点是,这一行的位置很重要,因为代码是从上到下处理的。因此,确保对自定义意图处理程序的调用在对InstantReflectHandler()类的调用之上。它应该是这样的:

171sb = SkillBuilder()
172
173sb.add_request_handler(LaunchRequestHandler())
174sb.add_request_handler(JokeIntentHandler()) 175sb.add_request_handler(HelloWorldIntentHandler())
176sb.add_request_handler(HelpIntentHandler())
177sb.add_request_handler(CancelOrStopIntentHandler())
178sb.add_request_handler(SessionEndedRequestHandler())
179
180# Make sure IntentReflectorHandler is last so it
181# Doesn't override your custom intent handlers
182sb.add_request_handler(IntentReflectorHandler())
183
184sb.add_exception_handler(CatchAllExceptionHandler())
185
186...

好了,是时候测试你的代码了!单击 Deploy 按钮保存更改并部署后端服务。你将从 Alexa 技能前端检查它是否会像预期的那样工作。

一旦部署过程成功,返回到开发人员控制台的测试部分并调用 JokeIntent 。请记住,输入话语短语来调用您的 Alexa Python 技能,然后输入短语来执行意图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果您得到类似于上图中的响应,那么这意味着您已经成功地为技能后端服务中的 JokeIntent 创建了一个意图处理程序。恭喜你!现在,剩下要做的就是将列表中的一个随机笑话返回给技能前端。

Remove ads

添加笑话

打开开发者控制台的代码部分。然后,在lambda_function.py中添加jokes变量:

15from ask_sdk_model import Response
16
17logger = logging.getLogger(__name__)
18logger.setLevel(logging.INFO)
19
20jokes = [ 21        "Did you hear about the semi-colon that broke the law? He was given two consecutive sentences.", 22        "I ate a clock yesterday, it was very time-consuming.", 23        "I've just written a song about tortillas; actually, it's more of a rap.", 24        "I woke up this morning and forgot which side the sun rises from, then it dawned on me.", 25        "I recently decided to sell my vacuum cleaner as all it was doing was gathering dust.", 26        "If you shouldn't eat at night, why do they put a light in the fridge?", 27        ] 28
29class JokeIntentHandler(AbstractRequestHandler):
30...

这里,jokes是一个类型为list的变量,包含一些一行笑话。确保将其添加到函数或类定义之外,以便它具有全局范围

**注意:**因为这个列表只会被JokeIntentHandler()类引用,所以你是否在函数体中声明它并不重要。然而,这样做确实有助于功能体摆脱混乱。

接下来,您将添加.handle()从笑话列表中随机选择一个笑话并将其返回给用户所需的功能。用以下代码修改JokeIntentHandler.handle()的主体:

29class JokeIntentHandler(AbstractRequestHandler):
30    def can_handle(self, handler_input):
31        return ask_utils.is_intent_name("JokeIntent")(handler_input)
32
33    def handle(self, handler_input):
34        speak_output = random.choice(jokes)
35 36        return (
37            handler_input.response_builder
38                .speak(speak_output)
39                .ask(speak_output)
40                .response
41        )

.handle()的正文中,你使用random.choice()从列表jokes中选择一个随机笑话,并将其作为对 Alexa 技能前端的响应返回。

最后,通过在lambda_function.py的顶部添加一个 import 语句来导入random包:

15from ask_sdk_model import Response
16
17import random 18
19logger = logging.getLogger(__name__)
20logger.setLevel(logging.INFO)
21
22...

编辑应该这样看待这一点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试前还有最后一个改动。你需要允许 Alexa 给出一个确认技能已经被触发。为此,在LaunchRequestHandler.handle()中查找speak_output变量,并将其值设置为下面突出显示行中的文本:

45class LaunchRequestHandler(AbstractRequestHandler):
46    """Handler for Skill Launch."""
47    def can_handle(self, handler_input):
48        # type: (HandlerInput) -> bool
49
50        return ask_utils.is_request_type("LaunchRequest")(handler_input)
51
52    def handle(self, handler_input):
53        # type: (HandlerInput) -> Response
54        speak_output = "Hey there! I am a Joke Bot. You can ask me to tell you a random Joke that might just make your day better!" 55
56        return (
57            handler_input.response_builder
58                .speak(speak_output)
59                .ask(speak_output)
60                .response
61        )
62...

你的笑话机器人已经准备好进行最终测试了!单击 Deploy 按钮保存更改并返回到开发人员控制台的测试部分。这一次,当你的技能第一次被调用时,你会看到一个新的问候信息。然后,当你让机器人给你讲一个笑话时,它应该每次给你讲一个不同的笑话:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就是这样!你已经成功创建了你作为 Alexa Python 开发者的第一个技能!

结论

祝贺你迈出了 Alexa Python 开发的第一步!你现在已经成功地建立了你自己的 Alexa Python 技能。您现在知道了如何创建新技能、创建意图、编写 Python 代码来处理这些意图,并将有价值的信息返回给用户。

尝试以下方法来提升你的技能:

  • 增加后台笑话列表。
  • 创建一个名为琐事的新意图,它将用一个有趣的琐事事实来回应。
  • 向亚马逊市场发布你的技能。

可能性是无穷无尽的,所以勇往直前吧!要了解更多关于 Alexa Python 开发的信息,请查看官方文档。您还可以探索聊天机器人TweepyInstaPyDiscord 的可能性,以了解如何使用 Python 为不同平台制作机器人。*****

使用 Bootstrap 3 的有效销售页面

原文:https://realpython.com/an-effective-sales-page-with-bootstrap-3/

最初关于 Bootstrap 的两部分系列已经变成了三部分,增加了 William Ghelfi 的博客文章。你可以从这个回购中抓取最终的样式/页面。

在第一篇帖子中,我们看了一下 Bootstrap 3 的基础知识以及如何设计一个基本的网站。让我们更进一步,创建一个漂亮的销售登陆页面

关于 Bootstrap 最常见的一个误区是,你实际上不能用它来做任何如此不同和高度专业化的事情,比如销售页面。

事实就是如此。一个神话。

让我们一起用最好的方式揭穿它:建造它。

高度专业化的页面是高度专业化的

成功的销售页面遵循精确的规则。我不认识他们所有人,我也不会告诉你我所知道的每一个人。

相反,我会试着把它们减少到你可以开始试验的小而有效的药片。

Remove ads

注意力吸引和第一步

抓住你注意力的最好方法是什么?

一个加粗的大问题,后面跟着一个像样的副标题。

然后,继续简要介绍您将从客户的 a 中消除的难题…从你客户的生活(!)感谢你的产品。

使用 Bootstrap,我们可以像这样构建它:

<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <title>Bootstrap Sales Page.</title>
  <meta name="author" content="" />
  <meta name="description" content="" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>

  <header class="container">
    <div class="row">
      <div class="col-md-10 col-md-offset-1">
        <h1>Have you ever seen the rain?</h1>
        <p class="lead">You should always open a sales page with a catchy question.</p>
      </div>
    </div>
  </header>

  <div class="container">
    <div class="row">

    <div class="col-xs-12 col-md-4 col-md-offset-1">
      <p>
        Proceed then with the classic triplet: the pain, the dream, the solution.
      </p>
      <p>
        <strong>The pain</strong> ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<br /><strong>The dream</strong> enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodoconsequat. Duis aute irure dolor in reprehenderit in voluptate velit essecillum dolore eu fugiat nulla pariatur.
      </p>
      <p>
        <strong>The solution</strong> selfies semiotics keffiyeh master cleanse Vice before they sold out. Vegan 90's tofu pork belly skateboard, Truffaut tote bag.
      </p>
      <p>
        <em><strong>Interested?<br />
        <a href="#packages">Go straight to the packages.</a></strong></em>
      </p>
    </div>

      <div class="col-xs-12 col-md-6">
        <figure class="text-center">
          <img src="http://placehold.it/400x300" alt="The Amazing Product" class="img-thumbnail" />
        </figure>
      </div>

  </div>

  <hr />

  <div class="row">
  <figure class="col-md-2 col-md-offset-1">
      <img src="http://placehold.it/100x100" alt="Jonathan F. Doe" class="img-responsive img-circle pull-right" />
  </figure>
  <blockquote class="col-md-7">
      <p>
          Testimonials are important. Be sure to include some in your sales page.
      </p>
      <p>
          With Product, my sales page was <strong>online in minutes</strong>. And it rocks!.
      </p>
      <small>Jonathan F. Doe, CIO at Lorem Ipsum</small>
  </blockquote>
  </div>

</div>

</body>
</html>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不错,但是让我们添加一些定制:

<style> @import  url(http://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700|Open+Sans:400italic,700italic,400,700|Fjalla+One); body  { font-size:  18px; font-family:  "Open Sans",  Arial,  sans-serif; color:  #292b33; } h1,  h2  { font-family:  'Fjalla One',  'Helvetica Neue',  Arial,  sans-serif; font-size:  52px; font-weight:  400; text-transform:  uppercase; letter-spacing:  1px; text-align:  left; margin:  1em  0  0  0; } p  { line-height:  1.5; margin:  0  0  20px  0; } blockquote  { border-left:  none; position:  relative; } blockquote::before  { content:  '“'; position:  absolute; top:  0; left:  0; font-size:  48px; font-family:  "inherit"; font-weight:  bold; } blockquote  p  { margin:  0  0  10px  20px; font-style:  italic; font-family:  "Georgia"; } em  { font-family:  Georgia,  serif; font-size:  1.1em; } .lead  { margin-top:  0.25em; } </style>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

开始变好了!

全部在

现在是时候更好地介绍你对痛苦的解决方案了。

开始提供价值:产品的免费样品。然后列出三种不同的变型/包装,有不同的附加功能,并相应地定价。

为什么是三个?

我们只能说,在 39 美元的基本套餐和 249 美元的完整套餐之间,99 美元的中等套餐感觉既实惠又有价值。

决定顺序——从低到高,或从高到低——只是要知道你在追求什么。

如果你的目标是卖出更多的最低包装,让你的访问者在到达其他人之前到达那个包装。否则,反其道而行之。

关于这个话题的更多信息,请见 Nathan Barry 的一篇精彩的帖子。

这是代码:

 ...
    <style> ... .bg-white-dark  { border-top:  1px  solid  #cccbd6; border-bottom:  1px  solid  #cccbd6; background-color:  #e7e6f3;  /* Older Browsers */ background-color:  rgba(231,  230,  243,  0.9); } .container  { padding-top:  2em; padding-bottom:  2em; } ul  { list-style-type:  circle; } </style>
<div class="bg-white-dark" id="free-sample">
  <div class="container">
    <div class="row">

    <div class="col-md-10 col-md-offset-1">
      <h2>Get a free sample</h2>
      <p class="lead">A taste of what is included with the product.</p>
    </div>

    </div>
    <div class="row">

      <div class="col-md-10 col-md-offset-1">
        <div class="panel panel-default">
          <div class="panel-body text-center">
            <img alt="sample" src="http://placehold.it/500x250" class="img-rounded" />
            <p>Describe the sample dolor sit amet and why should I want to get it adipiscint elit.</p>
            <p>
            <a href="#" class="btn btn-lg btn-default text-uppercase">
            <span class="icon icon-download-alt"></span>&#32;
            Download the sample
            </a>
            </p>
          </div>
        </div>
      </div>

      </div>
  </div>
</div>

<div class="container" id="packages">

  <div class="row">
    <div class="col-md-12">
      <h2>
        The Complete Package
        <span class="pull-right">
            <a class="btn btn-success btn-lg" href="#">
                <span class="text-uppercase"><span class="text-white-dark">Buy now for</span> $249</span>
            </a>
        </span>
      </h2>
      <p class="lead">
        Cosby sweater cray skateboard.
      </p>
    </div>
  </div>

  <div class="row">
    <div class="media">
      <figure class="pull-left col-xs-12 col-md-4">
          <img src="http://placehold.it/300x250" alt="The Amazing Product" class="media-object img-thumbnail" />
      </figure>
      <div class="media-body col-xs-12 col-md-7">
        <h3 class="media-heading">The best package for lorem ipsumer</h3>
        <p>
            Mustache farm-to-table deep v cardigan, Banksy Godard roof party PBR&amp;B.
        </p>
        <ul>
            <li>Details</li>
            <li>Lorem ipsum</li>
            <li>Nostrud exercitation</li>
            <li>Resources ipsum</li>
            <li>Adipiscit resource</li>
            <li>Resource numquam</li>
            <li>Resources ipsum</li>
            <li>Adipiscit resource</li>
            <li>Resource numquam</li>
        </ul>
      </div>
    </div>
  </div>

</div>

<div class="bg-white-dark">
  <div class="container">

  <div class="row">
    <div class="col-md-12">
      <h2>
        The Amazing Product + Resources
        <span class="pull-right">
            <a class="btn btn-success btn-lg" href="#">
                <span class="text-uppercase"><span class="text-white-dark">Buy now for</span> $99</span>
            </a>
        </span>
      </h2>
        <p class="lead">
            Cosby sweater cray skateboard.
        </p>
    </div>
  </div>

      <div class="row">
        <div class="media">
          <figure class="pull-left col-xs-12 col-md-4">
              <img src="http://placehold.it/300x250" alt="The Amazing Product" class="media-object img-thumbnail" />
          </figure>
          <div class="media-body col-xs-12 col-md-7">
              <h3 class="media-heading">Perfect for nostrud lorem</h3>
              <p>
                  Mustache farm-to-table deep v cardigan, Banksy Godard roof party PBR&amp;B.
              </p>
              <ul>
                  <li>Details</li>
                  <li>Lorem ipsum</li>
                  <li>Nostrud exercitation</li>
                  <li>Resources ipsum</li>
              </ul>
            </div>
        </div>
      </div>

  </div>
</div>

<div class="container">

  <div class="row">
      <div class="col-md-12">
        <h2>
            The Amazing Product
            <span class="pull-right">
                <a class="btn btn-success btn-lg" href="#">
                    <span class="text-uppercase"><span class="text-white-dark">Buy now for</span> $39</span>
                </a>
            </span>
        </h2>
        <p class="lead">
            Cosby sweater cray skateboard.
        </p>
      </div>
  </div>

  <div class="row">
    <div class="media">
      <figure class="pull-left col-xs-12 col-md-4">
          <img src="http://placehold.it/300x250" alt="The Amazing Product" class="media-object img-thumbnail" />
      </figure>
      <div class="media-body col-xs-12 col-md-7">
        <h3 class="media-heading">The budget option</h3>
        <p>
            Mustache farm-to-table deep v cardigan, Banksy Godard roof party PBR&amp;B.
        </p>
        <p>
            Cliche sartorial roof party, shabby chic sustainable VHS food truck 90's four loko. Etsy hoodie     distillery, organic beard DIY cliche.
        </p>
      </div>
    </div>
  </div>

</div>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传Remove ads

造型更多

我们差不多完成了,但是即使我们用.bg-white-dark添加了颜色变化——顺便说一下,它实际上是淡紫色的,因为使用像.bg-white-dark这样的名字,你可以切换到你喜欢的颜色,而不必改变类名——整体的外观&感觉还可以进一步改进,并且它可以与基本的引导更加不同。

让我们添加更多的样式:

...
<style> ... .text-white-dark  { color:  #e7e6f3; } .btn-default  { font-family:  "Fjalla One",  sans-serif; text-shadow:  0  -1px  0  rgba(0,  0,  0,  0.2); -webkit-box-shadow:  inset  0  1px  0  rgba(255,  255,  255,  0.15),  0  1px  1px  rgba(0,  0,  0,  0.075); box-shadow:  inset  0  1px  0  rgba(255,  255,  255,  0.15),  0  1px  1px  rgba(0,  0,  0,  0.075); } .btn-default:active, .btn-default.active  { -webkit-box-shadow:  inset  0  3px  5px  rgba(0,  0,  0,  0.125); box-shadow:  inset  0  3px  5px  rgba(0,  0,  0,  0.125); } .btn:active, .btn.active  { background-image:  none; } .btn-default  { text-shadow:  0  1px  0  #fff; background-image:  -webkit-gradient(linear,  left  0%,  left  100%,  from(#ffffff),  to(#e6e6e6)); background-image:  -webkit-linear-gradient(top,  #ffffff,  0%,  #e6e6e6,  100%); background-image:  -moz-linear-gradient(top,  #ffffff  0%,  #e6e6e6  100%); background-image:  linear-gradient(to  bottom,  #ffffff  0%,  #e6e6e6  100%); background-repeat:  repeat-x; border-color:  #e0e0e0; border-color:  #ccc; filter:  progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',  endColorstr='#ffe6e6e6',  GradientType=0); } .btn-default:active, .btn-default.active  { background-color:  #e6e6e6; border-color:  #e0e0e0; } .btn-success  { font-family:  "Fjalla One",  sans-serif; } .btn-success  { background:  #9292c0;  /* Old browsers */ background:  -moz-linear-gradient(top,  #9292c0  0%,  #8181b7  100%);  /* FF3.6+ */ background:  -webkit-gradient(linear,  left  top,  left  bottom,  color-stop(0%,#9292c0),  color-stop(100%,#8181b7));  /* Chrome,Safari4+ */ background:  -webkit-linear-gradient(top,  #9292c0  0%,#8181b7  100%);  /* Chrome10+,Safari5.1+ */ background:  -o-linear-gradient(top,  #9292c0  0%,#8181b7  100%);  /* Opera 11.10+ */ background:  -ms-linear-gradient(top,  #9292c0  0%,#8181b7  100%);  /* IE10+ */ background:  linear-gradient(to  bottom,  #9292c0  0%,#8181b7  100%);  /* W3C */ filter:  progid:DXImageTransform.Microsoft.gradient(  startColorstr='#9292c0',  endColorstr='#8181b7',GradientType=0  );  /* IE6-9 */ border-color:  #f1ddff; } .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active  { color:  #fbfafc; background:  #a3a3d1;  /* Old browsers */ background:  -moz-linear-gradient(top,  #a3a3d1  0%,  #9292c8  100%);  /* FF3.6+ */ background:  -webkit-gradient(linear,  left  top,  left  bottom,  color-stop(0%,#a3a3d1),  color-stop(100%,#9292c8));  /* Chrome,Safari4+ */ background:  -webkit-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* Chrome10+,Safari5.1+ */ background:  -o-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* Opera 11.10+ */ background:  -ms-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* IE10+ */ background:  linear-gradient(to  bottom,  #a3a3d1  0%,#9292c8  100%);  /* W3C */ filter:  progid:DXImageTransform.Microsoft.gradient(  startColorstr='#a3a3d1',  endColorstr='#9292c8',GradientType=0  );  /* IE6-9 */ border-color:  #f1ddff; } .btn-success.disabled:hover, .btn-success[disabled]:hover, fieldset[disabled]  .btn-success:hover, .btn-success.disabled:focus, .btn-success[disabled]:focus, fieldset[disabled]  .btn-success:focus, .btn-success.disabled:active, .btn-success[disabled]:active, fieldset[disabled]  .btn-success:active, .btn-success.disabled.active, .btn-success[disabled].active, fieldset[disabled]  .btn-success.active  { color:  #fbfafc; background:  #a3a3d1;  /* Old browsers */ background:  -moz-linear-gradient(top,  #a3a3d1  0%,  #9292c8  100%);  /* FF3.6+ */ background:  -webkit-gradient(linear,  left  top,  left  bottom,  color-stop(0%,#a3a3d1),  color-stop(100%,#9292c8));  /* Chrome,Safari4+ */ background:  -webkit-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* Chrome10+,Safari5.1+ */ background:  -o-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* Opera 11.10+ */ background:  -ms-linear-gradient(top,  #a3a3d1  0%,#9292c8  100%);  /* IE10+ */ background:  linear-gradient(to  bottom,  #a3a3d1  0%,#9292c8  100%);  /* W3C */ filter:  progid:DXImageTransform.Microsoft.gradient(  startColorstr='#a3a3d1',  endColorstr='#9292c8',GradientType=0  );  /* IE6-9 */ border-color:  #f1ddff; } </style>
...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结束语

就是这样。我们用 Bootstrap 构建并定制了一个最小的销售页面。该页面看起来一点也不像老一套的自举页面,最棒的是,它实际上有助于提高销售业绩!点击查看

好了,我想现在是完全披露的时候了:我们一起构建的页面是一个真实的、经过现实生活测试的销售页面的核心。

我写了 Bootstrap In Practice ,这是一本入门电子书,目的是他们快速生产回到盈利,而不会陷入官方文件中,也不会因为纯粹的求知欲而进行过多/过早的深入研究。

你可以在我的网站上找到真实、完整、久经考验的销售页面,在那里我还提供了一个关于自举技巧的30 天免费课程以及电子书中的一个免费样本章节。

下一次,我们将添加 Flask,并创建一个好看的样板文件,您可以将其用于您自己的 web 应用程序。干杯!**

用 Python 分析英国的肥胖问题

原文:https://realpython.com/analyzing-obesity-in-england-with-python/

昨天在健身房看到一个牌子,上面写着“孩子每十年变胖一次”。在那个标志下面有一张图表,基本上显示了五年后英国儿童的平均体重将和拖拉机一样重。

我发现这个说法有点不可信,所以我决定调查一下…

数据

数据取自Data.gov.uk。我们将使用 2014 年的 XLS 文件。下载它,并在您选择的电子表格工具中打开它。

然后导航到表 7.2,因为它包含我们正在寻找的数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,在我们开始用 Pandas 分析数据之前,让我们后退一步,解决房间里的大象:如果您可以在 Excel 中执行分析/绘图,为什么您会使用 Python?

Remove ads

Python vs Excel

**应该用 Python 还是 Excel?T3】

这个问题经常被刚开始做数据分析的人问到。虽然 Python 可能在编程社区中很流行,但 Excel 在更广阔的世界中更流行。大多数高级经理、销售人员、营销人员等。使用 Excel——这没什么不好。如果你知道如何很好地使用它,它是一个很棒的工具,它已经把许多非技术人员变成了专家分析师。

对于应该使用 Python 还是 Excel,这个问题并不容易回答。但是最后没有非此即彼:反而可以一起用。

Excel 非常适合查看数据、执行基本分析和绘制简单的图表,但它真的不适合清理数据(除非你愿意深入 VBA)。如果您有一个 500MB 的 Excel 文件,其中缺少数据、日期格式不同、没有标题,那么您将永远无法手工清理它。如果您的数据分散在十几个 CSV 文件中,情况也是如此,这很常见。

使用 Python 和用于数据分析的 Python 库 Pandas 来做所有这些清理工作是微不足道的。Pandas 建立在 Numpy 之上,使高级任务变得简单,你可以将你的结果写回 Excel 文件,这样你就可以继续与非程序员分享你的分析结果。

因此,虽然 Excel 不会消失,但如果您想要干净的数据并执行更高级别的数据分析,Python 是一个很好的工具。

代码

好了,让我们从代码开始——你可以从项目回购以及我上面链接的电子表格中获取,这样你就不用再下载了。

首先创建一个名为 obesity.py 的新脚本,并导入熊猫以及 matplotlib ,以便我们可以稍后绘制图形:

import pandas as pd
import matplotlib.pyplot as plt

确保安装了两个依赖项:pip install pandas matplotlib

接下来,让我们读入Excel 文件:

data = pd.ExcelFile("Obes-phys-acti-diet-eng-2014-tab.xls")

仅此而已。在一行中,我们读入了整个 Excel 文件。

让我们把现有的打印出来:

print data.sheet_names

运行脚本。

$ python obesity.py
[u'Chapter 7', u'7.1', u'7.2', u'7.3', u'7.4', u'7.5', u'7.6', u'7.7', u'7.8', u'7.9', u'7.10']

眼熟吗?这些是我们之前看到的床单。请记住,我们将重点关注第 7.2 页。现在,如果你在 Excel 中查看 7.2,你会看到上面的 4 行和下面的 14 行包含无用的信息。让我换个说法:它对人类有用,但对我们的脚本没用。我们只需要 5-18 排。

Remove ads

清理

所以当我们阅读表格时,我们需要确保任何不必要的信息都被忽略。

# Read 2nd section, by age
data_age = data.parse(u'7.2', skiprows=4, skipfooter=14)
print data_age

再次运行。

 Unnamed: 0  Total  Under 16  16-24  25-34  35-44  45-54  55-64  65-74  \
0         NaN    NaN       NaN    NaN    NaN    NaN    NaN    NaN    NaN
1     2002/03   1275       400     65    136    289    216     94     52
2     2003/04   1711       579     67    174    391    273    151     52
3     2004/05   2035       547    107    287    487    364    174     36
4     2005/06   2564       583     96    341    637    554    258     72

#...snip...#

我们阅读表格,跳过最上面的 4 行和最下面的 14 行(因为它们包含对我们没有用的数据)。然后我们把现有的打印出来。(为了简单起见,我只显示了打印输出的前几行。)

第一行代表列标题。你可以马上看到熊猫很聪明,因为它正确地捡起了大多数头球。当然除了第一个——比如Unnamed: 0。这是为什么呢?简单。在 Excel 中查看该文件,您会发现它缺少一个年份标题。

另一个问题是我们在原始文件中有一个空行,它显示为NaN(不是一个数字)。

所以我们现在需要做两件事:

  1. 将第一个标题重命名为Year,并且
  2. 去掉所有的空行。
# Rename unamed to year
data_age.rename(columns={u'Unnamed: 0': u'Year'}, inplace=True)

在这里,我们告诉熊猫将列重命名为年份*。使用内置的功能 rename()。*

inplace = True修改现有对象。如果没有这个,熊猫将创建一个新的对象并返回它。

接下来让我们删除填充有NaN的空行:

# Drop empties
data_age.dropna(inplace=True)

我们还需要做一件事,让我们的生活更轻松。如果查看 data_age 表,第一个值是一个数字。这是索引,Pandas 使用默认的 Excel 惯例,将一个数字作为索引。然而,我们想把指数改成。这将使绘制更加容易,因为指数通常被绘制为 x 轴。

data_age.set_index('Year', inplace=True)

我们将索引设置为Year

现在打印我们清理的数据:

print "After Clean up:"
print data_age

并运行:

 Total  Under 16  16-24  25-34  35-44  45-54  55-64  65-74  \
Year
2002/03   1275       400     65    136    289    216     94     52
2003/04   1711       579     67    174    391    273    151     52
2004/05   2035       547    107    287    487    364    174     36
2005/06   2564       583     96    341    637    554    258     72
#...snip...#

好多了。你可以看到索引现在是Year,所有的NaN都不见了。

Remove ads

图表

现在我们可以绘制我们所拥有的。

# Plot
data_age.plot()
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

哎呀。有一个问题:我们的原始数据包含一个总字段,它掩盖了所有其他内容。我们需要摆脱它。

# Drop the total column and plot
data_age_minus_total = data_age.drop('Total', axis=1)

axis =1有点令人困惑,但它真正的意思是——丢弃列,正如从这个堆栈溢出问题中描述的那样。

让我们画出我们现在拥有的。

data_age_minus_total.plot()
plt.show()

好多了。我们现在实际上可以看到各个年龄组。你能看出哪个年龄段的肥胖率最高吗?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

回到我们最初的问题:孩子越来越胖了吗?

我们只绘制一小部分数据:16 岁以下的儿童和 35-44 岁年龄段的成年人。

plt.close()

# Plot children vs adults
data_age['Under 16'].plot(label="Under 16")
data_age['35-44'].plot(label="35-44")
plt.legend(loc="upper right")
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那么到底是谁越来越胖呢?

没错。我们看到了什么?

虽然儿童肥胖率略有下降,但他们的父母却在不断增加。所以看起来父母需要担心的是他们自己而不是他们的孩子。

但是未来呢?

图表仍然没有告诉我们未来儿童肥胖会发生什么。有很多方法可以将这样的图表外推至未来,但在我们继续之前,我必须给出一个警告:肥胖数据没有潜在的数学基础。也就是说,我们找不到一个公式来预测这些值在未来会如何变化。一切本质上都是猜测。记住这个警告,让我们看看如何推断我们的图表。

首先, Scipy 确实提供了一个用于外推的函数,但是它只对单调增加的数据有效(当我们的数据上下波动时)。

我们可以尝试曲线拟合:

  • 曲线拟合通过尝试为数据生成一个数学函数,尝试通过图表上的点来拟合一条曲线。该函数可能非常准确,也可能不准确,这取决于数据。
  • 多项式插值一旦有了方程,就可以用多项式插值来尝试插值图上的任意值。

我们将结合使用这两个函数来尝试预测英国儿童的未来:

kids_values = data_age['Under 16'].values
x_axis = range(len(kids_values))

这里,我们提取 16 岁以下儿童的值。对于 x 轴,原始图表有日期。为了简化我们的图表,我们将只使用数字 0-10。

输出:

array([ 400.,  579.,  547.,  583.,  656.,  747.,  775.,  632.,  525., 495.,  556.])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

还有一点:曲线拟合使用不同的次多项式。很简单的说,度数越高,曲线拟合会越准确,但是也有可能结果是垃圾。如果度数太高,Scipy 有时会警告你。不要担心,当我们看一些例子时,这一点会更加清楚。

poly_degree = 3
curve_fit = np.polyfit(x_axis, kids_values, poly_degree)
poly_interp = np.poly1d(curve_fit)

我们将多项式次数设置为 3。然后,我们使用 Numpy polyfit() 函数试图通过我们拥有的数据拟合一个图形。然后在我们生成的等式上调用poly1d() 函数来创建一个函数,该函数将用于生成我们的值。这将返回一个名为poly_interp的函数,我们将在下面使用它:

poly_fit_values = []

for i in range(len(x_axis)):
    poly_fit_values.append(poly_interp(i))

我们从 0 到 10 循环,并对每个值调用poly_interp()函数。记住,这是我们运行曲线拟合算法时生成的函数。

在继续之前,让我们看看不同的多项式次数意味着什么。

我们将绘制原始数据和我们自己的数据,看看我们的方程与理想数据有多接近:

plt.plot(x_axis, poly_fit_values, "-r", label = "Fitted")
plt.plot(x_axis, kids_values, "-b", label = "Orig")

plt.legend(loc="upper right")

原始数据将以蓝色绘制,并标记为原始,而生成的数据将以红色绘制,并标记为拟合

多项式值为 3:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们发现它不是很合适,所以让我们试试 5:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

好多了。7 点怎么样?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们得到了一个几乎完美的匹配。那么,为什么我们不总是使用更高的值呢?

因为较高的值与该图紧密相关,所以它们使预测变得无用。如果我们试图从上图中推断,我们会得到垃圾值。尝试不同的值,我发现 3 和 4 的多项式次数是唯一给出准确结果的,所以这就是我们将要使用的。

我们将重新运行我们的poly_interp()函数,这次是从 0-15 的值,来预测未来五年。

x_axis2 = range(15)

poly_fit_values = []
for i in range(len(x_axis2)):
    poly_fit_values.append(poly_interp(i))

这是和以前一样的代码。让我们再次看看多项式次数为 3 和 4 的结果。新的外推线是绿色的线,显示了我们的预测。

用 3:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,肥胖率正在下降。4 点怎么样?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是在这里,它正在迅速增长,所以孩子们最终会像拖拉机一样重!

这两幅图哪一幅是正确的?这取决于你是为政府工作还是为反对派工作。

这其实是特性,不是 bug。你一定听说过这些政治辩论,其中双方从相同的数据中得出完全相反的结论?现在你看到了通过调整小参数来得出完全不同的结论的可能性。

这就是我们在接受游说者的数字和图表时必须小心的原因,尤其是如果他们不愿意分享原始数据的话。有时候,预测最好留给占星家。

干杯!***

如何在 Python 中使用 any()

原文:https://realpython.com/any-python/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。和写好的教程一起看,加深理解: Python any(): Powered Up 布尔函数

作为一名 Python 程序员,你会经常处理布尔值条件语句——有时非常复杂。在这些情况下,您可能需要依赖能够简化逻辑和整合信息的工具。好在 Python 中的 any() 就是这样一个工具。它遍历 iterable 中的元素,并返回一个值,指示在布尔上下文中是否有任何元素为 true,或 truthy。

在本教程中,您将学习:

  • 如何使用any()
  • 如何在any()or之间做出决定

让我们开始吧!

Python 中途站:本教程是一个快速实用的方法来找到你需要的信息,所以你会很快回到你的项目!

免费奖励: 并学习 Python 3 的基础知识,如使用数据类型、字典、列表和 Python 函数。

如何在 Python 中使用any()

想象一下,你正在为雇主的招聘部门编写一个程序。您可能希望安排符合以下任何标准的候选人参加面试:

  1. 已经了解 Python 了
  2. 有五年或五年以上的开发经验
  3. 有学位

可以用来编写这个条件表达式的一个工具是 or :

# recruit_developer.py
def schedule_interview(applicant):
    print(f"Scheduled interview with {applicant['name']}")

applicants = [
    {
        "name": "Devon Smith",
        "programming_languages": ["c++", "ada"],
        "years_of_experience": 1,
        "has_degree": False,
        "email_address": "devon@email.com",
    },
    {
        "name": "Susan Jones",
        "programming_languages": ["python", "javascript"],
        "years_of_experience": 2,
        "has_degree": False,
        "email_address": "susan@email.com",
    },
    {
        "name": "Sam Hughes",
        "programming_languages": ["java"],
        "years_of_experience": 4,
        "has_degree": True,
        "email_address": "sam@email.com",
    },
]
for applicant in applicants:
    knows_python = "python" in applicant["programming_languages"]
    experienced_dev = applicant["years_of_experience"] >= 5

    meets_criteria = (
        knows_python
        or experienced_dev
        or applicant["has_degree"]
    )
    if meets_criteria:
        schedule_interview(applicant)

在上面的例子中,您检查每个申请人的证书,如果申请人符合您的三个标准中的任何一个,就安排面试。

技术细节: Python 的any()or并不局限于计算布尔表达式。相反,Python 对每个参数执行真值测试,评估表达式是 真值还是假值 。例如,非零整数值被认为是真的,而零被认为是假的:

>>> 1 or 0
1

在本例中,or将非零值1评估为真值,即使它不属于布尔类型。or回归1,无需评价0的真实性。在本教程的后面,您将了解更多关于or的返回值和参数求值。

如果您执行这段代码,那么您将看到 Susan 和 Sam 将获得面试机会:

$ python recruit_developer.py
Scheduled interview with Susan Jones
Scheduled interview with Sam Hughes

该计划选择与苏珊和萨姆安排面试的原因是苏珊已经知道 Python 和萨姆有学位。注意每个候选人只需要满足一个标准。

评估申请人资格的另一种方法是使用any()。当您在 Python 中使用any()时,您必须将申请人的证书作为可迭代参数传递:

for applicant in applicants:
    knows_python = "python" in applicant["programming_languages"]
    experienced_dev = applicant["years_of_experience"] >= 5

    credentials = (
        knows_python,
        experienced_dev,
        applicant["has_degree"],
    )
    if any(credentials):
        schedule_interview(applicant)

当您在 Python 中使用any()时,请记住您可以将任何 iterable 作为参数传递:

>>> any([0, 0, 1, 0])
True

>>> any(set((True, False, True)))
True

>>> any(map(str.isdigit, "hello world"))
False

在每个例子中,any()循环遍历不同的 Python iterable,测试每个元素的真实性,直到找到一个真值或检查每个元素。

**注意:**最后一个例子使用 Python 内置的 map() ,返回一个迭代器,其中每个元素都是将字符串中的下一个字符传递给str.isdigit()的结果。这是使用any()进行更复杂检查的有效方法。

你可能想知道any()是否仅仅是or的装扮版。在下一节中,您将了解这些工具之间的区别。

Remove ads

如何区分orany()

Python 中的orany()有两个主要区别:

  1. 句法
  2. 返回值

首先,您将了解语法如何影响每个工具的可用性和可读性。其次,您将了解每个工具返回的值的类型。了解这些差异将有助于您决定哪种工具最适合给定的情况。

语法

or是一个操作符,所以它有两个参数,一边一个:

>>> True or False
True

另一方面,any()是一个接受一个参数的函数,一个对象的 iterable,它通过循环来评估真实性:

>>> any((False, True))
True

这种语法上的差异非常显著,因为它会影响每个工具的可用性和可读性。例如,如果您有一个 iterable,那么您可以将 iterable 直接传递给any()。要从or获得类似的行为,你需要使用一个循环或者一个类似 reduce() 的函数:

>>> import functools
>>> functools.reduce(lambda x, y: x or y, (True, False, False))
True

在上面的例子中,您使用了 reduce() 将一个 iterable 作为参数传递给or。用any可以更有效地做到这一点,它直接接受 iterables 作为参数。

为了说明每个工具的语法影响其可用性的另一种方式,假设您想要避免测试一个条件,如果任何前面的条件是True:

def knows_python(applicant):
    print(f"Determining if {applicant['name']} knows Python...")
    return "python" in applicant["programming_languages"]

def is_local(applicant):
    print(f"Determine if {applicant['name']} lives near the office...")

should_interview = knows_python(applicant) or is_local(applicant)

如果is_local()执行的时间相对较长,那么当knows_python()已经返回True时,你就不要调用它了。这叫做评估,或者 短路评估。默认情况下,or延迟评估条件,而any不会。

在上面的例子中,程序甚至不需要确定 Susan 是否是本地人,因为它已经确认她知道 Python。这足够安排一次面试了。在这种情况下,用or延迟调用函数将是最有效的方法。

为什么不用any()来代替?您在上面了解到any()将 iterable 作为参数,Python 根据 iterable 类型评估条件。因此,如果您使用一个列表,Python 将在创建该列表期间执行knows_python()is_local(),然后调用any():

should_interview = any([knows_python(applicant), is_local(applicant)])

在这里,Python 会为每一个申请人调用is_local(),即使是懂 Python 的人。因为is_local()将花费很长时间来执行,并且有时是不必要的,这是一个低效的逻辑实现。

在使用 iterables 时,有一些方法可以让 Python 延迟调用函数,比如用map()构建一个迭代器,或者使用生成器表达式:

any((meets_criteria(applicant) for applicant in applicants))

这个例子使用了一个生成器表达式来生成布尔值,表明申请人是否符合面试标准。一旦申请人符合标准,any()将返回True,而不检查其余的申请人。但是请记住,这些类型的解决方法也有其自身的问题,并不适合每种情况。

需要记住的最重要的事情是,any()or之间的语法差异会影响它们的可用性。

语法并不是影响这些工具可用性的唯一差异。接下来,让我们看看any()or的不同返回值,以及它们如何影响您决定使用哪个工具。

Remove ads

返回值

Python 的any()or返回不同类型的值。any()返回一个 Boolean 值,该值指示是否在 iterable 中找到了真值:

>>> any((1, 0))
True

在这个例子中,any()找到了一个真值(整数1,所以它返回了布尔值True

另一方面,or返回它找到的第一个真值,不一定是布尔值。如果没有真值,那么or返回最后一个值:

>>> 1 or 0
1

>>> None or 0
0

在第一个例子中,or评估了1,它是真的,并且在不评估0的情况下返回它。第二个例子中,None是 falsy,所以or接下来对0求值,也是 falsy。但是因为没有更多的表达式要检查,or返回最后一个值,0

当您决定使用哪个工具时,考虑您是否想知道对象的实际值或者只是真值是否存在于对象集合中的某个地方是很有帮助的。

结论

恭喜你!您已经了解了在 Python 中使用any()的来龙去脉,以及any()or之间的区别。随着对这两种工具理解的加深,您已经准备好在自己的代码中做出选择。

你现在知道了:

  • 如何在 Python 中使用 any()
  • 为什么你会用 any() 而不是 or

如果您想继续学习条件表达式以及如何使用 Python 中的orany()等工具,那么您可以查看以下资源:

立即观看**本教程有真实 Python 团队创建的相关视频课程。和写好的教程一起看,加深理解: Python any(): Powered Up 布尔函数***

Python 和 REST APIs:与 Web 服务交互

原文:https://realpython.com/api-integration-in-python/

网上有数量惊人的数据。许多网络服务,如 YouTube 和 GitHub,通过应用编程接口(API) 让第三方应用程序可以访问它们的数据。构建 API 最流行的方式之一是 REST 架构风格。Python 提供了一些很棒的工具,不仅可以从 REST API 获取数据,还可以构建自己的 Python REST APIs。

在本教程中,您将学习:

  • 什么是 REST 架构
  • REST APIs 如何提供对 web 数据的访问
  • 如何使用 requests 库消费 REST APIs 中的数据
  • 构建 REST API 需要采取什么步骤
  • 一些流行的 Python 工具用于构建 REST APIs

通过使用 Python 和 REST APIs,您可以检索、解析、更新和操作您感兴趣的任何 web 服务提供的数据。

免费奖励: ,并获得 Python + REST API 原则的实际操作介绍以及可操作的示例。

REST 架构

REST 代表representationsstatettransfer是一种软件架构风格,它定义了网络上客户机和服务器通信的模式。REST 为软件架构提供了一组约束,以提高系统的性能、可伸缩性、简单性和可靠性。

REST 定义了以下架构约束:

  • **无状态:**服务器不会在来自客户端的请求之间维护任何状态。
  • **客户机-服务器:**客户机和服务器必须相互解耦,允许各自独立开发。
  • **可缓存:**从服务器检索的数据应该可以被客户端或服务器缓存。
  • **统一接口:**服务器将提供统一的接口来访问资源,而不需要定义它们的表示。
  • **分层系统:**客户端可以通过代理负载均衡器等其他层间接访问服务器上的资源。
  • **按需编码(可选)😗*服务器可能会将自己可以运行的代码转移到客户端,比如针对单页面应用的 JavaScript

注意,REST 不是一个规范,而是一套关于如何构建网络连接软件系统的指南。

Remove ads

REST APIs 和 Web 服务

REST web 服务是任何遵守 REST 架构约束的 web 服务。这些 web 服务通过 API 向外界公开它们的数据。REST APIs 通过公共 web URLs 提供对 web 服务数据的访问。

例如,下面是 GitHub 的 REST API 的 URL 之一:

https://api.github.com/users/<username>

这个 URL 允许您访问特定 GitHub 用户的信息。您通过向特定的 URL 发送一个 HTTP 请求并处理响应来访问来自 REST API 的数据。

HTTP 方法

REST APIs 监听 HTTP 方法,如GETPOSTDELETE,以了解在 web 服务的资源上执行哪些操作。资源是 web 服务中可用的任何数据,可以通过对 REST API 的 HTTP 请求来访问和操作。HTTP 方法告诉 API 在资源上执行哪个操作。

虽然有许多 HTTP 方法,但下面列出的五种方法是 REST APIs 中最常用的:

HTTP 方法描述
GET检索现有资源。
POST创建新资源。
PUT更新现有资源。
PATCH部分更新现有资源。
DELETE删除资源。

REST API 客户端应用程序可以使用这五种 HTTP 方法来管理 web 服务中资源的状态。

状态代码

一旦 REST API 接收并处理一个 HTTP 请求,它将返回一个 HTTP 响应。这个响应中包含一个 HTTP 状态代码。此代码提供了有关请求结果的信息。向 API 发送请求的应用程序可以检查状态代码,并根据结果执行操作。这些操作可能包括处理错误或向用户显示成功消息。

下面是 REST APIs 返回的最常见的状态代码列表:

密码意义描述
200请求的操作成功。
201创造创建了新资源。
202可接受的请求已收到,但尚未进行修改。
204没有内容请求成功,但响应没有内容。
400错误的请求请求格式不正确。
401未经授权的客户端无权执行请求的操作。
404未发现找不到请求的资源。
415不支持的媒体类型服务器不支持请求数据格式。
422不可处理实体请求数据格式正确,但包含无效或丢失的数据。
500内部服务器错误处理请求时,服务器出现错误。

这十个状态代码仅代表可用的 HTTP 状态代码的一小部分。状态代码根据结果的类别进行编号:

代码范围种类
2xx成功的手术
3xx重寄
4xx客户端错误
5xx服务器错误

当使用 REST APIs 时,HTTP 状态代码很方便,因为您经常需要根据请求的结果执行不同的逻辑。

API 端点

REST API 公开了一组公共 URL,客户端应用程序使用这些 URL 来访问 web 服务的资源。在 API 的上下文中,这些 URL 被称为端点

为了帮助澄清这一点,请看下表。在这个表格中,您将看到一个假想的 CRM 系统的 API 端点。这些端点用于代表系统中潜在customers的客户资源:

HTTP 方法API 端点描述
GET/customers获取客户名单。
GET/customers/<customer_id>获得单个客户。
POST/customers创建新客户。
PUT/customers/<customer_id>更新客户。
PATCH/customers/<customer_id>部分更新客户。
DELETE/customers/<customer_id>删除客户。

上面的每个端点基于 HTTP 方法执行不同的操作。

**注意:**为简洁起见,省略了端点的基本 URL。实际上,您需要完整的 URL 路径来访问 API 端点:

https://api.example.com/customers

这是您用来访问此端点的完整 URL。基本 URL 是除了/customers之外的所有内容。

你会注意到一些端点的末尾有<customer_id>。这个符号意味着您需要在 URL 后面添加一个数字customer_id来告诉 REST API 您想要使用哪个customer

上面列出的端点仅代表系统中的一种资源。生产就绪的 REST APIs 通常有数十甚至数百个不同的端点来管理 web 服务中的资源。

Remove ads

REST 和 Python:消费 API

为了编写与 REST APIs 交互的代码,大多数 Python 开发人员求助于 requests 来发送 HTTP 请求。这个库抽象出了进行 HTTP 请求的复杂性。这是少数几个值得作为标准库的一部分来对待的项目之一。

要开始使用requests,需要先安装。您可以使用 pip 来安装它:

$ python -m pip install requests

现在您已经安装了requests,您可以开始发送 HTTP 请求了。

获取

GET是使用 REST APIs 时最常用的 HTTP 方法之一。该方法允许您从给定的 API 中检索资源。GET是一个只读的操作,所以您不应该使用它来修改现有的资源。

为了测试本节中的GET和其他方法,您将使用一个名为 JSONPlaceholder 的服务。这个免费服务提供了虚假的 API 端点,这些端点发送回requests可以处理的响应。

为了进行测试,启动 Python REPL 并运行以下命令向 JSONPlaceholder 端点发送一个GET请求:

>>> import requests
>>> api_url = "https://jsonplaceholder.typicode.com/todos/1"
>>> response = requests.get(api_url)
>>> response.json()
{'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}

这段代码调用requests.get()/todos/1发送一个GET请求,后者用 ID 为1todo项进行响应。然后可以在response对象上调用 .json() 来查看从 API 返回的数据。

响应数据被格式化为 JSON ,一个类似于 Python 字典的键值存储。这是一种非常流行的数据格式,也是大多数 REST APIs 事实上的交换格式。

除了从 API 查看 JSON 数据,您还可以查看关于response的其他内容:

>>> response.status_code
200

>>> response.headers["Content-Type"]
'application/json; charset=utf-8'

在这里,您访问response.status_code来查看 HTTP 状态代码。您还可以使用response.headers查看响应的 HTTP 头。这个字典包含关于响应的元数据,比如响应的Content-Type

帖子

现在,看看如何使用 REST API 的requestsPOST数据来创建新资源。您将再次使用 JSONPlaceholder,但是这次您将在请求中包含 JSON 数据。这是您将发送的数据:

{ "userId":  1, "title":  "Buy milk", "completed":  false }

该 JSON 包含一个新的todo项目的信息。回到 Python REPL,运行下面的代码来创建新的todo:

>>> import requests
>>> api_url = "https://jsonplaceholder.typicode.com/todos"
>>> todo = {"userId": 1, "title": "Buy milk", "completed": False}
>>> response = requests.post(api_url, json=todo)
>>> response.json()
{'userId': 1, 'title': 'Buy milk', 'completed': False, 'id': 201}

>>> response.status_code
201

这里,您调用requests.post()在系统中创建新的todo

首先,创建一个包含您的todo数据的字典。然后你将这个字典传递给requests.post()json关键字参数。当您这样做时,requests.post()自动将请求的 HTTP 头Content-Type设置为application/json。它还将todo序列化为一个 JSON 字符串,并将其附加到请求体中。

如果您不使用json关键字参数来提供 JSON 数据,那么您需要相应地设置Content-Type并手动序列化 JSON。下面是前面代码的等效版本:

>>> import requests
>>> import json >>> api_url = "https://jsonplaceholder.typicode.com/todos"
>>> todo = {"userId": 1, "title": "Buy milk", "completed": False}
>>> headers =  {"Content-Type":"application/json"} >>> response = requests.post(api_url, data=json.dumps(todo), headers=headers) >>> response.json()
{'userId': 1, 'title': 'Buy milk', 'completed': False, 'id': 201}

>>> response.status_code
201

在这段代码中,您添加了一个包含设置为application/json的单个标题Content-Typeheaders字典。这告诉 REST API 您正在发送带有请求的 JSON 数据。

然后调用requests.post(),但不是将todo传递给json参数,而是首先调用json.dumps(todo)来序列化它。在它被序列化之后,你把它传递给data关键字参数。data参数告诉requests请求中包含什么数据。您还可以将headers字典传递给requests.post()来手动设置 HTTP 头。

当您像这样调用requests.post()时,它与前面的代码具有相同的效果,但是您可以对请求进行更多的控制。

注: json.dumps() 来自于标准库中的 json 包。这个包提供了在 Python 中使用 JSON 的有用方法。

一旦 API 响应,您就调用response.json()来查看 JSON。JSON 包括为新的todo生成的id201状态代码告诉您一个新的资源已经创建。

Remove ads

除了GETPOSTrequests还提供了对所有其他 HTTP 方法的支持,这些方法可以和 REST API 一起使用。下面的代码发送一个PUT请求,用新数据更新现有的todo。通过PUT请求发送的任何数据将完全替换todo的现有值。

您将使用与用于GETPOST相同的 JSONPlaceholder 端点,但是这次您将把10附加到 URL 的末尾。这告诉 REST API 您想要更新哪个todo:

>>> import requests
>>> api_url = "https://jsonplaceholder.typicode.com/todos/10"
>>> response = requests.get(api_url)
>>> response.json()
{'userId': 1, 'id': 10, 'title': 'illo est ... aut', 'completed': True}

>>> todo = {"userId": 1, "title": "Wash car", "completed": True} >>> response = requests.put(api_url, json=todo) >>> response.json() {'userId': 1, 'title': 'Wash car', 'completed': True, 'id': 10}

>>> response.status_code
200

在这里,首先调用requests.get()来查看现有todo的内容。接下来,用新的 JSON 数据调用requests.put()来替换现有的待办事项值。调用response.json()时可以看到新的值。成功的PUT请求将总是返回200而不是201,因为您不是在创建一个新的资源,而是在更新一个现有的资源。

补丁

接下来,您将使用requests.patch()来修改现有todo上特定字段的值。PATCHPUT的不同之处在于,它不会完全取代现有的资源。它只修改与请求一起发送的 JSON 中设置的值。

您将使用上一个示例中的相同的todo来测试requests.patch()。以下是当前值:

{'userId': 1, 'title': 'Wash car', 'completed': True, 'id': 10}

现在您可以用新值更新title:

>>> import requests
>>> api_url = "https://jsonplaceholder.typicode.com/todos/10"
>>> todo = {"title": "Mow lawn"} >>> response = requests.patch(api_url, json=todo) >>> response.json()
{'userId': 1, 'id': 10, 'title': 'Mow lawn', 'completed': True}

>>> response.status_code
200

当你调用response.json()时,你可以看到title被更新为Mow lawn

删除

最后但同样重要的是,如果您想完全删除一个资源,那么您可以使用DELETE。下面是删除一个todo的代码:

>>> import requests
>>> api_url = "https://jsonplaceholder.typicode.com/todos/10"
>>> response = requests.delete(api_url) >>> response.json()
{}

>>> response.status_code
200

您用一个 API URL 调用requests.delete(),该 URL 包含您想要删除的todo的 ID。这将向 REST API 发送一个DELETE请求,然后 REST API 移除匹配的资源。删除资源后,API 发送回一个空的 JSON 对象,表明资源已经被删除。

requests库是使用 REST APIs 的一个很棒的工具,也是 Python 工具箱中不可或缺的一部分。在下一节中,您将改变思路,考虑如何构建 REST API。

REST 和 Python:构建 API

REST API 设计是一个巨大的话题,有很多层。如同技术领域的大多数事情一样,对于构建 API 的最佳方法有各种各样的观点。在本节中,您将看到一些在构建 API 时推荐遵循的步骤。

身份资源

构建 REST API 的第一步是识别 API 将管理的资源。通常将这些资源描述为复数名词,如customerseventstransactions。当您在 web 服务中识别不同的资源时,您将构建一个名词列表,描述用户可以在 API 中管理的不同数据。

当您这样做时,请确保考虑任何嵌套的资源。例如,customers可能有sales,或者events可能包含guests。当您定义 API 端点时,建立这些资源层次结构将会有所帮助。

Remove ads

定义您的端点

一旦您确定了 web 服务中的资源,您将希望使用这些资源来定义 API 端点。下面是一些您可能在支付处理服务的 API 中找到的transactions资源的端点示例:

HTTP 方法API 端点描述
GET/transactions获取交易列表。
GET/transactions/<transaction_id>获得单笔交易。
POST/transactions创建新的交易记录。
PUT/transactions/<transaction_id>更新交易记录。
PATCH/transactions/<transaction_id>部分更新交易记录。
DELETE/transactions/<transaction_id>删除交易记录。

这六个端点涵盖了您需要在 web 服务中创建、读取、更新和删除transactions的所有操作。基于用户可以使用 API 执行的操作,web 服务中的每个资源都有一个类似的端点列表。

**注意:**端点不应该包含动词。相反,您应该选择适当的 HTTP 方法来传达端点的操作。例如,下面的端点包含一个不需要的动词:

GET /getTransactions

这里,get在不需要的时候包含在端点中。HTTP 方法GET已经通过指示动作为端点提供了语义。您可以从端点中删除get:

GET /transactions

这个端点只包含一个复数名词,HTTP 方法GET传递动作。

现在来看一个嵌套资源端点的例子。在这里,您将看到嵌套在events资源下的guests的端点:

HTTP 方法API 端点描述
GET/events/<event_id>/guests弄一份宾客名单。
GET/events/<event_id>/guests/<guest_id>找一个单独的客人。
POST/events/<event_id>/guests创建新客人。
PUT/events/<event_id>/guests/<guest_id>更新客人。
PATCH/events/<event_id>/guests/<guest_id>部分更新客人。
DELETE/events/<event_id>/guests/<guest_id>删除客人。

有了这些端点,您可以管理系统中特定事件的guests

这不是为嵌套资源定义端点的唯一方式。有些人更喜欢使用查询字符串来访问嵌套资源。查询字符串允许您在 HTTP 请求中发送附加参数。在下面的端点中,您添加了一个查询字符串来获取特定event_idguests:

GET /guests?event_id=23

这个端点将过滤掉任何不引用给定event_idguests。与 API 设计中的许多事情一样,您需要决定哪种方法最适合您的 web 服务。

注意:【REST API 不太可能在 web 服务的整个生命周期中保持不变。资源会发生变化,您需要更新您的端点来反映这些变化。这就是 API 版本的用武之地。API 版本控制允许您修改 API,而不用担心破坏现有的集成。

有很多种版本控制策略。选择正确的选项取决于 API 的需求。下面是一些最流行的 API 版本控制选项:

无论您选择什么策略,对 API 进行版本控制都是重要的一步,以确保它能够适应不断变化的需求,同时支持现有用户。

既然您已经介绍了端点,那么在下一节中,您将会看到在 REST API 中格式化数据的一些选项。

选择您的数据交换格式

格式化 web 服务数据的两个流行选项是 XML 和 JSON。传统上,XML 和SOAPAPI 非常受欢迎,但是 JSON 和 REST APIs 更受欢迎。为了比较这两者,请看一个格式化为 XML 和 JSON 的示例book资源。

这是 XML 格式的书:

<?xml version="1.0" encoding="UTF-8" ?>
<book>
    <title>Python Basics</title>
    <page_count>635</page_count>
    <pub_date>2021-03-16</pub_date>
    <authors>
        <author>
            <name>David Amos</name>
        </author>
        <author>
            <name>Joanna Jablonski</name>
        </author>
        <author>
            <name>Dan Bader</name>
        </author>
        <author>
            <name>Fletcher Heisler</name>
        </author>
    </authors>
    <isbn13>978-1775093329</isbn13>
    <genre>Education</genre>
</book>

XML 使用一系列的元素来编码数据。每个元素都有一个开始和结束标记,数据在它们之间。元素可以嵌套在其他元素中。你可以在上面看到,几个<author>标签嵌套在<authors>中。

现在,看看 JSON 中的同一个book:

{ "title":  "Python Basics", "page_count":  635, "pub_date":  "2021-03-16", "authors":  [ {"name":  "David Amos"}, {"name":  "Joanna Jablonski"}, {"name":  "Dan Bader"}, {"name":  "Fletcher Heisler"} ], "isbn13":  "978-1775093329", "genre":  "Education" }

JSON 以类似于 Python 字典的键值对存储数据。像 XML 一样,JSON 支持任何级别的嵌套数据,因此您可以对复杂数据建模。

JSON 和 XML 本质上都没有谁更好,但是 REST API 开发人员更喜欢 JSON。当您将 REST API 与前端框架如 ReactVue 配对时尤其如此。

Remove ads

设计成功响应

一旦选择了数据格式,下一步就是决定如何响应 HTTP 请求。来自 REST API 的所有响应应该具有相似的格式,并包含正确的 HTTP 状态代码。

在这一节中,您将看到一个管理库存cars的假想 API 的一些示例 HTTP 响应。这些例子将让你知道应该如何格式化你的 API 响应。为了清楚起见,我们将查看原始的 HTTP 请求和响应,而不是使用像requests这样的 HTTP 库。

首先,看一下对/carsGET请求,它返回一个cars列表:

GET /cars HTTP/1.1
Host: api.example.com

这个 HTTP 请求由四部分组成:

  1. GET 是 HTTP 方法类型。
  2. /cars 是 API 端点。
  3. HTTP/1.1 是 HTTP 版本。
  4. Host: api.example.com 是 API 主机。

这四个部分就是你向/cars发送一个GET请求所需要的全部。现在来看看回应。这个 API 使用 JSON 作为数据交换格式:

HTTP/1.1 200 OK
Content-Type: application/json
...

[ { "id":  1, "make":  "GMC", "model":  "1500 Club Coupe", "year":  1998, "vin":  "1D7RV1GTXAS806941", "color":  "Red" }, { "id":  2, "make":  "Lamborghini", "model":"Gallardo", "year":2006, "vin":"JN1BY1PR0FM736887", "color":"Mauve" }, { "id":  3, "make":  "Chevrolet", "model":"Monte Carlo", "year":1996, "vin":"1G4HP54K714224234", "color":"Violet" } ]

API 返回一个包含一列cars的响应。您知道响应是成功的,因为有了200 OK状态代码。该响应还有一个设置为application/jsonContent-Type报头。这告诉用户将响应解析为 JSON。

**注意:**当你使用一个真正的 API 时,你会看到比这更多的 HTTP 头。这些头文件在不同的 API 之间是不同的,所以在这些例子中它们被排除了。

务必在回复中设置正确的Content-Type标题。如果你发送 JSON,那么设置Content-Typeapplication/json。如果是 XML,那么将其设置为application/xml。这个头告诉用户应该如何解析数据。

您还需要在回复中包含适当的状态代码。对于任何成功的GET请求,您应该返回200 OK。这告诉用户他们的请求按预期得到了处理。

看看另一个GET请求,这次是针对一辆车:

GET /cars/1 HTTP/1.1
Host: api.example.com

这个 HTTP 请求查询汽车1的 API。以下是回应:

HTTP/1.1 200 OK
Content-Type: application/json

{ "id":  1, "make":  "GMC", "model":  "1500 Club Coupe", "year":  1998, "vin":  "1D7RV1GTXAS806941", "color":  "Red" },

这个响应包含一个带有汽车数据的 JSON 对象。既然是单个对象,就不需要用列表包装。与上一个响应一样,这也有一个200 OK状态代码。

注意:GET请求不应该修改现有的资源。如果请求包含数据,那么这个数据应该被忽略,API 应该返回没有改变的资源。

接下来,查看添加新车的POST请求:

POST /cars HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "make":  "Nissan", "model":  "240SX", "year":  1994, "vin":  "1N6AD0CU5AC961553", "color":  "Violet" }

这个POST请求在请求中包含新车的 JSON。它将Content-Type头设置为application/json,这样 API 就知道请求的内容类型。API 将从 JSON 创建一辆新车。

以下是回应:

HTTP/1.1 201 Created
Content-Type: application/json

{ "id":  4, "make":  "Nissan", "model":  "240SX", "year":  1994, "vin":  "1N6AD0CU5AC961553", "color":  "Violet" }

这个响应有一个201 Created状态代码,告诉用户一个新的资源已经创建。确保对所有成功的POST请求使用201 Created而不是200 OK

这个响应还包括一个由 API 生成的带有id的新车副本。在响应中发回一个id非常重要,这样用户就可以再次修改资源。

**注意:**当用户用POST创建资源或者用PUTPATCH修改资源时,一定要发送回一份副本,这一点很重要。这样,用户可以看到他们所做的更改。

现在来看看一个PUT请求:

PUT /cars/4 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "make":  "Buick", "model":  "Lucerne", "year":  2006, "vin":  "4T1BF3EK8AU335094", "color":"Maroon" }

这个请求使用前一个请求中的id用所有新数据更新汽车。提醒一下,PUT用新数据更新资源上的所有域。以下是回应:

HTTP/1.1 200 OK
Content-Type: application/json

{ "id":  4, "make":  "Buick",  "model":  "Lucerne",  "year":  2006,  "vin":  "4T1BF3EK8AU335094",  "color":"Maroon"  }

该响应包括带有新数据的car的副本。同样,您总是希望为一个PUT请求发送回完整的资源。这同样适用于PATCH的请求:

PATCH /cars/4 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "vin":  "VNKKTUD32FA050307", "color":  "Green" }

请求只更新资源的一部分。在上面的请求中,vincolor字段将被更新为新值。以下是回应:

HTTP/1.1 200 OK
Content-Type: application/json

{ "id":  4, "make":  "Buick", "model":  "Lucerne", "year":  2006, "vin":  "VNKKTUD32FA050307",  "color":  "Green"  }

该响应包含car的完整副本。如您所见,只有vincolor字段被更新。

最后,看看当 REST API 收到一个DELETE请求时应该如何响应。这里有一个删除carDELETE请求:

DELETE /cars/4 HTTP/1.1

这个DELETE请求告诉 API 移除 ID 为4car。以下是回应:

HTTP/1.1 204 No Content

该响应仅包括状态代码204 No Content。此状态代码告诉用户操作成功,但是响应中没有返回任何内容。这是有意义的,因为car已经被删除了。没有理由在响应中发送它的副本。

当一切按计划进行时,上面的响应工作得很好,但是如果请求有问题会发生什么呢?在下一节中,您将看到当错误发生时,您的 REST API 应该如何响应。

Remove ads

设计错误响应

对 REST API 的请求总有可能失败。定义错误响应是一个好主意。这些响应应该包括发生了什么错误的描述以及相应的状态代码。在这一节中,您将看到几个例子。

首先,看一下对 API 中不存在的资源的请求:

GET /motorcycles HTTP/1.1
Host: api.example.com

这里,用户向/motorcycles发送一个GET请求,这个请求并不存在。API 发回以下响应:

HTTP/1.1 404 Not Found
Content-Type: application/json
...

{ "error":  "The requested resource was not found." }

该响应包括一个404 Not Found状态代码。除此之外,响应还包含一个带有描述性错误消息的 JSON 对象。提供一个描述性的错误信息给用户更多的错误上下文。

现在来看看用户发送无效请求时的错误响应:

POST /cars HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "make":  "Nissan", "year":  1994, "color":  "Violet"

这个POST请求包含 JSON,但是格式不正确。它的结尾缺少了一个右花括号(})。API 将无法处理这些数据。错误响应告诉用户有关问题的信息:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{ "error":  "This request was not properly formatted. Please send again." }

这个响应包括一个描述性的错误消息和400 Bad Request状态代码,告诉用户他们需要修复这个请求。

即使格式正确,请求也可能在其他几个方面出错。在下一个例子中,用户发送了一个POST请求,但是包含了一个不支持的媒体类型:

POST /cars HTTP/1.1
Host: api.example.com
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8" ?>
<car>
    <make>Nissan</make>
    <model>240SX</model>
    <year>1994</year>
    <vin>1N6AD0CU5AC961553</vin>
    <color>Violet</color>
</car>

在这个请求中,用户发送 XML,但是 API 只支持 JSON。API 的响应如下:

HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json

{ "error":  "The application/xml mediatype is not supported." }

这个响应包含了415 Unsupported Media Type状态代码,表明POST请求包含了 API 不支持的数据格式。这个错误代码对于格式错误的数据是有意义的,但是对于格式正确但仍然无效的数据呢?

在下一个例子中,用户发送了一个POST请求,但是包含了与其他数据的字段不匹配的car数据:

POST /cars HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "make":  "Nissan", "model":  "240SX", "topSpeed":  120  "warrantyLength":  10  }

在这个请求中,用户向 JSON 添加了topSpeedwarrantyLength字段。API 不支持这些字段,因此它会响应一条错误消息:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{ "error":  "Request had invalid or missing data." }

该响应包括422 Unprocessable Entity状态代码。此状态代码表示请求没有任何问题,但是数据无效。REST API 需要验证传入的数据。如果用户随请求发送数据,那么 API 应该验证数据并通知用户任何错误。

响应请求,不管是成功的还是错误的,都是 REST API 最重要的工作之一。如果你的 API 是直观的,并提供准确的响应,那么用户围绕你的 web 服务构建应用程序就更容易了。幸运的是,一些优秀的 Python web 框架抽象出了处理 HTTP 请求和返回响应的复杂性。在下一节中,您将看到三个流行的选项。

Remove ads

REST 和 Python:行业工具

在这一节中,您将看到用 Python 构建 REST APIs 的三个流行框架。每个框架都有优点和缺点,所以您必须评估哪个最适合您的需求。为此,在接下来的章节中,您将会看到每个框架中的 REST API。所有的例子都是针对一个类似的管理国家集合的 API。

每个国家将有以下字段:

  • name 是国家的名称。
  • capital 是这个国家的首都。
  • area 是以平方公里为单位的国家的面积。

字段namecapitalarea存储世界上某个特定国家的数据。

大多数时候,从 REST API 发送的数据来自数据库。连接数据库超出了本教程的范围。对于以下示例,您将在 Python 列表中存储数据。例外情况是 Django REST 框架示例,它运行 Django 创建的 SQLite 数据库。

**注意:**建议您为每个示例创建单独的文件夹,以分离源文件。您还会希望使用虚拟环境来隔离依赖性。

为了保持一致性,您将使用countries作为所有三个框架的主要端点。您还将使用 JSON 作为所有三个框架的数据格式。

现在您已经了解了 API 的背景,您可以继续下一部分,在这里您将看到 Flask 中的 REST API。

烧瓶

Flask 是用于构建 web 应用和 REST APIs 的 Python 微框架。Flask 为您的应用程序提供了坚实的基础,同时留给您许多设计选择。Flask 的主要工作是处理 HTTP 请求,并将它们路由到应用程序中适当的函数。

**注意:**本节中的代码使用了新的 Flask 2 语法。如果你运行的是老版本的 Flask,那么使用 @app.route("/countries") 而不是 @app.get("/countries")@app.post("/countries")

为了在旧版本的 Flask 中处理POST请求,您还需要将methods参数添加到@app.route():

@app.route("/countries", methods=["POST"])

这个路由处理对 Flask 1 中的/countriesPOST请求。

以下是 REST API 的 Flask 应用程序示例:

# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

countries = [
    {"id": 1, "name": "Thailand", "capital": "Bangkok", "area": 513120},
    {"id": 2, "name": "Australia", "capital": "Canberra", "area": 7617930},
    {"id": 3, "name": "Egypt", "capital": "Cairo", "area": 1010408},
]

def _find_next_id():
    return max(country["id"] for country in countries) + 1

@app.get("/countries")
def get_countries():
    return jsonify(countries)

@app.post("/countries")
def add_country():
    if request.is_json:
        country = request.get_json()
        country["id"] = _find_next_id()
        countries.append(country)
        return country, 201
    return {"error": "Request must be JSON"}, 415

这个应用程序定义了 API 端点/countries来管理国家列表。它处理两种不同的请求:

  1. GET /countries 返回countries的列表。
  2. POST /countries 向列表中添加一个新的country

**注意:**这个 Flask 应用程序只包含处理两种类型的 API 端点请求的函数,/countries。在一个完整的 REST API 中,您可能希望扩展它,以包含所有必需操作的函数。

您可以通过安装带有pipflask来试用这个应用程序:

$ python -m pip install flask

一旦安装了flask,将代码保存在一个名为app.py的文件中。要运行这个 Flask 应用程序,首先需要将一个名为FLASK_APP的环境变量设置为app.py。这告诉 Flask 哪个文件包含您的应用程序。

在包含app.py的文件夹中运行以下命令:

$ export FLASK_APP=app.py

这会将当前 shell 中的FLASK_APP设置为app.py。也可以将FLASK_ENV设置为development,将 Flask 置于调试模式:

$ export FLASK_ENV=development

除了提供有用的错误消息,调试模式还会在所有代码更改后触发应用程序的重新加载。如果没有调试模式,您必须在每次更改后重启服务器。

**注意:**以上命令可以在 macOS 或 Linux 上运行。如果您在 Windows 上运行它,那么您需要在命令提示符下像这样设置FLASK_APPFLASK_ENV:

C:\> set FLASK_APP=app.py
C:\> set FLASK_ENV=development

现在FLASK_APPFLASK_ENV被设置在 Windows 外壳内部。

准备好所有的环境变量后,您现在可以通过调用flask run来启动 Flask 开发服务器:

$ flask run
* Serving Flask app "app.py" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

这将启动运行该应用程序的服务器。打开你的浏览器,进入http://127.0.0.1:5000/countries,你会看到如下的回应:

[ { "area":  513120, "capital":  "Bangkok", "id":  1, "name":  "Thailand" }, { "area":  7617930, "capital":  "Canberra", "id":  2, "name":  "Australia" }, { "area":  1010408, "capital":  "Cairo", "id":  3, "name":  "Egypt" } ]

这个 JSON 响应包含在app.py开头定义的三个countries。看看下面的代码,看看这是如何工作的:

@app.get("/countries")
def get_countries():
    return jsonify(countries)

这段代码使用@app.get(),一个 Flask route decorator ,将GET请求连接到应用程序中的一个函数。当您访问/countries时,Flask 调用修饰函数来处理 HTTP 请求,然后返回一个响应。

上面的代码中,get_countries()countries,是一个 Python 列表,用jsonify()转换成 JSON。这个 JSON 在响应中返回。

**注意:**大多数时候,你可以直接从 Flask 函数返回一个 Python 字典。Flask 会自动将任何 Python 字典转换成 JSON。您可以通过下面的函数看到这一点:

@app.get("/country")
def get_country():
    return countries[1]

在这段代码中,您从countries返回第二个字典。Flask 会把这个字典转换成 JSON。当您请求/country时,您将看到以下内容:

{ "area":  7617930, "capital":  "Canberra", "id":  2, "name":  "Australia" }

这是你从get_country()返回的字典的 JSON 版本。

get_countries()中,您需要使用jsonify(),因为您返回的是一个字典列表,而不仅仅是一个字典。Flask 不会自动将列表转换成 JSON。

现在来看看add_country()。该函数处理对/countriesPOST请求,并允许您向列表中添加一个新的国家。它使用 Flask request 对象来获取关于当前 HTTP 请求的信息:

@app.post("/countries")
def add_country():
    if request.is_json:
        country = request.get_json()
        country["id"] = _find_next_id()
        countries.append(country)
        return country, 201
    return {"error": "Request must be JSON"}, 415

该函数执行以下操作:

  1. 使用 request.is_json 检查请求是否为 JSON
  2. 使用request.get_json()创建新的country实例
  3. 找到下一个id并将其设置在country
  4. 将新的country附加到countries
  5. 在响应中返回country以及201 Created状态代码
  6. 如果请求不是 JSON,则返回错误消息和415 Unsupported Media Type状态代码

add_country()也调用_find_next_id()来确定新countryid:

def _find_next_id():
    return max(country["id"] for country in countries) + 1

这个辅助函数使用一个生成器表达式来选择所有的国家 id,然后对它们调用 max() 来获得最大值。它将这个值递增1以获得下一个要使用的 ID。

您可以使用命令行工具 curl 在 shell 中尝试这个端点,它允许您从命令行发送 HTTP 请求。在这里,您将向countries列表中添加一个新的country:

$ curl -i http://127.0.0.1:5000/countries \
-X POST \
-H 'Content-Type: application/json' \
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}'

HTTP/1.0 201 CREATED
Content-Type: application/json
...

{
 "area": 357022,
 "capital": "Berlin",
 "id": 4,
 "name": "Germany"
}

这个 curl 命令有一些选项,了解这些选项很有帮助:

  • -X 为请求设置 HTTP 方法。
  • -H 给请求添加一个 HTTP 头。
  • -d 定义了请求数据。

设置好这些选项后,curl 在一个POST请求中发送 JSON 数据,其中Content-Type头设置为application/json。REST API 返回201 CREATED以及您添加的新country的 JSON。

**注意:**在这个例子中,add_country()不包含任何确认请求中的 JSON 与countries的格式匹配的验证。如果您想在 flask 中验证 json 的格式,请查看 flask-expects-json

您可以使用 curl 向/countries发送一个GET请求,以确认新的country已被添加。如果您没有在 curl 命令中使用-X,那么默认情况下它会发送一个GET请求:

$ curl -i http://127.0.0.1:5000/countries

HTTP/1.0 200 OK
Content-Type: application/json
...

[
 {
 "area": 513120,
 "capital": "Bangkok",
 "id": 1,
 "name": "Thailand"
 },
 {
 "area": 7617930,
 "capital": "Canberra",
 "id": 2,
 "name": "Australia"
 },
 {
 "area": 1010408,
 "capital": "Cairo",
 "id": 3,
 "name": "Egypt"
 },
 {
 "area": 357022,
 "capital": "Berlin",
 "id": 4,
 "name": "Germany"
 }
]

这将返回系统中国家的完整列表,最新的国家在底部。

这只是 Flask 功能的一个示例。这个应用程序可以扩展到包括所有其他 HTTP 方法的端点。Flask 还有一个庞大的扩展生态系统,为 REST APIs 提供额外的功能,比如数据库集成认证和后台处理。

Remove ads

Django REST 框架

构建 REST APIs 的另一个流行选项是 Django REST framework 。Django REST framework 是一个 Django 插件,它在现有 Django 项目的基础上增加了 REST API 功能。

要使用 Django REST 框架,您需要一个 Django 项目。如果您已经有了一个,那么您可以将本节中的模式应用到您的项目中。否则,继续下去,您将构建一个 Django 项目并添加到 Django REST 框架中。

首先,用pip安装Djangodjangorestframework:

$ python -m pip install Django djangorestframework

这将安装Djangodjangorestframework。你现在可以使用django-admin工具来创建一个新的 Django 项目。运行以下命令启动您的项目:

$ django-admin startproject countryapi

该命令在当前目录下创建一个名为countryapi的新文件夹。这个文件夹中是运行 Django 项目所需的所有文件。接下来,您将在您的项目中创建一个新的 Django 应用程序。Django 将项目的功能分解成应用程序。每个应用程序管理项目的不同部分。

**注意:**在本教程中,您将只接触到 Django 的皮毛。如果你有兴趣了解更多,请查看可用的 Django 教程

要创建应用程序,请将目录更改为countryapi并运行以下命令:

$ python manage.py startapp countries

这将在您的项目中创建一个新的countries文件夹。这个文件夹中是这个应用程序的基本文件。

既然您已经创建了一个应用程序,那么您需要将它告诉 Django。在您刚刚创建的countries文件夹旁边是另一个名为countryapi的文件夹。此文件夹包含项目的配置和设置。

**注意:**这个文件夹与 Django 在运行django-admin startproject countryapi时创建的根文件夹同名。

打开countryapi文件夹中的settings.py文件。在INSTALLED_APPS中添加以下几行,告诉 Django 关于countries应用程序和 Django REST 框架的信息:

# countryapi/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
 "rest_framework", "countries", ]

您已经为countries应用程序和rest_framework添加了一行。

您可能想知道为什么需要将rest_framework添加到应用程序列表中。您需要添加它,因为 Django REST 框架只是另一个 Django 应用程序。Django 插件是打包分发的 Django 应用程序,任何人都可以使用。

下一步是创建 Django 模型来定义数据的字段。在countries应用程序内部,用以下代码更新models.py:

# countries/models.py
from django.db import models

class Country(models.Model):
 name = models.CharField(max_length=100) capital = models.CharField(max_length=100) area = models.IntegerField(help_text="(in square kilometers)")

这段代码定义了一个Country模型。Django 将使用这个模型为国家数据创建数据库表和列。

运行以下命令,让 Django 根据这个模型更新数据库:

$ python manage.py makemigrations
Migrations for 'countries':
 countries/migrations/0001_initial.py
 - Create model Country

$ python manage.py migrate
Operations to perform:
 Apply all migrations: admin, auth, contenttypes, countries, sessions
Running migrations:
 Applying contenttypes.0001_initial... OK
 Applying auth.0001_initial... OK
 ...

这些命令使用 Django 迁移在数据库中创建一个新表。

这个表开始是空的,但是最好有一些初始数据,这样就可以测试 Django REST 框架。为此,您将使用一个 Django fixture 在数据库中加载一些数据。

将以下 JSON 数据复制并保存到一个名为countries.json的文件中,并保存在countries目录下:

[ { "model":  "countries.country", "pk":  1, "fields":  { "name":  "Thailand", "capital":  "Bangkok", "area":  513120 } }, { "model":  "countries.country", "pk":  2, "fields":  { "name":  "Australia", "capital":  "Canberra", "area":  7617930 } }, { "model":  "countries.country", "pk":  3, "fields":  { "name":  "Egypt", "capital":  "Cairo", "area":  1010408 } } ]

这个 JSON 包含三个国家的数据库条目。调用以下命令将该数据加载到数据库中:

$ python manage.py loaddata countries.json
Installed 3 object(s) from 1 fixture(s)

这将向数据库中添加三行。

至此,您的 Django 应用程序已经设置完毕,并填充了一些数据。您现在可以开始向项目中添加 Django REST 框架了。

Django REST 框架采用现有的 Django 模型,并将其转换为 JSON 用于 REST API。它通过模型序列化器来实现这一点。模型序列化器告诉 Django REST 框架如何将模型实例转换成 JSON,以及应该包含哪些数据。

您将从上面为Country模型创建您的序列化程序。首先在countries应用程序中创建一个名为serializers.py的文件。完成之后,将下面的代码添加到serializers.py:

# countries/serializers.py
from rest_framework import serializers
from .models import Country

class CountrySerializer(serializers.ModelSerializer):
    class Meta:
        model = Country
        fields = ["id", "name", "capital", "area"]

这个序列化器CountrySerializer继承了serializers.ModelSerializer,根据Country的模型字段自动生成 JSON 内容。除非指定,否则ModelSerializer子类将包含 JSON 中 Django 模型的所有字段。您可以通过将fields设置为您希望包含的数据列表来修改此行为。

就像 Django 一样,Django REST 框架使用视图从数据库中查询数据并显示给用户。不用从头开始编写 REST API 视图,你可以子类化 Django REST 框架的 ModelViewSet 类,该类拥有常见 REST API 操作的默认视图。

**注意:**Django REST 框架文档将这些视图称为动作

下面是ModelViewSet提供的动作及其等效 HTTP 方法的列表:

HTTP 方法行动描述
GET.list()获取国家列表。
GET.retrieve()得到一个国家。
POST.create()创建一个新的国家。
PUT.update()更新一个国家。
PATCH.partial_update()部分更新一个国家。
DELETE.destroy()删除一个国家。

如您所见,这些动作映射到 REST API 中的标准 HTTP 方法。你可以在你的子类中覆盖这些动作或者根据你的 API 的需求添加额外的动作

下面是名为CountryViewSetModelViewSet子类的代码。这个类将生成管理Country数据所需的视图。将以下代码添加到countries应用程序内的views.py:

# countries/views.py
from rest_framework import viewsets

from .models import Country
from .serializers import CountrySerializer

class CountryViewSet(viewsets.ModelViewSet):
    serializer_class = CountrySerializer
    queryset = Country.objects.all()

在这个类中,serializer_class被设置为CountrySerializerqueryset被设置为Country.objects.all()。这告诉 Django REST framework 要使用哪个序列化程序,以及如何在数据库中查询这个特定的视图集。

一旦创建了视图,就需要将它们映射到适当的 URL 或端点。为此,Django REST 框架提供了一个DefaultRouter,它将自动为一个ModelViewSet生成 URL。

countries应用程序中创建一个urls.py文件,并将以下代码添加到该文件中:

# countries/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter

from .views import CountryViewSet

router = DefaultRouter()
router.register(r"countries", CountryViewSet)

urlpatterns = [
    path("", include(router.urls))
]

这段代码创建了一个DefaultRouter,并在countries URL 下注册了CountryViewSet。这将把CountryViewSet的所有 URL 放在/countries/下。

注意: Django REST 框架自动在DefaultRouter生成的任何端点的末尾追加一个正斜杠(/)。您可以禁用此行为,如下所示:

router = DefaultRouter(trailing_slash=False)

这将禁用端点末尾的正斜杠。

最后,您需要更新项目的基本urls.py文件,以包含项目中所有的countriesURL。用下面的代码更新countryapi文件夹中的urls.py文件:

# countryapi/urls.py
from django.contrib import admin
from django.urls import path, include 
urlpatterns = [
    path("admin/", admin.site.urls),
 path("", include("countries.urls")), ]

这将所有的 URL 放在/countries/下。现在您已经准备好尝试 Django 支持的 REST API 了。在根目录countryapi中运行以下命令来启动 Django 开发服务器:

$ python manage.py runserver

开发服务器现在正在运行。继续向/countries/发送GET请求,以获得 Django 项目中所有国家的列表:

$ curl -i http://127.0.0.1:8000/countries/ -w '\n'

HTTP/1.1 200 OK
...

[
 {
 "id": 1,
 "name":"Thailand",
 "capital":"Bangkok",
 "area":513120
 },
 {
 "id": 2,
 "name":"Australia",
 "capital":"Canberra",
 "area":7617930
 },
 {
 "id": 3,
 "name":"Egypt",
 "capital":"Cairo",
 "area":1010408
 }
]

Django REST 框架发回一个 JSON 响应,其中包含您之前添加的三个国家。上面的回答是为了可读性而格式化的,所以你的回答看起来会有所不同。

您在countries/urls.py中创建的 DefaultRouter 为所有标准 API 端点的请求提供了 URL:

  • GET /countries/
  • GET /countries/<country_id>/
  • POST /countries/
  • PUT /countries/<country_id>/
  • PATCH /countries/<country_id>/
  • DELETE /countries/<country_id>/

您可以在下面多尝试几个端点。向/countries/发送一个POST请求,在 Django 项目中创建一个新的Country:

$ curl -i http://127.0.0.1:8000/countries/ \
-X POST \
-H 'Content-Type: application/json' \
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}' \
-w '\n'

HTTP/1.1 201 Created
...

{
 "id":4,
 "name":"Germany",
 "capital":"Berlin",
 "area":357022
}

这将使用您在请求中发送的 JSON 创建一个新的Country。Django REST 框架返回一个201 Created状态代码和新的Country

**注意:**默认情况下,响应末尾不包含新行。这意味着 JSON 可能会在您的命令提示符下运行。上面的 curl 命令包含了-w '\n'来在 JSON 后面添加一个换行符,以解决这个问题。

您可以通过向已有idGET /countries/<country_id>/发送请求来查看已有的Country。运行以下命令获得第一个Country:

$ curl -i http://127.0.0.1:8000/countries/1/ -w '\n'

HTTP/1.1 200 OK
...

{
 "id":1,
 "name":"Thailand",
 "capital":"Bangkok",
 "area":513120
}

响应包含第一个Country的信息。这些例子只涵盖了GETPOST请求。您可以自行尝试PUTPATCHDELETE请求,看看如何从 REST API 中完全管理您的模型。

正如您所看到的,Django REST 框架是构建 REST APIs 的一个很好的选择,尤其是如果您已经有了一个 Django 项目,并且想要添加一个 API。

Remove ads

FastAPI

FastAPI 是一个针对构建 API 而优化的 Python web 框架。它使用了 Python 类型提示,并且内置了对异步操作的支持。FastAPI 构建在 StarlettePydantic 之上,性能非常好。

下面是一个用 FastAPI 构建的 REST API 的例子:

# app.py
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

def _find_next_id():
    return max(country.country_id for country in countries) + 1

class Country(BaseModel):
    country_id: int = Field(default_factory=_find_next_id, alias="id")
    name: str
    capital: str
    area: int

countries = [
    Country(id=1, name="Thailand", capital="Bangkok", area=513120),
    Country(id=2, name="Australia", capital="Canberra", area=7617930),
    Country(id=3, name="Egypt", capital="Cairo", area=1010408),
]

@app.get("/countries")
async def get_countries():
    return countries

@app.post("/countries", status_code=201)
async def add_country(country: Country):
    countries.append(country)
    return country

这个应用程序使用 FastAPI 的特性为您在其他示例中看到的相同的country数据构建一个 REST API。

您可以通过安装带有pipfastapi来尝试此应用程序:

$ python -m pip install fastapi

您还需要安装uvicorn[standard],一个可以运行 FastAPI 应用程序的服务器:

$ python -m pip install uvicorn[standard]

如果你已经安装了fastapiuvicorn,那么将上面的代码保存在一个名为app.py的文件中。运行以下命令启动开发服务器:

$ uvicorn app:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

服务器现在正在运行。打开浏览器,进入http://127.0.0.1:8000/countries。您将看到 FastAPI 以如下方式响应:

[ { "id":  1, "name":"Thailand", "capital":"Bangkok", "area":513120 }, { "id":  2, "name":"Australia", "capital":"Canberra", "area":7617930 }, { "id":  3, "name":"Egypt", "capital":"Cairo", "area":1010408 } ]

FastAPI 用一个包含一列countries的 JSON 数组来响应。您也可以通过向/countries发送POST请求来添加新的国家:

$ curl -i http://127.0.0.1:8000/countries \
-X POST \
-H 'Content-Type: application/json' \
-d '{"name":"Germany", "capital": "Berlin", "area": 357022}' \
-w '\n'

HTTP/1.1 201 Created
content-type: application/json
...

{"id":4,"name":"Germany","capital":"Berlin","area": 357022}

您添加了一个新国家。您可以通过GET /countries确认这一点:

$ curl -i http://127.0.0.1:8000/countries -w '\n'

HTTP/1.1 200 OK
content-type: application/json
...

[
 {
 "id":1,
 "name":"Thailand",
 "capital":"Bangkok",
 "area":513120,
 },
 {
 "id":2,
 "name":"Australia",
 "capital":"Canberra",
 "area":7617930
 },
 {
 "id":3,
 "name":"Egypt",
 "capital":"Cairo",
 "area":1010408
 },
 {
 "id":4,
 "name": "Germany",
 "capital": "Berlin",
 "area": 357022
 }
]

FastAPI 返回一个 JSON 列表,其中包括您刚刚添加的新国家。

您会注意到 FastAPI 应用程序看起来类似于 Flask 应用程序。像 Flask 一样,FastAPI 也有一个集中的特性集。它并不试图处理 web 应用程序开发的所有方面。它旨在构建具有现代 Python 特性的 API。

如果你靠近app.py的顶部,你会看到一个叫做Country的类,它扩展了BaseModelCountry类描述了 REST API 中的数据结构:

class Country(BaseModel):
    country_id: int = Field(default_factory=_find_next_id, alias="id")
    name: str
    capital: str
    area: int

这是一个 Pydantic 模型的例子。Pydantic 模型在 FastAPI 中提供了一些有用的特性。它们使用 Python 类型注释来强制类中每个字段的数据类型。这允许 FastAPI 为 API 端点自动生成具有正确数据类型的 JSON。它还允许 FastAPI 验证传入的 JSON。

强调第一行很有帮助,因为这一行有很多内容:

country_id: int = Field(default_factory=_find_next_id, alias="id")

在这一行中,您可以看到country_id,它为Country的 ID 存储了一个整数。它使用 Pydantic 的 Field函数来修改country_id的行为。在这个例子中,您将关键字参数default_factoryalias传递给Field

第一个参数default_factory被设置为_find_next_id()。该参数指定每当创建新的Country时运行的函数。返回值将被赋给country_id

第二个参数alias被设置为id。这告诉 FastAPI 输出键"id"而不是 JSON 中的"country_id":

{ "id":1, "name":"Thailand", "capital":"Bangkok", "area":513120, },

这个alias也意味着当你创建一个新的Country时,你可以使用id。您可以在countries列表中看到:

countries = [
    Country(id=1, name="Thailand", capital="Bangkok", area=513120),
    Country(id=2, name="Australia", capital="Canberra", area=7617930),
    Country(id=3, name="Egypt", capital="Cairo", area=1010408),
]

这个列表包含 API 中初始国家的三个Country实例。Pydantic 模型提供了一些很棒的特性,并允许 FastAPI 轻松处理 JSON 数据。

现在看看这个应用程序中的两个 API 函数。第一个函数get_countries(),返回一个countries列表,用于对/countriesGET请求:

@app.get("/countries")
async def get_countries():
    return countries

FastAPI 将根据 Pydantic 模型中的字段自动创建 JSON,并根据 Python 类型提示设置正确的 JSON 数据类型。

当您向/countries发出POST请求时,Pydantic 模型也提供了一个好处。您可以在下面的第二个 API 函数中看到,参数country有一个Country注释:

@app.post("/countries", status_code=201)
async def add_country(country: Country):
    countries.append(country)
    return country

这个类型注释告诉 FastAPI 根据Country验证传入的 JSON。如果不匹配,那么 FastAPI 将返回一个错误。您可以通过用 JSON 发出一个与 Pydantic 模型不匹配的请求来尝试一下:

$ curl -i http://127.0.0.1:8000/countries \
-X POST \
-H 'Content-Type: application/json' \
-d '{"name":"Germany", "capital": "Berlin"}' \
-w '\n'

HTTP/1.1 422 Unprocessable Entity
content-type: application/json
...

{
 "detail": [
 {
 "loc":["body","area"],
 "msg":"field required",
 "type":"value_error.missing"
 }
 ]
}

这个请求中的 JSON 缺少一个值area,所以 FastAPI 返回一个响应,其中包含状态代码422 Unprocessable Entity以及关于错误的详细信息。Pydantic 模型使这种验证成为可能。

这个例子只是触及了 FastAPI 的皮毛。凭借其高性能和现代化的特性,如async函数和自动文档,FastAPI 值得考虑作为您的下一个 REST API。

Remove ads

结论

REST APIs 无处不在。了解如何利用 Python 来消费和构建 API 可以让您处理 web 服务提供的大量数据。

在本教程中,你已经学会了如何:

  • 识别 REST 架构风格
  • 使用 HTTP 方法和状态代码
  • 使用 requests 从外部 API 获取和使用数据
  • 为 REST API 定义端点数据响应
  • 开始使用 Python 工具构建一个 REST API

使用您的新 Python REST API 技能,您不仅能够与 web 服务交互,还能够为您的应用程序构建 REST API。这些工具为各种有趣的、数据驱动的应用和服务打开了大门。**********

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值