让-Python-说话-全-
让 Python 说话(全)
原文:
zh.annas-archive.org/md5/d9fcc1b4cb6fe7a9b5efafb595cee43b译者:飞龙
序言
银行本质上是科技公司。
——Hugo Banziger,前德意志银行首席风险官

Python 目前是全球最受欢迎的编程语言,超越了 Java 和 C 等更为传统的编程语言。一旦你开始使用 Python 编程,就会很容易理解为什么它如此受欢迎。Python 的两个主要优势是简洁性和开放性。Python 代码相对接近普通英语,因此即使经验不多,你通常也能猜出一个脚本想要实现的功能。
Python 是开源的,这不仅意味着软件对每个人免费使用,还意味着其他用户可以创建和修改库。实际上,Python 拥有庞大的生态系统,用户可以从社区成员那里获得资源和帮助。Python 程序员可以相互分享代码,因此,你不必从头开始构建一切,可以导入他人设计的模块,并将自己的模块分享给 Python 社区的其他成员。
当人们听说我正在写一本关于语音识别和语音合成的 Python 书籍时,他们的反应通常是一样的:“我以为你是金融教授。”我的典型回答是本章开头引用的 Hugo Banziger 名言,这是在 2008 年金融危机后不久说的。如今,你可以将银行替换成任何行业的公司——汽车制造商、零售商,几乎任何行业——这句话依然适用。如今,技术渗透到了我们生活的方方面面。未来已经到来,且就在眼前。
自 2018 年以来,Python 一直是全球最受欢迎的编程语言。在此之前,Python 在金融领域一直是领先的编程语言,广泛应用于金融服务、投资组合管理、算法交易、加密货币等方面。
在与我金融学硕士(MSF)学生的潜在雇主交流时,我被告知他们有懂金融的人,但不懂编程——也有懂编程的人,但对金融一无所知。他们希望招聘那些既懂金融又懂编程的人。因此,我们开始将 Python 纳入 MSF 课程。
金融学学生的反应各不相同。许多学生发现 Python 既易用又多功能,而另一些学生则不明白,既然可以在微软 Excel 中完成所有工作,为什么还需要学习 Python。所以我开始向他们展示 Python 中一些在 Excel 中无法实现的酷技能,比如通过语音命令获取实时股票价格、创建一个会说话的美国股市图表等。我想向他们展示,Python 可以做更多 Excel 做不到的事情,而且入门门槛不高,最重要的是,它很有趣!
在本书中,我专注于语音识别和文本到语音的功能,这些功能在有趣且真正有用的应用中得到了实现,比如语音翻译器、语音控制的在线广播、虚拟个人助手、语音控制的图形游戏等。我的目标是教授可以在现实生活中应用和适应的 Python 技能,同时让对 Python 持怀疑态度的学生对他们所做的事情保持兴趣。
本书简介
本书既是也是不是一本 Python 入门书。虽然本书并不打算作为 Python 基础的完整教程,但它足够简洁,完全的初学者也能跟得上。你将学习如何在你的计算机上安装 Python,并编写你的第一个脚本。你还将学习 Python 的基本规则,了解函数和模块的工作原理,以及每个 Python 用户都需要知道的各种数据类型。通过这些,你将能够完成大多数简单的 Python 任务。
同时,本书不是一本 Python 入门书。我会提供一个 Python 回顾,帮助你为后续章节做准备,但它不是一本全面的入门教程。有许多精彩的书籍涵盖了 Python 的所有基础知识。其中一个例子是 Eric Matthes 的《Python Crash Course》(No Starch Press, 2019)。
除了回顾基础知识,本书的目的是提高你的技能,构建你可以在日常生活中使用的真实工作应用程序。本书还会让你逐渐过渡到更高级的主题,例如如何创建你自己的 Python 模块和包。在第三章,你将学习如何使用自定义模块中的函数来包含所有语音识别功能和相关代码,这样你就不需要每次将语音转换为文本时都重复编写代码。在第五章,你将创建一个包,你可以从中导入模块中的函数,在所有需要此功能的章节中将语音转换为文本(这几乎是本书剩余所有章节的内容)。在此过程中,你将学习 Python 模块和包的工作原理。
每章末的练习是一个很好的工具,可以帮助你练习概念并检查你是否真正理解它们。你将在书的最后找到答案。
本书中的代码是跨平台的,因此它应该可以在 Windows、Mac 或 Linux 系统中运行。每当有差异时,我会讨论三种操作系统之间的不同之处。
本书内容
本书分为四个部分。第一部分讨论如何安装 Python,以及你在后续章节中需要掌握的基本 Python 规则和技能。第二部分介绍语音识别和文本到语音的功能,包括如何安装和微调所需的模块。你还将利用语音识别和文本到语音的功能创建一个虚拟个人助手。
第三部分涉及互动游戏。你将学习如何创建图形游戏,并为其添加语音合成和语音识别功能,使其能够说话并接受语音指令。在第四部分,我们将构建一些应用程序来跟踪金融市场,并了解如何让 Python 在主要世界语言中进行对话和聆听。本书的最后一章通过将互动游戏和语音翻译功能整合,构建我们的终极虚拟助手。以下是本书的概览:
第一部分:入门
第一章:设置 Python、Anaconda 和 Spyder
你将安装本书所需的 Python 软件,并开始运行 Python 脚本,即使你对编程一无所知。我们还将讨论 Python 中的基本操作。
第二章:Python 基础回顾
你将学习如何使用 Python 内置函数以及如何导入 Python 标准库中的模块。然后,你将了解函数和模块是如何工作的,并学习如何创建你自己的函数和模块。我将讨论如何在你的计算机上安装这些模块。最后,你将了解虚拟环境,它们的用途以及如何创建和激活它们。
Python 使用字符串、列表、字典和元组作为元素集合来完成某些任务。在这一章中,你将了解这四种集合类型,并看到它们的使用示例。
第二部分:学习说话
第三章:语音识别
你将安装与语音识别相关的 Python 模块,然后创建一个脚本,让 Python 识别你的语音并将其打印出来。你将使用语音控制来完成几项任务,比如语音听写、打开网页浏览器、打开文件以及在计算机上播放音乐。为了节省脚本空间,你将学习如何将所有与语音识别相关的代码放入自定义的本地模块中,这样最终的脚本既简洁又清晰。
第四章:让 Python 说话
在这一部分,你将学习如何让 Python 用人的声音回应你。你将安装语音合成模块,并教会 Python 朗读你在 Spyder 中输入的任何内容。我们还将添加语音识别功能,让 Python 重复你所说的内容。所有与语音合成功能相关的代码都将存储在另一个自定义模块中。
第五章:语音应用
你将把第三章和第四章中的语音识别和语音合成功能应用到几个实际项目中。首先,你将解析文本,从国家公共广播电台(NPR)提取新闻摘要,并让 Python 朗读出来。你还将构建一个脚本,根据你的语音查询从维基百科提取信息并读出答案。最后,你将学习如何通过语音遍历文件夹中的文件,目标是打造你自己的 Alexa。你可以说:“Python,播放 Selena Gomez 的歌曲”,然后一首保存在你计算机上的 Selena Gomez 的歌曲就会播放。
第六章:网页抓取播客、广播和视频
你将学习网页抓取的基础知识。我将介绍超文本标记语言(HTML)如何构建网页。你将解析 HTML 文件并提取信息。然后,你将利用这些技能通过语音激活播客、直播电台和各种网站上的视频。
第七章:构建虚拟个人助理
你将创建自己的虚拟个人助理(VPA),类似于亚马逊的 Alexa。每当你需要帮助时,只需说“Hello, Python”唤醒你的 VPA;你还可以使用语音命令将其置于待机模式。VPA 可以作为计时器和闹钟,讲笑话,并且完全免提地发送电子邮件。
第八章:全能 VPA
在这里,你将为你的虚拟个人助理(VPA)添加全能功能。具体来说,你将利用计算引擎 WolframAlpha 的广泛知识库,如果 WolframAlpha 无法回答你的问题,还会使用 Wikipedia 作为备用。你的全能 VPA 能够为你回答几乎任何问题。
第三部分:互动游戏
第九章:使用 Turtle 模块进行图形与动画
我们在第三部分的目标是构建语音控制的图形化游戏,如井字游戏、四子棋和猜词游戏。你将全部在 turtle 模块中完成。在这一章中,你将学习基本的 turtle 命令,这些命令可以让你设置海龟屏幕、绘制形状并创建动画。
第十章:井字游戏
你将构建一个语音控制的井字游戏,将迄今为止学到的所有新技能付诸实践。你将绘制游戏棋盘,检查有效的移动,并检测玩家是否获胜。然后,你将添加语音识别和文本转语音功能,并设置游戏,使你可以与自己的计算机对战。
第十一章:四子棋
接下来,你将构建一个语音控制的四子棋游戏。你将绘制棋盘,动画化棋子从列的顶部落入最低可用单元格的效果,并使用 Python 逻辑强制执行一套新的游戏规则。然后,你将为游戏添加语音功能。
第十二章:猜词游戏
你将构建一个语音控制的图形化猜词游戏,这个游戏是流行的猜单词游戏的改编版。这是一个有趣的挑战,因为在玩猜词游戏时,玩家通常会快速地口头交换信息,因此你需要微调脚本的听力功能。
第十三章:智能游戏:加入智能
在井字棋或四连棋的一人模式中,计算机总是随机选择一个动作。在这一章中,我们将使用两种技术来构建智能游戏,让你思考如何在编程中分解和解决问题。第一种方法是“提前三步思考”的方法,计算机会按照最有可能在三步后获得胜利的路径走。第二种方法则是使用机器学习。你将模拟一百万场游戏,其中两位玩家都随机选择动作。通过这些数据,计算机将逐步学习并选择最有可能导致胜利的动作。
第四部分:深入探索
第十四章:金融应用
这些编程技能、语音识别和文本转语音技术可以应用到你生活中的任何方面。在这里,我将向你展示如何将这些技能应用于监控金融市场。然后,你就可以将这些技术推广到你感兴趣的任何领域。你将构建三个项目:一个可以告诉你任何上市公司最新股价的应用;一个构建股价可视化图表的脚本;以及一个使用最近每日股价计算回报、进行回归分析和详细分析的应用。
第十五章:股市观察
你将创建一个图形化、语音化的应用,实时监控美国股市,并在所选股票超过某个预设阈值时,用语音更新你。为了掌握所需的技能,你将首先使用tkinter创建一个图形化的比特币监控应用,以显示实时价格信息。
第十六章:使用世界语言
到目前为止,我们已经教会了 Python 如何用英语说话和听话。但 Python 还能理解许多其他世界语言。在这一章中,你将首先教 Python 用你已经在使用的模块说几种其他语言。然后,我将介绍一个叫做translate的有用模块,它可以将一种语言翻译成另一种语言。你将用它来构建一个翻译器,把你说的话翻译成你选择的其他语言。
第十七章:终极虚拟个人助手
你将为你的虚拟个人助手加载本书中的有趣项目,如语音控制游戏、翻译器、音乐播放器等。你将首先为 VPA 添加一个聊天功能,以便与脚本进行日常对话。虚拟个人助手的核心理念是其便捷性,因此我们将调整这些项目,使所有新增功能都能实现百分百的免提操作。
附录 A:安装播放音频文件的模块
由于本书的重点是让 Python 能够听与说,因此播放音频文件非常重要。本附录介绍了几个可以用来播放音频文件的模块,以及它们的优缺点。
附录 B:章节末练习答案
本附录提供了章节末所有习题的建议答案。你可以使用这些答案来检查自己的答案,并在遇到困难时寻求帮助。
第一部分:入门
设置 Python、Anaconda 和 Spyder

即使你之前从未编写过代码,本章也会指导你安装运行本书 Python 脚本所需的软件。我们将使用 Anaconda 和 Spyder,因此我们将分别讨论选择此 Python 发行版和开发环境的优点。我会根据你的操作系统(无论是 Windows、Mac 还是 Linux)指导你完成安装过程。然后,你将学习如何在 Spyder 编辑器中开始编写代码。最后,我们将讨论基本的 Python 规则和操作。
在开始之前,在你的计算机上为本章创建文件夹/mpt/ch01/。本章(及后续章节)中的所有脚本都可以在本书的资源页面找到,www.nostarch.com/make-python-talk/。
介绍 Anaconda 和 Spyder
安装 Python 并运行脚本有很多方法。在本书中,我们将使用 Anaconda 和 Spyder。
Anaconda是一个开源的 Python 发行版、包和环境管理器。它用户友好,提供了许多实用的 Python 模块,安装这些模块本来可能非常麻烦。我们将从下载带有 Spyder 的 Anaconda Python 发行版开始。
Spyder是一个功能完整的集成开发环境(IDE),用于编写脚本。它提供许多有用的功能,如自动代码补全、自动调试、代码建议和警告。
安装 Anaconda 和 Spyder
Python 是一个跨平台的编程语言,意味着你可以在 Windows、Mac 或 Linux 上运行 Python 脚本。然而,软件和模块的安装在不同操作系统上可能会有所不同。我将展示如何在你的操作系统中安装各种模块。一旦这些模块正确安装,Python 代码在不同操作系统中运行是一样的。
在 Windows 上安装 Anaconda 和 Spyder
要在 Windows 上安装 Anaconda,请访问www.anaconda.com/products/individual/并下载最新版本的 Python 3。
我推荐使用图形安装程序,而不是命令行安装程序,特别是对初学者来说,以避免错误。确保下载适合你机器的 32 位或 64 位版本。运行安装程序并按照提示完成安装。
找到并打开 Anaconda 导航器,你应该能看到像图 1-1 那样的界面(如果需要,可以在搜索栏中搜索Anaconda navigator)。

图 1-1:Anaconda 导航器
点击启动按钮,位于 Spyder 图标下方。如果 Spyder 尚未安装,点击安装以安装 Spyder 开发环境。安装完成后,点击启动。
在 macOS 中安装 Anaconda 和 Spyder
要通过 Anaconda 在 macOS 上安装 Python,请访问www.anaconda.com/products/individual/,向下滚动并下载适用于 Mac 的最新 Python 3 版本。选择图形安装程序并按照提示进行操作。
通过在 Spotlight 搜索中搜索Anaconda navigator来打开 Anaconda 导航器。macOS 中的 Anaconda 导航器界面应类似于图 1-1,可能会有一些小的差异。
要启动 Spyder,请点击 Spyder 图标下的启动按钮(如果您看到的是安装按钮,请点击它先安装 Spyder)。
在 Linux 上安装 Anaconda 和 Spyder
在 Linux 上安装 Anaconda 和 Spyder 的步骤比其他操作系统更多。首先,访问www.anaconda.com/products/individual/,向下滚动并找到最新的 Linux 版本。选择适当的 x86 或 Power8 和 Power9 包。点击并下载最新的安装脚本。例如,在我的安装过程中,安装脚本的链接是repo.anaconda.com/archive/Anaconda3-2020.11-Linux-x86_64.sh。此链接会随时间变化,但我们将以此版本作为示例。
默认情况下,安装脚本会下载并保存在您的计算机的下载文件夹中。如果您的 bash 脚本路径不同,您应按照以下方式安装 Anaconda。
**bash ~/Downloads/Anaconda3-2020.11-Linux-x86_64.sh**
按下回车后,系统会提示您查看并同意许可协议。安装过程中的最后一个问题是:
installation finished.
Do you wish the installer to prepend the Anaconda3 install location to PATH
in your /home/mark/.bashrc ? [yes|no]
[no] >>>
您应输入 yes 并按回车,以便在终端中使用 conda 命令打开 Anaconda。
现在,您需要通过执行以下命令来激活安装:
**source ~/.bashrc**
要打开 Anaconda 导航器,请在终端中输入以下命令:
**anaconda-navigator**
您应该在您的计算机上看到 Anaconda 导航器,类似于图 1-1。要启动 Spyder,请点击 Spyder 图标下的启动按钮(如果您看到的是安装按钮,请点击它先安装 Spyder)。
使用 Spyder
为了帮助您快速上手,我们将在 Spyder 中创建一个非常简单的脚本。接着我将介绍一些在开始编写代码之前非常有用的基本概念。
在 Spyder 中编写 Python 代码
如前所述,Spyder 是一个功能齐全的集成开发环境(IDE)。让我们从一个简单的脚本开始。在启动 Spyder 开发环境后,您应该会看到类似于图 1-2 的布局。

图 1-2:Spyder 开发环境
Spyder 带有几个预定义的布局,您可以根据个人喜好自定义布局。默认布局包含三个面板。让我们来看看这个默认布局。
左侧是Spyder 编辑器,你可以在其中编写 Python 代码。右上方是变量资源管理器,它显示你的脚本生成的数据的详细信息。随着脚本变得复杂,变量资源管理器成为一个重要的工具,帮助你检查变量中存储的值。
右下角是交互式 Python(IPython)控制台,它显示脚本的输出或执行 Python 代码片段。IPython 控制台也是你输入需要用户信息的脚本输入的地方。如果脚本中有错误,它也会显示错误信息。
现在,让我们开始编程。前往 Spyder 编辑器窗口(默认位置在左侧),并输入以下代码:
**print("This is my very first Python script!")**
点击文件▶另存为,并将文件保存为my_first_script.py,保存在你的章节文件夹中。
运行脚本有三种方式,所有方式都会导致相同的结果:
-
转到运行菜单并选择运行。
-
按下键盘上的 F5 键。
-
按下图标栏中的绿色三角形图标►。
运行脚本后,你应该会看到类似图 1-3 的内容。在 IPython 控制台中显示的输出是一个简单的打印信息:This is my very first Python script! 恭喜你——你已经编写并成功运行了你的第一个 Python 脚本!

图 1-3:在 Spyder 开发环境中运行脚本
检查 Spyder 中的代码
除了运行整个脚本,Spyder 还可以逐行或逐块运行代码。逐步运行脚本对于仔细跟踪脚本的执行非常有用,可以验证脚本是否完全按照你的预期执行。返回到my_first_script.py示例,并添加另一行:
**print("This is my second Python message!")**
将光标放在第二行上并按 F9 键,你应该能看到如图 1-4 所示的输出。

图 1-4:在 Spyder 编辑器中仅运行一行代码
如你所见,只有高亮的那一行被执行。输出如下:
This is my second Python message!
现在按 F5 键,你会看到脚本中的每一行都被执行:
This is my very first Python script!
This is my second Python message!
若要运行特定的代码块,选中这些行并按 F9 键\。
理解 Python 编程
在我们开始介绍 Python 的编程概念之前,你需要了解一些基本的内容。本节将介绍 Python 的语法和基本的数学运算。
Python 语法
首先,Python 是区分大小写的。在处理大写字母和小写字母时,你需要非常小心。变量X和Y与变量x和y是不同的。字符串"Hello"和"hello"也彼此不同。
其次,在 Python 中缩进非常重要。像制表符这样的不可打印字符必须在整个脚本中一致地应用。如果你有其他编程语言的经验,比如 C 或 Java,你可能会注意到 Python 中缺少括号和分号;这是故意设计的。代码块通过缩进来定义。代码中的不必要空格很可能会暴露你的意图,正如我们在第二章讨论条件执行、循环和函数中的缩进时所看到的那样。
第三,Python 使用单引号和双引号(大多数情况下)可以互换使用。例如,将字符序列放在单引号内与放在双引号内效果相同(除非其中某个字符是转义字符或单引号)。
第四,Python 允许你做注释,这些注释被称为注释。一种常用的写注释的方法是使用井号(#)。井号后面的内容在同一行中不会被执行。最好在你的脚本中做些注释,这样别人可以更容易理解代码的作用——并且当你在几周或几个月后重新查看代码时,也能提醒自己当初做出的决定。例如,在 my_first_script.py 的第一行,我们有以下内容:
# -*- coding: utf-8 -*-
由于这一行以 # 开头,Python 会忽略它,理解它是注释而不是需要执行的代码。
当你的注释不能写在一行时,你可以将注释放在三重引号(""")中,第一组引号和最后一组引号之间的内容不会被 Python 脚本执行。例如,在 my_first_script.py 的第 2 到第 6 行,我们有以下内容:
"""
Created on Fri Apr 16 14:49:19 2021
@author: hlliu2
"""
所有这些行都会被 Python 忽略。
Python 中的基本操作
Python 能够进行基本的数学运算。例如,要计算 7 乘以 123,你可以在 Spyder 编辑器中输入以下内容:
print(7*123)
将光标放在这一行,按 F9,你会得到输出 861。
表 1-1 提供了 Python 中的其他基本数学运算。
表 1-1:基本数学运算符
| 运算符 | 操作 |
|---|---|
+ |
加法:print(5+6)会给你一个结果11。 |
- |
减法:print(9-4)会给你一个结果5。 |
/ |
除法:print(9/3)会给你一个结果3。 |
** |
指数:print(5**3)会给你一个结果125。 |
% |
余数:print(13%5)会给你一个结果3,因为 13 = 5 × 2 + 3。 |
// |
整数商:print(13//5)会给你一个结果2,因为 13 = 5 × 2 + 3。 |
这些运算有优先级,意味着它们会按照特定的顺序执行。运算顺序如下:括号内的运算具有最高优先级,其次是指数运算,再然后是乘法和除法,它们具有相同的优先级,按从左到右的顺序执行。加法和减法的优先级最低,且被平等对待,因此先出现的先执行。
对于更复杂的数学运算,比如三角学中的余弦函数或自然对数,我们需要导入模块,这部分内容我将在第二章讲解。
总结
在本章中,你学习了如何通过 Anaconda 安装 Python 和 Spyder。你还学会了如何使用 Spyder 运行 Python 脚本。
在第二章中,我们将讨论你在本书其余部分需要掌握的 Python 技能。你将学习四种主要的值类型以及如何将一种类型转换为另一种类型。我们还将讨论条件执行和循环,以及函数和模块在 Python 中的工作原理,从而帮助你完成更复杂的任务。
章节末练习
-
在my_first_script.py中添加一行代码,使其打印出一条第三条消息,内容为
Here is a third message! -
以下每个 Python 语句的输出是什么?首先写下答案,然后在 Spyder 中运行命令来验证。
print(2) print(3**2) print(7//3) print(7/3) print(7%3) print(2+2) print(10*2) -
如果你想计算 55 乘以 234,Spyder 编辑器中的命令行应该输入什么?
Python 基础回顾

本章是对基本 Python 知识的回顾。本章的目的不是全面回顾 Python 中的所有基本命令,而是向你提供对本书其余部分最重要的 Python 技能。
具体来说,你将学习四种 Python 变量类型(字符串、整数、浮点数和布尔值),以及如何将一种类型转换为另一种类型。函数是编程语言中的有用工具,你将学习如何使用 Python 内置函数以及如何导入 Python 标准库中的模块。
你还将学习函数是如何工作的以及如何定义你自己的函数。本书中我们使用的许多模块不在 Python 标准库中,你将学习如何在你的计算机上安装这些模块。
我们将讨论模块的工作原理以及如何创建你自己的自定义模块。然后你将学习虚拟环境,了解它为何有用,以及如何创建和激活它。
Python 使用字符串、列表、字典和元组作为元素集合来完成复杂的任务。在这一章中,你将逐一学习这四种集合类型,并查看它们的使用示例。
在你开始之前,为本章设置文件夹/mpt/ch02。像第一章一样,本章的所有脚本可以在本书的资源页面找到,www.nostarch.com/make-python-talk/。
变量和数值
变量是一个保留的内存位置,用于在 Python(以及其他编程语言)中存储值。我们可以将值赋给变量,并使用变量名来调用关联的值。Python 有四种类型的值:字符串、浮点数、整数和布尔值。
字符串
字符串是由引号括起来的一系列字符,通常用于表示文本。以下是一些字符串的示例:
Name1 = 'University of Kentucky '
Name2 = "Gatton College 2021"
你可以使用type()函数来查找一个变量的类型。在 Spyder 编辑器中输入以下内容:
**print(type(Name1))**
**print(type(Name2))**
执行后,你将看到以下输出:
<class 'str'>
<class 'str'>
这意味着两个变量都是字符串值。你可以对字符串进行加法或乘法运算,但不是传统的数学运算;而是可以连接字符串或重复字符串。例如,假设你在 Spyder 编辑器中运行以下两行代码:
print(Name1+Name2)
print(Name1*3)
你将看到以下输出:
University of Kentucky Gatton College 2021
University of Kentucky University of Kentucky University of Kentucky
加号将两个字符串连接在一起,而将一个字符串乘以 3 意味着将字符串中的字符重复三次。请注意,我故意在字符串University of Kentucky的末尾留了一个空格,这样它们连接在一起时,字符串之间会有一个空格。
浮动类型
浮点数,也称为浮动类型,是一种数字类型,相当于数学中的小数。以下是两个浮点数的示例:
x = -17.8912
y = 0.987
你可以使用round()函数将浮动数限制为小数点后特定的位数。浮动数可以是正数、负数或零。运行以下代码:
**print(type(x))**
**print(type(y))**
**print(round(x,3))**
**print(round(y,1))**
你将看到以下输出:
<class 'float'>
<class 'float'>
-17.891
1.0
浮动类型用于执行计算。
整数
整数是另一种数字类型;它们不能有小数部分,因此必须是整数。整数主要用于 Python 中的索引目的。整数可以是正数、负数或零。以下是一些整数的示例:
a = 7
b = -23
c = 0
重要的是要知道,浮点数总是有小数部分,而整数没有。你无需告诉 Python 你想使用哪种类型;相反,Python 可以通过你提供的信息自动判断。如果你输入一个没有引号且没有小数点的数字,Python 知道你在使用整数。即使你将一个浮动数字四舍五入到小数点后零位,数字后面仍会跟着小数点和零。运行以下代码:
**print(type(a))**
**print(type(b))**
**print(type(c))**
**print(round(7.346,1))**
**print(round(7.346,0))**
你将得到以下输出:
<class 'int'>
<class 'int'>
<class 'int'>
7.3
7.0
输出显示所有三个变量,a、b和c,都是整数。你不会从print(round(7.346,0))得到7,因为使用小数是 Python 区分整数和浮动类型的一种方式。
布尔值
布尔值,或称布尔型,是二进制变量,只能取True或False的值。请注意,True和False的首字母必须始终大写。我们使用布尔值来验证代码中的真值,并进行逻辑判断。例如,运行以下两行代码来比较两个数字:
**print(4 > 5)**
**print(10 >= 6)**
你将得到以下输出:
False
True
结果显示逻辑语句4 > 5的值为False,而逻辑语句10 >= 6的值为True。True或False(不带引号)不是字符串,而是 Python 保留的特殊值。试试以下命令:
**print('4 > 5')**
**print(type(4 > 5))**
**print(type('4 > 5'))**
这是输出:
4 > 5
<class 'bool'>
<class 'str'>
如你所见,一旦你将4 > 5放入引号中,它就变成了字符串变量,而不是布尔值。
布尔值也可以用1(或者实际上,任何非零值)表示True,用0表示False。运行此代码:
**print(int(True))**
**print(int(False))**
**print(float(True))**
**print(str(False))**
它输出如下:
1
0
1.0
'False'
bool() 函数将任何非零值转换为True,将0转换为False。运行以下代码:
**print(bool(1))**
**print(bool(-2))**
**print(bool(0))**
**print(bool('hello'))**
你将得到如下输出:
True
True
False
True
转换变量类型
你可以使用函数str()、int()、bool()和float()转换变量类型,但前提是你要转换的类型与目标类型兼容。例如,你可以使用int("17")或float("17")将字符串变量"17"转换为整数或浮点数,因为 17 是一个可以被识别为整数或浮点数的数字。然而,你不能将字符串"Kentucky"转换为整数或浮点数。
请考虑以下代码行:
print(int(17.0))
print(int("88"))
print(int("3.45"))
print(str(17.0))
print(float(-4))
输出如下:
17
88
ValueError: invalid literal for int() with base 10: '3.45'
'17.0'
-4.0
布尔值True和False可以分别转换为整数1和0,因为1和0通常用来表示True和False。而浮点数17.0和字符串变量"88"可以转换为整数,但字符串变量"3.45"不能转换为整数,因为它有小数点后的数值。
几乎任何东西都可以转换成字符串变量;例如,浮点数17.0可以转换为字符串变量"17.0"。你也可以将任何整数转换为浮点数:例如,整数–4可以转换为浮点数–4.0。
变量名规则
命名变量时有一些规则,并不是所有内容都可以作为变量名。变量名必须以字母(无论大写或小写)或下划线(_)开头。例如,你不能使用8python作为变量名,因为它以数字开头。
变量名唯一可以包含的特殊字符是下划线,因此像@或&这样的特殊字符是不允许的。请参阅 Python 命名规范:www.python.org/dev/peps/pep-0008/#id34/.
变量名不能是 Python 关键字或 Python 内建函数。要获取所有关键字的列表,可以在 Spyder 编辑器中运行以下两行代码:
**from keyword import kwlist**
**print(kwlist)**
输出是 Python 关键字的完整列表:
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break',
'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally',
'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal',
'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
然而,变量名可以包含关键字。例如,first_break和class1是有效的变量名,即使break和class不是。
变量名不应当是 Python 内建函数。图 2-1 列出了这些函数,相关内容可以在 Python 文档中的docs.python.org/3/library/functions.html. 你应该熟悉这个列表,并避免将这些术语作为变量名。

图 2-1:Python 内建函数列表
循环与条件执行
循环和条件语句让你在代码中做出决策,以便在特定条件发生时运行某些代码。
条件执行
if 语句允许你的代码根据条件是否满足来执行特定的操作。请考虑以下代码行:
x = 5
if x > 0:
print('x is positive')
else:
print('x is nonpositive')
这里,x > 0是条件。如果x的值大于 0,则条件成立,脚本会打印出消息x is positive。Python 中的条件语句总是在条件语句后面需要一个冒号(:)。如果条件不满足,脚本将跳到else分支并打印x is nonpositive。
我们还可以通过使用elif关键字来处理多个条件。考虑以下代码:
x = 5
if x > 0:
print('x is positive')
elif x == 0:
print('x is zero')
else:
print('x is negative')
Python 使用双等号(==)作为比较运算符,以区别于我们使用单等号(=)进行赋值操作。当满足某个条件时,脚本有三种可能的输出结果:x is positive,x is zero,或x is negative。
如果我们需要更多的条件,首个条件必须紧跟在if语句后,最后一个条件必须跟在else语句后,而所有中间的条件应使用elif关键字:
score = 88
if score >= 90:
print('grade is A')
elif score >= 80:
print('grade is B')
elif score >= 70:
print('grade is C')
elif score >= 60:
print('grade is D')
else:
print('grade is F')
脚本根据分数的值打印字母成绩:如果分数大于或等于 90,则为 A;如果不是,则分数大于 80 时为 B,依此类推。
循环
计算机的一个巨大优势是它们能够快速地重复执行相同的任务。这在编程中被称为循环或迭代。Python 有两种循环类型:while循环和for循环。
while循环
while循环用于在满足某个条件时执行一段代码块。在这里,我们使用while语句创建一个循环,每次循环都将n的值加 1,直到n达到3。然后,循环退出,脚本打印finished。将此保存为whileloop.py:
n = 0
while n < 3:
n = n+1
print(n)
print('finished')
我们首先将n赋值为0。然后,脚本以条件n < 3开始while循环。只要条件满足,循环就会继续执行。注意冒号,它告诉 Python 接下来缩进的行是循环的一部分。这些行将在每次循环运行时执行。最后一行没有缩进,只有在循环结束后才会执行。
在第一次迭代中,n的值从0增加到1,并打印出更新后的n值。在第二次迭代中,n的值增加到2,并打印出更新后的n值。在第三次迭代中,n的值增加到3,并打印出3。当脚本进入第四次迭代时,条件n < 3不再满足,循环停止。之后,执行最后一行。结果,我们看到来自whileloop.py的以下输出:
1
2
3
finished
while循环在我们事先不知道需要多少次迭代时最为有用,尽管它也可以用于执行与for循环相同的任务。在本书的后面部分,我们经常使用语句while True来创建一个无限循环,将脚本置于待机模式。
for循环
for循环通常用于你想要执行固定次数的代码块。以下脚本forloop.py是一个for循环的示例,它实现了与我们刚才做的while循环相同的功能,将变量n加 1,直到n达到3:
for n in range(3):
n = n + 1
print(n)
print('finished')
我们首先使用range(),这是 Python 中的一个内建函数,来生成从 0 到 2 的数值范围(Python 的计数从 0 开始)。这一行指示脚本循环遍历这三个值,每次循环处理一个值,并为每个值执行接下来的两行代码,每次循环将n加 1。当范围用尽时,循环退出,输出finished。
forloop.py中的代码输出与whileloop.py相同。
循环中的循环
你可以将一个循环放在另一个循环内,这就是所谓的嵌套。嵌套循环在每次外层循环迭代时,需要对内层循环的每一次迭代重复某些操作时非常有用。示例脚本loop_in_loop.py遍历一个列表和一个元组,在每次迭代中打印列表中的每个成员与元组中的每个成员,每次迭代打印一对值:
for letter in ["A", "B", "C"]:
for num in (1, 2):
print(f"this is {`letter`}{`num`}")
首先,我们使用for开始外层循环,然后第一行缩进代码开始内层循环。脚本获取外层循环中的第一个值,遍历内层循环的所有迭代,并在每次迭代时打印一条消息。然后它会用外层循环中的第二个值重复这个过程。我们需要将内层循环的内容缩进两次,这样脚本才能知道哪些代码行属于哪个循环。loop_in_loop.py的最终输出如下所示:
this is A1
this is A2
this is B1
this is B2
this is C1
this is C2
请注意,我们使用了f"{}"字符串格式化的方法。字符串f"this is {``letter``}{``num``}"告诉 Python 将花括号中的内容替换为相应变量的实际值。
你可以将循环嵌套得几乎无限深,脚本会在每次外层循环值的组合下,遍历内层循环的所有值。然而,过多的嵌套会使代码难以阅读,因此通常不推荐这种做法。
循环命令
循环有几个命令,方便控制循环的行为——即continue、break和pass。这些命令通过使用if语句,让你在循环中做出决策。
continue
continue命令告诉 Python 停止执行当前迭代中的剩余命令,并进入下一次迭代。当你希望在满足某些条件时跳过某些操作时,可以使用continue。例如,脚本forloop1.py使用continue命令跳过打印数字 2,并进入下一次迭代:
for n in (1, 2, 3):
if n == 2:
continue
1 print(n)
print('finished')
当n的值为2时,第一行代码不会执行,因为continue命令告诉脚本跳过这一行,直接进入下一次迭代。该脚本的输出如下所示:
1
3
finished
break
break命令告诉 Python 打破循环并跳过所有剩余的迭代。当你希望退出循环时,使用break。示例脚本forloop2.py使用break命令,当数字达到值2时退出for循环:
for n in (1, 2, 3):
if n == 2:
break
print(n)
1 print('finished')
当n的值为2时,整个循环停止,脚本直接跳到第 1 行。因此,输出如下:
1
finished
在本书的后面部分,我们将经常使用break命令来告诉脚本停止由while True语句生成的无限循环。
pass
pass命令告诉 Python 什么都不做,它在需要一行命令但不需要执行任何操作时使用。我们通常与try和except一起使用它,稍后我们将在本书中再次提到这个命令。脚本forloop3.py使用pass命令,当数字的值为2时,告诉脚本不执行任何操作:
for n in (1, 2, 3):
if n == 2:
pass
print(n)
print('finished')
当n的值为2时,不需要采取任何操作。因此,下面是前一个脚本的输出:
1
2
3
finished
这与forloop.py的输出相同。
字符串
字符串是由单引号或双引号括起来的字符序列。字符串中的字符可以是字母、数字、空格或特殊字符。我们将讨论如何对字符串中的元素进行索引、如何切片以及如何将多个字符串连接在一起。
字符串索引
字符串中的字符是从左到右按 0 开始索引的。这是因为 Python 使用零基索引,因此第一个元素的索引是 0,而不是 1\。
你可以通过使用方括号操作符和字符的索引来访问字符串中的字符:
msg = "hello"
print(msg[1])
由于e是字符串"hello"中的第二个字符,因此输出是:
e
Python 还使用负索引,它是从字符串的末尾开始的。字符串中的最后一个字符可以通过[-1]索引,倒数第二个字符通过[-2]索引,依此类推。当你有一个很长的字符串并且希望定位字符串末尾的字符时,这非常有用。
要查找字符串msg的倒数第三个字符,你可以使用以下代码:
print(msg[-3])
下面是输出:
l
字符串切片
切片字符串意味着提取字符的一个子集。我们再次使用方括号操作符:
msg = "hello"
print(msg[0:3])
这将输出以下内容:
hel
代码msg[``a``:``b``]从字符串msg中的位置a到位置b提取子字符串,其中位置a的字符包含在子字符串中,但位置b的字符不包含。因此,msg[0:3]会生成字符串msg中前面三个字符的子字符串。
字符串方法
我将介绍一些我们在本书中将会使用的常见字符串方法。
replace()
replace()方法用于将字符串中的某些字符或子字符串替换为其他字符。它接受两个参数:要替换的字符和用来替换它的字符。例如:
inp = "University of Kentucky"
inp1 = inp.replace(' ','+')
print(inp1)
我们使用replace()将所有空格替换为加号。前一个脚本的输出如下所示:
University+of+Kentucky
本方法将在本书稍后的章节中有用,当我们处理语音识别功能时。我们将使用 replace() 方法将语音引擎的语音文本转换为适合脚本的格式。
lower()
lower() 方法将字符串中的所有大写字母转换为小写字母。由于 Python 字符串区分大小写,因此在匹配字符串时将所有字母转换为小写,意味着我们不会错过应该匹配的大小写子字符串。
假设我们希望通过语音识别模块捕捉到“department of education”这一语音短语。我们不能确定该短语是否会被捕捉为 Department of Education。你可以使用 lower() 将短语转换为全小写字符串,以避免不匹配,示例如下:
inp = "Department of Education"
inp1 = "department of education"
print(inp.lower() == inp1.lower())
该脚本测试了当忽略大小写时,两个字符串 inp 和 inp1 是否相同。输出如下:
True
find()
你可以使用 find() 来查找字符在字符串中的位置。该方法返回字符在字符串中的索引。
将以下代码行输入 Spyder 编辑器并保存为 extract_last_name.py,然后运行:
**email = "John.Smith@uky.edu"**
**pos1 = email.find(".")**
**print(pos1)**
**pos2 = email.find("@")**
**print(pos2)**
**last_name = email[(1+pos1):pos2]**
**print(last_name)**
字符串变量 email 有一个模式:它由名字、点和姓氏组成,后面跟着 @uky.edu。我们使用这个模式来定位点和@符号的位置,然后根据这两个位置提取姓氏。
首先,我们获取 . 的位置,并将其定义为变量 pos1。然后,我们找到 @ 的位置,并将其定义为 pos2。最后,我们对字符串进行切片,取出两个位置之间的字符,并将子字符串作为变量 last_name 返回。
运行脚本后应该输出以下内容:
4
10
Smith
. 和 @ 在电子邮件中的索引分别是 4 和 10,而姓氏是 Smith。
你还可以使用字符串方法 find() 来定位子字符串。该方法返回子字符串在原始字符串中的起始位置。例如,如果你运行以下代码:
email = "John.Smith@uky.edu"
pos = email.find("uky.edu")
print(pos)
你将得到以下输出:
11
输出显示子字符串 uky.edu 从电子邮件中的第 12 个字符开始。
split()
split() 方法将字符串拆分成多个字符串,使用指定的分隔符。在 Spyder 中输入以下代码并运行:
**msg = "Please think of an integer"**
**words = msg.split()**
**print(words)**
输出如下:
['Please', 'think', 'of', 'an', 'integer']
默认的 delimiter(分隔符的 fancy 名称)是空格(' ')。你也可以在使用 split() 时指定分隔符。让我们重新审视从电子邮件地址中提取姓氏的例子,并将新脚本命名为 split_string.py,如 列表 2-1 所示。
email = "John.Smith@uky.edu"
(name, domain) = email.split('@')
(first, last) = name.split('.')
print(f"last name is {last}")
列表 2-1:使用分隔符拆分电子邮件地址
我们首先通过使用@作为分隔符,将电子邮件分为两部分,并将用户名和域名赋值给元组。(我们将在本章后面讨论元组的定义。)结果,元组中的第一个元素,即变量name,是子字符串:John.Smith。然后,脚本将John.Smith按照.分隔符分为名字和姓氏,并将它们保存在元组(``first``, ``last``)中。最后,我们打印出元组中的第二个元素作为姓氏。
输出如下所示:
last name is Smith
join()
join()方法将多个字符串连接成一个字符串,就像这个脚本中的join_string.py一样:
mylink = ('&')
strlist = ['University', 'of', 'Kentucky']
joined_string = mylink.join(strlist)
print(joined_string)
我们将&定义为变量mylink,作为分隔符。strlist是一个包含我们希望连接的三个单词的列表。我们使用join()将这三个单词组合成一个单一的字符串。请注意,join()需要放在分隔符之后。最后,我们打印出连接后的字符串:
University&of&Kentucky
列表
列表是由逗号分隔的值的集合。列表中的值称为元素,或项,它们可以是值、变量或其他列表。
创建一个列表
要创建一个新列表,只需将元素放在方括号中:
lst = [1, "a", "hello"]
我们定义了一个包含三个元素的列表lst:一个整数1和两个字符串。注意,list()是 Python 的内置函数,因此不能将list用作变量名或列表名。我建议你使用具有描述性的名称,以帮助未来的读者理解代码。
你可以使用一对空的方括号创建一个空列表:
lst1 = []
或者,你也可以使用list()函数:
lst2 = list()
访问列表中的元素
你可以使用括号操作符访问列表中的元素:
lst = [1, "a", "hello"]
print(lst[2])
这将生成以下内容:
hello
在这里,lst[2]指的是列表中的第三个元素,因为 Python 与大多数计算机编程语言一样,从零开始计数。
你可以使用循环遍历列表中的元素:
for x in range(len(lst)):
print(lst[x])
这给我们以下结果:
1
a
hello
我们使用内置函数len()来返回列表的长度,在此例中是3。内置函数range()在此返回0、1和2。
使用列表的列表
一个列表可以将其他列表作为元素。这对于将元素位置映射到二维空间中的坐标非常有用。以下是一个示例:
llst = [[1,2,3,5],
[2,2,6,8],
[2,3,5,9],
[3,5,4,7],
[1,3,5,0]]
print('the value of llst[1][2] is ', llst[1][2])
print('the value of llst[3][2] is ', llst[3][2])
print('the value of llst[1][3] is ', llst[1][3])
这是输出结果:
the value of llst[1][2] is 6
the value of llst[3][2] is 4
the value of llst[1][3] is 8
列表llst本身包含五个列表。为了查找llst[1][2]的值,代码首先查看外部列表llst中的第二项,即列表[2, 2, 6, 8]。该列表的第三个元素是6,因此llst[1][2] = 6。
现在,让我们在二维空间中绘制一个相应的图形,如图 2-2 所示。

图 2-2:将列表的列表映射到二维空间
我们将在第三部分中使用它来创建互动游戏的棋盘。
添加或乘法列表
你可以在列表上使用加号(+)和乘号(*)操作符,但不是在数学意义上。例如,运行以下代码:
**lst = [1, "a", "hello"]**
**print(lst + lst)**
**print(lst * 3)**
你应该看到以下输出:
[1, "a", "hello", 1, "a", "hello"]
[1, "a", "hello", 1, "a", "hello", 1, "a", "hello"]
加号操作符将两个列表合并成一个更大的列表,乘号操作符将列表中的元素重复。如果你将一个列表乘以 3,元素将出现三次。
列表方法
我将在这里介绍几个有用的列表方法,我们将在本书的后续章节中使用它们。
enumerate()
enumerate()方法会打印出列表中的所有元素及其对应的索引。假设我们有以下列表names:
names = ['Adam','Kate','Peter']
以下代码行
for x, name in enumerate(names):
print(x, name)
将生成如下输出:
0 Adam
1 Kate
2 Peter
索引为0的第一个元素是 Adam,索引为1的第二个元素是 Kate,依此类推。
你可以选择将起始值设置为 1 而不是 0,使用start=1,如下所示:
names = ['Adam','Kate','Peter']
for x, name in enumerate(names, start=1):
print(x, name)
输出结果如下:
1 Adam
2 Kate
3 Peter
append()
你可以通过使用append()方法将一个元素添加到列表的末尾。考虑这个脚本,list_append.py:
lst = [1, "a", "hello"]
1 lst.append(2)
print(lst)
这段代码正在将元素2添加到现有的列表lst中,生成如下结果:
[1, "a", "hello", 2]
新的lst现在有四个元素。
你每次只能添加一个元素,它会被默认添加到列表的末尾。添加两个元素会导致错误信息。请将脚本list_append.py中的第 1 行更改为如下:
lst.append(2**, 3)**
你将收到如下错误信息:
TypeError: append() takes exactly one argument (2 given)
但是,你可以将多个元素作为列表添加。将两个数字用方括号括起来,如下所示:
lst.append([2, 3])
你将得到以下输出:
[1, "a", "hello", [2, 3]]
新的列表有四个元素。
要将两个或更多元素添加到现有列表中,你应该使用加号操作符。例如,要将 2 和 3 作为两个独立的元素添加到列表中,可以使用以下代码:
lst + [2, 3]
输出将如下所示:
[1, a, "hello", 2, 3]
remove()
你可以通过使用remove()方法从列表中删除一个元素:
lst = [1, "a", "hello", 2]
lst.remove("a")
print(lst)
我们移除了索引为 1 的元素,结果如下:
[1, "hello", 2]
新的列表中不再有元素a。每次只能移除一个元素。
index()
你可以通过使用index()方法找到元素在列表中的位置:
lst = [1, "a", "hello", 2]
print(lst.index("a"))
从中我们得到如下结果:
1
结果告诉你,元素a在列表中的索引为1。
count()
你可以通过使用count()来计算一个元素在列表中出现的次数:
lst = [1, "a", "hello", 2, 1]
print(lst.count(1))
print(lst.count("a"))
这将生成如下结果:
2
1
这告诉我们,元素1在列表中出现了两次,而元素a出现了一次。
sort()
你可以通过使用sort()方法对列表中的元素进行排序。元素必须是相同类型(或者至少可以转换为相同类型)。例如,如果你的列表中同时包含整数和字符串,尝试对其排序时会出现如下错误信息:
TypeError: '<' not supported between instances of 'str' and 'int'
数字按从小到大的顺序排序。如果在方法中添加reverse=True作为选项,它将会反转排序顺序。以下是一个示例:
lst = [5, 47, 12, 9, 4, -1]
lst.sort()
print(lst)
lst.sort(reverse=True)
print(lst)
这将输出如下:
[-1, 4, 5, 9, 12, 47]
[47, 12, 9, 5, 4, -1]
字母按字母顺序排序,并且排在数字之后。考虑以下示例:
Lst = ['a', 'hello', 'ba', 'ahello', '2', '-1']
lst.sort()
print(lst)
输出显示如下:
['-1', '2', 'a', 'ahello', 'ba', 'hello']
使用内建函数与列表
我们可以直接对列表使用几个 Python 内置函数,包括min()、max()、sum()和len()。这些函数分别返回列表的最小值、最大值、总和和长度,如下所示:
lst = [5, 47, 12, 9, 4, -1]
print("the range of the numbers is", max(lst)-min(lst))
print("the mean of the numbers is", sum(lst)/len(lst))
这是输出结果:
the range of the numbers is 48
the mean of the numbers is 12.666666666666666
list()
你可以使用list()函数将字符串转换为字符列表:
msg = "hello"
letters = list(msg)
print(letters)
输出如下:
['h', 'e', 'l', 'l', 'o']
有趣的是,Python 字符串可以像字符列表一样处理。
字典
字典是一个包含键值对的集合。我们通过将元素放入花括号中来创建字典,如列表 2-2 所示。
scores = {'blue':10, 'white':12}
列表 2-2:创建一个包含两个键值对的字典
字典scores有两个键值对元素,用逗号分隔:第一个元素是键blue,值为10,通过位置和冒号分隔。第二个元素是'white':12。
要创建一个空字典,你可以使用dict()或一对空的花括号:
Dict1 = dict()
Dict2 = {}
你可以按如下方式向现有字典中添加一个新元素:
Dict3 = {}
Dict3['yellow'] = 6
print(Dict3)
这一行Dict3['yellow'] = 6将值6分配给键yellow。新的Dict3包含了元素6,可以通过键yellow访问该元素。
访问字典中的值
你可以通过使用方括号操作符来访问字典中的值。每个键值对中的键值充当索引。例如,我们可以按照如下方式访问列表 2-2 中score字典的值:
print(scores['blue'])
print(scores['white'])
这将给你以下结果:
10
12
我们还可以使用get()方法。使用get()的优点是,当用户请求一个不在字典中的键时,它会返回None作为默认值,而不是返回错误。请考虑以下脚本,get_score.py:
scores = {'blue':10, 'white':12}
print(scores['blue'])
print(scores['white'])
print(scores.get('yellow'))
print(scores.get('yellow',0))
这将产生以下结果:
10
12
None
0
由于键yellow不在scores中,方法get('yellow')返回值None。进一步地,当你在方法中添加选项0时,get('yellow', 0)将返回值0。
使用字典方法
你可以使用keys()方法来生成字典中所有键的列表:
scores = {'blue':10, 'white':12}
teams = list(scores.keys())
print(teams)
这将给我们以下结果:
['blue', 'white']
我们可以使用values()方法生成字典中所有值的列表:
points = list(scores.values())
print(points)
输出如下:
[10, 12]
我们可以使用items()获取每个键值对的列表,作为元组(参见第 37 页的“元组”)。
print(list(scores.items()))
这将产生以下结果:
[('blue', 10), ('white', 12)]
如何使用字典
字典中的值可以是任何类型的变量、列表,甚至另一个字典。这里我们有一个使用列表作为值的字典:
scores2 = {'blue':[5, 5, 10], 'white':[5, 7, 12]}
每个键的值是一个包含三个元素的列表。这三个值分别表示每个玩家在比赛上半场、下半场和总分的得分。要查找白队在下半场的得分,可以调用以下内容:
print(scores2['white'][1])
字典的优势在于它的键可以是任何值,不一定是整数。这使得字典在许多场景下都非常有用。例如,most_freq_word.py使用字典来统计单词:
news = (
'''Python is an interpreted, high-level, and general-purpose programming
language. Python's design philosophy emphasizes code readability with
its notable use of significant whitespace.
Its language constructs and object-oriented approach aim to help
programmers write clear, logical code for small- and large-scale
projects.
''')
wdcnt = dict()
wd = news.split()
for w in wd:
wdcnt[w] = wdcnt.get(w, 0) + 1
print(wdcnt)
for w in list(wdcnt.keys()):
if wdcnt[w] == max(list(wdcnt.values())):
print(w)
我们将news定义为一个包含简短段落的字符串变量。然后我们创建一个空字典wdcnt。接下来,我们将字符串拆分为一个单独单词的列表。然后我们统计每个单词的频率,并将信息存储在字典中,使用单词作为键,单词的计数作为值。因为我们使用get()方法,如果字典中没有该单词作为键,get()中的第二个参数会将该单词的值设为0。
最后,我们打印出频率最高的单词。结果如下:
{'Python': 1, 'is': 1, 'an': 1, 'interpreted,': 1, 'high-level': 1, 'and': 3,
'general-purpose': 1, 'programming': 1, 'language.': 1, "Python's": 1,
'design': 1, 'philosophy': 1, 'emphasizes': 1, 'code': 2, 'readability': 1,
'with': 1, 'its': 1, 'notable': 1, 'use': 1, 'of': 1, 'significant': 1,
'whitespace.': 1, 'Its': 1, 'language': 1, 'constructs': 1,
'object-oriented': 1, 'approach': 1, 'aim': 1, 'to': 1, 'help': 1,
'programmers': 1, 'write': 1, 'clear,': 1, 'logical': 1, 'for': 1,
'small-': 1, 'large-scale': 1, 'projects.': 1}
and
事实证明,新闻文章中最常见的单词是and,它出现了三次。
交换键和值
有时你可能需要交换键和值的位置。现在让我们字面上理解字典的含义,假设你有以下英语到西班牙语的字典,使用英语单词作为键,西班牙语翻译作为值:
spanish = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
你想创建一个西班牙语到英语的字典。你可以通过使用以下代码行来实现:
**english = {y:x for x,y in spanish.items()}**
命令x,y in spanish.items()检索spanish中的所有键值对。命令y:x for x,y交换了键和值的位置。你必须在等号右侧的所有内容周围加上花括号,以便脚本将其视为字典。为了验证,输入以下内容:
**print(english)**
你将得到以下输出:
{'uno': 'one', 'dos': 'two', 'tres': 'three'}
合并两个字典
要将两个字典x和y合并为一个大的字典z,你可以赋值z = {**x, **y}:
spanishenglish = {**spanish, **English}
结果是一个新的字典,称为spanishenglish,其中包含六个元素:三个来自spanish,三个来自english。
元组
元组是由逗号分隔的值的集合,类似于列表——但最大的区别在于元组在定义后不能被更改(也就是说,元组是不可变的)。元组的元素存在圆括号内,而不是方括号,以此来区分元组和列表。这里我们创建一个元组并尝试修改它:
tpl = (1, 2, 3, 9, 0)
tpl.append(4)
print(tpl)
我们得到以下错误信息:
AttributeError: 'tuple' object has no attribute 'append'
因为元组是不可变的,我们不能对其使用append()或remove()等方法。我们也无法对元组中的元素进行排序。
元组的元素是通过整数索引的,我们可以使用方括号运算符来访问它们:
tpl = (1, 2, 3, 9, 0)
print(tpl[3])
print(tpl[1:4])
我们的输出如下所示:
9
(2, 3, 9)
我们在split_string.py中看到过将值赋给元组的例子(清单 2-1)。
你可以比较两个元组。这个过程从比较它们的第一个元素开始。如果第一个元素相同,我们接着检查第二个元素是否匹配。如果第二个元素也相同,我们再比较第三个元素,依此类推,直到发现不同之处。
在你的 Spyder 编辑器中运行以下代码:
**lt = [(1, 2), (3, 9), (0, 7), (1, 0)]**
**lt.sort()**
**print(lt)**
你将看到以下输出:
[(0, 7), (1, 0), (1, 2), (3, 9)]
函数
函数(理想情况下)是一个为完成特定任务而设计的代码块。有许多执行各种任务的函数,但通常认为最佳实践是设计一个只执行一个任务的函数(并且不更改其他变量)。有些函数有定义的参数(输入)。我们可以将函数代码分配给一个变量名,这样每次需要完成任务时,就不必重复相同的代码。相反,我们只需调用该函数并输入参数。
函数还可以提高代码的可读性,使代码更有组织性,减少混乱并减少出错的可能性。
使用内置 Python 函数
Python 自带了许多可以直接使用的内置函数,包括第一章中的print()。在这里,我将讨论一些我们在本书中将频繁使用的内置函数。
range() 函数
range() 函数用于生成一个整数列表。当我们在第 21 页讨论循环时,我们已经介绍过range()。我们知道,例如,range(5)会生成值[0, 1, 2, 3, 4]。range()函数生成的默认起始值是0,因为 Python 使用零索引,但你也可以指定起始值。例如,range(3, 6)会生成以下三个值的列表:[3, 4, 5]。
默认增量值是1,但你也可以将增量值作为可选的第三个参数来指定。例如,代码
for x in range(-5, 6, 2):
print(x)
将会输出以下内容:
-5
-3
-1
1
3
5
range(-5, 6, 2)中的第三个参数告诉脚本每个元素的值增加 2。
如果增量值是负整数,列表中的值会减少。例如,range(9, 0, -3)会生成列表[9, 6, 3]。
input() 函数
语音合成是将书面文本转换为人类语音的过程,因此了解 Python 如何使用一个名为input()的内置函数来接收书面文本输入是很重要的。
在 Spyder 中运行以下脚本:
**color = input('What is your favorite color?')**
**print('I see, so your favorite color is {}'.format(color))**
你应该看到一个类似于图 2-3 的屏幕。

图 2-3:Python 请求输入时的截图
如你在图 2-3 中看到的,脚本在右下角的 IPython 控制台中请求你的输入。它会等待你输入一些文本并按下回车键,然后才会继续运行。如果你输入blue,脚本将输出以下内容:
What is your favorite color? **blue**
I see, so your favorite color is blue.
你可以像这样请求多个输入:
FirstName = input('What is your first name?\n')
LastName = input('What is your last name?\n')
print(f'Nice to meet you, {FirstName } {LastName }.')
脚本请求两个输入。序列\n是一个转义字符,用于在问题“你的名字是什么?”下方插入新的一行。
定义你自己的函数
除了使用内置函数外,我们还可以构建自己的函数。我将向你展示如何创建一个函数,这个过程也将展示函数是如何工作的。函数可以接受一个或多个输入,称为参数,或者没有任何输入。
没有参数的函数
我们将从构建一个打印Finished printing消息的函数开始。这个函数不接受任何输入:
def TheEnd():
print('Finished printing')
for i in (1, 2, 3):
print(i)
TheEnd()
我们使用 def 来表示函数定义,给函数命名,并紧跟空括号和冒号。冒号告诉 Python 要期待函数体。之后的所有缩进行都被视为函数的一部分。
脚本打印出三个数字,接着我们调用该函数。输出结果如下:
1
2
3
Finished printing
如你所见,函数中的命令只有在函数被调用时才会执行,而不是在函数定义时执行。
一个带有一个参数的函数
现在我们将编写一个接受一个输入的函数。我们需要给 50 个人写感谢信。除收件人的名字外,信息内容相同。我们将定义一个函数来打印信息,每次调用时只需要提供每个消息的名字。我们首先定义一个名为 msgs() 的函数,具体如下:
def msgs(name):
print(f"Thank you, {name}, I appreciate your help!")
函数的名称是 msgs,其唯一的输入是变量 name。如果我们按如下方式调用该函数两次:
msgs("Mary")
msgs("Bob")
输出将是这样的:
Thank you, Mary, I appreciate your help!
Thank you, Bob, I appreciate your help!
要写 50 张感谢信,你可以调用该函数 50 次:每次传入一个名字。
一个带有多个参数的函数
函数可以有两个或更多的输入参数。请参考列表 2-3 中的脚本 team_sales.py,它定义了一个需要三个输入的函数。
def team_sales(sales1, sales2, sales3):
sales = sales1 + sales2 + sales3
return sales
print(team_sales(100, 150, 120))
列表 2-3:定义一个带有三个参数的函数
我们定义了一个函数来计算一个由三名成员组成的小组的总销售额。该函数使用单个成员的销售额 sales1、sales2 和 sales3 作为三个参数。我们通过将三项销售额相加来计算团队的总销售额 sales。然后,我们通过使用 return 命令告诉脚本输出该函数的结果。因此,当调用函数 team_sales() 时,你将得到三项销售额的总和。
如果个人销售额分别为 100、150 和 120,当我们调用函数 team_sales() 时,输出将是 370。
一个接收不定数量参数的函数
有时输入的数量是未知的。例如,你想定义一个函数来计算一组销售人员的总销售额,但不同的小组有不同数量的销售人员。你可以通过使用参数 *args 来定义一个适用于任何小组规模的函数,该参数允许你将不同长度的多个值传递给函数。列表 2-4 中的 total_sales.py 就完成了这项工作。
def total_sales(*args):
total = 0
for arg in args:
total = total + arg
return total
`--snip--`
列表 2-4:total_sales.py 的第一部分
我们开始 total_sales(),它接收 *args 作为参数。我们将变量 total 的值设置为 0,然后遍历参数 args 中的每个元素。对于参数中的每个元素,我们将其加到变量 total 中。最后我们输出该小组的总销售额。可以通过列表 2-5 来测试。
`--snip--`
print(total_sales(200,100,100,100))
print(total_sales(800,500,400))
列表 2-5:脚本 total_sales.py 的第二部分
从中我们得到以下结果:
500
1700
如你所见,函数接收一个参数 *args,但你可以在函数中放入任意多个元素。
模块
你不仅仅限于使用 Python 内建函数。Python 标准库有许多模块,提供了其他可以从你自己的代码中调用的函数。
导入模块
我们将讨论使用模块中函数的三种方式以及每种方式的优缺点。
导入模块
第一种方式是导入整个模块。例如,要查找 30 度角的余弦值,你可以先导入 math 模块。然后,你可以通过调用模块名和函数名来使用模块中的 cos() 函数:math.cos()。
在 Spyder 中输入以下代码:
**import math**
**print(math.cos(30))**
你将得到一个输出 0.15425144988758405。
你必须在调用 math.cos() 之前先导入模块。如果你没有导入 math 模块并直接运行这个命令:
print(math.cos(30))
Python 会给出一个错误信息:
NameError: name 'math' is not defined
此外,当你调用函数时,必须始终将模块名放在函数名之前。在 Python 中输入以下两行代码:
**import math**
**print(cos(30))**
你将收到这个错误信息:
NameError: name 'cos' is not defined
这是因为 Python 不知道在哪里找到 cos() 函数,尽管你已经导入了 math 模块。
导入函数
如果你只需要某个模块中的一两个函数,可以通过只导入这一个或两个函数来节省时间。这样你就可以直接使用函数名,而不必附加模块名。输入以下两行代码:
**from math import cos, log**
**print(cos(30)+log(100))**
你将得到正确的输出 4.759421635875676。我们不需要使用 math,因为我们已经告诉脚本在哪里查找这两个函数。如果你需要在脚本中使用这个函数几十次或上百次,这种方式特别有用。
使用星号导入
如果你的脚本依赖于模块中的许多函数,你可以通过使用星号导入 from module import * 来节省时间。然而,许多 Python 社区的成员警告不要使用这种方式,因为 import * 语句可能会污染你的命名空间,可能会干扰你自己定义的函数(或来自其他模块的函数)。我们在本书中不会使用这种方法。
创建你自己的模块
在 列表 2-3 中,我们在 team_sales.py 文件中定义了 team_sales() 函数,然后调用了该函数。你可能需要在多个脚本中计算总销售额。通过将该函数构建到模块中,你可以避免在每个脚本中重新编写代码。
让我们首先创建一个名为 create_local_module.py 的脚本,如 列表 2-6 所示。
def team_sales(sales1, sales2, sales3):
sales = sales1 + sales2 + sales3
return sales
列表 2-6:本地模块的代码
这个脚本定义了 team_sales() 函数,但没有调用它。接下来,创建一个新的脚本 import_local_module.py,如 列表 2-7 所示,并将其保存在与 create_local_module.py 相同的文件夹中。
from create_local_module import team_sales
print(team_sales(100, 160, 200))
print(team_sales(200, 250, 270))
print(team_sales(150, 120, 200))
列表 2-7:导入本地模块的代码
当你导入一个模块时,Python 首先会在导入脚本所在的目录中查找,所以模块必须位于相同的文件夹中。这种模块被称为 本地模块。
如果你运行脚本,你将获得以下结果:
460
720
470
team_sales() 函数正确计算了三个团队的总销售额。
本地模块与 Python 标准库中的模块一样工作,但它们需要存放在 Python 期望它们所在的文件夹中。
对于你下载的模块,Python 会在后台存储已下载模块的文件路径,并在导入时遵循该路径。例如,tkinter 包就在我们稍后在本书中使用的 Python 标准库中。当你安装它时,文件会被放置在一个特定的路径下,在 Windows 系统中通常是以下类似的路径:
C:\Users\`ME`\Anaconda3\envs\MYEV\Lib\tkinter
它被这样隐藏起来,是为了防止你不小心更改或丢失它,否则你将无法继续使用它。
使用第三方模块
Python 的主要优势之一是程序员可以相互免费分享模块。这些模块中有许多不在 Python 标准库中,包括我们将用于文本转语音和语音识别功能的模块。这些外部模块,被称为 第三方模块,可以单独安装。在安装之前,你需要先检查该模块是否已安装。
检查已安装模块
Python 标准库中的所有模块都会在你安装 Python 时自动安装到你的计算机上。其他模块可能会在你下载各种软件或模块时一并安装。例如,当你在第十四章安装 pandas 时,约有 23 个其他支持模块也将被安装,因为 pandas 依赖它们。
你可以通过以下命令在 Spyder 编辑器中检查模块是否已经安装:
help("modules")
这将为你提供计算机上所有已安装模块的列表。不过,Python 列出所有模块并让你检查它们可能需要较长时间。
另外,你也可以通过尝试导入模块来检查它是否已安装:
import `ModuleName`
要检查 pandas 是否已安装在你的计算机上,运行 import pandas,如果没有错误消息,说明模块已经安装。如果输出显示 ModuleNotFoundError,则需要安装它。让我们来看一下如何安装。
Pip 安装模块
我们将在第四章使用的 gTTS 模块不包含在 Python 标准库中,因此我们需要通过 pip install 安装它。打开 Anaconda 提示符(Windows)或终端(Mac 或 Linux),并输入以下命令:
**pip install gTTS**
按照屏幕上的指示完成操作,gTTS 模块将被安装。
Conda 安装模块
如果你通过 pip install 找不到你想要的模块,可以尝试 conda install。
我们将通过在 Anaconda 提示符(Windows)或终端(Mac 或 Linux)中使用以下命令来安装 yt 模块:
**conda install yt**
许多人认为 pip install 和 conda install 是一样的,但其实它们并不相同。Pip 是 Python 包管理机构推荐的工具,用于从 Python 包索引安装包。你只能使用 pip install 安装 Python 软件。与此相对,Conda 是一个跨平台的包和环境管理器,它不仅安装 Python 软件,还可以安装 C 或 C++ 库中的包、R 包或其他软件。
随着你在 Python 中构建越来越多的项目,你将会安装许多模块。有些模块可能会与其他模块发生冲突,而且不同的项目可能使用相同模块的不同版本。为了避免模块冲突问题,我建议你为每个项目创建一个虚拟环境。虚拟环境是一种将项目彼此隔离的方式。
创建虚拟环境
要创建虚拟环境,请打开 Anaconda 提示符(在 Windows 中)或终端(在 Mac 或 Linux 中)。我们将为本书中的项目命名虚拟环境为 chatting。输入以下命令:
**conda create -n chatting**
按下回车键后,按照屏幕上的指示操作,当提示你输入 y/n 时按 y。一旦你在机器上创建了虚拟环境,就需要激活它。
在 Windows 中激活虚拟环境
在 Anaconda 提示符(在 Windows 中)或终端(在 Mac 或 Linux 中),输入以下命令:
**conda activate chatting**
在 Windows 中,你将在 Anaconda 提示符中看到以下内容:
(chatting) C:\>
你可以看到 (chatting) 提示符,表示命令行现在位于你刚刚创建的虚拟环境 chatting 中。
在 Mac 上,你应该在终端中看到类似以下内容(用户名会不同):
(chatting) Macs-MacBook-Pro:~ macuser$
在 Linux 中,你应该在终端中看到类似以下内容(用户名会不同):
(chatting) mark@mark-OptiPlex-9020:~$
在 Windows 中设置虚拟环境中的 Spyder
现在我们需要在新的虚拟环境中安装 Spyder。首先确保你已激活虚拟环境。然后运行以下命令:
**conda install spyder**
然后,在同一个已激活虚拟环境的终端中执行以下命令以启动 Spyder:
**spyder**
总结
在本章中,你学习了四种变量类型以及如何将一种类型转换为另一种类型。你还学习了 Python 中函数的工作原理。你学习了三种将模块导入脚本的方法,并了解了每种方法的优缺点。
你还创建了自己的函数。你创建了一个本地模块并将其导入到脚本中,以使代码更加简洁清晰。最后,你创建并激活了虚拟环境,以便在不同的项目中分离包。
在第三章中,你将学习如何安装与语音识别相关的模块,使 Python 能够理解人类的声音。
本章练习
-
假设:
name1 = 'Kentucky ' name2 = "Wildcats"以下每个 Python 语句的输出是什么?首先写下答案,然后在 Spyder 中运行该命令以验证。
print(type(name1)) print(type(name2)) print(name1 + name2) print(name2 + name1) print(name2 + ' @ ' + name1) print(3 * name2) -
假设:
x = 3.458 y = -2.35以下每个 Python 语句的结果是什么?
print(type(x)) print(type(y)) print(round(x, 2)) print(round(y, 1)) print(round(x, 0)) -
以下是一些整数的例子:
a = 57 b = -3 c = 0以下每条 Python 语句的结果是什么?
print(type(b)) print(str(a)) print(float(c)) -
以下每条 Python 语句的输出是什么?
print(type(5==9)) print('8<7') print(5==9) print(type('8<7')) print(type('True')) -
以下每条 Python 语句的输出是什么?
print(int(-23.0)) print(int("56")) print(str(-23.0)) print(float(8)) -
以下每条 Python 语句的输出是什么?
print(int(True)) print(float(False)) print(str(False)) -
以下每条 Python 语句的输出是什么?
print(bool(0)) print(bool(-23)) print(bool(17.6)) print(bool('Python')) -
以下变量名是否有效,为什么?
global 2pirnt print2 _squ list -
在以下脚本中使用了循环命令
break。输出应该是什么?首先写下答案,然后在 Spyder 中运行命令以验证。for letter in ("A", "B", "C"): if letter == "B": break for num in (1, 2): print(f"this is {letter}{num}") -
在以下脚本中使用了循环命令
continue。输出应该是什么?首先写下答案,然后在 Spyder 中运行命令以验证。for letter in ("A", "B", "C"): if letter == "B": continue for num in (1, 2): print(f"this is {letter}{num}") -
在以下脚本中使用了循环命令
pass。输出应该是什么?首先写下答案,然后在 Spyder 中运行命令以验证。for letter in ("A", "B", "C"): if letter == "B": pass for num in (1, 2): print(f"this is {letter}{num}") -
以下每条命令的输出是什么?首先写下答案,然后在 Spyder 中运行命令以验证。
for i in range(5): print(i)for i in range(10, 15): print(i)for i in range(10, 15, 2): print(i) -
根据本章中定义的函数,
team_sales(50, 100, 120)的值是多少? -
将脚本import_local_module.py中的模块导入方法从
frommoduleimportfunction方法改为importmodule方法。将新脚本命名为import_local_module1.py,并确保它产生相同的输出。 -
班级中八个小组的期中项目成绩保存在列表
midterm = [95, 78, 77, 86, 90, 88, 81, 66]中。使用 Python 内置函数计算成绩的范围和平均值。 -
假设
inp = "University of Kentucky",请确定inp[5:10]、inp[-1]、inp[:10]和inp[5:]的值。 -
如果
email = John.Smith@uky.edu,email.find("y")的结果是什么? -
假设
llst = [[1,2,3,5],[2,2,6,8],[2,3,5,9],[3,5,4,7],[1,3,5,0]]。llst[2]、llst[2][2]和llst[3][0]的值分别是多少? -
以下每条 Python 语句的输出是什么?
[1, "a", "hello", 2].remove(1) [1, "a", "hello", 2].append("hi") -
假设
scores2 = {'blue':[5, 5, 10], 'white':[5, 7, 12]}。scores2['blue'][2]的值是多少? -
这是一个元组的例子:
tpl = (1, 2, 3, 9, 0)。tpl[3:4]的值是多少? -
你有一个列表
lst = [1, "a", "hello", 2]。创建一个字典,包含四个键值对:键是lst中每个元素的位置,值是该位置上的元素。
第二部分:学习说话
语音识别

在本章中,我们将通过语音与 Python 互动。我们将首先安装SpeechRecognition模块;安装过程可能会让人感到沮丧,因此需要特别注意。然后,你将创建一个脚本,让 Python 识别你的语音并打印出来,确保语音识别功能在你的计算机上能够顺利运行。
你将使用语音控制完成几个任务,包括语音听写、打开网页浏览器、打开文件和播放计算机上的音乐。你会把所有与语音识别相关的代码放入一个自定义本地模块中,这样最终的脚本简洁易读。
在开始之前,为本章创建文件夹/mpt/ch03/。本章所有脚本都可以在本书的资源页面找到,www.nostarch.com/make-python-talk/。
安装 SpeechRecognition 模块
安装SpeechRecognition模块可能会有些棘手,甚至让人感到沮丧。别慌,我们会讨论如何在 Windows、Mac 和 Linux 上安装它。安装SpeechRecognition模块比大多数模块多了一步,因为它依赖于Pyaudio模块,我们需要手动安装它。Pyaudio模块提供了跨平台音频输入/输出库Portaudio的绑定。
你也不能在 Anaconda 提示符中使用pip install安装Pyaudio模块。相反,你需要使用conda install来安装它。
在 Windows 中
首先,你需要从第二章激活虚拟环境chatting。去你的 Anaconda 提示符,输入以下命令:
**conda activate chatting**
你应该看到一个修改过的提示符:
(chatting) c:\>
请注意,提示符中的(chatting)表示你现在处于虚拟环境chatting中。如果命令没有成功,返回到第二章,查看如何创建和激活虚拟环境的完整说明。
接下来,在 Anaconda 提示符中输入以下命令:
(chatting) c:\> **pip install SpeechRecognition**
如果你尝试导入并运行脚本,Spyder 会告诉你需要Pyaudio模块才能使SpeechRecognition模块正常运行。
在虚拟环境chatting激活的情况下,在你的 Anaconda 提示符中运行以下命令:
(chatting) c:\> **conda install pyaudio**
请按照说明完成所有步骤。
在 Mac 或 Linux 中
首先,激活虚拟环境chatting。打开终端,输入并执行以下命令:
**conda activate chatting**
接下来,在终端中执行以下命令:
**pip install SpeechRecognition**
如果你现在尝试导入SpeechRecognition并运行脚本,Spyder 会告诉你需要Pyaudio才能让SpeechRecognition正常工作。虚拟环境chatting激活后,在终端中运行以下命令:
**conda install pyaudio**
请按照说明完成所有步骤。
测试并微调 SpeechRecognition
接下来,我们将测试并微调SpeechRecognition模块,以便 Python 能够接受你的语音命令。
导入 SpeechRecognition
要在你的 Python 脚本中导入SpeechRecognition,请使用以下命令:
**import speech_recognition**
请注意,在安装模块和导入模块时,模块名称略有不同:一个是SpeechRecognition,另一个是speech_recognition。确保在导入时不要漏掉模块名中的下划线。
如果你使用的是台式电脑,记得要插入麦克风。大多数笔记本电脑自带内置麦克风,但有时候使用外接麦克风会更方便,能让你靠近麦克风说话,避免环境噪音的干扰。
测试 SpeechRecognition
接下来,我们来测试硬件和软件。将 Listing 3-1 输入到你的 Spyder 编辑器中,并将其保存为sr.py,或者你也可以从书籍的资源中下载该文件。
import speech_recognition as sr
speech = sr.Recognizer()
print('Python is listening...')
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
audio = speech.listen(source)
inp = speech.recognize_google(audio)
print(f'You just said {inp}.')
Listing 3-1:测试SpeechRecognition
我们导入SpeechRecognition模块。接下来,我们调用Recognizer()来从该模块创建一个Recognizer实例,这样脚本就准备好将语音转换为文本。我们将其保存为变量speech,并打印出一条消息,通知你麦克风已经准备好接收语音输入。
我们通过Microphone()告诉脚本音频来源来自麦克风。我们使用adjust_for_ambient_noise()方法来减少环境噪音对语音输入的影响。脚本从定义的麦克风捕获语音输入,将其转换为文本,并保存到inp变量中。我们打印出inp的值。
请注意,在这个脚本中,Recognizer实例使用recognize_google()来识别音频源中的语音。此方法使用 Google Web Speech 应用程序编程接口(API),需要良好的互联网连接。SpeechRecognition模块中的Recognizer实例还提供了其他方法,如recognize_bing()(使用微软 Bing 语音识别),recognize_ibm()(使用 IBM Speech to Text)等。唯一可以离线使用的方法是recognize_sphinx(),它使用 CMU Sphinx 的服务。然而,recognize_sphinx()的识别准确度远不如recognize_google(),因此我们在本书中将使用recognize_google()。
运行sr.py,并说一些简单的话,比如“Hello”或“How are you?”,来测试 Python 是否能正确打印出你的语音输入。如果你说了“How are you?”,应该会看到以下输出:
Python is listening...
You just said **how are you**.
如果脚本正常运行,说明你已经成功安装了语音识别功能。如果没有,请仔细检查前面的步骤,并确保你的麦克风正确连接。同时,确保你处在一个相对安静的地方,并且网络连接良好。
注意,Python 会将几乎所有的语音输入转换为小写文本,这是一个不错的功能,因为字符串变量是区分大小写的。这样,Python 就不会因为大小写问题而错过某个命令。
微调语音识别功能
现在你将微调语音识别代码,使其更适合本书的后续内容。我们将在一些常见错误上使用try和except,以便在遇到错误后,代码能够继续执行,而不是导致脚本崩溃。
常见错误UnknownValueError发生在 Google 语音识别服务器无法理解音频时,可能是因为语音不清晰或周围有噪音。RequestError错误发生在 Google 语音识别请求失败时,可能是由于网络连接差或服务器过于繁忙。WaitTimeoutError错误发生在脚本长时间未检测到麦克风的音频时。
如果不使用try和except,脚本会崩溃,你需要重新启动脚本。通过使用异常处理结构,脚本会继续运行而不会崩溃。我提到的这些错误并没有足够严重到需要处理,所以我们的脚本会允许这些错误通过。
列表 3-2,stand_by.py,使用了一个无限循环,首先进入待命状态,然后反复接收语音输入并打印出来。这样,我们就不必每次都重新运行脚本才能让脚本接收我们的语音输入。
import speech_recognition as sr
speech = sr.Recognizer()
while True:
print('Python is listening...')
inp = ""
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
1 try:
audio = speech.listen(source)
inp = speech.recognize_google(audio)
except sr.UnknownValueError:
pass
except sr.RequestError:
pass
except sr.WaitTimeoutError:
pass
2 print(f'You just said {inp}.')
if inp == "stop listening":
print('Goodbye!')
break
列表 3-2:stand_by.py 的代码
我们启动一个while循环,使脚本处于待命状态。这样,在获取你的语音输入后,脚本会打印出你说的话,并重新开始监听。每次迭代时,脚本都会打印Python is listening,让你知道它已经准备好。我们在每次迭代开始时将变量inp定义为空字符串。否则,如果用户一段时间内没有说话,脚本会使用上次迭代的inp值。通过清空字符串,我们避免了潜在的混淆。
我们在连接到 Google 语音识别服务器时使用异常处理。如果出现UnknownValueError、RequestError或WaitTimeoutError,我们让脚本继续运行而不会崩溃。
在每次迭代时,脚本会打印你说的话,以便你检查语音识别软件是否正确捕捉到你的声音。
最后,我们不希望脚本永远运行下去,所以我们添加了一个停止条件。当你说“停止监听”时,if分支会被激活,脚本会打印Goodbye!,然后while循环停止。
这是一个示例输出,我的语音输入以粗体显示:
Python is listening...
You just said **hello**.
Python is listening...
You just said **how are you**.
Python is listening...
You just said **today is a Saturday**.
Python is listening...
You just said **stop listening**.
Goodbye!
接下来,你将在多个项目中应用语音识别功能。它们中有些是实际且有用的,其他的则是为了为后续章节积累技能。
执行语音控制的网页搜索
我们的第一个项目是编写一个脚本,使用语音控制浏览网页。你将学习使用webbrowser模块在计算机上打开浏览器。然后,你将添加语音控制功能,打开浏览器并执行各种在线搜索。
使用 webbrowser 模块
webbrowser模块为你提供了使用计算机默认浏览器打开网站的工具。该模块属于 Python 标准库,因此无需安装。
要在你的计算机上测试webbrowser模块,请在 Spyder 编辑器中输入以下代码行并运行:
**import webbrowser**
**webbrowser.open("http://"+"wsj.com")**
我们在open()函数内部使用"http://"+,这样你只需要输入网站地址的主体部分,而不必输入完整的 URL。这是为了为下一部分的语音激活做准备。如果使用https://代替http://或在完整 URL 中包含www,网页浏览器会自动纠正 URL。
一个新的网页浏览器窗口应该会在华尔街日报网站上打开。Microsoft Edge 是我计算机上的默认浏览器,结果如图 3-1 所示。

图 3-1:使用webbrowser.open("http://"+"wsj.com")命令的结果
添加语音控制
现在我们将添加语音识别功能。将清单 3-3 保存为voice_browse.py。
import webbrowser
import speech_recognition as sr
speech = sr.Recognizer()
1 def voice_to_text():
voice_input = ""
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
voice_input = speech.recognize_google(audio)
except sr.UnknownValueError:
pass
except sr.RequestError:
pass
except sr.WaitTimeoutError:
pass
return voice_input
2 while True:
print('Python is listening...')
inp = voice_to_text()
print(f'You just said {inp}.')
if inp == "stop listening":
print('Goodbye!')
break
elif "browser" in inp:
inp = inp.replace('browser ','')
webbrowser.open("http://"+inp)
continue
清单 3-3:voice_browse.py代码
我们导入了该脚本所需的两个模块:webbrowser和SpeechRecognition。在 1 处,我们定义了voice_to_text()函数,该函数包含了stand_by.py中的大部分步骤:它以空字符串voice_input开始,将麦克风的音频转换为文本,并将其放入voice_input中。它还对UnknownValueError、RequestError和WaitTimeoutError进行异常处理。一旦调用,该函数将返回保存在voice_input中的值。
脚本启动一个无限循环,持续接收语音输入 2。在每次迭代时,它会打印Python is listening...,以便你知道它已准备好。
我们调用voice_to_text()来捕获你的语音输入,并将转换后的文本保存在inp中。请注意,我故意为局部变量voice_input和全局变量inp使用不同的变量名,以避免混淆。
如果你对麦克风说“Stop listening”,则if分支会被激活。脚本会打印Goodbye!并停止运行。如果语音命令中包含browser这个词,elif分支会被激活。脚本随后会把http://和你接下来所说的内容放到地址栏,并打开网页浏览器。例如,如果你说“browser abc.com”,replace()方法会将“browser”及其后面的空格替换为空字符串,这样就有效地将inp修改为abc.com。
这是一个示例输出,带有我用粗体显示的语音输入:
Python is listening...
You just said **browser cnn.com**.
Python is listening...
You just said **browser pbs.org**.
Python is listening...
You just said **stop listening**.
Goodbye!
相关的网页浏览器弹出窗口如图 3-2 所示。

图 3-2:voice_browse.py的一个示例输出
你使用browser而不是browse来确保脚本能够理解你的命令:如果你对着麦克风说“Browse”,Python 可能会将其转换为brows。你可能会遇到一些需要微调的情况。由于每个人的声音、麦克风和发音(口音、语调、重音等)不同,你的调整可能与我的不同。
执行 Google 搜索
接下来,我们将修改voice_browse.py,使你能够通过语音激活 Google 搜索。你只需要修改voice_browse.py中的这一行代码:
webbrowser.open("http://"+inp)
将其更改为:
webbrowser.open("http://**google.com/search?q=**"+inp)
然后将修改后的脚本保存为voice_search.py。(你也可以从本书的资源页面下载它。)
这里我们利用了 Google 搜索时会将搜索词附加在http://google.com/search?q=后面,并作为地址栏中的 URL。例如,当你在 Google 中搜索how many liters are in a gallon时,得到的结果与输入 URL http://google.com/search?q=how many liters are in a gallon相同。
在 Spyder 编辑器中运行voice_search.py。对着麦克风提问,比如“Browser yards in a mile”。脚本应打开默认浏览器,执行yards in a mile的 Google 搜索,并显示与图 3-3 类似的结果。

图 3-3:当你说“browser yards in a mile”时的结果
你还可以以任何使用 Google 的方式使用该脚本,例如作为语音控制的词典。如果你想知道单词diligence的准确定义,可以说:“Browser define diligence。”
打开文件
利用 Python 脚本中的语音识别功能,你可以通过语音控制完成许多操作。我们将编写一个脚本来打开各种类型的文件,包括文本文件、PDF 文件和音乐文件。
使用 os 和 pathlib 模块访问和打开文件
你可以使用os和pathlib模块访问计算机上的文件和文件夹。os模块用于访问操作系统的功能,比如进入文件夹、打开文件等等。然而,这些命令在不同操作系统上有所不同。例如,在 Windows 中打开文件的命令是explorer,在 Mac 中是open,在 Linux 中是xdg-open。
为了让你的脚本在跨平台上更具便携性,我们将使用platform模块,它允许脚本自动识别你的操作系统,并选择适合的命令。pathlib模块可以让你找到文件路径并指定文件或文件夹路径。幸运的是,pathlib是跨平台的,所以你不必担心正斜杠或反斜杠的问题。这三个模块——os、pathlib和platform——都在 Python 标准库中,因此无需额外安装。
在你的章节文件夹中,创建一个名为files的子文件夹,并将一个名为example.txt的文件保存在其中。然后,在 Spyder 编辑器中输入 Listing 3-4,并将其保存为os_platform.py。
import os
import pathlib
import platform
myfolder = pathlib.Path.cwd()
print(myfolder)
myfile = myfolder/'files'/'example.txt'
print(myfile)
if platform.system() == "Windows":
os.system(f"explorer {myfile}")
elif platform.system() == "Darwin":
os.system(f"open {myfile}")
else:
os.system(f"xdg-open {myfile}")
Listing 3-4:os_platform.py的代码
我们导入模块后,使用pathlib中的Path.cwd()找到脚本的当前工作目录。我们将以此作为导航的起始路径。
然后,我们指定要打开的文件的路径和名称。在pathlib模块中,我们使用正斜杠表示子文件夹,无论你使用的是哪个操作系统。命令/'files'指示脚本进入子文件夹files,/'example.txt'则表示将example.txt文件定义为myfile。
os模块中的system()方法在子 shell 中执行命令。explorer命令会在 Windows 中打开一个文件夹或文件。然而,如果你使用的是 Mac,os模块中的system()方法会使用open命令,在 Linux 中,命令则是xdg-open。因此,脚本会在子文件夹files中打开文件example.txt。
例如,假设你使用的是 Windows,并将脚本保存在章节文件夹C:\chat\mpt\ch03中。运行脚本后,你将在 IPython 控制台中看到以下输出:
C:\chat\mpt\ch03
C:\chat\mpt\ch03\files\example.txt
同时,文件example.txt应该被打开。
通过语音控制打开文件
现在,我们将演示如何打开各种文件类型,如 MP3、Microsoft Word、PowerPoint 和 Excel 文件,以及 PDF 文件。在运行以下脚本之前,请在你刚刚创建的章节文件夹中的files子文件夹中保存一个 MP3 文件、一个 Word 文件、一个 PowerPoint 文件、一个 Excel 文件和一个 PDF 文件。将这五个文件分别命名为presentation.mp3、lessons.docx、graduation.pptx、book.xlsx和desk.pdf。文件最好不要太大。
列表 3-5 显示了voice_open_file.py,该脚本也可以从书本的资源页面下载。
import os
import pathlib
import platform
import speech_recognition as sr
speech = sr.Recognizer()
directory = pathlib.Path.cwd()
1 def voice_to_text():
voice_input = ""
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
voice_input = speech.recognize_google(audio)
except sr.UnknownValueError:
pass
except sr.RequestError:
pass
except sr.WaitTimeoutError:
pass
return voice_input
def open_file(filename):
if platform.system() == "Windows":
os.system(f"explorer {directory}\\files\\{filename}")
elif platform.system() == "Darwin":
os.system(f"open {directory}/files/{filename}")
else:
os.system(f"xdg-open {directory}/files/{filename}")
2 while True:
print('Python is listening...')
inp = voice_to_text().lower()
print(f'You just said {inp}.')
if inp == "stop listening":
print('Goodbye!')
break
elif "open pdf" in inp:
inp = inp.replace('open pdf ','')
myfile = f'{inp}.pdf'
open_file(myfile)
continue
elif "open word" in inp:
inp = inp.replace('open word ','')
myfile = f'{inp}.docx'
open_file(myfile)
continue
elif "open excel" in inp:
inp = inp.replace('open excel ','')
myfile = f'{inp}.xlsx'
open_file(myfile)
continue
elif "open powerpoint" in inp:
inp = inp.replace('open powerpoint ','')
myfile = f'{inp}.pptx'
open_file(myfile)
continue
elif "open audio" in inp:
inp = inp.replace('open audio ','')
myfile = f'{inp}.mp3'
open_file(myfile)
continue
列表 3-5:voice_open_file.py的代码
和voice_browse.py一样,我们定义了voice_to_text()来将你的语音命令转换为文本。我们还定义了open_file()来识别你的操作系统并使用正确的命令,explorer、open或xdg-open,在你的计算机上打开文件。请注意,虽然 Windows 操作系统使用反斜杠(\)进入子文件夹,但 Mac 和 Linux 使用正斜杠(/)来实现这一目的。
然后,脚本通过使用while循环进入待机模式。在循环内,麦克风首先检测到你的声音并将其转换为文本。由于我们在voice_to_text()后面使用了lower()方法,变量inp中的所有字母都会变为小写,以避免因大小写不匹配而出错。
如果你说“停止监听”,脚本会打印Goodbye!并停止运行。如果语音命令中包含open pdf,第一个elif分支将被激活。然后,脚本将用空字符串替换open pdf,这样inp中就只剩下文件名。脚本会进入子文件夹并打开正确的 PDF 文件。例如,当你说“打开 PDF 桌面”时,文件desk.pdf将在你的计算机上打开。
当你说“打开 Word 课件”时,第二个elif分支会被激活。对于 Excel 文件和 PowerPoint 文件,同样的原理适用。当你说“打开音频演示”时,音频文件presentation.mp3会在你的计算机上播放,使用默认的 MP3 播放器。
这是我与系统交互的输出:
Python is listening...
You just said **open pdf desk**.
Python is listening...
You just said **open word lessons**.
Python is listening...
You just said
Python is listening...
You just said **open excel book**.
Python is listening...
You just said **open powerpoint graduation**.
Python is listening...
You just said **open audio presentation**.
Python is listening...
You just said **stop listening**.
Goodbye!
创建并导入本地模块
如你所见,三个脚本voice_browse.py、voice_search.py和voice_open_file.py共享大量相同的代码:导入语音识别模块并定义voice_to_text()函数的代码。
为了提高脚本效率,我们将所有与语音识别相关的命令行放入本地模块中。然后,我们可以在任何使用语音识别功能的脚本中导入这个模块。
创建本地模块 mysr
在 Spyder 编辑器中输入列表 3-6,并将其保存为mysr.py。或者,你可以从本书的资源页面下载它。
# Get rid of ALSA lib error messages in Linux
1 import platform
import speech_recognition as sr
if platform.system() == "Linux":
from ctypes import CFUNCTYPE, c_char_p, c_int, cdll
# Define error handler
error_handler = CFUNCTYPE\
(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
# Don't do anything if there is an error message
2 def py_error_handler(filename, line, function, err, fmt):
pass
# Pass to C
c_error_handler = error_handler(py_error_handler)
asound = cdll.LoadLibrary('libasound.so')
asound.snd_lib_error_set_handler(c_error_handler)
# Now define the voice_to_text() function for all platforms
3 import speech_recognition as sr
def voice_to_text():
voice_input = ""
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
voice_input = speech.recognize_google(audio)
except sr.UnknownValueError:
pass
except sr.RequestError:
pass
except sr.WaitTimeoutError:
pass
return voice_input
列表 3-6:自制模块mysr的代码
如果你不使用 Linux,可以忽略代码 1 的第一部分。高级 Linux 声音架构 (ALSA) 配置使用 C 编程语言编写,每次导入pyaudio模块时,都会输出如下警告信息:
ALSA lib pcm.c:2212:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2212:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2212:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib audio/pcm_bluetooth.c:1613:(audioservice_expect)
BT_GET_CAPABILITIES failed : Input/output error(5)
ALSA lib audio/pcm_bluetooth.c:1613:(audioservice_expect)
BT_GET_CAPABILITIES failed : Input/output error(5)
ALSA lib audio/pcm_bluetooth.c:1613:(audioservice_expect)
BT_GET_CAPABILITIES failed : Input/output error(5)
ALSA lib audio/pcm_bluetooth.c:1613:(audioservice_expect)
BT_GET_CAPABILITIES failed : Input/output error(5)
ALSA lib pcm_dmix.c:957:(snd_pcm_dmix_open)
The dmix plugin supports only playback stream
ALSA lib pcm_dmix.c:1018:(snd_pcm_dmix_open) unable to open slave
我们在 Python 2 中创建了一个错误处理程序,并将其传递给 C,以便在导入pyaudio时不会显示任何错误信息。具体细节超出了本书的范围,所以如果你不理解这一部分也没关系。只需将错误处理程序保留在模块mysr中,它不会影响你对本书其他部分的理解。
从第 3 行开始,我们导入了SpeechRecognition模块,初始化Recognizer()类,并定义了voice_to_text()函数。
请注意,如果你运行mysr.py,什么也不会发生。这是因为我们只在这个脚本中定义了voice_to_text(),但没有调用它。
导入 mysr
让我们重新审视stand_by.py并修改它以使用mysr。将列表 3-7 保存为stand_by1.py。
# Make sure you put mysr.py in the same folder as this script
from mysr import voice_to_text
while True:
print('Python is listening...')
1 inp = voice_to_text()
print(f'You just said {inp}.')
if inp == "stop listening":
print('Goodbye!')
break
列表 3-7:stand_by1.py的代码
我们用一行代码替代了所有与语音识别相关的代码:from mysr import voice_to_text。这行代码告诉脚本去本地模块mysr导入voice_to_text(),并在当前脚本中使用。
每当你需要将语音转换为文本时,只需调用voice_to_text() 1。
小结
在本章中,我们安装了SpeechRecognition模块,并使用try和except来处理潜在的错误。通过这种方式,我们防止脚本关闭,而是让它继续运行。我们使用几个项目测试了语音控制功能:语音控制网页浏览和语音控制网页搜索。
你学习了如何使用os模块打开文件,如何使用pathlib模块在文件路径中导航,以及如何使用platform模块使你的 Python 代码跨平台。
最后,你将所有与语音识别相关的代码放入一个自制的本地模块中,这样你的脚本看起来简洁、简短且清晰。我们将在本书的剩余部分使用这个模块。
本章结束练习
-
修改stand_by.py,使得你可以通过说“Quit the script”来结束
while循环,而不是说“Stop listening”。当while循环结束时,脚本会打印Have a great day! -
修改voice_open_file.py,使得当你说“Open text filename”时,filename.txt文件将在你的电脑上打开。
-
修改voice_open_file.py,使其从本地mysr模块导入
voice_to_text()。
让 Python 发声

在本章中,你将学习如何让 Python 用人类的声音回应你。你将首先根据操作系统安装文本转语音模块,然后教 Python 大声朗读你在电脑上输入的内容。你还将添加你在第三章中学到的语音识别功能,让 Python 重复你自己的话。最后,你将构建一个实际的应用程序,使用语音输入让 Python 计算矩形的面积,并用人类的声音告诉你答案。
为了节省空间,你将所有与文本转语音相关的代码放入自制模块中。完成后,你可以将该模块导入到任何需要文本转语音功能的脚本中。
你还将学习如何让 Python 大声朗读一篇长的文本文件,例如新闻文章。在开始之前,为本章设置文件夹/mpt/ch04/。像之前的章节一样,你可以从www.nostarch.com/make-python-talk/下载所有脚本的代码。
安装文本转语音模块
Python 有两个常用的文本转语音模块:pyttsx3和gTTS。如果你使用 Windows,你将安装pyttsx3并在本书中始终使用它。在 Windows 操作系统中,pyttsx3模块是离线工作的,语音听起来像人类,并且可以调整语音属性——即语音输出的速度、音量和性别。
然而,pyttsx3模块在 Mac 和 Linux 上的工作方式不同。语音听起来很机械,而且语音属性不容易调整。因此,如果你使用 Mac 或 Linux,需安装gTTS。gTTS模块需要互联网连接,因为它使用了 Google 翻译的文本转语音 API。此外,gTTS不会直接播放声音,而是将语音保存为音频文件或类似文件的对象。你需要使用自己的音频播放器来播放语音。gTTS生成的语音非常像人类的声音。
在第二章中,你创建了一个名为chatting的虚拟环境,并在第三章中使用它进行语音识别。你将在同一个虚拟环境中安装pyttsx3或gTTS模块,这样你的脚本就既能进行语音识别,又能实现文本转语音功能。
设置
如果你使用的是 Windows,请前往“在 Windows 中安装pyttsx3”部分,并跳过“在 Mac 或 Linux 中安装gTTS”部分。否则,请跳过“在 Windows 中安装pyttsx3”部分,转到“在 Mac 或 Linux 中安装gTTS”部分。
在 Windows 中安装 pyttsx3
pyttsx3模块不在 Python 标准库中,因此你需要通过 pip 安装它。
如果你还没有设置聊天虚拟环境,请返回第二章并按照指示进行设置。然后在 Anaconda 提示符中通过执行以下命令激活聊天虚拟环境:
conda activate chatting
激活你的聊天虚拟环境后,输入以下命令:
pip install pyttsx3
按照屏幕上的指示完成安装。
在 Mac 或 Linux 中安装 gTTS
gTTS模块不在 Python 标准库中,因此你需要通过 pip 安装它。
如果你还没有设置聊天虚拟环境,请返回第二章并按照指示进行设置。然后在终端中通过执行以下命令激活聊天虚拟环境:
conda activate chatting
激活你的聊天虚拟环境并在终端中输入以下命令:
pip install gTTs
按照屏幕上的指示完成安装。
测试你的文本转语音模块
在开始之前,你需要检查你的文本转语音模块是否正确安装并正常工作。根据你的操作系统,跳过不适用的部分。
在 Windows 中运行示例脚本
激活虚拟环境并打开 Spyder 后,将脚本test_pyttsx3.py复制到编辑器中,并将其保存在你的章节文件夹中。如果你愿意,也可以通过www.nostarch.com/make-python-talk/从书籍资源下载该文件。
import pyttsx3
engine = pyttsx3.init()
engine.say("hello, how are you?")
engine.runAndWait()
首先,将pyttsx3模块导入脚本。然后使用init()启动一个文本转语音引擎,并将其命名为engine。pyttsx3模块中的say()函数将文本转换为语音信号,并准备将其发送到扬声器。接下来,runAndWait()函数将实际的语音信号发送到扬声器,这样你就能听到声音。runAndWait()函数还会保持引擎运行,以便在脚本的后续部分需要将文本转换为语音时,无需重新启动引擎。
为了理解每一行代码的功能,使用 F9 键逐行运行test_pyttsx3.py。
如果模块正确安装,运行完整个脚本后,你应该听到一个声音说:“Hello, how are you?”如果没有,请重新检查指示并确保电脑扬声器工作正常且音量合适。本章稍后将讨论如何自定义与pyttsx3模块相关的语速、音量和语音性别。
在 Mac 或 Linux 中运行示例脚本
你将使用 gtts-cli 工具(cli代表命令行)将文本转换为语音,而不是先将文本转换为音频文件再播放。gtts-cli 工具比另一种方法更快。一旦安装了gTTS模块,gtts-cli 工具将在你的虚拟环境中的命令行中可用。gtts-cli 工具将文本转换为类似文件的对象,你需要选择一个音频播放器来播放它。我发现 mpg123 播放器效果很好。
首先,你需要在电脑上安装 mpg123 播放器。如果你使用的是 Mac,请在终端运行以下命令:
brew install mpg123
如果你使用的是 Linux,请在终端运行以下两个命令:
sudo apt-get update
sudo apt-get install mpg123
完成后,在虚拟环境激活的情况下,在终端运行以下命令:
gtts-cli --nocheck "hello, how are you?" | mpg123 -q -
如果你已经正确安装了一切,你应该能听到一个声音说:“你好,你好吗?”如果没有,重新检查指令,确保你电脑上的扬声器正常工作并且音量合适。此外,既然你已经在虚拟环境中安装了gTTS模块,你需要在激活虚拟环境的情况下运行上述命令。否则,它将无法正常工作。
此命令中的nocheck选项是为了加速执行。q标志指示该模块即使在交互模式下也不显示版权和版本信息。确保你没有遗漏命令末尾的连字符。
接下来,你将使用 Python 中的os模块在子 shell 中执行命令。
将test_gtts.py脚本复制到你的 Spyder 编辑器中,并保存在章节文件夹里。该脚本也可以通过www.nostarch.com/make-python-talk/在本书的资源中获取。
import os
os.system('gtts-cli --nocheck "hello, how are you?" | mpg123 -q -')
首先将os模块导入到脚本中。然后使用system()在子 shell 中执行命令,以达到与在终端中运行命令相同的效果。因此,gtts-cli 工具被用来将文本转换为类似文件的对象。之后,mpg123 播放器播放该声音对象,这样你就可以听到人声。
如果你做对了,你应该能听到一个声音说:“你好,你好吗?”
在 Windows 中将文本转换为语音
现在让我们练习将输入的文本转换成 Windows 中的人声。在你的虚拟环境已激活并且 Spyder 已打开的情况下,将 Listing 4-1 中显示的tts_windows.py脚本复制到编辑器中,保存并运行它。
import pyttsx3
engine = pyttsx3.init()
1 while True:
inp = input("What do you want to covert to speech?\n")
if inp == "done":
print(f"You just typed in {inp}; goodbye!")
engine.say(f"You just typed in {inp}; goodbye!")
engine.runAndWait()
break
2 else:
print(f"You just typed in {inp}")
engine.say(f"You just typed in {inp}")
engine.runAndWait()
continue
Listing 4-1:在 Windows 中将文本转换为语音
在导入pyttsx3模块并初始化文本转语音引擎后,开始一个无限循环来接受用户的文本输入。在每次迭代中,脚本会在 IPython 控制台中请求文本输入。如果你想停止脚本,请输入done,然后脚本会用人声打印并说:“你刚刚输入了 done;再见!”之后,循环停止,脚本退出运行。
如果文本输入不是done,else分支会执行 2,脚本会用人类的声音大声说出你的文本输入。之后,脚本会进入下一次迭代,再次接受你的文本输入。
以下是脚本的示例输出(用户输入为粗体):
What do you want to covert to speech?
**Python is great!**
You just typed in Python is great!
What do you want to covert to speech?
**Hello, world!**
You just typed in Hello, world!
What do you want to covert to speech?
**done**
You just typed in done; goodbye!
在 Mac 或 Linux 上将文本转换为语音
现在我们将练习将书面文本输入转换为人类声音,在 Mac 或 Linux 上。激活你的虚拟环境并打开 Spyder 后,将脚本tts_mac_linux.py(清单 4-2)复制到编辑器中,然后保存并运行。
import os
while True: 1
inp = input("What do you want to covert to speech?\n")
if inp == "done":
print(f"You just typed in {inp}; goodbye!")
os.system(f'gtts-cli --nocheck "You just typed in {inp}; goodbye!" | mpg123 -q -')
break
else: 2
print(f"You just typed in {inp}")
os.system(f'gtts-cli --nocheck "You just typed in {inp}" | mpg123 -q -')
continue
清单 4-2:在 Mac 和 Linux 上将文本转换为语音
在导入os模块后,你可以在子 shell 中运行命令,开始一个无限循环来接收用户的文本输入 1。在每次迭代中,脚本会在 IPython 控制台上请求文本输入。如果你想停止脚本,输入done,脚本会打印并用人类的声音说:“你刚输入了 done;再见!”之后,循环停止,脚本退出运行。
如果文本输入不是done,else分支会执行 2,脚本会用人类的声音大声说出你的文本输入。之后,脚本会进入下一次迭代,再次接受你的文本输入。
以下是脚本的示例输出(用户输入为粗体):
What do you want to covert to speech?
**Python is great!**
You just typed in Python is great!
What do you want to covert to speech?
**Hello, world!**
You just typed in Hello, world!
What do you want to covert to speech?
**done**
You just typed in done; goodbye!
跟我念
我们将从一个简单的脚本开始,它可以听到你大声说的话,并用人类的声音重复出来。这个脚本有两个目的。首先,你将学习脚本如何接收你的语音输入,以及哪些词语是脚本最容易理解的——一些不常见的词语是无法理解的。其次,你将学会如何将语音识别和文本转语音功能放在同一个脚本中,这样你就可以仅通过人类的声音与计算机进行交流。
我们还将使脚本具有跨平台可移植性。如果你使用的是 Windows,脚本会自动选择pyttsx3模块;如果不是,则选择gTTS模块。
开始一个新的脚本,命名为repeat_me.py,并输入清单 4-3 中的代码。确保将其保存在你的章节文件夹中。你还需要将第三章中的mysr.py文件复制到同一文件夹中,因为你将需要从该脚本中调用voice_to_text()。
# Make sure you put mysr.py in the same folder as this script
from mysr import voice_to_text
import platform 1
if platform.system() == "Windows":
import pyttsx3
engine = pyttsx3.init()
else:
import os
while True:
print('Python is listening...')
inp = voice_to_text() 2
if inp == "stop listening":
print(f'You just said {inp}; goodbye!')
if platform.system() == "Windows":
engine.say(f'You just said {inp}; goodbye!')
engine.runAndWait()
else:
os.system(f'gtts-cli --nocheck "You just said {inp}; goodbye!" | mpg123 -q -')
break
else:
print(f'You just said {inp}')
if platform.system() == "Windows": 3
engine.say(f'You just said {inp}')
engine.runAndWait()
else:
os.system(f'gtts-cli --nocheck "You just said {inp}" | mpg123 -q -')
continue
清单 4-3:大声重复
首先,从mysr模块导入voice_to_text()函数,将语音命令转换为字符串变量。然后,导入platform模块,让脚本自动识别你的操作系统,并为你选择合适的命令 1。如果你使用的是 Windows,脚本会导入pyttsx3模块并启动文本转语音引擎。否则,脚本会导入os模块,以便你在子 shell 中使用 gtts-cli 工具。
然后,你开始一个无限循环以接收语音输入。脚本将你的语音命令转换为一个名为inp 2 的字符串变量。如果你对着麦克风说“Stop listening”,脚本将大声说,“你刚才说的是停止监听;再见!”然后脚本停止。该脚本根据你的操作系统使用pyttsx3模块或 gtts-cli 工具。
如果你对着麦克风说任何其他话,循环将继续运行。在每次迭代中,脚本将重复你所说的话 3。
以下是我依次对着麦克风说“Hello”、“How are you”和“Stop listening”后脚本的输出:
Python is listening...
You just said **hello**
Python is listening...
You just said **how are you**
Python is listening...
You just said **stop listening**; goodbye!
自定义语音
在本节中,你将学习如何自定义你的文本到语音模块所产生的语音。你可以调整语速、音量以及在 Windows 中pyttsx3模块的语音身份。如果你使用的是 Mac 或 Linux,唯一可以自定义的就是pyttsx3模块中语音的语速。
跳过任何不适用于你操作系统的以下小节。
在 Windows 中检索pyttsx3模块的默认设置
首先,你需要查看pyttsx3模块中语速、音量和语音身份的默认参数值。
这个脚本将检索你的语音模块的默认设置。在 Spyder 中,输入 Listing 4-4 中的代码并将其保存为pyttsx3_property.py,并保存在章节文件夹中。
import pyttsx3
engine = pyttsx3.init()
1 voices = engine.getProperty('voices')
for voice in voices:
print(voice)
2 rate = engine.getProperty("rate")
print("the default speed of the speech is", rate)
vol = engine.getProperty("volume")
print("the default volume of the speech is", vol)
Listing 4-4:检索默认设置
在第 1 步,你使用getProperty()获取引擎中使用的语音的属性。然后,你遍历voices列表中的所有语音对象,并打印出每个语音对象。
你使用getProperty() 2 获取语速的属性并打印出默认语速,然后对默认音量做同样的操作。
如果你在 Windows 中运行这个脚本,你将看到类似以下的语音脚本默认设置输出:
<Voice id=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\TTS_MS_EN-US_DAVID_11.0
name=Microsoft David Desktop - English (United States)
languages=[]
gender=None
age=None>
<Voice id=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\TTS_MS_EN-US_ZIRA_11.0
name=Microsoft Zira Desktop - English (United States)
languages=[]
gender=None
age=None>
the default speed of the speech is 200
the default volume of the speech is 1.0
在这里,你可以看到pyttsx3模块中提供的两种语音。第一种语音,名为David,具有男性音调;第二种语音,名为Zira,具有女性音调。默认的语音是 David——因此你在test_pyttsx3.py中听到的是男性声音。
默认的语速为每分钟 200 个单词。默认的音量设置为 1。接下来你将学习如何在 Windows 中调整pyttsx3模块的语速、音量和身份。
在 Windows 中调整pyttsx3模块的语音属性
这个脚本将改变默认设置,以便你可以听到具有你偏好的语速、音量和身份的语音。将 Listing 4-5 保存为pyttsx3_adjust.py。
import pyttsx3
engine = pyttsx3.init()
voice_id = 1
1 voices = engine.getProperty('voices')
engine.setProperty('voice', voices[voice_id].id)
engine.setProperty('rate', 150)
engine.setProperty('volume', 1.2)
engine.say("This is a test of my speech id, speed, and volume.")
engine.runAndWait()
Listing 4-5:调整一些设置
选择第二个语音 ID,它有一个女性声音。在 1 的位置,脚本会获取文本到语音引擎中可用的语音对象,并将它们保存在名为voices的列表中。通过提供索引[1],选择列表voices中的第二个对象,它有一个女性的声音。setProperty()函数需要两个参数:要设置的属性和要设置的值。将值设置为voices[voice_id].id,以选择 Windows 中女性语音对象的id值,即HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices\Tokens\TTS_MS_EN-US_ZIRA_11.0。如果你想切换到 Windows 中的男性语音,可以使用voices[0].id。
接下来,你将语音速度设置为每分钟 150 个单词。我们大多数人在日常对话中的语速约为每分钟 125 个单词。对于更快的语速,将rate设置为大于 125 的数值;对于更慢的语速,将其设置为小于 125 的数值。
然后,音量设置为 1.2,比分贝默认值 1 更大。你可以根据自己的喜好和扬声器,将其设置为大于或小于 1 的值。
最后,脚本使用调整后的属性将'say()'中的文本转换为语音。尝试多次运行这个脚本,使用不同的参数组合,直到找到最适合你的设置。你可以随时返回此脚本并进行调整。
在 Mac 或 Linux 中自定义 gTTS 模块
你可以根据gTTS文档自定义语音的速度,但不能自定义音量或 ID;例如,参见buildmedia.readthedocs.org/media/pdf/gtts/latest/gtts.pdf。然而,gTTS可以将文本转换为大多数世界主要语言的语音,包括西班牙语、法语、德语等,而pyttsx3模块无法做到这一点。你将在第十六章中使用gTTS的这个功能来构建语音翻译器。
这个脚本会将默认的语速改为慢速模式,适用于gTTS模块。在 Spyder 中,输入以下代码并将其保存为* gtts_slow.py*,放入章节文件夹中:
import os
os.system('gtts-cli --nocheck --slow "hello, how are you?" | mpg123 -q -')
这个脚本与之前创建的test_gtts.py脚本相同,不同之处在于它添加了--slow选项。这会使语音输出比正常速度更慢。
如果你在 Mac 或 Linux 中运行这个脚本,你会听到计算机缓慢地说:“你好,你好吗?”
由于默认的速度设置是slow=False,而这是我们更倾向的设置,因此我们不会自定义gTTS模块。
构建本地 mysay 模块
在第三章中,你将所有与语音识别相关的命令放入一个名为mysr的本地模块。在这里你也会做同样的事,将所有与文本到语音相关的命令放入一个本地模块。
创建 mysay
你将创建一个本地模块mysay,并将其保存在与使用文本转语音功能的脚本相同的文件夹中。这样,你可以节省主脚本的空间。该模块已调整了pyttsx3_adjust.py中设置的语音速度、音量和性别属性(如果你使用的是 Windows)。如果你使用的是 Mac 或 Linux,本地模块mysay将使用gTTS模块中的默认属性。你可以根据自己的偏好修改这些参数。
输入列表 4-6 中的代码,并将其保存为mysay.py,放入你的章节文件夹中。
# Import the platform module to identify your OS
import platform
# If you are using Windows, use pyttsx3 for text to speech
1 if platform.system() == "Windows":
import pyttsx3
2 try:
engine = pyttsx3.init()
except ImportError:
pass
except RuntimeError:
pass
voices = engine.getProperty('voices')
engine.setProperty('voice', voices[1].id)
engine.setProperty('rate', 150)
engine.setProperty('volume', 1.2)
def print_say(txt):
print(txt)
engine.say(txt)
engine.runAndWait()
# If you are using Mac or Linux, use gtts for text to speech
3 if platform.system() == "Darwin" or platform.system() == "Linux":
import os
def print_say(texts):
print(texts)
texts = texts.replace('"','')
texts = texts.replace("'","")
os.system(f'gtts-cli --nocheck "{texts}" | mpg123 -q -')
列表 4-6:构建模块
你首先导入平台模块来识别你的操作系统。如果你使用的是 Windows 1,pyttsx3模块会被导入。你在初始化文本转语音引擎时使用了异常处理 2,以便如果遇到ImportError或RuntimeError,脚本会继续运行而不会崩溃。然后你定义了print_say(),该函数打印文本并将文本转换为语音。
如果你使用的是 Mac 或 Linux 3,os模块被导入以使用 gtts-cli 工具在子进程中运行命令。然后,你定义了一个不同的print_say()函数,该函数打印文本并将文本转为语音。
导入 mysay
准备好mysay后,你可以直接将该模块导入到你的脚本中,使用文本转语音功能。让我们重新审视脚本repeat_me.py并修改它,使用mysay模块。将以下内容保存为repeat_me1.py:
# Put mysr.py and mysay.py in the same folder as this script
from mysr import voice_to_text
from mysay import print_say
while True:
print('Python is listening...')
inp = voice_to_text()
if inp == "stop listening":
print_say(f'You just said {inp}; goodbye!')
break
else:
print_say(f'You just said {inp}')
continue
你首先从mysay导入print_say()。你还从第三章创建的mysr模块导入voice_to_text()。你使用voice_to_text()将语音命令转换为变量inp。当你想要将文本转换为语音时,你使用print_say()。
运行脚本并对着麦克风说话进行测试。我依次对着脚本说了“Hello again”,“这个使用的是文本转语音模块”,以及“Stop listening”。以下是输出结果:
Python is listening...
You just said **hello again**
Python is listening...
You just said **this one is using a text-to-speech module**
Python is listening...
You just said **stop listening**; goodbye!
构建一个语音控制的计算器
你将使用文本转语音和语音解析技巧,构建一个可以通过语音命令控制的计算器。计算器会找到矩形的面积,并用人声告诉你面积。
这个脚本接受你输入的矩形宽度和长度,并语音播报其面积。将列表 4-7 保存为area_hs.py,放入你的章节文件夹中。
# Put mysr.py and mysay.py in the same folder as this script
from mysr import voice_to_text
from mysay import print_say
# Ask the length of the rectangle
1 print_say('What is the length of the rectangle?')
# Convert the voice input to a variable inp1
inp1 = voice_to_text()
print_say(f'You just said {inp1}.')
# Ask the width of the rectangle
print_say('What is the width of the rectangle?')
# Save the answer as inp2
inp2 = voice_to_text()
print_say(f'You just said {inp2}.')
# Calculate the area
2 area = float(inp1)*float(inp2)
# Print and speak the result
print_say(f'The area of the rectangle is {area}.')
列表 4-7:计算矩形的面积
你首先从本地模块导入文本转语音和语音识别函数。脚本会询问你矩形的长度 1。对着麦克风说出一个数字,脚本将你的语音输入转换为文本并保存为变量inp1。然后脚本会询问你矩形的宽度。当你说出答案时,脚本会将你的语音输入保存在变量inp2中。
根据你的输入,脚本通过将语音输入转换为浮动变量并进行相乘来计算矩形的面积。
脚本不仅会大声朗读结果,还会将互动内容打印到屏幕上。以下是与脚本的一个互动示例:
What is the length of the rectangle?
You just said **5.**
What is the width of the rectangle?
You just said **3.**
The area of the rectangle is 15.0.
当我告诉脚本矩形的长度为 5,宽度为 3 时,脚本告诉我面积是 15.0。
如果你说的不是数字,脚本将无法工作。为了避免脚本意外将你的响应转换为字符串而不是数字类型,最安全的做法是包含小数点(例如,“五点零”)。
大声朗读文件
在这一节中,你将学习如何将文件读取到脚本中,让 Python 能够大声朗读文本。
清单 4-8 包含了你将使用的简短新闻文章。
Storm Dorian likely to strengthen into hurricane
Thomson Reuters
BY BRENDAN O'BRIEN Aug 25th 2019 3:49PM
Tropical Storm Dorian was likely to strengthen into a hurricane during the next two days as it churned westward in the Caribbean Sea, putting Puerto Rico, the Lesser Antilles and the Virgin Islands on alert, forecasters said on Sunday.
The storm, 465 miles (750 km) east-southeast of Barbados, packed 40 mph winds as it headed west at 14 mph. It was forecast to be near the central Lesser Antilles late on Monday or early Tuesday, the National Hurricane Center (NHC) said in a midday advisory on Sunday.
"Right now, it's a tropical storm and we are expecting it to strengthen close to or reaching hurricane intensity as it approaches," NHC meteorologist Michael Brennan told Reuters.
Dorian was expected to turn toward the west-northwest on Monday and continue on that path through Tuesday night, the NHC said.
As of Sunday afternoon, Barbados was under a tropical storm warning while a tropical storm watch was in effect for St. Lucia and St. Vincent and the Grenadines.
The NHC was likely to issue additional watches for portions of the Windward and Leeward Islands on Sunday, Brennan said, noting that Puerto Rico, the Virgin Islands and Hispaniola should monitor Dorian's progress.
"We are approaching the peak of the hurricane season so everybody in the Caribbean and along the U.S. South, Gulf and East Coast needs to be aware and follow these systems," Brennan said. Dorian's winds could weaken as it passes south of Puerto Rico and approaches Hispaniola. Many Caribbean islands are likely to receive 2 to 4 inches (5 to 10 cm) of rain, but some part of the
Lesser Antilles islands could get 6 inches, the NHC said.
清单 4-8:文本文件内容
直接将这篇文章包含到脚本中显然不太方便,因此将其保存为名为storm.txt的文本文件(你可以通过本书的其他资源下载storm.txt)。你可以先在章节文件夹中创建一个名为files的子文件夹,然后将storm.txt保存在子文件夹中。
将清单 4-9 保存为newsfile.py,让 Python 大声朗读新闻文章。
# Put mysay.py in the same folder as this script
from mysay import print_say
import pathlib
# Open the file, and read the content of the text file
1 myfile = pathlib.Path.cwd() / 'files' / 'storm.txt'
with open(myfile,'r') as f:
content = f.read()
# Let Python speak the text in the file
print_say(content)
清单 4-9:朗读文本文件
你首先让脚本知道在哪里找到新闻文件 1。你使用open()从子文件夹files访问storm.txt。然后,你使用read()将文件内容读取到一个名为content的字符串变量中。最后,脚本以人类的声音大声朗读文件内容。很简单!
如果你将storm.txt保存在与前面脚本相同的文件夹中,那么无需指定文件路径。Python 将在没有指定路径时自动查找脚本所在的文件夹。
总结
在这一章中,你学习了如何安装文本转语音模块让 Python 说话。你将关键的文本转语音功能移入了模块mysay,以便在脚本中导入。
你还学习了如何让 Python 重复你说的话。你将新学到的技能应用到一个实际的应用中:使用语音输入要求 Python 计算矩形的面积,并以人类声音告诉你答案。
现在你已经知道如何让 Python 说话和听话了,在第五章中你将学习如何将这两项功能应用到多个有趣的实际应用中。
章节末练习
-
如果你使用的是 Windows 系统,请在pyttsx3_adjust.py中按如下方式修改代码:
-
该语音为男性声音。
-
语速为每分钟 160 个单词。
-
音量是 0.8。
-
-
修改脚本area_hs.py,让它在你说出三角形的高度和底边长度时计算三角形的面积。
语音应用

现在你已经知道如何让 Python 说话和聆听,我们将创建几个利用这些技能的实际应用。但在此之前,你将创建一个本地包。由于你将在书中剩余的章节中使用 mysr 和 mysay 本地模块,你将创建一个 Python 包来包含所有本地模块。这样,你就不需要将这些模块复制粘贴到每一章节的文件夹中。这还可以帮助保持全书代码的一致性。在这个过程中,你将学习如何创建一个 Python 包以及它的工作原理。
在第一个应用中,你将创建一个“猜数字”游戏,该游戏能够接收语音命令并用人类的声音回应你。
接下来,你将学习如何解析文本,从国家公共广播电台(NPR)中提取新闻摘要,并让 Python 朗读它们。你还将构建一个脚本,根据你的语音查询从 Wikipedia 提取信息并读出答案。
最后,你将学习如何通过语音遍历文件夹中的文件,目标是构建你自己的 Alexa。你可以对脚本说:“Python,播放 Selena Gomez,”然后一首保存在你计算机中的 Selena Gomez 歌曲将开始播放。
和往常一样,你可以从 www.nostarch.com/make-python-talk/ 下载所有脚本的代码。在开始之前,为本章节创建文件夹 /mpt/ch05/。
创建你自己的本地 Python 包
在第三章中,你创建了一个自定义的本地模块 mysr 来包含所有与语音识别相关的代码。每当你需要使用语音识别功能时,就从该模块导入 voice_to_text()。类似地,在第四章中,你创建了一个自定义的本地模块 mysay 来包含所有与语音合成相关的代码。每当你使用语音合成功能时,就从该模块导入 print_say()。
在本章以及本书的其他章节中,你将使用这两个自定义的本地模块。为了使这些模块正常工作,你需要将模块文件(即 mysr.py 和 mysay.py)放在与使用这些模块的脚本相同的目录中。这意味着你可能需要将这些文件复制粘贴到本书几乎每一章的目录中。你可能会想:有没有更高效的方法来实现这一点?
答案是肯定的,这正是 Python 包的用途。
接下来,你将首先了解什么是 Python 包及其如何工作。然后,你将学习如何创建自定义的本地包。最后,你将使用 Python 脚本测试并导入你的包。
什么是 Python 包?
许多人认为 Python 模块和 Python 包是一样的。其实它们并不相同。
Python 模块是一个具有.py扩展名的单个文件。与此不同,Python 包是包含在单一目录中的多个 Python 模块集合。该目录必须有一个名为 init.py 的文件,以便将其与其他仅包含.py扩展名文件的目录区分开来。
我将一步一步地引导你完成创建本地包的过程。
创建你自己的 Python 包
要创建本地 Python 包,你需要为其创建一个单独的目录,并将所有相关文件放入其中。在本节中,你将创建一个本地包来包含我们的语音识别和语音合成模块文件——即 mysr.py 和 mysay.py。
创建包目录
首先,你需要为包创建一个目录。
在本书中,你为每一章使用一个单独的目录。例如,本章中的所有 Python 脚本和相关文件都放置在目录 /mpt/ch05/ 中。由于你正在创建一个将在本书所有章节中使用的包,你将创建一个与所有章节并列的目录。具体来说,你将使用目录 /mpt/mptpkg/,其中 mptpkg 是包名。图 5-1 解释了包相对于书籍章节的位置。

图 5-1:mptpkg 包相对于章节文件夹的位置
如你所见,包目录与章节目录并列,所有目录都包含在本书的目录 /mpt 中,就像 Make Python Talk 一样。
为你的包创建必要的文件
接下来,你需要创建并放置包中所需的文件。
首先,将你在第三章和第四章创建的两个模块 mysr.py 和 mysay.py 复制并粘贴到包目录 /mpt/mptpkg/ 中。不要对这两个文件进行任何更改。
然后将以下脚本 init.py 保存在包目录 /mpt/mptpkg/ 中(你也可以从本书资源中下载它):
from .mysr import voice_to_text
from .mysay import print_say
这个文件的目的是双重的:它导入 voice_to_text() 和 print_say(),使你可以在包级别使用这些函数,并且它还告诉 Python 该目录是一个包,而不是一个恰好有 Python 脚本的文件夹。
最后,将以下脚本 setup.py 保存在书籍目录 /mpt 中,位于包目录 /mpt/mptpkg/ 上一级。该脚本也可以从本书的资源中获得。
from setuptools import setup
setup(name='mptpkg',
version='0.1',
description='Install local package for Make Python Talk',
author='Mark Liu',
author_email='mark.liu@uky.edu',
packages=['mptpkg'],
zip_safe=False)
该文件提供关于包的信息,例如包名、作者、版本、描述等。
接下来,你将学习如何在计算机上安装这个本地包。
安装你的包
因为你将在本书后续部分修改本地包并为其添加更多功能,所以最好以可编辑模式安装该包。
打开 Anaconda 提示符(Windows)或终端(Mac 或 Linux),激活本书的虚拟环境 chatting。运行以下命令:
**pip install -e** `path-to-mpt`
将 path-to-mpt 替换为 /mpt 目录的实际路径。例如,在我的办公电脑上,运行 Windows 操作系统的 /mpt 目录是 C:\mpt,因此我使用以下命令安装本地包:
pip install -e C:\mpt
在我的 Linux 机器上,/mpt 目录的路径是 /home/mark/Desktop/mpt,因此我使用以下命令安装本地包:
pip install -e /home/mark/Desktop/mpt
-e选项告诉 Python 以可编辑模式安装包,这样你就可以在需要时随时修改该包。
至此,本地包已安装在你的计算机上。
测试你的包
现在你已经安装了自己制作的本地包,你将学习如何导入它。
你将编写一个 Python 脚本来测试你刚刚创建的包。
让我们回顾一下第四章中的脚本repeat_me1.py。在你的 Spyder 编辑器中输入以下代码行,并将其保存在第五章目录/mpt/ch05/下,命名为repeat_me2.py:
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
while True:
print('Python is listening...')
inp = voice_to_text()
if inp == "stop listening":
print_say(f'you just said {inp}; goodbye!')
break
else:
print_say(f'you just said {inp}')
continue
首先,直接从mptpkg包中导入voice_to_text()和print_say()函数。回想一下,在脚本__init__.py中,你已经将这两个函数从模块.mysr和.mysay导入到包中。因此,在这里你可以直接从包中导入这两个函数。
剩下的脚本与repeat_me1.py中的相同。它会重复你说的内容。如果你说“停止监听”,脚本会停止。
以下是与repeat_me2.py的交互,我的语音输入以粗体显示:
Python is listening...
you just said **how are you**
Python is listening...
you just said **I am testing a python package**
Python is listening...
you just said **stop listening**; goodbye!
如你所见,脚本正常工作,这意味着你已经成功从本地包中导入了函数。
更多关于 Python 包的内容
在继续之前,我想提到关于 Python 包的几点事项。
首先,你可以向你的包中添加更多模块。在本书的后续章节中,你将向现有的本地包mptpkg中添加更多模块。你将使用整个书籍中的唯一本地包。这将减少目录的数量并帮助组织文件。
其次,如果你有一个有趣的包,想要与全世界分享,你可以很容易做到。你只需要添加一些额外的文件,比如许可证、README 文件等。关于如何分发你的 Python 包的教程,请参阅例如 Python Packaging Authority 网站,packaging.python.org/tutorials/packaging-projects/。
互动猜数字游戏
猜数字是一个流行的游戏,其中一个玩家写下一个数字,要求另一个玩家在有限的尝试次数内猜出这个数字。每次猜测后,第一个玩家会告诉第二个玩家猜测是否正确,还是太高或太低。
游戏的各种版本可以在网上和书籍中找到,我们将查看我们自己的版本,猜一个 1 到 9 之间的数字。启动一个新脚本并将其保存为guess_hs.py;hs代表听和说。
因为脚本相对较长,我将把它分成三部分并逐一解释。列表 5-1 给出了第一部分。
1 import time
import sys
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
# Print and announce the rules of the game in a human voice
2 print_say('''Think of an integer,
bigger or equal to 1 but smaller or equal to 9,
and write it on a piece of paper''')
print_say("You have 5 seconds to write your number down")
# Wait for five seconds for you to write down the number
time.sleep(5)
print_say('''Now let's start. I will guess a number and you can say:
too high, that is right, or too small''')
# The script asks in a human voice whether the number is 5
print_say("Is it 5?")
# The script is trying to get your response and save it as re1
# Your response has to be 'too high', 'that is right', or 'too small'
3 while True:
re1 = voice_to_text()
print_say(f"You said {re1}")
if re1 in ("too high", "that is right", "too small"):
break
# If you say "that is right", game over
if re1 == "that is right":
print_say("Yay, lucky me!")
sys.exit
`--snip--`
列表 5-1:猜数字游戏的第一部分
我们通过导入所需的模块来开始脚本。首先,我们导入time模块,以便在脚本中暂停一段时间。我们还导入sys模块,以便在脚本结束时退出。
如前一节所述,我们从本地包mptpkg中导入voice_to_text()和print_say(),用以将语音转换为文本并打印出来,以及通过语音播报文本信息。
脚本接着会用语音和文本打印出游戏规则 2。由于说明内容跨越多行,我们将它们放在三引号中,以使其更易读。
脚本宣布你有五秒钟的时间写下一个数字,然后通过使用sleep()暂停五秒钟,给你时间写下数字。
脚本然后开始猜测,它会用人声问数字是否是五。在第 3 步,我们开始一个无限循环来接受你的语音输入。当你对着麦克风说话时,计算机会将你的语音输入转换为一个名为re1的文本字符串变量。脚本会把你说的内容重复给你听。你的回应必须是以下三种之一:“太高”,“正确”或“太小”。如果不是,脚本会继续要求你回应,直到它匹配其中一个短语。这给了你一个机会,在脚本继续进行下一步之前,做出正确的回应。
如果你的回应是“正确”,计算机会说:“耶,真幸运!”然后退出脚本。接下来我们进入“太高”回应的行为。清单 5-2 展示了guess_hs.py脚本的中间部分。
`--snip--`
# If you say "too high", the computer keeps guessing
elif re1 == "too high":
# The computer guesses 3 the second round
print_say("Is it 3?")
# The computer is trying to get your response to the second guess
while True:
re2 = voice_to_text()
print_say(f"You said {re2}")
if re2 in ("too high", "that is right", "too small"):
break
# If the second guess is right, game over
if re2 == "that is right":
print_say("Yay, lucky me!")
sys.exit
# If the second guess is too small, the computer knows it's 4
elif re2 == "too small":
print_say("Yay, it is 4!")
sys.exit
# If the second guess is too high, the computer guesses the third time
elif re2 == "too high":
# The third guess is 1
print_say("Is it 1?")
# The computer is getting your response to the third guess
while True:
re3 = voice_to_text()
print_say(f"You said {re3}")
if re3 in ("too high", "that is right", "too small"):
break
# If the third guess is too small, the computer knows it's 2
if re3 == "too small":
print_say("It is 2!")
sys.exit
# If the third guess is right, game over
elif re3 == "that is right":
print_say("Yay, lucky me!")
sys.exit
`--snip--`
清单 5-2: “太高”行为
如果你的回应是“太高”,计算机会继续猜测,这次猜一个更低的数字。计算机的第二次猜测将是三,因为猜三可以减少计算机需要的尝试次数,以便找到答案。脚本将检测并捕捉你对第二次猜测的回应。
以下是你对第二次猜测的回应选项:如果是“正确”,计算机会说“耶,真幸运!”然后退出脚本。如果是“太小”,计算机会知道数字是四,并且会这么说。如果是“太高”,计算机会进行第三次猜测,猜数字一。
接下来,计算机会捕捉你对第三次猜测的回应。如果你的回应是“太小”,计算机会知道数字是二。如果你的回应是“正确”,计算机会说:“耶,真幸运!”然后退出。
现在让我们来看一下guess_hs.py的最后一部分,它处理对第一个猜测的“太小”回应。清单 5-3 展示了代码。
`--snip--`
# If you say "too small", the computer keeps guessing
elif re1 == "too small":
# The computer guesses 7 the second round
print_say("Is it 7?")
# The computer is trying to get your response to the second guess
while True:
re2 = voice_to_text()
print_say(f"You said {re2}")
if re2 in ("too high", "that is right", "too small"):
break
# If the second guess is right, game over
if re2 == "that is right":
print_say("Yay, lucky me!")
sys.exit
# If the second guess is too high, the computer knows it's 6
elif re2 == "too high":
print_say("Yay, it is 6!")
sys.exit
# If the second guess is too small, the computer guesses the third time
elif re2 == "too small":
# The third guess is 8
print_say("Is it 8?")
while True:
re3 = voice_to_text ()
print_say(f"You said {re3}")
if re3 in ("too high", "that is right", "too small"):
break
# If the third guess is too small, the computer knows it's 9
if re3 == "too small":
print_say("It is 9!")
sys.exit
# If the third guess is right, game over
elif re3 == "that is right":
print_say("Yay, lucky me!")
sys.exit
清单 5-3: “太小”行为
脚本的最后一部分类似于中间部分。如果你告诉计算机第一次猜测的五是“太小”,计算机会给你第二次猜测七。脚本将捕捉你对第二次猜测的回应。
如果你回应“正确”,计算机会说:“耶,真幸运!”然后退出脚本。如果你说“太高”,计算机会知道数字是六。如果你的回应是“太小”,计算机会进行第三次猜测,猜八。
然后,计算机会捕捉到你对第三次猜测的回应。如果你的回答是“too small”,计算机将知道数字是九。如果你的回答是“that is right”,计算机将说:“耶,我真幸运!”然后退出脚本。
如果你在一个安静的环境中有良好的互联网连接,你就可以和计算机进行接近完美的沟通。互联网连接非常重要,因为我们使用 Google Web Speech API 将语音输入转换为文本。SpeechRecognition模块有一个离线方法叫做recognize_sphinx(),但它会出很多错误,所以我们使用在线方法。
这是脚本在我的数字是 8 时的输出(我的语音输入用粗体标注):
Please think of an integer,
bigger or equal to 1 but smaller or equal to 9,
and write on a piece of paper
You have 5 seconds to write it down
Now let's start. I will guess a number and you can say:
too high, that is right, or too small
Is it 5?
You said **too small**
Is it 7?
You said **too small**
Is it 8?
You said **that is right**
Yay, lucky me!
脚本完美地理解了我说的每一个字。当然,这部分原因是因为我选择了某些词汇来避免歧义。在构建你自己的项目时,你将需要使用独特的语音命令,或者将这些词语放在特定的语境中,以便获得一致且正确的结果。由于每个语音命令通常很短,Python 脚本可能会在理解你的语音输入的上下文并返回正确的单词时遇到困难。
例如,如果你对着麦克风说“too large”,脚本可能会返回“two large”,这虽然是一个有意义的短语。因此,我们在guess_hs.py中使用“too high”而不是“too large”。
类似地,当我对着麦克风说“too low”时,脚本时不时会返回“tulo”。当我使用“too small”时,我每次都会得到正确的回应。
语音播报新闻
在这个项目中,我们将抓取 NPR 新闻网站,以收集最新的新闻摘要,并让 Python 将其朗读出来。这个项目分为两个脚本:一个用于抓取和整理新闻,另一个用于处理语音识别和文本转语音功能。我们先从网页抓取开始。
抓取新闻摘要
首先,我们需要从新闻网站抓取信息,并将其整理成一个干净且易读的格式。
不同的新闻网站内容排版方式不同,因此抓取的方法通常会有所不同。你可以参考第六章了解网页抓取的基础。如果你有兴趣抓取其他新闻网站,你需要根据该网站的特点调整此代码。我们首先来看一下这个网站和相应的源代码。
我们感兴趣的新闻出现在 NPR 新闻网站的首页,见图 5-2。
这个页面的一个实用功能是简短的新闻摘要。正如你所看到的,首页列出了最新的新闻,每条新闻都有一个简短的摘要。
你想要提取每篇新闻的标题和简短摘要并打印出来。为此,你需要在 HTML 程序中定位相应的标签。

图 5-2:NPR 新闻首页的新闻摘要
在网页上,按下键盘上的 ctrl-U。网页的源代码应该会显示出来。你可以看到它有将近 2000 行长。要找到需要的标签,按下 ctrl-F 打开右上角的搜索框。由于第一篇新闻文章的标题是Answering Your Coronavirus Questions(回答您的冠状病毒问题),如 Figure 5-2 所示,你应该输入Answering Your Coronavirus Questions并点击Search。然后跳转到相应的 HTML 代码,如 Listing 5-4 所示。
`--snip--`
1 <div class="item-info">
<div class="slug-wrap">
<h3 class="slug">
<a href="https://www.npr.org/series/821003492/the-national-conversation-with-
all-things-considered">The National Conversation With All Things Considered
</a>
</h3>
</div>
2 <h2 class="title">
<a href="https://www.npr.org/2020/04/28/847585398/answering-your-coronavirus-
questions-new-symptoms-economy-and-virtual-celebratio" data-
metrics='{"action":"Click Featured Story Headline 1-
3","category":"Aggregation"}' >Answering Your Coronavirus Questions: New
Symptoms, Economy And Virtual Celebrations
</a>
</h2>
3 <p class="teaser">
<a href="https://www.npr.org/2020/04/28/847585398/answering-your-coronavirus-
questions-new-symptoms-economy-and-virtual-celebratio"><time datetime="2020-
04-28"><span class="date">April 28, 2020 • </span></time>On this
broadcast of <em>The National Conversation, </em>we answer your questions
about the economy, mental health and new symptoms of COVID-19\. We'll also
look at how people are celebrating big life events.
</a>
</p>
</div>
`--snip--`
Listing 5-4: NPR 新闻首页部分源代码
请注意,所有的标题和摘要信息都被封装在一个父<div>标签中,该标签的class属性为item-info1。新闻标题的信息位于一个子<h2>标签中,该标签的class属性为title2。摘要的信息位于一个子<p>标签中,该标签的class属性为teaser3。
我们将使用这些模式编写一个 Python 脚本来提取所需的信息。脚本news.py将抓取信息并以简洁的方式整理所有标题和摘要。我已经在需要详细解释的地方添加了注释。
脚本将编译新闻摘要并以文本形式打印出来。输入 Listing 5-5,并将其保存为news.py。
# Import needed modules
import requests
import bs4
# Obtain the source code from the NPR news website
1 res = requests.get('https://www.npr.org/sections/news/')
res.raise_for_status()
# Use beautiful soup to parse the code
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# Get the div tags that contain titles and teasers
div_tags = soup.find_all('div',class_="item-info")
# Index different news
2 news_index = 1
# Go into each div tag to retrieve the title and the teaser
3 for div_tag in div_tags:
# Print the news index to separate different news
print(f'News Summary {news_index}')
# Retrieve and print the h2 tag that contains the title
h2tag = div_tag.find('h2', class_="title")
print(h2tag.text)
# Retrieve and print the p tag that contains the teaser
ptag = div_tag.find('p', class_="teaser")
print(ptag.text)
# Limit to the first 10 news summaries
news_index += 1
if news_index>10:
break
Listing 5-5: 抓取 NPR 新闻首页的 Python 代码
我们首先导入需要的模块bs4和requests(bs4是最新版本的 Beautiful Soup 库)。如果需要,按照第二章中的三步安装这些模块。
在第 1 步,我们获取 NPR 新闻首页的源代码,该代码是 HTML 格式的。然后,我们使用bs4模块解析 HTML 文件。因为我们需要的信息被封装在class属性为item-info的<div>标签中,所以我们找到所有这样的标签并将它们放入名为div_tags的列表中。为了区分不同的新闻摘要,我们创建了一个变量news_index来标记它们 2。
然后我们进入每个收集到的单独<div>标签 3。首先,我们打印出新闻摘要的索引以区分单独的新闻项目。其次,我们提取包含新闻标题的<h2>标签并打印出来。接着,我们提取包含新闻摘要的<p>标签并打印出来。最后,如果新闻索引超过 10,我们就停止,以确保输出的新闻摘要数量不超过 10 条。
如果运行news.py,输出将如下所示:Listing 5-6。
News Summary 1
Answering Your Coronavirus Questions: New Symptoms, Economy And Virtual Celebrations
April 28, 2020 • On this broadcast of The National Conversation, we answer your questions
about the economy, mental health and new symptoms of COVID-19\. We'll also look at how people
are celebrating big life events.
News Summary 2
More Essential Than Ever, Low-Wage Workers Demand More
April 28, 2020 • In this lockdown, low-wage workers have been publicly declared "essential" —
up there with doctors and nurses. But the workers say their pay, benefits and protections
don't reflect it.
News Summary 3
We Asked All 50 States About Their Contact Tracing Capacity. Here's What We Learned
April 28, 2020 • To safely reopen without risking new COVID-19 outbreaks, states need enough
staffing to do the crucial work of contact tracing. We surveyed public health agencies to
find out how much they have.
News Summary 4
Coronavirus Has Now Killed More Americans Than Vietnam War
April 28, 2020 • The number of lives taken by COVID-19 in the U.S. has reached a grim
milestone: More people have died of the disease than the 58,220 Americans who perished in the
Vietnam War.
`--snip--`
Listing 5-6: 从 NPR 新闻首页抓取的新闻摘要
现在我们让 Python 给我们读新闻。
添加文本转语音功能
下一步是让文本转语音模块将新闻摘要转换成语音。将 Listing 5-7 添加到一个新文件中,并保存为news_hs.py。
# Import needed modules
import requests
import bs4
import sys
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
# Define the news_teaser() function
1 def news_teaser():
`--snip--`
2 print_say(f'News Summary {news_index}')
h2tag = div_tag.find('h2', class_="title")
print_say(h2tag.text)
ptag = div_tag.find('p', class_="teaser")
print_say(ptag.text)
`--snip--`
# Print and ask you if you like to hear the news summary
print_say("Would you like to hear the NPR news summary?")
# Capture your voice command
inp = voice_to_text().lower()
# If you answer yes, activate the newscast
if inp == "yes":
news_teaser()
# Otherwise, exit the script
else:
sys.exit
Listing 5-7: 语音激活新闻播报的 Python 代码
我们首先导入常用的模块,并从自制的mptpkg包中导入voice_to_text()和print_say()。
接着,我们定义了一个名为news_teaser()的函数 1,它完成了news.py所做的所有工作。唯一的例外是,它不仅打印新闻索引、标题和摘要,还会同时朗读出来 2。接着,我们设置脚本询问:“你想听 NPR 新闻摘要吗?”voice_to_text()函数捕捉到你的语音响应,并将其转换成一个全小写字母的字符串变量。如果你说“是”,Python 将开始播放新闻。如果你说的不是“是”,脚本将退出。
语音控制维基百科
在本节中,我们将构建一个会说话的维基百科。不同于新闻播报员项目,我们将使用wikipedia模块直接获取所需信息。之后,我们将使脚本理解你提出的问题,检索答案,并大声朗读出来。
访问维基百科
Python 有一个wikipedia模块,可以帮助你深入了解你想了解的主题,因此我们不需要自己编写这部分代码。这个模块不在 Python 标准库或 Anaconda 导航器中。你需要使用 pip 进行安装。在 Windows 中打开 Anaconda 提示符,或者在 Mac 或 Linux 中打开终端,然后运行以下命令:
**pip install wikipedia**
接下来,运行以下脚本作为wiki.py:
import wikipedia
my_query = input("What do you want to know?\n")
answer = wikipedia.summary(my_query)
print(answer)
脚本运行后,在右下角的 IPython 控制台中,输入你想了解的主题。脚本会将你的查询保存为变量my_query。summary()函数将生成你的问题的摘要答案。最后,脚本会打印出来自维基百科的答案。
我输入了U.S. China trade war并得到了以下结果:
What do you want to know?
**U.S. China trade war**
China and the United States have been engaged in a trade war through increasing tariffs and other measures since 2018\. Hong Kong economics professor Lawrence J. Lau argues that a major cause is the growing battle between China and the U.S. for global economic and technological dominance. He argues, "It is also a reflection of the rise of populism, isolationism, nationalism and protectionism almost everywhere in the world, including in the US."
这个答案相对较短。大多数维基百科的搜索结果会更长。如果你希望限制响应的长度,比如只显示前 200 个字符,你可以在answer后面输入[0:200]。
添加语音识别和语音合成
现在我们将把语音识别和语音合成功能添加到脚本中。输入 Listing 5-8 作为wiki_hs.py。
import wikipedia
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
# Ask what you want to know
1 print_say("What do you want to know?")
# Capture your voice input
2 my_query = voice_to_text()
print_say (f"you said {my_query}")
# Obtain answer from Wikipedia
ans = wikipedia.summary(my_query)
# Say the answer in a human voice
print_say(ans[0:200])
Listing 5-8: 语音控制的会说话维基百科的 Python 代码
一旦你启动脚本,系统会询问:“你想知道什么?” 1。2 时,脚本调用voice_to_text()将你的语音输入转化为文本。然后,脚本从维基百科获取问题的答案,将其保存为字符串变量ans,并将其转化为人声朗读。
运行脚本后,如果你对着麦克风说“美国联邦储备银行”,你将得到类似以下的结果:
What do you want to know?
you said **U.S. federal reserve bank**
The Federal Reserve System (also known as the Federal Reserve or simply the
Fed) is the central banking system of the United States of America. It was
created on December 23,
1913, with the enactment
我已经在变量ans后面添加了[0:200]字符限制,所以结果只会打印和朗读前 200 个字符。
就这样,你拥有了自己的语音控制会说话的维基百科。尽管问吧!
语音激活的音乐播放器
在这里,你将学习如何让 Python 通过一个简单的命令,比如“Python, play Selena Gomez”来播放某个艺术家或音乐类型的歌曲。你只需说出你想听的艺术家的名字,脚本会将其作为关键字,并在特定的文件夹中搜索这些关键字。为了实现这一点,你需要能够遍历文件和文件夹。
遍历文件夹中的文件
假设你在章节文件夹中有一个子文件夹chat。如果你想列出子文件夹中的所有文件,可以使用这个traverse.py脚本:
import os
with os.scandir("./chat") as files:
for file in files:
print(file.name)
首先,脚本导入了os模块。这个模块使脚本能够访问与操作系统相关的功能,比如访问文件夹中的所有文件。
接下来,你将所有子文件夹chat中的文件放入一个名为files的列表中。脚本会遍历列表中的所有项目,并打印出每个项目的名称。
我在我的电脑上运行上述脚本后的输出结果如下:
book.xlsx
desk.pdf
storm.txt
graduation.pptx
`--snip--`
HilaryDuffSparks.mp3
country
classic
lessons.docx
SelenaGomezWolves.mp3
TheHeartWantsWhatItWantsSelenaGomez.mp3
如你所见,我们可以遍历文件夹中的所有文件和子文件夹,并打印出它们的名称。文件名包括文件扩展名。子文件夹的名称后面没有扩展名。例如,我在chat文件夹中有两个子文件夹,country和classic。因此,你会在前面的输出中看到country和classic。
接下来,你将使用这个功能来选择你想播放的歌曲。
Python,播放 Selena Gomez
清单 5-9 中的脚本(play_selena_gomez.py)能够根据你指定的任何艺术家(例如,Selena Gomez)来挑选并播放歌曲。你可以将歌曲保存在子文件夹chat中,或者将文件路径替换为你电脑中存储音乐的路径。
# Import the required modules
import os
import random
from pygame import mixer
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
# Start an infinite loop to take your voice commands
1 while True:
print_say("how may I help you?")
inp = voice_to_text()
print_say(f"you just said {inp}")
# Stop the script if you say 'stop listening'
if inp == "stop listening":
print_say("Goodbye! ")
break
# If 'play' is in voice command, music mode is activated
2 elif "play" in inp:
# Remove the word play from voice command
3 inp = inp.replace('play ','')
# Separate first and last names
names = inp.split()
# Extract the first name
Firstname = names[0]
# Extract the last name
if len(names)>1:
lastname = names[1]
# If no last name, use the first name as last name;
else:
lastname = firstname
# Create a list to contain songs
mysongs = []
# If either first name or last name in the file name, put in list
with os.scandir("./chat") as files:
for file in files:
4 if (firstname in file.name or lastname in file.name) \
and "mp3" in file.name:
mysongs.append(file.name)
# Randomly select one from the list and play
5 mysong = random.choice(mysongs)
print_say(f"play the song {mysong} for you")
mixer.init()
mixer.music.load(f'./chat/{mysong}')
mixer.music.play()
break
清单 5-9:Python 代码,用于通过语音激活计算机上某个艺术家的歌曲
我们首先导入所需的模块。特别是,我们导入os模块来遍历文件,导入random模块来从脚本生成的列表中随机选择一首歌。我们使用pygame模块中的mixer()来播放音乐文件。
接下来,我们启动一个无限循环,将脚本置于待机模式,等待你的语音命令。如果脚本检测到语音命令中有play这个词,音乐模式就会被激活。然后,我们用空字符串替换命令中的play及其后面的空格,这样你的命令“Play Selena Gomez”就变成了Selena Gomez。接下来的命令将分离出名字和姓氏。对于只以名字为人熟知的艺术家(例如 Madonna、Prince 或 Cher),我们将其名字作为占位符存入变量lastname中。
然后,我们遍历子文件夹chat中的所有文件。如果一个文件具有mp3扩展名,并且包含艺术家的名字(无论是名字还是姓氏),它将被添加到列表mysongs中。我们使用random模块中的choice()来随机选择列表mysongs中的一首歌,并通过mixer.music.load()加载它。接着,我们使用mixer.music.play()来播放它。
结果是,一旦你对脚本说“播放 Selena Gomez”,它会随机播放子文件夹chat中的两首歌曲之一,SelenaGomezWolves.mp3或TheHeartWantsWhatItWantsSelenaGomez.mp3。
Python,播放一首乡村歌曲
我们现在做的事情类似于与脚本play_selena_gomez.py交互,但在这里你将学习如何通过使用os模块访问不同的子文件夹,以及另一种播放音乐文件的方法。
假设你已经按音乐类型整理了你的歌曲。你将所有古典音乐文件放在子文件夹classic中,将所有乡村音乐文件放在country文件夹中,依此类推。这些子文件夹都被放置在你刚刚创建的chat文件夹中。
我们要编写一个脚本,这样当你说“Python,播放一首乡村歌曲”时,脚本会从country文件夹中随机选择一首歌并播放。输入列表 5-10 中的代码并将其保存为play_genre.py。
# Import needed modules
import os
import random
from pygame import mixer
# Import functions from the local package mptpkg
from mptpkg import voice_to_text
from mptpkg import print_say
while True:
print_say("how may I help you?")
inp = voice_to_text().lower()
print_say(f'you just said {inp}')
if inp == "stop listening":
print_say('Goodbye!')
break
elif "play a" in inp and "song" in inp:
# Remove 'play a' and 'song' so that only the genre name is left
1 inp = inp.replace('play a ','')
2 inp = inp.replace(' song','')
# Go to the genre folder and randomly select a song
with os.scandir(f"./chat/{inp}") as entries:
mysongs = [entry.name for entry in entries]
# Use pygame mixer to play the song
3 mysong = random.choice(mysongs)
print_say(f"play the song {mysong} for you")
mixer.init()
mixer.music.load(f"./chat/{inp}/{mysong}")
mixer.music.play()
break
列表 5-10:用于按音乐类型语音激活歌曲的 Python 代码
Python 检查语音命令中的play a和song,如果找到,就激活音乐模式。然后,脚本将play a 1 和 song 2 以及它们后面的空格替换为空字符串,只留下类型——此处为country——在语音命令中。这个类型会被用作脚本搜索的文件夹:在本例中为./chat/country。最后,脚本从该文件夹 3 中随机选择一首歌并播放。
请注意,我们在脚本中使用lower()函数来处理voice_to_text(),这样语音命令就会变成全小写。这是因为脚本有时会将语音命令转换为play A Country Song。我们这样做是为了避免由于大小写不一致导致的匹配错误。另一方面,路径和文件名不区分大小写,因此即使路径或文件名中有大写字母,也不会出现匹配错误。
总结
在这一章中,你首先学会了创建一个 Python 包来包含本地的文本到语音和语音识别模块。之后,你构建了几个能够理解语音命令、做出反应并发声的真实应用。
你创建了一个语音控制的、能说话的“猜数字”游戏。在这个游戏中,你从 1 到 9 之间选择一个数字,并与脚本互动让它进行猜测。然后,你学习了如何解析文本,从 NPR 网站提取新闻摘要,加入语音识别和文本转语音功能,制作一个语音控制的新闻广播。
你学会了如何使用wikipedia模块来获取你查询的答案。
你使用os模块遍历了计算机上的文件夹中的文件,然后创建了一个脚本,当你要求时,它会播放某个音乐类型或艺术家的歌曲。
现在你已经知道如何让 Python 说话和倾听,你将在本书的其余部分中将这两个功能应用到许多其他有趣的情境中,这样你就可以仅通过语音与计算机互动。
章节结束练习
-
修改guess_hs.py,使得脚本的第三次猜测变为 2 而不是 1。
-
修改 wiki.py,使其打印出来自 Wikipedia 结果的前 300 个字符。
-
修改 play_genre.py,使脚本通过使用 os 模块和计算机上的默认音乐播放器播放音乐,而不是使用 pygame 模块。
-
假设你计算机上的音乐文件不是 MP3 格式,而是 WAV 格式。你如何修改play_selena_gomez.py,使得脚本仍然能够正常工作?
Web 爬取播客、电台和视频

在本章中,你将基于第五章中的 Web 爬取基础知识,使用这些技能实现语音激活播客、直播电台和不同网站上的视频。
你还将学习超文本标记语言(HTML)是如何工作的,以及各种 HTML 标签如何构建网页。你将学习如何使用 Python 的 Beautiful Soup 库来解析 HTML 文件并提取信息。
凭借这些技能,你将构建三个应用程序来执行以下操作:
-
解析在线播客的源文件,找到 MP3 文件并播放播客。
-
使用语音控制播放在线直播电台。
-
播放在线视频,例如 NBC 的 Nightly News with Lester Holt。
在开始之前,为本章设置文件夹 /mpt/ch06/。如同往常,你可以从 www.nostarch.com/make-python-talk/ 下载所有脚本的代码。
Web 爬取入门
Beautiful Soup 库旨在从网站中提取信息。在本书中,我们将经常使用它,正如许多 Python 程序员在现实世界中使用一样。
我将首先讨论 HTML 标记的基础知识以及不同类型的标签如何在网站上形成各种区块。然后,你将学习如何使用 Beautiful Soup 库通过解析源代码从网站中提取信息。
什么是 HTML?
正如本章开头所提到的,HTML 代表 超文本标记语言,是一种告诉浏览器如何构建和显示网页内容的编程语言。HTML 使用各种类型的标签来构建网页的结构。
HTML 标签的结构
表 6-1 列出了常用的一些标签及其主要功能。
表 6-1:常用 HTML 标签
| 标签名称 | 描述 |
|---|---|
<html> |
HTML 文档的根级标签。它包含了所有其他 HTML 标签。 |
<head> |
HTML 文档的头部部分,包含有关页面的元数据。 |
<title> |
网页的标题,将显示在浏览器的标签页上。 |
<body> |
HTML 文档的主体部分,包含所有显示的内容。 |
<h1> |
级别 1 标题,例如新闻文章的标题。 |
<p> |
一段显示的内容。 |
<div> |
用于页面元素的容器,将 HTML 文档分成不同的部分。 |
<a> |
超链接,用于将一个页面链接到另一个页面。 |
<li> |
一个列表项。 |
所有标签以< >开头,以</ >结尾,以便浏览器能够识别不同的标签。例如,段落标签以<p>开始,以</p>结束。
让我们用<a>标签来说明 HTML 标签的组成部分。下面是使用<a>标签创建超链接的一个例子:
<a class="suprablue" href="http://libraries.uky.edu">Libraries</a>
这个超链接在开口标签中有可选的属性:<a class="suprablue" href="http://libraries.uky.edu">。class属性告诉浏览器从层叠样式表(CSS)中使用哪种样式,suprablue类名是预定义的(你将在接下来的部分学习如何定义一个类)。href属性指定了超链接的目标地址,libraries.uky.edu/。标签的内容将在页面上显示,位于开口和闭合标签之间:Libraries。
从 HTML 标签到网页
为了理解 HTML 如何使用标签构建网页,让我们看一个极为简化的例子。输入清单 6-1 中的脚本,并将其保存为UKYexample.html到你的章节文件夹中,或者你也可以从书籍的资源页面下载该文件。所有 HTML 文件都需要以.html或.htm为扩展名。
1 <html>
<head>
<title>Example: University of Kentucky</title>
<style>
.redtext {
color: red;
}
.leftmargin {
margin-left: 10px;
}
</style>
</head>
2 <body>
<p>Below are some links:</p>
<p><a class="redtext" href="http://libraries.uky.edu/">
University of Kentucky Libraries</a></p>
<p><a class="leftmargin" href="https://directory.uky.edu/">
University of Kentucky Directory</a></p>
</body>
</html>
清单 6-1:一个简单网页的 HTML 代码
在我解释代码之前,让我们先看一下实际网页的样子。进入你的章节文件夹,用你喜欢的浏览器打开UKYexample.html。我使用的是谷歌浏览器,网页显示效果如图 6-1 所示。

图 6-1:一个简单的网页
现在让我们将 HTML 代码链接到网页显示。
在 1 处,我们开始一个开口的<html>标签,用来包含脚本中的所有代码。接着,我们有一个嵌套在<head>标签中的<title>标签。<head>标签通常用于包含元数据,例如文档标题或 CSS 样式。<title>标签的内容是Example: University of Kentucky,这将设置网页标题,在浏览器标签的左上角显示,如图 6-1 所示。
<style>标签中的内容是用来定义两个类:redtext和leftmargin。第一个类告诉 HTML 将内容以红色显示,而第二个类则告诉 HTML 留出 10 像素的左边距。你可以在一个类中指定多种样式,如背景颜色、内边距或边距等。
在 2 处,我们开始了网页主体 HTML 部分,该部分将显示在页面上。在其中我们有三个嵌套的<p>标签。<p>标签定义了 HTML 文档中的一个独立段落;添加一个新的<p>标签将开始一个新段落。第一个<p>标签包含信息以下是一些链接:。
然后,我们提供两个超链接,每个都位于嵌套在<p>标签中的<a>标签里。我们将每个<a>标签放在单独的<p>标签中,这样链接会作为两个不同的段落显示,而不是并排显示在同一行。如果点击第一个链接,它将带你到肯塔基大学图书馆。如果点击第二个链接,它会将你引导到肯塔基大学目录。第一个标签具有redtext的类属性,按之前在<style>标签中定义的方式将文本显示为红色。类似地,第二个标签具有leftmargin类属性,因此文本University of Kentucky Library前会有 10 像素的边距。
使用 Beautiful Soup 提取信息
现在你已经理解了几个基本的 HTML 标签是如何工作的,接下来你将使用 Beautiful Soup 库来解析 HTML 代码并提取你需要的信息。我将首先讨论如何解析本地保存的 HTML 文件。然后你将学习如何从实时网页中提取信息。
让我们重新查看你在章节文件夹中保存的简单示例UKYexample.html。假设你想从网页中提取一些网址。你可以使用列表 6-2 中的parse_local.py来完成此任务。
# Import the Beautiful Soup library
from bs4 import BeautifulSoup
# Open the local HTML file as a text file
1 textfile = open("UKYexample.html", encoding='utf8')
# Use the findAll() function to locate all <p> tags
soup = BeautifulSoup(textfile, "html.parser")
ptags = soup.findAll("p")
# Print out <p> tags
print(ptags)
# Find the <a> tag nested in the third <p> tag
2 atag = ptags[2].find('a')
print(atag)
# Print the web address of the hyperlink
print(atag['href'])
# Print the content of the <a> tag
print(atag.text)
列表 6-2:解析本地 HTML 文件
首先,我们从bs4模块导入BeautifulSoup(),这是 Beautiful Soup 的最新版本。在步骤 1 中,我们通过使用 Python 内建的open()函数以文本文件的形式打开本地 HTML 文件。然后我们使用findAll()来定位 HTML 文件中的所有<p>标签,并将它们放入ptags列表中。
列表ptags中有三个<p>标签:
[**<p>**Below are some links:</p>,
**<p>**<a class="redtext" href="http://libraries.uky.edu/">
University of Kentucky Libraries</a></p>,
**<p>**<a class="leftmargin" href="https://directory.uky.edu/">
University of Kentucky Directory</a></p>]
以第三个标签为例。在步骤 2 中,我们定位到嵌套在第三个<p>标签中的<a>标签。然后我们打印出该<a>标签的href属性:
https://directory.uky.edu/
最后,我们打印出<a>标签的内容:
University of Kentucky Directory
整个脚本的输出结果如下:
[<p>Below are some links:</p>,
<p><a class="redtext" href="http://libraries.uky.edu/">
University of Kentucky Libraries</a></p>,
<p><a class="leftmargin" href="https://directory.uky.edu/">
University of Kentucky Directory</a></p>]
<a class="leftmargin" href="https://directory.uky.edu/">
University of Kentucky Directory</a>
https://directory.uky.edu/
University of Kentucky Directory
抓取实时网页
现在让我们抓取一个实时网页。实时网页的 HTML 标记比我们简单的静态版本要复杂得多,可能有成千上万行,所以你需要学会快速定位你需要的代码行。
假设你想从肯塔基大学图书馆网站提取联系信息。访问libraries.uky.edu/。然后滚动到页面底部,你将看到不同区域的联系信息,如图 6-2 所示。

图 6-2:你从一个实时网页中获取的信息
你想要提取图 6-2 中显示的三个部门的信息:流通部、参考部和馆际借阅部的部门名称、电话号码和电子邮件地址。首先,你需要在 HTML 文档中找到相应的标签。
在网页上,按下键盘上的 ctrl-U(或者右键点击并选择查看▶源代码)。网页的源代码应该会出现。你会看到它有超过 2,000 行长。为了定位你需要的标签,按 ctrl-F 在右上角打开搜索框。输入Circulation并点击搜索,这样就可以跳转到对应的 HTML 代码,如清单 6-3 所示。
`--snip--`
1 <div class="sf-middle">
2 <div class="dashing-li">
<span class="contact_area">Circulation:</span>
<span class="contact_methods">
<div class="contact_phone"><a class="suprablue"
href="tel:8592181881">(859) 218-1881</a></div>
<div class="contact_email"><a class="suprablue"
href="mailto:lib.circdesk@email.uky.edu">
lib.circdesk@email.uky.edu</a></div>
</span>
</div>
3 <div class="dashing-li">
<span class="contact_area">Reference:</span>
<span class="contact_methods">
<div class="contact_phone"><a class="suprablue"
href="tel:8592182048">(859) 218-2048</a></div>
<div class="contact_email"><a class="suprablue"
href="mailto:refdesk@uky.edu">refdesk@uky.edu</a></div>
</span>
</div>
4 <div class="dashing-li">
<span class="contact_area">Interlibrary Loan:</span>
<span class="contact_methods">
<div class="contact_phone"><a class="suprablue"
href="tel:8592181880">(859) 218-1880</div>
<div class="contact_email"><a class="suprablue"
href="mailto:ILLBorrowing@uky.edu">
ILLBorrowing@uky.edu</a></div>
</span>
</div>
<div class="dashing-li-last">
<span class="featured_area">All Other Questions & Comments:
</span>
<span class="featured_email"><a class="suprablue"
href="mailto:webadmin@lsv.uky.edu">
WebAdmin@lsv.uky.edu</a></span>
</div>
</div>
`--snip--`
清单 6-3:实时网页源代码的一部分
注意,所有信息都被封装在一个class属性为sf-middle的父<div>标签中 1。Circulation 部门的信息(包括名称、电话号码和电子邮件地址)被存放在一个类属性为dashing-li的子<div>标签中 2。其他两个区域的信息,Reference 3 和 Interlibrary Loan 4,则存放在父标签中的两个其他子<div>标签里。在每个子标签内,子标签分别包含以下几项信息:部门名称、电话号码和电子邮件地址。
这些模式在编写 Python 脚本以提取所需信息时非常重要。接下来,我将解释如何利用这些模式从 HTML 文件中提取信息。
从书籍的资源页面下载scrape_live_web.py并保存在你的章节文件夹中。脚本的第一部分如清单 6-4 所示,定位了每个区域的<div>标签。
from bs4 import BeautifulSoup
import requests
# Provide the web address of the live web
url = 'http://libraries.uky.edu'
# Obtain information from the live web
1 page = requests.get(url)
# Parse the page to obtain the parent div tag
soup = BeautifulSoup(page.text, "html.parser")
div = soup.find('div', class_="sf-middle")
# Locate the three child div tags
2 contacts = div.find_all("div", class_="dashing-li")
# Print out the first child div tag to examine it
print(contacts[0])
`--snip--`
清单 6-4:爬取实时网页的 Python 代码
我们导入了requests模块来获取来自实时网页的源代码。网页地址保存在变量url中。在第 1 步,我们使用get()来获取 HTML 代码。然后,我们找到类值为sf-middle的<div>标签,并将其作为父标签。
在第 2 步,我们定位到三个子<div>标签,它们的类值为dashing-li,并将它们放入列表contacts中,因为每个子<div>标签包含一个部门的所有联系信息。列表中的每个元素对应一个部门。例如,第一个元素包含了 Circulation 部门的所有信息,我们在清单 6-5 中打印出来。
<div class="dashing-li">
<span class="contact_area">Circulation:</span>
<span class="contact_methods">
<div class="contact_phone"><a class="suprablue" href="tel:8592181881">
(859) 218-1881</a></div>
<div class="contact_email"><a class="suprablue"
href="mailto:lib.circdesk@email.uky.edu">lib.circdesk@email.uky.edu
</a></div>
</span>
</div>
清单 6-5:Circulation 部门的实时网页源代码
scrape_live_web.py的第二部分将打印出每个区域的详细信息。如清单 6-6 所示。
`--snip--`
# Obtain information from each child tag
for contact in contacts:
# Obtain the area name
area = contact.find('span', class_="contact_area")
print(area.text)
# Obtain the phone and email
atags = contact.find_all('a', href = True)
for atag in atags:
print(atag.text)
清单 6-6:打印出爬取信息的 Python 代码
我们遍历列表contacts中的每个元素。为了打印出部门名称,我们找到带有contact_area类属性的<span>标签。该标签的内容就是部门名称。两个<a>标签包含每个部门的电话号码和电子邮件地址,我们也将它们打印出来。输出结果如下所示:
Circulation:
(859) 218-1881
lib.circdesk@email.uky.edu
Reference:
(859) 218-2048
refdesk@uky.edu
Interlibrary Loan:
(859) 218-1880
ILLBorrowing@uky.edu
语音激活的播客
在这个项目中,我们的目标是编写一个脚本,使你可以说:“Python,告诉我最新的新闻。”脚本将播放来自 NPR 新闻播客的简报。你将首先学会如何提取与播客相关的 MP3 文件并播放它,然后你会将语音识别功能添加到脚本中,以便通过语音激活。由于新闻简报大约是五分钟长,你还将学会如何在新闻播放时通过语音控制停止播客。
提取并播放播客
首先,找到一个你喜欢的新闻播报网站。为此,我们将使用NPR News Now,因为它是免费的并且每小时更新,全天候提供。网址是www.npr.org/podcasts/500005/npr-news-now/。
访问该网站,你应该能看到类似图 6-3 的内容。

图 6-3:NPR News Now首页
如你所见,我的最新新闻简报是在 2021 年 2 月 9 日东部时间早上 7 点更新的。在其下方,你还可以看到早上 6 点、5 点等时段的新闻简报。
要定位包含新闻简报的 MP3 文件,右键单击页面上的任意位置,然后从弹出的菜单中选择查看页面源代码选项(或按 ctrl-U)。你应该能看到源代码,如图 6-4 所示。

图 6-4:NPR News Now的源代码
你会注意到 MP3 文件包含在<a>标签中。我们需要使用 Beautiful Soup 库提取所有包含 MP3 文件的<a>标签,然后从第一个标签中提取链接,这将包含最新的新闻简报。如果你愿意,还可以收听以前的新闻简报;例如,第二个和第三个标签包含了图 6-3 中的早上 6 点和 5 点的新闻简报。
接下来,我们需要提取链接,去除不需要的部分,并使用webbrowser模块打开 MP3 文件的 URL,以便开始播放播客。在清单 6-7 中,脚本npr_news.py展示了如何实现这一功能。
# Import needed modules
import requests
import bs4
import webbrowser
# Locate the website for the NPR news brief
url = 'https://www.npr.org/podcasts/500005/npr-news-now'
# Convert the source code to a soup string
response = requests.get(url)
1 soup = bs4.BeautifulSoup(response.text, 'html.parser')
# Locate the tag that contains the mp3 files
2 casts = soup.findAll('a', {'class': 'audio-module-listen'})
print(casts)
# Obtain the weblink for the mp3 file related to the latest news brief
3 cast = casts[0]['href']
print(cast)
# Remove the unwanted components in the link
4 pos = cast.find('?')
print(cast[0:pos])
# Extract the mp3 file link, and play the file
mymp3 = cast[0:pos]
webbrowser.open(mymp3)
清单 6-7:播放在线播客的脚本
我们首先使用get()方法从requests模块获取NPR News Now网站的源代码,并将其保存在变量response中。在步骤 1 中,我们使用 Beautiful Soup 库解析文本,并选择html.parser选项来指定源代码是 HTML 格式的。我们在图 6-4 中看到,MP3 文件被保存在具有class属性为audio-module-listen的<a>标签中。因此,在步骤 2 中,我们使用 Beautiful Soup 中的findAll()方法获取所有这些标签,并将它们存入casts列表中。清单 6-8 显示了casts的内容。
[<a class="audio-module-listen"
href="https://play.podtrac.com/500005/edge1.pod.npr.org/anon.npr-
mp3/npr/newscasts/2021/02/09/newscast070736.mp3?dl=1&
siteplayer=true&size=4500000&awCollectionId=500005&
awEpisodeId=965747474&dl=1">
<b class="audio-module-listen-inner">
<b class="audio-module-listen-icon icn-play"></b>
<b class="audio-module-listen-text">
<b class="audio-module-cta">Listen</b>
<b class="audio-module-listen-duration">
<span>· </span>
<span>5:00</span>
</b>
</b>
</b>
</a>, <a class="audio-module-listen"
href="https://play.podtrac.com/500005/edge1.pod.npr.org/anon.npr-
mp3/npr/newscasts/2021/02/09/newscast060736.mp3?dl=1&
siteplayer=true&size=4500000&awCollectionId=500005&
awEpisodeId=965731320&dl=1">
<b class="audio-module-listen-inner">
<b class="audio-module-listen-icon icn-play"></b>
<b class="audio-module-listen-text">
<b class="audio-module-cta">Listen</b>
<b class="audio-module-listen-duration">
<span>· </span>
<span>5:00</span>
</b>
</b>
</b>
</a>, <a class="audio-module-listen"
href="https://play.podtrac.com/500005/edge1.pod.npr.org/anon.npr-
mp3/npr/newscasts/2021/02/09/newscast050736.mp3?dl=1&
siteplayer=true&size=4500000&awCollectionId=500005&
awEpisodeId=965721223&dl=1">
`--snip--`
</a>]
清单 6-8:所有具有class属性为audio-module-listen的<a>标签
如你所见,多个<a>标签包含 MP3 文件。在步骤 3 中,我们提取列表中的第一个<a>标签,并获取该标签的href属性(指向 MP3 文件的链接),并将其保存在cast中。链接如下:
https://play.podtrac.com/500005/edge1.pod.npr.org/anon.npr-
mp3/npr/newscasts/2021/02/09/newscast070736.mp3?dl=1&siteplayer=true&size=450
0000&awCollectionId=500005&awEpisodeId=965747474&dl=1
我们修剪链接,以使其以.mp3扩展名结尾。为此,我们利用链接中.mp3后面紧跟的?字符,并使用字符串方法find()来定位链接中?的位置 4. 然后相应地修剪链接并打印出来。修剪后的链接如下所示:
https://play.podtrac.com/500005/edge1.pod.npr.org/anon.npr-
mp3/npr/newscasts/2021/02/09/newscast070736.mp3
最后,我们提取在线 MP3 文件的链接,并使用webbrowser模块中的open()打开和播放 MP3 文件。
如果您运行脚本,您应该听到最新的 NPR 新闻简报在您的默认网络浏览器中播放。
语音激活播客
接下来,我们将在脚本中添加语音识别,以便您可以语音激活播客。此外,由于播客大约有五分钟长,能够用语音停止它非常有用。为了实现这一点,我们需要安装pygame模块,因为它允许 Python 脚本在音频播放时停止音频文件。webbrowser模块没有这个功能。
在 Windows 中安装pygame非常简单。在 Anaconda 提示符下执行以下代码行,并确保虚拟环境已激活:
**pip install pygame**
然后按照说明操作。
如果您使用的是 Mac,最新版本的 macOS 需要安装 Pygame 2. 要安装它,请在终端中执行以下代码,并确保虚拟环境已激活:
**pip install pygame==2.0.0**
然后按照说明操作。
如果您使用的是 Linux,请在终端中执行以下三行代码,并确保虚拟环境已激活:
**sudo apt-get install python3-pip python3-dev**
**sudo pip3 install pygame**
**pip install pygame**
请参阅本书结尾的附录 A 以获取更多详细信息。如果安装不成功,可以使用vlc模块作为替代。
脚本news_brief_hs.py在列表 6-9 中展示了如何使用语音控制激活NPR News Now播客并在需要时停止它。
from io import BytesIO
import requests
import bs4
from pygame import mixer
# Import functions from the local package
from mptpkg import voice_to_text, print_say
1 def news_brief():
# Locate the website for the NPR news brief
url = 'https://www.npr.org/podcasts/500005/npr-news-now'
# Convert the source code to a soup string
response = requests.get(url)
soup = bs4.BeautifulSoup(response.text, 'html.parser')
# Locate the tag that contains the mp3 files
casts = soup.findAll('a', {'class': 'audio-module-listen'})
# Obtain the web link for the mp3 file
cast = casts[0]['href']
# Remove the unwanted components in the link
mp3 = cast.find("?")
mymp3 = cast[0:mp3]
# Play the mp3 using the pygame module
mymp3 = requests.get(mymp3)
Voice = BytesIO()
voice.write(mymp3.content)
voice.seek(0)
mixer.init()
mixer.music.load(voice)
mixer.music.play()
2 while True:
print_say('Python is listening…')
inp = voice_to_text().lower()
print_say(f'you just said: {inp}')
if inp == "stop listening":
print_say('Goodbye!')
break
# If "news" in your voice command, play news brief
3 elif "news" in inp:
news_brief()
# Python listens in the background
while True:
background = voice_to_text().lower()
# Stops playing if you say "stop playing"
if "stop playing" in background:
mixer.music.stop()
break
continue
列表 6-9:用于语音激活NPR News Now的 Python 脚本
首先导入所需的模块。特别是,我们从io模块导入BytesIO()来创建一个临时文件,用于保存新闻简报的音频文件。这可以防止在重新运行脚本时发生覆盖文件的崩溃。
我们定义news_brief() 1. 此函数完成了我们在npr_news.py中所做的事情,但有一些例外。我们下载 MP3 文件并将其保存到临时文件voice中。之后,我们使用pygame模块播放NPR News Now的最新新闻简报。
在 2 处,我们启动一个无限循环。在每次迭代中,脚本会捕捉您的语音。当您的语音命令中包含news时 3,脚本将调用news_brief()并开始播放最新的 NPR 新闻简报。当新闻播放时,脚本会在后台持续监听您的语音命令。当您说“停止播放”时,无论何时在或之后,循环将中断并返回主菜单。如果要结束脚本,只需说“停止监听”。
语音激活收音机播放器
我们在这个项目中的目标是编写一个脚本,通过语音控制播放在线直播广播。当你说“Python,播放在线广播”时,脚本将访问该网站并点击播放按钮,使得直播广播在你的计算机上开始播放。
我们将使用selenium模块来自动化从 Python 与网页浏览器的交互。接下来,我们将在脚本中添加语音控制,以实现语音激活。
安装 selenium 模块
selenium模块不在 Python 标准库中,因此我们首先需要安装它。打开 Anaconda 提示符(Windows)或终端(Mac 或 Linux),激活您的虚拟环境,并执行以下命令:
**conda install selenium**
按照屏幕上的指示完成安装。
控制网页
selenium模块允许您用 Python 自动化网页浏览器的交互。
在线广播盒(https://onlineradiobox.com/us/)将作为我们的广播平台。您可以将其更改为任何您喜欢的在线广播电台,例如 Magic 106.7 或 NPR 在线电台。
访问该网站,您应该看到类似于图 6-5 所示的屏幕。

图 6-5:在线广播盒的首页
当网页加载完成后,直播广播并未开始播放。您需要使用selenium与网页浏览器进行交互,点击播放按钮(如图 6-5 中底部的白色三角形按钮)。
现在,您将学习如何定位网站上播放按钮的 XPath。XPath是可扩展标记语言(XML)路径的缩写。它是通过使用 XML 路径表达式查找网页元素的语法。
以下是查找播放按钮 XPath 的步骤:
-
使用 Chrome 浏览器打开在线广播盒的网页,如图 6-5 所示。
-
将鼠标光标放在播放按钮上(不要点击)。然后右键单击,选择检查(Inspect)从弹出菜单中。源代码将在网页右侧显示,如图 6-6 所示。
-
右键单击页面右侧高亮的代码行,并选择复制▶XPath。
-
将 XPath 粘贴到一个空文件中,以便以后使用。在本例中,播放按钮的 XPath 是
//*[@id="b_top_play"]。

图 6-6:定位播放按钮的 XPath
接下来,您需要为特定的浏览器下载网页驱动。如果您想了解更多关于 Selenium 项目的信息,可以访问其官网。
按照chromedriver.chromium.org/downloads/上的说明下载适合您操作系统的可执行文件。在 Windows 中,这个文件是chromedriver_win32.zip;解压 ZIP 文件并将可执行文件放在章节文件夹中。在 Unix 操作系统中,可执行文件叫做chromedriver,而在 Windows 中,它的文件名是chromedriver.exe。
最后一步,将play_live_radio.py保存在您的章节文件夹中并运行它。该脚本也可以在本书的资源页面找到,显示在清单 6-10 中。
# Put your web driver in the same folder as this script
from selenium import web driver
browser = webdriver.Chrome(executable_path='./chromedriver')
browser.get("https://onlineradiobox.com/us/")
button = browser.find_element_by_xpath('//*[@id="b_top_play"]')
button.click()
清单 6-10:用于自动化在线直播电台的 Python 代码
我们首先从selenium模块中导入webdriver()。首先,脚本启动网页浏览器。然后,get()函数根据提供的网址将我们带到直播电台网站。接着,我们将播放按钮定义为变量button,使用我们生成的 XPath。最后,我们使用selenium模块中的click()来激活网站上的播放按钮。因此,如果一切安装和配置正确,当您运行脚本时,网页浏览器将打开,在线直播电台将开始播放。
使用 F9 键逐行运行脚本非常具有教育意义。您将看到,在第一行执行后,Chrome 浏览器会在您的计算机上打开,在第二行执行后,浏览器会将您带到在线电台网站。通过最后两行,播放按钮被激活。此时,您将听到正在播放的直播电台。
语音激活直播电台
我们将为脚本添加语音识别和文本转语音功能,以便您可以通过语音激活在线直播电台。脚本voice_live_radio.py在清单 6-11 中展示了如何实现这一功能。
# Put web driver in the same folder as this script
# Import the web driver function from selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# Import functions from the local package
from mptpkg import voice_to_text, print_say
1 def live_radio():
global button
chrome_options = Options()
chrome_options.add_argument("—headless")
browser = webdriver.Chrome\
(executable_path = './chromedriver',chrome_options = chrome_options)
browser.get("https://onlineradiobox.com/us/")
button = browser.find_element_by_xpath('//*[@id="b_top_play"]')
button.click()
2 while True:
print_say("how may I help you?")
3 inp = voice_to_text().lower()
print_say(f'you just said {inp}')
4 if inp == "stop listening":
print_say('Goodbye!')
break
5 elif "radio" in inp:
print_say('OK, play live radio online for you!')
live_radio()
while True:
background = voice_to_text().lower()
if "stop playing" in background:
button.click()
break
else:
continue
清单 6-11:用于语音激活在线直播电台的 Python 代码
我们首先导入所有需要的模块。由于我们需要语音识别和文本转语音功能,我们从本地的mptpkg包中导入voice_to_text()来将语音转换为文本。我们还从本地的mptpkg包中导入print_say()来将文本转换为语音。
然后我们定义live_radio(),通过一些修改来实现play_live_radio.py的功能。当该函数被激活时,脚本将访问在线直播电台站点并点击播放按钮,从而启动直播电台的播放。我们使用headless选项,因此您不会看到浏览器弹出。我们还将变量button设为全局变量,以便以后在脚本中使用该变量。
在第 2 行,一个无限循环开始。每次迭代时,脚本都会问:“我能为您做些什么?”在您对着麦克风讲话后,voice_to_text()将您的语音转换为文本,并将其保存为字符串变量inp。lower()函数将所有字符转换为小写,以避免由于字母大写而导致的匹配错误。
当你说“停止监听”时,代码中的if分支被激活 4。脚本会打印Goodbye,循环中断,脚本结束。当你的语音命令中包含radio时,代码中的elif分支被激活 5。因此,live_radio()被调用,在线直播电台开始播放。当电台播放时,脚本会在后台静默监听你的命令。如果你在播放过程中说“停止播放”,按钮会再次被点击,电台的状态会从播放变为停止。之后,脚本退出电台模式并返回主菜单。
语音激活视频
你可以应用在前一节中学到的方法,来语音激活预录的在线视频或甚至是在线直播电视。
NBC 的Nightly News with Lester Holt提供预录视频,网址为www.nbcnews.com/nightly-news-full-episodes/,如图 6-7 所示。

图 6-7:NBC 的Nightly News首页
我们将使用 Python 与网页浏览器互动,点击激活在线视频的播放按钮。你可以在视频框架中看到一个三角形的播放按钮。按照“控制网页”一节中的步骤(第 125 页)找到该按钮的 XPath。
列表 6-12 中的脚本voice_online_video.py展示了如何语音激活在线视频。
# Import functions from the local package
from mptpkg import voice_to_text, print_say
# Import the web driver function from selenium
from selenium import webdriver
def online_video():
browser = webdriver.Chrome(executable_path='./chromedriver')
browser.get("https://www.nbcnews.com/nightly-news-full-episodes")
button = browser.find_element_by_xpath\
('//*[@id="content"]/div[6]/div/div[3]/div/\
1 section[2]/div[2]/div/div[1]/article/div[1]/h2/a[2]/span')
button.click()
2 while True:
print_say("how may I help you?")
inp = voice_to_text().lower()
print_say(f'you just said {inp}')
if inp == "stop listening":
print('Goodbye!')
break
elif "video" in inp:
print_say('OK, play online video for you!')
online_video()
break
列表 6-12:语音激活在线视频的脚本
逻辑与处理直播电台时相同。我们首先定义online_video(),以便稍后调用。当该函数被激活时,脚本将访问网站,定位播放按钮的 XPath 1,并点击它以便视频开始播放。
一个无限循环从第 2 行开始。在每次循环中,脚本会询问:“我能帮您做什么?”在你对着麦克风说话后,voice_to_text()将你的语音转换为文本,并将其保存为一个全小写的字符串变量inp。
当你说“停止监听”时,代码中的if分支被激活。脚本会打印Goodbye!,循环中断,脚本结束。当你的语音命令中包含video时,代码中的elif分支被激活。结果,online_video()被调用,在线视频开始播放。
总结
在本章中,你学习了网页抓取的基础知识:HTML 是如何工作的,包括 HTML 标签的不同类型及其用途,以及如何使用 Beautiful Soup 库解析 HTML 文件并抓取所需信息。
利用这些技巧,你学会了如何解析播客《NPR News Now》的源文件并定位其 MP3 文件。然后,你使用webbrowser模块播放在线 MP3 文件。你还学会了如何语音激活在线播客,利用pygame模块播放音频文件,从而可以通过语音命令随时停止播放。
然后,你学习了如何语音激活在线电台盒子。具体来说,你学习了如何使用 Selenium 网络驱动程序与网页浏览器互动。你指示 Python 点击播放按钮以启动在线广播。你还学会了使用语音控制来完成这些任务。
最后,你将相同的思路应用到了在线视频中,比如 NBC 的 莱斯特·霍尔特晚间新闻。
章节末练习
-
修改 parse_local.py 以打印出
class属性值和肯塔基大学图书馆的<a>标签的网页地址。 -
修改 scrape_live_web.py 以打印出“所有其他问题与评论”区域的信息,如 图 6-2 所示。
-
这个网址指向由格温妮斯·帕特洛和奥普拉·温弗瑞主持的播客:https://goop.com/the-goop-podcast/gwyneth-x-oprah-power-perception-soul-purpose/. 编写一个脚本来语音激活这个在线播客。
构建虚拟个人助手

在本章和下一章,你将学习如何创建你自己的虚拟个人助手(VPA),类似于亚马逊的 Alexa。你将首先了解你的 VPA 及其功能概述。然后,你将一次性导入所有需要的模块,以便能够立即开始运行你的 VPA。你将编写一个脚本,让你的 VPA 全天候待命而不打扰你。每当你需要帮助时,可以说“你好,Python”来唤醒它,当你希望它再次待机时,可以使用语音命令让它进入待机模式。
接下来,你将检查为你的 VPA 添加的各种功能。前两个功能是定时器和闹钟。
第三个功能使你的虚拟个人助手(VPA)能够讲笑话。当你说“告诉我一个笑话”时,脚本会从列表中随机选择一个笑话并大声讲给你听。
第四个功能是发送电子邮件。如果你说“给杰西卡发邮件”,脚本将启动电子邮件功能,从你的收件人列表中提取杰西卡的电子邮件地址,并询问你输入主题行和内容,你可以口述后告诉 VPA 发送。
在第八章,你将学习如何让你的 VPA 能够回答(几乎)任何问题。在开始之前,请为本章创建文件夹 /mpt/ch07/。和往常一样,本章的所有脚本可以在书籍的资源页面找到,www.nostarch.com/make-python-talk/。
虚拟个人助手概述
在你了解 VPA 的功能之前,让我们先来看看它的结构。你将从下载所需文件并安装第三方模块开始。
下载 VPA 文件
让我们下载所需的文件。访问书籍的资源网站 www.nostarch.com/make-python-talk/,从 /mpt/mptpkg/ 目录中下载以下文件:mywakeup.py、mytimer.py、myalarm.py、myjoke.py 和 myemail.py。将它们放在你存放自己制作的本地包文件的计算机同一目录中。有关说明,请参考第五章。我将在本章后面解释这些文件的作用。
接下来,打开计算机中包目录 /mpt/mptpkg/ 下的脚本 init.py。正如你在第五章中所回忆的,你已经将以下两行代码放入其中:
from .mysr import voice_to_text
from .mysay import print_say
将清单 7-1 中的五行代码添加到 init.py 的末尾。
from .mywakeup import wakeup
from .mytimer import timer
from .myalarm import alarm
from .myjoke import joke
from .myemail import email
清单 7-1:从本地模块导入函数到本地包
这段代码从五个模块中导入了五个函数 wakeup()、timer()、alarm()、joke() 和 email() 到本地包中,以便你稍后可以在包级别导入它们。关于这一点,我稍后会详细解释。
接下来,访问书籍的资源网站,并从章节目录 /mpt/ch07/ 中下载 vpa.py。将其保存在你存放本章 Python 脚本的计算机位置。vpa.py 的代码展示在清单 7-2 中。
# Import functions from the local package
from mptpkg import voice_to_text, print_say, wakeup, timer, alarm, joke, email
# Put the script in standby
1 while True:
# Capture your voice command quietly in standby
wake_up = wakeup()
# You can wake up the VPA by saying "Hello Python"
while wake_up == "Activated":
print_say("How may I help you?")
inp = voice_to_text().lower()
print_say(f'You just said {inp}.')
if "back" in inp and "stand" in inp:
print_say('OK, back to standby; let me know if you need help!')
break
# Activate the timer
2 elif "timer for" in inp and ("hour" in inp or "minute" in inp):
timer(inp)
continue
# Activate the alarm clock
elif "alarm for" in inp and ("a.m." in inp or "p.m." in inp):
alarm(inp)
continue
# Activate the joke-telling functionality
elif "joke" in inp and "tell" in inp:
joke()
continue
# Activate the email-sending functionality
elif "send" in inp and "email" in inp:
email()
continue
else:
continue
# End the script by including "stop" in your voice command
if wake_up == "ToQuit":
print_say("OK, exit the script; goodbye!")
break
清单 7-2:VPA 的 Python 代码
我们首先从本地包 mptpkg 中导入七个函数(voice_to_text()、print_say()、wakeup() 等)。清单 7-1 中的代码已经从本地模块导入了五个函数(wakeup()、timer() 等)到 mptpkg,因此这里我们直接在包级别导入这些函数。
我们通过创建一个无限循环 1 来启动脚本。在每次迭代中,VPA 在后台安静地监听你的语音命令。你可以说“Hello Python”来唤醒 VPA。唤醒后,VPA 会询问:“How may I help you?”并接收你的语音命令。你可以激活 VPA 的四个功能之一 2:设置计时器、设置闹钟、讲笑话或发送电子邮件。
完成后,你可以通过在语音输入中加入“back”和“standby”将 VPA 置于待机状态。当脚本处于待机状态时,你可以通过说“停止脚本”或“停止监听”来终止脚本。
在运行 vpa.py 之前,你需要安装一个第三方模块。
安装 arrow 模块
我们首先安装 arrow 模块,用于为计时器和闹钟功能提供时间和日期信息。
Python 标准库中有几个可以提供时间和日期的模块,包括著名的 time 和 datetime。然而,它们不太易用,格式也复杂。而且,为了实现我们在本章中的目标,你需要使用多个 Python 标准库中的模块。因此,我们将使用第三方模块 arrow,它提供了一种更方便的时间处理方式。
您可以通过在 Anaconda 提示符(Windows)或终端(Mac 或 Linux)中使用以下命令安装 arrow,并激活虚拟环境 chatting:
**conda install arrow**
管理待机模式
在这里,您将为您的 VPA 设置待机模式。在本节结束时,您将能够通过说,“Hello Python”来激活 VPA。VPA 将回应,“How may I help you?”
然后,如果您说,“返回待机模式”,脚本将进入待机模式并保持安静。在待机模式下,您甚至可以选择通过在语音命令中包含 stop 来结束脚本。
创建本地模块 mywakeup
首先,您需要设置脚本以识别某些命令。在您的 Spyder 编辑器中打开您刚刚下载的 mywakeup.py。此脚本基于第三章的 mysr.py,并进行了重大修改。列表 7-3 突出了这些差异。
import speech_recognition as sr
speech = sr.Recognizer()
# Define a wakeup() function to determine the status of the VPA
1 def wakeup():
wakeup = "StandBy"
voice_input = ""
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
2 audio = speech.listen(source,timeout=3)
voice_input = speech.recognize_google(audio).lower()
except sr.UnknownValueError:
pass
except sr.RequestError:
pass
except sr.WaitTimeoutError:
pass
if "hello" in voice_input and "python" in voice_input:
wakeup = "Activated"
elif "stop" in voice_input:
wakeup = "ToQuit"
return wakeup
列表 7-3:mywakeup 模块的 Python 代码
我们首先导入 speech_recognition 并定义 wakeup() 1。我们创建一个变量 wakeup 并将默认值设置为 StandBy。然后,我们从麦克风捕获语音输入。
在这里,我做了一些调整,使脚本更具响应性:listen() 方法中的 timeout=3 选项告诉脚本每 3 秒超时并分析语音输入 2,这意味着它每 3 秒检查一次语音命令。如果没有这个选项,脚本可能会等待太长时间才能响应,您可能需要多次说,“Hello Python”才能引起脚本的注意。
我们将所有文本转换为小写字母,以避免由于大小写不匹配而导致的错误。我们还使用异常处理来防止脚本崩溃。
当捕获到语音命令时,脚本会检查语音输入中是否包含 hello 和 Python。如果包含,变量 wakeup 的值将更改为 Activated。类似地,如果您说,“停止监听”或“停止脚本”,变量 wakeup 将更改为 ToQuit。当调用该函数时,它将返回变量 wakeup 中存储的任何值。
设置一些响应
现在,您已经了解了 mywakeup 模块的工作原理,让我们学习如何管理待机模式。
在您的 Spyder 编辑器中运行 vpa.py。您会注意到,当脚本正在运行时,什么都没有发生。然而,您的 VPA 正在静静地在后台监听。您可以通过说,“Hello Python”来激活 VPA。一旦完成任务,您可以将其返回待机模式。
以下输出来自与脚本的一次交互,我的语音输入以粗体显示:
**hello Python**
How may I help you?
**go back to standby**
You just said go back to standby.
OK, back to standby; let me know if you need help!
**hello Python**
How may I help you?
**go back to standby**
You just said go back to standby.
OK, back to standby; let me know if you need help!
**stop listening**
OK, exit the script; goodbye!
如您所见,我激活了 VPA 然后又将其返回待机模式。我激活了 VPA 然后第二次将其返回待机模式。之后,我说,“停止监听”以结束脚本。
多次运行脚本,确保您可以通过语音激活 VPA,将其置于待机模式,并结束脚本。接下来,我们将逐个查看 VPA 的各项功能。
让您的 VPA 设置定时器
让我们探索第一个功能:设置定时器。为了做到这一点,你首先要学习如何在 Python 中获取时间。我们将使用arrow模块获取当前时间,然后创建一个接受书面命令的定时器。最后,我们将在本地模块mytimer中创建一个timer()函数,并将其导入到 VPA 脚本中;这样,我们就可以通过语音命令来设置定时器。
使用 Python 获取时间
让我们先学习如何使用 Python 获取时间。
以下脚本,get_time.py,展示了如何以不同格式获取当前时区的时间。这只是一个示例,帮助你熟悉arrow模块;它并不是 VPA 脚本的一部分。
import arrow
# Current time in HH:MM:SS format
1 current_time = arrow.now().format('H:m:s')
print('the current time is', current_time)
2 current_time12 = arrow.now().format('hh:mm:ss A')
print('the current time is', current_time12)
# We can also print out hour, minute, and second individually
3 print("the current hour is",arrow.now().format('H'))
print("the current minute is",arrow.now().format('m'))
print("the current second is",arrow.now().format('s'))
我们首先导入arrow模块。它的now()函数提供当前本地日期和时间,但你需要使用format()来指定格式和细节级别。
表 7-1 列出了format()函数在arrow模块中常用的格式及其含义。例如,大写的HH和H分别以 24 小时制生成当前小时值,带有或不带有前导零,而hh和h则以 12 小时制生成相同的值。
在第 1 步,我们以 24 小时制的H:m:s格式获取当前时间,并打印出来。在第 2 步,我们以 12 小时制的格式hh:mm:ss获取时间,并加上 AM 或 PM。最后,我们打印出当前时间的小时值。你也可以对分钟值或秒钟值执行相同的操作。
如果你运行该脚本,输出将类似如下:
the current time is 8:35:46
the current time is 08:35:46,AM
the current hour is 8
the current minute is 35
the current second is 46
表 7-1:arrow模块format()方法的一些常用格式
| 格式代码 | 含义 |
|---|---|
dddd |
完整的星期几名称 |
ddd |
星期几的简写 |
MMM |
月份的简写名称 |
MMMM |
完整的月份名称 |
YYYY |
年份的正常格式(例如,2021) |
HH |
带前导零的小时数(24 小时制,十进制) |
hh |
带前导零的小时数(12 小时制,十进制) |
A |
AM 或 PM |
mm |
带前导零的分钟数(十进制) |
ss |
带前导零的秒数(十进制) |
你还可以使用arrow模块获取今天的日期和星期几信息,如get_date.py脚本所示:
import arrow
# Get today's date
1 today_date = arrow.now()
# Print today's date in different formats
2 print("today is", today_date.format('MMMM DD, YYYY'))
print("today is", today_date.format('MMM D, YYYY'))
print("today is", today_date.format('MM/DD/YYYY'))
# Print today's weekday in different formats
3 print("today is", today_date.format('dddd'))
print("today is", today_date.format('ddd'))
在第 1 步,我们使用now()生成当前日期和时间,并将其保存到字符串变量today_date中。在第 2 步,我们以“2021 年 1 月 1 日”的格式打印出日期,月份名称使用简写形式,日期则使用 MM/DD/YYYY 模式的数字格式。在第 3 步,我们打印出星期几,并再次使用简写形式。
该脚本生成类似如下的输出:
today is March 01, 2021
today is Mar 01, 2021
today is 03/01/2021
today is Monday
today is Mon
现在你知道如何在 Python 中获取时间了,接下来你将学习如何设置定时器。
构建一个定时器
我们将使用新的arrow模块技能和time模块中的sleep()函数来构建一个可以接受书面命令的计时器。你不会在你的 VPA 脚本中使用这个,但你会学到构建一个可以接受语音命令的计时器所需的技能。
我们将限制输入只能包含小时、分钟或小时和分钟(脚本不会接受秒)。因此,你可以将计时器设置为 2 小时后响起,或者 1 小时 30 分钟后响起,或者 20 分钟后响起,但不能设置为 1 小时 30 分钟 20 秒后响起。
在详细介绍脚本之前,我们先理解一下其背后的逻辑。你的书面命令应该是set a timer for 1 hour 20 minutes、set a timer for 2 hours或set a timer for 25 minutes的形式。然后,脚本将你的命令保存在字符串变量inp中。
字符串方法find()如果找不到你要查找的字符,则返回-1。我们将使用这个特性来提取inp中的小时和分钟值。
有三种情况:
-
inp.find("hour")的值不是-1,而inp.find("minute")的值是-1。这意味着minute不在变量inp中,但hour在。你已经将计时器设置为set a timer for 2 hours的形式。我们提取timer for和hour之间的小时数,并将分钟数设置为0。 -
inp.find("hour")的值是-1,而inp.find("minute")的值不是-1。这意味着minute在变量inp中,而hour不在。你已经将计时器设置为set a timer for 25 minutes的形式。我们提取timer for和minute之间的分钟数,并将小时数设置为0。 -
inp.find("hour")的值和inp.find("minute")的值都不是-1。这意味着hour和minute都在变量inp中。你已经将计时器设置为set a timer for 1 hour 20 minutes的形式。我们提取timer for和hour之间的小时数,以及hour和minute之间的分钟数。
我们将把这个时间添加到当前时间中,以确定计时器应何时响起。然后,我们每 0.5 秒检查一次时间,确保不漏掉计时器响起的时刻。当时间达到预设时间时,计时器响起。
计时器在timer.py中设置,见列表 7-4。
import time
import arrow
# Tell you the format to set the timer
print('''set your timer; you can set it to the number of hours,
number of minutes,
or a combination of both ''')
# Set the timer
1 inp = input("How long do you want to set your timer for?\n")
# Find the positions of "timer for" and "hour" and "minute"
pos1 = inp.find("timer for")
pos2 = inp.find("hour")
pos3 = inp.find("minute")
# Handle the case "set a timer for hours only"
2 if pos3 == -1:
Addhour = inp[pos1+len("timer for"):pos2]
Addminute = 0
# Handle the case "set a timer for minutes only"
3 elif pos2 == -1:
addhour=0
addminute = inp[pos1+len("timer for"):pos3]
# Handle the case for "set a timer for hours and minutes"
4 else:
Addhour = inp[pos1+len("timer for"):pos2]
Addminute = inp[pos2+len("hour"):pos3]
# Current hour, minute, and second
startHH = arrow.now().format('H')
startmm = arrow.now().format('m')
startss = arrow.now().format('s')
# Obtain the time for the timer to go off
newHH = int(startHH)+int(addhour)
newmm = int(startmm)+int(addminute)
5 if newmm>59:
newmm -= 60
newHH += 1
newHH = newHH%24
end_time = str(newHH)+":"+str(newmm)+":"+startss
print("Your timer will go off at "+end_time)
while True:
timenow = arrow.now().format('H:m:s')
if timenow == end_time:
print("Your timer has gone off!")
break
time.sleep(0.5)
列表 7-4:设置计时器的脚本
我们首先打印出指令。在步骤 1 中,脚本获取用户的书面输入,指定设置计时器的时间,然后将其保存到变量inp中。
然后我们检查输入中是否包含hour和minute。如果输入 2 中没有minute,我们将addminute的值设置为0,并将addhour的值设置为timer for和hour之间的数字。我们将使用类似的方法处理当书面命令中没有hour的情况 3,或同时包含hour和minute的情况 4。
arrow 模块中的 now() 函数获取当前的小时、分钟和秒值。我们将 addminute 和 addhour 的值加到当前时间上,得到计时器应该响铃的时间。5 时,我们会调整分钟值超过 59 或小时值超过 23 的情况。然后我们将闹钟响铃的时间设置为 H:m:s 格式。
我们启动一个无限的 while 循环,每 0.5 秒检查一次当前时间。当当前时间达到设定的闹钟时间时,我们触发闹钟。脚本会打印出 Your timer has gone off!,然后脚本结束。
这是与 timer.py 的交互示例,用户输入以粗体显示:
set your timer; you can set it to the number of hours,
number of minutes,
or a combination of both
How long do you want to set your timer for?
**set a timer for 1 minute**
Your timer will go off at 21:9:15
Your timer has gone off!
创建 mytimer 模块
现在我们将创建一个类似于 timer.py 脚本的 timer() 函数,但我们将使用语音命令而不是书面命令。
打开你刚刚从书籍资源网站下载的 mytimer.py 文件,并在 Spyder 编辑器中打开它。该模块将定义你的 VPA 将使用的 timer() 函数,如 列表 7-5 所示。
import time
import arrow
from mptpkg import print_say
def timer(v_inp):
# Find the positions of "timer for" and "hour" and "minute"
pos1= v_inp.find("timer for")
pos2= v_inp.find("hour")
`--snip--`
print_say("Your timer will go off at "+end_time)
`--snip--`
print_say("Your timer has gone off!")
`--snip--`
列表 7-5:本地 mytimer 模块的脚本
设置计时器
现在你将测试 VPA 的第一个功能。让我们放大到你可以在 VPA 脚本中激活计时器的部分:
`--snip--`
from mptpkg import timer
`--snip--`
# Activate the timer
elif "timer for" in inp and ("hour" in inp or "minute" in inp):
timer(inp)
continue
`--snip--`
首先,我们将 timer() 函数导入到脚本中。其次,在内嵌的 while 循环中,if 分支和 else 分支之间的 elif 分支是设置计时器的位置。
如果你运行 vpa.py,它将以待机模式启动。你可以通过说“Hello Python”唤醒它。然后,你可以通过说“Set a timer for 1 hour 20 minutes”或“Set a timer for 2 hours”来设置计时器。
以下是与脚本交互时的输出,我的语音输入以粗体显示:
**hello Python**
How may I help you?
**set a timer for 1 minute**
You just said set a timer for 1 minute
Your timer will go off at 21:37:46
Your timer has gone off!
How may I help you?
`--snip--`
如你所见,我首先激活了 VPA,然后设定了一个一小时的计时器。VPA 告诉我,“你的计时器将在 21:37:46 时响起。” 一分钟后,计时器响起。
请求你的 VPA 设置闹钟
现在你将学习如何让你的 VPA 设置闹钟。你将首先使用书面命令设置闹钟。然后你将创建一个 myalarm 模块,在其中定义 alarm() 函数。最后,你将把 alarm() 导入到 VPA 脚本中,通过语音命令设置闹钟。
构建一个闹钟
构建闹钟与设置计时器类似,只是我们指定闹钟响起的时间,而不是说它应该从现在起在某个时间响起。你可以单独指定小时值,例如 8 点,或指定小时和分钟的值,例如 7:25 am。
目前,脚本将接收书面命令。脚本 alarm_clock.py 如 列表 7-6 所示。
import time
import arrow
# Tell you the format to set the timer
print('''set your alarm clock\nyou can use the format of:\n
\tset an alarm for 7 a.m., or
\tset an alarm for 2:15 p.m.''')
# Set the alarm
1 inp = input("What time would you like to set your alarm for?\n")
# Find the positions of the four indicators
1 p1 = inp.find("alarm for")
p2 = inp.find("a.m.")
p3 = inp.find("p.m.")
p4 = inp.find(":")
# Handle the four different cases
2 if p2 != -1 and p4 != -1:
inp=inp[p1+len("alarm for")+1:p2]+"AM"
elif p3 != -1 and p4 != -1:
inp=inp[p1+len("alarm for")+1:p3]+"PM"
elif p2 != -1 and p4 == -1:
inp=inp[p1+len("alarm for")+1:p2-1]+":00 AM"
elif p3 != -1 and p4 == -1:
inp=inp[p1+len("alarm for")+1:p3-1]+":00 PM"
print(f"OK, your alarm will go off at {inp}!")
3 while True:
# Obtain time and change it to "7:25 AM" format
tm = arrow.now().format('h:mm A')
time.sleep(5)
# If the clock reaches alarm time, the alarm clock goes off
if inp == tm:
print("Your alarm has gone off!")
break
列表 7-6:设置闹钟的脚本
首先,脚本捕获我们的书面输入并将其保存为字符串变量 inp。然后我们查找四个指示符的位置:alarm for、a.m.、p.m. 和 : 1。如果你的输入中包含冒号,脚本会知道检查分钟值。
根据你在 2 处传递的内容,产生四种不同的结果:
-
你输入
a.m.并指定了小时和分钟值。我们提取预设的时间值,在set alarm for和a.m.之间,将其转换为字符串,并在末尾加上AM。例如,如果你输入set an alarm for 7:34 a.m.,返回的字符串值是7:34 AM。 -
你输入
p.m.并指定了小时和分钟值。我们提取预设的时间值,将其转换为字符串,并在末尾加上PM。例如,如果你输入set an alarm for 2:55 p.m.,返回的字符串值是2:55 PM。 -
你输入
a.m.但只指定了小时值。我们提取预设的时间值,将其转换为字符串,并在末尾加上:00 AM。例如,如果你输入set an alarm for 7 a.m.,返回的字符串值是7:00 AM。 -
你输入
p.m.但只指定了小时值。我们提取预设的时间值,将其转换为字符串,并在末尾加上:00 PM。例如,如果你输入set an alarm for 3 p.m.,返回的字符串值是3:00 PM。
一旦我们提取出闹钟应该响起的时间,我们开始一个无限循环 3。在每次迭代中,我们每五秒检查一次当前时间,格式为 7:25 AM。
最后,我们检查设置的闹钟时间是否与当前时间匹配。如果时间匹配,闹钟会响起,并且脚本会打印出Your alarm has gone off!。
运行脚本并用它为自己设置一个闹钟。尝试四种情况:有无分钟值,以及末尾是否有 a.m. 或 p.m.。接下来,我们将基于这个脚本创建一个闹钟模块。
创建闹钟模块
现在,我们将创建 alarm() 函数,它将使用 alarm_clock.py 代码。该代码将接受语音输入,而不是书面输入,并同时提供语音和文本输出。
打开你刚从书籍资源网站下载的 myalarm.py,并在 Spyder 编辑器中打开它。该脚本将定义虚拟助手使用的 alarm() 函数,见 Listing 7-7。
import time
import arrow
from mptpkg import print_say
# Define the Alarm() function
def alarm(v_inp):
# Find the positions of the four indicators
p1 = v_inp.find("alarm for")
`--snip--`
Listing 7-7:本地 myalarm 模块的脚本
设置闹钟
现在,你可以要求你的虚拟助手(VPA)为你设置一个闹钟。让我们放大看看 vpa.py 中能够设置闹钟的部分:
`--snip--`
from mptpkg import alarm
`--snip--`
# Activate the alarm clock
elif "alarm for" in inp and ("a.m." in inp or "p.m." in inp):
alarm(inp)
continue
`--snip--`
首先,我们从自制包 mptpkg 中的本地 myalarm 模块导入了 alarm() 函数到脚本中。其次,在内部的 while 循环中,我们添加了一个 elif 分支,通过在语音命令中包含 alarm for 和 a.m. 或 p.m. 来激活闹钟。
运行 vpa.py。你可以在唤醒虚拟助手后设置闹钟。以下输出是与脚本的一次交互,我的语音输入为粗体:
**hello Python**
How may I help you?
**set an alarm for 8:38 a.m.**
You just said set an alarm for 8:38 a.m.
OK, your alarm will go off at 8:38 AM!
Your alarm has gone off!
How may I help you?
`--snip--`
让你的虚拟助手讲个笑话
在本节中,你将学习如何要求虚拟助手讲笑话。你将找到一个好的笑话列表,然后创建一个笑话模块,并将其导入到你的主脚本中,这样你的虚拟助手就能用人声讲笑话给你听。
创建你的笑话列表
你可以从许多资源中创建笑话列表。我使用的是 Quick, Funny Jokes! 网站(www.quickfunnyjokes.com/math.html)。
我挑选了 15 个笑话并将它们保存在我电脑上章节文件夹/mpt/ch07/中的文件jokes.txt里。你可以使用任意数量的笑话,只要你也将它们保存在一个单独的文本文件中,就像我们在这里做的一样。以下是我的 15 个笑话:
There are three kinds of people in the world—those who can count and those who can't.
Without geometry, life is pointless.
Write the expression for the volume of a thick-crust pizza with height "a" and radius "z".
Two random variables were talking in a bar. They thought they were being discrete, but I heard their chatter continuously.
3 out of 2 people have trouble with fractions.
Parallel lines have so much in common . . . it's a shame they'll never meet.
Math is like love; a simple idea, but it can get complicated.
Dear Math, please grow up and solve your own problems; I'm tired of solving them for you.
Dear Algebra, Please stop asking us to find your X. She's never coming back, and don't ask Y.
Old mathematicians never die; they just lose some of their functions.
I strongly dislike the subject of math; however, I am partial to fractions.
Zenophobia is the irrational fear of convergent sequences.
Philosophy is a game with objectives and no rules. Mathematics is a game with rules and no objectives.
Classification of mathematical problems as linear and nonlinear is like classification of the universe as bananas and non-bananas.
A circle is just a round straight line with a hole in the middle.
接下来,你将详细了解如何创建讲笑话模块。
创建笑话模块
在这一部分,你将创建一个joke()函数。当这个函数被调用时,它会访问你电脑中的文件jokes.txt,获取内容并将其拆分成单个笑话,然后将它们放入一个列表。它接着会从这个列表中随机选择一个笑话,并大声读出来。
我们将把脚本myjoke.py(见列表 7-8)作为本地模块导入到你的 VPA 中。
1 import random
from mptpkg import print_say
# Define the joke() function
2 def joke():
# Read the content from the file jokes.txt
with open('../ch07/jokes.txt','r') as f:
content = f.read()
# Split the content at double line breaks
3 jokelist = content.split('\n\n')
# Randomly select a joke from the list
joke = random.choice(jokelist)
print_say(joke)
列表 7-8:创建一个joke模块的脚本
首先,我们导入random模块,我们将用它来从列表中随机选择一个笑话。在第 1 行,我们开始定义joke()。
然后,我们读取文件jokes.txt中的内容,并将其存储在字符串变量content中。请注意,由于我们将jokes.txt放在了与模块脚本myjoke.py不同的目录中,我们需要指定文件的路径,../ch07/告诉 Python 文件位于名为mptpkg的平行文件夹中。通过这种方式,我们还可以在其他章节中使用讲笑话的功能,这一点我们将在第十七章中实现。
我们知道单个笑话之间是通过双空行分隔的,因此我们使用split()将文件内容拆分成独立的字符串,并将它们放入列表jokelist中。接着,我们使用random模块中的choice()随机选择一个笑话。最后,脚本将选中的笑话打印出来并大声朗读。
讲笑话
现在,你将把刚创建的笑话模块导入到你的 VPA 中,这样它就能用人类的声音讲笑话给你听。让我们聚焦在vpa.py中讲笑话的部分:
`--snip--`
from mptpkg import joke
`--snip--`
# Activate the joke-telling functionality
elif "joke" in inp and "tell" in inp:
joke()
continue
`--snip--`
我们首先从本地mptpkg包中导入新建的myjoke模块中的joke()函数。在 VPA 代码的内部while循环部分,有一个elif分支,在这个分支中,我们告诉 VPA,如果语音命令中包含tell和joke,则激活讲笑话的功能。
这是我与脚本vpa.py进行一次交互的结果,我的输入部分用粗体标出:
**hello Python**
How may I help you?
**tell me a joke**
You just said tell me a joke
I strongly dislike the subject of math; however, I am partial to fractions.
How may I help you?
`--snip--`
发送免提电子邮件
在这一部分,我们将探讨如何实现完全免提的发送电子邮件功能。你将首先学习如何使用书面命令通过 Python 发送电子邮件;这将使你能够创建一个接受语音命令的电子邮件模块。之后,你将把这个电子邮件模块导入到你的 VPA 中,这样你就可以用语音发送电子邮件了。
发送带有书面命令的电子邮件
在继续之前,你需要准备一些东西。
首先,你需要一个电子邮件帐户来通过 Python 发送电子邮件。这个示例使用了我的 Gmail 账户,ukmarkliu@gmail.com,你应该用自己的电子邮件地址替换它。
Gmail 和许多其他邮件提供商要求你申请一个单独的应用程序密码,这与常规的电子邮件密码不同。例如,Google 账户帮助页面展示了如何设置 Gmail 应用密码;请参见support.google.com/accounts/answer/185833/。
在 Python 中发送电子邮件需要几个步骤。你首先需要连接到你的邮件提供商的简单邮件传输协议(SMTP)服务器。SMTP 是用于发送电子邮件的互联网标准。一旦连接建立,你需要使用电子邮件地址和密码登录。然后,你需要提供收件人的电子邮件地址、主题行和电子邮件内容。最后,你将请求 Python 发送实际的电子邮件。
smtplib模块在 Python 标准库中,因此无需安装。你还需要至少一个电子邮件地址作为收件人的地址。你可以使用另一个自己的电子邮件地址,或者请求一个朋友的地址。
脚本emails.py可以接收你输入的命令,并使用 Python 发送电子邮件,如清单 7-9 所示。
import smtplib
# Build a dictionary of names and emails
emails = {'mark':'mark.liu@uky.edu',
'sarah':'Sarah email address here',
'chris':'Chris email address here'}
# Different email providers have different domain names and port numbers
1 mysmt = smtplib.SMTP('smtp.gmail.com', 587)
mysmt.ehlo()
mysmt.starttls()
# Use your own login info; you may need an app password
mysmt.login('`ukmarkliu@gmail.com`', '`{Your password here}`')
# Ask for the name of the recipient
2 name = input('Who do you want to send the email to?\n')
email = emails[name]
print(f"You just said {name}.")
# Ask for the subject line
subline = input('What is the subject line?\n')
print(f"You just said {subline}.")
# Ask for the email content
content = input('What is the email content?\n')
print(f"You just said {content}.")
# Send the actual email
3 mysmt.sendmail('`ukmarkliu@gmail.com`', email,
f'Subject: {subline}.\nHello, {content}.')
{}
print('Ok, email sent')
mysmt.quit()
清单 7-9:使用 Python 发送电子邮件的脚本
我们导入了smtplib模块,并创建了一个字典emails,用于将姓名与电子邮件地址匹配。这样,当你输入一个人的名字时,脚本将从字典中检索对应的电子邮件。
在第 1 步,我们连接到 Gmail 的 SMTP 服务器。如果你不是使用 Gmail,你需要查找你的邮件提供商的域名和端口号。如果你使用 Gmail,则无需更改。
然后,我们开始与您的邮件服务器建立通信,并使用传输层安全性(TLS)加密。脚本需要 TLS 加密来保证安全性。一旦连接建立,你需要使用电子邮件地址和密码登录,因此请确保将ukmarkliu@gmail.com替换为你自己的电子邮件地址。我已经在代码中屏蔽了我的 Gmail 密码。
然后,脚本请求一些信息以发送电子邮件 2。它首先请求收件人的姓名,你必须已经将其存储在字典emails中,供脚本检索。通过姓名,脚本从字典中检索电子邮件。然后,它还会要求你输入电子邮件的主题行和内容,你将在屏幕右下角的 IPython 控制台中输入。
在第 3 步,我们使用sendmail()发送电子邮件,它需要三个输入:你的电子邮件地址;收件人的电子邮件地址;以及用换行符\n分隔的主题行和电子邮件内容。
完成后,脚本将确认电子邮件已发送。你可以自己尝试这个脚本,并确保你可以使用 Python 发送电子邮件。
接下来,我们将创建一个使用 Python 发送电子邮件的模块,然后将其添加到您的 VPA 中。
创建电子邮件模块
我们首先需要创建脚本myemail.py,将其用作 VPA 中的本地模块。在该模块中,我们定义了一个email()函数。调用该函数后,它将连接到您的电子邮件服务器,并要求您通过语音输入——收件人姓名、主题行和电子邮件内容——然后发送电子邮件。
myemail.py的内容类似于emails.py,有一些不同之处:脚本将通过语音输入和打印消息来请求您的输入,并且您需要使用语音输入而不是书面输入。这些不同之处在列表 7-10 中有突出显示。
`--snip--`
from mptpkg import voice_to_text, print_say
# Define the email() function
def email():
# Build a dictionary of names and emails
`--snip--`
# Voice input the name of the recipient
print_say('Who do you want to send the email to?')
name = voice_to_text().lower()
email = emails[name]
print_say(f"You just said {name}.")
# Voice input the subject line
print_say('What is the subject line?')
subline = voice_to_text()
print_say(f"You just said {subline}.")
# Voice input the email content
print_say('What is the email content?')
content = voice_to_text()
print_say(f"You just said {content}.")
# Send the actual email
mysmt.sendmail('ukmarkliu@gmail.com', email,
f'Subject: {subline}.\nHello, {content}.')
{}
print_say('Ok, email sent.')
mysmt.quit()
列表 7-10:创建本地myemail模块的脚本
正如您所看到的,您需要从本地mptpkg包中导入voice_to_text()来捕捉您的语音输入,以口述收件人的姓名、电子邮件主题行和内容。您还需要从本地mptpkg包中导入print_say()来打印并朗读消息。
现在,模块已经准备好,可以导入到 VPA 脚本中了。
添加电子邮件功能
接下来,您需要将email()从myemail.py导入到 VPA 中,这样您就可以 100%免提地发送电子邮件。让我们聚焦到vpa.py中负责发送电子邮件的部分:
`--snip--`
from mptpkg import email
`--snip--`
# Activate the email-sending functionality
elif "email" in inp and "send" in inp:
email()
continue
`--snip--`
我们从本地mptpkg包中导入本地myemail模块中的email()函数。在其中有一个elif分支,您可以激活发送电子邮件功能。
以下是与vpa.py的一个交互示例,我的语音输入为粗体显示。所有输出都会打印并大声朗读。
**hello Python**
How may I help you?
**send an email**
You just said send an email
Who do you want to send the email to?
**mark**
You just said mark.
What is the subject line?
**this is from python**
You just said this is from python.
What is the email content?
**this email is sent using the Python programming language**
You just said this email is sent using the Python programming language
Ok, email sent
How may I help you?
`--snip--`
首先,您应该唤醒 VPA。在您说“发送电子邮件”之后,电子邮件功能会被激活。VPA 会询问收件人的姓名——我说了我的名字,然后我的肯塔基大学(UKY)电子邮件地址与之匹配。它还会询问主题行和电子邮件内容。收集完信息后,电子邮件被发送,脚本退出电子邮件功能。
图 7-1 显示了我在 UKY 电子邮件账户中收到的电子邮件。

图 7-1:使用 Python 脚本 100%免提发送的电子邮件
总结
在本章中,您学会了如何创建一个可以设置闹钟和计时器、讲笑话甚至免提发送电子邮件的 VPA!您通过说“Hello Python”唤醒 VPA,然后给出指令以激活四个功能之一。本章教会了您如何创建新功能,将其制作成本地模块,并在主脚本中使用。
在下一章中,您将学习如何使用 WolframAlpha API,利用该网站的广阔知识空间,使您的 VPA 能够回答(几乎)任何问题。
章节末练习
-
编写一个脚本,打印出一条消息并大声朗读今天的日期和时间,格式为“今天是 2021 年 9 月 8 日,现在的时间是 09:03:07 AM。”
-
修改mywakeup.py,使得结束脚本vpa.py的唯一方式是说“退出脚本”。
无所不知的 VPA

我们在第七章创建的虚拟个人助理(VPA)可以为你设置计时器或闹钟,讲笑话,或者发送邮件。现在我们将对它进行升级,使你可以向它询问几乎任何问题——包括每日新闻和天气、油价以及旅行信息——并利用它几乎无限的科学、数学、历史和社会知识。
在本章中,你将访问计算引擎 WolframAlpha 的信息库,并在 WolframAlpha 无法提供答案时,使用 Wikipedia 作为备选。如果两个网站都无法回答,你的 VPA 将告诉你:“我还在学习,暂时不知道答案。” 你的 VPA 将会完善,能够回答几乎任何问题。
在开始之前,为本章设置文件夹/mpt/ch08/。像往常一样,本章中的所有脚本都可以在书籍资源页面找到。
从 WolframAlpha 获取答案
WolframAlpha是一个计算知识引擎,提供用于事实查询的在线服务,特别专注于数值和计算能力,尤其是在科学和技术领域。在本节中,你将学习如何通过 API 从 WolframAlpha 获取答案,然后编写一个 Python 脚本来检索信息。
申请 API 密钥
第一步是申请一个 API 密钥。WolframAlpha 为你提供每月最多 2,000 次免费的非商业 API 调用。前往account.wolfram.com/login/create/并按照步骤创建账户,如图 8-1 所示。

图 8-1:创建你的免费 Wolfram ID。
点击创建 Wolfram ID,然后登录。Wolfram ID 本身只提供浏览器访问权限,因此你需要获取一个 AppID 才能使用 Python 进行查询。前往products.wolframalpha.com/api/申请 API,并点击左下角的获取 API 访问权限,如图 8-2 所示。

图 8-2:在 WolframAlpha 申请 API。
应该会弹出一个小对话框,如图 8-3 所示。

图 8-3:WolframAlpha 获取新 AppID 窗口
填写应用名称和描述信息,然后点击获取 AppID。例如,你可以在应用名称字段中输入虚拟助手,在描述字段中输入学习如何用 Python 构建自己的虚拟个人助手。
之后,你的 AppID 应该会出现在弹出窗口中。你需要点击确定来激活 AppID。这个密钥将是一个长且独特的字符字符串,用于区分其他用户,类似于HG**************YQ(我已遮挡中间的字符)。将你的 AppID 保存在安全的地方;你稍后会用到它。
获取信息
一旦你获得了 WolframAlpha API,你就可以使用 Python 脚本发送查询并从 WolframAlpha 获取答案。你必须首先在电脑上安装第三方 wolframalpha 模块。进入 Anaconda 提示符(Windows)或终端(Mac 或 Linux),激活虚拟 chatting 环境;然后在命令行中运行以下命令:
**pip install wolframalpha**
按照说明完成安装。
清单 8-1 中的 wolfram.py 脚本通过使用文本输入从 WolframAlpha 获取信息。
# Import the wolframalpha module
import wolframalpha
# Enter your own WolframAlpha APIkey below
APIkey = "`{your WolframAlpha APIkey}`"
wolf = wolframalpha.Client(APIkey)
# Enter your query
1 inp = input("What do you want to know from WolframAlpha?\n")
# Send your query to WolframAlpha and get a response
2 response = wolf.query(inp)
# Retrieve the text from the response
res = next(response.results).text
# Print out the response
print(res)
清单 8-1:用于脚本 wolfram.**py 的 Python 代码
我们首先导入 wolframalpha 模块。将你之前获得的 API 密钥作为 APIkey 变量的值输入。如果没有它,脚本将无法工作。
然后,我们使用你的 AppID 创建客户端。在第 1 步,脚本会要求用户输入一个查询,该查询将通过 IPython 控制台在 Spyder IDE 的右下面板中输入。
在第 2 步,我们将查询发送给 WolframAlpha,并检索 result 对象,将其保存在变量 response 中。result 对象包含一个生成器对象中的多个结果。生成器函数 是构建迭代器的便捷快捷方式,有时用于避免将大量数据保存在短期内存(RAM)中。你可以从权威的在线资源(例如,wiki.python.org/moin/Generators)了解更多关于生成器的内容。这就是我们为何使用内置函数 next() 来遍历来自 WolframAlpha 的不同答案组,并提取答案的文本部分。关于如何使用 wolframalpha 模块进行查询过程的详细描述,请参考 pypi.org/project/wolframalpha/。最后,提取的文本将被打印出来。
这是与 wolfram.py 的一个简单交互,输入的文本为加粗部分:
What do you want to know from WolframAlpha?
**How many states are in the USA?**
50
正如你所见,WolframAlpha 给出了一个正确且简洁的答案。
探索不同领域的知识
WolframAlpha 能提供关于多种主题的信息,因此我们将通过提问天气、常识、科学和数学问题来检验 wolfram.py,然后将 API 集成到你的 VPA 中。
实时信息
WolframAlpha 提供实时信息,例如你所在地区的当前温度。下面是与脚本 wolfram.py 的一次交互,我的输入文本加粗:
What do you want to know from WolframAlpha?
**What is the temperature outside right now?**
87 °F
(2 hours 21 minutes ago)
脚本会告诉你以华氏度表示的温度以及自信息获取以来经过的时间。WolframAlpha 通过查看与你的 IP 地址关联的位置来获取你的本地信息。如果你启用了虚拟专用网络(VPN),则本地信息将显示为你 VPN 提供商的位置。
你还可以获取某个地点特定日期的天气预报,方法如下:
What do you want to know from WolframAlpha?
**What is the weather forecast for Chicago in 2 days?**
between 70 °F and 74 °F
rain (very early morning) | clear (all day)
你还可以查询其他实时信息,如本地油价或美国通货膨胀率:
What do you want to know from WolframAlpha?
**What is the current gas price?**
$2.548/gal (US dollars per gallon) (Monday, February 8, 2021)
一般性问题
你可以询问一般知识类问题,比如一杯多少茶匙,如何将华氏温度转换为摄氏温度,地方销售税率,州首府等等:
What do you want to know from WolframAlpha?
**How many yards are in a mile?**
1760 yards
What do you want to know from WolframAlpha?
**What's the capital of West Virginia?**
Charleston, West Virginia, United States
What do you want to know from WolframAlpha?
**What is the calorie expenditure walking an hour at 5 miles per hour?**
energy expenditure | 366 Cal (dietary calories)
fat burned | 0.1 lb (pounds)
oxygen consumption | 19.3 gallons
metabolic equivalent | 4.8 metabolic equivalents
(estimates based on CDC standards)
What do you want to know from WolframAlpha?
**What is the speed of light?**
2.998×10⁸ m/s (meters per second)
WolframAlpha 收集了来自各种来源的信息,例如 CIA 的世界事实手册和美国地质调查局,因此它拥有全面的历史数据。你可以问关于事件、人物或事实的问题,比如车辆安全气囊是什么时候发明的:
What do you want to know from WolframAlpha?
**When was the airbag invented**
1941
你甚至可以通过使用define将 WolframAlpha 当作词典来使用,方法如下:
What do you want to know from WolframAlpha?
**Define obliterate**
1 | verb | mark for deletion, rub off, or erase
2 | verb | make undecipherable or imperceptible by obscuring or concealing
3 | verb | remove completely from recognition or memory
4 | verb | do away with completely, without leaving a trace
5 | adjective | reduced to nothingness
(5 meanings)
数学计算
WolframAlpha 可以回答你在数学、科学和技术领域的问题,涵盖从基础数学到微积分,再到常微分方程等内容。
例如,如果你想将 125 转换为二进制,你可以如下使用wolfram.py:
What do you want to know from WolframAlpha?
**convert 125 to binary**
1111101_2
输出末尾的2表示响应是二进制格式。WolframAlpha 还可以回答关于个人财务的问题,比如抵押贷款支付、信用卡计算和州税等。例如,要计算每月的抵押贷款支付,你只需要提供三项信息——贷款金额、利率和贷款期限——你就能得到答案:
What do you want to know from WolframAlpha?
**mortgage $150,000 6.5% 30 years**
monthly payment | $948
使用关键字mortgage,你告诉脚本贷款金额$150,000,利率6.5%,以及期限30 年。请注意,你查询的格式并不重要——你不需要在数字中使用逗号,参数的顺序也可以随意,脚本应该能够理解。
为你的虚拟个人助理(VPA)添加一个全能功能
我们在这里的目标是为你在第七章创建的虚拟个人助理(VPA)添加一个全能功能。我们主要依赖 WolframAlpha 来回答你的问题,但 WolframAlpha 也不能回答所有问题。在这种情况下,我们将搜索维基百科。如果维基百科也无法提供答案,VPA 将告诉你它没有答案。
为了使用下一个脚本,确保在虚拟环境激活的情况下安装以下包:
**pip install wikipedia**
WolframAlpha 无法回答的问题
尽管 WolframAlpha 拥有庞大的知识库,但它并不能回答所有问题。在某些领域,尤其是一般性参考问题,维基百科提供的答案比 WolframAlpha 更多。例如,如果你在wolfram.py中输入University of Kentucky作为查询,脚本将抛出一个StopIteration异常。这是因为next()无法在任何答案组中找到结果。
另一方面,如果你运行第五章中的脚本wiki.py并输入University of Kentucky作为查询,你将得到以下输出:
The University of Kentucky (UK) is a public university in Lexington,
Kentucky. Founded in 1865 by John Bryan Bowman as the Agricultural and
Mechanical College of Kentucky
`--snip--`
维基百科也不能回答你所有的问题。例如,如果你在wiki.py中输入how many people live outside the earth作为查询,API 将抛出一个PageError异常,导致该版本的脚本以错误状态突然结束。
我们将通过编写一个脚本来改进我们的 VPA,该脚本首先查询 WolframAlpha,如果没有找到结果,则查询 Wikipedia。如果在 Wikipedia 上也找不到答案,脚本将打印出 I am still learning. I don't know the answer to your question yet. 我们将通过将调用放在 try 块中并在 except 块中处理异常来处理这些外部 API 引发的错误。
访问书本的资源页面,下载 know_all.py 并将其保存在章节文件夹中。该脚本显示在 列表 8-2 中。
import wolframalpha
import wikipedia
# You must put your WolframApha APIkey below
1 APIkey = "{your WolframAlpha appID here}"
wolf = wolframalpha.Client(APIkey)
while True:
# Put your question here
Inp = input("What do you want to know?\n")
# Stop the loop if you type in "done"
if inp == "done":
break
# Look for answer in Wolfram Alpha
res = wolf.query(inp)
# Use try and except to handle errors
try:
print(next(res.results).text)
except:
# If no answer, try Wikipedia
try:
ans = wikipedia.summary(inp)
print(ans[0:200])
except:
# If still no answer
print('I am still learning. I don\'t know the answer to your question yet')
列表 8-2:脚本 know_all.py 的 Python 代码
我们首先导入了两个模块 wolframalpha 和 wikipedia。在第 1 行,你应该将你自己的 WolframAlpha AppID 放入脚本中,以使其正常工作。然后,我们将脚本放入一个无限的 while 循环中。在每次迭代中,它将你的文本输入作为查询。如果你输入 done 这个词,while 循环将停止,脚本结束。
脚本首先将查询发送到 WolframAlpha。我们使用 try 和 except 来处理 WolframAlpha API 可能引发的任何错误。如果 WolframAlpha 没有返回答案,脚本将同样的查询发送到 Wikipedia。如果 Wikipedia 上也没有找到答案,脚本会打印 I am still learning. I don't know the answer to your question yet.
现在,如果你运行脚本 know_all.py 并输入 University of Kentucky 和 How many people live outside the earth? 作为两个查询,你将得到以下输出:
What do you want to know?
**University of Kentucky**
The University of Kentucky (UK) is a public university in Lexington, Kentucky. Founded in 1865 by John Bryan Bowman as the Agricultural and Mechanical College of Kentucky, the university is one of the
What do you want to know?
**How many people live outside the earth?**
I am still learning. I don't know the answer to your question yet
What do you want to know?
**done**
如你所见,脚本不会崩溃,它为第一个查询提供了结果,但第二个查询没有结果。
创建 myknowall 模块
现在我们将创建 know_all() 函数,该函数将使用脚本 myknowall.py,但这次将接受语音命令,而不是书面命令,并且将同时打印和说出响应,而不仅仅是打印消息。
从书本资源中下载 myknowall.py 并将其保存在本地包文件夹 /mpt/mptpkg/ 中。由于我们将把它作为本地包中的一个本地模块使用,请确保将其保存在本地包文件夹中,而不是章节文件夹中。该脚本将定义 VPA 使用的 know_all() 函数,简化版本显示在 列表 8-3 中。
`--snip--`
# Import the print_say() function from the local package
from mptpkg import print_say
`--snip--`
def know_all(v_inp):
#look for answer in Wolfram Alpha
res = wolf.query(v_inp)
`--snip--`
print_say('I am still learning. I don\'t know the answer to your question yet')
列表 8-3:本地 myk**nowall 模块的脚本
know_all() 的内容与脚本 know_all.py 类似,只是输入和输出包括了语音。
一个可以回答(几乎)任何问题的 VPA
现在,你将使你的 VPA 能够回答(几乎)任何问题,使用 know_all.py 模块。
首先,打开你电脑上包目录 /mpt/mptpkg/ 中的脚本 init.py 文件。在文件末尾添加以下代码行并保存更改:
**from .myknowall import know_all**
这段代码从 myknowall 模块导入 know_all() 到本地包中,以便稍后可以在包级别导入它。
接下来,打开上一章的 vpa.py,在脚本中添加以下内容,并将其保存为本章文件夹中的 vpa.py。你需要删除内部 while 循环中的原 else 分支,并用以下内容替换:
# Import the know_all() function from the local package
from mptpkg import know_all
`--snip--`
# Activate the Know-It-All functionality
else:
if len(inp)>6:
know_all(inp)
continue
`--snip--`
我们从本地的 mptpkg 包中导入 know_all(),并替换原来的 else 分支。在第七章的 vpa.py 中,如果没有激活四种功能,脚本会进入下一次迭代。而在新的 vpa.py 脚本中,如果四种功能都没有激活,则会启用知无不言的功能,默认情况下,脚本会在 WolframAlpha 和 Wikipedia 中搜索答案。
注意,这里我们在调用 know_all() 之前添加了条件 if len(inp)>6。如果没有这个条件,如果你长时间什么都不说,脚本会将输入视为空字符串。结果,你会不断听到回答 I am still learning. I don't know the answer to your question yet. 有了这个条件,如果你没有说任何话,脚本会进入下一次迭代,而不会执行任何操作,因为空字符串的长度为 0。
运行 vpa.py 并通过说“Hello Python”来唤醒它。之后,你可以问任何你想问的问题。以下是与脚本交互的示例输出,我的语音输入以粗体显示:
**hello Python**
how may I help you?
**who was us president in 1981**
you just said who was us president in 1981
Jimmy Carter (from January 20, 1977 to January 20, 1981)
Ronald Reagan (from January 20, 1981 to January 20, 1989)
how may I help you?
**coronavirus**
you just said coronavirus
Coronaviruses are a group of related RNA viruses that cause diseases in mammals and birds. In humans, these viruses cause respiratory tract infections that can range from mild to lethal. Mild illness
`--snip--`
如你所见,激活 VPA 后,我首先问了 1981 年美国总统是谁。答案包括了两位总统,因为权力交接发生在 1981 年 1 月。之后,我问了关于冠状病毒的问题。VPA 提供了详细的回答。
概述
在本章中,你将第七章中的 VPA 进行了升级,现在你可以问它几乎任何问题——包括有关天气、油价、旅行情况的最新信息,以及关于科学、数学、历史和社会的几乎无限的事实。
你学会了如何申请 API 并访问计算引擎 WolframAlpha 中的庞大知识库,当 WolframAlpha 无法提供答案时,你还可以使用 Wikipedia 作为备选。如果这两个网站都无法回答,你的 VPA 会告知你。到此为止,你的 VPA 已经完成,能够为你回答几乎所有问题。使用这样的 API 是一项极为强大的技能。
在接下来的几章中,你将学习如何创建自己控制语音的图形游戏,游戏能够与你对话。
第三部分:互动游戏
使用 turtle 模块进行图形和动画

我们在接下来的几章中的目标是构建语音控制的图形游戏,如井字棋、四连棋和猜单词。你将使用 turtle 模块来完成所有这些。
在这一章,你将不会使用语音交互。相反,你将学习 turtle 模块的基本命令,这些命令将帮助你设置海龟屏幕、绘制形状和创建动画。这些功能将成为你将要构建的所有游戏的基础。
在开始之前,为本章设置文件夹 /mpt/ch09/。和往常一样,本章中的所有脚本都可以在本书的资源页面找到,www.nostarch.com/make-python-talk/。
基本命令
turtle 模块允许我们使用一个机器人海龟在画布上绘制图形并创建动画。海龟模仿了人们在物理画布上绘画的方式,但我们使用命令来移动海龟并创建图画。
对于其底层图形,turtle 模块使用了 tkinter 模块,这是 Python 的事实标准图形用户界面(GUI)包。turtle 和 tkinter 都包含在 Python 标准库中,因此无需安装它们。
海龟图形是在 1960 年代发明的,比 Python 语言早了三十年。turtle 模块允许 Python 程序员利用海龟图形的许多特性。第一个特性是它的简单性:turtle 比其他游戏模块,如 pygame 或 tkinter,更容易学习。turtle 模块还很直观,使得通过操控画笔在画布(即屏幕)上绘制图片和形状变得简单。
turtle 模块也更适合语音激活。与其他游戏模块不同,后者通常通过游戏循环运行得太快,无法捕捉语音命令,turtle 脚本不需要游戏循环。这使得语音控制的游戏成为可能。
创建一个海龟屏幕
要使用 turtle,你需要创建一个海龟屏幕来容纳脚本中的所有对象。下面的脚本向你展示了一个简单的海龟屏幕示例。在 Spyder 中输入以下代码行,并将脚本保存为 set_up_screen.py:
import turtle as t
1 t.Screen()
t.setup(600,500,100,200)
t.bgcolor('SpringGreen3')
2 t.title('Setting Up a Screen with Turtle Graphics')
t.done()
t.bye()
我们导入 turtle 模块并为其指定一个简短的别名 t。这是一个简短别名有利的情况,因为我们将多次调用模块中的多个函数。因此,我们希望在所有函数前面只使用 t.,而不是 turtle.。
在步骤 1,我们通过使用Screen()创建一个屏幕,这不需要任何参数。接着我们使用setup()来指定屏幕的大小和位置。四个参数依次是屏幕宽度、屏幕高度、距离计算机屏幕左上角的水平距离以及距离计算机屏幕左上角的垂直距离。我们的屏幕将是 600 像素宽、500 像素高,距离计算机屏幕左边缘 100 像素,距离上边缘 200 像素。
接下来,我们通过使用bgcolor()为海龟屏幕设置背景颜色。turtle模块提供了多种颜色,包括brown(棕色)、black(黑色)、gray(灰色)、white(白色)、yellow(黄色)、gold(金色)、orange(橙色)、red(红色)、purple(紫色)、navy(海军蓝)、blue(蓝色)、lightblue(浅蓝色)、darkblue(深蓝色)、cyan(青色)、turquoise(绿松石色)、lightgreen(浅绿色)、green(绿色)和darkgreen(深绿色)。
在步骤 2,我们为屏幕添加一个标题,你可以在屏幕顶部看到它,靠近海龟图形符号(图 9-1)。
done()命令告诉脚本开始事件,这样屏幕上的对象就可以进行动画处理。bye()命令则告诉脚本在你点击 X 符号时退出turtle模块。
屏幕应该看起来像图 9-1 所示。

图 9-1:设置屏幕的大小、背景颜色和标题。
海龟屏幕使用的是笛卡尔坐标系,坐标原点为(x = 0, y = 0)。x 值从左到右递增,y 值从下到上递增,就像你在高中数学中学到的二维平面。
创建动作
在早期,海龟光标实际上是一个在屏幕上移动的海龟图像。现在,默认的光标是一个小箭头,取代了原来的海龟图像。海龟有三个属性:位置、方向和画笔。你可以调整画笔的颜色和宽度,还可以决定是否将画笔放在平面上,这样海龟移动时就会在其路径上留下痕迹,或者将画笔抬起,这样就不会跟踪移动。
在我们查看模块中的各种动作之前,先看看实际的绘图。在 Spyder 编辑器中输入示例 9-1 中的代码,并将其保存为show_turtle.py,放在你的章节文件夹中。
import turtle as t
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('SpringGreen')
t.title('Show Turtle')
1 t.shape('turtle')
t.forward(200)
t.right(90)
t.up()
t.forward(100)
t.done()
t.bye()
示例 9-1:显示turtle模块中的海龟
在步骤 1,我们将光标的形状改回原始的海龟形状,正如图 9-2 所示。如果你运行脚本,你会看到海龟从位置(x = 0, y = 0)开始并朝右移动。它在默认的放下画笔状态下向前移动 200 像素,因此在画布上绘制了一条线。然后我们将海龟右转 90 度并抬起画笔,再向前移动 100 像素。这次由于画笔没有接触到画布,因此不会绘制线条。

图 9-2:海龟在画布上移动进行绘图。
现在,我们将详细讨论一些在turtle模块中非常有用的基本动作,这些动作对我们的项目非常重要。
forward() 和 backward() 函数
forward() 函数让乌龟在屏幕上向前移动指定的像素数。backward() 函数做相同的操作,只不过是向后移动。在 Spyder 编辑器中输入 列表 9-2 中显示的代码,并将其保存为 forward_backward.py 文件到您的章节文件夹中。
import turtle as t
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('blue')
t.title('Movements in Turtle Graphics')
1 t.forward(200)
2 t.backward(300)
t.done()
t.bye()
列表 9-2:turtle 模块中的基本运动函数
我们设置了一个不同背景颜色和标题的屏幕。在 1 处,乌龟前进 200 像素。乌龟的默认起始位置是 (x = 0, y = 0),面朝右侧,因此前进 200 像素后,乌龟的位置将到达 (x = 200, y = 0)。
在 2 处,乌龟从点 (x = 200, y = 0) 向后移动 300 像素,最终停在 (x = -100, y = 0) 处。
left() 和 right() 函数
left() 或 right() 函数改变乌龟的方向。我们给定一个角度值作为参数,用以表示乌龟要转动的角度。例如,90 度让乌龟转向与原方向垂直的方向。360 度的角度让乌龟旋转一圈,从而仍然朝向原来的方向。
列表 9-3 中的脚本 left_right.py 展示了 left() 和 right() 函数的用法。
import turtle as t
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('light blue')
t.title('Python Turtle Graphics')
1 t.pensize(5)
2 t.right(30)
t.forward(200)
t.left(30)
t.backward(400)
t.left(90)
3 t.pencolor('red')
t.forward(200)
t.done()
try:
t.bye()
except Terminator:
print('exit turtle')
列表 9-3:left_right.py 的 Python 代码
pensize() 函数指定乌龟绘制的线条粗细,默认值为 1 像素。在这里,我们将笔的大小设置为 5 像素。在 2 处,我们让乌龟右转 30 度。然后,我们让乌龟前进 200 像素。接着,我们让乌龟左转 30 度并后退 400 像素。
pencolor() 函数将绘图笔的颜色改为红色 3,默认颜色为黑色。经过这一步,绘制的线条将变为红色,而不是黑色。
运行脚本后,您应该会看到类似于 图 9-3 的屏幕。

图 9-3:turtle 模块中的 left() 和 right() 函数
goto() 函数
goto() 函数告诉乌龟移动到屏幕上的指定点。结合 up() 和 down(),它可以绘制直线和虚线。up() 函数意味着乌龟的笔没有接触到画布,因此在移动时不会绘制任何内容。down() 函数则将笔放在画布上,开始绘图。
如果乌龟的笔处于下方位置,goto() 将在当前位置和指定位置之间绘制一条直线。然而,如果乌龟的笔处于上方位置,goto() 将不会在屏幕上绘制任何东西,而只是将乌龟从当前位置移动到指定位置。通过绘制一系列短线并在它们之间留空,可以创建虚线。
在 列表 9-4 中输入脚本 create_lines.py。
import turtle as t
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('lightgreen')
t.title('Python Turtle Graphics')
t.pensize(6)
1 t.goto(200,100)
2 t.up()
t.pencolor('blue')
3 for i in range(8):
t.goto(-200+50*i,-150)
t.down()
t.goto(-200+50*i+30,-150)
t.up()
4 t.hideturtle()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
列表 9-4:create_lines.py 的 Python 代码
在第 1 步,我们告诉乌龟移动到 (x = 200, y = 100)。默认情况下,乌龟是处于下笔状态,起始位置是 (x = 0, y = 0),因此 goto(200,100) 会在两个点 (0, 0) 和 (200, 100) 之间画一条线,如图 9-4 所示。
在第 2 步,脚本指示乌龟抬起画笔,这样乌龟移动到另一个点时,屏幕上不会画出任何线条。然后我们将画笔颜色改为蓝色。在第 3 步,我们开始一个 for 循环。在每次迭代中,乌龟会到达一个点,放下画笔,然后移动到右边 30 像素的地方。这样就会留下一个 30 像素长的短线,重复八次,之间有间隙。
hideturtle() 函数可以隐藏乌龟,使得屏幕上不显示黑色的箭头光标 4。
运行脚本后,你应该能看到一个类似于 图 9-4 的屏幕。

图 9-4:使用 goto() 函数通过 turtle 模块绘制直线。
基本形状
turtle 模块有多个内建的形状,包括常用的 dot() 函数,它可以创建圆点。你还将学习如何创建基本形状,如三角形、正方形和网格线。
使用 dot() 函数
dot() 函数创建一个指定直径和颜色的圆点。例如,命令 dot(30, 'red') 会创建一个直径为 30 像素的红色圆点。我们将在井字游戏和四子棋游戏中使用这个功能来创建游戏棋子。
示例 9-5,dots.py 展示了 dot() 函数的使用方法。
import turtle as t
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('lightgreen')
t.title('Python Turtle Graphics')
1 t.up()
t.goto(150,100)
t.dot(120,'red')
t.goto(-150,100)
t.dot(135,'yellow')
2 t.goto(150,-100)
t.dot(125,'blue')
t.goto(-150,-100)
t.dot(140,'green')
t.hideturtle()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
示例 9-5:dots.py 的 Python 代码
首先我们抬起画笔 1。然后我们移动到点 (150, 100)。我们告诉乌龟在点 (150, 100) 处画一个直径为 120 像素的红色圆点。
接下来,我们将乌龟移到 (–150, 100),并画一个直径为 135 像素的黄色圆点。请注意,你不需要再次使用 up(),因为画笔已经抬起。画笔抬起时,乌龟依然可以画圆点。
从第 2 步开始,乌龟会到达 (150, –100) 并画出一个直径为 125 像素的蓝色圆点。然后它移动到 (–150, –100) 并画出一个直径为 140 像素的绿色圆点。图 9-5 展示了最终效果。

图 9-5:使用 turtle 模块创建圆点。
绘制自己的图形
你也可以使用 turtle 模块绘制自己的图形。我们将在这里介绍一些基本形状。
三角形
创建三角形的最简单方法是使用 goto()。示例 9-6 中,triangle.py 绘制了一个三角形,三个角的坐标分别为 (–50, –50)、(50, –50) 和 (0, 100)。
from turtle import *
Screen()
setup(600,500,100,200)
bgcolor('springgreen3')
title('Python Turtle Graphics')
hideturtle()
tracer(False)
1 pencolor('blue')
pensize(5)
up()
goto(-50,-50)
down()
goto(50,-50)
goto(0,100)
goto(-50,-50)
update()
done()
try:
bye()
except Terminator:
pass
示例 9-6:t**riangle.py 的 Python 代码
tracer() 函数告诉脚本是否追踪乌龟的运动。默认值是 tracer(True),意味着脚本会逐步展示乌龟的移动。当乌龟画出一些内容时,你将看到每一笔的绘制过程。这里我们使用 tracer(False),因此最终的图形会被打印出来,但脚本不会显示中间的步骤。
我们将笔的颜色改为蓝色 1,大小设置为 5。然后抬起笔,移动到点(–50, –50),再放下笔,移动到点(50, –50)。这形成了三角形的第一条边。笔下后,我们让海龟移动到点(0, 100),形成第二条边。最后,我们将笔移动回点(–50, –50),完成三角形的底边。
注意,由于我们使用了命令tracer(False)来禁用每一步绘图的显示(从而节省时间),因此在脚本末尾需要使用update()来显示完整的图像,如图 9-6 所示。

图 9-6: 使用 turtle 模块绘制三角形。
矩形
我们可以像绘制三角形一样,使用goto()绘制矩形,但我们也可以使用forward()和left()。在许多情况下,你可以通过使用goto()函数或forward()和left()函数来实现相同的目标。如果你知道目的地的坐标,goto()更简单;如果你知道两点之间的距离,方向函数则更易用。
在这里,我们将使用forward()和left()。你也可以通过使用goto()来实现相同的结果,详见第 187 页的“章节末练习”部分。
我们将绘制一个矩形,四个顶点为(0, 0)、(200, 0)、(200, 100)和(0, 100)。请输入列表 9-7 中的 rectangle.py 脚本。
import turtle as t
# Set up the screen
t.Screen()
t.setup(600,500,100,200)
t.bgcolor('green')
t.title('Python Turtle Graphics')
t.hideturtle()
t.tracer(False)
1 t.pensize(6)
# Draw the first side
2 t.forward(200)
t.left(90)
# Draw the second side
t.forward(100)
t.left(90)
# Draw the third side
t.forward(200)
t.left(90)
# Finish the rectangle
t.forward(100)
t.update()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
列表 9-7: rectangle.py 的 Python 代码
我们首先设置屏幕。在第 1 步,我们将笔的大小设置为 6。由于没有指定笔的颜色,因此使用默认的黑色。在第 2 步,海龟从初始位置(0, 0)向前移动 200 像素,形成矩形的第一条边。
接下来,海龟向左转 90 度,面朝上方。然后它向前移动 100 像素,形成第二条边。接着,我们让海龟向左转 90 度,面朝西方,并向前移动 200 像素,形成第三条边。矩形的最后一条边以类似方式绘制。
输出结果如图 9-7 所示。

图 9-7: 使用 turtle 模块绘制矩形。
我们将使用这种绘制矩形的技巧来为即将到来的游戏创建一个棋盘。
绘制网格线
像井字游戏和四连棋这样的游戏都使用网格。我们可以通过绘制方形来简单地创建网格。这里,我们将绘制一个包含六行七列的游戏棋盘;水平线将比垂直线更细、更浅,以符合我们在四连棋游戏中所做的设计。请输入列表 9-8 中的 grid_lines.py 代码。
import turtle as t
# Set up the screen
t.Screen()
t.setup(810,710, 10, 70)
t.hideturtle()
t.tracer(False)
t.bgcolor('lightgreen')
# Draw the vertical lines to create 7 columns
1 t.pensize(5)
for i in range(-350,400,100):
t.up()
t.goto(i, -298)
t.down()
t.goto(i, 303)
t.up()
# Draw the horizontal lines to separate the screen in 6 rows
2 t.pensize(1)
t.color('gray')
for i in range(-300,400,101):
t.up()
t.goto(-350,i)
t.down()
t.goto(350,i)
t.up()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
列表 9-8: grid_lines.py 的 Python 代码
我们首先设置屏幕。由于我们计划绘制一个包含六行七列的游戏棋盘,我们将屏幕大小设置为 810 像素宽、710 像素高。这样,我们可以将每个单元格设置为 100×100 像素,棋盘四周留有 55 像素的边距。考虑屏幕大小非常重要,这样可以帮助你计算出各个点的坐标。
我们用笔的大小为 5 1 画了八条粗的垂直线,将屏幕划分为七列。函数 range(-350,400,100) 会产生八个值:-350, -250, ..., 350。
之后,我们画七条细的灰色水平线,形成六行 2。如果你运行脚本,你会看到一个类似于 图 9-8 的屏幕。
我们将在第十一章的游戏中使用这个板。
动画
在这一部分,你将学习如何使用 clear() 和 update() 来清除当前图像并用下一个图像替换,从而产生动画帧。

图 9-8: 绘制网格线以形成一个六行七列的游戏板
如何动画工作
clear() 函数会清除海龟在屏幕上画的所有内容。然后你可以重新绘制对象,并使用 update() 将它们显示在屏幕上。如果你重复这个过程,图像的快速替换将产生动画效果。
我们将通过制作一个简单的时钟来探索动画,时钟在 示例 9-9 的 turtle_clock.py 中展示。
import turtle as t
import time
import arrow
# Set up the screen
t.setup(800,600, 10, 70)
t.tracer(False)
t.bgcolor('lightgreen')
t.hideturtle()
# Put the script in an infinite loop
1 while True:
# Clear the screen
t.clear()
# Obtain the current time
current_time = arrow.now().format('hh:mm:ss A')
t.color('blue')
t.up()
t.goto(-300,50)
# Write the first line of text
2 t.write('The Current Time Is\n',font=('Arial',50,'normal'))
t.color('red')
t.goto(-300,-100)
# Write what time it is
3 t.write(current_time,font=('Arial',80,'normal'))
time.sleep(1)
# Put everything on screen
t.update()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
示例 9-9: turtle_clock**.py 的 Python 代码
我们导入模块并设置屏幕。在 1 处,我们开始一个无限循环。在每次迭代中,脚本首先通过使用 clear() 清除屏幕上的所有内容。然后我们通过使用 arrow 模块获取当前时间,并将值存储在变量 current_time 中。
turtle 模块的 write() 函数将在屏幕上写文本。它的第一个参数是要显示的文本,第二个参数是使用的字体。在 2 处,我们用蓝色在屏幕上写 The Current Time Is。在 3 处,脚本用红色写下当前时间。
然后脚本暂停一秒,并确保通过使用 update() 更新所有新的绘图。如果你运行脚本,你会注意到时间每秒都会变化(见 图 9-9)。

图 9-9: 在 turtle 模块中创建动画
我们将在各种游戏中频繁使用这种方法来创建动画。
使用多个海龟
现在我们来看一下如何同时使用两个海龟——相当于使用两支笔。在第十二章,当我们创建一个猜词游戏时,我们将使用一个海龟在游戏板上画一个金币,另一个用来计算玩家剩余的机会次数。每当玩家错过一个字母时,我们将擦除之前的数字,并用新数字替换。如果我们只使用一个海龟,那么所有内容,包括金币图像都会被清除。如果我们使用第二个海龟,我们可以保持屏幕上的其他内容不变,只更改第二个海龟所绘制的部分。
在 示例 9-10,two_turtles.py 中,我们将使用一个海龟绘制一个正方形,另一个海龟在其下方写字。
import turtle as t
# Set up the screen
t.setup(810,710, 10, 70)
t.tracer(False)
t.hideturtle()
t.bgcolor('lightgreen')
t.color('blue')
t.pensize(5)
1 t.up()
t.goto(-200,-100)
t.down()
t.forward(400)
t.left(90)
t.forward(400)
t.left(90)
t.forward(400)
t.left(90)
t.forward(400)
# Create a second turtle
2 msg = t.Turtle()
msg.hideturtle()
msg.up()
msg.color('red')
msg.goto(-300,-200)
msg.write('this is written by the second turtle',font=('Arial',30,'normal'))
t.update()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
示例 9-10: two_turtles**.py 的 Python 代码
我们导入 turtle 模块,并设置一个大小为 810 x 710 像素的屏幕。从 1 开始,我们在屏幕中央画一个蓝色的正方形,类似于我们画矩形的方式,只不过四条边长度相等。
在 2 处,我们通过Turtle()创建了第二只海龟,并将其命名为msg。我们告诉脚本隐藏第二只海龟
第二只海龟msg抬起画笔,将颜色改为红色,移动到坐标(–300, –200),并写下消息this is written by the second turtle。update()函数刷新屏幕,绘制由两只海龟创建的所有内容,如图 9-10 所示。

图 9-10:由两只海龟创建的屏幕
总结
在这一章中,你学习了turtle模块的基础知识。你首先学习了如何设置海龟屏幕,然后学习了基本的移动方式,比如前进、后退以及左转、右转。你通过使用内置函数和基本移动命令创建了各种形状。
最后,你学会了通过使用clear()和update()函数在turtle模块中创建动画效果。在接下来的几章中,你将学习如何使用这些技能创建语音控制的图形游戏。
章节末练习
-
修改set_up_screen.py,使屏幕宽度为 500 像素,高度为 400 像素,背景颜色为蓝色,标题为
Modified Screen。 -
修改forward_backward.py,使得海龟先后退 100 像素,再前进 250 像素。
-
修改dots.py,使其只包含两个直径为 60 的浅绿色点,分别位于坐标(–100, –100)和(100, 100)处。
-
修改triangle.py,使得三角形的三条边为红色,边宽为 3\。
-
通过使用
goto()函数来复制rectangle.py中的结果。你不能使用forward()、backward()、left()或right()等函数。
井字棋

在这一章中,你将构建一个语音控制的井字棋游戏,将你所有的新技能付诸实践。你将绘制一个带有蓝色和白色棋子的游戏棋盘,禁止无效的移动,并检测玩家是否获胜。接着,你会添加语音识别和文本转语音功能,并设置游戏,让你与自己的电脑进行对战。
和往常一样,本章的所有脚本都可以在本书的资源页面上找到,www.nostarch.com/make-python-talk/。在开始之前,设置本章的文件夹/mpt/ch10/。
游戏规则
井字棋可能是世界上最著名的游戏之一,但为了确保,我们在创建游戏棋盘之前先复习一下规则。在井字棋中,两个玩家轮流在一个三乘三的网格中标记一个格子,标记为 X 或 O。第一个将三个 X 或 O 连成一排(横向、纵向或对角线)的人获胜。如果在所有格子填满之前没有人连成三个,游戏将以平局结束。我们将用蓝色和白色的点代替 X 和 O 作为棋子。
绘制游戏棋盘
我们将在屏幕上绘制一个三乘三的网格,并为每个单元格分配一个编号,以便告诉脚本在哪里放置每个棋子。打开你的 Spyder 编辑器,复制 清单 10-1 中的代码,并将脚本保存为 ttt_board.py,存放在你的章节文件夹中。
import turtle as t
# Set up the screen
t.setup(600,600,10,70)
t.tracer(False)
t.bgcolor("red")
t.hideturtle()
t.title("Tic-Tac-Toe in Turtle Graphics")
# Draw horizontal lines and vertical lines to form grid
t.pensize(5)
1 for i in (-100,100):
t.up()
t.goto(i,-300)
t.down()
t.goto(i,300)
t.up()
t.goto(-300,i)
t.down()
t.goto(300,i)
t.up()
# Create a dictionary to map cell numbers to cell center coordinates
2 cellcenter = {'1':(-200,-200), '2':(0,-200), '3':(200,-200),
'4':(-200,0), '5':(0,0), '6':(200,0),
'7':(-200,200), '8':(0,200), '9':(200,200)}
# Go to the center of each cell, write down the cell number
3 for cell, center in list(cellcenter.items()):
t.goto(center)
t.write(cell,font = ('Arial',20,'normal'))
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
清单 10-1:绘制井字游戏棋盘
我们导入 turtle 模块中的所有函数,并将屏幕设置为 600 像素 × 600 像素。由于我们有一个三乘三的网格,因此每个单元格的大小是 200 像素 × 200 像素。我们将背景色设置为红色,并将标题设置为 Tic-Tac-Toe in Turtle Graphics。
使用命令 for i in (-100, 100),我们将变量 i 在范围 -100 到 100 之间进行迭代。结果,for 循环产生了两条水平线和两条垂直线。两条水平线分别位于点 (–300, –100) 和 (300, –100),以及点 (–300, 100) 和 (300, 100) 之间。两条垂直线分别位于点 (–100, –300) 和 (–100, 300),以及点 (100, –300) 和 (100, 300) 之间。这些线均匀地将屏幕分成了九个单元格。
然后我们创建一个字典 cellcenter,将每个单元格编号映射到相应单元格中心的 x 和 y 坐标。例如,左下角的单元格是单元格编号 1,它的中心坐标是 (x = –200, y = –200)。我们为字典中的所有九个单元格执行相同操作,使用单元格编号作为键,坐标作为值。
在第 3 步,我们使用 for 循环迭代九对值,在每个单元格的中心写入单元格编号。命令 list(cellcenter.items()) 会生成一个包含来自 cellcenter 的九个键值对的列表,结果应该如下所示:
[('1', (-200, -200)), ('2', (0, -200)), ('3', (200, -200)), ('4', (-200, 0)),
('5', (0, 0)), ('6', (200, 0)), ('7', (-200, 200)), ('8', (0, 200)), ('9',
(200, 200))]
在每次迭代 for 循环时,海龟会移动到单元格的中心,并在那里写下单元格编号。运行脚本后,你应该能看到类似于 图 10-1 的屏幕。

图 10-1:井字游戏的棋盘
创建游戏棋子
现在,我们将添加代码,将游戏棋子放置到单元格中。你将首先了解鼠标点击在 turtle 模块中的工作原理,然后使用它们来放置棋子。
鼠标点击在 turtle 中的工作原理
当你在 turtle 屏幕上左键点击时,点击位置的 x 和 y 坐标会显示在屏幕上。清单 10-2 中的 mouse_click.py 处理一个简单的鼠标点击事件。这只是示范用的代码;我们不会在最终的脚本中使用这段代码,但会使用相同的原理。
import turtle as t
# Set up the screen
t.setup(620,620,360,100)
t.title("How Mouse-Clicks Work in Turtle Graphics")
# Define get_xy() to print the coordinates of the point you click
1 def get_xy(x,y):
print(f'(x, y) is ({x}, {y})')
# Hide the turtle so that you don't see the arrowhead
t.hideturtle()
# Bind the mouse click to the get_xy() function
2 t.onscreenclick(get_xy)
3 t.listen()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
清单 10-2:鼠标点击在 turtle 模块中的工作原理
和往常一样,我们导入 turtle 模块并设置屏幕。在第 1 步中,我们定义了函数 get_xy(),它会输出你点击的 x 和 y 坐标。我们还隐藏了海龟图形,这样你就不会看到光标在屏幕上移动了。在第 2 步中,我们使用 turtle 的 onscreenclick() 函数将屏幕上的鼠标点击绑定到 get_xy() 函数,该函数返回点击的 x 和 y 坐标。因此,onscreenclick(get_xy) 将鼠标点击的 x 和 y 坐标作为输入传递给 get_xy()。在第 3 步中,我们使用 listen() 来检测诸如鼠标点击和键盘按键等事件。
运行 mouse_click.py,随机点击屏幕几次,你应该会看到类似这样的输出:
(x, y) is (-46.0, 109.0)
(x, y) is (14.0, -9.0)
(x, y) is (-185.0, -19.0)
(x, y) is (-95.0, 109.0)
(x, y) is (13.0, -81.0)
在我五次点击中,onscreenclick() 捕捉到了点击点的 x 和 y 坐标,并将这两个值提供给 get_xy(),后者打印出了相应的 x 和 y 值。
将鼠标点击转换为单元格编号
接下来,我们将棋盘创建和点击检测脚本结合起来,这样当你点击一个单元格时,脚本会输出单元格编号。在图 10-2 中,我在游戏棋盘上标出了行和列的编号,以及网格线的 x 和 y 坐标。
打开 ttt_board.py,在底部(t.done() 之前)添加列表 10-3 中的代码,并将新的脚本保存为 cell_number.py 到你的章节文件夹中。这个脚本只是一个示例,我们在最终的代码中不会使用它,但会使用类似的代码。
`--snip--`
for cell, center in list(cellcenter.items()):
t.goto(center)
t.write(cell,font = ('Arial',20,'normal'))
# Define a function cell_number() to print out the cell number
1 def cell_number(x,y):
if -300<x<300 and -300<y<300:
# Calculate the column number based on x value
2 col = int((x+500)//200)
print('column number is ', col)
# Calculate the row number based on y value
row = int((y+500)//200)
print('row number is ', row)
# Calculate the cell number based on col and row
3 cellnumber = col+(row-1)*3
print('cell number is ', cellnumber)
else:
print('you have clicked outside the game board')
# Hide turtle so that you don't see the arrowhead
t.hideturtle()
# bind the mouse click to the cell_number() function
onscreenclick(cell_number)
t.listen()
`--snip--`
列表 10-3:将鼠标点击转换为单元格编号

图 10-2:在游戏棋盘上标出行和列的编号。
在第 1 步中,我们定义了 cell_number(),该函数将鼠标点击的 x 和 y 坐标转换为单元格编号。在函数内部,我们将你点击的点的 x 和 y 坐标限制在棋盘的范围内。如果你点击的地方超出了范围,脚本会输出 you have clicked outside the game board。
在第 2 步,我们将点击的 x 坐标转换为列号。第一列的点的 x 坐标在 -300 到 -100 之间,第二列的点的 x 坐标在 -100 到 100 之间,因此我们使用公式 col = int((x+500)//200) 来获取列中像素坐标的完整范围,从而将 x 坐标转换为列号。我们使用相同的方法将 y 坐标转换为行号。
然后我们使用公式 cellnumber = col+(row-1)*3 来计算单元格编号,因为单元格编号是从左到右、从下到上增加的。最后,我们将屏幕点击事件绑定到 cell_number()。
运行 cell_number.py。以下是与脚本交互的一次输出:
column number is 3
row number is 2
cell number is 6
column number is 1
row number is 3
cell number is 7
column number is 2
row number is 1
cell number is 2
每次点击单元格时,脚本会输出列号、行号和单元格编号。
放置游戏棋子
接下来,我们将把棋子放到棋盘上。当你第一次点击任何一个单元格时,一个蓝色棋子会出现在单元格的中心。再点击一次,棋子会变成白色,然后是蓝色,如此交替。
打开ttt_board.py,添加列表 10-4 中的代码,并将新脚本保存为mark_cell.py,保存在你的章节文件夹中。确保不要把这段代码加入到cell_number.py中!
`--snip--`
for cell, center in list(cellcenter.items()):
t.goto(center)
t.write(cell,font = ('Arial',20,'normal'))
# The blue player moves first
turn = "blue"
# Define a function mark_cell() to place a dot in the cell
1 def mark_cell(x,y):
# Make the variable turn a global variable
2 global turn
# Calculate the cell number based on x and y values
if -300<x<300 and -300<y<300:
col = int((x+500)//200)
row = int((y+500)//200)
# The cell number is a string variable
3 cellnumber = str(col + (row - 1)*3)
else:
print('you have clicked outside the game board')
# Go to the corresponding cell and place a dot of the player's color
t.up()
4 t.goto(cellcenter[cellnumber])
t.dot(180,turn)
t.update()
# give the turn to the other player
if turn == "blue":
turn = "white"
else:
turn = "blue"
# Hide the turtle so that you don't see the arrowhead
t.hideturtle()
# Bind the mouse click to the mark_cell() function
t.onscreenclick(Mark_cell)
t.listen()
`--snip--`
列表 10-4:在棋盘上放置棋子
我们先绘制棋盘,然后定义变量turn来跟踪轮到谁走棋。我们首先将blue赋值给该变量,这样蓝色玩家就会先走。
在第 1 步,我们定义了mark_cell(),它会在你点击的单元格中放置棋子。在第 2 步,我们声明了全局变量turn。Python 提供了global关键字,允许turn在mark_cell()函数内外都能使用。如果没有将变量声明为全局变量,每次点击棋盘时,你都会看到错误信息UnboundLocalError: local variable 'turn' referenced before assignment。
然后,我们将点击的 x 和 y 坐标转换为棋盘上的单元格编号 3。在同一行内,我们还将单元格编号从整数转换为字符串,以匹配字典cellcenter中使用的变量类型。
在第 4 步,我们从cellcenter获取被点击单元格的中心坐标,并告诉海龟到达该位置。海龟会绘制一个 180 像素宽的点,颜色为turn中存储的值。然后,轮到另一个玩家。最后,我们将mark_cell()绑定到鼠标点击事件上。
运行脚本后,你就可以点击棋盘并标记单元格了。点的颜色会在蓝色和白色之间交替,如图 10-3 所示。

图 10-3:在井字棋盘上标记单元格。
现在脚本已经可以进行游戏了!不过,我们需要实现三个新规则,以使其符合井字游戏的规则:
-
如果一个单元格已经被占用,你就不能再次标记它。
-
如果玩家在一条直线上——无论是水平、垂直还是对角线——标记了三个单元格,则该玩家获胜,游戏应该停止。
-
如果所有九个单元格都被占用,游戏应该停止,如果没有玩家获胜,则判定为平局。
确定有效的移动、胜利和平局
接下来,我们将实现这些规则,仅允许有效的移动,并判定胜利(或平局)。从书籍资源中下载ttt_click.py并将其保存到章节文件夹中,或者根据列表 10-5 中的不同之处修改mark_cell.py。
from tkinter import messagebox
`--snip--`
# The blue player moves first
turn = "blue"
# Count how many rounds played
rounds = 1 1
# Create a list of valid moves
validinputs = list(cellcenter.keys())
# Create a dictionary of moves made by each player
occupied = {"blue":[],"white":[]}
# Determine if a player has won the game
def win_game(): 2
win = False
if '1' in occupied[turn] and '2' in occupied[turn] and '3' in occupied[turn]:
win = True
if '4' in occupied[turn] and '5' in occupied[turn] and '6' in occupied[turn]:
win = True
if '7' in occupied[turn] and '8' in occupied[turn] and '9' in occupied[turn]:
win = True
if '1' in occupied[turn] and '4' in occupied[turn] and '7' in occupied[turn]:
win = True
if '2' in occupied[turn] and '5' in occupied[turn] and '8' in occupied[turn]:
win = True
if '3' in occupied[turn] and '6' in occupied[turn] and '9' in occupied[turn]:
win = True
if '1' in occupied[turn] and '5' in occupied[turn] and '9' in occupied[turn]:
win = True
if '3' in occupied[turn] and '5' in occupied[turn] and '7' in occupied[turn]:
win = True
return win
# Define a function mark_cell() to place a dot in the cell
def mark_cell(x,y):
# Declare global variables
global turn, rounds, validinputs 3
# Calculate the cell number based on x and y values
if -300<x<300 and -300<y<300:
col = int((x+500)//200)
row = int((y+500)//200)
# The cell number is a string variable
cellnumber = str(col + (row - 1)*3)
else:
print('you have clicked outside the game board')
# Check if the move is a valid one
if cellnumber in validinputs: 4
# Go to the corresponding cell and place a dot of the player's color
t.up()
t.goto(cellcenter[cellnumber])
t.dot(180,turn)
t.update()
# Add the move to the occupied list for the player
occupied[turn].append(cellnumber) 5
# Disallow the move in future rounds
validinputs.remove(cellnumber)
# Check if the player has won the game
if win_game() == True: 6
# If a player wins, invalid all moves, end the game
validinputs = []
messagebox.showinfo("End Game",f"Congrats player {turn}, you won!")
# If all cells are occupied and no winner, it's a tie
elif rounds == 9: 7
messagebox.showinfo("Tie Game","Game over, it's a tie!")
# Counting rounds
rounds += 1
# Give the turn to the other player
if turn == "blue":
turn = "white"
else:
turn = "blue"
# If the move is not a valid move, remind the player
else:
messagebox.showerror("Error","Sorry, that's an invalid move!")
# Bind the mouse click to the mark_cell() function
t.onscreenclick(mark_cell)
`--snip--`
列表 10-5:仅允许有效的移动并判定胜利和平局。
我们的第一个修改是从tkinter包中导入messagebox模块;这个模块会显示一个消息框,用于显示胜利、平局或无效的移动。
从第 1 行开始,我们创建了一个变量rounds、一个列表validinputs和一个字典occupied。变量rounds用于跟踪已进行的回合数,也就是已标记的单元格数。当回合数达到 9 且没有玩家获胜(在井字棋中这通常发生),我们将宣布平局。
我们使用validinputs来判断一个移动是否有效。如果一个单元格已被玩家标记,我们将把它从有效移动列表中移除。
字典occupied跟踪每个玩家的移动。在游戏开始时,键blue和white的值都是空列表。当玩家占据一个单元格时,该单元格编号将被添加到该玩家的列表中。例如,如果蓝色玩家占据了单元格 1、5 和 9,白色玩家占据了单元格 3 和 7,occupied将变为{"blue":["1","5","9"],"white":["3","7"]}。我们稍后将使用它来确定玩家是否获胜。
在第 2 行,我们定义了win_game()函数,用于检查玩家是否获胜。玩家可以通过八种方式获胜,我们将明确检查这些方式:
-
单元格 1、2 和 3 已被同一玩家占据。
-
单元格 4、5 和 6 已被同一玩家占据。
-
单元格 7、8 和 9 已被同一玩家占据。
-
单元格 1、4 和 7 已被同一玩家占据。
-
单元格 2、5 和 8 已被同一玩家占据。
-
单元格 3、6 和 9 已被同一玩家占据。
-
单元格 1、5 和 9 已被同一玩家占据。
-
单元格 3、5 和 7 已被同一玩家占据。
函数win_game()创建了变量win,并将其默认值设置为False。该函数检查字典occupied,获取当前轮到的玩家占据的单元格列表,并检查之前列出的八种获胜情况。如果其中一种情况匹配,win的值将变为True。当调用win_game()时,它返回存储在变量win中的值。
我们对mark_cell()做了重大修改。在第 3 行,我们声明了三个全局变量;所有这些都必须声明为全局变量,因为它们将在函数内部被修改。在第 4 行,我们检查最近点击的单元格编号是否在validinputs列表中;如果在,则该单元格上会标记一个点,且该单元格编号会被添加到玩家的占据单元格列表中。然后,单元格会从validinputs中移除,以防止玩家在未来的回合中再次标记该单元格。
在第 6 行,我们调用win_game(),查看当前玩家是否获胜。如果获胜,我们将validinputs清空,这样就不能再进行任何移动。弹出一个消息框,显示恭喜蓝色玩家,你获胜了!或恭喜白色玩家,你获胜了!,并使用messagebox模块的showinfo()函数(见图 10-4)。

图 10-4:蓝色玩家获胜!
如果玩家没有获胜,脚本会检查回合数是否达到了九回合(7)。如果是,脚本会宣布平局,显示游戏结束,平局!(图 10-5)。

图 10-5:平局
如果游戏没有结束,我们将增加回合数,并把回合交给另一个玩家。在游戏过程中,如果玩家点击了一个无效的格子,我们会显示抱歉,那是一个无效的动作!(图 10-6)。

图 10-6:无效的动作
语音控制版本
现在我们准备好添加语音控制和语音功能了。一个显著的变化是,我们将对手设置为计算机。我们将基于最新的ttt_click.py文件进行构建。在你作为蓝色玩家走完一步后,计算机会随机选择一个动作作为白色玩家,直到游戏结束。
从书本资源中下载ttt_hs.py,或按照列表 10-6 中的修改内容进行调整。
import turtle as t
from random import choice
from tkinter import messagebox
# Import functions from the local package
from mptpkg import voice_to_text, print_say
*--snip--*
if '3' in occupied[turn] and '5' in occupied[turn] and '7' in occupied[turn]:
win = True
return win
# Start an infinite loop to take voice inputs
1 while True:
# Ask for your move
print_say(f"Player {turn}, what's your move?")
# Capture your voice input
inp = voice_to_text()
print(f"You said {inp}.")
inp = inp.replace('number ','')
inp = inp.replace('one','1')
inp = inp.replace('two','2')
inp = inp.replace('three','3')
inp = inp.replace('four','4')
inp = inp.replace('five','5')
inp = inp.replace('six','6')
inp = inp.replace('seven','7')
inp = inp.replace('eight','8')
inp = inp.replace('nine','9')
if inp in validinputs:
# Go to the corresponding cell and place a dot of the player's color
t.up()
t.goto(cellcenter[inp])
t.dot(180,turn)
t.update()
# Add the move to the occupied list for the player
occupied[turn].append(inp)
# Disallow the move in future rounds
validinputs.remove(inp)
# **Check if the player has won the game**
2 if win_game() == True:
# If a player wins, invalid all moves, end the game
validinputs = []
print_say(f"Congrats player {turn}, you won!")
messagebox.showinfo\
("End Game",f"Congrats player {turn}, you won!")
break
# If all cells are occupied and no winner, game is a tie
elif rounds == 9:
print_say("Game over, it's a tie!")
messagebox.showinfo("Tie Game","Game over, it's a tie!")
break
# Counting rounds
rounds += 1
# Give the turn to the other player
if turn == "blue":
turn = "white"
else:
turn = "blue"
# The computer makes a random move
3 inp = choice(validinputs)
print_say(f'The computer occupies cell {inp}.')
t.up()
t.goto(cellcenter[inp])
t.dot(180,turn)
t.update()
occupied[turn].append(inp)
validinputs.remove(inp)
if win_game() == True:
validinputs = []
print_say(f"Congrats player {turn}, you won!")
messagebox.showinfo\
("End Game",f"Congrats player {turn}, you won!")
break
elif rounds == 9:
print_say("Game over, it's a tie!")
messagebox.showinfo("Tie Game","Game over, it's a tie!")
break
rounds += 1
if turn == "blue":
turn = "white"
else:
turn = "blue"
# If the move is not a valid move, remind the player
else:
print_say("Sorry, that's an invalid move!")
t.done()
`--snip--`
列表 10-6:添加语音和语音控制功能
我们导入所需的函数:从random模块导入choice()函数,让计算机随机选择一个动作,以及从自定义包mptpkg导入我们的print_say()和voice_to_text()函数。
在 1 处,我们开始了一个无限的while循环。在每次循环中,脚本都会大声询问你的动作。你对着麦克风说出你的动作,脚本捕捉到你的语音命令,将回应存储在变量inp中。
这里我们对voice_to_text()做了一些调整,使其更能响应你的语音命令。当你的语音输入只有一个词时,比如“One”或“Two”,软件很难理解该词的上下文并作出反应。另一方面,如果你说“Number one”或“Number two”,软件就能轻松理解你的意思。脚本简单地将语音命令中的“number”部分替换为空字符串,这样inp中就只剩下数字部分。有时voice_to_text()返回的是文字形式的数字,比如one或two,而不是数字形式,比如1或2。因此,我们还将所有的文字形式转换为数字形式。这样,你可以对着麦克风说“number one”或one,inp将始终是你想要的形式:1。
如果你的选择在validinputs中,脚本将执行一系列动作来完成这一步:在相应的格子中放置一个圆点,将该格子的编号添加到你占据的格子列表中,并从有效输入的列表中移除该格子的编号。
脚本接着检查你是否赢得了比赛或是否平局,并适当地大声回应。
一旦你的回合结束,计算机会从validinputs中随机选择一个动作与您对抗。脚本会检查计算机是否赢得了比赛或是否平局。如果你的语音命令不是有效的动作,脚本会发出警告。
这是与游戏的一次互动:
Player blue, what's your move?
You said 7.
The computer occupies cell 3.
Player blue, what's your move?
You said 8.
The computer occupies cell 1.
Player blue, what's your move?
You said 9.
Congrats player blue, you won!
我已经在三步内赢得了比赛!
总结
在本章中,你学会了构建一个语音控制的图形井字游戏,它能够用人声进行对话。在这个过程中,你掌握了一些新技能。
你学会了如何在turtle模块中处理鼠标点击。凭借这一知识,你在游戏棋盘上通过鼠标点击来标记单元格。
你学会了如何根据明确的游戏规则判断玩家是否赢得了井字游戏。这是游戏创建的核心。你列出了所有玩家可以获胜的情况,然后添加了代码来检查所有这些情况并判断是否有赢家。
你还将语音识别和文本转语音功能添加到了游戏中,进行了一些调整,以确保脚本能够理解你的输入。通过结合这些技能,你将能够创建自己的语音控制游戏。
章节末练习
-
修改ttt_board.py,使得单元格编号出现在每个单元格的左下角,字体大小为 15 点(从单元格中心水平和垂直偏移 80 像素)。
-
修改mouse_click.py,使得每次点击屏幕时,脚本输出附加信息
x + y is,后面跟着点击点的 x 和 y 坐标的实际值。 -
修改cell_number.py,使得每次点击屏幕时,脚本输出
you clicked the point (``x``,y``),然后再输出列、行和单元格编号,其中x和y是实际坐标。例如,如果你点击的是(x = –100, y = 50)的点,信息应显示为you clicked the point (-100, 50)。 -
修改mark_cell.py,使得白色玩家先手。
-
修改ttt_click.py,使得玩家只有通过横向或纵向连续标记三个单元格才能获胜,但不能通过对角线获胜。
四子棋

在本章中,你将构建一个语音控制的四子棋游戏。与第十章中的井字游戏相似,你将首先绘制棋盘并设置黄色和红色棋子交替轮流。你还将为棋子从列的顶部落下到最底层空白行的过程添加动画效果,使游戏更加具有视觉吸引力。你将禁止无效的移动,检测玩家是否获胜,并检测是否所有 42 个单元格都已被占满而没有赢家,意味着游戏平局。
在第十章中,你学会了如何通过列出所有的获胜情形,并检查当前的游戏棋盘是否符合其中一种情形,从而判断玩家是否获胜。我们将在这里应用同样的策略。你还将学习如何使用异常处理来防止在检查过程中崩溃,以及如何避免负索引错误。
一旦游戏设置完成,我们将添加语音识别和文本转语音功能,让你仅凭声音就能玩游戏。
首先,为本章设置文件夹/mpt/ch11/。本章的所有脚本可以通过本书的资源页面获取:www.nostarch.com/make-python-talk/。
游戏规则
连接四子是一个著名的棋盘游戏,但为了澄清即将展示的代码逻辑,我会先介绍一下规则。在连接四子中,两名玩家轮流将棋子从顶部投放到七列中的一个。一个玩家使用红色棋子,另一个使用黄色棋子。七列位于一个六行的垂直悬挂网格上。当棋子被投放到某一列时,它会掉落到该列中最低的可用空间。棋子无法从一列移动到另一列。
第一个形成四个连续棋子(无论是横向、纵向还是对角线)的玩家获胜。如果 42 个格子都被填满且没有人获胜,游戏平局。我们将用红色圆点和黄色圆点来代表棋子。
绘制游戏棋盘
我们首先绘制一个六行七列的网格。我们将在屏幕顶部为每一列编号,方便玩家进行游戏。
打开你的 Spyder 编辑器,输入列表 11-1 中的代码。将脚本保存为conn_board.py,并放在你的章节文件夹中。
import turtle as t
# Set up the screen
1 t.setup(700,600,10,70)
t.hideturtle()
t.tracer(False)
t.bgcolor("lightgreen")
t.title("Connect Four in Turtle Graphics")
# Draw six thick vertical lines
2 t.pensize(5)
for i in range(-250,350,100):
t.up()
t.goto(i,-350)
t.down()
t.goto(i,350)
t.up()
# Draw five thin gray horizontal lines to form grid
3 t.pensize(1)
t.pencolor("grey")
for i in range(-200,300,100):
t.up()
t.goto(-350,i)
t.down()
t.goto(350,i)
t.up()
# Write column numbers on the board
4 colnum = 1
for x in range(-300,350,100):
t.goto(x,270)
t.write(colnum,font = ('Arial',20,'normal'))
colnum += 1
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
列表 11-1:绘制连接四子的游戏棋盘
我们首先导入turtle模块中的所有函数,然后将屏幕设置为 700x600 像素。这使得我们可以将每个单元格设置为 100x100 像素,保持简单。我们将背景色设置为浅绿色,标题为Connect Four in Turtle Graphics。
我们接着画了六条粗的垂直线,将屏幕分成七列。在第 2 行,我们将画笔宽度设置为 5 像素。命令行for i in range(-250,350,100)让变量i遍历以下六个值:–250、–150、–50、50、150 和 250。这些是六条垂直线的 x 坐标。六条垂直线两个端点的 y 坐标都是–350 和 350。类似地,我们画了五条细的灰色横线,把屏幕分成六行。从第 3 行开始,画笔大小为 1 像素,颜色为灰色,这样线条看起来又细又轻。这就为我们提供了一个均匀的网格,包含七列六行。
接下来,我们为列编号,以便玩家知道将棋子放在哪一列。我们首先创建一个变量colnum并赋值为1。然后我们遍历七列中心的 x 坐标,并通过将colnum的值加一来写出相应的列号。
运行脚本后,你应该能看到类似于图 11-1 的画面。

图 11-1:连接四子游戏的棋盘
鼠标点击版
现在你有了一个游戏棋盘。让我们把一些棋子投放到列中。在这一部分,你将学习如何使用鼠标点击将棋子放入某一列,并让它掉落到最低可用的单元格。之后,你将检测无效的操作、胜利和平局。
投放棋子
在这里,你将使用鼠标点击将棋子放入你选择的列中。棋子所在的列由你点击的位置决定。行号则取决于该列中已有的棋子数量。
当你第一次点击一列时,红色的圆点将被放置在该列最底部可用的单元格中。每次点击时,颜色将交替变化。
打开conn_board.py并添加 Listing 11-2 中的代码。然后将新的脚本保存为show_disc.py,并放入你的章节文件夹中。
`--snip--`
# Write column numbers on the board
colnum = 1
for x in range(-300, 350, 100):
t.goto(x,270)
t.write(colnum,font = ('Arial',20,'normal'))
colnum += 1
# The red player moves first
1 turn = "red"
# The x-coordinates of the center of the 7 columns
2 xs = [-300,-200,-100,0,100,200,300]
# The y-coordinates of the center of the 6 rows
ys = [-250,-150,-50,50,150,250]
# Keep track of the occupied cells
occupied = [list(),list(),list(),list(),list(),list(),list()]
# Define a function conn() to place a disc in a cell
3 def conn(x,y):
# Make the variable turn a global variable
global turn
# Calculate the column number based on x- and y-values
if -350<x<350 and -300<y<300:
col = int((x+450)//100)
else:
print('You have clicked outside the game board!')
# Calculate the lowest available row number in that column
row = len(occupied[col-1])+1
# Go to the cell and place a dot of the player's color
t.up()
t.goto(xs[col-1],ys[row-1])
t.dot(80,turn)
# Add the move to the occupied list to keep track
occupied[col-1].append(turn)
# Give the turn to the other player
if turn == "red":
turn = "yellow"
else:
turn = "red"
# Bind the mouse click to the conn() function
t.onscreenclick(conn)
t.listen()
t.done()
`--snip--`
Listing 11-2: 在棋盘上放置棋子
红色玩家先行,所以在绘制完棋盘后,我们定义了变量turn并将值red赋给它 1。从第 2 行开始,我们定义了三个列表。列表xs包含与七列中点的 x 坐标对应的值。列表ys包含与六行中点的 y 坐标对应的六个值。稍后,我们将使用这些列表来确定所有 42 个单元格中心的 x 和 y 坐标。
列表occupied是一个列表的列表。它一开始是一个包含七个空列表的列表,每个列表代表一列。当你将棋子放置到某列时,棋子将被添加到对应的列表中。这样,occupied将追踪所有已放置的棋子及其位置。
在第 3 行,我们定义了conn(),它将棋子放置在你点击的列上。我们将turn声明为全局变量,这样它的值在conn()内外都能被识别。接着,我们将用户点击的 x 坐标转换为棋盘上的列号。然后,我们确定该列中最底部可用的行,这告诉我们应该将棋子放置在哪一行。请注意,occupied[col-1]是该列中当前所有棋子的列表,我们使用col-1而不是col,因为 Python 使用零索引,但我们的列是从 1 开始编号的。
然后,我们获取要放置新棋子的单元格中心的 x 和 y 坐标。turtle模块会以直径为 80 像素的大小放置一个圆点,颜色值为turn中存储的颜色。我们将棋子添加到occupied中的相应列表中,这样下次将棋子放置在同一列时,适当的单元格将被标记为无效。这样,玩家的回合就结束了,接着我们将回合交给另一位玩家。最后,我们将conn()绑定到鼠标点击事件上。
运行脚本,你应该能够点击游戏棋盘,并用红色或黄色圆点标记单元格。继续点击,圆点的颜色将在红色和黄色之间交替变化(见图 11-2)。

图 11-2:将棋子放置在四连棋的棋盘上。
动画化掉落的棋子
当你在现实世界中玩四连棋时,你将棋子从上方丢下,它会掉入适当的位置。接下来,你将创建棋子掉落的动画效果。这是一个很好的机会来学习如何使用turtle模块创建动画效果。
打开show_disc.py并添加 Listing 11-3 中的代码。将其保存为disc_fall.py,并放入你的章节文件夹中。
import turtle as t
1 from time import sleep
*--snip--*
# Keep track of the occupied cells
occupied = [list(),list(),list(),list(),list(),list(),list()]
# Create a second turtle to show disc falling
2 fall = t.Turtle()
fall.up()
3 fall.hideturtle()
# Define a function conn() to place a disc in a cell
def conn(x,y):
# Make the variable turn a global variable
global turn
# Calculate the column number based on x and y values
if -350<x<350 and -300<y<300:
col = int((x+450)//100)
else:
print('You have clicked outside the game board!')
# Calculate the lowest available row number in that column
row = len(occupied[col-1])+1
# Show the disc fall from the top
4 if row<6:
for i in range(6,row,-1):
fall.goto(xs[col-1],ys[i-1])
fall.dot(80,turn)
update()
sleep(0.05)
fall.clear()
# Go to the cell and place a dot of the player's color
up()
`--snip--`
Listing 11-3: 显示棋子掉落动画效果的脚本
我们导入sleep(),以便在脚本中暂停,让下落的棋盘片在单元格中停留片刻,允许用户看到它的移动 1。接下来,从第 2 行开始,我们创建一个名为fall的第二个海龟(turtle)。我们抬起新海龟的画笔,这样它移动时就不会留下痕迹。同时,我们使用hideturtle()来隐藏光标 3。
从第 4 行开始,我们动画化下落的棋盘片。首先,我们通过检查行号是否小于 6 来查看列是否已满。如果是的话,就显示动画效果。如果该列下方的行已经满了,棋盘片就可以停留在当前位置(无需展示棋盘片下落)。
我们通过迭代i遍历所有空单元格,直到最低可用单元格上方。如果最低可用位置是row = 2,例如,命令for i in range(6,row,-1)会将i依次设为 6、5、4 和 3。-1告诉 range 函数按倒序计数。在每次迭代中,fall海龟会在空单元格的中心放置一个点。脚本在屏幕上绘制一个点,暂停 0.05 秒后再擦除该点,然后进入下一次迭代。
现在脚本已经是一个完整的游戏了!然而,目前,玩家必须自行判断并遵守以下规则:
-
如果某列已经满了,就不能再在该列放置棋盘片。
-
如果某个玩家将四个单元格连成一条直线,该玩家获胜,游戏应停止。
-
如果所有 42 个单元格都已占满且没有人获胜,游戏应停止并宣布平局。
让我们将这些代码添加到游戏中。
确定有效移动、胜利和平局
接下来,我们将通过阻止无效移动并声明胜利或平局来改进游戏。打开disc_fall.py并添加清单 11-4 中的代码。将新的脚本保存为conn_click.py。代码更改分为两个部分,便于在阅读解释时参考代码。
import turtle as t
from time import sleep
from tkinter import messagebox
# Set up the screen
`--snip--`
# Create a second turtle to show disc falling
fall = t.Turtle()
fall.up()
fall.hideturtle()
# Create a list of valid moves
1 validinputs = [1,2,3,4,5,6,7]
# Define a horizontal4() function to check connecting 4 horizontally
2 def horizontal4(x, y, turn):
win = False
for dif in (-3, -2, -1, 0):
try:
if occupied[x+dif][y] == turn\
and occupied[x+dif+1][y] == turn\
and occupied[x+dif+2][y] == turn\
and occupied[x+dif+3][y] == turn\
and x+dif >= 0:
win = True
except IndexError:
pass
return win
# Define a vertical4() function to check connecting 4 vertically
3 def vertical4(x, y, turn):
win = False
try:
if occupied[x][y] == turn\
and occupied[x][y-1] == turn\
and occupied[x][y-2] == turn\
and occupied[x][y-3] == turn\
and y-3 >= 0:
win = True
except IndexError:
pass
return win
# Define a forward4() function to check connecting 4 diagonally in / shape
def forward4(x, y, turn):
win = False
for dif in (-3, -2, -1, 0):
try:
if occupied[x+dif][y+dif] == turn\
and occupied[x+dif+1][y+dif+1] == turn\
and occupied[x+dif+2][y+dif+2] == turn\
and occupied[x+dif+3][y+dif+3] == turn\
and x+dif >= 0 and y+dif >= 0:
win = True
except IndexError:
pass
return win
# Define a back4() function to check connecting 4 diagonally in \ shape
def back4(x, y, turn):
win = False
for dif in (-3, -2, -1, 0):
try:
if occupied[x+dif][y-dif] == turn\
and occupied[x+dif+1][y-dif-1] == turn\
and occupied[x+dif+2][y-dif-2] == turn\
and occupied[x+dif+3][y-dif-3] == turn\
and x+dif >= 0 and y-dif-3 >= 0:
win = True
except IndexError:
pass
return win
# Define a win_game() function to check if someone wins the game
4 def win_game(col, row, turn):
win = False
# Convert column and row numbers to indexes in the list of lists occupied
x = col-1
y = row-1
# Check all winning possibilities
if vertical4(x, y, turn) == True:
win = True
if horizontal4(x, y, turn) == True:
win = True
if forward4(x, y, turn) == True:
win = True
if back4(x, y, turn) == True:
win = True
# Return the value stored in win
return win
`--snip--`
清单 11-4:脚本的前半部分,用于禁止无效移动并声明胜利和平局
我们从tkinter包中导入messagebox模块,以便在游戏中显示关于胜利、平局和无效移动的消息。
在第 1 行,我们创建了列表validinputs来跟踪有效的移动。最开始,所有七列都是有效的。如果某列已满六个棋盘片,它将从列表中移除。
玩家可以通过在四个方向之一连成四个棋盘片来获胜:水平、垂直、斜向(/)或反斜向(\)。因此,我们定义了四个函数来检查每种获胜方式。
在 2 处,我们定义了 horizontal4(),它检查玩家是否通过成功地将四个棋子水平连接成一排赢得游戏。在这个函数中,我们创建了变量 win 并将其默认值设为 False。函数接着检查玩家是否将四个棋子水平连接成一排。如果是,win 的值会变为 True。当调用 horizontal4() 函数时,它返回存储在变量 win 中的值。让我们来看一下这个函数的详细情况。
我们将使用 x = col-1 和 y = row-1 将游戏板上的列号和行号转换为 occupied 列表中的索引。列号为 col 和行号为 row 的单元格对应于 occupied[x][y]。为了简便起见,本章其余部分我们将称这个单元格为 [x][y]。
玩家可以通过四种方式将四个棋子水平连接:
-
单元格
[x-3][y]、[x-2][y]和[x-1][y]的颜色与单元格[x][y]相同。 -
单元格
[x-2][y]、[x-1][y]和[x+1][y]的颜色与单元格[x][y]相同。 -
单元格
[x-1][y]、[x+1][y]和[x+2][y]的颜色与单元格[x][y]相同。 -
单元格
[x+1][y]、[x+2][y]和[x+3][y]的颜色与单元格[x][y]相同。
因此,我们定义了一个变量 dif 来遍历四个值 (-3, -2, -1, 0)。对于每个 dif 的值,我们检查四个单元格——[x+dif][y]、[x+dif+1][y]、[x+dif+2][y] 和 [x+dif+3][y]——是否具有相同的颜色。如果是,我们将 win 的值改为 True。
在此过程中,我们需要处理 IndexError 的例外情况,因为例如,x+3 的值可能是 8,但棋盘只有七列。如果我们不处理 IndexError 的例外情况,脚本在检查玩家是否赢得游戏的过程中会崩溃。
此外,我们确保所有索引都没有负值,因为负索引在 Python 中有非常特定的含义。在 Python 中,负索引会绕回到列表的开头,而不是在末尾掉出。例如,索引 -1 在 Python 中表示列表中的最后一个元素,-2 表示倒数第二个,以此类推。负索引不会引发 IndexOutOfBounds 错误,但它的行为也不会如你预期那样。
让我们看一个具体的例子:对于 x = 1 和 y = 2,当脚本检查单元格 [x-3][2] 时,它实际上会查看单元格 [-2][2],而这个单元格实际上是 [5][2],因为 -2 指的是 x 中倒数第二个值,即 5(也就是第六列,因为一共有七列)。因此,我们在函数中加入条件 x+dif >= 0 以确保没有任何地方出现负索引。
最后,我们在四种通过水平连接四个棋子的获胜情况中使用try和except。如果我们只为所有四种获胜情况使用一组try和except,那么每当发生IndexError时,脚本会跳过所有剩余的情况,直接进入except分支。这样就会导致脚本无法识别许多获胜的情况。
同样地,我们定义了vertical4()来检查通过竖直方向连接四个棋子是否获胜。然后,forward4()检查前斜线方向的获胜情况,back4()检查后斜线方向的获胜情况。
在第 4 行,我们定义了win_game(),该函数检查 13 种获胜场景中的任何一种(四个水平, 一个垂直,四个前斜线方向,四个后斜线方向)。在win_game()中,我们创建了变量win并将其默认值设为False。该函数首先将列和行号col和row转换为occupied列表中的索引x和y。然后,该函数调用先前定义的四个函数来检查玩家是否可能获胜。如果有任何一个函数返回True,win的值将变为True,并且每当调用win_game()时,它将返回True。
现在让我们查看脚本的后半部分(我们将其保存为conn_click.py),见清单 11-5。
`--snip--`
# Count the number of rounds
1 rounds=1
# Define a function conn() to place a disc in a cell
def conn(x,y):
# Declare global variables
2 global turn, rounds, validinputs
# Calculate the column number based on x and y values
if -350<x<350 and -300<y<300:
col = int((x+450)//100)
else:
print('You have clicked outside the game board!')
# Check if it's a valid move
if col in validinputs:
# Calculate the lowest available row number in that column
row = len(occupied[col-1])+1
`--snip--`
# Go to the cell and place a dot of the player's color
t.up()
t.goto(xs[col-1],ys[row-1])
t.dot(80,turn)
t.update()
# Add the move to the occupied list to keep track
occupied[col-1].append(turn)
# Check if the player has won
3 if win_game(col, row, turn) == True:
# If a player wins, invalid all moves, end the game
validinputs = []
messagebox.showinfo\
("End Game",f"Congrats player {turn}, you won!")
# If all cells are occupied and no winner, it's a tie
elif rounds == 42:
messagebox.showinfo("Tie Game","Game over, it's a tie!")
# Counting rounds
rounds += 1
# Update the list of valid moves
4 if len(occupied[col-1]) == 6:
validinputs.remove(col)
# Give the turn to the other player
if turn == "red":
turn = "yellow"
else:
turn = "red"
# If col is not a valid move, show error message
5 else:
messagebox.showerror("Error","Sorry, that's an invalid move!")
# Bind the mouse click to the conn() function
t.onscreenclick(conn)
t.listen()
`--snip--`
清单 11-5:脚本的后半部分,用于禁止无效操作并宣布胜利和平局
在第 1 行,我们创建了变量rounds,用来追踪游戏中进行的回合数,这与棋盘上棋子的数量相对应,以便在回合数达到 42 时可以宣布平局。
我们将conn()中的第 2 行修改为声明三个全局变量,这样它们的值就可以在函数内部和外部都能被识别。在第 3 行,我们调用win_game()来检查是否有人获胜。如果是,我们将validinputs更改为空列表,这样就无法再进行任何操作。一个消息框会弹出,显示恭喜红方玩家,您赢了!或恭喜黄方玩家,您赢了!
图 11-3 显示了红方玩家赢得了比赛。

图 11-3:红方获胜!较深色的棋子是红色,较浅色的是黄色。
如果没有人获胜,且rounds达到 42,脚本将声明平局(图 11-4)。

图 11-4:平局
如果没有玩家获胜或游戏没有平局,我们将rounds的值加一,并将轮到的玩家交给另一个玩家。我们还会更新有效操作的列表。如果当前列的棋子数达到六个,我们会从validinputs列表中移除该列的编号。
在游戏过程中,如果玩家点击了一个无效的单元格,第 5 行,消息框将显示抱歉,这是一个无效的操作!(图 11-5)。

图 11-5:一个无效的移动
语音控制版本
现在我们准备添加语音控制功能了!
首先,我们将计算机设为你的对手。在你作为红方落子后,计算机会随机选择一个黄方动作,直到游戏结束。一旦你了解了与计算机对战的方式,语音控制游戏中与其他人对战就非常简单了。我将这作为章节末的练习,脚本可以在书本资源网站上找到。
从书本资源中下载conn_hs.py并将其保存在章节文件夹中。清单 11-6 展示了conn_hs.py和conn_click.py之间的差异。
import turtle as t
from time import sleep
from tkinter import messagebox
from random import choice
# Import functions from the local package
from mptpkg import voice_to_text, print_say
# Set up the screen
`--snip--`
# Create a list of valid moves
validinputs = ['1','2','3','4','5','6','7']
`--snip--`
# Add a dictionary of words to replace
to_replace = {'number ':'', 'cell ':'',
'one':'1', 'two':'2', 'three':'3',
'four':'4', 'for':'4', 'five':'5',
'six':'6', 'seven':'7'}
# Start an infinite loop to take voice inputs
1 while True:
# Ask for your move
print_say(f"Player {turn}, what's your move?")
# Capture your voice input
inp = voice_to_text().lower()
print_say(f"You said {inp}.")
for x in list(to_replace.keys()):
inp = inp.replace(x, to_replace[x])
# If it is not a valid move, try again
if inp not in validinputs:
print_say("Sorry, that's an invalid move!")
# If your voice input is a valid move, play the move
2 else:
col = int(inp)
# Calculate the lowest available row number in that column
row = len(occupied[col-1])+1
# Show the disc fall from the top
if row<6:
for i in range(6,row,-1):
fall.goto(xs[col-1],ys[i-1])
fall.dot(80,turn)
t.update()
sleep(0.05)
fall.clear()
# Go to the cell and place a dot of the player's color
t.up()
t.goto(xs[col-1],ys[row-1])
t.dot(80,turn)
t.update()
# Add the move to the occupied list to keep track
occupied[col-1].append(turn)
# Check if the player has won
if win_game(col, row, turn) == True:
# If a player wins, invalid all moves, end the game
validinputs = []
3 print_say(f"Congrats player {turn}, you won!")
messagebox.showinfo/
("End Game",f"Congrats player {turn}, you won!")
break
# If all cells are occupied and no winner, it's a tie
elif rounds == 42:
print_say("Game over, it's a tie!")
messagebox.showinfo("Tie Game","Game over, it's a tie!")
break
# Counting rounds
rounds += 1
# Update the list of valid moves
if len(occupied[col-1]) == 6:
validinputs.remove(str(col))
# Give the turn to the other player
if turn == "red":
turn = "yellow"
else:
turn = "red"
# The computer randomly selects a move
4 if len(validinputs)>0:
col = int(choice(validinputs))
print_say(f'The computer chooses column {col}.')
# Calculate the lowest available row number in that column
row = len(occupied[col-1])+1
# Show the disc fall from the top
if row < 6:
for i in range(6,row,-1):
fall.goto(xs[col-1],ys[i-1])
fall.dot(80,turn)
update()
sleep(0.05)
fall.clear()
# Go to the cell and place a dot of the player's color
t.up()
t.goto(xs[col-1],ys[row-1])
t.dot(80,turn)
t.update()
# Add the move to the occupied list to keep track
occupied[col-1].append(turn)
# Check if the player has won
if win_game(col, row, turn) == True:
# If a player wins, invalid all moves, end the game
validinputs = []
5 print_say(f"Congrats player {turn}, you won!")
messagebox.showinfo\
("End Game",f"Congrats player {turn}, you won!")
break
# If all cells are occupied and no winner, it's a tie
elif rounds == 42:
print_say("Game over, it's a tie!")
messagebox.showinfo("Tie Game","Game over, it's a tie!")
break
# Counting rounds
rounds += 1
# Update the list of valid moves
if len(occupied[col-1])==6:
validinputs.remove(str(col))
# Give the turn to the other player
if turn == "red":
turn = "yellow"
else:
turn = "red"
t.done()
`--snip--`
清单 11-6:语音控制版 Connect Four 游戏的脚本亮点
我们导入了一些额外的模块。来自random模块的choice()函数让计算机随机选择一个动作来与您对战。我们还从本地包mptpkg中导入了本地的print_say()和voice_to_text()函数,用于处理语音控制功能。
这次,我们将使用字符串值而不是整数来表示列表validinputs中的七个列号,因为语音输入本质上是字符串变量,并且在许多情况下,尝试将语音输入转换为整数会导致脚本崩溃。
在第 1 步,我们开始了一个无限的while循环。在每次迭代中,脚本会大声询问你的动作。你通过麦克风说出你的动作,脚本捕捉到你的语音命令并将其存储在inp中。
我们对voice_to_text()进行了些微调整,使其对你的语音命令更加敏感,正如第十章中所做的那样(参见清单 10-6 以作提醒)。此外,脚本总是将number four解读为number for,因此我们用4替换for,以便从脚本获得更好的响应。
如果你的语音命令不在validinputs中,脚本会大声提醒你:“抱歉,那是一个无效的动作!”我已经将无效的语音输入提到前面,使得if和else分支在脚本中彼此靠近,这样你就能更容易理解逻辑。如果这两个分支相距太远,很容易在长长的代码行中迷失。
如果你的语音命令是有效的动作,脚本会按指示放置棋子,让棋子落到列中的最低可用位置,将该单元格号码添加到你已占用单元格的列表中,并将该单元格号码从有效输入列表中移除,等等。
脚本接着会检查你是否赢得了游戏,如果是,它会大声祝贺你。如果没有,它将检查是否平局,并相应地宣布。
当你的回合结束,并且如果你没有赢得游戏或平局,计算机会从validinputs中随机选择一个动作与您对战,进行该动作,并检查计算机是否赢得了游戏。它还会检查是否平局。
这是一次与游戏互动的打印消息:
Player red, what's your move?
You said number four.
The computer chooses column 2.
Player red, what's your move?
You said number four.
The computer chooses column 2.
Player red, what's your move?
You said number four.
The computer chooses column 2.
Player red, what's your move?
You said number four.
Congrats player red, you won!
图 11-6 显示了我获胜的游戏。

图 11-6:红方赢得了语音控制版本
总结
在这一章中,你创建了一个语音控制的图形化 Connect Four 游戏,并且它会用人类的声音与你对话。你设置了游戏板和机制,就像在第十章中一样,不过这一次你为游戏添加了动画效果。
你学会了如何让 Python 判断玩家是否赢得了游戏。在此过程中,你学习了列出所有获胜的情况,并使用脚本检查每一个。你还学会了如何正确使用异常处理,避免负索引导致脚本出错。
你添加了语音识别和文本转语音功能,并对代码进行了重构,以确保在添加新功能时代码仍然易于阅读。在接下来的几章中,你将创建更多的语音控制图形游戏,并使它们变得更加智能。
章节结束练习
-
修改 conn_board.py 使得六个行号出现在屏幕右侧,顶行是 6,底行是 1。并且将行号的 x 坐标设置为 325。
-
修改 disc_fall.py 使得圆盘以两倍的速度下落。
-
修改 conn_click.py 使得玩家仅通过横向或对角线连接四个相同颜色的圆盘来获胜,而不是通过垂直方向。
-
当前,当你使用最终版的 conn_hs.py 玩 Connect Four 时,如果你想将圆盘放在 4 列,你可以说“number four”或“four”。修改脚本,使得你也可以说“column four”将圆盘放入该列。
-
修改 conn_hs.py 使得你与一个人对战,而不是与计算机对战。
猜单词游戏

在这一章中,你将构建一个语音控制的图形化猜单词游戏。这是一个有趣的挑战,因为在玩猜单词时,玩家通常会说得很快,因此我们需要调整脚本的听力功能。
和往常一样,我们将回顾游戏规则并绘制游戏板;这个游戏板使用六个硬币代表你的六次猜测。你将学习如何将图片加载到 Python 脚本中,并在屏幕上创建多个图像。你还将学会让图像一个接一个地消失。
我们将通过书面输入开始游戏。然后,当游戏运行良好时,我们将添加语音识别和文本转语音功能。
本章中的所有脚本可以在本书的资源页面上找到:www.nostarch.com/make-python-talk/。首先为本章创建文件夹 /mpt/ch12/。
游戏规则
我们的猜单词游戏是基于猜字游戏(hangman)的一个简单版本。我们的游戏只会呈现四个字母的单词,以简化游戏,但当你熟悉其工作原理时,应该尝试稍后进行适配。让我们先来回顾一下游戏规则。
与“刽子手”游戏类似,我们的猜单词游戏涉及两名玩家。第一名玩家想出一个单词,并画出与单词字母数量相等的虚线。第一名玩家还在屏幕中央画出六个硬币,代表第二名玩家允许犯错的六次机会。
第二名玩家通过一次猜一个字母来试图猜出单词。如果猜中的字母在单词中,第一名玩家会在正确的位置填入该字母。如果猜中的字母不在单词中,第一名玩家将在屏幕中间擦掉一个硬币。如果第二名玩家在六次错误猜测之前完成了单词,他们就赢得了游戏。如果该玩家在用完六次错误机会之前没有猜出单词,他们就输了。
绘制游戏板
我们的游戏板将预加载四个虚线,表示单词的字母。我们还将在屏幕上显示incorrect guesses(猜错的字母)消息。打开你的 Spyder 编辑器,输入代码并保存为 Listing 12-1,文件名为guess_word_board.py。
import turtle as t
# Set up the board
t.setup(600,500)
t.hideturtle()
t.tracer(False)
t.bgcolor("lavender")
t.title("Guess the Word Game in Turtle Graphics")
# Define a variable to count how many guesses left
1 score = 6
# Create a second turtle to show guesses left
left = t.Turtle()
left.up()
left.hideturtle()
left.goto(-290,200)
left.write(f"guesses left: {score}",font=('Arial',20,'normal'))
# Put incorrect guesses on top
t.up()
t.goto(-290,150)
t.write("incorrect guesses:",font=('Arial',20,'normal'))
# Put four empty spaces for the four letters at bottom
2 for x in range(4):
t.goto(-275+150*x,-200)
t.down()
t.goto(-175+150*x,-200)
t.up()
t.done()
try:
t.bye()
except t.Terminator:
print('exit turtle')
Listing 12-1:绘制猜单词游戏板的 Python 脚本
我们导入了turtle模块,并将屏幕设置为 600 x 500 像素,背景为薰衣草色。标题将显示Guess the Word Game in Turtle Graphics。注意,我们在setup()中省略了最后两个参数,因此游戏板默认会出现在计算机屏幕的中央。
在第 1 步,我们创建了一个变量score来跟踪玩家剩余的猜测次数。它的初始值为 6。在游戏中,每当玩家猜错一个字母时,值将减少 1。我们还创建了一个新的海龟对象,命名为left,表示剩余的猜测次数。我们使用这个新的海龟对象来写下玩家剩余的机会,并擦除之前写的内容。通过使用新的海龟对象,我们限制了需要重新绘制的对象数量。
接下来,我们添加了文本incorrect guesses,它将显示玩家猜错的字母。我们在游戏板的底部绘制了四个虚线,用来表示单词中的四个字母。运行脚本后,你应该能看到一个与图 12-1 类似的游戏板。

图 12-1:猜单词游戏的游戏板
文本版本
在本节中,你将把六个硬币放在屏幕上,并允许玩家通过键盘输入字母。接下来,你将判断玩家是赢了还是输了。这样就完成了猜单词游戏的无声版本。
加载硬币
你将在屏幕中央放置六个硬币。在此过程中,你将学习如何将图片加载到脚本中,调整图片的大小,甚至可以根据需要放置任意数量的对象到海龟屏幕上。如前所述,每个硬币对应一个错误的猜测。
从书籍资源中下载图片文件cash.png并将其放入章节文件夹中。打开guess_word_board.py,将示例 12-2 中高亮的代码添加进去,然后将新脚本保存为show_coins.py,并将其保存在包含cash.png的相同章节文件夹中。
`--snip--`
from tkinter import PhotoImage
from time import sleep
`--snip--`
# Put four empty spaces for the four letters at bottom
for x in range(4):
t.goto(-275+150*x,-200)
t.down()
t.goto(-175+150*x,-200)
t.up()
# Load a picture of the coin to the script
1 coin = PhotoImage(file="cash.png").subsample(10,10)
t.addshape("coin", t.Shape("image", coin))
# Create six coins on screen
2 coins = [0]*6
for i in range(6):
coins[i] = t.Turtle('coin')
coins[i].up()
coins[i].goto(-100+50*i,0)
t.update()
3 sleep(3)
# Make the coins disappear one at a time
for i in range(6):
coins[-(i+1)].hideturtle()
t.update()
sleep(1)
t.done()
`--snip--`
示例 12-2:显示和移除金币的脚本
我们从tkinter模块中导入PhotoImage()类,从time模块中导入sleep()函数。然后我们使用PhotoImage()加载cash.png。我们使用subsample()来缩放图像至所需大小。在这个例子中,我们使用缩放因子(10,10),这意味着图片的宽度和高度都是原始图片的十分之一。
在第 2 步中,我们通过使用[0]*6创建了一个包含六个元素的列表coins。如果你打印出这个列表,它将是这样的:
[0, 0, 0, 0, 0, 0]
我们稍后会修改这些元素;0值只是占位符。
接下来,我们为coins中的每个元素创建一个新的海龟。然后我们让这些金币海龟移动到屏幕中心并水平排列。为了演示如何加载和隐藏金币,我们让它们在屏幕上停留三秒钟后,再通过调用turtle模块中的hideturtle()逐个将它们从屏幕上隐藏,顺序从最后一个开始。
图 12-2 显示了在前三秒内的屏幕,金币已排成一行。

图 12-2:在猜字游戏板上显示金币
猜字母
游戏的下一个版本将使用 15 个四个字母的单词,这些单词来自于诺特丹大学 Barry Keating 教授网站上列出的最常用四个字母的单词列表(https://bit.ly/3g7z7cg)。Keating 教授在商业预测和数据挖掘领域做了大量工作。他还是畅销教材《预测与预测分析》(McGraw Hill, 2018)的合著者。
在我们做出以下修改后,脚本将随机选择一个单词,要求你猜一个字母,并从 IPython 控制台接受输入。如果猜对了,字母会出现在对应位置的破折号上。如果字母在单词中出现了两次,它会在两个破折号上显示。如果字母不在单词中,它会显示在屏幕顶部的错误猜测列表中。我们将跳过在这个脚本中放置金币的部分,以便更容易地跟踪代码测试。
打开guess_word_board.py,将示例 12-3 中高亮的代码添加进去,然后将新脚本保存为guess_letter.py。
import turtle as t
from random import choice
`--snip--`
# Put four empty spaces for the four letters at bottom
for x in range(4):
t.goto(-275+150*x,-200)
t.down()
t.goto(-175+150*x,-200)
t.up()
t.update()
# Put words in a dictionary and randomly pick one
1 words = ['that', 'with', 'have', 'this', 'will', 'your',
'from', 'they', 'know', 'want', 'been',
'good', 'much', 'some', 'time']
word = choice(words)
# Create a missed list
2 missed = []
# Start the game loop
3 while True:
# Take written input
inp = input("What's your guess?\n").lower()
# Stop the loop if you key in "done"
if inp == "done":
break
# Check if the letter is in the word
4 elif inp in list(word):
# If yes, put it in the right position(s)
for w in range(4):
if inp == list(word)[w]:
t.goto(-250+150*w,-190)
t.write(inp,font=('Arial',60,'normal'))
# If the letter is not in the word, show it at the top
5 else:
missed.append(inp)
t.goto(-290+80*len(missed),60)
t.write(inp,font=('Arial',60,'normal'))
# Update everything that happens in the iteration
t.update()
try:
t.bye()
except t.Terminator:
print('exit turtle')
示例 12-3:将字母放到游戏板上的脚本
我们首先从random模块导入choice(),这样脚本可以从列表中随机选择一个单词。我们将 15 个单词放入列表words 1,并将随机选择的单词分配给word。在 2 处,我们创建了列表missed,用于保存所有猜错的字母。然后我们将脚本放入一个无限循环 3 中,不断接收你的文本输入。如果你想停止循环,可以在 Spyder IPython 控制台中输入done。
在 4 处,我们检查你猜测的字母是否出现在word中的字母里。我们使用list(),它将字符串变量作为输入,并将其分解为一个包含各个字母的列表;例如,命令list("have")会生成列表["h","a","v","e"]。
如果你猜测的字母在word中,函数会检查word中的每个字母,看你的猜测是否与该位置的字母匹配。如果匹配,函数将在相应位置显示该字母。
如果你的猜测不在word 5 中,字母会被添加到missed并显示在屏幕顶部的incorrect guesses部分。
请注意,我们还在此脚本中移除了t.done()这一行。这意味着,一旦你完成猜测并输入done,脚本将结束,屏幕上的所有内容将消失。
下面是与脚本的一次交互输出,当脚本从 15 个单词的列表中随机选择了单词have时,我输入的内容以粗体显示:
What's your guess?
**a**
What's your guess?
**b**
What's your guess?
**v**
What's your guess?
**v**
What's your guess?
**b**
What's your guess?
**h**
What's your guess?
**e**
What's your guess?
**f**
What's your guess?
**g**
What's your guess?
**h**
What's your guess?
**u**
What's your guess?
**done**
图 12-3 显示了结果屏幕。

图 12-3:一个带有字母的猜单词游戏板
它已经工作了,但你可能注意到有些地方需要改进。为了完成一个完整的猜单词游戏,我们需要让脚本执行以下操作:
-
防止玩家重复猜测同一个字母。在我之前的互动中,我重复猜测了b、v和h,浪费了猜测机会。
-
在单词完成时通知玩家。
-
玩家完成单词后停止接受输入。
-
将六个硬币显示在屏幕上,每当玩家猜错一个字母时,就移除一个硬币。
确定有效的猜测、胜利和失败
接下来,我们将禁止重复字母的猜测,如果你在错过少于六个字母的情况下完成了单词,就宣告胜利,否则宣告失败。
打开guess_letter.py,并在列表 12-4 中添加高亮的部分。然后将新脚本保存为guess_word.py。在guess_letter.py中的一段代码被修改并替换为新添加的代码块。如果你不确定有什么不同,可以从本书资源页面下载脚本guess_word.py。
import turtle as t
from random import choice
from tkinter import messagebox
from tkinter import PhotoImage
`--snip--`
# Create a missed list
missed = []
# Load a picture of the coin to the script
1 coin = PhotoImage(file = "cash.png").subsample(10,10)
t.addshape("coin", t.Shape("image", coin))
# Create six coins on screen
coins = [0]*6
for i in range(6):
coins[i] = t.Turtle('coin')
coins[i].up()
coins[i].goto(-100+50*i,0)
2 t.update()
# Prepare the validinputs and gotright lists
3 validinputs = list('abcdefghijklmnopqrstuvwxyz')
gotright = []
# Start the game loop
while True:
# Take written input
inp = input("What's your guess?\n").lower()
# Stop the loop if you key in "done"
if inp == "done":
break
# If the letter is not a valid input, remind
elif inp not in validinputs:
messagebox.showerror("Error","Sorry, that's an invalid input!")
# Otherwise, go ahead with the game
4 else:
# Check if the letter is in the word
if inp in list(word):
# If yes, put it in the right position(s)
for w in range(4):
if inp == list(word)[w]:
t.goto(-250+150*w,-190)
t.write(inp,font = ('Arial',60,'normal'))
gotright.append(inp)
# If got four positions right, the player wins
if len(gotright) == 4:
messagebox.showinfo\
("End Game","Great job, you got the word right!")
break
# If the letter is not in the word, show it at the top
5 else:
# Reduce guesses left by 1
score -= 1
# Remove a coin
coins[-(6-score)].hideturtle()
# Update the number of guesses left on board
left.clear()
left.write\
(f"guesses left: {score}",font = ('Arial',20,'normal'))
t.update()
missed.append(inp)
t.goto(-290+80*len(missed),60)
t.write(inp,font = ('Arial',60,'normal'))
if len(missed) == 6:
# If all six chances are used up, end game
messagebox.showinfo\
("End Game","Sorry, you used up all your six guesses!")
break
# Remove the letter from the validinputs list
validinputs.remove(inp)
# Update everything that happens in the iteration
t.update()
`--snip--`
列表 12-4:一个带有图形界面的猜单词游戏,接受书面输入
我们再次从tkinter Python 包中导入messagebox模块,以便能够在游戏屏幕上显示消息。
从 1 开始,我们在屏幕上显示六个硬币。我们更新屏幕,以确保我们放置的所有内容都能正确显示 2。
在 3 中,我们创建了validinputs列表,其中包含了字母表中的 26 个字母作为元素。在脚本后面,如果玩家猜测一个字母,我们会从列表中删除该字母,以避免同一个字母被猜测多次。我们还创建了一个空列表gotright,稍后我们将使用它来跟踪玩家在单词中正确猜中的位置数。
我们开始一个无限的while循环,每次迭代时请求你的键盘输入。如果你输入done,循环停止,脚本不再接收你的输入。如果你输入无效的内容(非字母或已经猜过的字母),脚本会弹出一个消息框,显示抱歉,这是无效输入!
如果你输入有效的内容 4,脚本会检查字母是否在单词中。如果是,脚本会检查单词中的四个位置,并对每一个匹配的字母,都会将该字母添加到gotright列表中。需要注意的是,由于同一个字母可以在单词中出现多次,所以一个字母可能会被多次添加到gotright列表中。
脚本接着检查gotright是否包含四个元素。如果是,说明所有四个字母都已正确猜出,脚本会弹出一个消息框,显示太棒了,你猜对了单词!
如果猜测的字母不在单词中 5,score的值减少 1,意味着玩家剩余的猜测次数减少。脚本会通过使用hideturtle()从屏幕上移除一个硬币。第二只海龟会清除它在屏幕上绘制的内容,并重新写上剩余的猜测次数。如果missed列表的长度达到了六个,一个消息框会出现,显示抱歉,你已经用完了所有六次猜测!
这是与脚本的一次交互,用户输入部分用粗体显示:
What's your guess?
**a**
What's your guess?
**o**
What's your guess?
**d**
What's your guess?
**c**
What's your guess?
**b**
What's your guess?
**k**
What's your guess?
**m**
我的失败游戏显示在图 12-4 中。

图 12-4:一个失败的猜单词游戏
语音控制版本
现在我们将基于已写输入版本的游戏,添加语音功能。下载guess_word_hs.py并将其保存在你的章节文件夹中。新代码在清单 12-5 中高亮显示。
`--snip--`
# Import functions from the local package
from mptpkg import voice_to_text, print_say
`--snip--`
# Start the game loop
1 while True:
# Ask for your move
print_say("What's your guess?")
# Capture your voice input
inp = voice_to_text().lower()
print_say(f"you said {inp}")
inp = inp.replace('letter ','')
# Say "stop listening" or press CTRL-C to stop the game
if inp == "stop listening":
break
# If the letter is not a valid input, remind
elif inp not in validinputs:
print_say("Sorry, that's an invalid input!")
# Otherwise, go ahead with the game
2 else:
# Check if the letter is in the word
if inp in list(word):
# If yes, put it in the right position(s)
for w in range(4):
if inp == list(word)[w]:
t.goto(-250+150*w,-190)
t.write(inp,font = ('Arial',60,'normal'))
gotright.append(inp)
# If got four positions right, the player wins
if len(gotright) == 4:
3 print_say("Great job, you got the word right!")
messagebox.showinfo\
("End Game","Great job, you got the word right!")
break
# If the letter is not in the word, show it at the top
else:
# Reduce guesses left by 1
score -= 1
# Remove a coin
coins[-(6-score)].hideturtle()
# Update the number of guesses left on board
left.clear()
left.write\
(f"guesses left: {score}",font = ('Arial',20,'normal'))
t.update()
missed.append(inp)
t.goto(-290+80*len(missed),60)
t.write(inp,font = ('Arial',60,'normal'))
if len(missed) == 6:
# If all six changes are used up, end game
4 print_say("Sorry, you used up all your six guesses!")
messagebox.showinfo\
("End Game","Sorry, you used up all your six guesses!")
`--snip--`
清单 12-5:一个图形化的猜单词游戏,支持语音输入
我们从本地包mptpkg中导入了常用函数:voice_to_text()和print_say()。因为我们已安装该包(以可编辑模式),所以不需要告诉系统在哪里找到它。
我们开始一个无限的while循环,在每次迭代中询问你选择的字母 1。你将你的猜测通过麦克风说出,脚本会捕捉到你的语音命令并将其存储在inp中。我们做了一些处理,以允许玩家说“字母 a”或仅说a。如果是前者,我们会将letter替换为空字符串,这样变量inp中只剩下a。
要停止while循环,你只需说:“停止监听。”如果你的猜测不在列表validinputs中,脚本会大声回答:“抱歉,那个是无效输入!”如果你的猜测在validinputs中,脚本会检查字母是否在单词里。这一次,当你在不漏掉六次的情况下完成单词时,游戏会说:“太棒了,你猜对了!”如果你猜错六次,语音会说:“抱歉,你已经用完了六次猜测!”
这是与脚本的一个对话,玩家成功猜出单词good,只漏掉了两个字母:
What's your choice?
you said letter a
What's your choice?
you said letter d
What's your choice?
you said letter f
What's your choice?
you said letter o
What's your choice?
you said letter g
Great job, you got the word right!
你可以在图 12-5 中看到屏幕。

图 12-5:赢得语音控制猜字游戏
总结
在本章中,你创建了一个语音控制的图形化猜字游戏,它能用人类的声音回应你。
你首先学习了如何绘制游戏板。然后你学会了将图片文件上传到脚本,并按你想要的大小缩放它。你使用这张图片在屏幕上创建了六个硬币,代表货币奖励,并让它们一个接一个地从屏幕上消失。你还学会了如何输入你的猜测并让它显示在屏幕上。你学会了如何禁止重复猜测同一个字母,以及如何判断玩家是赢了还是输了。
你添加了语音识别和文本转语音功能,使得游戏可以通过语音控制。在这个过程中,你学习了如何通过操作图片文件来创建图像,并学会了如何使用多个海龟来减少需要重新绘制的对象数量。
章末练习
-
修改show_coins.py,使得六个硬币的位置在垂直方向上比当前的位置下移 10 个像素,其他一切位置保持不变。
-
修改show_coins.py,使得最左边的硬币最先从屏幕上消失,最右边的硬币最后消失。
-
尝试弄明白下面这一行代码将产生什么结果。首先写下你的答案,然后在 Spyder 中运行代码进行验证。
list('Hi Python')
智能游戏:增加智能

在我们第十一章中创建的单人版“四连棋”游戏中,计算机总是随机选择一步。这让我们能够专注于游戏的语音识别和文本转语音功能。
然而,一旦你和随机计算机玩了几局游戏,你会开始想,是否有方法让我们的四连棋游戏更加具有挑战性。答案是肯定的,在这一章中,你将学习如何制作一个智能的四连棋对手。
在一种方法中,我们将让脚本思考三步,像人类玩游戏时那样:计算机走两步,玩家走一步。
在第一步中,计算机检查一步是否会立即导致游戏获胜。如果是,计算机将选择这一步。
在四连棋中思考两步意味着计算机会尽力防止对手在下一回合获胜。这是复杂的,因为有时计算机必须阻止某个位置,而其他时候又必须避免占据某个位置。计算机会区分这两种情况,既会阻挡某些步伐,也会避免某些步伐,以防止对手获胜。
通过三步思考,计算机会选择一条最有可能在三步内带来胜利的路径。在许多情况下,三步思考可以保证三步内获胜。特别是,如果有一步可以确保计算机在三步内获胜,计算机会选择这一作为最佳下一步。
第二种方法采用了一种可以归类为机器学习的方式。你将模拟百万场游戏,其中双方玩家随机选择步伐。然后记录游戏结果和中间步骤。通过这些数据,计算机会在每一步学习,选择最有可能导致胜利的步骤。
我们将评估这两种策略的有效性,并选择一种更难以战胜的策略。然后,我们会为智能四连棋加入语音识别和文本转语音功能。
在此过程中,我还将挑战你将相同的方法应用于第 267 页“章节末练习”中的井字游戏。和往常一样,所有脚本都可以在www.nostarch.com/make-python-talk/下载,你应为本章创建文件夹/mpt/ch13/。
三步思考策略
我们将首先使用鼠标点击版本的四连棋来加速脚本的测试。在我们加入“三步思考”策略之后,再将语音功能加入。
思考一步
在四连棋中思考一步很容易。计算机会检查所有可能的下一步,如果其中一步能立即赢得游戏,计算机会选择这一步。
从书中的资源下载conn_think1.py并保存到你的章节文件夹中。这个脚本基于第十一章中的conn_click.py,但我已经修改了代码,使得你在和一个自动化玩家对战,自动化玩家会思考一步,而不是与另一个人类玩家对战。
列表 13-1 展示了conn_think1.py中的关键部分。
`--snip--`
from random import choice
from copy import deepcopy
`--snip--`
# Define a horizontal4() function to check connecting 4 horizontally
1 def horizontal4(x, y, color, board):
win = False
for dif in (-3, -2, -1, 0):
try:
if board[x+dif][y] == color\
and board[x+dif+1][y] == color\
and board[x+dif+2][y] == color\
and board[x+dif+3][y] == color\
and x+dif >= 0:
win = True
except IndexError:
pass
return win
# Define a vertical4() function to check connecting 4 vertically
def vertical4(x, y, color, board):
`--snip--`
# Define a win_game() function to check if someone wins the game
2 def win_game(num, color, board):
win = False
# Convert column and row numbers to indexes in the list of lists board
x = num-1
y = len(board[x])-1
# Check all winning possibilities
if vertical4(x, y, color, board) == True:
win = True
if horizontal4(x, y, color, board) == True:
win = True
if forward4(x, y, color, board) == True:
win = True
if back4(x, y, color, board) == True:
win = True
# Return the value stored in win
return win
`--snip--`
# Define the best_move() function
3 def best_move():
# Take column 4 in the first move
if len(occupied[3]) == 0:
return 4
# If only one column has free slots, take it
if len(validinputs) == 1:
return validinputs[0]
# Otherwise, see what will happen in the next move hypothetically
4 winner = []
# Go through all possible moves and see if there is a winning move
for move in validinputs:
tooccupy = deepcopy(occupied)
tooccupy[move-1].append('red')
if win_game(move,'red',tooccupy) == True:
winner.append(move)
# If there is a winning move, take it
if len(winner)>0:
return winner[0]
5 def computer_move():
global turn, rounds, validinputs
# Choose the best move
col = best_move()
if col == None:
col = choice(validinputs)
# Calculate the lowest available row number in that column
row = 1+len(occupied[col-1])
`--snip--`
# Check if the player has won the game
6 if win_game(col, turn, occupied) == True:
`--snip--`
# Computer moves first
computer_move()
# Define a function conn() to place a disc in a cell
def conn(x,y):
# Declare global variables
global turn, rounds, validinputs
`--snip--`
7 if win_game(col, turn, occupied) == True:
`--snip--`
# Computer moves next
if len(validinputs)>0:
computer_move()
`--snip--`
列表 13-1:在四连棋游戏中思考一步。
我们导入了所有需要的模块。特别地,我们从random模块导入choice(),从copy模块导入deepcopy()。copy模块是 Python 标准库的一部分,因此无需安装。
为了寻找最佳策略,我们将提前一步预测在采取某些行动时可能发生的情况。我们需要deepcopy()来复制列表而不改变原始列表。在此脚本中,复制列表时不能简单地使用赋值语句。Python 中的赋值语句会创建与原始列表对象的链接,因此,如果我们修改副本,就会同时修改原始列表。修改原始列表不是我们所期望的,这会导致意外的行为。
在第 1 行,我们将horizontal4(x,y,color,board)函数改得更通用,以便它能应用于任何四个参数。在脚本后面,我们会使用它检查某些移动是否通过水平排列四个棋盘来获胜。我们也用类似的方式定义了vertical4()、forward4()和back4()函数。
在第 2 行,我们定义了win_game(num,color,board),它会检查玩家是否在之前的四种情况中获胜。我们还省略了行号作为参数,因为它会通过参数board来推断。
主要的操作在best_move()函数中,从第 3 行开始。此函数会为计算机(红色玩家)寻找最佳的行动。如果第 4 列为空,计算机会选择中心列。由于红色玩家先行,这行代码确保游戏的第一步总是中心列 4,从而让先手玩家获得优势。
如果只剩下一步(即六列已满,只有一列有空格),就没有必要再寻找最佳移动,因此计算机会采取唯一剩余的动作。
如果剩下多个动作,函数会检查每个可能的移动,看是否有任何一项会立即让计算机获胜。脚本创建了一个列表winner来包含潜在的获胜移动。我们遍历所有可能的下一步。我们使用win_game()来检查某个移动是否会赢得游戏。如果是,这个移动会被添加到winner列表中。接着,函数会检查winner是否为空,如果不为空,计算机会选择列表中的第一个可用动作。
然后我们定义了computer_move()函数。当调用时,该函数会让计算机执行由best_move()生成的动作。计算机然后将棋盘放置在相应的列中。棋盘放置完后,脚本使用win_game()来检查此举是否会赢得游戏。
计算机会先行一步。之后,我们定义了conn(),让你可以点击屏幕进行你的移动。脚本会检查你的移动是否获胜。如果游戏未结束,计算机会在你之后移动。
反复运行脚本并与计算机对战。你会注意到,如果有获胜的机会,计算机会总是选择获胜的动作。例如,在图 13-1 的左侧,红色玩家可以通过选择第 7 列来赢得比赛。计算机会提前一步,做出获胜的决策。


图 13-1:一个提前思考一步的“连接四”游戏
提前思考两步
在“连接四”中提前思考两步有点复杂。计算机的下一步可以是阻止对手(即你)获胜,或者帮助对手在下一回合赢得游戏。
我们将这两种情况分开:如果计算机的动作能阻止对手获胜,脚本会执行该动作;如果计算机的动作能帮助对手获胜,脚本会避免该动作。让我们通过示例来演示这两种情况。
需要避免的动作
在这个例子中,计算机应该避免某个动作,以防对手在下一轮获胜。
在图 13-2 的左边,轮到红色玩家。如果红色玩家选择第 6 列作为下一步,按照图中右侧的展示,对手可以在下一回合获胜。因此,红色玩家应该避免这个动作。


图 13-2:在这个例子中,红色玩家应该避免选择第 6 列。
这里,红色玩家已经走了一步,允许黄色玩家获胜。我们可以用这个规则避免这种获胜情况:如果你做出下一步* x,并且你的对手在两步之后将棋子放在同一列 x并赢得游戏,那么你应该在下一步避免走 x*。
需要阻止的动作
在下一个例子中,计算机应该阻止某个动作,以防对手在两步之内获胜。
在图 13-3 的左侧,轮到红色玩家。如果红色玩家下一步不选择第 3 列,对手可以选择第 3 列并在下一回合获胜。因此,红色玩家应该阻止这个动作。


图 13-3:红色玩家应该阻止第 3 列。
这里,红色玩家选择了另一个动作——第 6 列——并输了游戏。所以规则如下:如果红色玩家做出下一步* x,而黄色对手可以在两步内做出不同的动作 y并获胜,那么红色应该在下一步阻止黄色的动作 y*。
实现两步思考策略
让计算机通过使用刚才讨论的三种技术来提前思考两步(其中一种是思考一步,另外两种是思考两步)。
打开conn_think1.py,将其中的best_move()替换为在列表 13-2 中定义的新best_move()函数,并将新脚本保存为conn_think2.py到你的章节文件夹(或者你也可以从书籍资源中下载它)。
`--snip--`
# Define the best_move() function
def best_move():
# Take column 4 in the first move
if len(occupied[3]) == 0:
return 4
# If only one column has free slots, take it
if len(validinputs) == 1:
return validinputs[0]
# Otherwise, see what will happen in the next move hypothetically
winner = []
# Go through all possible moves and see if there is a winning move
1 for move in validinputs:
tooccupy = deepcopy(occupied)
tooccupy[move-1].append('red')
if win_game(move,'red',tooccupy) == True:
winner.append(move)
# If there is a winning move, take it
if len(winner)>0:
return winner[0]
# If no winning move, look two steps ahead
2 if len(winner) == 0 and len(validinputs)>=2:
loser = []
# Check if your opponent has a winning move
for m1 in validinputs:
for m2 in validinputs:
if m2 != m1:
tooccupy = deepcopy(occupied)
tooccupy[m1-1].append('red')
tooccupy[m2-1].append('yellow')
if win_game(m2, 'yellow',tooccupy) == True:
winner.append(m2)
if m2 == m1 and len(occupied[m1-1]) <= 4:
tooccupy2 = deepcopy(occupied)
tooccupy2[m1-1].append('red')
tooccupy2[m2-1].append('yellow')
if win_game(m2,'yellow',tooccupy2) == True:
loser.append(m2)
# If your opponent has a winning move, block it
if len(winner)>0:
return winner[0]
# If you can make a move to help your opponent to win, avoid it
3 if len(loser)>0:
myvalids = deepcopy(validinputs)
for i in range(len(loser)):
myvalids.remove(loser[i])
if len(myvalids)>0:
return choice(myvalids)
`--snip--`
列表 13-2:让计算机提前思考两步。
在新定义的函数best_move()中,脚本根据当前棋盘上的棋子搜索最佳移动。如果这是游戏的第一步,函数会选择中间的列。如果只剩下一步,函数将定义最佳移动为剩下的唯一一步。
如果剩下多个着法,函数会检查每个可能的着法,看看是否有任何一个能让红方玩家(计算机)立刻获胜 1。如果有,函数将返回这个着法作为最佳着法,并将其存储在winner中。如果没有,函数将向前看两步,看看对手是否能在两步之内获胜 2。
该函数检查两种不同的情况:如果红方玩家的第一步m1和黄方玩家的第二步m2在两步后导致黄方获胜,我们将把着法m2添加到winner列表。如果红方玩家的第一步m1和黄方玩家的第二步m2=m1导致黄方在两步内获胜,我们将把着法m2添加到loser列表。
脚本检查winner是否为空。如果不为空,计算机将选择对手的获胜着法来阻止对手获胜。否则,计算机将检查loser列表是否为空。如果不为空,计算机将避免所有loser中的元素,以免帮助对手获胜 3。
运行conn_think2.py并多次与计算机对战。你会注意到游戏有了改进,因为计算机现在可以考虑两步之后的局面,并尝试阻止你在下一轮获胜。
三步先思考
这一部分将允许计算机在下棋之前考虑最多三步。如果计算机在下一步没有获胜的着法,并且对手在两步后也没有获胜的着法,计算机将考虑三步之后的局面。
计算机将采取最有可能在三步内获胜的下一步。特别是,如果有一着能够保证计算机在三步内获胜,计算机将选择该步作为最佳着法。让我们用一个例子来演示。
三步内获胜的例子
脚本conn_think2.py比conn_think1.py更难击败,但并非不可能。一个高水平的玩家会注意到,计算机错过了一些能够导致三步内获胜的着法。
这是一个例子。在图 13-4 的左侧,轮到计算机(红方玩家)下棋。如果计算机在第 3 列落子,计算机保证在下一步获胜,因为对手(黄方玩家)只能阻止第 1 列或第 5 列中的其中一列。然后,计算机可以占据另外一列(第 5 列或第 1 列),在第三步赢得比赛。
而是,计算机选择了第 6 列,如图 13-4 右侧所示,错过了保证获胜的机会。


图 13-4:计算机(红方玩家)未能做出能够保证获胜的着法。
因此,我们应该进一步改进游戏。你将构建一个能够考虑三步先思考的游戏。
实现三步先思考策略
让我们允许计算机考虑最多三步之后的局面。
打开 conn_think2.py,将新定义的 validmoves() 函数和 列表 13-3 中的高亮部分添加到 best_move() 函数,并将新脚本保存为 conn_think.py 到你的章节文件夹中。或者,你可以从书籍资源中下载该脚本。这是我们思考提前策略的完整脚本。
`--snip--`
# Define the validmoves() function to ensure three future moves
# will not cause any column to have more than six discs in it
def validmoves(m1,m2,m3,occupied):
validmove = False
if m1 == m2 == m3 and len(occupied[m1-1]) <= 3:
validmove = True
if m1 == m2 and m2 != m3 and len(occupied[m1-1]) <= 4:
validmove = True
if m1 == m3 and m2 != m3 and len(occupied[m1-1]) <= 4:
validmove = True
if m3 == m2 and m2 != m1 and len(occupied[m3-1]) <= 4:
validmove = True
return validmove
# Define the best_move() function
def best_move():
# Take column 4 in the first move
`--snip--`
# Otherwise, look 3 moves ahead
1 if len(winner) == 0 and len(loser) == 0:
# Look at all possible combinations of 3 moves ahead
for m1 in validinputs:
for m2 in validinputs:
for m3 in validinputs:
if validmoves(m1,m2,m3,occupied) == True:
tooccupy3 = deepcopy(occupied)
tooccupy3[m1-1].append('red')
tooccupy3[m2-1].append('yellow')
tooccupy3[m3-1].append('red')
if win_game(m3, 'red', tooccupy3) == True:
winner.append(m1)
# See if there is a move now that can lead to winning in 3 moves
if len(winner)>0:
2 cnt = {winner.count(x):x for x in winner}
maxcnt = sorted(cnt.keys())[-1]
return cnt[maxcnt]
`--snip--`
列表 13-3:让计算机提前考虑最多三步。
我们首先定义 validmoves(m1,m2,m3,occupied) 来确保游戏板上未来三个假设的步伐 m1、m2 和 m3(由列表 occupied 表示)不会导致任何列中有超过六个棋子。如果这三步导致七列中的任何一列含有超过六个棋子,函数将返回 False;否则,它返回 True。
与 conn_think2.py 中一样,计算机首先检查是否可以立即做出获胜的步伐。如果可以,它将采取该步伐。如果不能,它将检查是否可以在两步之后为对手制造获胜的机会。如果可以,计算机会尽力阻止对手。
如果对手在两步之内没有获胜的机会,计算机会考虑三步之内的情况 1。它检查所有三步的组合:计算机的下一步 m1;对手两步之内的步伐 m2;以及计算机第三步的移动 m3。如果某个组合能导致玩家获胜,那么下一步 m1 就会被添加到 winner 列表中。
然而,仅仅因为某个步伐 x 出现在 winner 中,并不意味着这个步伐能够确保计算机在三步内获胜,因为它不能保证对手在第二步选择 m2。此外,winner 中可能包含多个值。因此,best_move() 函数会寻找 winner 中最常见的值,因为这就是最可能在三步内为计算机带来胜利的步伐。
与 Python 中的大多数情况一样,找到列表中最常见的值有很多种方法。 我们利用一个技巧,称为 列表推导式,来创建一个内联字典 cnt。 在这个字典中,键是一个动作在 winner 中出现的次数,值是该动作 2。例如,如果 winner 有六个元素 [7, 6, 6, 5, 5, 5],则字典 cnt 将是 {1:7, 2:6, 3:5}。然后我们对 cnt 中的键进行排序,以找到出现频率最高的值,并将其称为 maxcnt。在这里,maxcnt 的值为 3,因为出现次数最多的值是三次。最后,我们使用 maxcnt 来获取频率最高的字典元素。在此情况下,5 这个步伐在 winner 中出现的频率最高。
如果你运行 conn_think.py 并进行游戏,你会发现计算机几乎不可能被击败。如果你一切都做对,你可以打平比赛。一旦你走错一步,计算机会抓住机会并赢得比赛。
机器学习策略
另一种让四子棋更智能的方法是让计算机从实际的游戏结果中学习。你将生成一百万局游戏,在这些游戏中,两位玩家都采用随机的步骤。你将记录每局游戏的中间步骤和结果。计算机将使用这些游戏结果数据来设计最佳策略。
在每一步棋中,计算机会查看所有具有与当前棋盘相同游戏历史的游戏。它计算每个可能的下一步棋的平均结果,并选择最可能导致有利结果的那一步。
创建模拟游戏的数据集
机器学习策略的第一步是生成可供学习的数据。我们将模拟两个玩家选择随机步骤,并记录结果和达到该结果的步骤。尽管两名玩家的步骤是随机的,我们会多次重复游戏。通过大数法则,这些游戏中的随机性被消除。因此,结果数据对计算机预测一步棋的结果非常有用。
从书籍资源中下载conn_simulation.py。我在列表 13-4 中解释了该脚本。
from random import choice
import pickle
# Define a simulate() function to generate a complete game
def simulate():
occupied=[list(),list(),list(),list(),list(),list(),list()]
validinputs=[1,2,3,4,5,6,7]
# Define a horizontal4() function to check connecting 4 horizontally
def horizontal4(x, y, turn):
win=False
for dif in (-3, -2, -1, 0):
`--snip--`
1 def win_game(col, row, turn):
win=False
`--snip--`
# Return the value stored in win
return win
# The red player takes the first move
2 turn="red"
# Keep track of all intermediate moves
moves=[]
# Use winlose to record game outcome, default value is 0 (a tie)
winlose=[0]
# Play a maximum of 42 steps
3 for i in range(42):
# The player randomly selects a move
col=choice(validinputs)
row=len(occupied[col-1])+1
moves.append(col)
# Check if the player has won
if win_game(col, row, turn)==True:
if turn=='red':
winlose[0]=1
if turn=='yellow':
winlose[0]=-1
break
# Add the move to the occupied list to keep track
occupied[col-1].append(turn)
# Update the list of valid moves
if len(occupied[col-1])==6 and col in validinputs:
validinputs.remove(col)
# Give the turn to the other player
if turn=="red":
turn="yellow"
else:
turn="red"
# Record both game outcome and intermediate steps
return winlose+moves
# Simulate the game 1 million times and record all games
results=[]
4 for x in range(1000000):
result=simulate()
results.append(result)
# Save the simulation data on your computer
5 with open('conn_simulates.pickle', 'wb') as fp:
pickle.dump(results,fp)
# Read the data and print out the first 10 games
with open('conn_simulates.pickle', 'rb') as fp:
mylist=pickle.load(fp)
print(mylist[0:10])
列表 13-4:模拟一百万局四子棋游戏
我们首先定义simulate()。调用时,它会模拟一局完整的四子棋游戏,并记录每一步棋和游戏结果。为了节省时间,我们省略了游戏的图形部分。
我们定义了win_game()来检查玩家是否获胜 1。每局游戏中,红方玩家先行 2。我们创建了两个列表moves和winlose,分别记录中间步骤和游戏结果。
我们创建了一个游戏循环,最多迭代 42 次,因为每局四子棋游戏最多有 42 步 3。在每次迭代中,玩家随机选择一个步骤。该步骤被添加到moves中,以跟踪游戏的历史。在每一步,我们检查玩家是否获胜。如果是的话,如果获胜的是红方玩家,则记录结果为1;如果获胜的是黄方玩家,则记录结果为-1。默认结果是平局,这时记录0。
然后我们调用simulate()一百万次 4。每局游戏的结果保存在一个名为result的列表中,其第一个元素是游戏的结果(-1、1或0),后面跟着游戏的中间步骤。
百万局游戏的结果和中间步骤保存在conn_simulates.pickle中,供以后使用 5。我们打印出前 10 局游戏的结果,见列表 13-5。
[[1, 1, 7, 1, 5, 7, 6, 5, 1, 5, 7, 5, 2, 5],
[1, 5, 4, 2, 7, 5, 2, 5, 6, 2, 7, 5],
[1, 7, 3, 5, 5, 3, 7, 3, 7, 4, 2, 7, 7, 6],
[-1, 6, 7, 6, 6, 5, 1, 5, 3, 5, 7, 6, 5, 4, 2, 5, 7, 3, 4,
7, 1, 1, 6, 4, 5, 6, 1, 1, 4, 1, 7, 3, 3, 7, 2, 3, 2, 3, 4],
[-1, 1, 3, 5, 1, 4, 5, 4, 6, 2, 7, 3, 2, 3, 4, 2, 3],
[1, 6, 5, 7, 1, 3, 3, 1, 5, 5, 5, 2, 3, 6, 7, 2, 6, 3, 2, 7,
5, 4, 3, 7, 6, 7, 6, 6, 1, 2, 2, 4, 5, 4, 7, 3, 2, 1, 1, 4],
[1, 2, 5, 3, 5, 3, 4, 7, 7, 5, 3, 4, 2, 2, 2, 5, 4, 4, 4, 4, 6, 6],
[1, 2, 5, 6, 4, 6, 7, 5, 5, 7, 4, 1, 3, 6, 3, 2, 1, 7, 1, 6],
[1, 7, 4, 4, 6, 3, 1, 2, 2, 3, 3, 4, 6, 3, 6, 1, 3, 4, 1, 3, 7, 7, 5, 4],
[-1, 1, 4, 1, 4, 1, 2, 4, 5, 6, 6, 6, 3]]
列表 13-5:前 10 局模拟的四子棋游戏
例如,第一局游戏的输出是[1, 1, 7, 1, 5, 7, 6, 5, 1, 5, 7, 5, 2, 5]。第一个元素1表示红方玩家获胜。剩余的元素1, 7, 1...表示玩家将棋子放入的列,红黄玩家交替进行。红方玩家最终通过在第 5 列垂直连接四个红色棋子获胜。
应用数据
下一步是使用结果数据为计算机设计智能动作。每次移动时,计算机会查阅模拟数据,检索所有相同历史的游戏。它会搜索所有可能的下一步,找到最有利的结果并将其作为下一步。
下载conn_ml.py并将其保存在你的章节文件夹中。该脚本基于conn_think.py。列表 13-6 突出了它们的主要区别。
`--snip--`
# A history of moves made
moves_made=[]
# Obtain game data
with open('conn_simulates.pickle', 'rb') as fp:
gamedata=pickle.load(fp)
# Define the best_move() function
1 def best_move():
# Take column 4 in the first move
if len(occupied[3])==0:
return 4
# If there is only one column has free slots, use the column
if len(validinputs)==1:
return validinputs[0]
simu=[]
for y in gamedata:
if y[1:len(moves_made)+1]==moves_made:
simu.append(y)
# Now we look at the next move;
outcomes={x:[] for x in validinputs}
# We collect all the outcomes for each next move
for y in simu:
outcomes[y[len(moves_made)+1]].append(y[0])
# Set the initial value of bestoutcome
bestoutcome=-2;
# Randomly select a move to be best_move
best_move=validinputs[0]
# iterate through all possible next moves
for move in validinputs:
if len(outcomes[move])>0:
outcome=sum(outcomes[move])/len(outcomes[move])
# If the average outcome beats the current best
if outcome>bestoutcome:
# Update the bestoutcome
bestoutcome=outcome
# Update the best move
best_move=move
return best_move
# Define a function computer_move()
2 def computer_move():
# Declare global variables
global turn, rounds, validinputs
# Get the best move
col=best_move()
if col==None:
col=choice(validinputs)
`--snip--`
moves_made.append(col)
`--snip--`
# Computer moves first
3 computer_move()
# Define a function conn() to place a disc in a cell
4 def conn(x,y):
# Declare global variables
global turn, rounds, validinputs
`--snip--`
moves_made.append(col)
`--snip--`
# Computer moves next
if len(validinputs)>0:
computer_move()
`--snip--`
列表 13-6:具有机器学习策略的“连接四”游戏玩家
我们创建了一个新的列表moves_made来跟踪目前为止游戏中的所有动作;稍后我们将在best_move()中使用它。我们打开模拟的“连接四”游戏数据并将其保存在列表gamedata中。
在best_move()中,我们确保第一次移动总是将棋子放置在第 4 列,因为这给计算机提供了一个起始优势 1。我们检查是否只剩下一步,如果是,就将其作为下一个最佳选择。否则,我们检查所有与当前游戏历史相同的模拟游戏,并查看哪个下一步最有利于红方玩家。我们将该动作指定为最佳动作。我将在ml_move.py中使用具体示例详细解释我们是如何做到这一点的。
在 2 处,我们定义了computer_move()。当轮到计算机玩家时,它会调用best_move()来生成一个动作。计算机执行这个动作,并将该动作添加到列表moves_made中以跟踪游戏历史。
我们设置计算机先手 3。之后,玩家点击进行自己的回合 4。人类玩家的动作也会被添加到moves_made中。如果游戏没有结束,计算机将在玩家之后进行下一步。
运行conn_ml.py并进行几次游戏。你可能会惊讶地发现,赢得比赛相对容易。机器学习策略远不如我们的三步法有效。我们将在本章后面探讨原因。
测试两种策略的有效性
接下来,我们想要衡量这两种策略的智能程度。我们将模拟 1,000 场游戏并记录结果。在每场比赛中,智能计算机版本将与一个选择随机动作的简单计算机玩家对战。我们将查看智能玩家获胜或平局的次数。
三步预测策略
我们将从三步法版本开始。脚本 outcome_conn_think.py,如列表 13-7 所示,让我们的两个计算机玩家进行 1,000 场对战,然后输出获胜、平局和失败的场次。
import pickle
from random import choice
from copy import deepcopy
# Define the simulate() function to play a complete game
1 def simulate():
occupied=[list(),list(),list(),list(),list(),list(),list()]
validinputs=[1,2,3,4,5,6,7]
`--snip--`
def win_game(num, color, lst):
win=False
`--snip--`
def best_move():
# Take column 4 in the first move
if len(occupied[3])==0:
return 4
`--snip--`
# The red player takes the first move
turn="red"
# Keep track of all intermediate moves
moves_made=[]
2 winlose=[0]
# Play a maximum of 42 steps (21 rounds)
for i in range(21):
# The player selects the best move
3 col=best_move()
if col==None:
col=choice(validinputs)
moves_made.append(col)
`--snip--`
# The other player randomly selects a move
col=choice(validinputs)
moves_made.append(col)
`--snip--`
# Record both game outcome and intermediate steps
4 return winlose+moves_made
# Repeat the game 1000 times and record all game outcomes
results=[]
5 for x in range(1000):
result=simulate()
results.append(result)
with open('outcome_conn_think.pickle', 'wb') as fp:
pickle.dump(results,fp)
with open('outcome_conn_think.pickle', 'rb') as fp:
mylist=pickle.load(fp)
winlose=[x[0] for x in mylist]
# Print out the number of winning games
print("the number of winning games is", winlose.count(1))
# Print out the number of tying games
print("the number of tying games is", winlose.count(0))
# Print out the number of losing games
print("the number of losing games is", winlose.count(-1))
列表 13-7:测试三步预测策略的有效性。
在 1 处,我们定义了simulate(),它让使用三步预测策略的智能计算机(红方玩家)与一个选择随机动作的计算机玩家对战。
win_game()和best_move()函数与conn_think.py中定义的相同。我们使用winlose列表来记录游戏结果:1表示红方获胜,-1表示黄方获胜,0表示平局。
游戏开始后,红方玩家调用best_move()获得了第 3 步,而黄方玩家随机选择了第 4 步。
在第 5 步时,我们调用simulate() 1,000 次并记录所有游戏的结果。然后我们打印出获胜、平局和失败的游戏数量,将1、-1和0的计数汇总,方便查看。以下是输出示例:
the number of winning games is 995
the number of tying games is 0
the number of losing games is 5
在所有的游戏中,使用三步提前思考策略的智能玩家赢了 995 次,从未平局,输了 5 次。
机器学习策略
现在我们将以相同的方式测试机器学习策略。下载outcome_conn_ml.py并将其保存在你的章节文件夹中。它与outcome_conn_think.py类似,因此我只会在这里强调其中的不同点:
`--snip--`
# Obtain gamedata
with open('conn_simulates.pickle', 'rb') as fp:
gamedata=pickle.load(fp)
# Define the best_move() function based on the machine-learning strategy
def best_move():
# Take column 4 in the first move
if len(occupied[3])==0:
return 4
`--snip--`
with open('outcome_conn_ml.pickle', 'wb') as fp:
pickle.dump(results,fp)
with open('outcome_conn_ml.pickle', 'rb') as fp:
mylist=pickle.load(fp)
`--snip--`
首先,我们获取之前在conn_simulation.py中生成的模拟游戏结果数据。其次,我们将best_move()的定义基于机器学习策略,而不是三步策略。
我们调用simulate() 1,000 次并记录结果,像以前一样打印出来。以下是一个示例输出:
the number of winning games is 882
the number of tying games is 0
the number of losing games is 118
在所有的游戏中,计算机赢了 882 次,从未平局,输了 118 次——这比三步策略的表现差得多。让我们看看为什么。
为什么机器学习策略在四子棋中效果不好?
机器学习策略在我们的游戏中效果较差,主要是因为在四子棋游戏中可供选择的步数太多:最多有 42 步。这意味着,指数级地,存在大量可能的游戏结果。我们模拟了 100 万局游戏,虽然听起来很多,但当数据分布在众多游戏结果之间时,不可避免地会有一些游戏结果不在模拟数据中。因此,对于许多游戏历史,无法找到最佳策略。
作为一个例子,我们将用一个特定的游戏历史来测试机器学习策略。假设红方和黄方玩家各已经走了三步,现在轮到红方玩家。此时的棋盘如图 13-5 所示。

图 13-5:一个游戏模拟
我们将在代码中模拟这个游戏设置,看看我们的机器学习策略如何决定下一步走哪一步。请参见ml_move.py,如清单 13-8 所示。
import pickle
validinputs=[1,2,3,4,5,6,7]
# A game history
moves_made=[4,5,4,5,4,5]
# The game board
occupied=[list(),list(),list(),
['red','red','red'],
['yellow','yellow','yellow'],
list(),list()]
# Obtain gamedata
with open('conn_simulates.pickle', 'rb') as fp:
gamedata=pickle.load(fp)
1 simu=[]
for y in gamedata:
if y[1:len(moves_made)+1]==moves_made:
simu.append(y)
# Now we look at the next move
outcomes={x:[] for x in validinputs}
# We collect all the outcomes for each next move
for y in simu:
outcomes[y[len(moves_made)+1]].append(y[0])
2 print(outcomes)
# Set the initial value of bestoutcome
bestoutcome=-2;
# Randomly select a move to be best_move
best_move=validinputs[0]
# Iterate through all possible next moves
3 for move in validinputs:
if len(outcomes[move])>0:
outcome=sum(outcomes[move])/len(outcomes[move])
print\
(f'when the next move is {move}, the average outcome is {outcome}')
# If the average outcome from that move beats the current best move
if outcome>bestoutcome:
# Update the best outcome
bestoutcome=outcome
# Update the best move
best_move=move
4 print(f'the best next move is {best_move}')
清单 13-8:搜索最佳机器学习策略步伐。
我们导入pickle,它使我们能够处理以pickle格式保存的数据集。我们打开早先在conn_simulation.py中创建的模拟数据文件conn_simulates.pickle。数据保存在gamedata列表中。
此时,红方玩家可以在下一步中将棋子放入任意七列中的一列,因此我们在validinputs中有所有七个值。我们将图 13-4 中已经完成的六个步骤,[4, 5, 4, 5, 4, 5],保存在列表moves_made中。列表occupied跟踪当前游戏板上的棋子位置。
我们检查百万次模拟游戏的数据,看看其中是否有任何游戏的历史与当前游戏的历史匹配。如果有,我们将所有匹配的历史游戏放入列表simu1。然后,我们专注于这些游戏中的第七步。我们查看与每个七个可能的步骤(从 1 到 7)相关联的所有游戏的结果(胜、负或平),并将它们放入字典outcomes中。
然后我们打印出outcomes2 的内容:
{1: [], 2: [-1, 1], 3: [1], 4: [1], 5: [-1], 6: [-1, -1, 1], 7: [-1]}
正如你所看到的,九局游戏有相同的游戏历史:没有一局将棋子放入第 1 列,两个使用了第 2 列作为下一步,另一个使用了第 3 列,以此类推。方括号中的-1、0和1表示红方玩家分别输了、平局和赢了。
为了帮助我们比较七个步骤中哪个对红方玩家最有利,我们计算每个步骤的平均结果 3。如果某一步每次都赢,平均结果为1;如果某一步的结果是 50%的胜利和 50%的失败,平均结果为0;如果某一步每次都输,平均结果为-1。
我们打印出平均结果(我们没有第一步的结果,因为在simu中没有模拟游戏使用这个步骤):
when the next move is 2, the average outcome is 0.0
when the next move is 3, the average outcome is 1.0
when the next move is 4, the average outcome is 1.0
when the next move is 5, the average outcome is -1.0
when the next move is 6, the average outcome is -0.3333333333333333
when the next move is 7, the average outcome is -1.0
第 3 步和第 4 步都导致了平均结果为1。脚本打印出了第一个最佳步骤,在这种情况下是 3:图 4:
the best next move is 3
然而,当我们在游戏中查看这个步骤时(图 13-6),我们可以清楚地看到它并不是我们可以做出的最佳步骤。

图 13-6:机器学习计算机犯了一个错误。
正如你所看到的,机器学习策略的问题在于我们没有足够的模拟游戏匹配我们的游戏历史。
你可能会想,是否可以通过增加模拟游戏的数量来解决这个问题。答案是既可以,也不行。增加模拟游戏的数量会让策略变得更加智能,但它也会增加数据量,导致机器学习脚本的响应变慢。因此,玩家将不得不等待很长时间,才能让计算机做出决策。这就是使用机器学习时的权衡。
我们来通过将模拟游戏的数量增加到 1000 万次来测试这一点。生成这些数据需要几个小时。我们用更大的数据集重新运行ml_move.py,并得到以下输出:
{1: [-1, 1, -1, 1, -1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1],
2: [1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1],
3: [-1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1],
4: [1, 1, 1, 1, 1, 1, 1, 1, 1],
5: [1, 1, 1, 1, 1, 1, -1],
6: [-1, -1, 1, -1, -1, 1, 1, -1, 1, 1, -1, 1, -1],
7: [1, -1, 1, -1, -1, -1, 1, -1, -1, 1]}
when the next move is 1, the average outcome is -0.06666666666666667
when the next move is 2, the average outcome is 0.42857142857142855
when the next move is 3, the average outcome is -0.8181818181818182
when the next move is 4, the average outcome is 1.0
when the next move is 5, the average outcome is 0.7142857142857143
when the next move is 6, the average outcome is -0.07692307692307693
when the next move is 7, the average outcome is -0.2
the best next move is 4
现在我们有更多的数据来为我们的决策提供依据,机器学习策略正确地推荐了第 4 列,导致了图 13-7。

图 13-7:在 1000 万次模拟游戏中,策略做出了正确的决策。
语音控制的智能四子棋游戏
本章的最后,我们将为智能四子棋游戏添加语音识别和文本转语音功能。
一个能预测的语音控制游戏
我们将之前创建的两个脚本conn_think.py和conn_hs.py合并成conn_think_hs.py。从书籍资源中下载该文件并保存在你的章节文件夹中。主要的不同之处见清单 13-9。
`--snip--`
1 def best_move():
# Take column 4 in the first move
if len(occupied[3])==0:
return 4
`--snip--`
# Define the computer_move() function
2 def computer_move():
global turn, rounds, validinputs
# Choose the best move
col=best_move()
if col==None:
col=choice(validinputs)
print_say(f"The computer chooses column {col}.")
`--snip--`
# Check if the player has won
if win_game(col, turn, occupied)==True:
# If a player wins, invalid all moves, end the game
validinputs=[]
3 print_say(f"Congrats player {turn}, you won!")
messagebox.showinfo("End Game",f"Congrats player {turn}, you won!")
# If all cells are occupied and no winner, it's a tie
if rounds==42:
print_say("Game over, it's a tie!")
messagebox.showinfo("Tie Game","Game over, it's a tie!")
`--snip--`
# Computer moves first
4 computer_move()
# Add a dictionary of words to replace
to_replace = {'number ':'', 'cell ':'', 'column ':'',
'one':'1', 'two':'2', 'three':'3',
'four':'4', 'for':'4', 'five':'5',
'six':'6', 'seven':'7'}
# Start a while loop to take voice inputs
5 while len(validinputs)>0:
# Ask for your move
print_say(f"Player {turn}, what's your move?")
# Capture your voice input
inp= voice_to_text().lower()
print_say(f"You said {inp}.")
for x in list(to_replace.keys()):
inp = inp.replace(x, to_replace[x])
try:
col=int(inp)
except:
print_say("Sorry, that's an invalid input!")
continue
# If col is not a valid move, try again
6 if col not in validinputs:
print_say("Sorry, that's an invalid move!")
continue
# If your voice input is a valid column number, play the move
else:
# Calculate the lowest available row number in that column
row=len(occupied[col-1])+1
`--snip--`
print_say(f"Congrats player {turn}, you won!")
`--snip--`
print_say("Game over, it's a tie!")
`--snip--`
if len(validinputs)>0:
computer_move()
`--snip--`
清单 13-9:带有三步策略的语音控制四子棋游戏
函数best_move()与脚本conn_think.py中的相同 1。我们定义了computer_move()2,它使用best_move()选择一个移动,并大声宣布选择的列。如果计算机的移动赢得或平局了游戏,脚本也会宣布这一结果 3。
计算机随后会进行游戏的第一步 4,并启动一个while循环,只要列表validinputs不为空 5,它会一直运行。在每次迭代中,脚本会捕捉你的语音输入,应该是你想放置棋子的列号。你可以说“数字五”,“列五”或“5”。然后它会将语音命令转换为整数,以匹配validinputs中的格式,这样它就可以将你的输入与列表进行比较。如果你说了无法转换为整数的内容,脚本会说:“抱歉,这是一个无效的输入。”
如果你给出了一个无效的移动 6,脚本会说:“抱歉,这是一个无效的移动。”如果你的移动有效,脚本将在游戏板上放置棋子。在此过程中,它会检查你是否赢得或平局了游戏,如果是,脚本会大声宣布结果。如果游戏尚未结束,计算机会进行下一步。
运行脚本并与计算机一起玩语音控制游戏。你会发现,这个游戏更具挑战性,也更有趣。
一个使用机器学习的语音控制游戏
我们将之前创建的两个脚本conn_ml.py和conn_hs.py合并成conn_ml_hs.py。从书籍资源中下载该文件并保存在你的章节文件夹中。清单 13-10 显示了主要的不同之处。
`--snip--`
import pickle
`--snip--`
# A history of moves made
moves_made=[]
# Obtain gamedata
with open('conn_simulates.pickle', 'rb') as fp:
gamedata=pickle.load(fp)
# Define the best_move() function based on machine learning
def best_move():
# Take column 4 in the first move
if len(occupied[3])==0:
return 4
`--snip--`
# Define the computer_move() function
def computer_move():
global turn, rounds, validinputs
# Choose the best move
move=best_move()
if move==None:
move=choice(validinputs)
print_say(f"The computer decides to occupy cell {move}.")
`--snip--`
moves_made.append(move)
`--snip--`
# Computer moves first
computer_move()
# Start an infinite loop to take voice inputs
while len(validinputs)>0:
# Ask for your move
print_say(f"Player {turn}, what's your move?")
# Capture your voice input
inp= voice_to_text().lower()
`--snip--`
moves_made.append(inp)
`--snip--`
# Computer moves
if len(validinputs)>0:
computer_move()
`--snip--`
清单 13-10:使用机器学习策略的语音控制四子棋游戏
这和语音控制三步版的工作方式相同。运行脚本并进行游戏。你会发现游戏有趣,但比三步策略更容易战胜。
小结
在本章中,你通过使用两种方法:三步预测策略和机器学习策略,创建了智能的语音控制图形版四子棋游戏。这教会了一些重要的推理技巧——我们如何让脚本变得智能?——以及一些基本的机器学习技巧。
你学会了将这两种策略进行概括,并将它们应用于特定的游戏。你可以运用这些技能来创建你自己的智能语音控制游戏。
章节末尾的练习
-
修改conn_think1.py,使得人类玩家先手,计算机后手。
-
将第十章中的ttt_click.py与conn_think1.py结合,创建一个鼠标点击版的井字棋游戏,其中计算机提前思考一步。
-
创建一个鼠标点击版的井字棋游戏,其中计算机根据ttt_click.py和conn_think2.py提前思考两步。
-
在
best_move()函数中,定义在conn_think.py文件里,如果列表winner有八个元素[7, 7, 4, 5, 6, 6, 6, 6],那么cnt、maxcnt和cnt[maxcnt]的值分别是多少? -
设计一个鼠标点击版的井字棋游戏,其中计算机根据ttt_click.py和conn_think.py提前思考三步。
-
模拟一百万场井字棋游戏,并将游戏结果和中间步骤保存为ttt_simulates.pickle。然后创建一个鼠标点击版的井字棋游戏,其中计算机使用机器学习策略,类似于我们在conn_simulation.py和conn_ml.py中所做的。
-
修改outcome_conn_think.py和outcome_conn_ml.py,测试在你刚刚创建的井字棋游戏中,三步策略和机器学习策略的效果。
-
在运行conn_simulation.py后,我们从数据集conn_simulates.pickle中打印出 10 条观察数据,如列表 13-5 所示。第 10 条观察数据是
[-1, 1, 4, 1, 4, 1, 2, 4, 5, 6, 6, 6, 3]。谁赢得了第 10 局?四个棋子是垂直、水平还是对角线连接的?
第四部分:进一步深入
金融应用

语音识别和文本到语音技术可以应用于生活的许多方面。在本章中,我们将专注于追踪金融市场,但你在这里学到的技术可以轻松推广并应用到你感兴趣的任何领域。
在本章中,你将构建三个项目:一个应用程序,报告任何上市公司最新的股价;一个构建股价可视化的脚本;以及一个应用程序,使用最近的日常股价计算回报、执行回归分析并进行详细分析。
和往常一样,所有脚本都可以通过本书的资源页面获取:www.nostarch.com/make-python-talk/。首先创建 /mpt/ch14/ 文件夹,用于本章内容。
Python,Facebook 股票价格是多少?
在这个项目中,你将使用 yahoo_fin 包来根据股票的代码获取实时价格信息。股票代码 是一串字符或代码,用于唯一标识一只股票。大多数人并不知道公司关联的股票代码。
这提供了一个从后向前工作的机会。你将学习如何抓取网页,以便从公司名称中获取股票的代码。当你在脚本中输入一个公司的名称时,Python 会告诉你该公司股票的代码。最后,你将添加文本到语音和语音识别功能。
获取最新的股票价格
yahoo_fin 包可以让你从 Yahoo! Finance 获取最新的股价信息。这个包不在 Python 标准库中,因此你需要首先使用 pip install 来安装它。
打开你的 Anaconda 提示符(Windows 中)或终端(Mac 或 Linux 中),激活虚拟环境 chatting,并运行以下命令(注意包名中间有下划线):
**pip install yahoo_fin**
接下来,打开你的 Spyder 编辑器,并将列表 14-1 保存为 live_price.py 到你的章节文件夹中。要使用这个脚本,你需要事先找到你感兴趣的股票的代码。
from yahoo_fin import stock_info as si
# Start an infinite loop
1 while True:
# Obtain ticker symbol from you
ticker = input("Which stock (ticker symbol) are you looking for?\n")
# If you want to stop, type in "done"
2 if ticker == "done":
break
# Otherwise, type in a stock ticker symbol
else:
# Obtain stock price from Yahoo!
3 price = si.get_live_price(ticker)
# Print out the stock price
print(f"The stock price for {ticker} is {price}.")
列表 14-1:获取实时股价
我们从 yahoo_fin 包中导入 stock_info 模块,并给它取别名为 si。然后,我们将脚本放入一个无限循环中,持续接收你输入的请求,获取股票代码。每当你想停止脚本时,可以输入 done。否则,脚本将自动继续从 Yahoo! Finance 获取你请求的公司最新股价信息。最后,脚本将打印出股票价格信息。
下面是与脚本交互的输出,其中用户输入部分用粗体显示:
Which stock (ticker symbol) are you looking for?
**MSFT**
The stock price for MSFT is 183.25.
Which stock (ticker symbol) are you looking for?
**AAPL**
The stock price for AAPL is 317.94000244140625.
Which stock (ticker symbol) are you looking for?
**done**
如你所见,我输入了微软和苹果的股票代码(MSFT 和 AAPL),脚本返回了它们的最新价格。
注意,Apple 股票的价格有很多小数位。稍后我们会调整代码,只显示所有股票价格的小数点后两位。
为了让脚本正常工作,你需要公司的股票代码,例如 MSFT 或 AAPL。你可能会想,如果我不知道我感兴趣的股票的股票代码怎么办?如果我只知道公司名称,比如 Microsoft 或 Apple,Python 能找到它吗?答案是可以的,这时你在第六章学到的网页抓取技术就派上用场了。
查找股票代码
很多时候,你知道你感兴趣的公司名称,但不知道它的股票代码。这个脚本会在你输入公司名称时找到股票代码。这一点很重要,因为我们的最终目标是创建在金融市场中可以通过语音控制的应用程序。通过语音命令获取股票代码对 Python 脚本来说相对较难,但识别公司名称要容易得多。
我们首先需要找到一个可以可靠提供公司股票代码的网站。我们将使用 Yahoo! Finance,并通过 URL query1.finance.yahoo.com/v1/finance/search?q= 查询该网站,后面跟上你想查询的公司名称。例如,如果你在后面加上 Bank of America,你将得到一组适合 Python 使用的数据结果,如 图 14-1 所示。

图 14-1:查询美国银行股票代码时的结果
这些数据采用 JSON 格式,JSON 是 JavaScript 对象表示法 的缩写。该文件格式用于浏览器与服务器之间的通信,使用人类可读的文本来存储和传输数据对象。JSON 起源于 JavaScript,但现在已成为一种与语言无关的数据格式,被许多编程语言使用,包括 Python。
为了让 JSON 数据更易读,我们将使用在线 JSON 数据格式化工具 jsonformatter.curiousconcept.com/。打开该网址,你将看到一个类似 图 14-2 的页面。

图 14-2:格式化 JSON 数据的网站
将 图 14-1 中的数据粘贴到指定区域,并点击 处理。格式化工具将把数据转换为更易读的格式,如 清单 14-2 所示。
{
"explains":[
],
"count":18,
"quotes":
{
"exchange":"NYQ",
"shortname":"Bank of America Corporation",
"quoteType":"EQUITY",
1 "symbol":"BAC",
"index":"quotes",
"score":208707.0,
"typeDisp":"Equity",
"longname":"Bank of America Corporation",
"isYahooFinance":true
},
{
"exchange":"NYQ",
"shortname":"Bank of America Corporation Non",
"quoteType":"EQUITY",
"symbol":"BAC-PL",
"index":"quotes",
"score":20322.0,
"typeDisp":"Equity",
"longname":"Bank of America Corporation",
"isYahooFinance":true
},
{
"exchange":"NYQ",
"shortname":"Bank of America Corporation Dep",
"quoteType":"EQUITY",
"symbol":"BAC-PC",
"index":"quotes",
"score":20183.0,
"typeDisp":"Equity",
"longname":"Bank of America Corporation",
"isYahooFinance":true
},
`--snip--`
}
清单 14-2:股票代码查询的格式化 JSON 数据
数据集是一个包含多个元素的大型字典,键值包括 explains、count、quotes 等等。quotes 键的值是一个包含多个字典的列表。第一个字典包含键 exchange、shortname、quoteType —— 更重要的是,包含 symbol,它的值是 BAC,即我们需要的股票代码 1。
接下来,我们使用一个 Python 脚本,根据之前的模式提取股票代码符号。脚本get_ticker_symbol.py(见[列表 14-3)实现了这一功能。
import requests
# Start an infinite loop
1 while True:
# Obtain company name from you
firm = input("Which company's ticker symbol are you looking for?\n")
# If you want to stop, type in "done"
if firm == "done":
break
# Otherwise, type in a company name
2 else:
3 try:
# Extract the source code from the website
url = 'https://query1.finance.yahoo.com/v1/finance/search?q='+firm
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the value corresponding to "quotes"
4 quotes = response_json['quotes']
# Get the ticker symbol
ticker = quotes[0]['symbol']
# Print out the ticker
print(f"The ticker symbol for {firm} is {ticker}.")
except:
print("Sorry, not a valid entry!")
continue
列表 14-3:根据公司名称查找股票的代码符号
我们导入requests模块,它允许 Python 发送超文本传输协议(HTTP)请求。在第 1 行,我们开始一个无限循环,在每次迭代中要求提供书面输入。要退出循环,输入done。否则,输入公司名称 2。我们使用异常处理来防止崩溃 3。
我们进入 JSON 数据,提取与quotes键对应的列表 4。然后我们查找第一个元素,并寻找与symbol键对应的值。脚本将在 IPython 控制台输出股票代码符号。如果没有结果,脚本将打印出抱歉,输入无效!。
多次运行脚本,搜索几家公司,检查它是否正常工作。以下是与脚本的一次交互输出:
Which company's ticker symbol are you looking for?
**ford motor**
The ticker symbol for ford motor is F.
Which company's ticker symbol are you looking for?
**walt disney company**
The ticker symbol for walt disney company is DIS.
Which company's ticker symbol are you looking for?
**apple**
The ticker symbol for apple is AAPL.
Which company's ticker symbol are you looking for?
**done**
如你所见,脚本适用于单词公司名称(如 Apple)以及更长的公司名称(如 Walt Disney Company)。
通过语音获取股票价格
现在我们将脚本live_price.py和get_ticker_symbol.py结合起来,并添加语音识别和文本转语音功能。在 Spyder 编辑器中输入列表 14-4,并将其保存为live_price_hs.py,保存在你的章节文件夹中,或者从书籍资源中下载该脚本。
import requests
from yahoo_fin import stock_info as si
from mptpkg import voice_to_text, print_say
# Start an infinite loop
1 while True:
# Obtain company name from you
print_say("Which company's stock price do you want to know?")
firm = voice_to_text()
print_say(f"You just said {firm}.")
# If you want to stop, type in "stop listening"
if firm == "stop listening":
print_say("OK, goodbye then!")
break
# Otherwise, say a company name
2 else:
try:
# Extract the source code from the website
url = 'https://query1.finance.yahoo.com/v1/finance/search?q='+firm
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the value corresponding to "quotes"
quotes = response_json['quotes']
# Get the ticker symbol
ticker = quotes[0]['symbol']
# Obtain live stock price from Yahoo!
3 price = round(float(si.get_live_price(ticker)),2)
# Speak the stock price
print_say(f"The stock price for {firm} is {price}.")
# In case the price cannot be found, the script will tell you
except:
print_say("Sorry, I cannot find what you are looking for!")
continue
列表 14-4:通过语音获取实时股票价格
我们现在从本地mptpkg包中导入print_say()和voice_to_text(),以添加文本转语音和语音识别功能。
在第 1 行,我们开始一个无限循环,要求提供语音输入。要退出循环,你可以说“停止监听”。否则,你说出公司名称 2,脚本会搜索股票代码符号。我们在这里使用try和except来防止脚本因 Yahoo! Finance 没有结果而崩溃。
我们将 Yahoo! Finance 中的股票价格保存在price中 3。注意,我们使用round()将股票价格四舍五入到小数点后两位。脚本将朗读公司的股票价格,或者如果没有结果,将说“抱歉,我找不到你要找的信息!”
以下是一个交互示例:
Which company's stock price do you want to know?
You just said **JPMorgan Chase**.
The stock price for JPMorgan Chase is 97.31.
Which company's stock price do you want to know?
You just said **Goldman Sachs**.
The stock price for Goldman Sachs is 196.49.
Which company's stock price do you want to know?
You just said **stop listening**.
OK, goodbye then!
语音控制数据可视化
分析数据的一种高效方法——例如,寻找股票走势中的模式——是通过数据可视化。数据可视化将数据呈现为图形和图表等可视化形式,使人脑更容易理解。
您在本章第一个项目中获得的股价是股票的最新价格。也就是说,您查询的每只股票都有一个数据点。然而,为了更好地了解某只股票,最好获取该股票的多个最近价格,以便您可以了解其速度和方向。该股票是保持在差不多的价值、上涨还是下跌?如果价格发生变化,这一变化有多快?
在这个项目中,您将从 Yahoo! Finance 获取最新的每日股价信息。然后,您将绘制图表,以查看股价随时间的变化。您还将学习如何创建蜡烛图,以便查看日内股票的运动模式。设置好这些后,我们将添加语音识别和文本到语音功能。
创建股价图
我们将使用pandas_datareader模块和matplotlib来创建过去六个月的股价图表。首先,您将学习如何提取数据,然后学习如何创建图表。
在我们开始之前,您需要安装一些第三方模块。打开 Anaconda 提示符(Windows)或终端(Mac 或 Linux),并激活虚拟的聊天环境。然后逐行运行以下代码:
**conda install pandas**
**conda install matplotlib**
**pip install pandas_datareader**
按照说明完成安装。pandas_datareader模块可以从各种来源提取在线数据,并将其导入到pandas数据框中。然后,在您的 Spyder 编辑器中输入示例 14-5 并将脚本保存为price_plot.py,放在您的章节文件夹中。
import matplotlib.pyplot as plt
from pandas_datareader import data as pdr
import matplotlib.dates as mdates
# Set the start and end dates
1 start_date = "2020-09-01"
end_date = "2021-02-28"
# Choose stock ticker symbol
2 ticker = "TSLA"
# Get stock price
3 stock = pdr.get_data_yahoo(ticker, start=start_date, end=end_date)
print(stock)
# Obtain dates
4 stock['Date']=stock.index.map(mdates.date2num)
# Choose figure size
5 fig = plt.figure(dpi=128, figsize=(10, 6))
# Format date to place on the x-axis
6 formatter = mdates.DateFormatter('%m/%d/%Y')
plt.gca().xaxis.set_major_formatter(formatter)
# Plot data
7 plt.plot(stock['Date'], stock['Adj Close'], c='blue')
# Format plot
8 plt.title("The Stock Price of Tesla", fontsize=16)
plt.xlabel('Date', fontsize=10)
fig.autofmt_xdate()
plt.ylabel("Price", fontsize=10)
9 plt.show()
示例 14-5:创建股价图的脚本
我们导入模块,然后指定我们想要提取的数据的开始和结束日期 1。这些日期目前将被硬编码;稍后我们会使日期动态化。日期应采用YYYY-MM-DD格式。在此案例中,我们将使用从 2020 年 9 月 1 日到 2021 年 2 月 28 日的六个月期间。我们还提供了股票的股票代码——在此情况下为特斯拉,股票代码为 TSLA 2。
我们使用get_data_yahoo()函数,该函数位于pandas_datareader模块中,用于提取每日股价信息,并将数据保存为名为stock的pandas数据框。数据集如下所示:
High Low ... Volume Adj Close
Date ...
2020-09-01 502.489990 470.510010 ... 90119400 475.049988
2020-09-02 479.040009 405.119995 ... 96176100 447.369995
2020-09-03 431.799988 402.000000 ... 87596100 407.000000
2020-09-04 428.000000 372.019989 ... 110321900 418.320007
2020-09-08 368.739990 329.880005 ... 115465700 330.209991
... ... ... ... ...
2021-02-22 768.500000 710.200012 ... 37269700 714.500000
2021-02-23 713.609985 619.000000 ... 66606900 698.840027
2021-02-24 745.000000 694.169983 ... 36767000 742.020020
2021-02-25 737.210022 670.580017 ... 39023900 682.219971
2021-02-26 706.700012 659.510010 ... 41011300 675.500000
[123 rows x 6 columns]
数据集使用日期作为索引。123 行代表六个月期间的 123 个交易日。六列分别代表每个交易日的信息:最高价、最低价、开盘价、收盘价、交易量和调整后的收盘价。
然后,我们将数据集的时间戳索引读取为数字,并将其保存为一个额外的(第七)列 4。此步骤是必要的,因为数据集没有将索引识别为单独的变量,但我们需要日期信息来作为图表的 x 轴。然后,我们使用matplotlib.pyplot中的figure()函数来指定图表的大小和分辨率,并将生成的图形命名为fig5。dpi=128参数使输出为每英寸 128 像素。figsize=(10,6)参数将图表宽度设置为 10 英寸,高度设置为 6 英寸。
我们使用matplotlib.dates中的DateFormatter()方法来指定我们想要显示的日期格式 6。实际绘图是通过使用plot()7 来完成的。前两个参数分别是 x 轴和 y 轴的变量。我们还使用第三个参数来指定颜色。在这种情况下,我们将调整后的收盘价与日期作图,并使用蓝色作为颜色。
从第 8 步开始,我们在图表上添加标题并标注 x 轴和 y 轴。我们还使用autofxt_xdate()将 x 轴的日期显示为斜体,以防止文字重叠。
最后,调用show()来显示图表 9。图 14-3 显示了输出结果。

图 14-3:2020 年 9 月到 2021 年 2 月的特斯拉股票价格图
我们可以看到特斯拉在这六个月期间的价格波动模式。2020 年 9 月初股票价格不到每股 500 美元,但到了 12 月下旬涨到了每股超过 800 美元,之后在 2 月中旬略有下跌。这个可视化比之前的stock数据框输出更具可读性(也更有信息量)!
创建蜡烛图
价格图表非常适合通过每天一个观测值来总结模式。有时你会对几种日内价格波动感兴趣,比如当天的价格波动范围、收盘价是否高于或低于开盘价等等。使用蜡烛图,你可以可视化每天四个价格信息:日最高价、日最低价、开盘价和收盘价。
以下脚本生成了 2021 年 2 月亚马逊股票的蜡烛图。我不建议绘制超过一个月的股票价格图,因为图表可能会变得过于拥挤,导致很难发现模式。
首先,你需要安装第三方的mplfinance模块。打开你的 Anaconda 命令行(Windows 系统)或终端(Mac 或 Linux 系统),激活虚拟环境chatting,并运行以下命令:
**pip install mplfinance**
然后打开你的 Spyder 编辑器,并将列表 14-6 保存为* candle_stick.py*文件,放入你的章节文件夹中。
import matplotlib.pyplot as plt
from pandas_datareader import data as pdr
import matplotlib.dates as mdates
from mplfinance.original_flavor import candlestick_ohlc
# Set the start and end date
start_date = "2021-02-01"
end_date = "2021-02-28"
# Choose stock ticker symbol
ticker = "AMZN"
# Get stock price
stock = pdr.get_data_yahoo(ticker, start=start_date, end=end_date)
# Obtain dates
stock['Date'] = stock.index.map(mdates.date2num)
# Choose the four daily prices: open, high, low, and close
1 df_ohlc = stock[['Date','Open', 'High', 'Low', 'Close']]
# Choose figure size
figure, fig = plt.subplots(dpi=128, figsize = (8,4))
# Format dates
formatter = mdates.DateFormatter('%m/%d/%Y')
# Choose x-axis
fig.xaxis.set_major_formatter(formatter)
fig.xaxis_date()
2 plt.setp(fig.get_xticklabels(), rotation = 10)
# Create the candlestick chart
3 candlestick_ohlc(fig,
df_ohlc.values,
width=0.8,
colorup='black',
colordown='gray')
# Put text in the chart that black color means close > open
4 plt.figtext(0.3,0.2,'Black: Close > Open')
# Put text in the chart that gray color means close < open
plt.figtext(0.3,0.15,'Gray: Close < Open')
# Put chart title and axis labels
5 plt.title(f'Candlesticks Chart for {ticker}')
plt.ylabel('Price')
plt.xlabel('Date')
plt.show()
列表 14-6:创建蜡烛图的脚本
我们导入所有需要的模块和函数,包括来自mplfinance模块的candlestick_ohlc()函数,这个函数将用于创建蜡烛图。
在第 1 步,我们选择了要提取并在图表中可视化的四个日价格:开盘价、日最高价、日最低价和收盘价。
来自matplotlib的setp()函数用于设置对象属性,我们调用它来旋转 x 轴上的日期 2。我们传递两个参数(第一个用于获取 x 轴标签,第二个用于设置属性),将 x 轴标签旋转 10 度,以防文本重叠。在 3 处,我们使用candlestick_ohlc()生成蜡烛图。第一个参数指定图表放置的位置,第二个指定使用的数据。第三个参数是蜡烛实体相对于两次观察之间的距离(即 x 轴上两个交易日之间的距离)的宽度。
蜡烛图使用颜色传达额外的数据。我们使用黑色表示收盘价高于开盘价;否则,使用灰色。该信息也通过图例 4 传达。最后,我们给图表添加标题并标注两个坐标轴 5。
2021 年 2 月亚马逊股票价格的蜡烛图如图 14-4 所示。图表中的空白区域表示非交易日(周末和节假日)。
每日最高价和最低价位于细线的两端(看起来像蜡烛的烛芯),而开盘价和收盘价位于宽线的两端(看起来像蜡烛的实体部分)。因此得名!
从中我们可以快速看到,2 月 1 日股价猛涨:蜡烛的实体部分跨度接近 100 美元,并且是黑色的。与第二天对比,尽管细线相对较长,蜡烛的实体部分却很短,表明尽管有波动,但收盘价几乎与开盘价相同。

图 14-4:2021 年 2 月亚马逊每日股票价格的蜡烛图
添加语音控制
让我们添加语音功能。当你说出公司名称时,脚本将搜索该公司股票的股票代码,获取每日价格信息,并显示绘图或图表。我们首先需要创建两个本地模块:一个用于显示股票价格绘图,另一个用于显示蜡烛图。
股票价格绘图模块
我们将基于price_plot.py创建一个股票价格绘图模块。在你的 Spyder 编辑器中输入 Listing 14-7,并将其保存为myplot.py。
`--snip--`
from datetime import date, timedelta
from mptpkg import print_say
1 def price_plot(firm):
try:
# Extract the source code from the website
2 url = 'https://query1.finance.yahoo.com/v1/finance/search?q='+firm
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the value corresponding to "quotes"
quotes = response_json['quotes']
# Get the ticker symbol
ticker = quotes[0]['symbol']
# Set the start and end date
3 end_date = date.today().strftime("%Y-%m-%d")
start_date = (date.today() - timedelta(days=180)).strftime("%Y-%m-%d")
# Get stock price
stock = pdr.get_data_yahoo(ticker, start=start_date, end=end_date)
# Obtain dates
stock['Date']=stock.index.map(mdates.date2num)
# Choose figure size
4 fig = plt.figure(dpi=128, figsize=(10, 6))
# Format date to place on the x-axis
formatter = mdates.DateFormatter('%m/%d/%Y')
plt.gca().xaxis.set_major_formatter(formatter)
# Plot data
plt.plot(stock['Date'], stock['Adj Close'], c='blue')
# Format plot
plt.title\
(f"The Stock Price of {firm} in the Last Six Months", fontsize=16)
plt.xlabel('Date', fontsize=10)
fig.autofmt_xdate()
plt.ylabel("Price", fontsize=10)
plt.show()
# Let you know that the plot is ready via voice and print
5 print_say(f"OK, here is the stock price plot for {firm}.")
except:
print_say("Sorry, not a valid entry!")
Listing 14-7:股票绘图模块的脚本
我们导入了所需的模块,包括用于绘制股票价格和解析 HTML 源文件以查找公司股票代码的模块。我们还从本地的mptpkg包中导入了print_say()函数。
在 1 处,我们启动stock_plot(),它以公司名称作为参数。我们再次使用try和except来防止程序崩溃。首先,我们查找公司的股票代码 2。
在这里,我们使价格信息变得动态 3。结束日期是今天的日期,而开始日期是六个月前。脚本将生成一个绘图 4,并告诉你绘图已经准备好 5。如果找不到股票代码或价格信息,脚本将打印并说:“抱歉,输入无效!”
蜡烛图模块
接下来我们将创建蜡烛图模块。打开书中资源中的mychart.py,如清单 14-8 所示。
from mplfinance.original_flavor import candlestick_ohlc
from mptpkg import print_say
from datetime import date, timedelta
*--snip--*
1 def candle_stick(firm):
`--snip--`
# Set the start and end date
start_date = (date.today() - timedelta(days=14)).strftime("%Y-%m-%d")
end_date = date.today().strftime("%Y-%m-%d")
`--snip--`
# Choose the four daily prices: open, high, low, and close
2 df_ohlc = stock[['Date','Open', 'High', 'Low', 'Close']]
# Choose figure size
figure, fig = plt.subplots(dpi=128, figsize = (8,4))
`--snip--`
plt.show()
3 print_say(f"Here is the candlestick chart for {firm}.")
`--snip--`
except:
print_say("Sorry, not a valid entry!")
清单 14-8:创建蜡烛图模块的脚本
我们导入模块,包括来自mplfinance模块的candlestick_ohlc()函数。
我们在第 1 行定义candle_stick()。这里我们使得价格信息具有动态性。结束日期是今天的日期,而开始日期是两周前。然后我们执行与myplot.py相同的操作来查找股票代码。通过股票代码,我们从 Yahoo! Finance 获取过去 14 天的每日股价信息。为了节省空间,我已经剪裁了这部分脚本。
用于蜡烛图的数据将包括日期、开盘价、每日最高和最低价格以及收盘价 2。脚本会构建蜡烛图并在完成时通知你 3。
主脚本
接下来,我们将导入这两个模块到主脚本中,以便可以通过语音激活股价图表或蜡烛图。将清单 14-9 输入到你的 Spyder 编辑器中,并将其保存为plot_chart_hs.py,保存在你的章节文件夹中。
from myplot import price_plot
from mychart import candle_stick
from mptpkg import voice_to_text, print_say
# Start an infinite loop
1 while True:
# Obtain voice input from you
print_say("How may I help you?")
inp = voice_to_text()
print_say(f"You said {inp}.")
# If you want to stop, say "stop listening"
2 if "stop listening" in inp:
print_say("Nice talking to you, goodbye!")
break
# If "price pattern for" in voice, activate plot functionality
3 elif "price pattern for" in inp:
pos = inp.find('price pattern for ')
firm = inp[pos+len('price pattern for '):]
price_plot(firm)
continue
# If "candlestick chart for" in voice, activate chart functionality
4 elif "chart for" in inp:
pos = inp.find('chart for ')
firm = inp[pos+len('chart for '):]
candle_stick(firm)
continue
# Otherwise, go to the next iteration
else:
continue
清单 14-9:语音控制绘制和图表创建的脚本
我们导入模块并添加print_say()和voice_to_text()函数。我们还从本地myplot模块导入price_plot(),并从本地mychart模块导入我们刚刚创建的candle_stick()。
在第 1 行,我们启动一个无限循环,询问你的语音输入。要退出脚本,你可以说:“停止监听”2。要查看某个公司的股价图表(比如,Goldman Sachs),你可以说:“Goldman Sachs 的股价图案”。“股价图案”会触发股价图表功能 3。我们使用“股价图案”而不是“股价图表”,因为这样更容易被麦克风识别。脚本随后提取公司名称,这里是 Goldman Sachs,并将其作为price_plot()函数的参数。
要查看某个公司的蜡烛图(比如,通用汽车),你可以说:“通用汽车的图表。”语音命令中的“图表”部分将触发蜡烛图功能 4。脚本随后提取公司名称,并将其作为candle_stick()函数的参数。
这是我的示例输出:
How may I help you?
You said **price pattern for Oracle.**
OK, here is the stock price plot for Oracle.
How may I help you?
You said **chart for Intel.**
Here is the candlestick chart for Intel.
How may I help you?
You said **stop listening.**
Nice talking to you, goodbye!
“Oracle 的价格图案”这个短语触发了价格图表功能,脚本为 Oracle 生成了价格图表,如图 14-5 所示。

图 14-5:Oracle 的语音控制股价图表
“Intel 的图表”这个短语促使脚本为 Intel 创建了一个蜡烛图,如图 14-6 所示。

图 14-6:Intel 的语音控制蜡烛图
语音控制股票报告
虽然价格图表和蜡烛图可以让我们看到最近的价格波动,但它们并不能提供关于股票相对于整体市场表现的信息。许多时候,投资者关心的是股票与基准指数相比的表现如何。他们还关心股票的风险,即股票价格相对于整体市场的波动性。
为此,我们将进一步详细分析股票的价格。你将获取最近的每日股票价格信息,并进行回归分析,以了解股票的近期表现和市场风险。你将通过对股票回报与市场回报的回归分析,计算股票的异常回报(alpha,即股票相对于整体市场的表现)和市场风险(beta,即股票回报相对于整体市场的波动性)。
分析近期股票表现和风险
你将使用我们迄今为止使用的方法,通过pandas_datareader模块从 Yahoo! Finance 提取最近的每日股票价格信息。然后,你将使用一个新的模块statsmodels进行统计分析。
首先,我们将安装第三方模块并提取数据。进入你的 Anaconda 提示符(在 Windows 中)或终端(在 Mac 或 Linux 中),并激活虚拟的chatting环境。然后运行以下命令:
**conda install statsmodels**
在你的 Spyder 编辑器中输入 Listing 14-10,并将脚本保存为alpha_beta.py,保存在你的章节文件夹中。
from datetime import date, timedelta
import statsmodels.api as sm
from pandas_datareader import data as pdr
# Set the start and end dates
end_date = date.today().strftime("%Y-%m-%d")
start_date = (date.today() - timedelta(days=180)).strftime("%Y-%m-%d")
market = "^GSPC"
ticker = "MSFT"
# Retrieve prices
sp = pdr.get_data_yahoo(market, start=start_date, end=end_date)
stock = pdr.get_data_yahoo(ticker, start=start_date, end=end_date)
# Calculate returns for sp500 and the stock
sp['ret_sp'] = (sp['Adj Close']/sp['Adj Close'].shift(1))-1
stock['ret_stock'] = (stock['Adj Close']/stock['Adj Close'].shift(1))-1
# Merge the two datasets, keep only returns
df = sp[['ret_sp']].merge(stock[['ret_stock']],\
left_index=True, right_index=True)
# Add risk-free rate (assume constant for simplicity)
1 df['rf'] = 0.00001
# We need a constant to run regressions
df['const'] = 1
df['exret_stock'] = df.ret_stock - df.rf
df['exret_sp'] = df.ret_sp - df.rf
# Remove missing values
df.dropna(inplace=True)
# Calculate the stock's alpha and beta
2 reg = sm.OLS(endog=df['exret_stock'],\
exog=df[['const', 'exret_sp']], missing='drop')
results = reg.fit()
print(results.summary())
3 alpha = round(results.params['const']*100,3)
beta = round(results.params['exret_sp'],2)
# Print the values of alpha and beta
print(f'The alpha of the stock of {ticker} is {alpha} percent.')
print(f'The beta of the stock of {ticker} is {beta}.')
Listing 14-10:计算股票 alpha 和 beta 的脚本
我们导入模块后,指定你想要提取的数据的起始和结束日期。我们再次使用最近的六个月数据。我们还提供市场指数的股票代码,市场指数代表整体市场。S&P 500 指数通常被用作市场指数,我们也将使用该指数。我们将分析的公司是微软公司。我们使用pandas_datareader模块中的get_data_yahoo()方法提取市场指数和微软的每日股票价格信息,并将数据分别保存为两个pandas DataFrame,命名为sp和stock。
然后,我们计算 S&P 500 和微软的每日股票回报。pandas中的shift()方法允许我们将索引按所需的周期数移动。我们使用shift(1)获取前一个交易日的价格信息。这使我们能够看到今天与昨天的比较。比较这两天可以帮助我们计算回报。毛回报是当前价值与前一个交易日收盘价的比值,净回报是毛回报减去 1。
为了计算 alpha 和 beta,我们首先将两个数据集合并为一个。为了简便,我们使用一个小常数值作为无风险利率 1。然后,我们使用statsmodels模块中的OLS()方法进行回归 2,并输出回归结果。我们需要的 alpha 和 beta 分别是常数项和市场超额收益的回归系数 3。
图 14-7 显示了回归结果。

图 14-7:微软回归分析结果
最后,我们按如下方式输出公司的 alpha 和 beta 值:
The alpha of the stock MSFT is 0.202 percent.
The beta of the stock MSFT is 1.1.
分析显示,alpha 和 beta 分别为 0.202%和 1.1。这意味着微软的日均表现超越了市场上类似股票 0.202%,并且该公司市场风险稍大于平均公司(其 beta 为 1),这意味着该股票的回报波动性略高于整体市场。
添加语音控制
让我们加入语音控制!你可以询问某家公司,脚本将搜索其股票代码,获取每日股价信息并计算 alpha 和 beta。然后,脚本会通过语音告诉你这些信息。短语“某公司股票报告”将触发股票报告功能。
在你的 Spyder 编辑器中输入列表 14-11,并将脚本保存为alpha_beta_hs.py在你的章节文件夹中。
from datetime import date, timedelta
import statsmodels.api as sm
from pandas_datareader import data as pdr
import requests
from mptpkg import voice_to_text, print_say
1 def alpha_beta(firm):
try:
# Extract the source code from the website
2 url = 'https://query1.finance.yahoo.com/v1/finance/search?q='+firm
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the value corresponding to "quotes"
quotes = response_json['quotes']
# Get the ticker symbol
ticker = quotes[0]['symbol']
`--snip--`
# Speak the values of alpha and beta
3 print_say(f'The alpha of the stock of {firm} is {alpha} percent.')
print_say(f'The beta of the stock of {firm} is {beta}.')
# Start an infinite loop
4 while True:
# Obtain voice input from you
print_say("How may I help you?")
inp = voice_to_text()
print_say(f"You said {inp}.")
# If you want to stop, say "stop listening"
if inp == "stop listening":
print_say("Nice talking to you; goodbye!")
break
# If keywords in command, go to the stock report functionality
elif "stock report for" in inp:
# Locate the company name
pos = inp.find('stock report for ')
5 firm = inp[pos+len('stock report for '):]
alpha_beta(firm)
continue
# Otherwise, go to the next iteration
else:
continue
列表 14-11:语音控制股票 alpha 和 beta 的计算
我们导入模块,包括requests模块以及print_say()和voice_to_text()函数。
在第 1 步,我们开始定义alpha_beta()函数,使用公司名称作为其参数。如之前所示,我们使用加号将单词连接起来,作为 Yahoo! Finance 2 上的股票代码的搜索关键词。我们使用try和except来防止程序崩溃,并让用户知道输入是否无效。然后,脚本计算公司的 alpha 和 beta,正如在alpha_beta.py中一样,最后输出并朗读 alpha 和 beta 3。
在第 4 步,我们开始一个无限循环,询问你的语音输入。要退出脚本,可以说“停止监听”。否则,你可以说“某公司股票报告”,后接公司名称,以激活股票报告功能。脚本将从你的语音命令中提取公司名称,并为你准备报告 5。
这是我的示例交互:
How may I help you?
You said **stock report for alibaba.**
The alpha of the stock alibaba is 0.059 percent.
The beta of the stock alibaba is 0.61.
How may I help you?
You said **stop listening.**
Nice talking to you; goodbye!
我询问了“阿里巴巴的股票报告”,脚本为我获取了报告并回答说:“阿里巴巴的 alpha 为 0.059%,阿里巴巴股票的 beta 为 0.61。”
总结
在本章中,你将语音识别和文本转语音技术应用于金融市场。这些技能——抓取信息、构建可用于 URL 的搜索词,并获取实时以及最近的每日股价信息——可以广泛应用于各种网页应用程序。你还学到了一些数据分析和可视化技能,这些技能在许多应用中也非常有用。
在下一章中,你将创建图形化的语音市场监控工具,适用于如美国股票市场或外汇市场等金融市场。
章节末练习
-
修改 price_plot.py,使得开始日期和结束日期分别为 2021 年 3 月 1 日和 2021 年 6 月 1 日,并且图表的颜色为红色。
-
修改 candle_stick.py,使得 x 轴上的日期格式为 01-01-2021(而不是 01/01/2021 或 2021 年 1 月 1 日),并旋转 15 度。
股票市场监控工具

在这一章中,你将创建一个图形化的语音应用程序,实时监控美国股票市场。当你在交易时段运行脚本时,你将看到主要股指和你选择的几只股票的图形展示。该应用程序还会用人的声音告诉你股指和股票价格的数值。
为了掌握必要的技能,你将首先创建一个图形化的比特币监控工具,显示实时价格信息,使用 Python 的 tkinter 包。你可以将这些技巧应用于其他金融市场,例如世界股票市场或美国国债市场。
一如既往,所有脚本可以通过本书的资源页面访问:www.nostarch.com/make-python-talk/,你应为本章创建文件夹 /mpt/ch15/。
比特币监控工具
我们将从比特币开始,因为比特币的价格是 24/7 更新的,不像股票市场,股票市场只有在开放时才会更新实时价格。在创建比特币监控工具的过程中,你将学会构建其他金融市场监控工具所需的技能。该脚本会在比特币价格变化时提醒你,或者当价格超出预设的上下限时发出警告。
首先,你将学习如何读取 JSON 数据以及一些 tkinter 包的基础知识。
如何读取 JSON 数据
比特币的价格可以在网上免费获取,并且每分钟更新一次,全天候更新。我们将通过 Python 使用 API api.coindesk.com/v1/bpi/currentprice.json 来获取比特币价格。用浏览器打开该 URL,你应该能看到类似 图 15-1 的价格信息。

图 15-1:比特币价格的实时在线信息
这些数据是 JSON 格式的,且难以阅读。由于有许多嵌套字典,很难分辨每个字典的起始和结束位置。我们在第十四章中讨论了如何通过使用在线 JSON 数据格式化工具来使数据更易于理解。
类似于你在那一章所做的,访问在线 JSON 数据格式化网站 jsonformatter.curiousconcept.com/,将 图 15-1 中的数据粘贴到指定位置,然后点击 Process。格式化工具会将数据转换为更易读的格式,如 清单 15-1 所示。
{
1 "time":{
"updated":"Mar 3, 2021 09:58:00 UTC",
"updatedISO":"2021-03-03T09:58:00+00:00",
"updateduk":"Mar 3, 2021 at 09:58 GMT"
},
2 "disclaimer":"This data was produced from the CoinDesk
Bitcoin Price Index (USD). Non-USD currency data converted
using hourly conversion rate from openexchangerates.org",
3 "chartName":"Bitcoin",
4 "bpi":{
"USD":{
"code":"USD",
"symbol":"$",
"rate":"51,462.6831",
"description":"United States Dollar",
"rate_float":51462.6831
},
"GBP":{
"code":"GBP",
"symbol":"£",
"rate":"36,859.0146",
"description":"British Pound Sterling",
"rate_float":36859.0146
},
"EUR":{
"code":"EUR",
"symbol":"€",
"rate":"42,617.8433",
"description":"Euro",
"rate_float":42617.8433
}
}
}
列表 15-1:有关比特币价格的格式化 JSON 数据
数据集是一个包含四个元素的大字典,键分别为time 1、disclaimer 2、chartName 3 和 bpi 4。bpi键的值又是一个字典,包含三个键:USD、GBP和EUR,分别表示比特币在美元、英镑和欧元中的价格。
我们想要获取美元(USD)的比特币价格。脚本bitcoin_price.py(见列表 15-2)获取比特币价格并将其打印出来。
import requests
# Specify the url to find the bitcoin price
url = 'https://api.coindesk.com/v1/bpi/currentprice.json'
# Retrieve the live information from bitcoin url
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the USD dictionary
usd = response_json['bpi']['USD']
# Get the price
price = usd['rate_float']
print(f"The Bitcoin price is {price} dollars.")
列表 15-2:用于获取比特币价格的脚本
我们导入了requests模块,并指定了实时比特币价格的 URL。然后,我们使用requests模块中的get()方法从 API 中拉取数据。requests模块中的json()方法将信息读取为 JSON 格式。接着,我们提取包含所有比特币价格信息的美元(USD)字典。我们需要从字典中获取的值是价格,并使用rate_float键来检索它。
最后,我们打印出比特币价格。输出应该类似于以下内容:
The Bitcoin price is 51462.6831 dollars.
tkinter包简要介绍
Python 用于构建图形用户界面(GUI)的默认标准包是tkinter,即Tk 界面的缩写。tkinter包有多种控件,例如按钮、标签、输入框和消息框等各种工具。控件作为小型窗口出现在顶层根窗口内,但也可以是独立的实体。我们将重点介绍标签控件,因为在市场监控项目中,我们将使用它们。
tkinter包是 Python 标准库的一部分,无需安装。如果你使用的是 Linux,并且在导入tkinter时遇到ModuleNotFoundError,请在终端执行以下命令来安装它:
**sudo apt-get install python3-tk**
我将向你介绍tkinter的基础知识,包括如何设置屏幕和创建标签控件。脚本tk_label.py(见列表 15-3)设置了一个屏幕并添加了一个标签。
import tkinter as tk
# Create the root window
root = tk.Tk()
# Specify the title and size of the root window
root.title("A Label Inside a Root Window")
root.geometry("800x200")
# Create a label inside the root window
label = tk.Label(text="this is a label", fg="Red", font=("Helvetica", 80))
label.pack()
# Run the game loop
root.mainloop()
列表 15-3:在tkinter包中创建标签
我们导入了tkinter包。我们设置了一个根窗口,用来容纳我们将要添加到脚本中的所有控件。我们使用命令Tk()并将根窗口命名为root。
标签是用于显示消息或图片的简单控件,主要用于信息展示。我们为根窗口设置一个标题A Label Inside a Root Window,该标题会显示在标题栏中。我们调用geometry()方法来指定根窗口的宽度和高度为 800x200 像素。
我们通过使用Label()来初始化一个标签,该方法接受你想要显示的文本(或图片)。你也可以选择性地指定颜色和字体。我们使用红色,并将字体设置为("Helvetica", 80)。
使用pack()方法,我们可以指定标签的位置。默认情况下,控件从根窗口的顶部中心开始对齐。最后,mainloop()启动游戏循环,使得窗口出现在计算机屏幕上并保持显示。
运行脚本,你应该看到图 15-2。

图 15-2:tkinter中根窗口内的标签
一个图形化的比特币监视器
现在,我们将使用tkinte包创建一个图形化比特币监视器。打开你的 Spyder 编辑器,并将列表 15-4 中的代码保存为bitcoin_tk.py,存放在章节文件夹中。
import tkinter as tk
import requests
1 import arrow
# Specify the url to find the Bitcoin price
url = 'https://api.coindesk.com/v1/bpi/currentprice.json'
# Create a root window to hold all widgets
2 root = tk.Tk()
# Specify the title and size of the root window
root.title("Bitcoin Watch")
root.geometry("1000x400")
# Create a first label using the Label() function
3 label = tk.Label(text="", fg="Blue", font=("Helvetica", 80))
label.pack()
# Create a second label
label2 = tk.Label(text="", fg="Red", font=("Helvetica", 60))
label2.pack()
# Define the bitcoin_watch() function
4 def bitcoin_watch():
# Get the live information from Bitcoin url
response = requests.get(url)
response_json = response.json()
price = response_json['bpi']['USD']['rate_float']
# Obtain current date and time information
tdate = arrow.now().format('MMMM DD, YYYY')
tm = arrow.now().format('hh:mm:ss A')
# Put the date and time information in the first label
5 label.configure(text=tdate + "\n" + tm)
# Put price info in the second label
label2.configure(text=f'Bitcoin: {price}', justify=tk.LEFT)
# Call the bitcoin_watch() function after 1000 milliseconds
6 root.after(1000, bitcoin_watch)
# Call the bitcoin_watch() function
bitcoin_watch()
# Run the game loop
root.mainloop()
列表 15-4:创建一个图形化比特币价格监视器
我们导入了必要的函数和模块,包括用于显示当前时间和日期的arrow模块 1。然后我们使用Tk()方法创建一个顶级根窗口,并指定标题和大小 2。
我们使用Label()创建了两个标签 3。首先我们将两个标签中的信息设置为空字符串,因为这些信息将从比特币监视器中填充。在第 4 行,我们定义了bitcoin_watch()。该函数首先使用 requests 模块从我们提供的 URL 获取比特币价格信息。我们还获取了当前的日期和时间,并分别将它们保存在变量tdate和tm中。
在第 5 行,我们将当前的日期和时间信息放入第一个标签,使用转义字符\n来分隔行。我们将实时的比特币价格放入第二个标签。
接下来我们设置动画效果 6。我们使用after()在指定的时间后调用另一个函数。命令after(1000, bitcoin_watch)在 1000 毫秒后调用bitcoin_watch()函数。在bitcoin_watch()函数内调用该命令,会创建一个无限循环,每 1000 毫秒执行一次bitcoin_watch()中的所有命令行。结果是时间不断更新,你会看到时间值每秒变化。如果你让屏幕长时间保持活跃,你也会看到比特币价格每隔大约一分钟发生变化。
运行时,脚本应类似于图 15-3。

图 15-3:使用after()函数创建一个动画比特币监视器
一个会说话的比特币监视器
接下来,我们将添加语音功能。每当价格更新时,脚本会用人声通知你。我们还将添加一个警报系统:当比特币价格超出预设的上下限时,脚本会大声提醒你。
打开章节文件夹中的bitcoin_watch.py。它与bitcoin_tk.py的区别在列表 15-5 中有突出显示。
`--snip--`
from mptpkg import print_say
# Specify the url to find the Bitcoin price
url = 'https://api.coindesk.com/v1/bpi/currentprice.json'
`--snip--`
# Create a second label
label2 = tk.Label(text="", fg="Red", font=("Helvetica", 60))
label2.pack()
# Set up the price bounds
response = requests.get(url)
response_json = response.json()
1 oldprice = response_json['bpi']['USD']['rate_float']
maxprice = oldprice * 1.05
minprice = oldprice * 0.95
2 print_say(f'The Bitcoin price is now {oldprice}!')
# Define the bitcoin_watch() function
def bitcoin_watch():
3 global oldprice
# Get the live information from Bitcoin url
response = requests.get(url)
response_json = response.json()
price = response_json['bpi']['USD']['rate_float']
# If there is update in price, announce it
4 if price != oldprice:
oldprice = price
print_say(f'The Bitcoin price is now {oldprice}!')
# If price goes out of bounds, announce it
5 if price > maxprice:
print_say('The Bitcoin price has gone above the upper bound!')
if price < price:
print_say('The Bitcoin price has gone below the lower bound!')
# Obtain current date and time information
tdate = arrow.now().format('MMMM DD, YYYY')
tm = arrow.now().format('hh:mm:ss A')
`--snip--`
列表 15-5:创建一个会说话的图形化比特币价格监视器的脚本
我们导入了必要的模块,包括来自本地mptpkg包的print_say()函数。
我们获取比特币的价格作为起始价格,并将其保存为oldprice 1。我们设置上限和下限,分别为oldprice存储值的上下 5%的数值,并将其保存为maxprice和minprice。脚本会用人声宣布比特币当时的价格 2。
我们将oldprice声明为全局变量,这样它可以在bitcoin_watch()函数内外都能被识别 3。每次调用bitcoin_watch()时,它都会获取最新的比特币价格,并与oldprice中存储的值进行比较。如果值不同,oldprice的值会更新为新价格,并且脚本会宣布更新后的价格 4。
在第 5 步,脚本会检查价格是否超过上限;如果是,脚本会进行播报。类似地,脚本还会检查价格是否低于下限,如果是,也会进行播报。
这是运行脚本几分钟后的输出:
The Bitcoin price is now 51418.8064!
The Bitcoin price is now 51377.4967!
The Bitcoin price is now 51419.3027!
一个会讲解的股市监控
现在我们将使用这些技巧来构建一个可以讲解的、图形化的实时美国股市监控。我们会对比特币版本做出一些重要的修改。
首先,我们将不再只展示一个资产,而是涵盖市场中的三大玩家:苹果、亚马逊和特斯拉。我们还会展示我们感兴趣的主要指数:道琼斯工业平均指数和标准普尔 500 指数。
其次,我们将脚本的更新时间从每千毫秒一次调整为每两分钟更新一次。脚本需要获取五个信息,而不是仅仅一个,频繁更新会导致信息过载,从而可能导致脚本冻结。更重要的是,市场指数和前三只股票的价格在交易时间内每几秒更新一次。过于频繁的更新会导致语音宣布不停,反而分散注意力。你可以根据自己的喜好调整脚本的更新时间频率。
将列表 15-6 中的脚本保存为stock_watch.py,放入章节文件夹中,或者从书籍的资源页面下载。
import tkinter as tk
import arrow
from yahoo_fin import stock_info as si
from mptpkg import print_say
# Create a root window hold all widgets
1 root = tk.Tk()
# Specify the title and size of the root window
root.title("U.S. Stock Market Watch")
root.geometry("1100x750")
# Create a first label using the Label() function
label = tk.Label(text="", fg="Blue", font=("Helvetica", 80))
label.pack()
# Create a second label
label2 = tk.Label(text="", fg="Red", font=("Helvetica", 60))
label2.pack()
# Set up tickers and names
tickers = ['^DJI', '^GSPC', 'AAPL', 'AMZN', 'TSLA']
names = ['DOW JONES', 'S&P500', 'Apple', 'Amazon', 'Tesla']
# Set up the oldprice values and price bounds
2 oldprice = []
maxprice = []
minprice = []
for i in range(5):
p = round(float(si.get_live_price(tickers[i])), 2)
oldprice.append(p)
maxprice.append(p * 1.05)
minprice.append(p * 0.95)
if i <= 1:
print_say(f'The latest value for {names[i]} is {p}!')
else:
print_say(f'The latest stock price for {names[i]} is {p} dollars!')
# Define the stock_watch() function
3 def stock_watch():
# Declare global variables
global oldprice, maxprice, minprice
# Obtain live information about the DOW JONES index from Yahoo
4 p1 = round(float(si.get_live_price("^DJI")), 2)
m1 = f'DOW JONES: {p1}'
# Obtain live information about the SP500 index from Yahoo
p2 = round(float(si.get_live_price("^GSPC")), 2)
m2 = f'S&P500: {p2}'
# Obtain live price information for Apple stock from Yahoo
p3 = round(float(si.get_live_price("AAPL")), 2)
m3 = f'Apple: {p3}'
# Obtain live price information for Amazon stock from Yahoo
p4 = round(float(si.get_live_price("AMZN")), 2)
m4 = f'Amazon: {p4}'
# Obtain live price information for Tesla stock from Yahoo
p5 = round(float(si.get_live_price("TSLA")), 2)
m5 = f'Tesla: {p5}'
# Put the five prices in a list p
5 p = [p1, p2, p3, p4, p5]
# Obtain current date and time information
tdate = arrow.now().format('MMMM DD, YYYY')
tm = arrow.now().format('hh:mm:ss A')
# Put the date and time information in the first label
label.configure(text=tdate + "\n" + tm)
# Put all the five messages on the stock market in the second label
label2.configure(text=m1 +\
"\n" + m2 + "\n" + m3 + "\n" + m4 + "\n" + m5, justify=tk.LEFT)
# If there is update in the market, announce it
6 for i in range(5):
if p[i] != oldprice[i]:
oldprice[i] = p[i]
if i <= 1:
print_say(f'The latest value for {names[i]} is {p[i]}!')
else:
print_say\
(f'The latest stock price for {names[i]} is {p[i]} dollars!')
# If price goes out of bounds, announce it
7 for i in range(5):
if p[i] > maxprice[i]:
print_say(f'{names[i]} has moved above the upper bound!')
if p[i] < minprice[i]:
print_say(f'{names[i]} has moved below the lower bound!')
# Call the stock_watch() function
8 root.after(120000, stock_watch)
# Call the stock_watch() function
stock_watch()
# Run the game loop
root.mainloop()
列表 15-6:创建一个讲解的、图形化的实时美国股市监控脚本
我们导入了模块,包括用于显示时间和日期的arrow以及用于获取股票价格信息的yahoo_fin。我们还从本地的mptpkg包中导入了print_say()来进行语音播报。
从第 1 步开始,我们创建了tKinter根窗口,并在其中放置了两个标签,就像在bitcoin_watch.py中做的一样。然后我们创建了三个列表:oldprice、maxprice和minprice 2。我们使用oldprice来跟踪脚本运行时两个指数和三只股票的价格。maxprice列表保存上限,数值为oldprice中相应值的 5%之上。同样,我们在minprice中定义了五个下限。
然后,脚本会宣布两个指数的值和三只股票的价格。请注意,我们在三只股票价格后加上了dollars,但没有在两个指数值后加,因为指数值不是以美元为单位的。
我们在 3 处定义了stock_watch(),声明了oldprice为全局变量。每次调用该函数时,它会检索我们感兴趣的值 4。我们将所有值保留两位小数,并将它们保存在列表p中 5。
我们获取时间和日期并将其放入第一个标签。我们将两个指数和三只股票的值放入第二个标签。在 6 时,我们检查五个值是否有更新,并打印并宣布任何更新。同时,我们还会相应地更新存储在oldprice中的值。
从 7 处开始,我们检查五个值是否有越界。如果有,脚本会发出通知。最后,我们使用after()创建动画效果 8。stock_watch()函数每隔 120,000 毫秒调用一次自身,每两分钟更新一次屏幕。
以下是与脚本交互的一次输出:
The latest value for DOW JONES is 31477.02!
The latest value for S&P500 is 3861.02!
The latest stock price for Apple is 124.65 dollars!
The latest stock price for Amazon is 3062.5 dollars!
The latest stock price for Tesla is 692.41 dollars!
The latest value for DOW JONES is 31460.43!
The latest value for S&P500 is 3859.14!
The latest stock price for Apple is 124.49 dollars!
The latest stock price for Amazon is 3062.32 dollars!
The latest stock price for Tesla is 690.8 dollars!
The latest value for DOW JONES is 31434.83!
The latest value for S&P500 is 3853.88!
The latest stock price for Apple is 124.26 dollars!
The latest stock price for Amazon is 3052.31 dollars!
The latest stock price for Tesla is 687.56 dollars!
仅仅几分钟,脚本已经更新了所有五个值三次。图 15-4 展示了最终的屏幕。

图 15-4:一个图形化的美国股市实时监控
将方法应用于其他金融市场
我们可以将这些方法应用到其他金融市场。如果从 Yahoo! Finance 可以获取价格信息,修改就很小:我们只需在脚本中更改股票代码。
如果从 Yahoo! Finance 无法获取价格信息,请在线查找一个提供市场 JSON 数据的网站,然后使用我们之前用来获取比特币价格的方法。
总结
在本章中,你首先学会了如何从 JSON 数据中获取信息,并使用这些信息创建一个图形化的比特币监控,使用了tkinter包。你在线获取了实时比特币价格,并在tkinter中创建了带动画的小部件。
通过这些技能,你创建了一个带语音提示的美国股市实时图形监控。脚本生成了两个主要美国股指和你感兴趣的三只股票的图形化显示。当价格发生变化时,脚本会用人声告诉你。若某个指数值或股票价格超出了预设范围,脚本也会发出警报。
你还学会了如何应用这个过程,为其他金融市场创建一个带语音的图形化市场监控。
章节末练习
-
修改bitcoin_price.py,以检索英镑价格而不是美元价格,并将其作为字符串变量而非浮动点数。
-
修改tk_label.py,使得根窗口的大小为 850x160 像素,并且标签中的消息显示为
here is your label。 -
修改bitcoin_tk.py,使得屏幕每 0.8 秒刷新一次。
-
修改 bitcoin_watch.py,使得当您开始运行脚本时,上下限设置为价格的 3% 上下浮动。
使用世界语言

到目前为止,我们已经教会了 Python 如何用英语进行听说。但是 Python 可以理解许多其他世界语言。在本章中,您将首先教会 Python 使用我们之前使用的模块进行几种其他语言的口语表达。然后,我会介绍一个有用的模块 translate,它可以将一种语言翻译成另一种语言,您将用它来静默翻译语言。接着我们将添加语音识别和语音合成功能,这样您就可以用一种语言对 Python 脚本说话,脚本将以您选择的另一种语言进行翻译。
和往常一样,本章的所有脚本可以在本书的资源页面找到,网址为 www.nostarch.com/make-python-talk/。首先创建文件夹 /mpt/ch16/ 来存放本章内容。
其他语言的语音合成
为了使用非英语语言,我们将使用 gTTS,因为它支持大多数主要的世界语言。使用 gTTS 的缺点是它需要一个单独的模块来播放音频文件,但替代方案 (pyttsx3) 不支持广泛的非英语语言。这里我们将尝试使用 gTTS 模块进行一些示例。
安装模块
在 Windows 中安装 gTTS 模块,激活虚拟环境 chatting,然后在 Anaconda 提示符中执行以下命令,并按照屏幕上的说明操作:
**pip install gTTS**
如果您使用的是 Mac 或 Linux,您应该已经在第四章中安装了 gTTS 模块。然而,Google 翻译已知会对该模块进行重大更改,因此您应该通过在终端中运行以下命令来升级到最新版本,同时激活虚拟环境 chatting:
**pip install --upgrade gTTS**
您还需要安装 pydub 模块来播放音频文件。无论您使用的是 Windows、Mac 还是 Linux,您都需要执行这一步。在激活 chatting 虚拟环境的情况下,在 Anaconda 提示符(Windows)或终端(Mac 或 Linux)中执行以下两行代码:
**conda install -c conda-forge pydub**
**conda install -c conda-forge ffmpeg**
按照指示一直执行下去。
将文本转换为西班牙语语音
在 清单 16-1 中的脚本 speak_spanish.py 展示了 gTTS 模块如何将书面西班牙语转换为口语西班牙语。在 Spyder 编辑器中输入这些代码行,并将脚本保存为 speak_spanish.py,保存在您的章节文件夹中。
from io import BytesIO
from gtts import gTTS
from pydub import AudioSegment
from pydub.playback import play
# Convert text to speech in Spanish
tts = gTTS(text='Buenos días',lang='es')
# Create a temporary file
voice = BytesIO()
# Save the voice output as an audio file
tts.write_to_fp(voice)
# Play the audio file
voice.seek(0)
play(AudioSegment.from_mp3(voice))
清单 16-1:将书面西班牙语转换为口语西班牙语的脚本
我们首先导入模块,包括 gTTS 和 pydub,这些模块将播放音频文件。
接下来,我们使用gTTS()函数将西班牙语短语Buenos días转换为语音西班牙语。该短语可以字面翻译为早安。gTTS()的第一个参数指定要转换的短语,第二个参数指定使用的语言。在这种情况下,我们使用es,即Español,或者西班牙语(请参见表 16-1 查看语言代码列表)。
脚本通过使用BytesIO()函数在io模块中生成一个临时文件voice。如果你使用固定的文件名(例如myfile.mp3),脚本可能会阻止你在重新运行时覆盖该文件,并可能导致崩溃。通过每次运行脚本时使用临时文件,可以避免崩溃。
最后,我们将语音输出保存为我们刚才创建的临时文件voice中的音频文件。然后,我们使用pydub模块播放音频文件。运行脚本可以听到 Python 用西班牙语说“Buenos días”。
支持其他语言的文本转语音
gTTS模块可以将文本转换为大多数主要语言的语音。表 16-1 提供了该模块支持的语言的一个不完全列表,后面跟着在gTTS()函数中使用的代码。
表 16-1:主要世界语言及其在gTTS模块中的对应代码
| 语言名称 | 语言代码 |
|---|---|
| 阿拉伯语 | ar |
| 中文 | zh |
| 荷兰语 | nl |
| 英语 | en |
| 法语 | fr |
| 德语 | de |
| 意大利语 | it |
| 日语 | ja |
| 韩语 | ko |
| 葡萄牙语 | pt |
| 俄语 | ru |
| 西班牙语 | es |
你可以在cloud.google.com/speech-to-text/docs/languages/找到更全面的列表。
接下来,你将创建一个脚本来选择你想要的语言。之后,你会让脚本将短语从文本转换为语音。
将文本转换为世界语言的语音
脚本speak_world_languages.py(见列表 16-2)演示了如何将文本转换为几种主要世界语言的语音。
from io import BytesIO
from gtts import gTTS
from pydub import AudioSegment
from pydub.playback import play
# Create a dictionary of languages and the corresponding codes
1 lang_abbre = {"english":"en",
"chinese":"zh",
"spanish":"es",
"french":"fr",
"japanese":"ja",
"portuguese":"pt",
"russian":"ru",
"korean":"ko",
"german":"de",
"italian":"it"}
2 lang = input("What language do you want to use?\n")
phrase = input("What phrase do you want to convert to voice?\n")
# Convert text to speech
tts = gTTS(text=phrase,lang=lang_abbre[lang])
# Create a temporary file
voice = BytesIO()
# Save the voice output as an audio file
tts.write_to_fp(voice)
# Play the audio file
voice.seek(0)
play(AudioSegment.from_mp3(voice))
列表 16-2:将书面语言转换为语音语言的脚本
我们创建了一个字典lang_abbre,将不同的外语映射到gTTS模块中的相应代码。然后脚本会询问你想使用的语言。你可以在 IPython 控制台中输入你的选择。然后在提示符中输入你想转换为语音的短语。
脚本将你的短语转换成音频文件,并保存在临时文件voice中。然后它使用pydub模块播放音频文件。
以下是与脚本的交互,我的文本输入为粗体:
What language do you want to use?
**chinese**
What phrase do you want to convert to voice?
**嗨,你好吗?**
我首先选择了中文语言,然后输入了文本嗨,你好吗?,这是中文的嗨,你好吗? 运行脚本后,我听到了 Python 用中文说话。
主要世界语言的语音识别
本书中使用的语音识别模块也能够识别其他主要世界语言。我们只需要告诉脚本我们想使用哪种语言。
我们将使用日语作为示例来说明它是如何工作的。列表 16-3 中的脚本 sr_japanese.py 能够识别日语语音并将你的声音转换为文字。
import speech_recognition as sr
# Initiate speech recognition
speech = sr.Recognizer()
# Use it to capture spoken Japanese
print('Python is listening in Japanese...')
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
1 my_input = speech.recognize_google(audio, language="ja")
print(f"you said: {my_input}")
except sr.UnknownValueError:
pass
列表 16-3:日语语音识别
我们首先导入语音识别模块。然后,我们通过使用 Recognizer() 函数启动语音识别。脚本会打印出信息 Python is listening in Japanese 来提示你对着麦克风说日语。我们使用 adjust_for_ambient_noise() 函数来减少环境噪声对语音输入的影响。
在第 1 行中,我们通过在 recognize_google() 函数中传递 language="ja" 来指定日语。回想一下第三章,recognize_google() 使用的是 Google Web Speech API;与此相对的是其他方法,例如使用 Microsoft Bing Speech 服务的 recognize_bing(),或者使用 IBM Speech to Text 服务的 recognize_ibm()。然后,脚本会输出你用日语输入的语音内容。
这是我与计算机互动的输出:
Python is listening in Japanese...
you said: ありがとうございます
我对着麦克风用日语说“谢谢”。脚本正确地捕捉到这句话并输出。
你可以轻松修改 sr_japanese.py,只需将 language="ja"(以及提示中的适当语言标题)替换为你选择的语言,这样你就可以用另一种语言与计算机互动。世界语言及其相应代码的列表可以在 www.science.co.il/language/Locale-codes.php. 找到。
会话维基百科
维基百科支持大多数主要世界语言,详细信息请参见 en.wikipedia.org/wiki/List_of_Wikipedias。在第五章中,我们创建了一个英文版的会话维基百科。我们将构建一个版本,您可以将其调整为适应任何主要语言。列表 16-4 使用了中文。将以下代码输入到 Spyder 编辑器中并保存为 wiki_world_languages.py。
from io import BytesIO
import speech_recognition as sr
from gtts import gTTS
from pydub import AudioSegment
from pydub.playback import play
import Wikipedia
from mptpkg import print_say
# Create a dictionary of languages and the corresponding codes
lang_abbre = {"english":"en",
"chinese":"zh",
"spanish":"es",
"french":"fr",
"japanese":"ja",
"portuguese":"pt",
"russian":"ru",
"korean":"ko",
"german":"de",
"italian":"it"}
Lang = input("What language do you want to use?\n")
# Initiate speech recognition
speech = sr.Recognizer()
# Request a query in a specified language
1 print_say(f"Say what you want to know in {lang}...")
# Capture your voice query in the language of your choice
2 with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
while True:
try:
audio = speech.listen(source)
my_input = speech.recognize_google(audio, language=lang_abbre[lang])
break
except sr.UnknownValueError:
print_say("Sorry, I cannot understand what you said!")
# Print out what you said
3 print(f"you said: {my_input}")
# Obtain answer from Wikipedia and print out
wikipedia.set_lang(lang_abbre[lang])
Ans = wikipedia.summary(my_input)[0:200]
print(ans)
# Convert text to speech in the language of your choice
4 tts = gTTS(text=ans,lang=lang_abbre[lang])
# Create a temporary file
Voice = BytesIO()
# Save the voice output as an audio file
tts.write_to_fp(voice)
# Play the audio file
voice.seek(0)
play(AudioSegment.from_mp3(voice))
列表 16-4:多种主要世界语言的会话维基百科
我们导入模块,包括在第五章中使用的 wikipedia 模块。字典 lang_abbre 将不同的外语映射到 gTTS 模块中的相应代码。我们还将使用 speech_recognition 模块和 wikipedia 模块中的语言代码。
然后,脚本会询问你想使用哪种语言 1。你可以在 IPython 控制台中输入你的选择。然后,用你选择的语言对着麦克风说出你的查询 2。脚本会捕捉语音输入,将其转为文字,并保存在 my_input 中。
然后,脚本会打印出你的查询 3。完成后,我们将维基百科的语言设置为你选择的语言。接着,我们将查询发送给维基百科并打印结果。最后,我们将答案转换为语音,并让脚本用人声播放 4。
下面是与脚本交互的输出,其中我的书面输入和语音输入以粗体显示:
What language do you want to use?
**chinese**
Say what you want to know in chinese...
美利堅合眾國(英語:United States of America, 縮寫為 USA,一般稱為 United States(U.S.或 US),或 America),中文通稱「美國」,是由其下轄 50 个州、華盛頓哥倫比亞特區、五个自治领土及外岛共同組成的聯邦共和国。美國本土 48 州和联邦特区位於北美洲中部,東臨大西洋,北面是加拿大,南部和墨西哥及墨西哥灣接壤,本土位於溫帶、副熱帶地區。阿拉斯加州位於北美大陸西
我首先输入了chinese作为我的语言选择。然后,我用中文在麦克风前说出了“United States of America”(美国),脚本存储了美国的简短描述,并将其打印出来,同时用语音播报。
创建你自己的语音翻译器
现在你将创建你自己的语音翻译器。当你用任何主要语言对着脚本讲话时,脚本会将其翻译成你选择的另一种语言并用语音播报。
我们首先会使用translate模块创建一个文本版本,然后添加语音识别和文本转语音功能。
一种基于文本的翻译器
我们首先需要安装由谷歌翻译提供支持的translate模块。该模块不在 Python 标准库中,因此我们需要使用pip install进行安装。打开 Anaconda 提示符(Windows 中)或终端(Mac 或 Linux 中)。在激活虚拟环境chatting的情况下,运行以下命令:
**pip install translate**
按照说明完成安装。
清单 16-5 中的脚本将英语翻译为中文,并将中文翻译为英语,使用文本输入。打开 Spyder 编辑器并复制以下代码;然后将其保存为english_chinese.py,放入你的章节文件夹中。
# Import the Translator function from the translate module
from translate import Translator
# Specify the input and output languages
translator = Translator(from_lang="en",to_lang="zh")
# Do the actual translation
translation = translator.translate("hello all")
print(translation)
# Specify the input and output languages
translator = Translator(from_lang="zh",to_lang="en")
# Do the actual translation
translation = translator.translate("请再说一遍")
print(translation)
清单 16-5:英语和中文之间的翻译
我们首先从translate模块导入Translator()函数。我们需要指定输入语言(此处为英语from_lang="en")和输出语言(此处为中文to_lang="zh")。我们将短语hello all从英语翻译为中文并打印出来。
然后,我们反转输入和输出语言,将短语请再说一遍从中文翻译为英语并打印出来。输出如下:
大家好!
please say it again
我们可以在english_chinese.py中修改输入和输出语言,以使用任何两种主要的世界语言。要查看translate模块支持的语言及其对应的代码,请访问www.labnol.org/code/19899-google-translate-languages/。
一种基于语音的翻译器
接下来,我们将添加语音识别和文本转语音功能。再次强调,我们将硬编码要翻译的语言,但你可以轻松地将此脚本适配为任何支持的语言。
这个版本将英语翻译为西班牙语,西班牙语翻译为英语。打开你的 Spyder 编辑器并复制清单 16-6。将脚本保存为voice_translator.py,并放入你的章节文件夹中。
from io import BytesIO
from translate import Translator
import speech_recognition as sr
from gtts import gTTS
from pydub import AudioSegment
from pydub.playback import play
# Initiate speech recognition
speech = sr.Recognizer()
# Prompt you to say something in English
print('say something in English')
# Capture spoken English
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
my_input = speech.recognize_google(audio, language="en")
print(f"you said: {my_input}")
except sr.UnknownValueError:
pass
# Specify the input and output languages
1 translator = Translator(from_lang="en",to_lang="es")
# Do the actual translation
translation = translator.translate(my_input)
2 print(translation)
# Convert text to speech in Spanish
tts = gTTS(text=translation,lang='es')
# Create a temporary file
voice = BytesIO()
# Save the voice output as an audio file
3 tts.write_to_fp(voice)
# Play the audio file
voice.seek(0)
play(AudioSegment.from_mp3(voice))
# Prompt you to say something in Spanish
4 print('say something in Spanish')
# Capture spoken Spanish
with sr.Microphone() as source:
speech.adjust_for_ambient_noise(source)
try:
audio = speech.listen(source)
my_input = speech.recognize_google(audio, language="es")
print(f"you said: {my_input}")
except sr.UnknownValueError:
pass
# Specify the input and output languages
Translator = Translator(from_lang="es",to_lang="en")
# Do the actual translation
translation = translator.translate(my_input)
print(translation)
# Convert text to speech in Spanish
tts = gTTS(text=translation,lang='en')
# Create a temporary file
voice = BytesIO()
# Save the voice output as an audio file
tts.write_to_fp(voice)
# Play the audio file
voice.seek(0)
play(AudioSegment.from_mp3(voice))
清单 16-6:英语和西班牙语之间的语音翻译器
我们首先导入所有模块。然后,通过使用Recognizer()函数启动语音识别。接下来,脚本打印say something in English,提示你说出想要翻译的英语短语。
脚本捕捉到你的语音输入,将其保存在变量my_input中并打印。在第 1 步,我们指定输入语言为英语,输出语言为西班牙语。然后,我们将存储在my_input中的文本翻译成西班牙语并打印出来 2。打印完翻译后,我们将西班牙语文本转换为语音。最后,我们将翻译保存为音频文件并播放 3。
从第 4 步开始,我们反转了输入和输出语言。你可以说出一句西班牙语短语进行翻译,计算机会给出英语翻译。
这是与脚本交互的输出,包含我用粗体标出的语音输入:
say something in English
you said: **today is a great day**
Hoy es un gran día.
say something in Spanish
you said: **uno dos tres**
1 2 3
我用英语说了短语“Today is a great day”。脚本打印并朗读了西班牙语翻译Hoy es un gran día。然后,我用西班牙语说“uno, dos, tres”。脚本正确地打印并朗读了英语翻译1 2 3。
总结
在本章中,你调整了你的语音脚本,以使用任何主要的世界语言。在此过程中,你学习了如何将文本转化为西班牙语、中文、日语、法语等主要世界语言的语音。你还学习了如何在这些主要世界语言中进行语音识别。通过这些技能,你能够用非英语语言与计算机互动。
你学习了如何安装translate模块,它可以将文本从一种语言翻译成另一种语言。我们将该模块与语音识别和文本转语音功能结合,创建了一个语音翻译器。这是一个非常实用的现实功能,可以帮助使你部署的应用程序具备全球适应性。
终极虚拟个人助理

在本章中,你将加载一个虚拟个人助理(VPA),并结合本书中的有趣项目,如语音控制游戏、语音翻译器、语音音乐激活等。你将首先向脚本添加聊天功能,以便你可以与 VPA 进行日常对话。你将创建一个问答字典,每当你的语音命令与字典中的某个问题匹配时,VPA 就会从字典中说出答案。这使得 VPA 能够以非常特定的方式回答某些问题,而不是从维基百科或 WolframAlpha 获取答案。
之后,你将添加以下功能:
-
第五章中的语音激活音乐播放器
-
第六章中的语音激活 NPR 新闻功能
-
第六章中的语音激活收音机功能
-
第十三章中的语音激活四子棋游戏(以及练习中的井字游戏)
-
第十五章中的股票价格功能,让你可以查询美国股票的最新价格及其指数值
-
第十六章中的翻译功能,可以将英语短语翻译成世界任何主要语言。
VPA 的核心理念是其便利性,因此我们将在这些项目中进行调整,使所有新增的功能都可以完全免手操作。一旦功能完成,VPA 将返回主菜单并等待你的语音命令。
和往常一样,本章中的所有脚本可以在本书的资源页面找到,www.nostarch.com/make-python-talk/。首先,为本章创建文件夹/mpt/ch17/。
最终 VPA 概览
让我们来看看最终 VPA 的完整脚本。我将逐一解释其中的每个功能。
首先,你需要下载几个本地模块文件。从本书的资源页面(www.nostarch.com/make-python-talk/),在/mpt/mptpkg/目录下找到以下文件:mymusic.py、mynews.py、myradio.py、myttt.py、myconn.py、mystock.py和mytranslate.py。将它们放入与自己创建的本地包文件相同的目录中(可参见第五章的说明)。确保将它们放在包文件夹/mpt/mptpkg/中,而不是章节文件夹/mpt/ch17/中。稍后在本章中,我会解释这些文件的作用。
接下来,打开/mpt/mptpkg/中的init.py文件。你在第五章已经开始了这个文件,并在第七章和第八章进行了修改,因此它目前应该类似于下面这样:
from .mysr import voice_to_text
from .mysay import print_say
`--snip--`
from .myknowall import know_all
将列表 17-1 中的七行代码添加到init.py的末尾。
from .mymusic import music_play, music_stop
from .mynews import news_brief, news_stop
from .myradio import live_radio, radio_stop
from .myttt import ttt
from .myconn import conn
from .mystock import stock_market, stock_price
from .mytranslate import voice_translate
列表 17-1:从本地模块导入函数到本地包
这段代码将从七个模块中导入 11 个函数(music_play()、music_stop()等)到本地包,这样你以后就可以在包级别导入它们。
打开第八章中的脚本vpa.py,并按照列表 17-2 中标注的部分进行添加。保存新的脚本为vpa_final.py。你也可以从本书的资源页面下载脚本。
**import random**
**import json**
# Ensure the following functions are imported in /mpt/mptpkg/__init__.py
from mptpkg import voice_to_text, print_say, wakeup, timer,\
alarm, joke, email, know_all, music_play, music_stop,\
news_brief, news_stop, live_radio, radio_stop, ttt,\
conn, stock_price, stock_market, voice_translate
**# Open chats.json and put it in a dictionary**
**with open('chats.json','r') as content:**
**chats = json.load(content)**
# Put the script in standby
while True:
`--snip--`
# The script goes back to standby if you choose
if "back" in inp and "stand" in inp:
print_say('OK, back to standby, let me know if you need help!')
break
# Activate chatting
elif inp in list(chats.keys()):
print_say(random.choice(chats[inp]))
continue
# Activate music
elif "music by" in inp:
music_play(inp)
# Say stop to stop the music anytime
while True:
background = voice_to_text().lower()
if "stop" in background:
music_stop()
break
else:
continue
# Activate news
elif "npr news" in inp:
news_brief()
# Say stop to stop the news anytime
while True:
background = voice_to_text().lower()
if "stop" in background:
news_stop()
break
else:
continue
# Activate the radio
# Put chromedriver.exe in the same folder as this script
elif "live radio" in inp:
live_radio()
# Say stop to stop the radio anytime
while True:
background = voice_to_text().lower()
if "stop" in background:
radio_stop()
break
else:
continue
# Activate the tic-tac-toe game
elif "tic" in inp and "tac" in inp and "toe" in inp:
ttt()
continue
# Activate the Connect Four game
elif "connect" in inp and ('4' in inp or 'four' in inp):
conn()
continue
# Activate the stock price functionality
elif "stock price of" in inp:
stock_price(inp)
continue
# Get market indexes
elif "stock market" in inp:
stock_market()
continue
# Activate the voice translator
elif "how to say" in inp and " in " in inp:
voice_translate(inp)
continue
# Activate the timer
elif "timer for" in inp and ("hour" in inp or "minute" in inp):
timer(inp)
continue
`--snip--`
列表 17-2:你的最终 VPA
我们首先从本地包mptpkg中导入函数voice_to_text()、print_say()、wakeup()等。我们已经在init.py文件中从本地模块导入了这些函数到本地包mptpkg,因此在这里我们可以直接在包级别导入这些函数。而且,由于自定义包mptpkg已经安装在你的计算机上(处于可编辑模式),系统知道在哪里找到这些文件,因此不需要在脚本中指定文件位置。
然后,我们使用一系列elif语句将功能添加到脚本中。我们从聊天功能开始。我们准备了八对问题和答案,并将它们存入字典chats中。如果你的语音输入匹配其中一个问题,聊天功能将被激活,VPA 将根据chats中的内容说出相应的答案。
音乐功能通过短语音乐由来激活。脚本将提取你在“音乐由……”之后说出的艺术家名称,并播放该艺术家的随机歌曲。
新闻功能通过短语NPR 新闻来激活。脚本将提取并播放来自NPR News Now的最新新闻简报音频文件。你可以说“停止”来停止播放新闻,脚本将返回主菜单并询问:“我能帮您做什么?”
收音机功能通过短语直播广播来激活。脚本将播放来自在线广播电台的流媒体音频。你可以随时说“停止”返回主菜单。
井字游戏功能通过同时说出tic、tac和toe来激活。游戏棋盘将出现在屏幕上,在游戏开始之前,你可以选择先手或后手,以及选择对战一个人、一个简单的计算机或一个智能计算机。
四连棋功能通过同时说出connect和four(或文本中的4)来激活。游戏棋盘将出现在屏幕上,你可以选择先手或后手,以及选择对战一个人、一个简单的计算机或一个智能计算机。
股票价格功能通过短语股票价格为来激活。脚本会提取你在“股票价格为……”之后说出的公司名称,并告诉你最新的价格。
股票市场功能通过短语股票市场来激活。脚本将告诉你美国股市主要指数的最新值。
语音翻译功能通过短语如何说与单词在一起激活。脚本将提取你想要翻译的英语短语和目标外语,并大声告诉你翻译结果。
让我们逐一详细了解各个功能。
聊天功能
这个聊天功能是新的。它允许 VPA 提供一个你在代码中指定的预定义回答,而不是来自 Wikipedia 或 WolframAlpha 的回答。我们正在构建一个简单的聊天机器人,只有八个问题,但有兴趣的读者可以使用这里的原理来创建一个更复杂的聊天功能,包含更多的问题和回答。也许将这个功能与人工智能结合,扩展它,也会很有趣。
我们将创建一个问题和答案的字典。输入列表 17-3 中的文本,并将其保存为文件chats.json,位置在/mpt/ch17/。这些就是我们的问答对。
{
"how are you":["i am good","i am fine"],
"who are you":["i am a Python script","i am a computer script"],
"what are your hobbies":["a script doesn't have hobbies"],
"what's your favorite color":["blue","white"],
"hi":["hi","hello"],
"hello":["hello","hi"],
"what can you do":["lots of things, try me"],
"how old are you":["a script doesn't have age",
"good question, I don't really know the answer to that"]}
列表 17-3:聊天功能中的八对问答
文件采用 JSON 格式,这意味着它可以在不同的脚本语言之间共享。
为了使聊天功能更有趣,我们为一些问题准备了多个答案。Python 会读取 JSON 文件并将数据加载到字典对象中。值都是 Python 列表,脚本会从列表中随机选择一个答案。例如,如果问题是 who are you,答案可能是 i am a Python script 或 i am a computer script。
让我们重点关注 vpa_final.py 中与聊天功能相关的部分:
import import random
import json
`--snip--`
with open('chats.json', 'r') as content:
chats = json.load(content)
`--snip--`
# Activate chatting
1 elif inp in list(chats.keys()):
print_say(random.choice(chats[inp]))
continue
`--snip--`
我们导入了两个模块。random 模块用于随机选择一个答案。json 模块用于读取 JSON 数据。这两个模块都在 Python 标准库中,因此无需安装。
接着我们打开 chats.json 文件,将其内容读取为一个大字符串变量。我们使用 json 模块中的 load() 函数将其加载到字典 chats 中。当你运行 VPA 脚本时,你的声音会被捕捉并转换为文本,存储在字符串变量 inp 中。如果你的问题与 chats 中的八个问题之一匹配,聊天功能就会被激活 1。请注意,list(chats.keys()) 会生成 chats 中八个键的列表,如果你打印这个列表,结果如下:
["how are you", "who are you", "what are your hobbies", "what's your favorite
color", "hi", "hello", "what can you do", "how old are you"]
脚本使用 inp 作为键来定位相应的值,这个值是一个包含一个或两个答案的列表。脚本从列表中随机选择一个答案并朗读出来。
这是一个交互示例,我的语音输入用粗体表示:
`--snip--`
how may I help you?
you just said **hello**
hello
how may I help you?
you just said **who are you**
i am a computer script
how may I help you?
you just said **what can you do**
lots of things, try me
how may I help you?
you just said **how old are you**
a script doesn't have age
`--snip--`
计算机问我:“How may I help you?” 我对着麦克风说:“Hello”。由于 hello 是八个问题之一,聊天功能被激活,计算机从两个答案中选择了一个(在这种情况下是 hello)。
然后,我问了三个问题:Who are you? What can you do? How old are you? 它们都激活了聊天功能。
音乐功能
我们将修改第五章中的脚本 play_selena_gomez.py,并为我们的最终 VPA 添加音乐功能。你将创建一个音乐模块,并将其导入到主脚本中。
创建音乐模块
打开你刚刚从书本资源下载并保存在本地包文件夹 /mpt/mptpkg 中的文件 mymusic.py。代码见列表 17-4。
import os
import random
1 from pygame import mixer
from mptpkg import print_say
# Define a function to play music
2 def music_play(v_inp):
# Extract artist name
pos = v_inp.find("music by ")
v_inp = v_inp[pos+len('music by '):]
# Separate first and last names
names = v_inp.split()
# Extract the first name
firstname = names[0]
# Extract the last name
if len(names)>1:
Lastname = names[1]
# If no last name, use first name as placeholder
else:
lastname = firstname
# Create a list to contain songs
mysongs = []
# If either first name or last name in the filename, put in list
with os.scandir("../ch05/chat") as files:
for file in files:
if (firstname in file.name.lower() or lastname\
in file.name.lower()) and "mp3" in file.name:
mysongs.append(file.name)
# Let you know if no song by the artist
if len(mysongs) == 0:
print_say(f"I cannot find any song by {names}.")
else:
# Randomly select one from the list and play
mysong = random.choice(mysongs)
print_say(f"play the song {mysong} for you.")
mixer.init()
mixer.music.load(f'../ch05/chat/{mysong}')
mixer.music.play()
# Define a function to stop music
3 def music_stop():
try:
mixer.music.stop()
except:
print('no music to stop')
列表 17-4:添加音乐功能的脚本
在第五章中,你在章节文件夹 /mpt/ch05/ 中创建了子文件夹 /chat/ 并保存了一些 MP3 文件。每个文件名应包含艺术家的名字,例如 SelenaGomezWolves.mp3 或 katy_perry_roar.mp3,这样 Python 脚本才能定位到它。典型的歌曲大约四分钟长,如果你听到的是一首不喜欢的歌,这四分钟就显得特别漫长,因此你还学会了如何在歌曲播放时停止它。playsound 和 pydub 模块在歌曲播放时不会让脚本执行下一行代码,但使用 pygame 时,脚本可以在歌曲播放的同时执行下一行代码,从而允许你停止歌曲。
在 1 处,我们从pygame导入了mixer模块,它可以播放音频文件。在 2 处,我们开始定义music_play()函数,该函数将语音命令v_inp作为参数。我们在语音命令中定位music by短语,并使用该短语提取艺术家名称。
我们使用split()函数将名字和姓氏分开,并将其与变量firstname和lastname关联。脚本然后进入适当的文件夹,并选择一首包含艺术家名字或姓氏的歌曲来播放。这里请注意,我们使用../ch05/chat访问平行文件夹/mpt/ch05中的子文件夹/chat。
我们还定义了一个music_stop()函数,用于停止播放音乐 3。我们在此使用try和except,以防脚本误解你的语音输入,并告诉你找不到该艺术家的任何歌曲。如果发生这种情况,你仍然可以说“停止”返回主菜单,而不会导致脚本崩溃。
激活音乐功能
接下来,你将把音乐模块添加到最终的 VPA 中。以下是与音乐功能相关的vpa_final.py部分:
`--snip--`
from mptpkg import music_play, music_stop
`--snip--`
# Activate music
1 elif "music by" in inp:
music_play(inp)
# Say stop to stop the music any time
2 while True:
background = voice_to_text().lower()
if "stop" in background:
music_stop()
break
else:
continue
`--snip--`
我们导入了你刚刚创建的music_play()和music_stop(),然后检查激活词music by 1。一旦激活,就会调用music_play()函数,并将你的语音输入作为参数。
当音乐播放时,脚本继续执行下一行代码,该代码开始一个无限循环,在后台监听你的语音输入 2。任何检测到的语音输入都会转换为变量background。如果检测到stop一词,则调用music_stop()函数。如果没有检测到stop一词,脚本将进入下一次迭代,继续监听背景语音输入。
这是与音乐功能的交互示例,我的语音输入为加粗:
`--snip--`
how may I help you?
you just said **play music by katy perry**
play the song KatyPerry- Hey Hey Hey.mp3 for you
听完大约一分钟的旋律后,我说:“停止播放。”音乐停止了,脚本返回主菜单并问:“我能为您做些什么?”
新闻简报模块
我们将修改第六章中的脚本npr_news.py并为我们的最终 VPA 添加新闻功能。你将创建一个新闻模块并将其导入主脚本。
创建新闻模块
Listing 17-5 中的脚本mynews.py创建了新闻模块。该文件可以从书籍资源中获取,并需要保存在本地包目录中。
from random import choice
import requests
import bs4
1 from pygame import mixer
# Define news_brief() function
2 def news_brief():
# Locate the website for the NPR news brief
url = 'https://www.npr.org/podcasts/500005/npr-news-now'
# Convert the source code to a soup string
response = requests.get(url)
response.raise_for_status()
soup = bs4.BeautifulSoup(response.text, 'html.parser')
# Locate the tag that contains the mp3 files
casts = soup.findAll('a', {'class': 'audio-module-listen'})
# Obtain the weblink for the mp3 file related to the latest news brief
cast = casts[0]['href']
pos = cast.find("?")
# Download the mp3 file
3 mymp3 = cast[0:pos]
x = choice(range(1000000))
mymp3_file = requests.get(mymp3)
with open(f'f{x}.mp3','wb') as f:
f.write(mymp3_file.content)
# Play the mp3 file
mixer.init()
mixer.music.load(f'f{x}.mp3')
4 mixer.music.play()
# Define the news_stop() function
5 def news_stop():
try:
mixer.music.stop()
except:
print('no news to stop')
Listing 17-5:创建新闻功能的脚本
在 1 处,我们从pygame导入了mixer。我们将使用pygame模块,以便随时停止新闻简报。在 2 处,我们定义了news_brief()。当该函数被调用时,脚本会访问 NPR 新闻网站,提取与最新新闻简报相关的 MP3 文件,并将其保存在你的计算机上 3。脚本使用music.play()来播放音频文件 4。
我们还定义了一个news_stop()函数,用于停止播放新闻文件 5。
激活新闻功能
将你刚刚创建的功能添加到最终的 VPA 中。以下是与新闻功能相关的* vpa_final.py *部分:
`--snip--`
from mptpkg import news_brief, news_stop
`--snip--`
# Activate news
elif "npr news" in inp:
news_brief()
# Say stop to stop the news any time
while True:
background = voice_to_text().lower()
if "stop" in background:
news_stop()
break
else:
continue
`--snip--`
我们从mynews导入news_brief()和news_stop()。我们检查语音命令中是否包含激活词NPR News。建议你说“播放 NPR 新闻”或“告诉我最新的 NPR 新闻”,而不仅仅是“NPR 新闻”,因为前一个或两个词可能会因时间问题被切掉。在“NPR 新闻”之前加上一些词语作为缓冲是个好主意。
一旦激活,news_brief()函数会被调用,它从NRR News Now网站提取新闻简报音频文件,并使用pygame播放。
在新闻播放期间,脚本启动一个无限循环,在后台监听你的语音输入,监听词语stop。如果检测到这个词,news_stop()函数会被调用。否则,脚本会进入下一轮并继续监听后台命令。
与播放音乐功能一样,你需要将扬声器音量调低,这样你就可以通过语音输入停止音频。新闻简报播放完毕后,你需要说“停止”以返回主菜单。
实时广播模块
我们将修改第六章中的play_live_radio.py并为最终的 VPA 添加一个广播模块。像往常一样,你需要创建广播模块并将其导入主脚本。
创建一个广播模块
首先,我们将创建一个广播模块。脚本myradio.py在清单 17-6 中展示。
# Put chromedriver.exe in the same folder as vpa_final.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
1 def live_radio():
global button
chrome_options = Options()
chrome_options.add_argument("--headless")
browser = webdriver.Chrome(executable_path='./chromedriver',\
chrome_options=chrome_options)
browser.get("https://onlineradiobox.com/us/")
button = browser.find_element_by_xpath('//*[@id="b_top_play"]')
button.click()
2 def radio_stop():
global button
try:
button.click()
except:
print('no radio to stop')
清单 17-6:创建直播广播功能的脚本
首先,你需要将文件chromedrive.exe放置在与 VPA 脚本相同的文件夹中(即在/mpt/ch17中)。在第 1 步中,我们定义了live_radio()函数。我们将button设为全局变量,以便稍后在另一个函数中使用它。我们使用headless选项,它提供与常规 Chrome 浏览器相同的功能,但不会在桌面上显示浏览器窗口。然后,我们将button定义为在线电台网站 Online Radio Box 上的播放按钮。通过语音控制点击该按钮,这样当live_radio()被调用时,广播就开始流式播放。
在第 2 步中,我们定义了一个radio_stop()函数来停止广播播放。请注意,这里我们也需要将button设为全局变量,以便在radio_stop()中修改它。
激活广播功能
接下来,将你刚刚创建的广播功能添加到最终的 VPA 中。以下是与此相关的* vpa_final.py *部分:
`--snip--`
from mptpkg import live_radio, radio_stop
`--snip--`
# Activate the radio
# Put chromedriver.exe in the same folder as this script
elif "live radio" in inp:
live_radio()
# Say stop to stop the radio anytime
while True:
background = voice_to_text().lower()
if "stop" in background:
radio_stop()
break
else:
continue
`--snip--`
我们首先从本地mptpkg包中导入你刚刚创建的live_radio()和radio_stop()函数。我们监听激活词live radio。再次提醒,在“live radio”之前加入一个或两个词语作为缓冲是个好主意。
一旦激活,live_radio()会被调用,它会进入在线电台盒并点击播放按钮以流式播放音频。
当收音机播放时,脚本启动一个无限循环来监听背景语音输入,一旦检测到,输入会被存储在background中。如果检测到单词stop,则调用radio_stop()函数再次按下播放按钮,以便停止音频流播放。否则,脚本会进入下一次循环,继续监听背景语音命令。
井字游戏模块
我们将添加一个井字游戏模块,以便你可以通过语音激活游戏并与电脑进行 100%免提对战。在这里,我们使用一个脚本提供六个版本的井字游戏:你可以选择与另一个人对战、与一个做随机动作的简单电脑对战,或者与一个思考三步的聪明电脑对战(参见第十三章)。你还可以选择先手或后手。
你将创建一个井字游戏模块并将其导入到主脚本中。
创建一个井字游戏模块
首先,我们将创建一个本地的井字游戏模块。脚本myttt.py基于第十章的脚本ttt_hs.py和第十三章章节末尾练习题#5 的答案ttt_think.py,你可以在本书的资源网站上找到。我在列表 17-7 中突出显示了myttt.py的关键部分。
`--snip--`
def ttt():
t.setup(600,600,100,200)
`--snip--`
# Define the smart_computer() function
1 def smart_computer():
if turn == "blue":
nonturn = "white"
else:
nonturn = "blue"
# Choose center at the first move
if "5" in validinputs:
return "5"
`--snip--`
for move in valids:
tooccupy = deepcopy(occupied)
tooccupy[turn].append(move)
if win_game(tooccupy,turn) == True:
winner.append(move)
`--snip--`
# Obtain move from a human player
2 def person():
print_say(f"Player {turn}, what's your move?")
return voice_to_text().lower()
# Obtain a move from a simple computer
3 def simple_computer():
return choice(validinputs)
# Ask you for your choice of opponent
4 while True:
print_say('''Do you want your opponent to be a person,
a simple computer, or a smart computer?''')
which_player = voice_to_text().lower()
print_say(f"You said {which_player}.")
if 'person' in which_player:
player = person
break
elif 'simple' in which_player:
player = simple_computer
break
elif 'smart' in which_player:
player = smart_computer
break
# Ask if you want to play first or second
5 while True:
print_say("Do you want to play first or second?")
preference = voice_to_text().lower()
print_say(f"You said {preference}.")
if 'first' in preference:
preference = 1
break
elif 'second' in preference:
preference = 2
break
# Add a dictionary of words to replace
to_replace = {'number ':'', 'cell ':'', 'column ':'',
'one':'1', 'two':'2', 'three':'3',
'four':'4', 'for':'4', 'five':'5',
'six':'6', 'seven':'7', 'eight':'8','nine':'9'}
# Start game loop
while True:
# See whose turn to play
6 if (preference+rounds)%2 == 0:
print_say(f"Player {turn}, what's your move?")
inp = voice_to_text().lower()
else:
7 inp = player()
if inp == None:
inp = choice(validinputs)
8 print_say(f"Player {turn} chooses {inp}.")
`--snip--`
# If the move is a not valid one, remind
9 if inp not in validinputs:
print_say("Sorry, that's an invalid move!")
# If the move is valid, go ahead
else:
# Go to the cell and place a dot of the player's color
`--snip--`
a try:
bye()
except Terminator:
print('exit turtle')
列表 17-7:创建井字游戏功能的脚本
与之前的井字游戏版本不同,在这里我们不使用messagebox模块来提醒我们胜利、平局和无效操作,因为我们无法使用语音命令将消息框从屏幕上移除。你需要手动点击框框才能让它消失。相反,我们将直接打印并宣布胜利、平局和无效操作。
我们定义了ttt()函数,我们将从 VPA 脚本中调用它来绘制游戏板,并询问你是否想与一个人、一个简单的电脑,还是一个聪明的电脑对战。之后,脚本会询问你是否想先手还是后手。游戏结束后,棋盘会从屏幕上消失,脚本会自动返回到 VPA 的主菜单。
在ttt()函数中,我们使用smart_computer()函数 1,它基于ttt_think.py中的best_move()函数,但允许你选择先手或后手。我们将blue和white分别改为turn和nonturn,这样如果电脑后手,它就可以成为白色棋手。我们还允许聪明的电脑如果后手时占据空的 5 号格,因为这样做可以增加它赢得游戏的机会。
然后我们定义了person()函数 2,它允许人类玩家通过语音命令进行操作。同样,simple_computer()函数允许电脑进行随机操作 3。
在第 4 步,我们开始一个无限循环。在每次迭代中,脚本会询问你是否想选择一个人、一个简单的电脑,还是一个智能的电脑作为对手。如果你的回答包含人,那么变量 player 将被赋值为 person。如果你的回答包含简单或智能,则 player 会被赋值为 simple_computer 或 smart_computer。稍后,当我们调用 player() 函数时,会根据 player 中存储的函数名调用三个函数中的一个,即 person()、simple_computer() 或 smart_computer()。
在第 5 步,我们开始一个无限循环来确定你是想先手还是后手。如果你的回答包含先手,变量 preference 将被赋值为 1。如果你的回答包含后手,则 preference 被赋值为 2。
然后我们开始游戏循环。在每次迭代中,我们首先根据 preference 和 rounds 6 的值确定你还是你的对手先行动。例如,如果你选择先手,preference 的值为 1,而当游戏开始时,rounds 的值为 1。因此,条件 (preference+rounds)%2==0 被满足,你将获得游戏开始时的第一回合。
当轮到你的对手时,第 7 步,player() 函数被调用。这意味着将根据 player 变量中存储的值,调用 person()、simple_computer() 或 smart_computer() 中的一个函数。脚本会宣布走法 8。如果该走法无效,脚本会要求你或你的对手重新选择 9。否则,一枚棋子会被放置到游戏板上。
最后,当游戏结束时,我们没有在脚本中包括 done() 函数。正如你从第十二章的脚本 guess_letter.py 中回忆到的,若没有 done(),脚本将在 while 循环结束后转到 bye() 函数。这样,游戏板会从屏幕上消失,你可以返回 VPA 脚本的主菜单。
激活井字棋
现在让我们将井字棋功能添加到最终的 VPA 中。以下是 vpa_final.py 脚本的相关部分:
`--snip--`
from mptpkg import ttt
`--snip--`
# Activate the tic-tac-toe game
elif "tic" in inp and "tac" in inp and "toe" in inp:
ttt()
continue
`--snip--`
我们从本地 mptpkg 包中导入你刚创建的 ttt() 函数。要激活井字棋游戏,你需要在语音命令中包括 tic、tac 和 toe。一旦游戏结束,游戏板将消失,你将返回主菜单。
这里是一次交互的示例,我的语音输入以粗体显示:
`--snip--`
How may I help you?
You just said **play tic-tac-toe**.
Do you want your opponent to be a person, a simple computer, or a smart computer?
You said **simple computer**.
Do you want to play first or second?
You said **first**.
Player blue, what's your move?
Player blue chooses **5**.
Player white chooses 9.
Player blue, what's your move?
Player blue chooses **number 7**.
Player white chooses 6.
Player blue, what's your move?
Player blue chooses **number three**.
Congrats player blue, you won!
How may I help you?
`--snip--`
我通过说“玩井字棋”来启动游戏。然后,我选择先与简单的电脑对战作为对手。我通过占据格子 5、7 和 3 赢得了比赛。
Connect Four 模块
此时,添加 Connect Four 模块应该是直接的。我们可以修改井字棋模块,将游戏改为 Connect Four。然后你需要将本地模块导入主脚本中。
创建 Connect Four 模块
首先,我们将创建一个四连棋模块。脚本myconn.py基于第十三章的conn_think_hs.py和你刚刚创建的myttt.py。同样,我们不会使用messagebox来提醒我们关于胜利、平局和无效移动的消息。我们将定义一个conn()函数,这样当该函数被调用时,游戏会出现在屏幕上,你可以开始游戏。
和井字游戏模块一样,你可以选择谁先走以及谁是你的对手。我们将red和yellow分别改为turn和nonturn,以便如果电脑选择后手,它可以成为黄色玩家。
为了节省篇幅,我在这里不会详细解释myconn.py,但它可以在本书的资源中找到,位于/mpt/mptpkg文件夹中。现在打开它,看看,然后回到 VPA 的主脚本。
激活四连棋
将你刚刚创建的四连棋模块添加到最终的 VPA 中,如下所示的vpa_final.py:
`--snip--`
from mptpkg import conn
*--snip**--*
# Activate Connect Four
elif "connect" in inp and ('4' in inp or 'four' in inp):
conn()
continue
`--snip--`
我们从本地的mptpkg包中导入你刚刚创建的conn()函数。我们会监听你语音命令中的激活短语Connect Four。注意,脚本可能会将你的语音转化为connect four或connect 4。因此,我们需要使用'4' in inp or 'four' in inp来覆盖这两种情况。
这是一个来自游戏的示例输出:
`--snip--`
How may I help you?
You just said **play connect four**.
Do you want your opponent to be a person, a simple computer, or a smart computer?
You said **smart computer**.
Do you want to play first or second?
You said **second**.
Player red chooses 4.
Player yellow, what's your move?
Player yellow chooses **number three**.
Player red chooses 1.
Player yellow, what's your move?
Player yellow chooses **number three**.
Player red chooses 5.
Player yellow, what's your move?
Player yellow chooses **number three**.
Player red chooses 3.
Player yellow, what's your move?
Player yellow chooses **number two**.
Player red chooses 7.
Player yellow, what's your move?
Player yellow chooses **number two**.
Player red chooses 6.
Congrats player red, you won!
How may I help you?
`--snip--`
我选择了与智能电脑对战,且我选择了后手。在 4、5、7 和 6 列中横向连接了四个棋子,智能电脑赢得了比赛。
股票价格模块
现在,让我们将股票价格功能添加到最终的 VPA 中,构建该模块并导入它。
创建一个股票市场追踪模块
首先,我们将创建股票监控功能。脚本mystock.py包含在清单 17-8 中所示的代码。
import requests
from yahoo_fin import stock_info as si
from mptpkg import print_say
# Define stock_price() function
1 def stock_price(v_inp):
# Extract company name
pos = v_inp.find("stock price of")
myfirm = v_inp[pos+len("stock price of "):]
# Extract the source code from the website
# Prevent crashing in case there is no result
try:
# Extract the source code from the website
2 url = 'https://query1.finance.yahoo.com/v1/finance/search?q='+myfirm
response = requests.get(url)
# Read the JSON data
response_json = response.json()
# Obtain the value corresponding to "quotes"
quotes = response_json['quotes']
# Get the ticker symbol
ticker = quotes[0]['symbol']
# Obtain real-time stock price from Yahoo
3 price = round(float(si.get_live_price(ticker)),2)
# Speak the stock price
print_say(f"the stock price for {myfirm} is {price} dollars")
# If price is not found, the script will tell you
except:
print_say("sorry, I cannot find what you are looking for!")
# Define stock_market() function
4 def stock_market():
# Obtain real-time index values from Yahoo
dow = round(float(si.get_live_price('^DJI')),2)
sp500 = round(float(si.get_live_price('^GSPC')),2)
# Announces the index values
print_say(f"The Dow Jones Industry Average is {dow}.")
print_say(f"The S&P 500 is {sp500}.")
清单 17-8:创建股票市场追踪功能的脚本
在第 1 步,我们定义了stock_price()函数,将语音命令v_inp作为参数。然后,我们在你的语音命令中定位公司名称,并用它来提取该公司股票的股票代码。脚本访问 Yahoo! Finance 并根据股票代码获取股票价格。最后,脚本会打印并宣布股票价格。
我们还定义了stock_market() 4。当此函数被调用时,它会获取道琼斯工业平均指数和标准普尔 500 指数的最新值。然后,脚本会打印并宣布这两个值。
激活股票市场追踪功能
现在,将你刚刚创建的股票监控模块添加到最终的 VPA 中。以下是vpa_final.py中的相关部分:
`--snip--`
from mptpkg import stock_market, stock_price
`--snip--`
# Activate the stock price functionality
elif "stock price of" in inp:
stock_price(inp)
continue
# Get market indexes
elif "stock market" in inp:
stock_market()
continue
`--snip--`
我们首先导入stock_price()和stock_market()函数,并监听语音命令中的激活短语stock price of,例如,“告诉我通用汽车的股票价格。”stock_price()函数将你的语音命令作为参数,并告诉你该公司的最新股票价格。
然后,我们监听激活短语股市,以便调用stock_market()函数。脚本会获取市场指数的最新值并宣布给你。
以下是与股市模块的一个交互示例,其中我的语音输入以粗体显示:
`--snip--`
how may I help you?
you just said **tell me the stock price of general motors**
the stock price for general motors is 24.39 dollars
how may I help you?
you just said **tell me about the stock market**
the Dow Jones Industry Average is 26075.3
the S&P 500 is 3185.04
how may I help you?
`--snip--`
语音翻译模块
最后,我们将添加翻译功能,以便你的 VPA 可以将英文短语翻译成你选择的外语。
创建翻译模块
首先,我们将创建一个翻译模块。脚本mytranslate.py在 Listing 17-9 中展示。
from mptpkg import print_say
1 lang_abbre = {"english":"en",
"chinese":"zh",
"spanish":"es",
"french":"fr",
"japanese":"ja",
"portuguese":"pt",
"russian":"ru",
"korean":"ko",
"german":"de",
"italian":"it"}
# Import the platform module to identify your OS
import platform
# If you are using Windows, use gtts
if platform.system() == "Windows":
import random
from translate import Translator
from gtts import gTTS
from pydub import AudioSegment
from pydub.playback import play
2 def voice_translate(inp):
# Extract the phrase and the language name
ps1 = inp.find('how to say')
ps2 = inp.rfind(' in ')
try:
eng_phrase = inp[ps1+10:ps2]
tolang = inp[ps2+4:]
translator = Translator(from_lang="english",to_lang=tolang)
translation = translator.translate(eng_phrase)
tts = gTTS(text=translation, lang=lang_abbre[tolang])
print_say(f"The {tolang} for {eng_phrase} is")
print(translation)
x = random.choice(range(1000000))
tts.save(f'file{x}.mp3')
play(AudioSegment.from_mp3(f"file{x}.mp3"))
except:
print_say("Sorry, cannot find what you are looking for!")
# If you are not using Windows, use gtts-cli
if platform.system() == "Darwin" or platform.system() == "Linux":
import os
from translate import Translator
from gtts import gTTS
def voice_translate(inp):
# Extract the phrase and the language name
ps1 = inp.find('how to say')
ps2 = inp.rfind(' in ')
try:
eng_phrase = inp[ps1+10:ps2]
tolang = inp[ps2+4:]
translator = Translator(from_lang="english",to_lang=tolang)
translation = translator.translate(eng_phrase)
print_say(f"The {tolang} for {eng_phrase} is")
print(translation)
tr = translation.replace('"','')
ab = lang_abbre[tolang]
3 os.system(f'gtts-cli --nocheck "{tr}" --lang {ab} | mpg123 -q -')
except:
print_say("sorry, cannot find what you are looking for!")
Listing 17-9:创建语音翻译功能的脚本
我们首先导入所需的模块,特别是导入platform以识别你的操作系统。在第 1 步,我们创建一个字典lang_abbre,它将多种语言映射到谷歌翻译中的语言代码。Listing 17-9 包含了 10 种语言,你可以根据需要在字典中添加更多语言。
如果你使用的是 Windows 系统,在第 2 步,我们开始定义voice_translate()函数,该函数将你的语音命令作为参数。你的语音命令应该包含how to say和in。例如,你可以问:“Python,如何用日语说thank you?”脚本会找到how to say和in在你语音中的位置,然后提取你想翻译的英文短语和目标语言,并将它们分别存储在变量eng_phrase和tolang中。
然后,我们使用translate中的Translator()类将英文短语翻译成你想要的语言。接下来,脚本将翻译结果转化为语音,并保存为 MP3 文件,然后使用pydub模块播放它。
如果你使用的是 Mac 或 Linux 系统,过程类似,只是你不需要创建和播放音频文件。相反,我们使用命令行方法gtts-cli直接播放音频文件,而无需保存和检索音频文件,类似于我们在第四章第三部分所做的。由于我们将外语转化为语音,因此需要添加--lang选项,并跟上该语言的缩写。
激活语音翻译功能
接下来,你将把刚刚创建的语音翻译模块添加到最终的 VPA 中,如下所示:
`--snip--`
from mptpkg import voice_translate
`--snip--`
# Activate the voice translator
elif "how to say" in inp and " in " in inp:
voice_translate(inp)
continue
`--snip--`
我们导入刚刚创建的voice_translate()函数并监听激活短语。一旦翻译功能被激活,voice_translate()就会被调用,使用你的语音输入作为参数。该函数会用人声告诉你翻译结果。
以下是与该功能的一个交互示例,其中我的语音输入以粗体显示:
`--snip--`
how may I help you?
you just said **how to say good afternoon in japanese**
the japanese for good afternoon is
こんにちは
`--snip--`
概述
在本章中,你将书中早期创建的几个项目添加到你的 VPA 中。在这个过程中,你学习了如何修改现有项目,如何将它们模块化,并在你的 VPA 中使用它们的功能。你学会了如何通过语音控制来激活某个功能,从而实现完全的免手操作,以及如何在功能完成后返回主菜单。你还通过在游戏开始前让脚本向你提问几个问题,高效地将井字游戏或四连棋的六个版本合并到一个模块中。凭借这些技能,你将能够创建自己的功能并将其添加到你的 VPA 中。
第一章:A
安装播放音频文件的模块

在本附录中,我将讨论在 Python 中播放音频文件的各种模块。虽然无法考虑到所有硬件和操作系统组合的差异,但我已经在多种硬件和软件平台上测试了本书中的指示。在此过程中,我遇到了各种问题,我希望帮助你避免这些问题。
播放音频文件有两种类型的模块。第一种类型(我们可以称之为阻塞型)将控制脚本的执行,直到音频文件播放完毕才会继续执行下一行代码。我们将讨论这一类别中的两个模块:playsound 和 pydub。你只需要使其中一个模块在本书中正常工作即可。第二种类型不会控制脚本的执行;它会在音频文件开始播放后立即继续执行下一行代码(我们可以称之为非阻塞型)。我们将讨论这一类别中的两个模块:vlc 和 pygame。同样,你只需要使其中一个模块在本书中正常工作即可。
本书使用了两种类型的模块。阻塞型是最常见的,我们用它来播放大多数脚本中的音频文件。非阻塞型在音频文件较长时很有用,你可以在播放时选择暂停或停止。我们将在第六章的脚本 news_brief_hs.py 以及第十七章最终的 VPA 脚本 vpa_final.py 中使用这一功能。
接下来,我们将讨论如何在不同的操作系统中安装这四个模块。
安装 playsound 模块
playsound 模块使用简单,因为所需的代码行数很少。然而,在 Mac 或 Linux 上的安装可能会有些困难,尽管我已经在所有三个操作系统上成功安装了它。
Windows
在 Windows 上安装 playsound,在激活 chatting 虚拟环境的 Anaconda 提示符中执行以下命令:
**pip install playsound**
跟随指示操作。
Mac
在 Mac 上安装 playsound,在激活 chatting 虚拟环境的终端中执行以下两条命令:
**pip install playsound**
**conda install -c conda-forge pygobject**
跟随指示操作。
Linux
在 Linux 上安装 playsound,在激活 chatting 虚拟环境的终端中执行以下三条命令:
**pip install playsound**
**conda install –c conda-forge pygobject**
**conda install gstreamer**
跟随指示操作。
安装 pydub 模块
pydub 模块在 Mac 或 Linux 上安装比较简单。然而,在 Windows 上的安装可能会有些困难,尽管我已经在所有三个操作系统上成功安装了它。要安装 pydub,在激活 chatting 虚拟环境的 Anaconda 提示符(Windows)或终端(Mac 和 Linux)中执行以下命令:
**conda install –c conda-forge pydub**
**conda install –c conda-forge ffmpeg**
安装 pygame 模块
由于软件不断更新,安装说明可能会有所变化。如果你遇到问题,建议参考 Pygame 官方网站 www.pygame.org/wiki/GettingStarted/ 以获取最新的安装说明。
Windows
在你的 Anaconda 提示符中执行此命令,确保 chatting 虚拟环境已激活:
**pip install pygame**
按照说明进行操作。
Mac
最近版本的 macOS 需要安装 Pygame 2。要安装,请在终端中执行以下命令,并确保 chatting 虚拟环境已激活:
**pip install pygame==2.0.0**
然后按照说明进行操作。
Linux
在 Linux 中,在终端执行以下三个命令,并确保 chatting 虚拟环境已激活:
**sudo apt-get install python3-pip python3-dev**
**sudo pip3 install pygame**
**pip install pygame**
安装 vlc 模块
对于 vlc 模块,无论你使用哪种操作系统,都需要在计算机上安装 VLC 媒体播放器。请访问 VLC 网站 www.videolan.org/index.html 下载并安装该软件。
在 Linux 中,你可以通过在终端执行此命令来安装该应用:
**sudo apt-get install vlc**
在激活了 chatting 虚拟环境的 Anaconda 提示符(Windows)或终端(Mac 或 Linux)中,运行以下命令安装 Python vlc 模块:
**pip install python-vlc**
测试四个模块的示例脚本
在本节中,我们为每个四个模块提供一个示例脚本,以测试模块是否成功运行。再次提醒,本书只需要安装 playsound 和 pydub 中的一个模块,以及 vlc 和 pygame 中的一个模块。
前往书籍资源下载用于测试的 hello.mp3 文件。确保将文件放置在与接下来创建的测试脚本相同的文件夹中。
playsound 模块
在你的 Spyder 编辑器中输入以下代码行。将脚本保存为 test_playsound.py 并运行。或者,你也可以从书籍资源中下载。
**from playsound import playsound**
**playsound("hello.mp3")**
如果成功,你应该能听到一个人类声音说:“Hello, how are you?”
pydub 模块
在你的 Spyder 编辑器中输入以下代码行。将脚本保存为 test_pydub.py 并运行。或者,你也可以从书籍资源中下载。
**from pydub import AudioSegment**
**from pydub.playback import play**
**play(AudioSegment.from_mp3("hello.mp3")**
如果成功,你应该能听到一个人类声音说:“Hello, how are you?”
pygame 模块
在你的 Spyder 编辑器中输入以下代码行。将脚本保存为 test_pygame.py 并运行。或者,你也可以从书籍资源中下载。
**from pygame import mixer**
**mixer.init()**
**mixer.music.load("hello.mp3")**
**mixer.music.play()**
如果成功,你应该能听到一个人类声音说:“Hello, how are you?”
vlc 模块
在你的 Spyder 编辑器中输入以下代码行。将脚本保存为 test_vlc.py 并运行。或者,你也可以从书籍资源中下载。
**from vlc import MediaPlayer**
**player=MediaPlayer("hello.mp3")**
**player.play()**
如果成功,你应该能听到一个人类声音说:“Hello, how are you?”
第二章:B
章节末尾练习的建议答案

本附录提供了本书所有章节末尾练习的建议答案。如果在任何章节问题上遇到困难,您可以在这里查看答案并继续学习。虽然大多数章节都有章节末尾的练习,8、16 和 17 章没有,但它们有很多“动手做”的问题,帮助您进行练习。
第一章
-
添加这一行代码:
print("Here is a third message!") -
以下是分别输出:
2 9 2 2.3333333333333335 1 4 20 -
添加这一行代码:
print(55 * 234)
第二章
-
以下是输出:
<class 'str'> <class 'str'> Kentucky Wildcats WildcatsKentucky Wildcats @ Kentucky WildcatsWildcatsWildcats -
输出如下:
<class 'float'> <class 'float'> 3.46 -2.4 3.0 -
以下是输出:
<class 'int'> 57 0.0 -
输出如下:
<class 'bool'> 8<7 False <class 'str'> <class 'str'> -
这些是输出:
-23 56 -23.0 8.0 -
以下是输出:
1 0.0 False -
输出如下:
False True True True -
以下是答案:
-
global:不行,因为它是 Python 的关键字 -
2print:不行,因为变量不能以数字开头 -
print2:是的 -
_squ:是的 -
list:不行,因为list()是 Python 内建函数 -
这是输出:
this is A1 this is A2 -
输出如下:
this is A1 this is A2 this is C1 this is C2 -
这是输出:
this is A1 this is A2 this is B1 this is B2 this is C1 this is C2 -
答案如下:
0 1 2 3 410 11 12 13 1410 12 14 -
270
-
请参见书籍资源中的新脚本import_local_module1.py。
-
分数的范围和平均值分别是 29 和 82.625。您可以使用以下代码行来获取答案:
midterm = [95, 78, 77, 86, 90, 88, 81, 66] print("the range is", max(midterm)-min(midterm)) print("the average is", sum(midterm)/len(midterm)) -
输出如下:
rsity y University rsity of Kentucky -
如果
email = John.Smith@uky.edu,则email.find("y")返回13。 -
答案如下:
[2, 3, 5, 9] 5 3 -
输出如下:
["a", "hello", 2] [1, "a", "hello", 2, "hi"] -
10 -
(9,) -
一种方式是使用
enumerate()方法,如下所示:lst = [1, "a", "hello", 2] newdict = {} for i, x in enumerate(lst): newdict[i] = x print(newdict)另一种方式如下:
lst = [1, "a", "hello", 2] newdict = {i:lst[i] for i in range(len(lst))} print(newdict)
第三章
-
更改
if inp == "stop listening": print('Goodbye!')到
if inp == "`quit the script`": print('`Have a great day!`') -
将以下内容添加到脚本的末尾:
elif "open text" in inp: inp = inp.replace('open text ','') myfile = f'{inp}.txt)' open_file(myfile) continue -
更改
import speech_recognition as sr speech = sr.Recognizer() def voice_to_text(): voice_input = "" with sr.Microphone() as source: speech.adjust_for_ambient_noise(source) try: audio = speech.listen(source) voice_input = speech.recognize_google(audio) except sr.UnknownValueError: pass except sr.RequestError: pass except sr.WaitTimeoutError: pass return voice_input到
`from mysr import voice_to_text`确保将脚本mysr.py与voice_open_file.py放在同一文件夹中。
第四章
-
请参见此处的pyttsx3_adjust1.py:
import pyttsx3 engine = pyttsx3.init() voices = engine.getProperty('voices') engine.setProperty('voice', voices[0].id) engine.setProperty('rate', 160) engine.setProperty('volume', 0.8) engine.say("This is a test of my speech id, speed, and volume.") engine.runAndWait() -
请参见此处的area_hs1.py:
# Put mysr.py and mysay.py in the same folder as this script from mysr import voice_to_text from mysay import print_say # Ask the base length of the triangle print_say('What is the base length of the triangle?') # Convert the voice input to a variable inp1 inp1 = voice_to_text() print_say(f'You just said {inp1}.') # Ask the height of the triangle print_say('What is the height of the triangle?') # Save the answer as inp2 inp2 = voice_to_text() print_say(f'You just said {inp2}.') # Calculate the area area = float(inp1)*float(inp2)/2 # Print and speak the result print_say(f'The area of the triangle is {area}.')
第五章
-
更改
elif re2 == "too high": print_say("Is it 1?") while True: re3 = voice_to_text() print_say(f"You said {re3}.") if re3 in ("too high", "that is right", "too small"): break if re3 == "too small": print_say("It is 2!") sys.exit elif re3 == "that is right": print_say("Yay, lucky me!") sys.exit到
elif re2 == "too high": print_say("Is it `2`?") while True: re3 = voice_to_text() print_say(f"You said {re3}.") if re3 in ("too high", "that is right", "too small"): break if re3 == "`too high`": print_say("It is `1`!") sys.exit elif re3 == "that is right": print_say("Yay, lucky me!") sys.exit -
更改
print(answer)到
print(answer`[0:300]`) -
更改
from pygame import mixer到
`import platform`并更改
mixer.init() mixer.music.load(f"./chat/{mysong}") mixer.music.play()到
`if platform.system() == "Windows":` `os.system(f"explorer ./chat/{mysong}")` `elif platform.system() == "Darwin":` `os.system(f"open ./chat/{mysong}")` `else:` `os.system(f"xdg-open ./chat/{mysong}")` -
更改
and "mp3" in file.name到
and "`wav`" in file.name
第六章
-
请参见此处的parse_local2.py:
from bs4 import BeautifulSoup textfile = open("UKYexample.html", encoding='utf8') soup = BeautifulSoup(textfile, "html.parser") ptags = soup.findAll("p") atag = ptags[1].find("a") print(atag['class']) print(atag['href']) -
请参见此处的scrape_live_web2.py:
from bs4 import BeautifulSoup import requests url = 'http://libraries.uky.edu' page = requests.get(url) soup = BeautifulSoup(page.text, "html.parser") div = soup.find('div', class_="sf-middle") contact = div.find("div", class_="dashing-li-last") area = contact.find('span', class_="featured_area") print(area.text) atag = contact.find('span', class_="featured_email") print(atag.text) -
请参见此处的voice_podcast.py:
from io import BytesIO import requests import bs4 from pygame import mixer # Import functions from the local package from mptpkg import voice_to_text, print_say def podcast(): # Break a long url into multiple lines url = ('https://goop.com/the-goop-podcast/' 'gwyneth-x-oprah-power-perception-soul-purpose/') # Convert the source code to a soup string response=requests.get(url) soup = bs4.BeautifulSoup(response.text, 'lxml') casts = soup.findAll\ ('audio', {'class':'podcast-episode__audio-player'}) casts = str(casts) start = casts.find("https") end = casts.find(".mp3") cast= casts[start:end+4] # Play the mp3 using the pygame module mymp3 = requests.get(cast) voice = BytesIO() voice.write(mymp3.content) voice.seek(0) mixer.init() mixer.music.load(voice) mixer.music.play() while True: print_say('Python is listening...') inp = voice_to_text().lower() print_say(f'you just said: {inp}') if inp == "stop listening": print_say('Goodbye!') break # If "podcast" in your voice command, play podcast elif "podcast" in inp: podcast() # Python listens in the background while True: background = voice_to_text().lower() # Stops playing if you say "stop playing" if "stop playing" in background: mixer.music.stop() break
第七章
-
以下是脚本:
import arrow from mptpkg import print_say dt = arrow.now().format('MMMM D, YYYY') tm = arrow.now().format('hh:mm:ss A') print_say(f'today is {dt}, and the time now is {tm}.') -
更改
elif "stop" in voice_input:到
elif "`quit the script`" in voice_input:
第九章
-
变更如下:
import turtle as t t.Screen() t.setup(`500`,`400`,100,200) t.bgcolor('`blue`') t.title('`Modified Screen`') t.done() t.bye() -
更改
t.forward(200) t.backward(300)到
`t.backward(100)` `t.forward(250):` -
变更如下:
import turtle as t t.Screen() t.setup(600,500,100,200) t.title('Python Turtle Graphics') t.hideturtle() t.up() `t.goto(100,100)` `t.dot(60,'lightgreen')` `t.goto(-100,-100)` `t.dot(60,'lightgreen')` t.done() try: t.bye() except Terminator: pass -
更改
t.pencolor('blue') t.pensize(5)到
t.pencolor('`red`') t.pensize(`3`) -
代码如下:
import turtle as t t.Screen() t.setup(600,500,100,200) t.bgcolor('green') t.title('Python Turtle Graphics') t.hideturtle() t.tracer(False) t.pensize(6) t.goto(200,0) t.goto(200,100) t.goto(0,100) t.goto(0,0) t.update() t.done() try: t.bye() except Terminator: pass
第十章
-
更改
t.goto(center) t.write(cell,font = ('Arial',20,'normal'))到
t.goto(`(`center`[0]-80, center[1]-80)`) t.write(cell,font = ('Arial',`15`,'normal')) -
更改
print(f'(x, y) is ({x}, {y})')到
print(f'(x, y) is ({x}, {y})') `print('x+y is ', x+y)` -
更改
print('row number is ', row)到
`print(f'you clicked on the point ({x}, {y})')` print('row number is ', row) -
更改
# The blue player moves first turn = "blue"到
# The `white` player moves first turn = `"white"` -
删除脚本中的以下内容:
if '1' in occupied[turn] and '5' in occupied[turn] and '9' in occupied[turn]: win = True if '3' in occupied[turn] and '5' in occupied[turn] and '7' in occupied[turn]: win = True
第十一章
-
更改
done()到
`rownum = 1` `for y in range(-250, 300, 100):` `goto(325,y)` `write(rownum,font=('Arial',20,'normal'))` `rownum += 1` done() -
更改
sleep(0.05)到
sleep(`0.025`) -
删除
vertical4()函数的定义,并删除脚本中的以下内容:if vertical4(x, y, turn) == True: win = True -
将以下内容添加到字典
to_replace:'column ':'', -
请参见
nostarch.com/make-python-talk/中的conn_hs_2player.py。
第十二章
-
更改
coins[i].goto(-100 + 50 * i, 0)到
coins[i].goto(-100 + 50 * i, `-10`) -
更改
coins[-(i+1)].hideturtle()到
coins[`i`].hideturtle() -
['H', 'i', ' ', 'P', 'y', 't', 'h', 'o', 'n']
第十三章
-
更改
# The red player moves first turn = "red"到
# The `yellow` player moves first turn = "`yellow`"然后,删除
# Computer moves first computer_move()同时,删除
# Take column 4 in the first move if len(occupied[3]) == 0: return 4完整的脚本在
nostarch.com/make-python-talk/中的conn_think1_second.py里。 -
请参见书籍资源网站中的ttt_think1.py。
-
请参见书籍资源网站中的ttt_think2.py。
-
值如下:
cnt = {2:7, 1:5, 4:6}请注意,由于
4和5这两个值各自出现一次,因此它们在字典cnt中只会显示一个,因为字典不能有相同键的两个元素:maxcnt = 4,并且cnt[maxcnt] = 6。 -
请参见书籍资源网站中的ttt_think.py。
-
请参阅书籍资源网站上的ttt_simulation.py和ttt_ml.py。
-
请参阅书籍资源网站上的outcome_ttt_think.py和outcome_ttt_ml.py。
-
黄色玩家赢得了第 10 局比赛。四个棋盘上的棋子水平连接(在第 2、3、4 和 5 列)。
第十四章
-
更改
start_date = "2020-09-01" end_date = "2021-02-28"为
start_date = "`2021-03-01`" end_date = "2021-`06-01`"并更改
plt.plot(stock['Date'], stock['Adj Close'], c = 'blue')为
plt.plot(stock['Date'], stock['Adj Close'], c = '`red`') -
更改
formatter = mdates.DateFormatter('%m/%d/%Y')为
formatter = mdates.DateFormatter('%m`-`%d`-`%Y')并更改
plt.setp(fig.get_xticklabels(), rotation = 10)为
plt.setp(fig.get_xticklabels(), rotation = `15`)
第十五章
-
更改
usd = response_json['bpi']['USD'] # Get the price price = usd['rate_float'] print(f"The Bitcoin price is {price} dollars.")为
`gbp` = response_json['bpi']['`GBP`'] # Get the price price = `gbp`['`rate`'] print(f"The Bitcoin price is {price} `pounds`.") -
更改
root.geometry("800x200") # Create a label inside the root window label=tk.Label(text="this is a label", fg="Red", font=("Helvetica", 80))为
root.geometry("`850x160`") # Create a label inside the root window label=tk.Label(text="`here is your label`", fg="Red", font=("Helvetica", 80)) -
更改
root.after(1000, bitcoin_watch)为
root.after(`800`, bitcoin_watch) -
更改
maxprice = oldprice * 1.05 minprice = oldprice * 0.95为
maxprice = oldprice * `1.03` minprice = oldprice * `0.97`


浙公网安备 33010602011771号