Python编程快速上手 让繁琐工作自动化 - GitHubliuxianhui/python GitHub Wiki
[美] Al Sweigart 著 王海鹏 译 资深Python程序员力作 带你快速掌握Python高效编程 Python编程快速上手 —让繁琐工作自动化 人 民 邮 电 出 版 社 北 京 [美] Al Sweigart 著 王海鹏 译 Python编程快速上手 —让繁琐工作自动化 版权声明 Simplified Chinese-language edition copyright ? 2016 by Posts and Telecom Press. Copyright ? 2015 by Al Sweigart.Title of English-language original: Automate The Boring Stuff with Python ISBN-13: 978-1-59327-599-0, published by No Starch Press. All rights reserved. 本书中文简体字版由美国No Starch 出版社授权人民邮电出版社出版。未经出版者书面许可,对本书 任何部分不得以任何方式复制或抄袭。 版权所有,侵权必究。 ? 著 [美] Al Sweigart 译 王海鹏 责任编辑 陈冀康 责任印制 焦志炜 ? 人民邮电出版社出版发行 北京市丰台区成寿寺路11 号 邮编 100164 电子邮件 [email protected] 网址 http://www.ptpress.com.cn 北京鑫正大印刷有限公司印刷 ? 开本:800?1000 1/16 印张:26.25 字数:590 千字 2016 年7 月第1 版 印数:1 – 3 000 册 2016 年7 月北京第1 次印刷 著作权合同登记号 图字:01-2015-2962 号 定价:69.00 元 读者服务热线:(010)81055410 印装质量热线:(010)81055316 反盗版热线:(010)81055315 内容提要 如今,人们面临的大多数任务都可以通过编写计算机软件来完成。Python 是一 种解释型、面向对象、动态数据类型的高级程序设计语言。通过Python 编程,我们 能够解决现实生活中的很多任务。 本书是一本面向实践的Python 编程实用指南。本书的目的,不仅是介绍Python 语言的基础知识,而且还通过项目实践教会读者如何应用这些知识和技能。本书的 第一部分介绍了基本的Python 编程概念,第二部分介绍了一些不同的任务,通过编 写Python 程序,可以让计算机自动完成它们。第二部分的每一章都有一些项目程序, 供读者学习。每章的末尾还提供了一些习题和深入的实践项目,帮助读者巩固所学 的知识。附录部分提供了所有习题的解答。 本书适合任何想要通过Python 学习编程的读者,尤其适合缺乏编程基础的初学 者。通过阅读本书,读者将能利用最强大的编程语言和工具,并且将体会到Python 编程的快乐。 作者简介 Al Sweigart 是一名软件开发者和技术图书作者,居住在旧金山。Python 是他最 喜欢的编程语言,他开发了几个开源模块。他的其他著作都在他的网站http://www. inventwithpython.com/上。 技术评审者简介 Ari Lacenski 是Android 应用程序和Python 软件开发者。她住在旧金山,她写了 一些关于Android 编程的文章,放在http://gradlewhy.ghost.io/上,并与Women Who Code 合作提供指导。她还是一个民谣吉他手。 致谢 没有很多人的帮助,我不可能写出这样一本书。我想感谢Bill Pollock,我的编 辑Laurel Chun、Leslie Shen、Greg Poulos 和Jennifer Griffith-Delgado,以及No Starch Press 的其他工作人员,感谢他们非常宝贵的帮助。感谢我的技术评审Ari Lacenski, 她提供了极好的建议、编辑和支持。 非常感谢Guido van Rossum,以及Python 软件基金会的每个人,感谢他们了不 起的工作。Python 社区是我在业界看到的最佳社区。 最后,我要感谢我的家人和朋友,以及在Shotwell 的伙伴,他们不介意我在写 这本书时忙碌的生活。干杯! 译者序 会编程的人不一样 这是机器代替人的时代,也是人控制机器的时代。这是程序员的时代,也是非 程序员学编程的时代。这是算法的时代,也是编程语言的时代。翻译本书期间,深 度学习的人工智能程序AlphaGo 以4:1 击败了李世石九段。 每一个不会编程的年轻人都应该认真考虑:是不是应该开始学习编程? 学习一门新的语言,总是让人感到畏缩。这让我想起大学时英语老师教的学习 方法:听说领先,读写跟上。确实,学语言效果最好的方法就是“用”。本书就遵 循了这样的宗旨。本书是面对编程初学者的书,假定读者没有任何编程知识。在简 单介绍Python 编程语言的基本知识后,就开始用一个接一个的例子,教我们如何用 Python 来完成一些日常工作,利用计算机这个强大的工具,节省工作时间,提高工 作效率,避免手工操作容易带来的错误。 真正的程序员,用编程来解决自己和别人的问题。俄罗斯有一个程序员编写了 一个程序,会给老婆发加班短信,会在宿醉不醒时给自己请假,会自动根据邮件恢 复客户的数据库,还可以一键远程煮咖啡。加拿大一名零编程基础的农场主,在学 习了一门编程课后,开发了一个程序,自动控制拖拉机,配合联合收割机收割谷物。 若是已经掌握了其他编程语言,想学习Python,本书也是不错的参考。每一种 编程语言,都会提供一种独特的视角,让你对编程有新的认识。我非常喜欢Python 没有花括号和分号,程序很“清爽”,符合奥卡姆剃刀原理:如无必要,勿增实体。 本书并没有深入介绍面向对象和函数式编程范式,如果想了解Python 这方面的内 容,请参考其他书籍。 在本书的翻译过程,我自己也在项目中使用Python 编程,从中得到许多启发。 因此,郑重向大家推荐。翻译中的错误,请不吝指出。 王海鹏 2016 年春于上海 前 言 前 言 “你在2 个小时里完成的事,我们3 个人要做两天。”21 世纪早期,我的大学室友在一个电子产品零售商店工作。商店 偶尔会收到一份电子表格,其中包含竞争对手的数千种产品的 价格。由3 个员工组成的团队,会将这个电子表格打印在一叠 厚厚的纸上,然后3 个人分一下。针对每个产品价格,他们会 查看自己商店的价格,并记录竞争对手价格较低的所有产品。 这通常会花几天的时间。 “如果你有打印件的原始文件,我会写一个程序来做这件事。”我的室友告诉他 们,当时他看到他们坐在地板上,周围都是散落堆叠的纸张。 几个小时后,他写了一个简短的程序,从文件读取竞争对手的价格,在商店的 数据库中找到该产品,并记录竞争对手是否更便宜。他当时还是编程新手,花了许 多时间在一本编程书籍中查看文档。实际上程序只花了几秒钟运行。我的室友和他 的同事们那天享受了超长的午餐。 这就是计算机编程的威力。计算机就像瑞士军刀,可以用来完成数不清的任务。 许多人花上数小时点击鼠标和敲打键盘,执行重复的任务,却没有意识到,如果他 们给机器正确的指令,机器就能在几秒钟内完成他们的工作。 本书的读者对象 软件是我们今天使用的许多工具的核心:几乎每个人都使用社交网络来进行交 流,许多人的手机中都有连接因特网的计算机,大多数办公室工作都涉及操作计算 机来完成工作。因此,对编程人才的需求暴涨。无数的图书、交互式网络教程和开 发者新兵训练营,承诺将有雄心壮志的初学者变成软件工程师,获得6 位数的薪水。 前 言 本书不是针对这些人的。它是针对所有其他的人。 就它本身来说,这本书不会让你变成一个职业软件开发者,就像几节吉他课程 不会让你变成一名摇滚巨星。但如果你是办公室职员、管理者、学术研究者,或使 用计算机来工作或娱乐的任何人,你将学到编程的基本知识,这样就能将下面这样 一些简单的任务自动化: ??移动并重命名几千个文件,将它们分类,放入文件夹; ??填写在线表单,不需要打字; ??在网站更新时,从网站下载文件或复制文本; ??让计算机向客户发出短信通知; ??更新或格式化Excel 电子表格; ??检查电子邮件并发出预先写好的回复。 对人来说,这些任务简单,但很花时间。它们通常很琐碎、很特殊,没有现成 的软件可以完成。有一点编程知识,就可以让计算机为你完成这些任务。 编码规范 本书没有设计成参考手册,它是初学者指南。编码风格有时候违反最佳实践(例 如,有些程序使用全局变量),但这是一种折中,让代码更简单,以便学习。本书 的目的是让人们编写用完即抛弃的代码,所以没有太多时间来关注风格和优雅。复 杂的编程概念(如面向对象编程、列表推导和生成器),在本书中也没有介绍,因 为它们增加了复杂性。编程老手可能会指出,本书中的代码可以修改得更有效率, 但本书主要考虑的是用最少的工作量得到能工作的程序。 什么是编程 在电视剧和电影中,常常看到程序员在闪光的屏幕上迅速地输入密码般的一串1 和0,但现代编程没有这么神秘。编程只是输入指令让计算机来执行。这些指令可 能运算一些数字,修改文本,在文件中查找信息,或通过因特网与其他计算机通信。 所有程序都使用基本指令作为构件块。下面是一些常用的指令,用自然语言的 形式来表示: “做这个,然后做那个。” “如果这个条件为真,执行这个动作,否则,执行那个动作。” “按照指定次数执行这个动作。” “一直做这个,直到条件为真。” 也可以组合这些构件块,实现更复杂的决定。例如,这里有一些编程指令,称 为源代码,是用Python 编程语言编写的一个简单程序。从头开始,Python 软件执 前 言 行每行代码(有些代码只有在特定条件为真时执行,否则Python 会执行另外一些代 码),直到到达底部。 ??passwordFile = open('SecretPasswordFile.txt') ??secretPassword = passwordFile.read() ??print('Enter your password.') typedPassword = input() ??if typedPassword == secretPassword: ??print('Access granted') ??if typedPassword == '12345': ??print('That password is one that an idiot puts on their luggage.') else: ??print('Access denied') 你可能对编程一无所知,但读了上面的代码,也许就能够合理地猜测它做的事。 首先,打开了文件SecretPasswordFile.txt?,读取了其中的密码?。然后,提示用户 (通过键盘)输入一个密码?。比较这两个密码?,如果它们一样,程序就在屏幕上 打印Access granted?。接下来,程序检查密码是否为12345?,提示说这可能并不 是最好的密码?。如果密码不一样,程序就在屏幕上打印Access denied?。 什么是Python Python 指的是Python 编程语言(包括语法规则,用于编写被认为是有效的 Python 代码),以及Python 解释器软件,它读取源代码(用python 语言编写),并 执行其中的指令。Python 解释器可以从http://python.org/免费下载,有针对Linux、 OS X 和Windows 的版本。 Python 的名字来自于英国超现实主义喜剧团体,而不是来自于蛇。Python 程序 员被亲切地称为Pythonistas。Monty Python 和与蛇相关的引用常常出现在Python 的 指南和文档中。 程序员不需要知道太多数学 我听到的关于学习编程的最常见的顾虑,就是人们认为这需要很多数学知识。 其实,大多数编程需要的数学知识不超过基本算数。实际上,善于编程与善于解决 数独问题没有太大差别。 要解决数独问题,数字1 到9 必须填入9×9 的棋盘上每一行、每一列,以及每 个3×3 的内部方块。通过推导和起始数字的逻辑,你会找到一个答案。例如,在图 1 的数独问题中,既然5 出现在了左上角,它就不能出现在顶行、最左列,或左上角 3×3 方块中的其他位置。每次解决一行、一列或一个方块,将为剩下的部分提供更 多的数字线索。 仅仅因为数独使用了数字,并不意味着必须精通数学才能求出答案。编程也是 这样。就像解决数独问题一样,编程需要将一个问题分解为单个的、详细的步骤。 类似地,在调试程序时(即寻找和修复错误),你会耐心地观察程序在做什么,找 前 言 出缺陷的原因。像所有技能一样,编程越多,你就掌握得越好。 图1 一个新的数独问题(左边)及其答案(右边)。尽管使用了数字, 数独并不需要太多数学知识 编程是创造性活动 编程是一项创造性任务,有点类似于用乐高积木构建一个城堡。你从基本的想 法开始,希望城堡看起来像怎样,并盘点可用的积木。然后开始构建。在你完成构 建程序后,可以让代码变得更美观,就像对你的城堡那样。 编程与其他创造性活动的不同之处在于,在编程时,你需要的所有原材料都在 计算机中,你不需要购买额外的画布、颜料、胶片、纱线、乐高积木或电子器件。 在程序写好后,很容易将它在线共享给整个世界。尽管在编程时你会犯错,这项活 动仍然很有乐趣。 本书简介 本书的第一部分介绍了基本Python 编程概念,第二部分介绍了一些不同的任 务,你可以让计算机自动完成它们。第二部分的每一章都有一些项目程序,供你学 习。下面简单介绍一下每章的内容。 第一部分:Python 编程基础 “第 1 章:Python 基础”介绍了表达式、Python 指令的最基本类型,以及如何 使用Python 交互式环境来尝试运行代码。 “第 2 章:控制流”解释了如何让程序决定执行哪些指令,以便代码能够智能 地响应不同的情况。 “第3 章:函数”介绍了如何定义自己的函数,以便将代码组织成可管理的部分。 “第4 章:列表”介绍了列表数据类型,解释了如何组织数据。 “第 5 章:字典和结构化数据”介绍了字典数据类型,展示了更强大的数据组 织方法。 前 言 “第6 章:字符串操作”介绍了处理文本数据(在Python 中称为字符串)。 第二部分:自动化任务 “第7 章:模式匹配与正则表达式”介绍了Python 如何用正则表达式处理字符 串,以及查找文本模式。 “第 8 章:读写文件”解释了程序如何读取文本文件的内容,并将信息保存到 硬盘的文件中。 “第 9 章:组织文件”展示了Python 如何用比手工操作快得多的速度,复制、 移动、重命名和删除大量的文件,也解释了压缩和解压缩文件。 “第10 章:调试”展示了如何使用Python 的缺陷查找和缺陷修复工具。 “第 11 章:从Web 抓取信息”展示了如何编程来自动下载网页,解析它们,获 取信息。这称为从Web 抓取信息。 “第 12 章:处理Excel 电子表格”介绍了编程处理Excel 电子表格,这样你就 不必去阅读它们。如果你必须分析成百上千的文档,这是很有帮助的。 “第13 章:处理PDF 和Word 文档”介绍了编程读取Word 和PDF 文档。 “第14 章:处理CSV 文件和JSON 数据”解释了如何编程操作CSV 和JSON 文件。 “第15 章:保持时间、计划任务和启动程序”解释了Python 程序如何处理时间 和日期,如何安排计算机在特定时间执行任务。这一章也展示了Python 程序如何启 动非Python 程序。 “第16 章:发送电子邮件和短信”解释了如何编程来发送电子邮件和短信。 “第17 章:操作图像”解释了如何编程来操作JPG 或PNG 这样的图像。 “第18 章:用GUI 自动化控制键盘和鼠标”解释了如何编程控制鼠标和键盘, 自动化鼠标点击和击键。 下载和安装Python 可以从http://python.org/downloads/免费下载针对Windows、OS X 和Ubuntu 的 Python 版本。如果你从该网站的下载页面下载了最新的版本,本书中的所有程序应 该都能工作。 注意 请确保下载Python 3 的版本(诸如3.4.0)。本书中的程序将运行在Python 3 上, 有一部分程序在Python 2 上也许不能正常运行。 你需要在下载页面上找到针对64 位或32 位计算机以及特定操作系统的Python 安装程序,所以先要弄清楚你需要哪个安装程序。如果你的计算机是2007 年或以 后购买的,很有可能是64 位的系统。否则,可能是32 位的系统,但下面是确认 前 言 的方法: ??在Windows 上。选择Start?ControlPanel?System。检查系统类型是64 位或 32 位。 ??在OS X 上,进入Apple 菜单,选择About This Mac?MoreInfo?SystemReport? Hardware,然后查看Processor Name 字段。如果是Intel Core Solo 或Intel Core Duo, 机器是32 位的。如果是其他(包括Intel Core 2 Duo),机器是64 位的。 ??在Ubuntu Linux 上,打开终端窗口,运行命令uname -m。结果是i686 表示是 32 位,x86_64 表示是64 位。 在Windows 上,下载Python 安装程序(文件扩展名是.msi),并双击它。按照 安装程序显示在屏幕上的指令来安装Python,步骤如下。 1.选择Install for All Users,然后点击Next。 2.通过点击Next 安装到C:\Python34 文件夹。 3.再次点击Next,跳过定制Python 的部分。 在OS X 上,下载适合你的OS X 版本的.dmg 文件,并双击它。按照安装程序 显示在屏幕上的指令来安装Python,步骤如下。 1.当DMG 包在一个新窗口中打开时,双击Python.mpkg 文件。你可能必须输 入管理员口令。 2.点击Continue,跳过欢迎部分,并点击Agree,接受许可证。 3.选择HD Macintosh(或者你的硬盘的名字),并点击Install。 如果使用的是Ubuntu,可以从终端窗口安装Python,步骤如下。 1.打开终端窗口。 2.输入sudo apt-get install python3。 3.输入sudo apt-get install idle3。 4.输入sudo apt-get install python3-pip。 启动IDLE Python 解释器是运行Python 程序的软件,而交互式开发环境(IDLE)是输入 程序的地方,就像一个字处理软件。现在让我们启动IDLE。 ??在Windows7 或更新的版本上,点击屏幕左下角的开始图标,在搜索框中输入 IDLE,并选择IDLE(Python GUI)。 ??Windows XP 上,点击开始按钮,然后选择Programs?Python 3.4?IDLE(Python GUI)。 ??在OS X 上,打开Finder 窗口,点击Applications,点击Python 3.4,然后点击 IDLE 的图标。 ??在Ubuntu 上,选择Applications?Accessories?Terminal,然后输入idle3(也许你也 前 言 可以点击屏幕顶部的Applications,选择Programming,然后点击IDLE 3)。 交互式环境 无论你使用什么操作系统,初次出现的IDLE 窗口应该基本上是空的,除了类 似下面这样的文本: Python 3.4.0 (v3.4.0:04f714765c13, Mar 16 2014, 19:25:23) [MSC v.1600 64 bit (AMD64)] on win32Type "copyright", "credits" or "license()" for more information.
这个窗口称为交互式环境。这是让你向计算机输入指令的程序,很像OS X 上 的终端窗口,或Windows 上的命令行提示符。Python 的交互式环境让你输入指令,供 Python 解释器软件来执行。计算机读入你输入的指令,并立即执行它们。 例如,在交互式环境的>>>提示符后输入以下指令:
print('Hello world!') 在输入该行并按下回车键后,交互式环境将显示以下内容作为响应: print('Hello world!') Hello world! 如何寻求帮助 独自解决编程问题可能比你想的要容易。如果你不相信,就让我们故意产生一 个错误:在交互式环境中输入'42' + 3。现在你不需要知道这条指令是什么意思,但 结果看起来应该像这样: '42' + 3 ??Traceback (most recent call last): File "<pyshell#0>", line 1, in '42' + 3 ??TypeError: Can't convert 'int' object to str implicitly
这里出现了错误信息?,因为Python 不理解你的指令。错误信息的traceback 部 分?显示了Python 遇到困难的特定指令和行号。如果你不知道怎样处理特定的错误 信息,就在线查找那条错误信息。在你喜欢的搜索引擎上输入“TypeError: Can't convert 'int' object to str implicitly”(包括引号),你就会看到许多的链接,解释这条错误信 息的含义,以及什么原因导致这条错误,如图2 所示。 你常常会发现,别人也遇到了同样的问题,而其他乐于助人的人已经回答了这 个问题。没有人知道编程的所有方面,所以所有软件开发者的工作,都是每天在寻 找技术问题的答案。 前 言 图2 错误信息的Google 搜索结果可能非常有用 聪明地提出编程问题 如果不能在线查找到答案,请尝试在Stack Overlow(http://stackoverflow.com/)或 “learnprogramming”subreddit(http://reddit.com/r/learnprogramming/)这样的论坛上提 问。但要记住,用聪明的方式提出编程问题,这有助于别人来帮助你。确保阅读这 些网站的FAQ(常见问题),了解正确的提问方式。 在提出编程问题时,要记住以下几点。 ??说明你打算做什么,而不只是你做了什么。这让帮助你的人知道你是否走错了路。 ??明确指出发生错误的地方。它是在程序每次启动时发生,还是在你做了某些动 作之后? ??将完整的错误信息和你的代码复制粘贴到http://pastebin.com/或http://gist. github.com/。 这些网站让你很容易在网上与他人共享大量的代码,而不会丢失任何文本格 式。然后你可以将贴出的代码的URL 放在电子邮件或论坛帖子中。例如,这 里是我贴出的一些代码片段:http://pastebin.com/SzP2DbFx/和https://gist.github. com/ asweigart/6912168/。 ??解释你为了解决这个问题已经尝试了哪些方法。这会告诉别人你已经做了一些 工作来弄清楚状况。 前 言 ??列出你使用的Python 版本(Python 2 解释器和Python3 解释器之间有一些重要 的区别)。而且,要说明你使用的操作系统和版本。 ??如果错误在你更改了代码之后出现,准确说明你改了什么。 ??说明你是否在每次运行该程序时都能重现该错误,或者它只是在特定的操作执 行之后才出现。如果是这样,解释是哪些操作。 也要遵守良好的在线礼节。例如,不要全用大写提问,或者对试图帮助你的人 提出无理的要求。 小结 对于大多数人,他们的计算机只是设备,而不是工具。但通过学习如何编程, 你就能利用现代社会中最强大的工具,并且你会一直感到快乐。编程不是脑外科手 术,业余人士是完全可以尝试或犯错的。 我喜欢帮助人们探索Python 。我在自己的博客上编写编程指南( http:// inventwithpython.com/blog/),你可以发邮件向我提问([email protected])。 本书将从零编程知识开始,但你的问题可能超出本书的范围。记住如何有效地 提问,知道如何寻找答案,这对你的编程之旅是无价的工具。 让我们开始吧! 目 录 目 录 第一部分 Python 编程基础 第1 章 Python 基础 ..................................... 3 1.1 在交互式环境中输入表达式 ....... 3 1.2 整型、浮点型和字符串数据类型 ... 6 1.3 字符串连接和复制 ........................ 6 1.4 在变量中保存值 ............................ 7 1.4.1 赋值语句 ............................... 7 1.4.2 变量名 ................................... 9 1.5 第一个程序 .................................... 9 1.6 程序剖析 ...................................... 11 1.6.1 注释 ..................................... 11 1.6.2 print()函数 ........................... 11 1.6.3 input()函数 .......................... 11 1.6.4 打印用户的名字 ................. 12 1.6.5 len()函数 ............................. 12 1.6.6 str()、int()和float()函数 ..... 13 1.7 小结 ............................................... 15 1.8 习题 ............................................... 15 第2 章 控制流 ............................................. 17 2.1 布尔值 .......................................... 18 2.2 比较操作符 ................................. 19 2.3 布尔操作符 ................................. 20 2.3.1 二元布尔操作符 .................20 2.3.2 not 操作符 ...........................21 2.4 混合布尔和比较操作符 ............. 21 2.5 控制流的元素 ............................. 22 2.5.1 条件 .....................................22 2.5.2 代码块 .................................22 2.6 程序执行 ...................................... 23 2.7 控制流语句 ................................. 23 2.7.1 if 语句 ..................................23 2.7.2 else 语句 ..............................24 2.7.3 elif 语句 ...............................25 2.7.4 while 循环语句 ...................30 2.7.5 恼人的循环 .........................31 2.7.6 break 语句 ...........................33 2.7.7 continue 语句 .......................34 2.7.8 for 循环和range()函数 .......37 2.7.9 等价的while 循环...............39 目 录 2.7.10 range()的开始、停止和 步长参数 ........................... 39 2.8 导入模块 ...................................... 40 from import 语句 ............................. 41 2.9 用sys.exit()提前结束程序 ......... 41 2.10 小结 ............................................. 41 2.11 习题 ............................................. 41 第3 章 函数 ................................................. 43 3.1 def 语句和参数 ............................ 44 3.2 返回值和return 语句 .................. 45 3.3 None 值 ......................................... 46 3.4 关键字参数和print() .................. 47 3.5 局部和全局作用域 ...................... 48 3.5.1 局部变量不能在全局作用 域内使用 ............................. 48 3.5.2 局部作用域不能使用其他 局部作用域内的变量 ......... 49 3.5.3 全局变量可以在局部作用 域中读取 ............................. 49 3.5.4 名称相同的局部变量和全局 变量 ..................................... 50 3.6 global 语句 ................................... 50 3.7 异常处理 ...................................... 52 3.8 一个小程序:猜数字 ................. 54 3.9 小结 ............................................... 55 3.10 习题 ............................................. 56 3.11 实践项目 .................................... 56 3.11.1 Collatz 序列 ....................... 56 3.11.2 输入验证 ........................... 57 第4 章 列表 ................................................. 59 4.1 列表数据类型 .............................. 59 4.1.1 用下标取得列表中的 单个值 ................................. 60 4.1.2 负数下标 ............................. 61 4.1.3 利用切片取得子列表 ......... 61 4.1.4 用len()取得列表的长度 ..... 62 4.1.5 用下标改变列表中的值 .....62 4.1.6 列表连接和列表复制 .........62 4.1.7 用del 语句从列表中 删除值 .................................63 4.2 使用列表 ...................................... 63 4.2.1 列表用于循环 .....................64 4.2.2 in 和not in 操作符 ..............65 4.2.3 多重赋值技巧 .....................66 4.3 增强的赋值操作 ......................... 66 4.4 方法 .............................................. 67 4.4.1 用index()方法在列表中 查找值 .................................67 4.4.2 用append()和insert()方法在 列表中添加值 .....................68 4.4.3 用remove()方法从列表中 删除值 .................................69 4.4.4 用sort()方法将列表中的值 排序 .....................................69 4.5 例子程序:神奇8 球和列表 .... 70 4.6 类似列表的类型:字符串和 元组 .............................................. 71 4.6.1 可变和不可变数据类型 .....72 4.6.2 元组数据类型 .....................73 4.6.3 用list()和tuple()函数来 转换类型 .............................74 4.7 引用 .............................................. 75 4.7.1 传递引用 .............................76 4.7.2 copy 模块的copy()和 deepcopy()函数 ...................77 4.8 小结 .............................................. 78 4.9 习题 .............................................. 78 4.10 实践项目.................................... 79 4.10.1 逗号代码 ...........................79 4.10.2 字符图网格 .......................79 第5 章 字典和结构化数据 .........................81 5.1 字典数据类型 ............................. 81 目 录 5.1.1 字典与列表 ......................... 82 5.1.2 keys()、values()和items() 方法 ..................................... 83 5.1.3 检查字典中是否存在键 或值 ..................................... 84 5.1.4 get()方法 ............................. 84 5.1.5 setdefault()方法 ................... 85 5.2 漂亮打印 ...................................... 86 5.3 使用数据结构对真实世界建模 .... 87 5.3.1 井字棋盘 ............................. 88 5.3.2 嵌套的字典和列表 ............. 91 5.4 小结 ............................................... 92 5.5 习题 ............................................... 93 5.6 实践项目 ...................................... 93 5.6.1 好玩游戏的物品清单 ......... 93 5.6.2 列表到字典的函数,针对 好玩游戏物品清单 ............. 94 第6 章 字符串操作 ..................................... 95 6.1 处理字符串 .................................. 95 6.1.1 字符串字面量 ..................... 95 6.1.2 双引号 ................................. 96 6.1.3 转义字符 ............................. 96 6.1.4 原始字符串 ......................... 96 6.1.5 用三重引号的多行字符串 .... 97 6.1.6 多行注释 ............................. 97 6.1.7 字符串下标和切片 ............. 98 6.1.8 字符串的in 和not in 操作符 ................................. 98 6.2 有用的字符串方法 ...................... 99 6.2.1 字符串方法upper()、lower()、 isupper()和islower()............99 6.2.2 isX 字符串方法 ................. 100 6.2.3 字符串方法startswith()和 endswith() .......................... 102 6.2.4 字符串方法join()和 split() .................................. 102 6.2.5 用rjust()、ljust()和center() 方法对齐文本 ................... 103 6.2.6 用strip()、rstrip()和lstrip() 删除空白字符 ................... 104 6.2.7 用pyperclip 模块拷贝粘贴字 符串 ................................... 105 6.3 项目:口令保管箱 ................... 106 第1 步:程序设计和数据结构 ..... 106 第2 步:处理命令行参数 ........... 106 第3 步:复制正确的口令 ........... 107 6.4 项目:在Wiki 标记中添加无序 列表 ............................................ 108 第1 步:从剪贴板中复制和 粘贴 ............................... 108 第2 步:分离文本中的行,并添加 星号 ............................... 109 第3 步:连接修改过的行 ........... 109 6.5 小结 ............................................ 110 6.6 习题 ............................................ 110 6.7 实践项目 ..................................... 111 表格打印 ....................................... 111 第二部分 自动化任务 第7 章 模式匹配与正则表达式 ............... 115 7.1 不用正则表达式来查找文本 模式 ............................................ 116 7.2 用正则表达式查找文本模式 .... 117 7.2.1 创建正则表达式对象 ....... 118 7.2.2 匹配Regex 对象 ............... 118 7.2.3 正则表达式匹配复习 ....... 119 目 录 7.3 用正则表达式匹配更多模式 ..... 119 7.3.1 利用括号分组 ................... 119 7.3.2 用管道匹配多个分组 ....... 120 7.3.3 用问号实现可选匹配 ....... 121 7.3.4 用星号匹配零次或多次..... 121 7.3.5 用加号匹配一次或多次..... 122 7.3.6 用花括号匹配特定次数 .... 122 7.4 贪心和非贪心匹配 .................... 123 7.5 findall()方法 ............................... 124 7.6 字符分类 .................................... 124 7.7 建立自己的字符分类 ............... 125 7.8 插入字符和美元字符 ............... 126 7.9 通配字符 .................................... 126 7.9.1 用点-星匹配所有字符 ...... 127 7.9.2 用句点字符匹配换行 ....... 127 7.10 正则表达式符号复习 ............. 128 7.11 不区分大小写的匹配.............. 128 7.12 用sub()方法替换字符串 ........ 129 7.13 管理复杂的正则表达式 ......... 129 7.14 组合使用re.IGNOREC ASE、 re.DOTALL 和re.VERBOSE ... 130 7.15 项目:电话号码和E-mail 地址 提取程序 ................................. 130 第1 步:为电话号码创建一个正则 表达式 ........................... 131 第2 步:为E-mail 地址创建一个 正则表达式 ................... 132 第3 步:在剪贴板文本中找到所有 匹配 ............................... 132 第4 步:所有匹配连接成一个字符 串,复制到剪贴板 ....... 133 第5 步:运行程序 ....................... 133 第6 步:类似程序的构想 ........... 134 7.16 小结 ........................................... 134 7.17 习题 ........................................... 134 7.18 实践项目 .................................. 136 7.18.1 强口令检测 ..................... 136 7.18.2 strip()的正则表达式 版本 ................................. 136 第8 章 读写文件 ....................................... 137 8.1 文件与文件路径 ....................... 137 8.1.1 Windows 上的倒斜杠以及 OS X 和Linux 上的 正斜杠 ............................... 138 8.1.2 当前工作目录 ................... 139 8.1.3 绝对路径与相对路径 ....... 139 8.1.4 用os.makedirs()创建新 文件夹 ............................... 140 8.1.5 os.path 模块 ....................... 140 8.1.6 处理绝对路径和相对 路径 ................................... 141 8.1.7 查看文件大小和文件夹 内容 ................................... 142 8.1.8 检查路径有效性 ............... 143 8.2 文件读写过程 ........................... 144 8.2.1 用open()函数打开文件 ..... 145 8.2.2 读取文件内容 ................... 145 8.2.3 写入文件 ........................... 146 8.3 用shelve 模块保存变量 .......... 147 8.4 用pprint.pformat()函数保存 变量 ............................................ 148 8.5 项目:生成随机的测验试卷 文件 ............................................ 149 第1 步:将测验数据保存在一个 字典中 ........................... 149 第2 步:创建测验文件,并打乱 问题的次序 ................... 150 第3 步:创建答案选项 ............... 151 第4 步:将内容写入测验试卷和 答案文件 ....................... 151 8.6 项目:多重剪贴板 ................... 153 目 录 第1 步:注释和shelf 设置 ......... 153 第2 步:用一个关键字保存剪贴板 内容 ............................... 154 第3 步:列出关键字和加载关键字的 内容 ................................ 154 8.7 小结 ............................................. 155 8.8 习题 ............................................. 155 8.9 实践项目 .................................... 156 8.9.1 扩展多重剪贴板 ............... 156 8.9.2 疯狂填词 ........................... 156 8.9.3 正则表达式查找 ............... 156 第9 章 组织文件 ....................................... 157 9.1 shutil 模块 .................................. 158 9.1.1 复制文件和文件夹 ........... 158 9.1.2 文件和文件夹的移动与 改名 ................................... 158 9.1.3 永久删除文件和文件夹..... 160 9.1.4 用send2trash 模块安全地 删除 ................................... 160 9.2 遍历目录树 ................................ 161 9.3 用zipfile 模块压缩文件 ........... 162 9.3.1 读取ZIP 文件 ................... 163 9.3.2 从ZIP 文件中解压缩 ....... 164 9.3.3 创建和添加到ZIP 文件 ..... 164 9.4 项目:将带有美国风格日期的 文件改名为欧洲风格日期 ....... 165 第1 步:为美国风格的日期创建一个 正则表达式 .................... 165 第2 步:识别文件名中的日期 部分 ............................... 166 第3 步:构成新文件名,并对文件 改名 ............................... 167 第4 步:类似程序的想法 ........... 168 9.5 项目:将一个文件夹备份到一个 ZIP 文件 ..................................... 168 第1 步:弄清楚ZIP 文件的 名称 ............................... 168 第2 步:创建新ZIP 文件 ........... 169 第3 步:遍历目录树并添加到 ZIP 文件 ........................ 170 第4 步:类似程序的想法 ........... 170 9.6 小结 ............................................ 171 9.7 习题 ............................................ 171 9.8 实践项目 .................................... 171 9.8.1 选择性拷贝 ....................... 171 9.8.2 删除不需要的文件 ........... 172 9.8.3 消除缺失的编号 ............... 172 第10 章 调试 ............................................. 173 10.1 抛出异常.................................. 174 10.2 取得反向跟踪的字符串 ........ 175 10.3 断言 .......................................... 176 10.3.1 在交通灯模拟中使用 断言 ................................. 177 10.3.2 禁用断言 ......................... 178 10.4 日志 .......................................... 178 10.4.1 使用日志模块 ................. 178 10.4.2 不要用print()调试 .......... 180 10.4.3 日志级别 ......................... 180 10.4.4 禁用日志 ......................... 181 10.4.5 将日志记录到文件 ......... 182 10.5 IDLE 的调试器 ....................... 182 10.5.1 Go .................................... 183 10.5.2 Step .................................. 183 10.5.3 Over ................................. 183 10.5.4 Out ................................... 183 10.5.5 Quit .................................. 183 10.5.6 调试一个数字相加的 程序 ................................. 184 10.5.7 断点 ................................. 185 10.6 小结 .......................................... 187 目 录 10.7 习题 ........................................... 187 10.8 实践项目 .................................. 188 第11 章 从Web 抓取信息 ....................... 189 11.1 项目:利用webbrowser 模块的 mapIt.py .................................... 190 第1 步:弄清楚URL .................. 190 第2 步:处理命令行参数 ........... 191 第3 步:处理剪贴板内容,加载 浏览器 ........................... 191 第4 步:类似程序的想法 ........... 192 11.2 用requests 模块从Web 下载 文件 ........................................... 192 11.2.1 用requests.get()函数下载 一个网页 ......................... 193 11.2.2 检查错误 ......................... 193 11.3 将下载的文件保存到硬盘 ..... 194 11.4 HTML ....................................... 195 11.4.1 学习HTML 的资源 ........ 195 11.4.2 快速复习 ......................... 195 11.4.3 查看网页的HTML 源代码 ............................. 196 11.4.4 打开浏览器的开发者 工具 ................................. 197 11.4.5 使用开发者工具来寻找 HTML 元素 ..................... 198 11.5 用BeautifulSoup 模块解析 HTML ....................................... 199 11.5.1 从HTML 创建一个 BeautifulSoup 对象 ......... 200 11.5.2 用select()方法寻找元素 .... 200 11.5.3 通过元素的属性获取 数据 ................................. 202 11.6 项目:“I’m Feeling Lucky” Google 查找 ............................. 202 第1步:获取命令行参数,并请求 查找页面 ....................... 203 第2步:找到所有的结果 ........... 203 第3 步:针对每个结果打开Web 浏览器 ........................... 204 第4 步:类似程序的想法 ........... 205 11.7 项目:下载所有XKCD 漫画 ......................................... 205 第1 步:设计程序 ....................... 206 第2 步:下载网页 ....................... 207 第3 步:寻找和下载漫画图像 ..... 207 第4 步:保存图像,找到前一张 漫画 ............................... 208 第5 步:类似程序的想法 ........... 209 11.8 用selenium 模块控制浏览器 ... 210 11.8.1 启动selenium 控制的 浏览器 ............................. 210 11.8.2 在页面中寻找元素 .......... 211 11.8.3 点击页面 ......................... 212 11.8.4 填写并提交表单.............. 212 11.8.5 发送特殊键 ..................... 213 11.8.6 点击浏览器按钮.............. 213 11.8.7 关于selenium 的更多 信息 ................................. 214 11.9 小结 .......................................... 214 11.10 习题 ........................................ 214 11.11 实践项目 ................................ 215 11.11.1 命令行邮件程序 ............ 215 11.11.2 图像网站下载 ................ 215 11.11.3 2048 ................................ 215 11.11.4 链接验证 ........................ 215 第12 章 处理Excel 电子表格 ................. 217 12.1 Excel 文档 ............................... 217 12.2 安装openpyxl 模块 ................ 218 12.3 读取Excel 文档 ...................... 218 12.3.1 用openpyxl 模块打开Excel 目 录 文档 ................................. 219 12.3.2 从工作簿中取得工作表 .... 219 12.3.3 从表中取得单元格 ......... 220 12.3.4 列字母和数字之间的 转换 ................................. 221 12.3.5 从表中取得行和列 ......... 222 12.3.6 工作簿、工作表、 单元格 ............................. 223 12.4 项目:从电子表格中读取 数据 ........................................... 223 第1 步:读取电子表格数据 ....... 224 第2 步:填充数据结构 ............... 225 第3 步:将结果写入文件 ........... 226 第4 步:类似程序的思想 ........... 227 12.5 写入Excel 文档 ....................... 227 12.5.1 创建并保存Excel 文档 .... 227 12.5.2 创建和删除工作表 ......... 228 12.5.3 将值写入单元格 ............. 229 12.6 项目:更新一个电子表格 ..... 229 第1 步:利用更新信息建立数据 结构 ............................... 230 第2 步:检查所有行,更新不正确的 价格 ............................... 231 第3 步:类似程序的思想 ........... 231 12.7 设置单元格的字体风格 ......... 232 12.8 Font 对象 .................................. 232 12.9 公式 ........................................... 234 12.10 调整行和列 ............................ 235 12.10.1 设置行高和列宽 ........... 235 12.10.2 合并和拆分单元格 ....... 236 12.10.3 冻结窗格 ....................... 237 12.10.4 图表 ............................... 238 12.11 小结 ......................................... 240 12.12 习题 ........................................ 240 12.13 实践项目 ................................ 241 12.13.1 乘法表 ........................... 241 12.13.2 空行插入程序 ............... 241 12.13.3 电子表格单元格翻转 程序 ............................... 242 12.13.4 文本文件到电子表格 ..... 242 12.13.5 电子表格到文本文件 ..... 242 第13 章 处理PDF 和Word 文档 ........... 243 13.1 PDF 文档 ................................. 243 13.1.1 从PDF 提取文本 ............ 244 13.1.2 解密PDF ......................... 245 13.1.3 创建PDF ......................... 246 13.1.4 拷贝页面 ......................... 246 13.1.5 旋转页面 ......................... 247 13.1.6 叠加页面 ......................... 248 13.1.7 加密PDF ......................... 249 13.2 项目:从多个PDF 中合并 选择的页面 ............................. 250 第1 步:找到所有PDF 文件 ...... 250 第2 步:打开每个PDF 文件 ...... 251 第3 步:添加每一页 ................... 252 第4 步:保存结果 ....................... 252 第5 步:类似程序的想法 ........... 253 13.3 Word 文档 ................................ 253 13.3.1 读取Word 文档 ............... 254 13.3.2 从.docx 文件中取得完整的 文本 ................................. 254 13.3.3 设置Paragraph 和Run 对象 的样式 ............................. 255 13.3.4 创建带有非默认样式的 Word 文档 ........................ 257 13.3.5 Run 属性 .......................... 257 13.3.6 写入Word 文档 ............... 258 13.3.7 添加标题 ......................... 260 13.3.8 添加换行符和换页符 ..... 261 13.3.9 添加图像 ......................... 261 13.4 小结 .......................................... 262 目 录 13.5 习题 ........................................... 262 13.6 实践项目 .................................. 263 13.6.1 PDF 偏执狂 ..................... 263 13.6.2 定制邀请函,保存为Word 文档 ................................. 263 13.6.3 暴力PDF 口令破解 程序 ................................. 264 第14 章 处理CSV 文件和JSON 数据 .... 265 14.1 csv 模块 .................................... 265 14.1.1 Reader 对象 ..................... 266 14.1.2 在for 循环中,从Reader 对象读取数据 ................. 267 14.1.3 Writer 对象 ...................... 268 14.1.4 delimiter 和lineterminator 关键字参数 ..................... 269 14.2 项目:从CSV 文件中删除 表头 .......................................... 269 第1 步:循环遍历每个CSV 文件 ............................... 270 第2 步:读入CSV 文件 ............. 270 第3 步:写入CSV 文件,没有 第一行 ........................... 271 第4 步:类似程序的想法 ........... 272 14.3 JSON 和API ............................ 272 14.4 json 模块 ................................... 273 14.4.1 用loads()函数读取 JSON ............................... 273 14.4.2 用dumps 函数写出 JSON ............................... 273 14.5 项目:取得当前的天气数据 ... 274 第1 步:从命令行参数获取 位置 ............................... 274 第2 步:下载JSON 数据............ 275 第3 步:加载JSON 数据并打印 天气 ............................... 275 第4 步:类似程序的想法 ........... 277 14.6 小结 .......................................... 277 14.7 习题 .......................................... 277 14.8 实践项目.................................. 277 第15 章 保持时间、计划任务和启动 程序 ............................................ 279 15.1 time 模块 ................................. 279 15.1.1 time.time()函数 ............... 279 15.1.2 time.sleep()函数 .............. 280 15.2 数字四舍五入 ......................... 281 15.3 项目:超级秒表 ..................... 282 第1 步:设置程序来记录时间 ..... 282 第2 步:记录并打印单圈时间 ..... 283 第3 步:类似程序的想法 ........... 283 15.4 datetime 模块 .......................... 284 15.4.1 timedelta 数据类型.......... 285 15.4.2 暂停直至特定日期 ......... 286 15.4.3 将datetime 对象转换为 字符串 ............................. 287 15.4.4 将字符串转换成datetime 对象 ................................. 288 15.5 回顾Python 的时间函数 ....... 288 15.6 多线程 ...................................... 289 15.6.1 向线程的目标函数传递 参数 ................................. 290 15.6.2 并发问题 ......................... 291 15.7 项目:多线程XKCD 下载 程序 .......................................... 291 第1 步:修改程序以使用函数 ..... 292 第2 步:创建并启动线程 ........... 293 第3 步:等待所有线程结束 ....... 293 15.8 从Python 启动其他程序 ....... 294 15.8.1 向Popen()传递命令行 参数 ................................. 295 15.8.2 Task Scheduler、launchd 和 目 录 cron .................................. 296 15.8.3 用Python 打开网站 ........ 296 15.8.4 运行其他Python 脚本 .... 296 15.8.5 用默认的应用程序打开 文件 ................................. 297 15.9 项目:简单的倒计时程序 ..... 298 第1 步:倒计时 ........................... 298 第2 步:播放声音文件 ............... 298 第3 步:类似程序的想法 ........... 299 15.10 小结 ........................................ 299 15.11 习题 ......................................... 300 15.12 实践项目 ................................ 300 15.12.1 美化的秒表 ................... 300 15.12.2 计划的Web 漫画下载 ... 301 第16 章 发送电子邮件和短信 ................. 303 16.1 SMTP ........................................ 303 16.2 发送电子邮件 .......................... 304 16.2.1 连接到SMTP 服务器 ..... 304 16.2.2 发送SMTP 的“Hello” 消息 ................................. 305 16.2.3 开始TLS 加密 ................ 306 16.2.4 登录到SMTP 服务器 ..... 306 16.2.5 发送电子邮件 ................. 306 16.2.6 从SMTP 服务器断开 ..... 307 16.3 IMAP ........................................ 307 16.4 用IMAP 获取和删除电子 邮件 .......................................... 307 16.4.1 连接到IMAP 服务器 ..... 308 16.4.2 登录到IMAP 服务器 ..... 309 16.4.3 搜索电子邮件 ................. 309 16.4.4 选择文件夹 ..................... 309 16.4.5 执行搜索 ......................... 310 16.4.6 大小限制 ......................... 312 16.4.7 取邮件并标记为已读 ..... 312 16.4.8 从原始消息中获取电子 邮件地址 ......................... 313 16.4.9 从原始消息中获取正文 .... 314 16.4.10 删除电子邮件 ............... 315 16.4.11 从IMAP 服务器断开 .... 315 16.5 项目:向会员发送会费提醒 电子邮件 ................................. 316 第1 步:打开Excel 文件 ............ 316 第2 步:查找所有未付成员 ....... 317 第3 步:发送定制的电子邮件 提醒 ............................... 318 16.6 用Twilio 发送短信 ................ 319 16.6.1 注册Twilio 账号 ............. 319 16.6.2 发送短信 ......................... 320 16.7 项目:“只给我发短信” 模块 .......................................... 321 16.8 小结 .......................................... 322 16.9 习题 .......................................... 323 16.10 实践项目 ............................... 323 16.10.1 随机分配家务活的电子 邮件程序 ....................... 323 16.10.2 伞提醒程序 ................... 324 16.10.3 自动退订 ....................... 324 16.10.4 通过电子邮件控制你的 电脑 ............................... 324 第17 章 操作图像 ..................................... 327 17.1 计算机图像基础 ..................... 327 17.1.1 颜色和RGBA 值 ............ 328 17.1.2 坐标和Box 元组 ............. 329 17.2 用Pillow 操作图像 ................ 330 17.2.1 处理Image 数据类型 ..... 331 17.2.2 裁剪图片 ......................... 332 17.2.3 复制和粘贴图像到其他 图像 ................................. 333 17.2.4 调整图像大小 ................. 335 17.2.5 旋转和翻转图像 ............. 336 目 录 17.2.6 更改单个像素 ................. 338 17.3 项目:添加徽标 ...................... 339 第1 步:打开徽标图像 ............... 340 第2 步:遍历所有文件并打开 图像 ............................... 341 第3 步:调整图像的大小 ........... 341 第4 步:添加徽标,并保存 更改 ............................... 342 第5 步:类似程序的想法 ........... 343 17.4 在图像上绘画 .......................... 344 17.4.1 绘制形状 ......................... 344 17.4.2 绘制文本 ......................... 346 17.5 小结 ........................................... 347 17.6 习题 ........................................... 348 17.7 实践项目 .................................. 348 17.7.1 扩展和修正本章项目的 程序 ................................. 348 17.7.2 在硬盘上识别照片 文件夹 ............................. 349 17.7.3 定制的座位卡 ................. 350 第18 章 用GUI 自动化控制键盘和 鼠标 ............................................ 351 18.1 安装pyautogui 模块 ............... 351 18.2 走对路 ...................................... 352 18.2.1 通过注销关闭所有程序 .... 352 18.2.2 暂停和自动防故障装置 .... 352 18.3 控制鼠标移动 .......................... 353 18.3.1 移动鼠标 ......................... 354 18.3.2 获取鼠标位置 ................. 354 18.4 项目:“现在鼠标在 哪里?” .................................. 355 第1 步:导入模块 ....................... 355 第2 步:编写退出代码和无限 循环 ............................... 355 第3 步:获取并打印鼠标坐标 ...... 356 18.5 控制鼠标交互 ......................... 357 18.5.1 点击鼠标 ......................... 357 18.5.2 拖动鼠标 ......................... 357 18.5.3 滚动鼠标 ......................... 359 18.6 处理屏幕.................................. 360 18.6.1 获取屏幕快照 ................. 360 18.6.2 分析屏幕快照 ................. 360 18.7 项目:扩展mouseNow 程序 ... 361 18.8 图像识别.................................. 362 18.9 控制键盘.................................. 363 18.9.1 通过键盘发送一个 字符串 ............................. 363 18.9.2 键名 ................................. 364 18.9.3 按下和释放键盘 ............. 365 18.9.4 热键组合 ......................... 365 18.10 复习PyAutoGUI 的函数 ..... 366 18.11 项目:自动填表程序 ........... 367 第1 步:弄清楚步骤 ................... 368 第2 步:建立坐标 ....................... 368 第3 步:开始键入数据 ............... 370 第4 步:处理选择列表和单选 按钮 ............................... 371 第5 步:提交表单并等待 ........... 372 18.12 小结 ........................................ 372 18.13 习题 ........................................ 373 18.14 实践项目 ............................... 373 18.14.1 看起来很忙 ................... 373 18.14.2 即时通信机器人 ........... 373 18.14.3 玩游戏机器人指南 ....... 374 附录A 安装第三方模块 ............................ 375 附录B 运行程序 ........................................ 377 附录C 习题答案 ....................................... 381 第一部分 Python 编程基础 第 章 Python 基础 Python 编程语言有许多语法结构、标准库函数和交互式开 发环境功能。好在,你可以忽略大多数内容。你只需要学习部 分内容,就能编写一些方便的小程序。 但在动手之前,你必须学习一些基本编程概念。就像魔法 师培训,你可能认为这些概念既深奥又啰嗦,但有了一些知识 和实践,你就能像魔法师一样指挥你的计算机,完成难以置信 的事情。 本章有几个例子,我们鼓励你在交互式环境中输入它们。交互式环境让你每次执行 一条Python 指令,并立即显示结果。使用交互式环境对于了解基本Python 指令的行为 是很好的,所以你在阅读时要试一下。做过的事比仅仅读过的内容,更令人印象深刻。 1.1 在交互式环境中输入表达式 启动IDLE 就运行了交互式环境,这是和Python 一起安装的。在Windows 上, 打开“开始”菜单,选择“All Programs?Python 3.3”,然后选择“IDLE(Python GUI)”。 在OS X 上,选择“Applications?MacPython 3.3?IDLE”。在Ubuntu 上,打开新的 终端窗口并输入idle3。 1 2 Python 编程快速上手——让繁琐工作自动化 一个窗口会出现,包含>>>提示符,这就是交互式环境。在提示符后输入2 + 2, 让Python 做一些简单的算术。
2 + 2 4 IDLE 窗口现在应该显示下面这样的文本: Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:06:53) [MSC v.1600 64 bit (AMD64)] on win32 Type "copyright", "credits" or "license()" for more information. 2 + 2 4
在Python 中,2 + 2 称为“表达式”,它是语言中最基本的编程结构。表达式包 含“值”(例如2)和“操作符”(例如+),并且总是可以求值(也就是归约)为单 个值。这意味着在Python 代码中,所有使用表达式的地方,也可以使用一个值。 在前面的例子中,2 + 2 被求值为单个值4。没有操作符的单个值也被认为是一 个表达式,尽管它求值的结果就是它自己,像下面这样:
2 2 错误没关系! 如果程序包含计算机不能理解的代码,就会崩溃,这将导致Python 显示错 误信息。错误信息并不会破坏你的计算机,所以不要害怕犯错误。“崩溃”只是 意味着程序意外地停止执行。 如果你希望对一条错误信息了解更多,可以在网上查找这条信息的准确文本, 找到关于这个错误的更多内容。也可以查看http://nostarch.com/automatestuff/,这里 有常见的Python 错误信息和含义的列表。 Python 表达式中也可以使用大量其他操作符。例如,表 1-1 列出了Python 的所 有数学操作符。 表1-1 数学操作符,优先级从高到低 操作符 操作 例子 求值为 ** 指数 2 ** 3 8 % 取模/取余数 22 % 8 6 // 整除/商数取整 22 // 8 2 / 除法 22 / 8 2.75
- 乘法 3 * 5 15
- 减法 5 - 2 3
- 加法 2 + 2 4 数学操作符的操作顺序(也称为“优先级”)与数学中类似。*操作符首先求 值,接下来是、/、//和%操作符,从左到右。+和-操作符最后求值,也是从左到右。 第1 章 Python 基础 如果需要,可以用括号来改变通常的优先级。在交互式环境中输入下列表达式:
2 + 3 * 6 20 (2 + 3) * 6 30 48565878 * 578453 28093077826734 2 ** 8 256 23 / 7 3.2857142857142856 23 // 7 3 23 % 7 2 2 + 2 4 (5 - 1) * ((7 + 1) / (3 - 1)) 16.0 在每个例子中,作为程序员,你必须输入表达式,但Python 完成较难的工作, 将它求值为单个值。Python 将继续求值表达式的各个部分,直到它成为单个值,如 图1-1 所示。 图1-1 表达式求值将它归约为单个值 将操作符和值放在一起构成表达式的这些规则,是 Python 编程语言的基本部 分,就像帮助我们沟通的语法规则一样。下面是例子: This is a grammatically correct English sentence. This grammatically is sentence not English correct a. 第二行很难解释,因为它不符合英语的规则。类似地,如果你输入错误的 Python 指令,Python 也不能理解,就会显示出错误信息,像下面这样: 5 + File "", line 1 5 + ^ SyntaxError: invalid syntax 42 + 5 + * 2 File "", line 1 42 + 5 + * 2 Python 编程快速上手——让繁琐工作自动化 ^ SyntaxError: invalid syntax 你总是可以在交互式环境中输入一条指令,检查它是否能工作。不要担心会弄 坏计算机:最坏的情况就是Python 显示出错信息。专业的软件开发者在编写代码时, 常常会遇到错误信息。 1.2 整型、浮点型和字符串数据类型 记住,表达式是值和操作符的组合,它们可以通过求值成为单个值。“数据类 型”是一类值,每个值都只属于一种数据类型。表1-2 列出了Python 中最常见的数 据类型。例如,值-2 和30 属于“整型”值。整型(或int)数据类型表明值是整数。 带有小数点的数,如3.14,称为“浮点型”(或float)。请注意,尽管42 是一个整 型,但42.0 是一个浮点型。 表1-2 常见数据类型 数据类型 例子 整型 -2, -1, 0, 1, 2, 3, 4, 5 浮点型 -1.25, -1.0, - -0.5, 0.0, 0.5, 1.0, 1.25 字符串 'a', 'aa', 'aaa', 'Hello!', '11 cats' Python 程序也可以有文本值,称为“字符串”,或strs(发音为“stirs”)。总是 用单引号(')包围住字符串(例如'Hello'或'Goodbye cruel world!'),这样Python 就 知道字符串的开始和结束。甚至可以有没有字符的字符串,称为“空字符串”。第4 章更详细地解释了字符串。 如果你看到错误信息SyntaxError: EOL while scanning string literal,可能是忘记 了字符串末尾的单引号,如下面的例子所示: 'Hello world! SyntaxError: EOL while scanning string literal 1.3 字符串连接和复制 根据操作符之后的值的数据类型,操作符的含义可能会改变。例如,在操作两 个整型或浮点型值时,+是相加操作符。但是,在用于两个字符串时,它将字符串 连接起来,成为“字符串连接”操作符。在交互式环境中输入以下内容: 'Alice' + 'Bob' 'AliceBob' 该表达式求值为一个新字符串,包含了两个字符串的文本。但是,如果你对一 个字符串和一个整型值使用加操作符,Python 就不知道如何处理,它将显示一条错 第1 章 Python 基础 误信息。 'Alice' + 42 Traceback (most recent call last): File "<pyshell#26>", line 1, in 'Alice' + 42 TypeError: Can't convert 'int' object to str implicitly 错误信息Can't convert 'int' object to str implicitly 表示Python 认为,你试图将一 个整数连接到字符串'Alice'。代码必须显式地将整数转换为字符串,因为Python 不 能自动完成转换。(1.6 节“程序剖析”在讨论函数时,将解释数据类型转换。) 在用于两个整型或浮点型值时,操作符表示乘法。但操作符用于一个字符串 值和一个整型值时,它变成了“字符串复制”操作符。在交互式环境中输入一个字 符串乘一个数字,看看效果。 'Alice' * 5 'AliceAliceAliceAliceAlice' 该表达式求值为一个字符串,它将原来的字符串重复若干次,次数就是整型的 值。字符串复制是一个有用的技巧,但不像字符串连接那样常用。 *操作符只能用于两个数字(作为乘法),或一个字符串和一个整型(作为字符 串复制操作符)。否则,Python 将显示错误信息。 'Alice' * 'Bob' Traceback (most recent call last): File "<pyshell#32>", line 1, in 'Alice' * 'Bob' TypeError: can't multiply sequence by non-int of type 'str' 'Alice' * 5.0 Traceback (most recent call last): File "<pyshell#33>", line 1, in 'Alice' * 5.0 TypeError: can't multiply sequence by non-int of type 'float' Python 不理解这些表达式是有道理的:你不能把两个单词相乘,也很难将一个 任意字符串复制小数次。 1.4 在变量中保存值 “变量”就像计算机内存中的一个盒子,其中可以存放一个值。如果你的程序 稍后将用到一个已求值的表达式的结果,就可以将它保存在一个变量中。 1.4.1 赋值语句 用“赋值语句”将值保存在变量中。赋值语句包含一个变量名、一个等号(称 为赋值操作符),以及要存储的值。如果输入赋值语句spam = 42,那么名为spam 的变量将保存一个整型值42。 可以将变量看成一个带标签的盒子,值放在其中,如图1-2 所示。 Python 编程快速上手——让繁琐工作自动化 图1-2 spam = 42 就像是告诉程序“变量spam 现在有整数42 放在里面” 例如,在交互式环境中输入以下内容: ??>>> spam = 40 spam 40 eggs = 2 ??>>> spam + eggs 42 spam + eggs + spam 82 ??>>> spam = spam + 2 spam 42 第一次存入一个值,变量就被“初始化”(或创建)?。此后,可以在表达式中 使用它,以及其他变量和值?。如果变量被赋了一个新值,老值就被忘记了?。这 就是为什么在例子结束时,spam 求值为42,而不是40。这称为“覆写”该变量。 在交互式环境中输入以下代码,尝试覆写一个字符串: spam = 'Hello' spam 'Hello' spam = 'Goodbye' spam 'Goodbye' 就像图1-3 中的盒子,这个例子中的spam 变量保存了'Hello',直到你用'Goodbye' 替代它。 图1-3 如果一个新值赋给变量,老值就被遗忘了 第1 章 Python 基础 1.4.2 变量名 表1-3 中有一些合法变量名的例子。你可以给变量取任何名字,只要它遵守以 下3 条规则: 1.只能是一个词。 2.只能包含字母、数字和下划线。 3.不能以数字开头。 表1-3 有效和无效的变量名 有效的变量名 无效的变量名 balance current-balance(不允许中划线) currentBalance current balanc(不允许空格) current_balance 4account(不允许数字开头) spam 42(不允许数字开头) SPAM total
$um(不允许$ 这样的特殊字符) account4 'hello'(不允许'这样的特殊字符) 变量名是区分大小写的。这意味着,spam、 SPAM、Spam 和sPaM 是4 个不 同的变量。变量用小写字母开头是Python 的惯例。 本书的变量名使用了驼峰形式, 没有用下划线。也就是说, 变量名用 lookLikeThis,而不是looking_like_this。一些有经验的程序员可能会指出,官方的 Python 代码风格PEP 8,即应该使用下划线。我喜欢驼峰式,这没有错,并认为PEP 8 本身“愚蠢的一致性是头脑狭隘人士的心魔”: “一致地满足风格指南是重要的。但最重要的是,知道何时要不一致,因为有 时候风格指南就是不适用。如果有怀疑,请相信自己的最佳判断。” 好的变量名描述了它包含的数据。设想你搬到一间新屋子,搬家纸箱上标的都 是“东西”。你永远找不到任何东西!本书的例子和许多Python 的文档,使用spam、 eggs 和bacon 等变量名作为一般名称(受到Monty Python 的“Spam”短剧的影响), 但在你的程序中,具有描述性的名字有助于提高代码可读性。 1.5 第一个程序 虽然交互式环境对于一次运行一条 Python 指令很好,但要编写完整的Python 程序,就需要在文件编辑器中输入指令。“文件编辑器”类似于Notepad 或TextMate 这样的文本编辑器,它有一些针对输入源代码的特殊功能。要在IDLE 中打开文件 编辑器,请选择File?New Window。 出现的窗口中应该包含一个光标,等待你输入,但它与交互式环境不同。在交 Python 编程快速上手——让繁琐工作自动化 互式环境中,按下回车,就会执行Python 指令。文件编辑器允许输入许多指令,保 存为文件,并运行该程序。下面是区别这两者的方法: ??交互式环境窗口总是有>>>提示符。 ??文件编辑器窗口没有>>>提示符。 现在是创建第一个程序的时候了!在文件编辑器窗口打开后,输入以下内容: ??# This program says hello and asks for my name. ??print('Hello world!') print('What is your name?') # ask for their name ??myName = input() ??print('It is good to meet you, ' + myName) ??print('The length of your name is:') print(len(myName)) ??print('What is your age?') # ask for their age myAge = input() print('You will be ' + str(int(myAge) + 1) + ' in a year.') 在输入完源代码后保存它,这样就不必在每次启动IDLE 时重新输入。从文件 编辑器窗口顶部的菜单,选择File?Save As。在“Save As”窗口中,在输入框输入 hello.py,然后点击“Save”。 在输入程序时,应该过一段时间就保存你的程序。这样,如果计算机崩溃,或 者不小心退出了IDLE,也不会丢失代码。作为快捷键,可以在Windows 和Linux 上按Ctrl-S,在OS X 上按?-S,来保存文件。 在保存文件后,让我们来运行程序。选择Run?Run Module,或按下F5 键。 程序将在交互式环境窗口中运行,该窗口是首次启动IDLE 时出现的。记住,必须 在文件编辑器窗口中按F5,而不是在交互式环境窗口中。在程序要求输入时,输入 你的名字。在交互式环境中,程序输出应该看起来像这样: Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:06:53) [MSC v.1600 64 bit (AMD64)] on win32 Type "copyright", "credits" or "license()" for more information. ================================ RESTART ================================
Hello world! What is your name? Al It is good to meet you, Al The length of your name is: 2 What is your age? 4 You will be 5 in a year.
如果没有更多代码行要执行,Python 程序就会“中止”。也就是说,它停止运 行。(也可以说Python 程序“退出”了。) 可以通过点击窗口上部的X,关闭文件编辑器。要重新加载一个保存了的程序, 就在菜单中选择File?Open。现在请这样做,在出现的窗口中选择hello.py,并点 第1 章 Python 基础 击“Open”按钮。前面保存的程序hello.py 应该在文件编辑器窗口中打开。 1.6 程序剖析 新程序在文件编辑器中打开后,让我们快速看一看它用到的Python 指令,逐一 查看每行代码。 1.6.1 注释 下面这行称为“注释”。 ??# This program says hello and asks for my name. Python 会忽略注释,你可以用它们来写程序注解,或提醒自己代码试图完成的 事。这一行中,#标志之后的所有文本都是注释。 有时候,程序员在测试代码时,会在一行代码前面加上#,临时删除它。这称 为“注释掉”代码。在你想搞清楚为什么程序不工作时,这样做可能有用。稍后, 如果你准备还原这一行代码,可以去掉#。 Python 也会忽略注释之后的空行。在程序中,想加入空行时就可以加入。这会 让你的代码更容易阅读,就像书中的段落一样。 1.6.2 print()函数 print()函数将括号内的字符串显示在屏幕上。 ??print('Hello world!') print('What is your name?') # ask for their name 代码行print('Hello world!')表示“打印出字符串'Hello world!'的文本”。Python 执行到这行时,你告诉Python 调用print()函数,并将字符串“传递”给函数。传递 给函数的值称为“参数”。请注意,引号没有打印在屏幕上。它们只是表示字符串 的起止,不是字符串的一部分。 也可以用这个函数在屏幕上打印出空行,只要调用print()就可以了,括号内没 有任何东西。 在写函数名时,末尾的左右括号表明它是一个函数的名字。这就是为什么在本 书中你会看到print(),而不是print。第2 章更详细地探讨了函数。 1.6.3 input()函数 函数等待用户在键盘上输入一些文本,并按下回车键。 ??myName = input() 这个函数求值为一个字符串,即用户输入的文本。前面的代码行将这个字符串 Python 编程快速上手——让繁琐工作自动化 赋给变量myName。 你可以认为input()函数调用是一个表达式,它求值为用户输入的任何字符串。 如果用户输入'Al',那么该表达式就求值为myName = 'Al'。 1.6.4 打印用户的名字 接下来的print()调用,在括号间包含表达式'It is good to meet you, ' + myName。 ??print('It is good to meet you, ' + myName) 要记住,表达式总是可以求值为一个值。如果'Al'是上一行代码保存在myName 中的值,那么这个表达式就求值为'It is good to meet you, Al'。这个字符串传给print(), 它将输出到屏幕上。 1.6.5 len()函数 你可以向len()函数传递一个字符串(或包含字符串的变量),然后该函数求值 为一个整型值,即字符串中字符的个数。 ??print('The length of your name is:') print(len(myName)) 在交互式环境中输入以下内容试一试:
len('hello') 5 len('My very energetic monster just scarfed nachos.') 46 len('') 0 就像这些例子,len(myName)求值为一个整数。然后它被传递给print(),在屏幕 上显示。请注意,print()允许传入一个整型值或字符串。但如果在交互式环境中输 入以下内容,就会报错: print('I am ' + 29 + ' years old.') Traceback (most recent call last): File "<pyshell#6>", line 1, in print('I am ' + 29 + ' years old.') TypeError: Can't convert 'int' object to str implicitly 导致错误的原因不是print()函数,而是你试图传递给print()的___________表达式。如果在 交互式环境中单独输入这个表达式,也会得到同样的错误。 'I am ' + 29 + ' years old.' Traceback (most recent call last): File "<pyshell#7>", line 1, in 'I am ' + 29 + ' years old.' TypeError: Can't convert 'int' object to str implicitly 报错是因为,只能用+操作符加两个整数,或连接两个字符串。不能让一个整 第1 章 Python 基础 数和一个字符串相加,因为这不符合Python 的语法。可以使用字符串版本的整数, 修复这个错误。这在下一节中解释。 1.6.6 str()、int()和float()函数 如果想要连接一个整数(如29)和一个字符串,再传递给print(),就需要获得 值'29'。它是29 的字符串形式。str()函数可以传入一个整型值,并求值为它的字符 串形式,像下面这样: str(29) '29' print('I am ' + str(29) + ' years old.') I am 29 years old. 因为str(29)求值为’29’,所以表达式'I am ' + str(29) + ' years old.'求值为'I am ' + '29' + ' years old.',它又求值为'I am 29 years old.'。这就是传递给print()函数的值。 str()、int()和float()函数将分别求值为传入值的字符串、整数和浮点数形式。请 尝试用这些函数在交互式环境中转换一些值,看看会发生什么。 str(0) '0' str(-3.14) '-3.14' int('42') 42 int('-99') -99 int(1.25) 1 int(1.99) 1 float('3.14') 3.14 float(10) 10.0 前面的例子调用了str()、int()和float()函数,向它们传入其他数据类型的值,得 到了字符串、整型或浮点型的值。 如果想要将一个整数或浮点数与一个字符串连接,str()函数就很方便。如果你 有一些字符串值,希望将它们用于数学运算,int()函数也很有用。例如,input()函 数总是返回一个字符串,即便用户输入的是一个数字。在交互式环境中输入spam = input(),在它等待文本时输入101。 spam = input() 101 spam '101' 保存在spam 中的值不是整数101,而是字符串'101'。如果想要用spam 中的值进 行数学运算,那就用int()函数取得spam 的整数形式,然后将这个新值存在spam 中。 Python 编程快速上手——让繁琐工作自动化 spam = int(spam) spam 101 现在你应该能将spam 变量作为整数,而不是字符串使用。 spam * 10 / 5 202.0 请注意,如果你将一个不能求值为整数的值传递给int(),Python 将显示出错信息。 int('99.99') Traceback (most recent call last): File "<pyshell#18>", line 1, in int('99.99') ValueError: invalid literal for int() with base 10: '99.99' int('twelve') Traceback (most recent call last): File "<pyshell#19>", line 1, in int('twelve') ValueError: invalid literal for int() with base 10: 'twelve' 如果需要对浮点数进行取整运算,也可以用int()函数。 int(7.7) 7 int(7.7) + 1 8 在你的程序中,最后3 行使用了函数int()和str(),取得适当数据类型的值。 ??print('What is your age?') # ask for their age myAge = input() print('You will be ' + str(int(myAge) + 1) + ' in a year.') myAge 变量包含了input()函数返回的值。因为input()函数总是返回一个字符串 (即使用户输入的是数字),所以你可以使用int(myAge)返回字符串的整型值。这个 整型值随后在表达式int(myAge) + 1 中与1 相加。 相加的结果传递给str()函数:str(int(myAge) + 1)。然后,返回的字符串与字符 串'You will be '和' in a year.'连接,求值为一个更长的字符串。这个更长的字符串最 终传递给print(),在屏幕上显示。 假定用户输入字符串'4',保存在myAge 中。字符串'4'被转换为一个整型,所以 你可以对它加1。结果是5。str()函数将这个结果转化为字符串,这样你就可以将它 与第二个字符串'in a year.'连接,创建最终的消息。这些求值步骤如图1-4 所示。 文本和数字相等判断 虽然数字的字符串值被认为与整型值和浮点型值完全不同,但整型值可以与 浮点值相等。 42 == '42' False 42 == 42.0 第1 章 Python 基础 True 42.0 == 0042.000 True Python 进行这种区分,因为字符串是文本,而整型值和浮点型都是数字。 图1-4 如果4 保存在myAge 中,求值的步骤 1.7 小结 你可以用一个计算器来计算表达式,或在文本处理器中输入字符串连接。甚至 可以通过复制粘贴文本,很容易地实现字符串复制。但是表达式以及组成它们的值 (操作符、变量和函数调用),才是构成程序的基本构建块。一旦你知道如何处理这 些元素,就能够用Python 操作大量的数据。 最好是记住本章中介绍的不同类型的操作符(+、-、、/、//、%和**是数学操 作符,+和是字符串操作符),以及3 种数据类型(整型、浮点型和字符串)。 我们还介绍了几个不同的函数。print()和input()函数处理简单的文本输出(到屏幕) 和输入(通过键盘)。len()函数接受一个字符串,并求值为该字符串中字符的数目。 在下一章中,你将学习如何告诉Python 根据它拥有的值,明智地决定什么代码 要运行,什么代码要跳过,什么代码要重复。这称为“控制流”,它让你编写程序 来做出明智的决定。 1.8 习题 1.下面哪些是操作符,哪些是值?
/ Python 编程快速上手——让繁琐工作自动化 + 5 2.下面哪个是变量,哪个是字符串? spam 'spam' 3.说出3 种数据类型。 4.表达式由什么构成?所有表达式都做什么事? 5.本章介绍了赋值语句,如spam = 10。表达式和语句有什么区别? 6.下列语句运行后,变量bacon 的值是什么? bacon = 20 bacon + 1 7.下面两个表达式求值的结果是什么? 'spam' + 'spamspam' 'spam' * 3 8.为什么eggs 是有效的变量名,而100 是无效的? 9.哪3 个函数能分别取得一个值的整型、浮点型或字符串版本? 10.为什么这个表达式会导致错误?如何修复? 'I have eaten ' + 99 + ' burritos.' 附加题:在线查找len()函数的Python 文档。它在一个标题为“Built-in Functions” 的网页上。扫一眼Python 的其他函数的列表,查看round()函数的功能,在交互式 环境中使用它。 第 章 控 制 流 你已经知道了单条指令的基本知识。程序就是一系列指 令。但编程真正的力量不仅在于运行(或“执行”)一条接一 条的指令,就像周末的任务清单那样。根据表达式求值的结果, 程序可以决定跳过指令,重复指令,或从几条指令中选择一条 运行。实际上,你几乎永远不希望程序从第一行代码开始,简 单地执行每行代码,直到最后一行。“控制流语句”可以决定 在什么条件下执行哪些Python 语句。 这些控制流语句直接对应于流程图中的符号,所以在本章中,我将提供示例代 码的流程图。图2-1 展示了一张流程图,内容是如果下雨怎么办。按照箭头构成的 路径,从开始到结束。 在流程图中,通常有不止一种方法从开始走到结束。计算机程序中的代码行也 是这样。流程图用菱形表示这些分支节点,其他步骤用矩形表示。开始和结束步骤 用带圆角的矩形表示。 但在学习流程控制语句之前,首先要学习如何表示这些yes 和no 选项。同时你 也需要理解,如何将这些分支节点写成Python 代码。要做到这一点,让我们先看看 布尔值、比较操作符和布尔操作符。 2 2 Python 编程快速上手——让繁琐工作自动化 图2-1 一张流程图,告诉你如果下雨要做什么 2.1 布尔值 虽然整型、浮点型和字符串数据类型有无数种可能的值,但“布尔”数据类型 只有两种值:True 和False。Boolean(布尔)的首字母大写,因为这个数据类型是 根据数学家George Boole 命名的。在作为Python 代码输入时,布尔值True 和False 不像字符串,两边没有引号,它们总是以大写字母T 或F 开头,后面的字母小写。 在交互式环境中输入下面内容,其中有些指令是故意弄错的,它们将导致出错信息。 ??>>> spam = True
spam True ??>>> true Traceback (most recent call last): File "<pyshell#2>", line 1, in true NameError: name 'true' is not defined ??>>> True = 2 + 2 SyntaxError: assignment to keyword 像其他值一样,布尔值也用在表达式中,并且可以保存在变量中?。如果大小 写不正确?,或者试图使用True 和False 作为变量名?,Python 就会给出错误信息。 第2 章 控制流 2.2 比较操作符 “比较操作符”比较两个值,求值为一个布尔值。表2-1 列出了比较操作符。 表2-1 比较操作符 操作符 含义 == 等于 != 不等于 < 小于 大于 <= 小于等于 = 大于等于 这些操作符根据给它们提供的值,求值为True 或False。现在让我们尝试一些 操作符,从==和!=开始。 42 == 42 True 42 == 99 False 2 != 3 True 2 != 2 False 如果两边的值一样,==(等于)求值为True。如果两边的值不同,!=(不等于) 求值为True。==和!=操作符实际上可以用于所有数据类型的值。 'hello' == 'hello' True 'hello' == 'Hello' False 'dog' != 'cat' True True == True True True != False True 42 == 42.0 True ??>>> 42 == '42' False 请注意,整型或浮点型的值永远不会与字符串相等。表达式42 == '42'?求值为 False 是因为,Python 认为整数42 与字符串'42'不同。 另一方面,<、>、<=和>=操作符仅用于整型和浮点型值。 42 < 100 True 42 > 100 False Python 编程快速上手——让繁琐工作自动化 42 < 42 False eggCount = 42 ??>>> eggCount <= 42 True myAge = 29 ??>>> myAge >= 10 True 操作符的区别 你可能已经注意到,==操作符(等于)有两个等号,而=操作符(赋值)只 有一个等号。这两个操作符很容易混淆。只要记住: ??==操作符(等于)问两个值是否彼此相同。 ??=操作符(赋值)将右边的值放到左边的变量中。 为了记住谁是谁,请注意==操作符(等于)包含两个字符,就像!=操作符(不 等于)包含两个字符一样。 你会经常用比较操作符比较一个变量和另外某个值。就像在例子eggCount <= 42?和myAge >= 10?中一样(毕竟,除了在代码中输入'dog' != 'cat'以外,你本来也 可以直接输入True)。稍后,在学习控制流语句时,你会看到更多的例子。 2.3 布尔操作符 3 个布尔操作符(and、or 和not)用于比较布尔值。像比较操作符一样,它们 将这些表达式求值为一个布尔值。让我们仔细看看这些操作符,从and 操作符开始。 2.3.1 二元布尔操作符 and 和or 操作符总是接受两个布尔值(或表达式),所以它们被认为是“二元” 操作符。如果两个布尔值都为True,and 操作符就将表达式求值为True,否则求值 为False。在交互式环境中输入某个使用and 的表达式,看看效果。 True and True True True and False False “真值表”显示了布尔操作符的所有可能结果。表2-2 是操作符and 的真值表。 表2-2 and 操作符的真值表 表达式 求值为 True and True True True and False False False and True False False and False False 第2 章 控制流 另一方面,只要有一个布尔值为真,or 操作符就将表达式求值为True。如果都 是False,所求值为False。 False or True True False or False False 可以在or 操作符的真值表中看到每一种可能的结果,如表2-3 所示。 表2-3 or 操作符的真值表 表达式 求值为 True or True True True or False True False or True True False or False False 2.3.2 not 操作符 和and 和or 不同,not 操作符只作用于一个布尔值(或表达式)。not 操作符求 值为相反的布尔值。 not True False ??>>> not not not not True True 就像在说话和写作中使用双重否定,你可以嵌套not 操作符?,虽然在真正的 程序中并不经常这样做。表2-4 展示了not 的真值表。 表2-4 not 操作符的真值表 表达式 求值为 not True False not False True 2.4 混合布尔和比较操作符 既然比较操作符求值为布尔值,就可以和布尔操作符一起,在表达式中使用。 回忆一下,and、or 和not 操作符称为布尔操作符是因为,它们总是操作于布尔 值。虽然像4 < 5 这样的表达式不是布尔值,但可以求值为布尔值。在交互式环境 中,尝试输入一些使用比较操作符的布尔表达式。 (4 < 5) and (5 < 6) True (4 < 5) and (9 < 6) False Python 编程快速上手——让繁琐工作自动化 (1 == 2) or (2 == 2) True 计算机将先求值左边的表达式,然后再求值右边的表达式。知道两个布尔值后, 它又将整个表达式再求值为一个布尔值。你可以认为计算机求值(4 < 5)和(5 < 6)的 过程,如图2-2 所示。 图2-2 (4 < 5)和 (5 < 6) 求值为True 的过程 也可以在一个表达式中使用多个布尔操作符,与比较操作符一起使用。 2 + 2 == 4 and not 2 + 2 == 5 and 2 * 2 == 2 + 2 True 和算术操作符一样,布尔操作符也有操作顺序。在所有算术和比较操作符求值 后,Python 先求值not 操作符,然后是and 操作符,然后是or 操作符。 2.5 控制流的元素 控制流语句的开始部分通常是“条件”,接下来是一个代码块,称为“子句”。 在开始学习具体的Python 控制流语句之前,我将介绍条件和代码块。 2.5.1 条件 你前面看到的布尔表达式可以看成是条件,它和表达式是一回事。“条件”只是在 控制流语句的上下文中更具体的名称。条件总是求值为一个布尔值,True 或False。控制 流语句根据条件是True 还是False,来决定做什么。几乎所有的控制流语句都使用条件。 2.5.2 代码块 一些代码行可以作为一组,放在“代码块”中。可以根据代码行的缩进,知道 代码块的开始和结束。代码块有3 条规则。 1.缩进增加时,代码块开始。 2.代码块可以包含其他代码块。 3.缩进减少为零,或减少为外面包围代码块的缩进,代码块就结束了。 看一些有缩进的代码,更容易理解代码块。所以让我们在一小段游戏程序中, 第2 章 控制流 寻找代码块,如下所示: if name == 'Mary': ??print('Hello Mary') if password == 'swordfish': ??print('Access granted.') else: ??print('Wrong password.') 第一个代码块?开始于代码行print('Hello Mary'),并且包含后面所有的行。在 这个代码块中有另一个代码块?,它只有一行代码:print('Access Granted.')。第三个 代码块?也只有一行:print('Wrong password.')。 2.6 程序执行 在第1 章的hello.py 程序中,Python 开始执行程序顶部的指令,然后一条接一 条往下执行。“程序执行”(或简称“执行”)这一术语是指当前被执行的指令。如 果将源代码打印在纸上,在它执行时用手指指着每一行代码,你可以认为手指就是 程序执行。 但是,并非所有的程序都是从上至下简单地执行。如果用手指追踪一个带 有控制流语句的程序,可能会发现手指会根据条件跳过源代码,有可能跳过整 个子句。 2.7 控制流语句 现在,让我们来看最重要的控制流部分:语句本身。语句代表了在图2-1 的流 程图中看到的菱形,它们是程序将做出的实际决定。 2.7.1 if 语句 最常见的控制流语句是if 语句。if 语句的子句(也就是紧跟if 语句的语句块), 将在语句的条件为True 时执行。如果条件为False,子句将跳过。 在英文中,if 语句念起来可能是:“如果条件为真,执行子句中的代码。”在Python 中,if 语句包含以下部分: ??if 关键字; ??条件(即求值为True 或False 的表达式); ??冒号; ??在下一行开始,缩进的代码块(称为if 子句)。 例如,假定有一些代码,检查某人的名字是否为Alice(假设此前曾为name 赋值)。 Python 编程快速上手——让繁琐工作自动化 if name == 'Alice': print('Hi, Alice.') 所有控制流语句都以冒号结尾,后面跟着一个新的代码块(子句)。语句的if 子句是代码块,包含print('Hi, Alice.')。图2-3 展示了这段代码的流程图。 图2-3 if 语句的流程图 2.7.2 else 语句 if 子句后面有时候也可以跟着else 语句。只有if 语句的条件为False 时,else 子句才会执行。在英语中,else 语句读起来可能是:“如果条件为真,执行这段 代码。否则,执行那段代码”。else 语句不包含条件,在代码中,else 语句中包 含下面部分: ??else 关键字; ??冒号; ??在下一行开始,缩进的代码块(称为else 子句)。 回到Alice 的例子,我们来看看使用else 语句的一些代码,在名字不是Alice 时,提供不一样的问候。 if name == 'Alice': print('Hi, Alice.') else: print('Hello, stranger.') 图2-4 展示了这段代码的流程图。 第2 章 控制流 图2-4 else 语句的流程图 2.7.3 elif 语句 虽然只有if 或else 子句会被执行,但有时候可能你希望,“许多”可能的子句 中有一个被执行。elif 语句是“否则如果”,总是跟在if 或另一条elif 语句后面。它 提供了另一个条件,仅在前面的条件为False 时才检查该条件。在代码中,elif 语句 总是包含以下部分: ??elif 关键字; ??条件(即求值为True 或False 的表达式); ??冒号; ??在下一行开始,缩进的代码块(称为elif 子句)。 让我们在名字检查程序中添加elif,看看这个语句的效果。 if name == 'Alice': print('Hi, Alice.') elif age < 12: print('You are not Alice, kiddo.') 这一次,检查此人的年龄。如果比 12 岁小,就告诉他一些不同的东西。可以 在图2-5 中看到这段代码的流程图。 如果age < 12 为True 并且name == 'Alice'为False,elif 子句就会执行。但是, 如果两个条件都为False,那么两个子句都会跳过。“不能”保证至少有一个子句会 被执行。如果有一系列的elif 语句,仅有一条或零条子句会被执行。一旦一个语句 的条件为True,剩下的elif 子句会自动跳过。例如,打开一个新的文件编辑器窗口, Python 编程快速上手——让繁琐工作自动化 输入以下代码,保存为vampire.py。 if name == 'Alice': print('Hi, Alice.') elif age < 12: print('You are not Alice, kiddo.') elif age > 2000: print('Unlike you, Alice is not an undead, immortal vampire.') elif age > 100: print('You are not Alice, grannie.') 图2-5 elif 语句的流程图 这里,我添加了另外两条elif 语句,让名字检查程序根据age 的不同答案而发 出问候。图2-6 展示了这段代码的流程图。 但是,elif 语句的次序确实重要。让我们重新排序,引入一个缺陷。回忆一下, 一旦找到一个True 条件,剩余的子句就会自动跳过。所以如果交换vampire.py 中的 一些子句,就会遇到问题。像下面这样改变代码,将它保存为vampire2.py。 if name == 'Alice': print('Hi, Alice.') elif age < 12: print('You are not Alice, kiddo.') 第2 章 控制流 ??elif age > 100: print('You are not Alice, grannie.') elif age > 2000: print('Unlike you, Alice is not an undead, immortal vampire.') 图2-6 vampire.py 程序中多重elif 语句的流程图 假设在这段代码执行之前,age 变量的值是3000。你可能预计代码会打印出字 符串'Unlike you, Alice is not an undead, immortal vampire.'。但是,因为age > 100 条 件为真(毕竟3000 大于100)?,字符串'You are not Alice, grannie.'被打印出来,剩 Python 编程快速上手——让繁琐工作自动化 下的语句自动跳过。别忘了,最多只有一个子句会执行,对于elif 语句,次序是很 重要的。 图2-7 展示了前面代码的流程图。请注意,菱形age > 100 和age > 2000 交换了 位置。 图2-7 vampire2.py 程序的流程图。打叉的路径在逻辑上永远不会发生, 因为如果age 大于2000,它就已经大于100 了 第2 章 控制流 你可以选择在最后的elif 语句后面加上else 语句。在这种情况下,保证至少一 个子句(且只有一个)会执行。如果每个if 和elif 语句中的条件都为False,就执行 else 子句。例如,让我们使用if、elif 和else 子句重新编写Alicee 程序。 if name == 'Alice': print('Hi, Alice.') elif age < 12: print('You are not Alice, kiddo.') else: print('You are neither Alice nor a little kid.') 图2-8 展示了这段新代码的流程图,我们将它保存为littleKid.py。 图2-8 前面littleKid.py 程序的流程图 在英语中,这类控制流结构会使得:“如果第一个条件为真,做这个。否则, 如果第二个条件为真,做那个。否则,做另外的事。”如果你同时使用这3 个语句, Python 编程快速上手——让繁琐工作自动化 要记住这些次序规则,避免图2-7 中那样的缺陷。首先,总是只有一个if 语句。所 有需要的elif 语句都应该跟在if 语句之后。其次,如果希望确保至少一条子句被执 行,在最后加上else 语句。 2.7.4 while 循环语句 利用while 语句,可以让一个代码块一遍又一遍的执行。只要while 语句的条 件为True,while 子句中的代码就会执行。在代码中,while 语句总是包含下面几 部分: ??关键字; ??条件(求值为True 或False 的表达式); ??冒号; ??从新行开始,缩进的代码块(称为while 子句)。 可以看到,while 语句看起来和if 语句类似。不同之处是它们的行为。if 子句 结束时,程序继续执行if 语句之后的语句。但在while 子句结束时,程序执行跳回 到while 语句开始处。while 子句常被称为“while 循环”,或就是“循环”。 让我们来看一个if 语句和一个while 循环。它们使用同样的条件,并基于该条 件做出同样的动作。下面是if 语句的代码: spam = 0 if spam < 5: print('Hello, world.') spam = spam + 1 下面是while 语句的代码: spam = 0 while spam < 5: print('Hello, world.') spam = spam + 1 这些语句类似,if 和while 都检查spam 的值,如果它小于5,就打印一条消息。 但如果运行这两段代码,它们各自的表现非常不同。对于if 语句,输出就是"Hello, world."。但对于while 语句,输出是"Hello, world."重复了5 次!看一看这两段代码 的流程图,图2-9 和2-10,找一找原因。 带有if 语句的代码检查条件,如果条件为True,就打印一次"Hello, world."。带 有while 循环的代码则不同,会打印5 次。打印5 次后停下来是因为,在每次循环 迭代末尾,spam 中的整数都增加1。这意味着循环将执行5 次,然后spam < 5 变为 False。 在while 循环中,条件总是在每次“迭代”开始时检查(也就是每次循环执行 时)。如果条件为True,子句就会执行,然后,再次检查条件。当条件第一次为False 时,while 子句就跳过。 第2 章 控制流 图2-9 if 语句代码的流程图 图2-10 while 语句代码的流程图 2.7.5 恼人的循环 这里有一个小例子,它不停地要求你输入“your name”(就是这个字符串,而 Python 编程快速上手——让繁琐工作自动化 不是你的名字)。选择File?New Window,打开一个新的文件编辑器窗口,输入以 下代码,将文件保存为yourName.py: ??name = '' ??while name != 'your name': print('Please type your name.') ??name = input() ??print('Thank you!') 首先,程序将变量name?设置为一个空字符串。这样,条件name != 'your name' 就会求值为True,程序就会进入while 循环的子句?。 这个子句内的代码要求用户输入他们的名字,然后赋给name 变量?。因为这是语 句块的最后一行,所以执行就回到while 循环的开始,重新对条件求值。如果name 中的 值“不等于”字符串'your name',那么条件就为True,执行将再次进入while 子句。 但如果用户输入your name,while 循环的条件就变成'your name' != 'your name', 它求值为False。条件现在是False,程序就不会再次进入while 循环子句,而是跳 过它,继续执行程序后面的部分?。图2-11 展示了yourName.py 程序的流程图。 图2-11 yourName.py 程序的流程图 第2 章 控制流 现在,让我们来看看yourName.py程序的效果。按F5 键运行它,输几次your name 之外的东西,然后再提供程序想要的输入。 Please type your name. Al Please type your name. Albert Please type your name. %#@#%*(^&!!! Please type your name. your name Thank you! 如果永不输入your name,那么循环的条件就永远为False,程序将永远问 下去。这里,input()调用让用户输入正确的字符串,以便让程序继续。在其他 程序,条件可能永远没有实际变化,这可能会出问题。让我们来看看如何跳出 循环。 2.7.6 break 语句 有一个捷径,让执行提前跳出while 循环子句。如果执行遇到break 语句,就 会马上退出while 循环子句。在代码中,break 语句仅包含break 关键字。 非常简单,对吗?这里有一个程序,和前面的程序做一样的事情,但使用了break 语句来跳出循环。输入以下代码,将文件保存为yourName2.py: ??while True: print('Please type your name.') ??name = input() ??if name == 'your name': ??break ??print('Thank you!') 第一行?创建了一个“无限循环”,它是一个条件总是为True 的while 循环。(表 达式True 总是求值为True。)程序执行将总是进入循环,只有遇到break 语句执行 时才会退出(“永远不”退出的无限循环是一个常见的编程缺陷)。 像以前一样,程序要求用户输入your name?。但是现在,虽然执行仍然在 while 循环内,但有一个if 语句会被执行?,检查name 是否等于your name。 如果条件为True,break 语句就会运行?,执行就会跳出循环,转到print('Thank you!') ?。否则,包含break 语句的if 语句子句就会跳过,让执行到达while 循 环的末尾。此时,程序执行跳回到while 语句的开始?,重新检查条件。因为 条件是True,所以执行进入循环,再次要求用户输入your name。这个程序的流 程图参见图2-12。 运行yourName2.py,输入你为yourName.py 程序输入的同样文本。重写的程序 应该和原来的程序反应相同。 Python 编程快速上手——让繁琐工作自动化 图2-12 带有无限循环的程序的流程图。注意打叉路径在逻辑上 永远不会发生,因为循环条件总是为True 2.7.7 continue 语句 像break 语句一样,continue 语句用于循环内部。如果程序执行遇到continue 语句,就会马上跳回到循环开始处,重新对循环条件求值(这也是执行到达循环末 尾时发生的事情)。 第2 章 控制流 让我们用continue 写一个程序,要求输入名字和口令。在一个新的文件编辑窗 口中输入以下代码,将程序保存为swordfish.py。 while True: print('Who are you?') name = input() ??if name != 'Joe': ??continue print('Hello, Joe. What is the password? (It is a fish.)') ??password = input() if password == 'swordfish': ??break ??print('Access granted.') 如果用户输入的名字不是Joe?,continue 语句?将导致程序执行跳回到循环开 始处。再次对条件求值时,执行总是进入循环,因为条件就是True。如果执行通过 了if 语句,用户就被要求输入口令?。如果输入的口令是swordfish,break 语句运 行?,执行跳出while 循环,打印Access granted?。否则,执行继续到while 循环 的末尾,又跳回到循环的开始。这个程序的流程图参见图2-13。 陷在无限循环中? 如果你运行一个有缺陷的程序,导致陷在一个无限循环中,那么请按Ctrl-C。 这将向程序发送KeyboardInterrupt 错误,导致它立即停止。试一下,在文件编辑 器中创建一个简单的无限循环,将它保存为infiniteloop.py。 while True: print('Hello world!') 如果运行这个程序,它将永远在屏幕上打印Hello world!因为while 语句的 条件总是True。在IDLE 的交互式环境窗口中,只有两种办法停止这个程序:按 下Ctrl-C 或从菜单中选择Shell?Restart Shell。如果你希望马上停止程序,即使 它不是陷在一个无限循环中,Ctrl-C 也是很方便的。 运行这个程序,提供一些输入。只有你声称是Joe,它才会要求输入口令。一 旦输入了正确的口令,它就会退出。 Who are you? I'm fine, thanks. Who are you? Who are you? Joe Hello, Joe. What is the password? (It is a fish.) Mary Who are you? Joe Hello, Joe. What is the password? (It is a fish.) swordfish Access granted. Python 编程快速上手——让繁琐工作自动化 图2-13 swordfish.py 的流程图。打叉的路径在逻辑上永远不会执行,因为循环条件总是True 第2 章 控制流 2.7.8 for 循环和range()函数 在条件为True 时,while 循环就会继续循环(这是它的名称的由来)。但如果你 想让一个代码块执行固定次数,该怎么办?可以通过for 循环语句和range()函数来 实现。 “类真”和“类假”的值 其他数据类型中的某些值,条件认为它们等价于True 和False。在用于条件 时,0、0.0 和' '(空字符串)被认为是False,其他值被认为是True。例如,请看 下面的程序: name = '' while not name:? print('Enter your name:') name = input() print('How many guests will you have?') numOfGuests = int(input()) if numOfGuests:? print('Be sure to have enough room for all your guests.')? print('Done') 如果用户输入一个空字符串给name,那么while 语句的条件就会是True ?, 程序继续要求输入名字。如果numOfGuests 不是0 ?,那么条件就被认为是True, 程序就会为用户打印一条提醒信息?。 可以用not name != ' '代替not name,用numOfGuests != 0 代替numOfGuests, 但使用类真和类假的值会让代码更容易阅读。 在代码中,for 语句看起来像for i in range(5):这样,总是包含以下部分: ??for 关键字; ??一个变量名; ??in 关键字; ??调用range()方法,最多传入3 个参数; ??冒号; ??从下一行开始,缩退的代码块(称为for 子句)。 让我们创建一个新的程序,名为fiveTimes.py,看看for 循环的效果。 print('My name is') for i in range(5): print('Jimmy Five Times (' + str(i) + ')') for 循环子句中的代码运行了5 次。第一次运行时,变量i 被设为0。子句中的 print()调用将打印出Jimmy Five Times (0)。Python 完成for 循环子句内所有代码的 一次迭代之后,执行将回到循环的顶部,for 语句让i 增加1。这就是为什么range(5) 导致子句的5 次迭代,i 分别被设置为0、1、2、3、4。变量i 将递增到(但不包括) 传递给range()函数的整数。图2-14 展示了fiveTimes.py 程序的流程图。 Python 编程快速上手——让繁琐工作自动化 图2-14 fiveTimes.py 的流程图 运行这个程序时,它将打印5 次Jimmy Five Times 和i 的值,然后离开for 循环。 My name is Jimmy Five Times (0) Jimmy Five Times (1) Jimmy Five Times (2) Jimmy Five Times (3) Jimmy Five Times (4) 也可以在循环中使用continue 语句。continue 语句将让for 循环变量继续下一个 值,就像程序执行已经到达循环的末尾并返回开始一样。实际上,只能在while 和 for 循环内部使用continue 和break 语句。如果试图在别处使用这些语句,Python 将 报错。 作为for 循环的另一个例子,请考虑数学家高斯的故事。当高斯还是一个小孩 时,老师想给全班同学布置很多计算作业。老师让他们从0 加到100。高斯想到了 一个聪明办法,在几秒钟内算出了答案,但你可以用for 循环写一个Python 程序, 替你完成计算。 ??total = 0 ??for num in range(101): ??total = total + num ??print(total) 结果应该是5050。程序刚开始时,total 变量被设为0。然后for 循环执行100 次total = total + num。当循环完成100 次迭代时,0 到100 的每个整数都加给了total。 第2 章 控制流 这时,total 被打印到屏幕上。即使在最慢的计算机上,这个程序也不用1 秒钟就能 完成计算。 (小高斯想到,有50 对数加起来是100:1 + 99, 2 + 98, 3 + 97……直到49 + 51。 因为50 × 100 是5000,再加上中间的50,所以0 到100 的所有数之和是5050。 聪明的孩子!) 2.7.9 等价的while 循环 实际上可以用while 循环来做和for 循环同样的事,for 循环只是更简洁。让我 们用与for 循环等价的while 循环,重写fiveTimes.py。 print('My name is') i = 0 while i < 5: print('Jimmy Five Times (' + str(i) + ')') i = i + 1 运行这个程序,输出应该和使用for 循环的fiveTimes.py 程序一样。 2.7.10 range()的开始、停止和步长参数 某些函数可以用多个参数调用,参数之间用逗号分开,range()就是其中之一。 这让你能够改变传递给range()的整数,实现各种整数序列,包括从0 以外的值开始。 for i in range(12, 16): print(i) 第一个参数是for 循环变量开始的值,第二个参数是上限,但不包含它,也就 是循环停止的数字。 12 13 14 15 range()函数也可以有第三个参数。前两个参数分别是起始值和终止值,第三个 参数是“步长”。步长是每次迭代后循环变量增加的值。 for i in range(0, 10, 2): print(i) 所以调用range(0, 10, 2)将从0 数到8,间隔为2。 0 2 4 6 8 在为for 循环生成序列数据方面,range()函数很灵活。举例来说,甚至可以用 负数作为步长参数,让循环计数逐渐减少,而不是增加。 Python 编程快速上手——让繁琐工作自动化 for i in range(5, -1, -1): print(i) 运行一个for 循环,用range(5, -1, -1)来打印i,结果将从5 降至0。 5 4 3 2 1 0 2.8 导入模块 Python 程序可以调用一组基本的函数,这称为“内建函数”,包括你见到过的 print()、input()和len()函数。Python 也包括一组模块,称为“标准库”。每个模块都 是一个Python 程序,包含一组相关的函数,可以嵌入你的程序之中。例如,math 模块有数学运算相关的函数,random 模块有随机数相关的函数,等等。 在开始使用一个模块中的函数之前,必须用import 语句导入该模块。在代码中, import 语句包含以下部分: ??import 关键字; ??模块的名称; ??可选的更多模块名称,之间用逗号隔开。 在导入一个模块后,就可以使用该模块中所有很酷的函数。让我们试一试 random 模块,它让我们能使用random.ranint()函数。 在文件编辑器中输入以下代码,保存为printRandom.py: import random for i in range(5): print(random.randint(1, 10)) 如果运行这个程序,输出看起来可能像这样: 4 1 8 4 1 random.randint()函数调用求值为传递给它的两个整数之间的一个随机整数。因 为randint()属于random 模块,必须在函数名称之前先加上random.,告诉python 在 random 模块中寻找这个函数。 下面是import 语句的例子,它导入了4 个不同的模块: import random, sys, os, math 现在我们可以使用这4 个模块中的所有函数。本书后面我们将学习更多的相关内容。 第2 章 控制流 from import 语句 import 语句的另一种形式包括from 关键字,之后是模块名称,import 关键字和 一个星号,例如from random import *。 使用这种形式的import 语句,调用random 模块中的函数时不需要random.前缀。 但是,使用完整的名称会让代码更可读,所以最好是使用普通形式的import 语句。 2.9 用sys.exit()提前结束程序 要介绍的最后一个控制流概念,是如何终止程序。当程序执行到指令的底部时, 总是会终止。但是,通过调用sys.exit()函数,可以让程序终止或退出。因为这个函 数在sys 模块中,所以必须先导入sys,才能使用它。 打开一个新的文件编辑器窗口,输入以下代码。保存为exitExample.py: import sys while True: print('Type exit to exit.') response = input() if response == 'exit': sys.exit() print('You typed ' + response + '.') 在IDLE 中运行这个程序。该程序有一个无限循环,里面没有break 语句。结 束该程序的唯一方式,就是用户输入exit,导致sys.exit()被调用。如果response 等 于exit,程序就会中止。因为response 变量由input()函数赋值,所以用户必须输入 exit,才能停止该程序。 2.10 小结 通过使用求值为True 或False 的表达式(也称为条件),你可以编写程序来决 定哪些代码执行,哪些代码跳过。可以在循环中一遍又一遍地执行代码,只要某个 条件求值为True。如果需要跳出循环或回到开始处,break 和continue 语句很有用。 这些控制流语句让你写出非常聪明的程序。还有另一种类型的控制流,你可以 通过编写自己的函数来实现。这是下一章的主题。 2.11 习题 1.布尔数据类型的两个值是什么?如何拼写? 2.3 个布尔操作符是什么? Python 编程快速上手——让繁琐工作自动化 3.写出每个布尔操作符的真值表(也就是操作数的每种可能组合,以及操作 的结果)。 4.以下表达式求值的结果是什么? (5 > 4) and (3 == 5) not (5 > 4) (5 > 4) or (3 == 5) not ((5 > 4) or (3 == 5)) (True and True) and (True == False) (not False) or (not True) 5.6 个比较操作符是什么? 6.等于操作符和赋值操作符的区别是什么? 7.解释什么是条件,可以在哪里使用条件。 8.识别这段代码中的3 个语句块: spam = 0 if spam == 10: print('eggs') if spam > 5: print('bacon') else: print('ham') print('spam') print('spam') 9.编写代码,如果变量spam 中存放1,就打印Hello,如果变量中存放2,就 打印Howdy,如果变量中存放其他值,就打印Greetings! 10.如果程序陷在一个无限循环中,你可以按什么键? 11.break 和continue 之间的区别是什么? 12.在for 循环中,range(10)、range(0, 10)和range(0, 10, 1)之间的区别是什么? 13.编写一小段程序,利用for 循环,打印出从1 到10 的数字。然后利用while 循环,编写一个等价的程序,打印出从1 到10 的数字。 14.如果在名为spam 的模块中,有一个名为bacon()的函数,那么在导入spam 模块后,如何调用它? 附加题:在因特网上查找round()和abs()函数,弄清楚它们的作用。在交互式 环境中尝试使用它们。 第 章 函 数 从前面的章节中,你已经熟悉了print()、input()和len() 函数。Python 提供了这样一些内建函数,但你也可以编写自 己的函数。“函数”就像一个程序内的小程序。 为了更好地理解函数的工作原理,让我们来创建一 个函数。在文件编辑器中输入下面的程序, 保存为 helloFunc.py: ??def hello(): ??print('Howdy!') print('Howdy!!!') print('Hello there.') ??hello() hello() hello() 第一行是def 语句?,它定义了一个名为hello()的函数。def 语句之后的代码块 是函数体?。这段代码在函数调用时执行,而不是在函数第一次定义时执行。 函数之后的hello()语句行是函数调用?。在代码中,函数调用就是函数名后跟 上括号,也许在括号之间有一些参数。如果程序执行遇到这些调用,就会跳到函数 的第一行,开始执行那里的代码。如果执行到达函数的末尾,就回到调用函数的那 3 2 Python 编程快速上手——让繁琐工作自动化 行,继续像以前一样向下执行代码。 因为这个程序调用了3 次hello()函数,所以函数中的代码就执行了3 次。在运 行这个程序时,输出看起来像这样: Howdy! Howdy!!! Hello there. Howdy! Howdy!!! Hello there. Howdy! Howdy!!! Hello there. 函数的一个主要目的就是将需要多次执行的代码放在一起。如果没有函数定 义,你可能每次都需要复制粘贴这些代码,程序看起来可能会像这样: print('Howdy!') print('Howdy!!!') print('Hello there.') print('Howdy!') print('Howdy!!!') print('Hello there.') print('Howdy!') print('Howdy!!!') print('Hello there.') 一般来说,我们总是希望避免复制代码,因为如果一旦决定要更新代码(比如 说,发现了一个缺陷要修复),就必须记住要修改所有复制的代码。 随着你获得更多的编程经验,常常会发现自己在为代码“消除重复”,即去除 一些重复或复制的代码。消除重复能够使程序更短、更易读、更容易更新。 3.1 def 语句和参数 如果调用print()或len()函数,你会传入一些值,放在括号之间,在这里称为“参 数”。也可以自己定义接收参数的函数。在文件编辑器中输入这个例子,将它保存 为helloFunc2.py: ??def hello(name): ??print('Hello ' + name) ??hello('Alice') hello('Bob') 如果运行这个程序,输出看起来像这样: Hello Alice Hello Bob 在这个程序的hello()函数定义中,有一个名为name 的变元?。“变元”是一个 变量,当函数被调用时,参数就存放在其中。hello()函数第一次被调用时,使用的 第3 章 函数 参数是'Alice'?。程序执行进入该函数,变量name 自动设为'Alice',就是被print() 语句打印出的内容?。 关于变元有一件特殊的事情值得注意:保存在变元中的值,在函数返回后就丢 失了。例如前面的程序,如果你在hello('Bob')之后添加print(name),程序会报 NameError,因为没有名为name 的变量。在函数调用hello('Bob')返回后,这个变量 被销毁了,所以print(name)会引用一个不存在的变量name。 这类似于程序结束时,程序中的变量会丢弃。在本章稍后,当我们探讨函数的 局部作用域时,我会进一步分析为什么会这样。 3.2 返回值和return 语句 如果调用len()函数,并向它传入像'Hello'这样的参数,函数调用就求值为整数5。 这是传入的字符串的长度。一般来说,函数调用求值的结果,称为函数的“返回值”。 用def 语句创建函数时,可以用return 语句指定应该返回什么值。return 语句包 含以下部分: ??return 关键字; ??函数应该返回的值或表达式。 如果在return 语句中使用了表达式,返回值就是该表达式求值的结果。例如, 下面的程序定义了一个函数,它根据传入的数字参数,返回一个不同的字符串。在 文件编辑器中输入以下代码,并保存为magic8Ball.py: ??import random ??def getAnswer(answerNumber): ??if answerNumber == 1: return 'It is certain' elif answerNumber == 2: return 'It is decidedly so' elif answerNumber == 3: return 'Yes' elif answerNumber == 4: return 'Reply hazy try again' elif answerNumber == 5: return 'Ask again later' elif answerNumber == 6: return 'Concentrate and ask again' elif answerNumber == 7: return 'My reply is no' elif answerNumber == 8: return 'Outlook not so good' elif answerNumber == 9: return 'Very doubtful' ??r = random.randint(1, 9) ??fortune = getAnswer(r) ??print(fortune) Python 编程快速上手——让繁琐工作自动化 在这个程序开始时,Python 首先导入random 模块?。然后getAnswer()函数被 定义?。因为函数是被定义(而不是被调用),所以执行会跳过其中的代码。接下来, random.randint()函数被调用,带两个参数,1 和9?。它求值为1 和9 之间的一个随 机整数(包括1 和9),这个值被存在一个名为r 的变量中。 getAnswer()函数被调用,以r 作为参数?。程序执行转移到getAnswer()函数的 顶部?,r 的值被保存到名为answerNumber 的变元中。然后,根据answerNumber 中的值,函数返回许多可能字符串中的一个。程序执行返回到程序底部的代码行, 即原来调用getAnswer()的地方?。返回的字符串被赋给一个名为fortune 变量,然 后它又被传递给print()调用?,并被打印在屏幕上。 请注意,因为可以将返回值作为参数传递给另一个函数调用,所以你可以将下 面3 行代码 r = random.randint(1, 9) fortune = getAnswer(r) print(fortune) 缩写成一行等价的代码: print(getAnswer(random.randint(1, 9))) 记住,表达式是值和操作符的组合。函数调用可以用在表达式中,因为它求值 为它的返回值。 3.3 None 值 在Python 中有一个值称为None,它表示没有值。None 是NoneType 数据类型 的唯一值(其他编程语言可能称这个值为null、nil 或undefined)。就像布尔值True 和False 一样,None 必须大写首字母N。 如果你希望变量中存储的东西不会与一个真正的值混淆,这个没有值的值就可 能有用。有一个使用None 的地方就是print()的返回值。print()函数在屏幕上显示文 本,但它不需要返回任何值,这和len()或input()不同。但既然所有函数调用都需要 求值为一个返回值,那么print()就返回None。要看到这个效果,请在交互式环境中 输入以下代码。 spam = print('Hello!') Hello! None == spam True 在幕后,对于所有没有return 语句的函数定义,Python 都会在末尾加上return None。这类似于while 或for 循环隐式地以continue 语句结尾。而且,如果使用不 带值的return 语句(也就是只有return 关键字本身),那么就返回None。 第3 章 函数 3.4 关键字参数和print() 大多数参数是由它们在函数调用中的位置来识别的。例如,random.randint(1, 10) 与random.randint(10, 1)不同。函数调用random.randint(1, 10)将返回1 到10 之间的 一个随机整数,因为第一个参数是范围的下界,第二个参数是范围的上界(而 random.randint(10, 1)会导致错误)。 但是,“关键字参数”是由函数调用时加在它们前面的关键字来识别的。关键 字参数通常用于可选变元。例如,print()函数有可选的变元end 和sep,分别指定在 参数末尾打印什么,以及在参数之间打印什么来隔开它们。 如果运行以下程序: print('Hello') print('World') 输出将会是: Hello World 这两个字符串出现在独立的两行中,因为print()函数自动在传入的字符串末尾 添加了换行符。但是,可以设置end 关键字参数,将它变成另一个字符串。例如, 如果程序像这样: print('Hello', end='') print('World') 输出就会像这样: HelloWorld 输出被打印在一行中,因为在'Hello'后面不再打印换行,而是打印了一个空 字符串。如果需要禁用加到每一个print()函数调用末尾的换行,这就很有用。 类似地,如果向print()传入多个字符串值,该函数就会自动用一个空格分隔它 们。在交互式环境中输入以下代码: print('cats', 'dogs', 'mice') cats dogs mice 但是你可以传入sep 关键字参数,替换掉默认的分隔字符串。在交互式环境中 输入以下代码: print('cats', 'dogs', 'mice', sep=',') cats,dogs,mice 也可以在你编写的函数中添加关键字参数,但必须先在接下来的两章中学习列 表和字典数据类型。现在只要知道,某些函数有可选的关键字参数,在函数调用时 可以指定。 Python 编程快速上手——让繁琐工作自动化 3.5 局部和全局作用域 在被调用函数内赋值的变元和变量,处于该函数的“局部作用域”。在所有函 数之外赋值的变量,属于“全局作用域”。处于局部作用域的变量,被称为“局部 变量”。处于全局作用域的变量,被称为“全局变量”。一个变量必是其中一种,不 能既是局部的又是全局的。 可以将“作用域”看成是变量的容器。当作用域被销毁时,所有保存在该作用 域内的变量的值就被丢弃了。只有一个全局作用域,它是在程序开始时创建的。如 果程序终止,全局作用域就被销毁,它的所有变量就被丢弃了。否则,下次你运行 程序的时候,这些变量就会记住它们上次运行时的值。 一个函数被调用时,就创建了一个局部作用域。在这个函数内赋值的所有变量, 存在于该局部作用域内。该函数返回时,这个局部作用域就被销毁了,这些变量就 丢失了。下次调用这个函数,局部变量不会记得该函数上次被调用时它们保存的值。 作用域很重要,理由如下: ??全局作用域中的代码不能使用任何局部变量; ??但是,局部作用域可以访问全局变量; ??一个函数的局部作用域中的代码,不能使用其他局部作用域中的变量。 ??如果在不同的作用域中,你可以用相同的名字命名不同的变量。也就是说,可 以有一个名为spam 的局部变量,和一个名为spam 的全局变量。 Python 有不同的作用域,而不是让所有东西都成全局变量,这是有理由的。这 样一来,当特定函数调用中的代码修改变量时,该函数与程序其他部分的交互,只 能通过它的参数和返回值。这缩小了可能导致缺陷的代码作用域。如果程序只包含 全局变量,又有一个变量赋值错误的缺陷,那就很难追踪这个赋值错误发生的位置。 它可能在程序的任何地方赋值,而你的程序可能有几百到几千行!但如果缺陷是因 为局部变量错误赋值,你就会知道,只有那一个函数中的代码可能产生赋值错误。 虽然在小程序中使用全局变量没有太大问题,但当程序变得越来越大时,依赖 全局变量就是一个坏习惯。 3.5.1 局部变量不能在全局作用域内使用 考虑下面的程序,它在运行时会产生错误: def spam(): eggs = 31337 spam() print(eggs) 如果运行这个程序,输出将是: Traceback (most recent call last): File "C:/test3784.py", line 4, in 第3 章 函数 print(eggs) NameError: name 'eggs' is not defined 发生错误是因为,eggs 变量只属于spam()调用所创建的局部作用域。在程序执 行从spam 返回后,该局部作用域就被销毁了,不再有名为eggs 的变量。所以当程 序试图执行print(eggs),Python 就报错,说eggs 没有定义。你想想看,这是有意义 的。当程序执行在全局作用域中时,不存在局部作用域,所以不会有任何局部变量。 这就是为什么只有全局变量能用于全局作用域。 3.5.2 局部作用域不能使用其他局部作用域内的变量 一个函数被调用时,就创建了一个新的局部作用域,这包括一个函数被另一个 函数调用时的情况。请看以下代码: def spam(): ??eggs = 99 ??bacon() ??print(eggs) def bacon(): ham = 101 ??eggs = 0 ??spam() 在程序开始运行时,spam()函数被调用?,创建了一个局部作用域。局部变量 eggs?被赋值为99。然后bacon()函数被调用?,创建了第二个局部作用域。多个局 部作用域能同时存在。在这个新的局部作用域中,局部变量ham 被赋值为101。局 部变量eggs(与spam()的局部作用域中的那个变量不同)也被创建?,并赋值为0。 当bacon()返回时,这次调用的局部作用域被销毁。程序执行在spam()函数中继 续,打印出eggs 的值?。因为spam()调用的局部作用域仍然存在,eggs 变量被赋值 为99。这就是程序的打印输出。 要点在于,一个函数中的局部变量完全与其他函数中的局部变量分隔开来。 3.5.3 全局变量可以在局部作用域中读取 请看以下程序: def spam(): print(eggs) eggs = 42 spam() print(eggs) 因为在spam()函数中,没有变元名为eggs,也没有代码为eggs 赋值,所以当 spam()中使用eggs 时,Python 认为它是对全局变量eggs 的引用。这就是前面的程 序运行时打印出42 的原因。 Python 编程快速上手——让繁琐工作自动化 3.5.4 名称相同的局部变量和全局变量 要想生活简单,就要避免局部变量与全局变量或其他局部变量同名。但在技术 上,在Python 中让局部变量和全局变量同名是完全合法的。为了看看实际发生的情 况,请在文件编辑器中输入以下代码,并保存为sameName.py: def spam(): ??eggs = 'spam local' print(eggs) # prints 'spam local' def bacon(): ??eggs = 'bacon local' print(eggs) # prints 'bacon local' spam() print(eggs) # prints 'bacon local' ??eggs = 'global' bacon() print(eggs) # prints 'global' 运行该程序,输出如下: bacon local spam local bacon local global 在这个程序中,实际上有3 个不同的变量,但令人迷惑的是,它们都名为eggs。 这些变量是: ?名为eggs 的变量,存在于spam()被调用时的局部作用域; ?名为eggs 的变量,存在于bacon()被调用时的局部作用域; ?名为eggs 的变量,存在于全局作用域。 因为这3 个独立的变量都有相同的名字,追踪某一个时刻使用的是哪个变量, 可能比较麻烦。这就是应该避免在不同作用域内使用相同变量名的原因。 3.6 global 语句 如果需要在一个函数内修改全局变量,就使用global 语句。如果在函数的顶部 有global eggs 这样的代码,它就告诉Python,“在这个函数中,eggs 指的是全局变 量,所以不要用这个名字创建一个局部变量。”例如,在文件编辑器中输入以下代 码,并保存为sameName2.py: def spam(): ??global eggs ??eggs = 'spam' eggs = 'global' spam() print(eggs) 第3 章 函数 运行该程序,最后的print()调用将输出: spam 因为eggs 在spam()的顶部被声明为global?,所以当eggs 被赋值为'spam'时?, 赋值发生在全局作用域的spam 上。没有创建局部spam 变量。 有4 条法则,来区分一个变量是处于局部作用域还是全局作用域: 1.如果变量在全局作用域中使用(即在所有函数之外),它就总是全局变量。 2.如果在一个函数中,有针对该变量的global 语句,它就是全局变量。 3.否则,如果该变量用于函数中的赋值语句,它就是局部变量。 4.但是,如果该变量没有用在赋值语句中,它就是全局变量。 为了更好地理解这些法则,这里有一个例子程序。在文件编辑器中输入以下代 码,并保存为sameName3.py: def spam(): ??global eggs eggs = 'spam' # this is the global def bacon(): ??eggs = 'bacon' # this is a local def ham(): ??print(eggs) # this is the global eggs = 42 # this is the global spam() print(eggs) 在spam()函数中,eggs 是全局eggs 变量,因为在函数的开始处,有针对eggs 变量的global 语句?。在bacon()中,eggs 是局部变量,因为在该函数中有针对它的 赋值语句?。在ham()中?,eggs 是全局变量,因为在这个函数中,既没有赋值语 句,也没有针对它的global 语句。如果运行sameName3.py,输出将是: spam 在一个函数中,一个变量要么总是全局变量,要么总是局部变量。函数中的代码 没有办法先使用名为eggs 的局部变量,稍后又在同一个函数中使用全局eggs 变量。 如果想在一个函数中修改全局变量中存储的值,就必须对该变量使用global 语句。 在一个函数中,如果试图在局部变量赋值之前就使用它,像下面的程序这样,Python 就会报错。为了看到效果,请在文件编辑器中输入以下代码,并保存为sameName4.py: def spam(): print(eggs) # ERROR! ??eggs = 'spam local' ??eggs = 'global' spam() 运行前面的程序,会产生出错信息。 Python 编程快速上手——让繁琐工作自动化 Traceback (most recent call last): File "C:/test3784.py", line 6, in spam() File "C:/test3784.py", line 2, in spam print(eggs) # ERROR! UnboundLocalError: local variable 'eggs' referenced before assignment 发生这个错误是因为,Python 看到spam()函数中有针对eggs 的赋值语句?,因 此认为eggs 变量是局部变量。但是因为print(eggs)的执行在eggs 赋值之前,局部变 量eggs 并不存在。Python 不会退回到使用全局eggs 变量?。 3.7 异常处理 到目前为止,在Python 程序中遇到错误,或“异常”,意味着整个程序崩溃。 你不希望这发生在真实世界的程序中。相反,你希望程序能检测错误,处理它们, 然后继续运行。 例如,考虑下面的程序,它有一个“除数为零”的错误。打开一个新的文件编 辑器窗口,输入以下代码,并保存为zeroDivide.py: def spam(divideBy): return 42 / divideBy print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) 我们已经定义了名为spam 的函数,给了它一个变元,然后打印出该函数带各 种参数的值,看看会发生什么情况。下面是运行前面代码的输出: 21.0 3.5 Traceback (most recent call last): File "C:/zeroDivide.py", line 6, in print(spam(0)) File "C:/zeroDivide.py", line 2, in spam return 42 / divideBy ZeroDivisionError: division by zero 当试图用一个数除以零时,就会发生ZeroDivisionError。根据错误信息中给出 的行号,我们知道spam()中的return 语句导致了一个错误。 函数作为“黑盒” 通常,对于一个函数,你要知道的就是它的输入值(变元)和输出值。 你并非总是需要加重自己的负担,弄清楚函数的代码实际是怎样工作的。 如果以这种高层的方式来思考函数,通常大家会说,你将该函数看成是一 个黑盒。 这个思想是现代编程的基础。本书后面的章节将向你展示一些模块,其中 第3 章 函数 的函数是由其他人编写的。尽管你在好奇的时候也可以看一看源代码,但为了 能使用它们,你并不需要知道它们是如何工作的。而且,因为鼓励在编写函数 时不使用全局变量,你通常也不必担心函数的代码会与程序的其他部分发生交 叉影响。 错误可以由try 和except 语句来处理。那些可能出错的语句被放在try 子句中。 如果错误发生,程序执行就转到接下来的except 子句开始处。 可以将前面除数为零的代码放在一个try 子句中,让except 子句包含代码,来 处理该错误发生时应该做的事。 def spam(divideBy): try: return 42 / divideBy except ZeroDivisionError: print('Error: Invalid argument.') print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) 如果在try 子句中的代码导致一个错误,程序执行就立即转到except 子句的代 码。在运行那些代码之后,执行照常继续。前面程序的输出如下: 21.0 3.5 Error: Invalid argument. None 42.0 请注意,在函数调用中的try 语句块中,发生的所有错误都会被捕捉。请考虑 以下程序,它的做法不一样,将spam()调用放在语句块中: def spam(divideBy): return 42 / divideBy try: print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) except ZeroDivisionError: print('Error: Invalid argument.') 该程序运行时,输出如下: 21.0 3.5 Error: Invalid argument. print(spam(1))从未被执行是因为,一旦执行跳到except 子句的代码,就不会回 Python 编程快速上手——让繁琐工作自动化 到try 子句。它会继续照常向下执行。 3.8 一个小程序:猜数字 到目前为止,前面展示的小例子适合于介绍基本概念。现在让我们看一看,如 何将所学的知识综合起来,编写一个更完整的程序。在本节中,我将展示一个简单 的猜数字游戏。在运行这个程序时,输出看起来像这样: I am thinking of a number between 1 and 20. Take a guess. 10 Your guess is too low. Take a guess. 15 Your guess is too low. Take a guess. 17 Your guess is too high. Take a guess. 16 Good job! You guessed my number in 4 guesses! 在文件编辑器中输入以下代码,并保存为guessTheNumber.py:
import random secretNumber = random.randint(1, 20) print('I am thinking of a number between 1 and 20.')
for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input()) if guess < secretNumber: print('Your guess is too low.') elif guess > secretNumber: print('Your guess is too high.') else: break # This condition is the correct guess! if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!') else: print('Nope. The number I was thinking of was ' + str(secretNumber)) 让我们逐行来看看代码,从头开始。
import random secretNumber = random.randint(1, 20) 首先,代码顶部的一行注释解释了这个程序做什么。然后,程序导入了模块 random,以便能用random.randint()函数生成一个数字,让用户来猜。返回值是一个 第3 章 函数 1 到20 之间的随机整数,保存在变量secretNumber 中。 print('I am thinking of a number between 1 and 20.')
for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input()) 程序告诉玩家,它有了一个秘密数字,并且给玩家6 次猜测机会。在for 循环 中,代码让玩家输入一次猜测,并检查该猜测。该循环最多迭代6 次。循环中发 生的第一件事情,是让玩家输入一个猜测数字。因为input()返回一个字符串,所 以它的返回值被直接传递给int(),它将字符串转变成整数。这保存在名为guess 的变量中。 if guess < secretNumber: print('Your guess is too low.') elif guess > secretNumber: print('Your guess is too high.') 这几行代码检查该猜测是小于还是大于那个秘密数字。不论哪种情况,都在屏 幕上打印提示。 else: break # This condition is the correct guess! 如果该猜测既不大于也不小于秘密数字,那么它就一定等于秘密数字,这时你 希望程序执行跳出for 循环。 if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!') else: print('Nope. The number I was thinking of was ' + str(secretNumber)) 在for 循环后,前面的if...else 语句检查玩家是否正确地猜到了该数字,并将相 应的信息打印在屏幕上。不论哪种情况,程序都会打印一个包含整数值的变量 (guessesTaken 和secretNumber)。因为必须将这些整数值连接成字符串,所以它将 这些变量传递给str()函数,该函数返回这些整数值的字符串形式。现在这些字符串 可以用+操作符连接起来,最后传递给print()函数调用。 3.9 小结 函数是将代码逻辑分组的主要方式。因为函数中的变量存在于它们自己的局部 作用域内,所以一个函数中的代码不能直接影响其他函数中变量的值。这限制了哪 些代码才能改变变量的值,对于调试代码是很有帮助的。 函数是很好的工具,帮助你组织代码。你可以认为他们是黑盒。它们以参数的 Python 编程快速上手——让繁琐工作自动化 形式接收输入,以返回值的形式产生输出。它们内部的代码不会影响其他函数中的 变量。 在前面几章中,一个错误就可能导致程序崩溃。在本章中,你学习了try 和except 语句,它们在检测到错误时会运行代码。这让程序在面对常见错误时更有灵活性。 3.10 习题 1.为什么在程序中加入函数会有好处? 2.函数中的代码何时执行:在函数被定义时,还是在函数被调用时? 3.什么语句创建一个函数? 4.一个函数和一次函数调用有什么区别? 5.Python 程序中有多少全局作用域?有多少局部作用域? 6.当函数调用返回时,局部作用域中的变量发生了什么? 7.什么是返回值?返回值可以作为表达式的一部分吗? 8.如果函数没有返回语句,对它调用的返回值是什么? 9.如何强制函数中的一个变量指的是全局变量? 10.None 的数据类型是什么? 11.import areallyourpetsnamederic 语句做了什么? 12.如果在名为spam 的模块中,有一个名为bacon()的函数,在引入spam 后, 如何调用它? 13.如何防止程序在遇到错误时崩溃? 14.try 子句中发生了什么?except 子句中发生了什么? 3.11 实践项目 作为实践,请编写程序完成下列任务。 3.11.1 Collatz 序列 编写一个名为collatz()的函数,它有一个名为number 的参数。如果参数是偶数, 那么collatz()就打印出number // 2,并返回该值。如果number 是奇数,collatz()就打 印并返回3 * number + 1。 然后编写一个程序,让用户输入一个整数,并不断对这个数调用collatz(),直 到函数返回值1(令人惊奇的是,这个序列对于任何整数都有效,利用这个序列, 你迟早会得到1!既使数学家也不能确定为什么。你的程序在研究所谓的“Collatz 序列”,它有时候被称为“最简单的、不可能的数学问题”)。 第3 章 函数 记得将input()的返回值用int()函数转成一个整数,否则它会是一个字符串。 提示 如果number % 2 == 0,整数number 就是偶数,如果number % 2 == 1,它 就是奇数。 这个程序的输出看起来应该像这样: Enter number: 3 10 5 16 8 4 2 1 3.11.2 输入验证 在前面的项目中添加try 和except 语句,检测用户是否输入了一个非整数的字 符串。正常情况下,int()函数在传入一个非整数字符串时,会产生ValueError 错误, 比如int('puppy')。在except 子句中,向用户输出一条信息,告诉他们必须输入一个 整数。 第 章 列 表 在你能够开始编写程序之前,还有一个主题需要理解,那 就是列表数据类型及元组。列表和元组可以包含多个值,这样 编写程序来处理大量数据就变得更容易。而且,由于列表本身 又可以包含其他列表,所以可以用它们将数据安排成层次结构。 本章将探讨列表的基础知识。我也会讲授关于方法的内 容。方法也是函数,它们与特定数据类型的值绑定。然后我会 简单介绍类似列表的元组和字符串数据类型,以及它们与列表 值的比较。下一章将介绍字典数据类型。 4.1 列表数据类型 “列表”是一个值,它包含多个字构成的序列。术语“列表值”指的是列表本 身(它作为一个值,可以保存在变量中,或传递给函数,像所有其他值一样),而 不是指列表值之内的那些值。列表值看起来像这样:['cat', 'bat', 'rat', 'elephant']。就 像字符串值用引号来标记字符串的起止一样,列表用左方括号开始,右方括号结束, 即[]。列表中的值也称为“表项”。表项用逗号分隔(就是说,它们是“逗号分隔的”)。 例如,在交互式环境中输入以下代码: 4 2 Python 编程快速上手——让繁琐工作自动化
[1, 2, 3] [1, 2, 3] ['cat', 'bat', 'rat', 'elephant'] ['cat', 'bat', 'rat', 'elephant'] ['hello', 3.1415, True, None, 42] ['hello', 3.1415, True, None, 42] ??>>> spam = ['cat', 'bat', 'rat', 'elephant'] spam ['cat', 'bat', 'rat', 'elephant'] spam 变量?仍然只被赋予一个值:列表值。但列表值本身包含多个值。[]是一 个空列表,不包含任何值,类似于空字符串’’。 4.1.1 用下标取得列表中的单个值 假定列表['cat', 'bat', 'rat', 'elephant']保存在名为spam 的变量中。Python 代码spam[0] 将求值为'cat',spam[1]将求值为'bat',依此类推。列表后面方括号内的整数被称为“下 标”。列表中第一个值的下标是0,第二个值的下标是1,第三个值的下标是2,依此 类推。图4-1 展示了一个赋给spam 的列表值,以及下标表达式的求值结果。 图4-1 一个列表值保存在spam 变量中,展示了每个下标指向哪个值 例如,在交互式环境中输入以下表达式。开始将列表赋给变量spam。 spam = ['cat', 'bat', 'rat', 'elephant'] spam[0] 'cat' spam[1] 'bat' spam[2] 'rat' spam[3] 'elephant' ['cat', 'bat', 'rat', 'elephant'][3] 'elephant' ??>>> 'Hello ' + spam[0] ??'Hello cat' 'The ' + spam[1] + ' ate the ' + spam[0] + '.' 'The bat ate the cat.' 请注意,表达式'Hello ' + spam[0] ?求值为'Hello ' + 'cat',因为spam[0]求值为字 符串'cat'。这个表达式也因此求值为字符串'Hello cat'?。 如果使用的下标超出了列表中值的个数,Python 将给出IndexError 出错信息。 spam = ['cat', 'bat', 'rat', 'elephant'] spam[10000] Traceback (most recent call last): File "<pyshell#9>", line 1, in spam[10000] IndexError: list index out of range 第4 章 列表 下标只能是整数,不能是浮点值。下面的例子将导致TypeError 错误: spam = ['cat', 'bat', 'rat', 'elephant'] spam[1] 'bat' spam[1.0] Traceback (most recent call last): File "<pyshell#13>", line 1, in spam[1.0] TypeError: list indices must be integers, not float spam[int(1.0)] 'bat' 列表也可以包含其他列表值。这些列表的列表中的值,可以通过多重下标来访 问,像这样: spam = 'cat', 'bat'], [10, 20, 30, 40, 50 spam[0] ['cat', 'bat'] spam[0][1] 'bat' spam[1][4] 50 第一个下标表明使用哪个列表值,第二个下标表明该列表值中的值。例如, spam[0][1]打印出'bat',即第一个列表中的第二个值。如果只使用一个下标,程序将 打印出该下标处的完整列表值。 4.1.2 负数下标 虽然下标从0 开始并向上增长,但也可以用负整数作为下标。整数值?1 指的是 列表中的最后一个下标,?2 指的是列表中倒数第二个下标,以此类推。在交互式环 境中输入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] spam[-1] 'elephant' spam[-3] 'bat' 'The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.' 'The elephant is afraid of the bat.' 4.1.3 利用切片取得子列表 就像下标可以从列表中取得单个值一样,“切片”可以从列表中取得多个值, 结果是一个新列表。切片输入在一对方括号中,像下标一样,但它有两个冒号分隔 的整数。请注意下标和切片的不同。 ??spam[2]是一个列表和下标(一个整数)。 ??spam[1:4]是一个列表和切片(两个整数)。 在一个切片中,第一个整数是切片开始处的下标。第二个整数是切片结束处的 Python 编程快速上手——让繁琐工作自动化 下标。切片向上增长,直至第二个下标的值,但不包括它。切片求值为一个新的列 表值。在交互式环境中输入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] spam[0:4] ['cat', 'bat', 'rat', 'elephant'] spam[1:3] ['bat', 'rat'] spam[0:-1] ['cat', 'bat', 'rat'] 作为快捷方法,你可以省略切片中冒号两边的一个下标或两个下标。省略第一 个下标相当于使用0,或列表的开始。省略第二个下标相当于使用列表的长度,意 味着分片直至列表的末尾。在交互式环境中输入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] spam[:2] ['cat', 'bat'] spam[1:] ['bat', 'rat', 'elephant'] spam[:] ['cat', 'bat', 'rat', 'elephant'] 4.1.4 用len()取得列表的长度 len()函数将返回传递给它的列表中值的个数,就像它能计算字符串中字符的个 数一样。在交互式环境中输入以下代码: spam = ['cat', 'dog', 'moose'] len(spam) 3 4.1.5 用下标改变列表中的值 一般情况下,赋值语句左边是一个变量名,就像spam = 4。但是,也可以使用 列表的下标来改变下标处的值。例如,spam[1] = 'aardvark'意味着“将列表spam 下 标1 处的值赋值为字符串'aardvark'。在交互式环境中输入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] spam[1] = 'aardvark' spam ['cat', 'aardvark', 'rat', 'elephant'] spam[2] = spam[1] spam ['cat', 'aardvark', 'aardvark', 'elephant'] spam[-1] = 12345 spam ['cat', 'aardvark', 'aardvark', 12345] 4.1.6 列表连接和列表复制 +操作符可以连接两个列表,得到一个新列表,就像它将两个字符串合并成一 第4 章 列表 个新字符串一样。操作符可以用于一个列表和一个整数,实现列表的复制。在交 互式环境中输入以下代码: [1, 2, 3] + ['A', 'B', 'C'] [1, 2, 3, 'A', 'B', 'C'] ['X', 'Y', 'Z'] * 3 ['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z'] spam = [1, 2, 3] spam = spam + ['A', 'B', 'C'] spam [1, 2, 3, 'A', 'B', 'C'] 4.1.7 用del 语句从列表中删除值 del 语句将删除列表中下标处的值,表中被删除值后面的所有值,都将向前移 动一个下标。例如,在交互式环境中输入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] del spam[2] spam ['cat', 'bat', 'elephant'] del spam[2] spam ['cat', 'bat'] del 语句也可用于一个简单变量,删除它,作用就像是“取消赋值”语句。如 果在删除之后试图使用该变量,就会遇到NameError 错误,因为该变量已不再存在。 在实践中,你几乎永远不需要删除简单变量。del 语句几乎总是用于删除列表 中的值。 4.2 使用列表 当你第一次开始编程时,很容易会创建许多独立的变量,来保存一组类似的值。 例如,如果要保存我的猫的名字,可能会写出这样的代码: catName1 = 'Zophie' catName2 = 'Pooka' catName3 = 'Simon' catName4 = 'Lady Macbeth' catName5 = 'Fat-tail' catName6 = 'Miss Cleo' 事实表明这是一种不好的编程方式。举一个例子,如果猫的数目发生改变,程序就 不得不增加变量,来保存更多的猫。这种类型的程序也有很多重复或几乎相等的代码。 考虑下面的程序中有多少重复代码,在文本编辑器中输入它并保存为allMyCats1.py: print('Enter the name of cat 1:') catName1 = input() print('Enter the name of cat 2:') catName2 = input() print('Enter the name of cat 3:') Python 编程快速上手——让繁琐工作自动化 catName3 = input() print('Enter the name of cat 4:') catName4 = input() print('Enter the name of cat 5:') catName5 = input() print('Enter the name of cat 6:') catName6 = input() print('The cat names are:') print(catName1 + ' ' + catName2 + ' ' + catName3 + ' ' + catName4 + ' ' + catName5 + ' ' + catName6) 不必使用多个重复的变量,你可以使用单个变量,包含一个列表值。例如,下面 是新的改进版本的allMyCats1.py 程序。这个新版本使用了一个列表,可以保存用户输 入的任意多的猫。在新的文件编辑器窗口中,输入以下代码并保存为allMyCats2.py。 catNames = [] while True: print('Enter the name of cat ' + str(len(catNames) + 1) + ' (Or enter nothing to stop.):') name = input() if name == '': break catNames = catNames + [name] # list concatenation print('The cat names are:') for name in catNames: print(' ' + name) 运行这个程序,输出看起来像这样: Enter the name of cat 1 (Or enter nothing to stop.): Zophie Enter the name of cat 2 (Or enter nothing to stop.): Pooka Enter the name of cat 3 (Or enter nothing to stop.): Simon Enter the name of cat 4 (Or enter nothing to stop.): Lady Macbeth Enter the name of cat 5 (Or enter nothing to stop.): Fat-tail Enter the name of cat 6 (Or enter nothing to stop.): Miss Cleo Enter the name of cat 7 (Or enter nothing to stop.): The cat names are: Zophie Pooka Simon Lady Macbeth Fat-tail Miss Cleo 使用列表的好处在于,现在数据放在一个结构中,所以程序能够更灵活的处理 数据,比放在一些重复的变量中方便。 4.2.1 列表用于循环 在第2 章中,你学习了使用循环,对一段代码执行一定次数。从技术上说,循环是 第4 章 列表 针对一个列表或类似列表中的每个值,重复地执行代码块。例如,如果执行以下代码: for i in range(4): print(i) 程序的输出将是: 0 1 2 3 这是因为range(4)的返回值是类似列表的值。Python 认为它类似于[0, 1, 2, 3]。 下面的程序和前面的程序输出相同: for i in [0, 1, 2, 3]: print(i) 前面的for 循环实际上是在循环执行它的子句,在每次迭代中,让变量依次设 置为列表中的值。 注意 在本书中,我使用术语“类似列表”,来指技术上称为“序列”的数据类型。 但是,你不需要知道这个术语的技术定义。 一个常见的Python 技巧,是在for 循环中使用range(len(someList)),迭代列表 的每一个下标。例如,在交互式环境中输入以下代码: supplies = ['pens', 'staplers', 'flame-throwers', 'binders'] for i in range(len(supplies)): print('Index ' + str(i) + ' in supplies is: ' + supplies[i]) Index 0 in supplies is: pens Index 1 in supplies is: staplers Index 2 in supplies is: flame-throwers Index 3 in supplies is: binders 在前面的循环中使用range(len(supplies))很方便,这是因为,循环中的代码可以访 问下标(通过变量i),以及下标处的值(通过supplies[i])。最妙的是,range(len(supplies)) 将迭代supplies 的所有下标,无论它包含多少表项。 4.2.2 in 和not in 操作符 利用in 和not in 操作符,可以确定一个值否在列表中。像其他操作符一样,in 和not in 用在表达式中,连接两个值:一个要在列表中查找的值,以及待查找 的列表。这些表达式将求值为布尔值。在交互式环境中输入以下代码: 'howdy' in ['hello', 'hi', 'howdy', 'heyas'] True spam = ['hello', 'hi', 'howdy', 'heyas'] 'cat' in spam False 'howdy' not in spam Python 编程快速上手——让繁琐工作自动化 False 'cat' not in spam True 例如,下面的程序让用户输入一个宠物名字,然后检查该名字是否在宠物列表 中。打开一个新的文件编辑器窗口,输入以下代码,并保存为myPets.py: myPets = ['Zophie', 'Pooka', 'Fat-tail'] print('Enter a pet name:') name = input() if name not in myPets: print('I do not have a pet named ' + name) else: print(name + ' is my pet.') 输出可能像这样: Enter a pet name: Footfoot I do not have a pet named Footfoot 4.2.3 多重赋值技巧 多重赋值技巧是一种快捷方式,让你在一行代码中,用列表中的值为多个变量 赋值。所以不必像这样: cat = ['fat', 'black', 'loud'] size = cat[0] color = cat[1] disposition = cat[2] 而是输入下面的代码: cat = ['fat', 'black', 'loud'] size, color, disposition = cat 变量的数目和列表的长度必须严格相等,否则Python 将给出ValueError: cat = ['fat', 'black', 'loud'] size, color, disposition, name = cat Traceback (most recent call last): File "<pyshell#84>", line 1, in size, color, disposition, name = cat ValueError: need more than 3 values to unpack 4.3 增强的赋值操作 在对变量赋值时,常常会用到变量本身。例如,将42 赋给变量spam 之后,用 下面的代码让spam 的值增加1: spam = 42 spam = spam + 1 spam 43 第4 章 列表 作为一种快捷方式,可以用增强的赋值操作符+=来完成同样的事: spam = 42 spam += 1 spam 43 针对+、-、、/和%操作符,都有增强的赋值操作符,如表4-1 所示。 表4-1 增强的赋值操作符 增强的赋值语句 等价的赋值语句 spam += 1 spam = spam + 1 spam -= 1 spam = spam - 1 spam = 1 spam = spam * 1 spam /= 1 spam = spam / 1 spam %= 1 spam = spam % 1 +=操作符也可以完成字符串和列表的连接,=操作符可以完成字符串和列表的 复制。在交互式环境中输入以下代码: spam = 'Hello' spam += ' world!' spam 'Hello world!' bacon = ['Zophie'] bacon = 3 bacon ['Zophie', 'Zophie', 'Zophie'] 4.4 方法 方法和函数是一回事,只是它是调用在一个值上。例如,如果一个列表值存储 在spam 中,你可以在这个列表上调用index()列表方法(稍后我会解释),就像 spam.index('hello')一样。方法部分跟在这个值后面,以一个句点分隔。 每种数据类型都有它自己的一组方法。例如,列表数据类型有一些有用的方法, 用来查找、添加、删除或操作列表中的值。 4.4.1 用index()方法在列表中查找值 列表值有一个index()方法,可以传入一个值,如果该值存在于列表中,就返回它 的下标。如果该值不在列表中,Python 就报ValueError。在交互式环境中输入以下代码: spam = ['hello', 'hi', 'howdy', 'heyas'] spam.index('hello') 0 spam.index('heyas') 3 spam.index('howdy howdy howdy') Python 编程快速上手——让繁琐工作自动化 Traceback (most recent call last): File "<pyshell#31>", line 1, in spam.index('howdy howdy howdy') ValueError: 'howdy howdy howdy' is not in list 如果列表中存在重复的值,就返回它第一次出现的下标。在交互式环境中输入 以下代码,注意index()返回1,而不是3: spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka'] spam.index('Pooka') 1 4.4.2 用append()和insert()方法在列表中添加值 要在列表中添加新值,就使用append()和 insert()方法。在交互式环境中输入以 下代码,对变量spam 中的列表调用append()方法: spam = ['cat', 'dog', 'bat'] spam.append('moose') spam ['cat', 'dog', 'bat', 'moose'] 前面的append()方法调用,将参数添加到列表末尾。insert()方法可以在列表任 意下标处插入一个值。insert()方法的第一个参数是新值的下标,第二个参数是要插 入的新值。在交互式环境中输入以下代码: spam = ['cat', 'dog', 'bat'] spam.insert(1, 'chicken') spam ['cat', 'chicken', 'dog', 'bat'] 请注意,代码是spam.append('moose')和spam.insert(1, 'chicken'),而不是spam = spam.append('moose')和spam = spam.insert(1, 'chicken')。append()和insert()都不会将 spam 的新值作为其返回值(实际上,append()和insert()的返回值是None,所以你 肯定不希望将它保存为变量的新值)。但是,列表被“当场”修改了。在4.6.1 节“可 变和不变数据类型”中,将更详细地介绍当场修改一个列表。 方法属于单个数据类型。append()和insert()方法是列表方法,只能在列表上调 用,不能在其他值上调用,例如字符串和整型。在交互式环境中输入以下代码,注 意产生的AttributeError 错误信息: eggs = 'hello' eggs.append('world') Traceback (most recent call last): File "<pyshell#19>", line 1, in eggs.append('world') AttributeError: 'str' object has no attribute 'append' bacon = 42 bacon.insert(1, 'world') Traceback (most recent call last): File "<pyshell#22>", line 1, in bacon.insert(1, 'world') AttributeError: 'int' object has no attribute 'insert' 第4 章 列表 4.4.3 用remove()方法从列表中删除值 给 remove()方法传入一个值,它将从被调用的列表中删除。在交互式环境中输 入以下代码: spam = ['cat', 'bat', 'rat', 'elephant'] spam.remove('bat') spam ['cat', 'rat', 'elephant'] 试图删除列表中不存在的值,将导致ValueError 错误。例如,在交互式环境中 输入以下代码,注意显示的错误: spam = ['cat', 'bat', 'rat', 'elephant'] spam.remove('chicken') Traceback (most recent call last): File "<pyshell#11>", line 1, in spam.remove('chicken') ValueError: list.remove(x): x not in list 如果该值在列表中出现多次,只有第一次出现的值会被删除。在交互式环境中 输入以下代码: spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat'] spam.remove('cat') spam ['bat', 'rat', 'cat', 'hat', 'cat'] 如果知道想要删除的值在列表中的下标,del 语句就很好用。如果知道想要从 列表中删除的值,remove()方法就很好用。 4.4.4 用sort()方法将列表中的值排序 数值的列表或字符串的列表,能用sort()方法排序。例如,在交互式环境中输 入以下代码: spam = [2, 5, 3.14, 1, -7] spam.sort() spam [-7, 1, 2, 3.14, 5] spam = ['ants', 'cats', 'dogs', 'badgers', 'elephants'] spam.sort() spam ['ants', 'badgers', 'cats', 'dogs', 'elephants'] 也可以指定reverse 关键字参数为True,让sort()按逆序排序。在交互式环境中 输入以下代码: spam.sort(reverse=True) spam ['elephants', 'dogs', 'cats', 'badgers', 'ants'] 关于sort()方法,你应该注意3 件事。首先,sort()方法当场对列表排序。不要 Python 编程快速上手——让繁琐工作自动化 写出spam = spam.sort()这样的代码,试图记录返回值。 其次,不能对既有数字又有字符串值的列表排序,因为Python 不知道如何比较 它们。在交互式环境中输入以下代码,注意TypeError 错误: spam = [1, 3, 2, 4, 'Alice', 'Bob'] spam.sort() Traceback (most recent call last): File "<pyshell#70>", line 1, in spam.sort() TypeError: unorderable types: str() < int() 第三,sort()方法对字符串排序时,使用“ASCII 字符顺序”,而不是实际的字 典顺序。这意味着大写字母排在小写字母之前。因此在排序时,小写的a 在大写的 Z 之后。例如,在交互式环境中输入以下代码: spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats'] spam.sort() spam ['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats'] 如果需要按照普通的字典顺序来排序,就在sort()方法调用时,将关键字参数 key 设置为str.lower。 spam = ['a', 'z', 'A', 'Z'] spam.sort(key=str.lower) spam ['a', 'A', 'z', 'Z'] 这将导致sort()方法将列表中所有的表项当成小写,但实际上并不会改变它们 在列表中的值。 4.5 例子程序:神奇8 球和列表 前一章我们写过神奇8 球程序。利用列表,可以写出更优雅的版本。不是用一 些几乎一样的elif 语句,而是创建一个列表,针对它编码。打开一个新的文件编辑 器窗口,输入以下代码,并保存为magic8Ball2.py: import random messages = ['It is certain', 'It is decidedly so', 'Yes definitely', 'Reply hazy try again', 'Ask again later', 'Concentrate and ask again', 'My reply is no', 'Outlook not so good', 'Very doubtful'] print(messages[random.randint(0, len(messages) - 1)]) 第4 章 列表 Python 中缩进规则的例外 在大多数情况下,代码行的缩进告诉Python 它属于哪一个代码块。但是, 这个规则有几个例外。例如在源代码文件中,列表实际上可以跨越几行。这些行 的缩进并不重要。Python 知道,没有看到结束方括号,列表就没有结束。例如, 代码可以看起来像这样: spam = ['apples', 'oranges', 'bananas', 'cats'] print(spam) 当然,从实践的角度来说,大部分人会利用Python 的行为,让他们的列表 看起来漂亮且可读,就像神奇8 球程序中的消息列表一样。 也可以在行末使用续行字符\,将一条指令写成多行。可以把\看成是“这条 指令在下一行继续”。\续行字符之后的一行中,缩进并不重要。例如,下面是有 效的Python 代码: print('Four score and seven ' +
'years ago...') 如果希望将一长行的Python 代码安排得更为可读,这些技巧是有用的。 运行这个程序,你会看到它与前面的magic8Ball.py 程序效果一样。 请注意用作messages 下标的表达式:random.randint(0, len(messages) - 1)。这产 生了一个随机数作为下标,不论messages 的大小是多少。也就是说,你会得到0 与 len(messages) - 1 之间的一个随机数。这种方法的好处在于,很容易向列表添加或删 除字符串,而不必改变其他行的代码。如果稍后更新代码,就可以少改几行代码, 引入缺陷的可能性也更小。 4.6 类似列表的类型:字符串和元组 列表并不是唯一表示序列值的数据类型。例如,字符串和列表实际上很相似, 只要你认为字符串是单个文本字符的列表。对列表的许多操作,也可以作用于字符 串:按下标取值、切片、用于for 循环、用于len(),以及用于in 和not in 操作符。 为了看到这种效果,在交互式环境中输入以下代码: name = 'Zophie' name[0] 'Z' name[-2] 'i' name[0:4] 'Zoph' Python 编程快速上手——让繁琐工作自动化 'Zo' in name True 'z' in name False 'p' not in name False for i in name: print(' * * ' + i + ' * * *')
-
-
- Z * * *
-
-
-
- o * * *
-
-
-
- p * * *
-
-
-
- h * * *
-
-
-
- i * * *
-
-
-
- e * * * 4.6.1 可变和不可变数据类型 但列表和字符串在一个重要的方面是不同的。列表是“可变的”数据类型,它 的值可以添加、删除或改变。但是,字符串是“不可变的”,它不能被更改。尝试 对字符串中的一个字符重新赋值,将导致TypeError 错误。在交互式环境中输入以 下代码,你就会看到:
-
name = 'Zophie a cat' name[7] = 'the' Traceback (most recent call last): File "<pyshell#50>", line 1, in name[7] = 'the' TypeError: 'str' object does not support item assignment “改变”一个字符串的正确方式,是使用切片和连接。构造一个“新的”字符 串,从老的字符串那里复制一些部分。在交互式环境中输入以下代码: name = 'Zophie a cat' newName = name[0:7] + 'the' + name[8:12] name 'Zophie a cat' newName 'Zophie the cat' 我们用[0:7]和[8:12]来指那些不想替换的字符。请注意,原来的'Zophie a cat'字 符串没有被修改,因为字符串是不可变的。尽管列表值是可变的,但下面代码中的 第二行并没有修改列表eggs: eggs = [1, 2, 3] eggs = [4, 5, 6] eggs [4, 5, 6] 这里eggs 中的列表值并没有改变,而是整个新的不同的列表值([4, 5, 6]),覆写 了老的列表值。如图4-2 所示。 第4 章 列表 图4-2 当eggs = [4, 5, 6]被执行时,eggs 的内容被新的列表值取代 如果你确实希望修改eggs 中原来的列表,让它包含[4, 5, 6],就要这样做: eggs = [1, 2, 3] del eggs[2] del eggs[1] del eggs[0] eggs.append(4) eggs.append(5) eggs.append(6) eggs [4, 5, 6] 在第一个例子中,eggs 最后的列表值与它开始的列表值是一样的。只是这个列 表被改变了,而不是被覆写。图4-3 展示了前面交互式脚本的例子中,前7 行代码 所做的7 次改动。 图4-3 del 语句和append()方法当场修改了同一个列表值 改变一个可变数据类型的值(就像前面例子中del 语句和append()方法所做的 事),当场改变了该值,因为该变量的值没有被一个新的列表值取代。 区分可变与不可变类型,似乎没有什么意义,但4.7.1 节“传递引用”将解释, 使用可变参数和不可变参数调用函数时产生的不同行为。首先,让我们来看看元组 数据类型,它是列表数据类型的不可变形式。 4.6.2 元组数据类型 除了两个方面,“元组”数据类型几乎与列表数据类型一样。首先,元组输入 Python 编程快速上手——让繁琐工作自动化 时用圆括号(),而不是用方括号[]。例如,在交互式环境中输入以下代码: eggs = ('hello', 42, 0.5) eggs[0] 'hello' eggs[1:3] (42, 0.5) len(eggs) 3 但元组与列表的主要区别还在于,元组像字符串一样,是不可变的。元组不能 让它们的值被修改、添加或删除。在交互式环境中输入以下代码,注意TypeError 出错信息: eggs = ('hello', 42, 0.5) eggs[1] = 99 Traceback (most recent call last): File "<pyshell#5>", line 1, in eggs[1] = 99 TypeError: 'tuple' object does not support item assignment 如果元组中只有一个值,你可以在括号内该值的后面跟上一个逗号,表明这种 情况。否则,Python 将认为,你只是在一个普通括号内输入了一个值。逗号告诉 Python,这是一个元组(不像其他编程语言,Python 接受列表或元组中最后表项后 面跟的逗号)。在交互式环境中,输入以下的type()函数调用,看看它们的区别: type(('hello',)) <class 'tuple'> type(('hello')) <class 'str'> 你可以用元组告诉所有读代码的人,你不打算改变这个序列的值。如果需要一 个永远不会改变的值的序列,就使用元组。使用元组而不是列表的第二个好处在于, 因为它们是不可变的,它们的内容不会变化,Python 可以实现一些优化,让使用元 组的代码比使用列表的代码更快。 4.6.3 用list()和tuple()函数来转换类型 正如str(42)将返回'42',即整数42 的字符串表示形式,函数list()和tuple()将返 回传递给它们的值的列表和元组版本。在交互式环境中输入以下代码,注意返回值 与传入值是不同的数据类型: tuple(['cat', 'dog', 5]) ('cat', 'dog', 5) list(('cat', 'dog', 5)) ['cat', 'dog', 5] list('hello') ['h', 'e', 'l', 'l', 'o'] 如果需要元组值的一个可变版本,将元组转换成列表就很方便。 第4 章 列表 4.7 引用 正如你看到的,变量保存字符串和整数值。在交互式环境中输入以下代码: spam = 42 cheese = spam spam = 100 spam 100 cheese 42 你将42 赋给spam 变量,然后拷贝spam 中的值,将它赋给变量cheese。当稍 后将spam中的值改变为100 时,这不会影响cheese 中的值。这是因为spam和cheese 是不同的变量,保存了不同的值。 但列表不是这样的。当你将列表赋给一个变量时,实际上是将列表的“引用” 赋给了该变量。引用是一个值,指向某些数据。列表引用是指向一个列表的值。这 里有一些代码,让这个概念更容易理解。在交互式环境中输入以下代码: ??>>> spam = [0, 1, 2, 3, 4, 5] ??>>> cheese = spam ??>>> cheese[1] = 'Hello!' spam [0, 'Hello!', 2, 3, 4, 5] cheese [0, 'Hello!', 2, 3, 4, 5] 这可能让你感到奇怪。代码只改变了cheese 列表,但似乎cheese 和spam 列表 同时发生了改变。 当创建列表时?,你将对它的引用赋给了变量。但下一行?只是将spam 中的列 表引用拷贝到cheese,而不是列表值本身。这意味着存储在spam 和cheese 中的值, 现在指向了同一个列表。底下只有一个列表,因为列表本身实际从未复制。所以当 你修改cheese 变量的第一个元素时?,也修改了spam 指向的同一个列表。 记住,变量就像包含着值的盒子。本章前面的图显示列表在盒子中,这并不准 确,因为列表变量实际上没有包含列表,而是包含了对列表的“引用”(这些引用 包含一些ID 数字,Python 在内部使用这些ID,但是你可以忽略)。利用盒子作为 变量的隐喻,图4-4 展示了列表被赋给spam 变量时发生的情形。 图4-4 spam = [0, 1, 2, 3, 4, 5]保存了对列表的引用,而非实际列表 Python 编程快速上手——让繁琐工作自动化 然后,在图4-5 中,spam 中的引用被复制给cheese。只有新的引用被创建并保 存在cheese 中,而非新的列表。请注意,两个引用都指向同一个列表。 图4-5 spam = cheese 复制了引用,而非列表 当你改变cheese 指向的列表时,spam 指向的列表也发生了改变,因为cheese 和spam 都指向同一个列表,如图4-6 所示。 图4-6 cheese[1] = 'Hello!'修改了两个变量指向的列表 变量包含对列表值的引用,而不是列表值本身。但对于字符串和整数值,变量 就包含了字符串或整数值。在变量必须保存可变数据类型的值时,例如列表或字典, Python 就使用引用。对于不可变的数据类型的值,例如字符串、整型或元组,Python 变量就保存值本身。 虽然Python 变量在技术上包含了对列表或字典值的引用,但人们通常随意地 说,该变量包含了列表或字典。 4.7.1 传递引用 要理解参数如何传递给函数,引用就特别重要。当函数被调用时,参数的值被 复制给变元。对于列表(以及字典,我将在下一章中讨论),这意味着变元得到的 是引用的拷贝。要看看这导致的后果,请打开一个新的文件编辑器窗口,输入以下 代码,并保存为passingReference.py: def eggs(someParameter): someParameter.append('Hello') spam = [1, 2, 3] 第4 章 列表 eggs(spam) print(spam) 请注意,当eggs()被调用时,没有使用返回值来为spam 赋新值。相反,它直接 当场修改了该列表。在运行时,该程序产生输出如下: [1, 2, 3, 'Hello'] 尽管spam和someParameter 包含了不同的引用,但它们都指向相同的列表。这就是 为什么函数内的append('Hello')方法调用在函数调用返回后,仍然会对该列表产生影响。 请记住这种行为:如果忘了Python 处理列表和字典变量时采用这种方式,可能 会导致令人困惑的缺陷。 4.7.2 copy 模块的copy()和deepcopy()函数 在处理列表和字典时,尽管传递引用常常是最方便的方法,但如果函数修改了 传入的列表或字典,你可能不希望这些变动影响原来的列表或字典。要做到这一点, Python 提供了名为copy 的模块,其中包含copy()和deepcopy()函数。第一个函数 copy.copy(),可以用来复制列表或字典这样的可变值,而不只是复制引用。在交互 式环境中输入以下代码: import copy spam = ['A', 'B', 'C', 'D'] cheese = copy.copy(spam) cheese[1] = 42 spam ['A', 'B', 'C', 'D'] cheese ['A', 42, 'C', 'D'] 现在spam 和cheese 变量指向独立的列表,这就是为什么当你将42 赋给下标7 时,只有cheese 中的列表被改变。在图4-7 中可以看到,两个变量的引用ID 数字 不再一样,因为它们指向了独立的列表。 图4-7 cheese = copy.copy(spam)创建了第二个列表,能独立于第一个列表修改 如果要复制的列表中包含了列表,那就使用copy.deepcopy()函数来代替。 Python 编程快速上手——让繁琐工作自动化 deepcopy()函数将同时复制它们内部的列表。 4.8 小结 列表是有用的数据类型,因为它们让你写代码处理一组可以修改的值,同时仅 用一个变量。在本书后面的章节中,你会看到一些程序利用列表来完成工作。没有 列表,这些工作很困难,甚至不可能完成。 列表是可变的,这意味着它们的内容可以改变。元组和字符串虽然在某些方面 像列表,却是不可变的,不能被修改。包含一个元组或字符串的变量,可以被一个 新的元组或字符串覆写,但这和现场修改原来的值不是一回事,不像append()和 remove()方法在列表上的效果。 变量不直接保存列表值,它们保存对列表的“引用”。在复制变量或将列表作 为函数调用的参数时,这一点很重要。因为被复制的只是列表引用,所以要注意, 对该列表的所有改动都可能影响到程序中的其他变量。如果需要对一个变量中的列 表修改,同时不修改原来的列表,就可以用copy()或deepcopy()。 4.9 习题 1.什么是[]? 2.如何将'hello'赋给列表的第三个值,而列表保存在名为spam 的变量中?(假 定变量包含[2, 4, 6, 8, 10])。 对接下来的3 个问题,假定spam 包含列表['a', 'b', 'c', 'd']。 3.spam[int('3' * 2) / 11]求值为多少? 4.spam[-1]求值为多少? 5.spam[:2]求值为多少? 对接下来的3 个问题。假定bacon 包含列表[3.14, 'cat', 11, 'cat', True]。 6.bacon.index('cat')求值为多少? 7.bacon.append(99)让bacon 中___________的列表值变成什么样? 8.bacon.remove('cat')让bacon 中的列表时变成什么样? 9.列表连接和复制的操作符是什么? 10.append()和insert()列表方法之间的区别是什么? 11.从列表中删除值有哪两种方法? 12.请说出列表值和字符串的几点相似之处。 13.列表和元组之间的区别是什么? 14.如果元组中只有一个整数值42,如何输入该元组? 15.如何从列表值得到元组形式?如何从元组值得到列表形式? 16.“包含”列表的变量,实际上并未真地直接包含列表。它们包含的是什么? 第4 章 列表 17.copy.copy()和copy.deepcopy()之间的区别是什么? 4.10 实践项目 作为实践,编程完成下列任务。 4.10.1 逗号代码 假定有下面这样的列表: spam = ['apples', 'bananas', 'tofu', 'cats'] 编写一个函数,它以一个列表值作为参数,返回一个字符串。该字符串包含所 有表项,表项之间以逗号和空格分隔,并在最后一个表项之前插入and。例如,将 前面的spam 列表传递给函数,将返回'apples, bananas, tofu, and cats'。但你的函数应 该能够处理传递给它的任何列表。 4.10.2 字符图网格 假定有一个列表的列表,内层列表的每个值都是包含一个字符的字符串,像这样: grid = [['.', '.', '.', '.', '.', '.'], ['.', 'O', 'O', '.', '.', '.'], ['O', 'O', 'O', 'O', '.', '.'], ['O', 'O', 'O', 'O', 'O', '.'], ['.', 'O', 'O', 'O', 'O', 'O'], ['O', 'O', 'O', 'O', 'O', '.'], ['O', 'O', 'O', 'O', '.', '.'], ['.', 'O', 'O', '.', '.', '.'], ['.', '.', '.', '.', '.', '.']] 你可以认为grid[x][y]是一幅“图”在x、y 坐标处的字符,该图由文本字符组 成。原点(0, 0)在左上角,向右x 坐标增加,向下y 坐标增加。 复制前面的网格值,编写代码用它打印出图像。 ..OO.OO.. .OOOOOOO. .OOOOOOO. ..OOOOO.. ...OOO... ....O.... 提示 你需要使用循环嵌套循环,打印出grid[0][0],然后grid[1][0],然后grid[2][0],以此 类推,直到grid[8][0]。这就完成第一行,所以接下来打印换行。然后程序将打印出 grid[0][1],然后grid[1][1],然后grid[2][1],以此类推。程序最后将打印出grid[8][5]。 而且,如果你不希望在每次print()调用后都自动打印换行,记得向print()传递 end 关键字参数。 第 章 字典和结构化数据 在本章中,我将介绍字典数据类型,它提供了一种灵活 的访问和组织数据的方式。然后,结合字典与前一章中关于 列表的知识,你将学习如何创建一个数据结构,对井字棋盘 建模。 5.1 字典数据类型 像列表一样,“字典”是许多值的集合。但不像列表的下标,字典的索引可以 使用许多不同数据类型,不只是整数。字典的索引被称为“键”,键及其关联的值 称为“键-值”对。 在代码中,字典输入时带花括号{}。在交互式环境中输入以下代码: myCat = {'size': 'fat', 'color': 'gray', 'disposition': 'loud'} 这将一个字典赋给myCat 变量。这个字典的键是'size'、'color'和'disposition'。这 些键相应的值是'fat'、'gray'和'loud'。可以通过它们的键访问这些值: 5 82 Python 编程快速上手——让繁琐工作自动化 myCat['size'] 'fat' 'My cat has ' + myCat['color'] + ' fur.' 'My cat has gray fur.' 字典仍然可以用整数值作为键,就像列表使用整数值作为下标一样,但它们不 必从0 开始,可以是任何数字。 spam = {12345: 'Luggage Combination', 42: 'The Answer'} 5.1.1 字典与列表 不像列表,字典中的表项是不排序的。名为spam 的列表中,第一个表项是spam[0]。 但字典中没有“第一个”表项。虽然确定两个列表是否相同时,表项的顺序很重要, 但在字典中,键-值对输入的顺序并不重要。在交互式环境中输入以下代码: spam = ['cats', 'dogs', 'moose'] bacon = ['dogs', 'moose', 'cats'] spam == bacon False eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'} ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'} eggs == ham True 因为字典是不排序的,所以不能像列表那样切片。 尝试访问字典中不存在的键,将导致KeyError 出错信息。这很像列表的“越界” IndexError 出错信息。在交互式环境中输入以下代码,并注意显示的出错信息,因 为没有'color'键: spam = {'name': 'Zophie', 'age': 7} spam['color'] Traceback (most recent call last): File "<pyshell#1>", line 1, in spam['color'] KeyError: 'color' 尽管字典是不排序的,但可以用任意值作为键,这一点让你能够用强大的方式来 组织数据。假定你希望程序保存朋友生日的数据,就可以使用一个字典,用名字作为 键,用生日作为值。打开一个新的文件编辑窗口,输入以下代码,并保存为birthdays.py: ? birthdays = {'Alice': 'Apr 1', 'Bob': 'Dec 12', 'Carol': 'Mar 4'} while True: print('Enter a name: (blank to quit)') name = input() if name == '': break ? if name in birthdays: ? print(birthdays[name] + ' is the birthday of ' + name) else: print('I do not have birthday information for ' + name) print('What is their birthday?') 第5 章 字典和结构化数据 83 bday = input() ? birthdays[name] = bday print('Birthday database updated.') 你创建了一个初始的字典,将它保存在birthdays 中?。用in 关键字,可以看看输入 的名字是否作为键存在于字典中?,就像查看列表一样。如果该名字在字典中,你可以用 方括号访问关联的值?。如果不在,你可以用同样的方括号语法和赋值操作符添加它?。 运行这个程序,结果看起来如下所示: Enter a name: (blank to quit) Alice Apr 1 is the birthday of Alice Enter a name: (blank to quit) Eve I do not have birthday information for Eve What is their birthday? Dec 5 Birthday database updated. Enter a name: (blank to quit) Eve Dec 5 is the birthday of Eve Enter a name: (blank to quit) 当然,在程序终止时,你在这个程序中输入的所有数据都丢失了。在第 8 章中, 你将学习如何将数据保存在硬盘的文件中。 5.1.2 keys()、values()和items()方法 有3 个字典方法,它们将返回类似列表的值,分别对应于字典的键、值和键-值对: keys()、values()和items()。这些方法返回的值不是真正的列表,它们不能被修改,没有 append()方法。但这些数据类型(分别是dict_keys、dict_values 和dict_items)可以用于 for 循环。为了看看这些方法的工作原理,请在交互式环境中输入以下代码: spam = {'color': 'red', 'age': 42} for v in spam.values(): print(v) red 42 这里,for 循环迭代了spam 字典中的每个值。for 循环也可以迭代每个键,或 者键-值对: for k in spam.keys(): print(k) color age for i in spam.items(): print(i) ('color', 'red') ('age', 42) 84 Python 编程快速上手——让繁琐工作自动化 利用keys()、values()和items()方法,循环分别可以迭代键、值或键-值对。请注 意,items()方法返回的dict_items 值中,包含的是键和值的元组。 如果希望通过这些方法得到一个真正的列表,就把类似列表的返回值传递给 list 函数。在交互式环境中输入以下代码: spam = {'color': 'red', 'age': 42} spam.keys() dict_keys(['color', 'age']) list(spam.keys()) ['color', 'age'] list(spam.keys())代码行接受keys()函数返回的dict_keys 值,并传递给list()。然 后返回一个列表,即['color', 'age']。 也可以利用多重赋值的技巧,在for 循环中将键和值赋给不同的变量。在交互 式环境中输入以下代码: spam = {'color': 'red', 'age': 42} for k, v in spam.items(): print('Key: ' + k + ' Value: ' + str(v)) Key: age Value: 42 Key: color Value: red 5.1.3 检查字典中是否存在键或值 回忆一下,前一章提到,in 和not in 操作符可以检查值是否存在于列表中。也 可以利用这些操作符,检查某个键或值是否存在于字典中。在交互式环境中输入以 下代码: spam = {'name': 'Zophie', 'age': 7} 'name' in spam.keys() True 'Zophie' in spam.values() True 'color' in spam.keys() False 'color' not in spam.keys() True 'color' in spam False 请注意,在前面的例子中,'color' in spam 本质上是一个简写版本。相当于'color' in spam.keys()。这种情况总是对的:如果想要检查一个值是否为字典中的键,就可 以用关键字in(或not in),作用于该字典本身。 5.1.4 get()方法 在访问一个键的值之前,检查该键是否存在于字典中,这很麻烦。好在,字典有一 个get()方法,它有两个参数:要取得其值的键,以及如果该键不存在时,返回的备用值。 第5 章 字典和结构化数据 85 在交互式环境中输入以下代码: picnicItems = {'apples': 5, 'cups': 2} 'I am bringing ' + str(picnicItems.get('cups', 0)) + ' cups.' 'I am bringing 2 cups.' 'I am bringing ' + str(picnicItems.get('eggs', 0)) + ' eggs.' 'I am bringing 0 eggs.' 因为picnicItems 字典中没有'egg'键,get()方法返回的默认值是0。不使用get(), 代码就会产生一个错误消息,就像下面的例子: picnicItems = {'apples': 5, 'cups': 2} 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' Traceback (most recent call last): File "<pyshell#34>", line 1, in 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' KeyError: 'eggs' 5.1.5 setdefault()方法 你常常需要为字典中某个键设置一个默认值,当该键没有任何值时使用它。代 码看起来像这样: spam = {'name': 'Pooka', 'age': 5} if 'color' not in spam: spam['color'] = 'black' setdefault()方法提供了一种方式,在一行中完成这件事。传递给该方法的第一 个参数,是要检查的键。第二个参数,是如果该键不存在时要设置的值。如果该键 确实存在,方法就会返回键的值。在交互式环境中输入以下代码: spam = {'name': 'Pooka', 'age': 5} spam.setdefault('color', 'black') 'black' spam {'color': 'black', 'age': 5, 'name': 'Pooka'} spam.setdefault('color', 'white') 'black' spam {'color': 'black', 'age': 5, 'name': 'Pooka'} 第一次调用setdefault()时,spam 变量中的字典变为{'color': 'black', 'age': 5, 'name': 'Pooka'}。该方法返回值'black',因为现在该值被赋给键'color'。当spam.setdefault('color', 'white')接下来被调用时,该键的值“没有”被改变成'white',因为spam 变量已经有 名为'color'的键。 setdefault()方法是一个很好的快捷方式,可以确保一个键存在。下面有一个小 程序,计算一个字符串中每个字符出现的次数。打开一个文件编辑器窗口,输入以 下代码,保存为characterCount.py: message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} 86 Python 编程快速上手——让繁琐工作自动化 for character in message: count.setdefault(character, 0) count[character] = count[character] + 1 print(count) 程序循环迭代message 字符串中的每个字符,计算每个字符出现的次数。setdefault() 方法调用确保了键存在于count 字典中(默认值是0),这样在执行count[character] = count[character] + 1 时,就不会抛出KeyError 错误。程序运行时,输出如下: {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'c': 3, 'b': 1, 'e': 5, 'd': 3, 'g': 2, 'i': 6, 'h': 3, 'k': 2, 'l': 3, 'o': 2, 'n': 4, 'p': 1, 's': 3, 'r': 5, 't': 6, 'w': 2, 'y': 1} 从输出可以看到,小写字母c 出现了3 次,空格字符出现了13 次,大写字母A 出现了1 次。无论message 变量中包含什么样的字符串,这个程序都能工作,即使 该字符串有上百万的字符! 5.2 漂亮打印 如果程序中导入pprint 模块,就可以使用pprint()和pformat()函数,它们将“漂亮 打印”一个字典的字。如果想要字典中表项的显示比print()的输出结果更干净,这就有 用了。修改前面的characterCount.py 程序,将它保存为prettyCharacterCount.py。 import pprint message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} for character in message: count.setdefault(character, 0) count[character] = count[character] + 1 pprint.pprint(count) 这一次,当程序运行时,输出看起来更干净,键排过序。 {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'b': 1, 'c': 3, 'd': 3, 'e': 5, 'g': 2, 'h': 3, 'i': 6, 'k': 2, 'l': 3, 'n': 4, 'o': 2, 'p': 1, 第5 章 字典和结构化数据 87 'r': 5, 's': 3, 't': 6, 'w': 2, 'y': 1} 如果字典本身包含嵌套的列表或字典,pprint.pprint()函数就特别有用。 如果希望得到漂亮打印的文本作为字符串,而不是显示在屏幕上,那就调用 pprint.pformat()。下面两行代码是等价的: pprint.pprint(someDictionaryValue) print(pprint.pformat(someDictionaryValue)) 5.3 使用数据结构对真实世界建模 甚至在因特网之前,人们也有办法与世界另一边的某人下一盘国际象棋。每个 棋手在自己家里放好一个棋盘,然后轮流向对方寄出明信片,描述每一着棋。要做 到这一点,棋手需要一种方法,无二义地描述棋盘的状态,以及他们的着法。 在“代数记谱法”中,棋盘空间由一个数字和字母坐标确定,如图5-1 所示。 图5-1 代数记谱法中棋盘的坐标 棋子用字母表示:K 表示王,Q 表示后,R 表示车,B 表示象,N 表示马。描述一 次移动,用棋子的字母和它的目的地坐标。一对这样的移动表示一个回合(白方先下), 例如,棋谱2. Nf3 Nc6 表明在棋局的第二回合,白方将马移动到f3,黑方将马移动到c6。 代数记谱法还有更多内容,但要点是你可以用它无二义地描述象棋游戏,不需 要站在棋盘前。你的对手甚至可以在世界的另一边!实际上,如果你的记忆力很好, 甚至不需要物理的棋具:只需要阅读寄来的棋子移动,更新心里想的棋盘。 计算机有很好的记忆力。现在计算机上的程序,很容易存储几百万个像'2. Nf3 Nc6'这样的字符串。这就是为什么计算机不用物理棋盘就能下象棋。它们用数据建 模来表示棋盘,你可以编写代码来使用这个模型。 这里就可以用到列表和字典。可以用它们对真实世界建模,例如棋盘。作为第 88 Python 编程快速上手——让繁琐工作自动化 一个例子,我们将使用比国际象棋简单一点的游戏:井字棋。 5.3.1 井字棋盘 井字棋盘看起来像一个大的井字符号(#),有9 个空格,可以包含X、O 或空。 要用字典表示棋盘,可以为每个空格分配一个字符串键,如图5-2 所示。 图5-2 井字棋盘的空格和它们对应的键 可以用字符串值来表示,棋盘上每个空格有什么:'X'、'O'或' '(空格字符)。因此, 需要存储9 个字符串。可以用一个字典来做这事。带有键'top-R'的字符串表示右上角, 带有键'low-L'的字符串表示左下角,带有键'mid-M'的字符串表示中间,以此类推。 这个字典就是表示井字棋盘的数据结构。将这个字典表示的棋盘保存在名为 theBoard 的变量中。打开一个文件编辑器窗口,输入以下代码,并保存为 ticTacToe.py: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} 保存在theBoard 变量中的数据结构,表示了图5-3 中的井字棋盘。 图5-3 一个空的井字棋盘 因为theBoard 变量中每个键的值都是单个空格字符,所以这个字典表示一个完 全干净的棋盘。如果玩家X 选择了中间的空格,就可以用下面这个字典来表示棋盘: 第5 章 字典和结构化数据 89 theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} theBoard 变量中的数据结构现在表示图5-4 中的井字棋盘。 图5-4 第一着 一个玩家O 获胜的棋盘上,他将O 横贯棋盘的顶部,看起来像这样: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} theBoard 变量中的数据结构现在表示图5-5 中的井字棋盘。 图5-5 玩家O 获胜 当然,玩家只看到打印在屏幕上的内容,而不是变量的内容。让我们创建一个 函数,将棋盘字典打印到屏幕上。将下面代码添加到ticTacToe.py(新代码是黑体的): theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) 运行这个程序时,printBoard()将打印出空白井字棋盘。 90 Python 编程快速上手——让繁琐工作自动化 | | -+-+- | | -+-+- | | printBoard()函数可以处理传入的任何井字棋数据结构。尝试将代码改成以下的样子: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) 现在运行该程序,新棋盘将打印在屏幕上。 O|O|O -+-+- X|X| -+-+- | |X 因为你创建了一个数据结构来表示井字棋盘,编写了printBoard()中的代码来解 释该数据结构,所以就有了一个程序,对井字棋盘进行了“建模”。也可以用不同 的方式组织数据结构(例如,使用'TOP-LEFT'这样的键来代替'top-L'),但只要代码 能处理你的数据结构,就有了正确工作的程序。 例如,printBoard()函数预期井字棋数据结构是一个字典,包含所有9 个空格的 键。假如传入的字典缺少'mid-L'键,程序就不能工作了。 O|O|O -+-+- Traceback (most recent call last): File "ticTacToe.py", line 10, in printBoard(theBoard) File "ticTacToe.py", line 6, in printBoard print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) KeyError: 'mid-L' 现在让我们添加代码,允许玩家输入他们的着法。修改ticTacToe.py 程序如下所示: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) turn = 'X' for i in range(9): ? printBoard(theBoard) 第5 章 字典和结构化数据 91 print('Turn for ' + turn + '. Move on which space?') ? move = input() ? theBoard[move] = turn ? if turn == 'X': turn = 'O' else: turn = 'X' p rintBoard(theBoard) 新的代码在每一步新的着法之前,打印出棋盘?,获取当前棋手的着法?,相 应地更新棋盘?,然后改变当前棋手?,进入到下一着。 运行该程序,它看起来像这样: | | -+-+- | | -+-+- | | Turn for X. Move on which space? mid-M | | -+-+- |X| -+-+- | | Turn for O. Move on which space? low-L | | -+-+- |X| -+-+- O| | --snip-- O|O|X -+-+- X|X|O -+-+- O| |X Turn for X. Move on which space? low-M O|O|X -+-+- X|X|O -+-+- O|X|X 这不是一个完整的井字棋游戏(例如,它并不检查玩家是否获胜),但这已足 够展示如何在程序中使用数据结构。 注意 如果你很好奇,完整的井字棋程序的源代码在网上有介绍,网址是 http://nostarch.com/automatestuff/。 5.3.2 嵌套的字典和列表 对井字棋盘建模相当简单:棋盘只需要一个字典,包含9 个键值对。当你对复 92 Python 编程快速上手——让繁琐工作自动化 杂的事物建模时,可能发现字典和列表中需要包含其他字典和列表。列表适用于包 含一组有序的值,字典适合于包含关联的键与值。例如,下面的程序使用字典包含 其他字典,用于记录谁为野餐带来了什么食物。totalBrought()函数可以读取这个数 据结构,计算所有客人带来的食物的总数。 allGuests = {'Alice': {'apples': 5, 'pretzels': 12}, 'Bob': {'ham sandwiches': 3, 'apples': 2}, 'Carol': {'cups': 3, 'apple pies': 1}} def totalBrought(guests, item): numBrought = 0 ? for k, v in guests.items(): ? numBrought = numBrought + v.get(item, 0) return numBrought print('Number of things being brought:') print(' - Apples ' + str(totalBrought(allGuests, 'apples'))) print(' - Cups ' + str(totalBrought(allGuests, 'cups'))) print(' - Cakes ' + str(totalBrought(allGuests, 'cakes'))) print(' - Ham Sandwiches ' + str(totalBrought(allGuests, 'ham sandwiches'))) p rint(' - Apple Pies ' + str(totalBrought(allGuests, 'apple pies'))) 在totalBrought()函数中,for 循环迭代guests 中的每个键值对?。在这个循环里, 客人的名字字符串赋给k,他们带来的野餐食物的字典赋给v。如果食物参数是字 典中存在的键,它的值(数量)就添加到numBrought?。如果它不是键,get()方法 就返回0,添加到numBrought。 该程序的输出像这样: Number of things being brought:
- Apples 7
- Cups 3
- Cakes 0
- Ham Sandwiches 3
- Apple Pies 1 这似乎对一个非常简单的东西建模,你可能认为不需要费事去写一个程序来做 到这一点。但是要认识到,这个函数totalBrought()可以轻易地处理一个字典,其中 包含数千名客人,每个人都带来了“数千种”不同的野餐食物。这样用这种数据结 构来保存信息,并使用totalBrought()函数,就会节约大量的时间! 你可以用自己喜欢的任何方法,用数据结构对事物建模,只要程序中其他代码能够 正确处理这个数据模型。在刚开始编程时,不需要太担心数据建模的“正确”方式。随 着经验增加,你可能会得到更有效的模型,但重要的是,该数据模型符合程序的需要。 5.4 小结 在本章中,你学习了字典的所有相关知识。列表和字典是这样的值,它们可以 包含多个值,包括其他列表和字典。字典是有用的,因为你可以把一些项(键)映 第5 章 字典和结构化数据 93 射到另一些项(值),它不像列表,只包含一系列有序的值。字典中的值是通过方 括号访问的,像列表一样。字典不是只能使用整数下标,而是可以用各种数据类型 作为键:整型、浮点型、字符串或元组。通过将程序中的值组织成数据结构,你可 以创建真实世界事物的模型。井字棋盘就是这样一个例子。 这就介绍了Python 编程的所有基本概念!在本书后面的部分,你将继续学习一 些新概念,但现在你已学习了足够多的内容,可以开始编写一些有用的程序,让一 些任务自动化。你可能不觉得自己有足够的Python 知识,来实现页面下载、更新电 子表格,或发送文本消息。但这就是Python 模块要干的事!这些模块由其他程序员 编写,提供了一些函数,让这些事情变得容易。所以让我们学习如何编写真正的程 序,实现有用的自动化任务。 5.5 习题 1.空字典的代码是怎样的? 2.一个字典包含键'fow'和值42,看起来是怎样的? 3.字典和列表的主要区别是什么? 4.如果spam 是{'bar': 100},你试图访问spam['foo'],会发生什么? 5.如果一个字典保存在spam 中,表达式'cat' in spam 和'cat' in spam.keys()之间 的区别是什么? 6.如果一个字典保存在变量中,表达式'cat' in spam 和'cat' in spam.values()之间 的区别是什么? 7.下面代码的简洁写法是什么? if 'color' not in spam: spam['color'] = 'black' 8.什么模块和函数可以用于“漂亮打印”字典值? 5.6 实践项目 作为实践,编程完成下列任务。 5.6.1 好玩游戏的物品清单 你在创建一个好玩的视频游戏。用于对玩家物品清单建模的数据结构是一个字 典。其中键是字符串,描述清单中的物品,值是一个整型值,说明玩家有多少该物 品。例如,字典值{'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}意味着玩 家有1 条绳索、6 个火把、42 枚金币等。 写一个名为displayInventory()的函数,它接受任何可能的物品清单,并显示如下: 94 Python 编程快速上手——让繁琐工作自动化 Inventory: 12 arrow 42 gold coin 1 rope 6 torch 1 dagger Total number of items: 62 提示 你可以使用for 循环,遍历字典中所有的键。
stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} def displayInventory(inventory): print("Inventory:") item_total = 0 for k, v in inventory.items(): print(str(v) + ' ' + k) item_total += v print("Total number of items: " + str(item_total)) displayInventory(stuff) 5.6.2 列表到字典的函数,针对好玩游戏物品清单 假设征服一条龙的战利品表示为这样的字符串列表: dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] 写一个名为addToInventory(inventory, addedItems)的函数,其中inventory 参数 是一个字典,表示玩家的物品清单(像前面项目一样),addedItems 参数是一个列表, 就像dragonLoot。 addToInventory()函数应该返回一个字典,表示更新过的物品清单。请注意,列 表可以包含多个同样的项。你的代码看起来可能像这样: def addToInventory(inventory, addedItems):
inv = {'gold coin': 42, 'rope': 1} dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] inv = addToInventory(inv, dragonLoot) displayInventory(inv) 前面的程序(加上前一个项目中的displayInventory()函数)将输出如下: Inventory: 45 gold coin 1 rope 1 ruby 1 dagger Total number of items: 48 第 章 字符串操作 文本是程序需要处理的最常见的数据形式。你已经知道如 何用+操作符连接两个字符串,但能做的事情还要多得多。可 以从字符串中提取部分字符串,添加或删除空白字符,将字母 转换成小写或大写,检查字符串的格式是否正确。你甚至可以 编写Python 代码访问剪贴板,复制或粘贴文本。 在本章中,你将学习所有这些内容和更多内容。然后你会 看到两个不同的编程项目:一个是简单的口令管理器,另一个 将枯燥的文本格式化工作自动化。 6.1 处理字符串 让我们来看看,Python 提供的写入、打印和访问字符串的一些方法。 6.1.1 字符串字面量 在Python 中输入字符串值相当简单的:它们以单引号开始和结束。但是如何才 能在字符串内使用单引号呢?输入'That is Alice's cat.'是不行的,因为Python 认为这 个字符串在Alice 之后就结束了,剩下的(s cat.')是无效的Python 代码。好在,有 6 2 Python 编程快速上手——让繁琐工作自动化 几种方法来输入字符串。 6.1.2 双引号 字符串可以用双引号开始和结束,就像用单引号一样。使用双引号的一个好处, 就是字符串中可以使用单引号字符。在交互式环境中输入以下代码:
spam = "That is Alice's cat." 因为字符串以双引号开始,所以Python 知道单引号是字符串的一部分,而不是 表示字符串的结束。但是,如果在字符串中既需要使用单引号又需要使用双引号, 那就要使用转义字符。 6.1.3 转义字符 “转义字符”让你输入一些字符,它们用其他方式是不可能放在字符串里的。转义 字符包含一个倒斜杠(\),紧跟着是想要添加到字符串中的字符。(尽管它包含两个字符, 但大家公认它是一个转义字符。)例如,单引号的转义字符是\’。你可以在单引号开始和 结束的字符串中使用它。为了看看转义字符的效果,在交互式环境中输入以下代码: spam = 'Say hi to Bob's mother.' Python 知道,因为Bob's 中的单引号有一个倒斜杠,所以它不是表示字符串结 束的单引号。转义字符'和"让你能在字符串中加入单引号和双引号。 表6-1 列出了可用的转义字符。 表6-1 转义字符 转义字符 打印为 ' 单引号 " 双引号 \t 制表符 \n 换行符 \ 倒斜杠 在交互式环境中输入以下代码: print("Hello there!\nHow are you?\nI'm doing fine.") Hello there! How are you? I'm doing fine. 6.1.4 原始字符串 可以在字符串开始的引号之前加上r,使它成为原始字符串。“原始字符串”完 全忽略所有的转义字符,打印出字符串中所有的倒斜杠。例如,在交互式环境中输 入以下代码: 第6 章 字符串操作 print(r'That is Carol's cat.') That is Carol's cat. 因为这是原始字符串,Python 认为倒斜杠是字符串的一部分,而不是转义字符 的开始。如果输入的字符串包含许多倒斜杠,比如下一章中要介绍的正则表达式字 符串,那么原始字符串就很有用。 6.1.5 用三重引号的多行字符串 虽然可以用\n转义字符将换行放入一个字符串,但使用多行字符串通常更容易。 在Python 中,多行字符串的起止是3 个单引号或3 个双引号。“三重引号”之间的 所有引号、制表符或换行,都被认为是字符串的一部分。Python 的代码块缩进规则 不适用于多行字符串。 打开文件编辑器,输入以下代码: print('''Dear Alice, Eve's cat has been arrested for catnapping, cat burglary, and extortion. Sincerely, Bob''') 将该程序保存为catnapping.py 并运行。输出看起来像这样: Dear Alice, Eve's cat has been arrested for catnapping, cat burglary, and extortion. Sincerely, Bob 请注意,Eve's 中的单引号字符不需要转义。在原始字符串中,转义单引号和双 引号是可选的。下面的print()调用将打印出同样的文本,但没有使用多行字符串: print('Dear Alice,\n\nEve's cat has been arrested for catnapping, cat burglary, and extortion.\n\nSincerely,\nBob') 6.1.6 多行注释 虽然井号字符(#)表示这一行是注释,但多行字符串常常用作多行注释。下 面是完全有效的Python 代码: """This is a test Python program. Written by Al Sweigart [email protected] This program was designed for Python 3, not Python 2. """ def spam(): """This is a multiline comment to help explain what the spam() function does.""" print('Hello!') Python 编程快速上手——让繁琐工作自动化 6.1.7 字符串下标和切片 字符串像列表一样,使用下标和切片。可以将字符串'Hello world!'看成是一个 列表,字符串中的每个字符都是一个表项,有对应的下标。 ' H e l l o w o r l d ! ' 0 1 2 3 4 5 6 7 8 9 10 11 字符计数包含了空格和感叹号,所以'Hello world!'有12 个字符,H 的下标是0,! 的下标是11。在交互式环境中输入以下代码: spam = 'Hello world!' spam[0] 'H' spam[4] 'o' spam[-1] '!' spam[0:5] 'Hello' spam[:5] 'Hello' spam[6:] 'world!' 如果指定一个下标,你将得到字符串在该处的字符。如果用一个下标和另一个 下标指定一个范围,开始下标将被包含,结束下标则不包含。因此,如果spam是'Hello world!',spam[0:5]就是'Hello'。通过spam[0:5]得到的子字符串,将包含spam[0]到 spam[4]的全部内容,而不包括下标5 处的空格。 请注意,字符串切片并没有修改原来的字符串。可以从一个变量中获取切片, 记录在另一个变量中。在交互式环境中输入以下代码: spam = 'Hello world!' fizz = spam[0:5] fizz 'Hello' 通过切片并将结果子字符串保存在另一个变量中,就可以同时拥有完整的字符 串和子字符串,便于快速简单的访问。 6.1.8 字符串的in 和not in 操作符 像列表一样,in 和not in 操作符也可以用于字符串。用in 或not in 连接两个字 符串得到的表达式,将求值为布尔值True 或False。在交互式环境中输入以下代码: 'Hello' in 'Hello World' True 'Hello' in 'Hello' True 'HELLO' in 'Hello World' False '' in 'spam' 第6 章 字符串操作 True 'cats' not in 'cats and dogs' False 这些表达式测试第一个字符串(精确匹配,区分大小写)是否在第二个字符串中。 6.2 有用的字符串方法 一些字符串方法会分析字符串,或生成转变过的字符串。本节介绍了这些方法, 你会经常使用它们。 6.2.1 字符串方法upper()、lower()、isupper()和islower() upper()和lower()字符串方法返回一个新字符串,其中原字符串的所有字母都被 相应地转换为大写或小写。字符串中非字母字符保持不变。 在交互式环境中输入以下代码: spam = 'Hello world!' spam = spam.upper() spam 'HELLO WORLD!' spam = spam.lower() spam 'hello world!' 请注意,这些方法没有改变字符串本身,而是返回一个新字符串。如果你希望改 变原来的字符串,就必须在该字符串上调用upper()或lower(),然后将这个新字符串 赋给保存原来字符串的变量。这就是为什么必须使用 spam = spam.upper(),才能改变 spam 中的字符串,而不是仅仅使用spam.upper()(这就好比,如果变量eggs 中包含 值10,写下eggs + 3 并不会改变eggs 的值,但是eggs = eggs + 3 会改变egg 的值)。 如果需要进行大小写无关的比较,upper()和lower()方法就很有用。字符串'great' 和'GREat'彼此不相等。但在下面的小程序中,用户输入Great、GREAT 或grEAT 都 没关系,因为字符串首先被转换成小写。 print('How are you?') feeling = input() if feeling.lower() == 'great': print('I feel great too.') else: print('I hope the rest of your day is good.') 在运行该程序时,先显示问题,然后输入变形的great,如GREat,程序将给出 输出I feel great too。在程序中加入代码,处理多种用户输入情况或输入错误,诸如 大小写不一致,这会让程序更容易使用,且更不容易失效。 How are you? GREat I feel great too. Python 编程快速上手——让繁琐工作自动化 如果字符串至少有一个字母,并且所有字母都是大写或小写,isupper()和 islower()方法就会相应地返回布尔值True。否则,该方法返回False。在交互式环境 中输入以下代码,并注意每个方法调用的返回值: spam = 'Hello world!' spam.islower() False spam.isupper() False 'HELLO'.isupper() True 'abc12345'.islower() True '12345'.islower() False '12345'.isupper() False 因为upper()和lower()字符串方法本身返回字符串,所以也可以在“那些”返回 的字符串上继续调用字符串方法。这样做的表达式看起来就像方法调用链。在交互 式环境中输入以下代码: 'Hello'.upper() 'HELLO' 'Hello'.upper().lower() 'hello' 'Hello'.upper().lower().upper() 'HELLO' 'HELLO'.lower() 'hello' 'HELLO'.lower().islower() True 6.2.2 isX 字符串方法 除了islower()和isupper(),还有几个字符串方法,它们的名字以is 开始。这些 方法返回一个布尔值,描述了字符串的特点。下面是一些常用的isX 字____________符串方法: ??isalpha()返回True,如果字符串只包含字母,并且非空; ??isalnum()返回True,如果字符串只包含字母和数字,并且非空; ??isdecimal()返回True,如果字符串只包含数字字符,并且非空; ??isspace()返回True,如果字符串只包含空格、制表符和换行,并且非空; ??? istitle()返回True,如果字符串仅包含以大写字母开头、后面都是小写字母的单词。 在交互式环境中输入以下代码: 'hello'.isalpha() True 'hello123'.isalpha() False 'hello123'.isalnum() True 第6 章 字符串操作 'hello'.isalnum() True '123'.isdecimal() True ' '.isspace() True 'This Is Title Case'.istitle() True 'This Is Title Case 123'.istitle() True 'This Is not Title Case'.istitle() False 'This Is NOT Title Case Either'.istitle() False 如果需要验证用户输入,isX 字符串方法是有用的。例如,下面的程序反复询 问用户年龄和口令,直到他们提供有效的输入。打开一个新的文件编辑器窗口,输 入以下程序,保存为validateInput.py: while True: print('Enter your age:') age = input() if age.isdecimal(): break print('Please enter a number for your age.') while True: print('Select a new password (letters and numbers only):') password = input() if password.isalnum(): break print('Passwords can only have letters and numbers.') 在第一个while 循环中,我们要求用户输入年龄,并将输入保存在age 中。如 果age 是有效的值(数字),我们就跳出第一个while 循环,转向第二个循环,询问 口令。否则,我们告诉用户需要输入数字,并再次要求他们输入年龄。在第二个 while 循环中,我们要求输入口令,客户的输入保存在password 中。如果输入是字 母或数字,就跳出循环。如果不是,我们并不满意,于是告诉用户口令必须是字母 或数字,并再次要求他们输入口令。 如果运行,该程序的输出看起来如下: Enter your age: forty two Please enter a number for your age. Enter your age: 42 Select a new password (letters and numbers only): secr3t! Passwords can only have letters and numbers. Select a new password (letters and numbers only): secr3t 在变量上调用isdecimal()和isalnum(),我们就能够测试保存在这些变量中的值 是否为数字,是否为字母或数字。这里,这些测试帮助我们拒绝输入forty two,接 Python 编程快速上手——让繁琐工作自动化 受42,拒绝secr3t!,接受secr3t。 6.2.3 字符串方法startswith()和endswith() startswith()和endswith()方法返回True,如果它们所调用的字符串以该方法传入 的字符串开始或结束。否则,方法返回False。在交互式环境中输入以下代码: 'Hello world!'.startswith('Hello') True 'Hello world!'.endswith('world!') True 'abc123'.startswith('abcdef') False 'abc123'.endswith('12') False 'Hello world!'.startswith('Hello world!') True 'Hello world!'.endswith('Hello world!') True 如果只需要检查字符串的开始或结束部分是否等于另一个字符串,而不是整个 字符串,这些方法就可以替代等于操作符==,这很有用。 6.2.4 字符串方法join()和split() 如果有一个字符串列表,需要将它们连接起来,成为一个单独的字符串,join() 方法就很有用。join()方法在一个字符串上调用,参数是一个字符串列表,返回一个 字符串。返回的字符串由传入的列表中每个字符串连接而成。例如,在交互式环境 中输入以下代码: ', '.join(['cats', 'rats', 'bats']) 'cats, rats, bats' ' '.join(['My', 'name', 'is', 'Simon']) 'My name is Simon' 'ABC'.join(['My', 'name', 'is', 'Simon']) 'MyABCnameABCisABCSimon' 请注意,调用join()方法的字符串,被插入到列表参数中每个字符串的中间。例如, 如果在', '字符串上调用join(['cats', 'rats', 'bats']),返回的字符串就是'cats, rats, bats'。 要记住,join()方法是针对一个字符串而调用的,并且传入一个列表值(很容易 不小心用其他的方式调用它)。split()方法做的事情正好相反:它针对一个字符串调 用,返回一个字符串列表。在交互式环境中输入以下代码: 'My name is Simon'.split() ['My', 'name', 'is', 'Simon'] 默认情况下,字符串'My name is Simon'按照各种空白字符分割,诸如空格、制表 符或换行符。这些空白字符不包含在返回列表的字符串中。也可以向split()方法传入一 个分割字符串,指定它按照不同的字符串分割。例如,在交互式环境中输入以下代码: 第6 章 字符串操作 'MyABCnameABCisABCSimon'.split('ABC') ['My', 'name', 'is', 'Simon'] 'My name is Simon'.split('m') ['My na', 'e is Si', 'on'] 一个常见的split()用法,是按照换行符分割多行字符串。在交互式环境中输入 以下代码: spam = '''Dear Alice, How have you been? I am fine. There is a container in the fridge that is labeled "Milk Experiment". Please do not drink it. Sincerely, Bob''' spam.split('\n') ['Dear Alice,', 'How have you been? I am fine.', 'There is a container in the fridge', 'that is labeled "Milk Experiment".', '', 'Please do not drink it.', 'Sincerely,', 'Bob'] 向split()方法传入参数’\n’,我们按照换行符分割变量中存储的多行字符串,返 回列表中的每个表项,对应于字符串中的一行。 6.2.5 用rjust()、ljust()和center()方法对齐文本 rjust()和ljust()字符串方法返回调用它们的字符串的填充版本,通过插入空格来 对齐文本。这两个方法的第一个参数是一个整数长度,用于对齐字符串。在交互式 环境中输入以下代码: 'Hello'.rjust(10) ' Hello' 'Hello'.rjust(20) ' Hello' 'Hello World'.rjust(20) ' Hello World' 'Hello'.ljust(10) 'Hello ' 'Hello'.rjust(10)是说我们希望右对齐,将'Hello'放在一个长度为10 的字符串中。 'Hello'有5 个字符,所以左边会加上5 个空格,得到一个10 个字符的字符串,实现 'Hello'右对齐。 rjust()和ljust()方法的第二个可选参数将指定一个填充字符,取代空格字符。在 交互式环境中输入以下代码: 'Hello'.rjust(20, '*') '***************Hello' 'Hello'.ljust(20, '-') 'Hello---------------' center()字符串方法与ljust()与rjust()类似,但它让文本居中,而不是左对齐或 右对齐。在交互式环境中输入以下代码: Python 编程快速上手——让繁琐工作自动化 'Hello'.center(20) ' Hello ' 'Hello'.center(20, '=') '=======Hello========' 如果需要打印表格式数据,留出正确的空格,这些方法就特别有用。打开一个 新的文件编辑器窗口,输入以下代码,并保存为picnicTable.py: def printPicnic(itemsDict, leftWidth, rightWidth): print('PICNIC ITEMS'.center(leftWidth + rightWidth, '-')) for k, v in itemsDict.items(): print(k.ljust(leftWidth, '.') + str(v).rjust(rightWidth)) picnicItems = {'sandwiches': 4, 'apples': 12, 'cups': 4, 'cookies': 8000} printPicnic(picnicItems, 12, 5) printPicnic(picnicItems, 20, 6) 在这个程序中,我们定义了printPicnic()方法,它接受一个信息的字典,并利用 center()、ljust()和rjust(),以一种干净对齐的表格形式显示这些信息。 我们传递给printPicnic()的字典是picnicItems。在picnicItems 中,我们有4 个三 明治、12 个苹果、4 个杯子和8000 块饼干。我们希望将这些信息组织成两行,表 项的名字在左边,数量在右边。 要做到这一点,就需要决定左列和右列的宽度。与字典一起,我们将这些值传 递给printPicnic()。 printPicnic()接受一个字典,一个leftWidth 表示表的左列宽度,一个rightWidth 表示表的右列宽度。它打印出标题PICNIC ITEMS,在表上方居中。然后它遍历字 典,每行打印一个键-值对。键左对齐,填充句号。值右对齐,填充空格。 在定义printPicnic()后,我们定义了字典picnicItems,并调用printPicnic()两次, 传入不同的表左右列宽度。 运行该程序,野餐用品就会显示两次。第一次左列宽度是12 个字符,右列宽 度是5 个字符。第二次它们分别是20 个和6 个字符。 ---PICNIC ITEMS-- sandwiches.. 4 apples...... 12 cups........ 4 cookies..... 8000 -------PICNIC ITEMS------- sandwiches.......... 4 apples.............. 12 cups................ 4 cookies............. 8000 利用rjust()、ljust()和center()让你确保字符串整齐对齐,即使你不清楚字符串 有多少字符。 6.2.6 用strip()、rstrip()和lstrip()删除空白字符 有时候你希望删除字符串左边、右边或两边的空白字符(空格、制表符和换行 第6 章 字符串操作 符)。strip()字符串方法将返回一个新的字符串,它的开头或末尾都没有空白字符。 lstrip()和rstrip()方法将相应删除左边或右边的空白字符。 在交互式环境中输入以下代码: spam = ' Hello World ' spam.strip() 'Hello World' spam.lstrip() 'Hello World ' spam.rstrip() ' Hello World' 有一个可选的字符串参数,指定两边的哪些字符应该删除。在交互式环境中输 入以下代码: spam = 'SpamSpamBaconSpamEggsSpamSpam' spam.strip('ampS') 'BaconSpamEggs' 向strip()方法传入参数'ampS',告诉它在变量中存储的字符串两端,删除出现的 a、m、p 和大写的S。传入strip()方法的字符串中,字符的顺序并不重要:strip('ampS') 做的事情和strip('mapS')或strip('Spam')一样。 6.2.7 用pyperclip 模块拷贝粘贴字符串 pyperclip 模块有copy()和paste()函数,可以向计算机的剪贴板发送文本,或从 它接收文本。将程序的输出发送到剪贴板,使它很容易粘贴到邮件、文字处理程序 或其他软件中。pyperclip 模块不是Python 自带的。要安装它,请遵从附录A 中安 装第三方模块的指南。安装pyperclip 模块后,在交互式环境中输入以下代码: import pyperclip pyperclip.copy('Hello world!') pyperclip.paste() 'Hello world!' 当然,如果你的程序之外的某个程序改变了剪贴板的内容,paste()函数就会返 回它。例如,如果我将这句话复制到剪贴板,然后调用paste(),看起来就会像这样: pyperclip.paste() 'For example, if I copied this sentence to the clipboard and then called paste(), it would look like this:' 在IDLE 之外运行Python 脚本 到目前为止,你一直在使用IDLE 中的交互式环境和文件编辑器来运行 Python 脚本。但是,你不想每次运行一个脚本时,都打开IDLE 和Python 脚本, 这样不方便。好在,有一些快捷方式,让你更容易地建立和运行Python 脚本。 这些步骤在Windows、OS X 和Linux 上稍有不同,但每一种都在附录B 中描述。 请翻到附录B,学习如何方便地运行Python 脚本,并能够向它们传递命令行参数。 Python 编程快速上手——让繁琐工作自动化 (使用IDLE 时,不能向程序传递命令行参数。) 6.3 项目:口令保管箱 你可能在许多不同网站上拥有账号,每个账号使用相同的口令是个坏习惯。如 果这些网站中任何一个有安全漏洞,黑客就会知道你所有的其他账号的口令。最好 是在你的计算机上,使用口令管理器软件,利用一个主控口令,解锁口令管理器。 然后将某个账户口令拷贝到剪贴板,再将它粘贴到网站的口令输入框。 你在这个例子中创建的口令管理器程序并不安全,但它基本展示了这种程序的 工作原理。 本章项目 这是本书的第一个章内项目。以后,每章都会有一些项目,展示该章介绍的 一些概念。这些项目的编写方式,让你从一个空白的文件编辑器窗口开始,得到 一个完整的、能工作的程序。就像交互式环境的例子一样,不要只看项目的部分, 要注意计算机的提示! 第1 步:程序设计和数据结构 你希望用一个命令行参数来运行这个程序,该参数是账号的名称。例如,账号 的口令将拷贝到剪贴板,这样用户就能将它粘贴到口令输入框。通过这种方式,用 户可以有很长而复杂的口令,又不需要记住它们。 打开一个新的文件编辑器窗口,将该程序保存为pw.py。程序开始时需要有一 行#!(参见附录B),并且应该写一些注释,简单描述该程序。因为你希望关联每个 账号的名称及其口令,所以可以将这些作为字符串保存在字典中。字典将是组织你 的账号和口令数据的数据结构。让你的程序看起来像下面这样: #! python3
PASSWORDS = {'email': 'F7minlBDDuvMJuxESSKHFhTxFtjVB6', 'blog': 'VmALvQyKAxiVH5G8v01if1MLZF3sdt', 'luggage': '12345'} 第2 步:处理命令行参数 命令行参数将存储在变量sys.argv 中(关于如何在程序中使用命令行参数,更多 信息请参见附录B)。sys.argv 列表中的第一项总是一个字符串,它包含程序的文件名 ('pw.py')。第二项应该是第一个命令行参数。对于这个程序,这个参数就是账户名称, 你希望获取它的口令。因为命令行参数是必须的,所以如果用户忘记添加参数(也就 是说,如果列表中少于两个值),你就显示用法信息。让你的程序看起来像下面这样: 第6 章 字符串操作 #! python3
PASSWORDS = {'email': 'F7minlBDDuvMJuxESSKHFhTxFtjVB6', 'blog': 'VmALvQyKAxiVH5G8v01if1MLZF3sdt', 'luggage': '12345'} import sys if len(sys.argv) < 2: print('Usage: python pw.py [account] - copy account password') sys.exit() account = sys.argv[1] # first command line arg is the account name 第3 步:复制正确的口令 既然账户名称已经作为字符串保存在变量account 中,你就需要看看它是不是 PASSWORDS 字典中的键。如果是,你希望利用pyperclip.copy(),将该键的值复制 到剪贴板(既然用到了pyperclip 模块,就需要导入它)。请注意,实际上不需要account 变量,你可以在程序中所有使用account 的地方,直接使用sys.argv[1]。但名为account 的变量更可读,不像是神秘的sys.argv[1]。 让你的程序看起来像这样: #! python3
PASSWORDS = {'email': 'F7minlBDDuvMJuxESSKHFhTxFtjVB6', 'blog': 'VmALvQyKAxiVH5G8v01if1MLZF3sdt', 'luggage': '12345'} import sys, pyperclip if len(sys.argv) < 2: print('Usage: py pw.py [account] - copy account password') sys.exit() account = sys.argv[1] # first command line arg is the account name if account in PASSWORDS: pyperclip.copy(PASSWORDS[account]) print('Password for ' + account + ' copied to clipboard.') else: print('There is no account named ' + account) 这段新代码在PASSWORDS 字典中查找账户名称。如果该账号名称是字典中的 键,我们就取得该键对应的值,将它复制到剪贴板,然后打印一条消息,说我们已 经复制了该值。否则,我们打印一条消息,说没有这个名称的账号。 这就是完整的脚本。利用附录B 中的指导,轻松地启动命令行程序,现在你就 有了一种快速的方式,将账号的口令复制到剪贴板。如果需要更新口令,就必须修 改源代码的PASSWORDS 字典中的值。 当然,你可能不希望把所有的口令都放在一个地方,让某人能够轻易地复制。 但你可以修改这个程序,利用它快速地将普通文本复制到剪贴板。假设你需要发出 一些电子邮件,它们有许多同样的段落。你可以将每个段落作为一个值,放在 Python 编程快速上手——让繁琐工作自动化 PASSWORDS 字典中(此时你可能希望对这个字典重命名),然后你就有了一种方 式,快速地选择一些标准的文本,并复制到剪贴板。 在Windows 上,你可以创建一个批处理文件,利用Win-R 运行窗口,来运行 这个程序(关于批处理文件的更多信息,参见附录B)。在文件编辑器中输入以下 代码,保存为pw.bat,放在C:\Windows 目录下: @py.exe C:\Python34\pw.py %* @pause 有了这个批处理文件,在Windows 上运行口令保存程序,就只要按下Win-R, 再输入pw 。 6.4 项目:在Wiki 标记中添加无序列表 在编辑一篇维基百科的文章时,你可以创建一个无序列表,即让每个列表项占 据一行,并在前面放置一个星号。但是假设你有一个非常大的列表,希望添加前面 的星号。你可以在每一行开始处输入这些星号,一行接一行。或者也可以用一小段 Python 脚本,将这个任务自动化。 bulletPointAdder.py 脚本将从剪贴板中取得文本,在每一行开始处加上星号和空 格,然后将这段新的文本贴回到剪贴板。例如,如果我将下面的文本复制到剪贴板 (取自于维基百科的文章“List of Lists of Lists”): Lists of animals Lists of aquarium life Lists of biologists by author abbreviation Lists of cultivars 然后运行bulletPointAdder.py 程序,剪贴板中就会包含下面的内容:
- Lists of animals
- Lists of aquarium life
- Lists of biologists by author abbreviation
- Lists of cultivars 这段前面加了星号的文本,就可以粘贴回维基百科的文章中,成为一个无序列表。 第1 步:从剪贴板中复制和粘贴 你希望bulletPointAdder.py 程序完成下列事情: 1.从剪贴板粘贴文本; 2.对它做一些处理; 3.将新的文本复制到剪贴板。 第2 步有一点技巧,但第1 步和第3 步相当简单,它们只是利用了pyperclip.copy() 和pyperclip.paste()函数。现在,我们先写出程序中第1 步和第3 步的部分。输入以 下代码,将程序保存为bulletPointAdder.py: 第6 章 字符串操作 #! python3
import pyperclip text = pyperclip.paste()
pyperclip.copy(text) TODO 注释是提醒,你最后应该完成这部分程序。下一步实际上就是实现程序 的这个部分。 第2 步:分离文本中的行,并添加星号 调用pyperclip.paste()将返回剪贴板上的所有文本,结果是一个大字符串。如果 我们使用“List of Lists of Lists”的例子,保存在text 中的字符串就像这样: 'Lists of animals\nLists of aquarium life\nLists of biologists by author abbreviation\nLists of cultivars' 在打印到剪贴板,或从剪贴板粘贴时,该字符串中的\n 换行字符,让它能显示为 多行。在这一个字符串中有许多“行”。你想要在每一行开始处添加一个星号。 你可以编写代码,查找字符串中每个\n 换行字符,然后在它后面添加一个星号。 但更容易的做法是,使用split()方法得到一个字符串的列表,其中每个表项就是原 来字符串中的一行,然后在列表中每个字符串前面添加星号。 让程序看起来像这样: #! python3
import pyperclip text = pyperclip.paste()
lines = text.split('\n') for i in range(len(lines)): # loop through all indexes in the "lines" list lines[i] = '* ' + lines[i] # add star to each string in "lines" list pyperclip.copy(text) 我们按换行符分割文本,得到一个列表,其中每个表项是文本中的一行。我们 将列表保存在lines 中,然后遍历lines 中的每个表项。对于每一行,我们在开始处 添加一个新号和一个空格。现在lines 中的每个字符串都以星号开始。 第3 步:连接修改过的行 lines 列表现在包含修改过的行,每行都以星号开始。但pyperclip.copy()需要一 个字符串,而不是字符串的列表。要得到这个字符串,就要将lines 传递给join 方 Python 编程快速上手——让繁琐工作自动化 法,连接列表中字符串。让你的程序看起来像这样: #! python3
import pyperclip text = pyperclip.paste()
lines = text.split('\n') for i in range(len(lines)): # loop through all indexes for "lines" list lines[i] = '* ' + lines[i] # add star to each string in "lines" list text = '\n'.join(lines) pyperclip.copy(text) 运行这个程序,它将取代剪贴板上的文本,新的文本每一行都以星号开始。现 在程序完成了,可以在剪贴板中复制一些文本,试着运行它。 即使不需要自动化这样一个专门的任务,也可能想要自动化某些其他类型的文 本操作,诸如删除每行末尾的空格,或将文本转换成大写或小写。不论你的需求是 什么,都可以使用剪贴板作为输入和输出。 6.5 小结 文本是常见的数据形式,Python 自带了许多有用的字符串方法,来处理保存在字符 串中的文本。在你写的几乎每个Python 程序中,都会用到取下标、切片和字符串方法。 现在你写的程序似乎不太复杂,因为它们没有图形用户界面,没有图像和彩色 的文本。到目前为止,你在利用print()显示文本,利用input()让用户输入文本。但 是,用户可以通过剪贴板,快速输入大量的文本。这种能力提供了一种有用的编程 方式,可以操作大量的文本。这些基于文本的程序可能没有闪亮的窗口或图形,但 它们能很快完成大量有用的工作。 操作大量文本的另一种方式,是直接从硬盘读写文件。在下一章中,你将学习 如何用Python 来做到这一点。 6.6 习题 1.什么是转义字符? 2.转义字符\n 和\t 代表什么? 3.如何在字符串中放入一个倒斜杠字符\? 4.字符串"Howl's Moving Castle"是有效字符串。为什么单词中的单引号没有转 义,却没有问题? 5.如果你不希望在字符串中加入\n,怎样写一个带有换行的字符串? 6.下面的表达式求值为什么? 第6 章 字符串操作 ? 'Hello world!'[1] ? 'Hello world!'[0:5] ? 'Hello world!'[:5] ? 'Hello world!'[3:] 7.下面的表达式求值为什么? ? 'Hello'.upper() ? 'Hello'.upper().isupper() ? 'Hello'.upper().lower() 8.下面的表达式求值为什么? ? 'Remember, remember, the fifth of November.'.split() ? '-'.join('There can be only one.'.split()) 9.什么字符串方法能用于字符串右对齐、左对齐和居中? 10.如何去掉字符串开始或末尾的空白字符? 6.7 实践项目 作为实践,编程完成下列任务。 表格打印 编写一个名为printTable()的函数,它接受字符串的列表的列表,将它显示在组 织良好的表格中,每列右对齐。假定所有内层列表都包含同样数目的字符串。例如, 该值可能看起来像这样: tableData = [['apples', 'oranges', 'cherries', 'banana'], ['Alice', 'Bob', 'Carol', 'David'], ['dogs', 'cats', 'moose', 'goose']] 你的printTable()函数将打印出: apples Alice dogs oranges Bob cats cherries Carol moose banana David goose 提示 你的代码首先必须找到每个内层列表中最长的字符串,这样整列就有足够的宽度以 放下所有字符串。你可以将每一列的最大宽度,保存为一个整数的列表。printTable()函 数的开始可以是colWidths = [0] * len(tableData),这创建了一个列表,它包含了一些0, 数目与tableData 中内层列表的数目相同。这样,colWidths[0]就可以保存tableData[0]中 最长字符串的宽度,colWidths[1]就可以保存tableData[1]中最长字符串的宽度,以此类推。 然后可以找到colWidths 列表中最大的值,决定将什么整数宽度传递给rjust()字符串方法。 第二部分 自动化任务 第 章 模式匹配与正则表达式 你可能熟悉文本查找,即按下Ctrl-F,输入你要查找的词。“正 则表达式”更进一步,它们让你指定要查找的“模式”。你也许不 知道一家公司的准确电话号码,但如果你住在美国或加拿大,你就 知道它有3 位数字,然后是一个短横线,然后是4 位数字(有时候 以3 位区号开始)。因此作为一个人,你看到一个电话号码就知道: 415-555-1234 是电话号码,但4,155,551,234 不是。 正则表达式很有用,但如果不是程序员,很少会有人了解 它,尽管大多数现代文本编辑器和文字处理器(诸如微软的Word 或OpenOffice), 都有查找和查找替换功能,可以根据正则表达式查找。正则表达式可以节约大量时 间,不仅适用于软件用户,也适用于程序员。实际上,技术作家Cory Doctorow 声 称,甚至应该在教授编程之前,先教授正则表达式: “知道[正则表达式]可能意味着用3 步解决一个问题,而不是用3000 步。如果 你是一个技术怪侠,别忘了你用几次击键就能解决的问题,其他人需要数天的烦琐 工作才能解决,而且他们容易犯错。”1 1 Cory Doctorow, “Here’s what ICT should really teach kids: how to do regular expressions,”Guardian, December 4, 2012, http://www.theguardian.com/technology/2012/dec/04/ict-teach-kids-regular-expressions/. 7 2 Python 编程快速上手——让繁琐工作自动化 在本章中,你将从编写一个程序开始,先不用正则表达式来寻找文本模式。然后 再看看,使用正则表达式让代码变得多么简洁。我将展示用正则表达式进行基本匹配, 然后转向一些更强大的功能,诸如字符串替换,以及创建你自己的字符类型。最后, 在本章末尾,你将编写一个程序,从一段文本中自动提取电话号码和E-mail 地址。 7.1 不用正则表达式来查找文本模式 假设你希望在字符串中查找电话号码。你知道模式:3 个数字,一个短横线,3 个数字,一个短横线,再是4 个数字。例如:415-555-4242。 假定我们用一个名为isPhoneNumber()的函数,来检查字符串是否匹配模式,它 返回True 或False。打开一个新的文件编辑器窗口,输入以下代码,然后保存为 isPhoneNumber.py: def isPhoneNumber(text): ??if len(text) != 12: return False for i in range(0, 3): ??if not text[i].isdecimal(): return False ??if text[3] != '-': return False for i in range(4, 7): ??if not text[i].isdecimal(): return False ??if text[7] != '-': return False for i in range(8, 12): ??if not text[i].isdecimal(): return False ??return True print('415-555-4242 is a phone number:') print(isPhoneNumber('415-555-4242')) print('Moshi moshi is a phone number:') print(isPhoneNumber('Moshi moshi')) 运行该程序,输出看起来像这样: 415-555-4242 is a phone number: True Moshi moshi is a phone number: False isPhoneNumber()函数的代码进行几项检查,看看text 中的字符串是不是有效的 电话号码。如果其中任意一项检查失败,函数就返回False。代码首先检查该字符串 是否刚好有12 个字符?。然后它检查区号(就是text 中的前3 个字符)是否只包含 数字?。函数剩下的部分检查该字符串是否符合电话号码的模式:号码必须在区号 后出现第一个短横线?,3 个数字?,然后是另一个短横线?,最后是4 个数字?。 如果程序执行通过了所有的检查,它就返回True?。 第7 章 模式匹配与正则表达式 用参数'415-555-4242'调用isPhoneNumber()将返回真。用参数'Moshi moshi'调用 isPhoneNumber()将返回假,第一项测试失败了,因为不是12 个字符。 必须添加更多代码,才能在更长的字符串中寻找这种文本模式。用下面的代码, 替代isPhoneNumber.py 中最后4 个print()函数调用: message = 'Call me at 415-555-1011 tomorrow. 415-555-9999 is my office.' for i in range(len(message)): ??chunk = message[i:i+12] ??if isPhoneNumber(chunk): print('Phone number found: ' + chunk) print('Done') 该程序运行时,输出看起来像这样: Phone number found: 415-555-1011 Phone number found: 415-555-9999 Done 在for 循环的每一次迭代中,取自message 的一段新的12 个字符被赋给变量 chunk?。例如,在第一次迭代,i 是0,chunk 被赋值为message[0:12](即字符串'Call me at 4')。在下一次迭代,i 是1,chunk 被赋值为message[1:13](字符串'all me at 41')。 将chunk 传递给isPhoneNumber(),看看它是否符合电话号码的模式?。如果符 合,就打印出这段文本。 继续遍历message,最终chunk 中的12 个字符会是一个电话号码。该循环遍历 了整个字符串,测试了每一段12 个字符,打印出所有满足isPhoneNumber()的chunk。 当我们遍历完message,就打印出Done。 在这个例子中,虽然message 中的字符串很短,但它也可能包含上百万个字符, 程序运行仍然不需要一秒钟。使用正则表达式查找电话号码的类似程序,运行也不 会超过一秒钟,但用正则表达式编写这类程序会快得多。 7.2 用正则表达式查找文本模式 前面的电话号码查找程序能工作,但它使用了很多代码,做的事却有限: isPhoneNumber()函数有17 行,但只能查找一种电话号码模式。像415.555.4242 或 (415) 555-4242 这样的电话号码格式,该怎么办呢?如果电话号码有分机,例如 415-555-4242 x99,该怎么办呢?isPhoneNumber()函数在验证它们时会失败。你可 以添加更多的代码来处理额外的模式,但还有更简单的方法。 正则表达式,简称为regex,是文本模式的描述方法。例如,\d 是一个正则表 达式,表示一位数字字符,即任何一位 0 到 9 的数字。Python 使用正则表达式 \d\d\d-\d\d\d-\d\d\d\d,来匹配前面isPhoneNumber()函数匹配的同样文本:3 个数字、 一个短横线、3 个数字、一个短横线、4 个数字。所有其他字符串都不能匹配 \d\d\d-\d\d\d-\d\d\d\d 正则表达式。 Python 编程快速上手——让繁琐工作自动化 但正则表达式可以复杂得多。例如,在一个模式后加上花括号包围的3({3}), 就是说,“匹配这个模式3 次”。所以较短的正则表达式\d{3}-\d{3}-\d{4},也匹配正 确的电话号码格式。 7.2.1 创建正则表达式对象 Python 中所有正则表达式的函数都在re 模块中。在交互式环境中输入以下代 码,导入该模块:
import re 注意 本章后面的大多数例子都需要re 模块,所以要记得在你写的每个脚本开始处导入 它,或重新启动IDLE 时。否则,就会遇到错误消息NameError: name 're' is not defined。 向re.compile()传入一个字符串值,表示正则表达式,它将返回一个Regex 模式 对象(或者就简称为Regex 对象)。 要创建一个Regex 对象来匹配电话号码模式,就在交互式环境中输入以下代码 (回忆一下,\d 表示“一个数字字符”,\d\d\d-\d\d\d-\d\d\d\d 是正确电话号码模式的 正则表达式)。 phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') 现在phoneNumRegex 变量包含了一个Regex 对象。 7.2.2 匹配Regex 对象 Regex 对象的search()方法查找传入的字符串,寻找该正则表达式的所有匹配。如 果字符串中没有找到该正则表达式模式,search()方法将返回None。如果找到了该模式, search()方法将返回一个Match 对象。Match 对象有一个group()方法,它返回被查找字 符串中实际匹配的文本(稍后我会解释分组)。例如,在交互式环境中输入以下代码: phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') mo = phoneNumRegex.search('My number is 415-555-4242.') print('Phone number found: ' + mo.group()) Phone number found: 415-555-4242 变量名mo 是一个通用的名称,用于Match 对象。这个例子可能初看起来有点 复杂,但它比前面的isPhoneNumber.py 程序要短很多,并且做的事情一样。 这里,我们将期待的模式传递给re.compile(),并将得到的Regex 对象保存在 phoneNumRegex 中。然后我们在phoneNumRegex 上调用search(),向它传入想查找 的字符串。查找的结果保存在变量mo 中。在这个例子里,我们知道模式会在这个 字符串中找到,所以我们知道会返回一个Match 对象。知道mo 包含一个Match 对 象,而不是空值None,我们就可以在mo 变量上调用group(),返回匹配的结果。 将mo.group()写在打印语句中,显示出完整的匹配,即415-555-4242。 第7 章 模式匹配与正则表达式 向re.compile()传递原始字符串 回忆一下,Python 中转义字符使用倒斜杠(\)。字符串'\n'表示一个换行字符, 而不是倒斜杠加上一个小写的n。你需要输入转义字符\,才能打印出一个倒斜杠。 所以'\n'表示一个倒斜杠加上一个小写的n。但是,通过在字符串的第一个引号之 前加上r,可以将该字符串标记为原始字符串,它不包括转义字符。 因为正则表达式常常使用倒斜杠,向re.compile()函数传入原始字符串就很方 便, 而不是输入额外得到斜杠。输入r'\d\d\d-\d\d\d-\d\d\d\d' , 比输入 '\d\d\d-\d\d\d-\d\d\d\d'要容易得多。 7.2.3 正则表达式匹配复习 虽然在Python 中使用正则表达式有几个步骤,但每一步都相当简单。 1.用import re 导入正则表达式模块。 2.用re.compile()函数创建一个Regex 对象(记得使用原始字符串)。 3.向Regex 对象的search()方法传入想查找的字符串。它返回一个Match 对象。 4.调用Match 对象的group()方法,返回实际匹配文本的字符串。 注意 虽然我鼓励你在交互式环境中输入示例代码,但你也应该利用基于网页的正 则表达式测试程序。它可以向你清楚地展示,一个正则表达式如何匹配输入的 一段文本。我推荐的测试程序位于http://regexpal.com/。 7.3 用正则表达式匹配更多模式 既然你已知道用Python 创建和查找正则表达式对象的基本步骤,就可以尝试一 些更强大的模式匹配功能了。 7.3.1 利用括号分组 假定想要将区号从电话号码中分离。添加括号将在正则表达式中创建“分组”: (\d\d\d)-(\d\d\d-\d\d\d\d)。然后可以使用group()匹配对象方法,从一个分组中获取匹 配的文本。 正则表达式字符串中的第一对括号是第1 组。第二对括号是第2 组。向group() 匹配对象方法传入整数1 或2,就可以取得匹配文本的不同部分。向group()方法传 入0 或不传入参数,将返回整个匹配的文本。在交互式环境中输入以下代码: phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)') mo = phoneNumRegex.search('My number is 415-555-4242.') mo.group(1) '415' Python 编程快速上手——让繁琐工作自动化 mo.group(2) '555-4242' mo.group(0) '415-555-4242' mo.group() '415-555-4242' 如果想要一次就获取所有的分组,请使用groups()方法,注意函数名的复数形式。 mo.groups() ('415', '555-4242') areaCode, mainNumber = mo.groups() print(areaCode) 415 print(mainNumber) 555-4242 因为mo.groups()返回多个值的元组,所以你可以使用多重复制的技巧,每个值 赋给一个独立的变量,就像前面的代码行:areaCode, mainNumber = mo.groups()。 括号在正则表达式中有特殊的含义,但是如果你需要在文本中匹配括号,怎么 办?例如,你要匹配的电话号码,可能将区号放在一对括号中。在这种情况下,就 需要用倒斜杠对(和)进行字符转义。在交互式环境中输入以下代码: phoneNumRegex = re.compile(r'((\d\d\d)) (\d\d\d-\d\d\d\d)') mo = phoneNumRegex.search('My phone number is (415) 555-4242.') mo.group(1) '(415)' mo.group(2) '555-4242' 传递给re.compile()的原始字符串中,(和)转义字符将匹配实际的括号字符。 7.3.2 用管道匹配多个分组 字符|称为“管道”。希望匹配许多表达式中的一个时,就可以使用它。例如, 正则表达式r'Batman|Tina Fey'将匹配'Batman'或'Tina Fey'。 如果Batman 和Tina Fey 都出现在被查找的字符串中,第一次出现的匹配文本, 将作为Match 对象返回。在交互式环境中输入以下代码: heroRegex = re.compile (r'Batman|Tina Fey') mo1 = heroRegex.search('Batman and Tina Fey.') mo1.group() 'Batman' mo2 = heroRegex.search('Tina Fey and Batman.') mo2.group() 'Tina Fey' 注意 利用findall()方法,可以找到“所有”匹配的地方。这在7.5 节“findall()方法” 中讨论。 也可以使用管道来匹配多个模式中的一个,作为正则表达式的一部分。例如, 第7 章 模式匹配与正则表达式 假设你希望匹配'Batman'、'Batmobile'、'Batcopter'和'Batbat'中任意一个。因为所有这 些字符串都以Bat 开始,所以如果能够只指定一次前缀,就很方便。这可以通过括 号实现。在交互式环境中输入以下代码: batRegex = re.compile(r'Bat(man|mobile|copter|bat)') mo = batRegex.search('Batmobile lost a wheel') mo.group() 'Batmobile' mo.group(1) 'mobile' 方法调用mo.group()返回了完全匹配的文本'Batmobile',而mo.group(1)只是返 回第一个括号分组内匹配的文本'mobile'。通过使用管道字符和分组括号,可以指定 几种可选的模式,让正则表达式去匹配。 如果需要匹配真正的管道字符,就用倒斜杠转义,即|。 7.3.3 用问号实现可选匹配 有时候,想匹配的模式是可选的。就是说,不论这段文本在不在,正则表达式 都会认为匹配。字符?表明它前面的分组在这个模式中是可选的。例如,在交互式 环境中输入以下代码: batRegex = re.compile(r'Bat(wo)?man') mo1 = batRegex.search('The Adventures of Batman') mo1.group() 'Batman' mo2 = batRegex.search('The Adventures of Batwoman') mo2.group() 'Batwoman' 正则表达式中的(wo)?部分表明,模式wo 是可选的分组。该正则表达式匹配的文本 中,wo 将出现零次或一次。这就是为什么正则表达式既匹配'Batwoman',又匹配'Batman'。 利用前面电话号码的例子,你可以让正则表达式寻找包含区号或不包含区号的 电话号码。在交互式环境中输入以下代码: phoneRegex = re.compile(r'(\d\d\d-)?\d\d\d-\d\d\d\d') mo1 = phoneRegex.search('My number is 415-555-4242') mo1.group() '415-555-4242' mo2 = phoneRegex.search('My number is 555-4242') mo2.group() '555-4242' 你可以认为?是在说,“匹配这个问号之前的分组零次或一次”。 如果需要匹配真正的问号字符,就使用转义字符?。 7.3.4 用星号匹配零次或多次 (称为星号)意味着“匹配零次或多次”,即星号之前的分组,可以在文本中出 Python 编程快速上手——让繁琐工作自动化 现任意次。它可以完全不存在,或一次又一次地重复。让我们再来看看Batman 的例子。 batRegex = re.compile(r'Bat(wo)man') mo1 = batRegex.search('The Adventures of Batman') mo1.group() 'Batman' mo2 = batRegex.search('The Adventures of Batwoman') mo2.group() 'Batwoman' mo3 = batRegex.search('The Adventures of Batwowowowoman') mo3.group() 'Batwowowowoman' 对于'Batman',正则表达式的(wo)部分匹配wo 的零个实例。对于'Batwoman', (wo)匹配wo 的一个实例。对于'Batwowowowoman',(wo)匹配wo 的4 个实例。 如果需要匹配真正的星号字符,就在正则表达式的星号字符前加上倒斜杠,即*。 7.3.5 用加号匹配一次或多次 意味着“匹配零次或多次”,+(加号)则意味着“匹配一次或多次”。星号不要求 分组出现在匹配的字符串中,但加号不同,加号前面的分组必须“至少出现一次”。这不 是可选的。在交互式环境中输入以下代码,把它和前一节的星号正则表达式进行比较: batRegex = re.compile(r'Bat(wo)+man') mo1 = batRegex.search('The Adventures of Batwoman') mo1.group() 'Batwoman' mo2 = batRegex.search('The Adventures of Batwowowowoman') mo2.group() 'Batwowowowoman' mo3 = batRegex.search('The Adventures of Batman') mo3 == None True 正则表达式Bat(wo)+man 不会匹配字符串'The Adventures of Batman',因为加号 要求wo 至少出现一次。 如果需要匹配真正的加号字符,在加号前面加上倒斜杠实现转义:+。 7.3.6 用花括号匹配特定次数 如果想要一个分组重复特定次数,就在正则表达式中该分组的后面,跟上花括 号包围的数字。例如,正则表达式(Ha){3}将匹配字符串'HaHaHa',但不会匹配'HaHa', 因为后者只重复了(Ha)分组两次。 除了一个数字,还可以指定一个范围,即在花括号中写下一个最小值、一个逗号和 一个最大值。例如,正则表达式(Ha){3,5}将匹配'HaHaHa'、'HaHaHaHa'和'HaHaHaHaHa'。 也可以不写花括号中的第一个或第二个数字,不限定最小值或最大值。例如, 第7 章 模式匹配与正则表达式 (Ha){3,}将匹配3 次或更多次实例,(Ha){,5}将匹配0 到5 次实例。花括号让正则表 达式更简短。这两个正则表达式匹配同样的模式: (Ha){3} (Ha)(Ha)(Ha) 这两个正则表达式也匹配同样的模式: (Ha){3,5} ((Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha)(Ha)) 在交互式环境中输入以下代码: haRegex = re.compile(r'(Ha){3}') mo1 = haRegex.search('HaHaHa') mo1.group() 'HaHaHa' mo2 = haRegex.search('Ha') mo2 == None True 这里,(Ha){3}匹配'HaHaHa',但不匹配'Ha'。因为它不匹配'Ha',所以search() 返回None。 7.4 贪心和非贪心匹配 在字符串'HaHaHaHaHa'中,因为(Ha){3,5}可以匹配3 个、4 个或5 个实例,你可能 会想,为什么在前面花括号的例子中,Match 对象的group()调用会返回'HaHaHaHaHa', 而不是更短的可能结果。毕竟,'HaHaHa'和'HaHaHaHa'也能够有效地匹配正则表达 式(Ha){3,5}。 Python 的正则表达式默认是“贪心”的,这表示在有二义的情况下,它们会尽 可能匹配最长的字符串。花括号的“非贪心”版本匹配尽可能最短的字符串,即在 结束的花括号后跟着一个问号。 在交互式环境中输入以下代码,注意在查找相同字符串时,花括号的贪心形式 和非贪心形式之间的区别: greedyHaRegex = re.compile(r'(Ha){3,5}') mo1 = greedyHaRegex.search('HaHaHaHaHa') mo1.group() 'HaHaHaHaHa' nongreedyHaRegex = re.compile(r'(Ha){3,5}?') mo2 = nongreedyHaRegex.search('HaHaHaHaHa') mo2.group() 'HaHaHa' 请注意,问号在正则表达式中可能有两种含义:声明非贪心匹配或表示可选的 分组。这两种含义是完全无关的。 Python 编程快速上手——让繁琐工作自动化 7.5 findall()方法 除了search 方法外,Regex 对象也有一个findall()方法。search()将返回一个Match 对象,包含被查找字符串中的“第一次”匹配的文本,而findall()方法将返回一组 字符串,包含被查找字符串中的所有匹配。为了看看search()返回的Match 对象只 包含第一次出现的匹配文本,请在交互式环境中输入以下代码: phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') mo = phoneNumRegex.search('Cell: 415-555-9999 Work: 212-555-0000') mo.group() '415-555-9999' 另一方面,findall()不是返回一个Match 对象,而是返回一个字符串列表,只要 在正则表达式中没有分组。列表中的每个字符串都是一段被查找的文本,它匹配该 正则表达式。在交互式环境中输入以下代码: phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') # has no groups phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000') ['415-555-9999', '212-555-0000'] 如果在正则表达式中有分组,那么findall 将返回元组的列表。每个元组表示一个找 到的匹配,其中的项就是正则表达式中每个分组的匹配字符串。为了看看findall()的效果, 请在交互式环境中输入以下代码(请注意,被编译的正则表达式现在有括号分组): phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d)-(\d\d\d\d)') # has groups phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000') [('415', '555', '1122'), ('212', '555', '0000')] 作为findall()方法的返回结果的总结,请记住下面两点: 1.如果调用在一个没有分组的正则表达式上,例如\d\d\d-\d\d\d-\d\d\d\d,方法 findall()将返回一个匹配字符串的列表,例如['415-555-9999', '212-555-0000']。 2.如果调用在一个有分组的正则表达式上,例如(\d\d\d)-(\d\d\d)-(\d\d\d\d),方 法findall()将返回一个字符串的元组的列表(每个分组对应一个字符串),例如[('415', '555', '1122'), ('212', '555', '0000')]。 7.6 字符分类 在前面电话号码正则表达式的例子中,你知道\d 可以代表任何数字。也就是说,\d 是正则表达式(0|1|2|3|4|5|6|7|8|9)的缩写。有许多这样的“缩写字符分类”,如表7-1 所示。 表7-1 常用字符分类的缩写代码 缩写字符分类 表示 \d 0 到9 的任何数字 \D 除0 到9 的数字以外的任何字符 第7 章 模式匹配与正则表达式 续表 缩写字符分类 表示 \w 任何字母、数字或下划线字符(可以认为是匹配“单词”字符) \W 除字母、数字和下划线以外的任何字符 \s 空格、制表符或换行符(可以认为是匹配“空白”字符) \S 除空格、制表符和换行符以外的任何字符 字符分类对于缩短正则表达式很有用。字符分类[0-5]只匹配数字0 到5,这比 输入(0|1|2|3|4|5)要短很多。 例如,在交互式环境中输入以下代码: xmasRegex = re.compile(r'\d+\s\w+') xmasRegex.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge') ['12 drummers', '11 pipers', '10 lords', '9 ladies', '8 maids', '7 swans', '6 geese', '5 rings', '4 birds', '3 hens', '2 doves', '1 partridge'] 正则表达式\d+\s\w+匹配的文本有一个或多个数字(\d+),接下来是一个空白字 符(\s),接下来是一个或多个字母/数字/下划线字符(\w+)。findall()方法将返回所有匹 配该正则表达式的字符串,放在一个列表中。 7.7 建立自己的字符分类 有时候你想匹配一组字符,但缩写的字符分类(\d、\w、\s 等)太宽泛。你可 以用方括号定义自己的字符分类。例如,字符分类[aeiouAEIOU]将匹配所有元音字 符,不论大小写。在交互式环境中输入以下代码: vowelRegex = re.compile(r'[aeiouAEIOU]') vowelRegex.findall('RoboCop eats baby food. BABY FOOD.') ['o', 'o', 'o', 'e', 'a', 'a', 'o', 'o', 'A', 'O', 'O'] 也可以使用短横表示字母或数字的范围。例如,字符分类[a-zA-Z0-9]将匹配所 有小写字母、大写字母和数字。 请注意,在方括号内,普通的正则表达式符号不会被解释。这意味着,你不需 要前面加上倒斜杠转义.、、?或()字符。例如,字符分类将匹配数字0 到5 和一个 句点。你不需要将它写成[0-5.]。 通过在字符分类的左方括号后加上一个插入字符(^),就可以得到“非字符类”。 非字符类将匹配不在这个字符类中的所有字符。例如,在交互式环境中输入以下代码: consonantRegex = re.compile(r'[^aeiouAEIOU]') consonantRegex.findall('RoboCop eats baby food. BABY FOOD.') ['R', 'b', 'c', 'p', ' ', 't', 's', ' ', 'b', 'b', 'y', ' ', 'f', 'd', '.', ' ', 'B', 'B', 'Y', ' ', 'F', 'D', '.'] 现在,不是匹配所有元音字符,而是匹配所有非元音字符。 Python 编程快速上手——让繁琐工作自动化 7.8 插入字符和美元字符 可以在正则表达式的开始处使用插入符号(^),表明匹配必须发生在被查找文 本开始处。类似地,可以再正则表达式的末尾加上美元符号($),表示该字符串必 须以这个正则表达式的模式结束。可以同时使用^和$,表明整个字符串必须匹配该 模式,也就是说,只匹配该字符串的某个子集是不够的。 例如,正则表达式r'^Hello'匹配以'Hello'开始的字符串。在交互式环境中输入以 下代码: beginsWithHello = re.compile(r'^Hello') beginsWithHello.search('Hello world!') <_sre.SRE_Match object; span=(0, 5), match='Hello'> beginsWithHello.search('He said hello.') == None True 正则表达式r'\d$'匹配以数字0 到9 结束的字符串。在交互式环境中输入以下代码: endsWithNumber = re.compile(r'\d$') endsWithNumber.search('Your number is 42') <_sre.SRE_Match object; span=(16, 17), match='2'> endsWithNumber.search('Your number is forty two.') == None True 正则表达式r'^\d+$'匹配从开始到结束都是数字的字符串。在交互式环境中输入 以下代码: wholeStringIsNum = re.compile(r'^\d+$') wholeStringIsNum.search('1234567890') <_sre.SRE_Match object; span=(0, 10), match='1234567890'> wholeStringIsNum.search('12345xyz67890') == None True wholeStringIsNum.search('12 34567890') == None True 前面交互式脚本例子中的最后两次search()调用表明,如果使用了^和$,那么 整个字符串必须匹配该正则表达式。 我总是会混淆这两个符号的含义,所以我使用助记法“Carrots cost dollars”,提 醒我插入符号在前面,美元符号在后面。 7.9 通配字符 在正则表达式中,.(句点)字符称为“通配符”。它匹配除了换行之外的所有 字符。例如,在交互式环境中输入以下代码: atRegex = re.compile(r'.at') atRegex.findall('The cat in the hat sat on the flat mat.') ['cat', 'hat', 'sat', 'lat', 'mat'] 要记住,句点字符只匹配一个字符,这就是为什么在前面的例子中,对于文本 第7 章 模式匹配与正则表达式 flat,只匹配lat。要匹配真正的句点,就是用倒斜杠转义:.。 7.9.1 用点-星匹配所有字符 有时候想要匹配所有字符串。例如,假定想要匹配字符串'First Name:',接下来 是任意文本,接下来是'Last Name:',然后又是任意文本。可以用点-星(.)表示“任 意文本”。回忆一下,句点字符表示“除换行外所有单个字符”,星号字符表示“前 面字符出现零次或多次”。 在交互式环境中输入以下代码: nameRegex = re.compile(r'First Name: (.) Last Name: (.)') mo = nameRegex.search('First Name: Al Last Name: Sweigart') mo.group(1) 'Al' mo.group(2) 'Sweigart' 点-星使用“贪心”模式:它总是匹配尽可能多的文本。要用“非贪心”模式匹配 所有文本,就使用点-星和问号。像和大括号一起使用时那样,问号告诉Python 用非贪 心模式匹配。在交互式环境中输入以下代码,看看贪心模式和非贪心模式的区别: nongreedyRegex = re.compile(r'<.?>') mo = nongreedyRegex.search(' for dinner.>') mo.group() '' greedyRegex = re.compile(r'<.>') mo = greedyRegex.search(' for dinner.>') mo.group() ' for dinner.>' 两个正则表达式都可以翻译成“匹配一个左尖括号,接下来是任意字符,接下 来是一个右尖括号”。但是字符串' for dinner.>'对右肩括号有两种可能的 匹配。在非贪心的正则表达式中,Python 匹配最短可能的字符串:''。 在贪心版本中,Python 匹配最长可能的字符串:' for dinner.>'。 7.9.2 用句点字符匹配换行 点-星将匹配除换行外的所有字符。通过传入re.DOTALL 作为re.compile()的第 二个参数,可以让句点字符匹配所有字符,包括换行字符。 在交互式环境中输入以下代码: noNewlineRegex = re.compile('.') noNewlineRegex.search('Serve the public trust.\nProtect the innocent. \nUphold the law.').group() 'Serve the public trust.' newlineRegex = re.compile('.', re.DOTALL) newlineRegex.search('Serve the public trust.\nProtect the innocent. Python 编程快速上手——让繁琐工作自动化 \nUphold the law.').group() 'Serve the public trust.\nProtect the innocent.\nUphold the law.' 正则表达式noNewlineRegex 在创建时没有向re.compile()传入re.DOTALL,它 将匹配所有字符,直到第一个换行字符。但是,newlineRegex 在创建时向re.compile()传 入了re.DOTALL,它将匹配所有字符。这就是为什么newlineRegex.search()调用匹配完 整的字符串,包括其中的换行字符。 7.10 正则表达式符号复习 本章介绍了许多表示法,所以这里快速复习一下学到的内容: ???匹配零次或一次前面的分组。 ??匹配零次或多次前面的分组。 ??+匹配一次或多次前面的分组。 ??{n}匹配n 次前面的分组。 ??{n,}匹配n 次或更多前面的分组。 ??{,m}匹配零次到m 次前面的分组。 ??{n,m}匹配至少n 次、至多m 次前面的分组。 ??{n,m}?或?或+?对前面的分组进行非贪心匹配。 ??^spam 意味着字符串必须以spam 开始。 ??spam$意味着字符串必须以spam 结束。 ??.匹配所有字符,换行符除外。 ??\d、\w 和\s 分别匹配数字、单词和空格。 ??\D、\W 和\S 分别匹配出数字、单词和空格外的所有字符。 ??[abc]匹配方括号内的任意字符(诸如a、b 或c)。 ??[^abc]匹配不在方括号内的任意字符。 7.11 不区分大小写的匹配 通常,正则表达式用你指定的大小写匹配文本。例如,下面的正则表达式匹配 完全不同的字符串: regex1 = re.compile('RoboCop') regex2 = re.compile('ROBOCOP') regex3 = re.compile('robOcop') regex4 = re.compile('RobocOp') 但是,有时候你只关心匹配字母,不关心它们是大写或小写。要让正则表达式 不区分大小写,可以向re.compile()传入re.IGNORECASE 或re.I,作为第二个参数。 在交互式环境中输入以下代码: 第7 章 模式匹配与正则表达式 robocop = re.compile(r'robocop', re.I) robocop.search('RoboCop is part man, part machine, all cop.').group() 'RoboCop' robocop.search('ROBOCOP protects the innocent.').group() 'ROBOCOP' robocop.search('Al, why does your programming book talk about robocop so much?').group() 'robocop' 7.12 用sub()方法替换字符串 正则表达式不仅能找到文本模式,而且能够用新的文本替换掉这些模式。Regex 对象的sub()方法需要传入两个参数。第一个参数是一个字符串,用于取代发现的匹 配。第二个参数是一个字符串,即正则表达式。sub()方法返回替换完成后的字符串。 例如,在交互式环境中输入以下代码: namesRegex = re.compile(r'Agent \w+') namesRegex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.') 'CENSORED gave the secret documents to CENSORED.' 有时候,你可能需要使用匹配的文本本身,作为替换的一部分。在sub()的第一 个参数中,可以输入\1、\2、\3……。表示“在替换中输入分组1、2、3……的文本”。 例如,假定想要隐去密探的姓名,只显示他们姓名的第一个字母。要做到这一 点,可以使用正则表达式Agent (\w)\w*,传入r'\1****'作为sub()的第一个参数。字 符串中的\1 将由分组1 匹配的文本所替代,也就是正则表达式的(\w)分组。 agentNamesRegex = re.compile(r'Agent (\w)\w*') agentNamesRegex.sub(r'\1****', 'Agent Alice told Agent Carol that Agent Eve knew Agent Bob was a double agent.') A**** told C**** that E**** knew B**** was a double agent.' 7.13 管理复杂的正则表达式 如果要匹配的文本模式很简单,正则表达式就很好。但匹配复杂的文本模式, 可能需要长的、费解的正则表达式。你可以告诉re.compile(),忽略正则表达式字符 串中的空白符和注释,从而缓解这一点。要实现这种详细模式,可以向re.compile() 传入变量re.VERBOSE,作为第二个参数。 现在,不必使用这样难以阅读的正则表达式: phoneRegex = re.compile(r'((\d{3}|(\d{3}))?(\s|-|.)?\d{3}(\s|-|.)\d{4} (\s*(ext|x|ext.)\s*\d{2,5})?)') 你可以将正则表达式放在多行中,并加上注释,像这样: phoneRegex = re.compile(r'''( (\d{3}|(\d{3}))? # area code (\s|-|.)? # separator Python 编程快速上手——让繁琐工作自动化 \d{3} # first 3 digits (\s|-|.) # separator \d{4} # last 4 digits (\s*(ext|x|ext.)\s*\d{2,5})? # extension )''', re.VERBOSE) 请注意,前面的例子使用了三重引号('"),创建了一个多行字符串。这样就可以 将正则表达式定义放在多行中,让它更可读。 正则表达式字符串中的注释规则,与普通的Python 代码一样:#符号和它后面直 到行末的内容,都被忽略。而且,表示正则表达式的多行字符串中,多余的空白字符 也不认为是要匹配的文本模式的一部分。这让你能够组织正则表达式,让它更可读。 7.14 组合使用re.IGNOREC ASE、re.DOTALL 和re.VERBOSE 如果你希望在正则表达式中使用re.VERBOSE 来编写注释,还希望使用 re.IGNORECASE 来忽略大小写,该怎么办?遗憾的是,re.compile()函数只接受一 个值作为它的第二参数。可以使用管道字符(|)将变量组合起来,从而绕过这个限 制。管道字符在这里称为“按位或”操作符。 所以,如果希望正则表达式不区分大小写,并且句点字符匹配换行,就可以这 样构造re.compile()调用: someRegexValue = re.compile('foo', re.IGNORECASE | re.DOTALL) 使用第二个参数的全部3 个选项,看起来像这样: someRegexValue = re.compile('foo', re.IGNORECASE | re.DOTALL | re.VERBOSE) 这个语法有一点老式,源自于早期的Python 版本。位运算符的细节超出了本书 的范围,更多的信息请查看资源http://nostarch.com/automatestuff/。可以向第二个参 数传入其他选项,它们不常用,但你也可以在前面的资源中找到有关它们的信息。 7.15 项目:电话号码和E-mail 地址提取程序 假设你有一个无聊的任务,要在一篇长的网页或文章中,找出所有电话号码和 邮件地址。如果手动翻页,可能需要查找很长时间。如果有一个程序,可以在剪贴 板的文本中查找电话号码和E-mail 地址,那你就只要按一下Ctrl-A 选择所有文本, 按下Ctrl-C 将它复制到剪贴板,然后运行你的程序。它会用找到的电话号码和E-mail 地址,替换掉剪贴板中的文本。 当你开始接手一个新项目时,很容易想要直接开始写代码。但更多的时候,最 好是后退一步,考虑更大的图景。我建议先草拟高层次的计划,弄清楚程序需要做 什么。暂时不要思考真正的代码,稍后再来考虑。现在,先关注大框架。 例如,你的电话号码和E-mail 地址提取程序需要完成以下任务: 第7 章 模式匹配与正则表达式 ??从剪贴板取得文本。 ??找出文本中所有的电话号码和E-mail 地址。 ??将它们粘贴到剪贴板。 现在你可以开始思考,如何用代码来完成工作。代码需要做下面的事情: ??使用pyperclip 模块复制和粘贴字符串。 ??创建两个正则表达式,一个匹配电话号码,另一个匹配E-mail 地址。 ??对两个正则表达式,找到所有的匹配,而不只是第一次匹配。 ??将匹配的字符串整理好格式,放在一个字符串中,用于粘贴。 ??如果文本中没有找到匹配,显示某种消息。 这个列表就像项目的路线图。在编写代码时,可以独立地关注其中的每一步。 每一步都很好管理。它的表达方式让你知道在Python 中如何去做。 第1 步:为电话号码创建一个正则表达式 首先,你需要创建一个正则表达式来查找电话号码。创建一个新文件,输入以 下代码,保存为phoneAndEmail.py: #! python3
import pyperclip, re phoneRegex = re.compile(r'''( (\d{3}|(\d{3}))? # area code (\s|-|.)? # separator (\d{3}) # first 3 digits (\s|-|.) # separator (\d{4}) # last 4 digits (\s*(ext|x|ext.)\s*(\d{2,5}))? # extension )''', re.VERBOSE)
TODO 注释仅仅是程序的框架。当编写真正的代码时,它们会被替换掉。 电话号码从一个“可选的”区号开始,所以区号分组跟着一个问号。因为区号 可能只是3 个数字(即\d{3}),或括号中的3 个数字(即(\d{3})),所以应该用管 道符号连接这两部分。可以对这部分多行字符串加上正则表达式注释# Area code, 帮助你记忆(\d{3}|(\d{3}))?要匹配的是什么。 电话号码分割字符可以是空格(\s)、短横(-)或句点(.),所以这些部分也应 该用管道连接。这个正则表达式接下来的几部分很简单:3 个数字,接下来是另一 个分割符,接下来是4 个数字。最后的部分是可选的分机号,包括任意数目的空格, 接着ext、x 或ext.,再接着2 到5 位数字。 Python 编程快速上手——让繁琐工作自动化 第2 步:为E-mail 地址创建一个正则表达式 还需要一个正则表达式来匹配E-mail 地址。让你的程序看起来像这样: #! python3
import pyperclip, re phoneRegex = re.compile(r'''( --snip--
emailRegex = re.compile(r'''( ??[a-zA-Z0-9._%+-]+ # username ??@ # @ symbol ??[a-zA-Z0-9.-]+ # domain name (.[a-zA-Z]{2,4}) # dot-something )''', re.VERBOSE)
E-mail 地址的用户名部分?是一个或多个字符,字符可以包括:小写和大写字 母、数字、句点、下划线、百分号、加号或短横。可以将所有这些放入一个字符分 类:[a-zA-Z0-9._%+-]。 域名和用户名用@符号分割?,域名?允许的字符分类要少一些,只允许字母、 数字、句点和短横:[a-zA-Z0-9.-]。最后是“dot-com”部分(技术上称为“顶级域 名”),它实际上可以是“dot-anything”。它有2 到4 个字符。 E-mail 地址的格式有许多奇怪的规则。这个正则表达式不会匹配所有可能的、 有效的E-mail 地址,但它会匹配你遇到的大多数典型的电子邮件地址。 第3 步:在剪贴板文本中找到所有匹配 既然已经指定了电话号码和电子邮件地址的正则表达式,就可以让 Python 的re 模块做辛苦的工作,查找剪贴板文本中所有的匹配。pyperclip.paste()函数将取得一个 字符串,内容是剪贴板上的文本,findall()正则表达式方法将返回一个元组的列表。 让你的程序看起来像这样: #! python3
import pyperclip, re phoneRegex = re.compile(r'''( --snip--
text = str(pyperclip.paste()) ??matches = [] ??for groups in phoneRegex.findall(text): phoneNum = '-'.join([groups[1], groups[3], groups[5]]) 第7 章 模式匹配与正则表达式 if groups[8] != '': phoneNum += ' x' + groups[8] matches.append(phoneNum) ??for groups in emailRegex.findall(text): matches.append(groups[0])
每个匹配对应一个元组,每个元组包含正则表达式中每个分组的字符串。回忆一 下,分组0 匹配整个正则表达式,所以在元组下标0 处的分组,就是你感兴趣的内容。 在?处可以看到,你将所有的匹配保存在名为matches 的列表变量中。它从一 个空列表开始,经过几个for 循环。对于E-mail 地址,你将每次匹配的分组0 添加 到列表中?。对于匹配的电话号码,你不想只是添加分组0。虽然程序可以“检测” 几种不同形式的电话号码,你希望添加的电话号码是唯一的、标准的格式。 phoneNum 变量包含一个字符串,它由匹配文本的分组1、3、5 和8 构成?。(这些 分组是区号、前3 个数字、后4 个数字和分机号。) 第4 步:所有匹配连接成一个字符串,复制到剪贴板 现在,E-mail 地址和电话号码已经作为字符串列表放在matches 中,你希望将 它们复制到剪贴板。pyperclip.copy()函数只接收一个字符串值,而不是字符串的列 表,所以你在matches 上调用join()方法。 为了更容易看到程序在工作,让我们将所有找到的匹配都输出在终端上。如果 没有找到电话号码或E-mail 地址,程序应该告诉用户。 让你的程序看起来像这样: #! python3
--snip-- for groups in emailRegex.findall(text): matches.append(groups[0])
if len(matches) > 0: pyperclip.copy('\n'.join(matches)) print('Copied to clipboard:') print('\n'.join(matches)) else: print('No phone numbers or email addresses found.') 第5 步:运行程序 作为一个例子,打开你的Web 浏览器,访问No Starch Press 的联系页面 http://www.nostarch.com/contactus.htm。按下Ctrl-A 选择该页的所有文本,按下Ctrl-C 将它复制到剪贴板。运行这个程序,输出看起来像这样: Copied to clipboard: 800-420-7240 Python 编程快速上手——让繁琐工作自动化 415-863-9900 415-863-9950 [email protected] [email protected] [email protected] [email protected] 第6 步:类似程序的构想 识别文本的模式(并且可能用sub()方法替换它们)有许多不同潜在的应用。 ??寻找网站的URL,它们以http://或https://开始。 ??整理不同日期格式的日期(诸如3/14/2015、03-14-2015 和2015/3/14),用唯一 的标准格式替代。 ??删除敏感的信息,诸如社会保险号或信用卡号。 ??寻找常见打字错误,诸如单词间的多个空格、不小心重复的单词,或者句子末 尾处多个感叹号。它们很烦人!! 7.16 小结 虽然计算机可以很快地查找文本,但你必须精确地告诉它要找什么。正则表达 式让你精确地指明要找的文本模式。实际上,某些文字处理和电子表格应用提供了 查找替换功能,让你使用正则表达式进行查找。 Python 自带的re 模块让你编译Regex 对象。该对象有几种方法:search()查找 单词匹配,findall()查找所有匹配实例,sub()对文本进行查找和替换。 除本章介绍的语法以外,还有一些正则表达式语法。你可以在官方Python 文档 中找到更多内容:http://docs.python.org/3/library/re.html。指南网站http://www.regularexpressions. info/也是很有用的资源。 既然已经掌握了如何操纵和匹配字符串,接下来就该学习如何在计算机硬盘上 读写文件了。 7.17 习题 1.创建Regex 对象的函数是什么? 2.在创建Regex 对象时,为什么常用原始字符串? 3.search()方法返回什么? 4.通过Match 对象,如何得到匹配该模式的实际字符串? 5.用r'(\d\d\d)-(\d\d\d-\d\d\d\d)'创建的正则表达式中,分组0 表示什么?分组1 呢?分组2 呢? 6.括号和句点在正则表达式语法中有特殊的含义。如何指定正则表达式匹配 第7 章 模式匹配与正则表达式 真正的括号和句点字符? 7.findall()方法返回一个字符串的列表,或字符串元组的列表。是什么决定它 提供哪种返回? 8.在正则表达式中,|字符表示什么意思? 9.在正则表达式中,?字符有哪两种含义? 10.在正则表达式中,+和*字符之间的区别是什么? 11.在正则表达式中,{3}和{3,5}之间的区别是什么? 12.在正则表达式中,\d、\w 和\s 缩写字符类是什么意思? 13.在正则表达式中,\D、\W 和\S 缩写字符类是什么意思? 14.如何让正则表达式不区分大小写? 15.字符.通常匹配什么?如果re.DOTALL 作为第二个参数传递给re.compile(), 它会匹配什么? 16..和?之间的区别是什么? 17.匹配所有数字和小写字母的字符分类语法是什么? 18.如果numRegex = re.compile(r'\d+'),那么numRegex.sub('X', '12 drummers, 11 pipers, five rings, 3 hens')返回什么? 19.将re.VERBOSE 作为第二个参数传递给re.compile(),让你能做什么? 20.如何写一个正则表达式,匹配每3 位就有一个逗号的数字?它必须匹配以 下数字: ??'42' ??'1,234' ??'6,368,745' 但不会匹配: ??'12,34,567' (逗号之间只有两位数字) ??'1234' (缺少逗号) 21.如何写一个正则表达式,匹配姓Nakamoto 的完整姓名?你可以假定名字 总是出现在姓前面,是一个大写字母开头的单词。该正则表达式必须匹配: ??'Satoshi Nakamoto' ??'Alice Nakamoto' ??'RoboCop Nakamoto' 但不匹配: ??'satoshi Nakamoto'(名字没有大写首字母) ??'Mr. Nakamoto'(前面的单词包含非字母字符) ??'Nakamoto' (没有名字) ??'Satoshi nakamoto'(姓没有首字母大写) 22.如何编写一个正则表达式匹配一个句子,它的第一个词是Alice、Bob 或 Python 编程快速上手——让繁琐工作自动化 Carol,第二个词是eats、pets 或throws,第三个词是apples、cats 或baseballs。该句 子以句点结束。这个正则表达式应该不区分大小写。它必须匹配: ??'Alice eats apples.' ??'Bob pets cats.' ??'Carol throws baseballs.' ??'Alice throws Apples.' ??'BOB EATS CATS.' 但不匹配: ??'RoboCop eats apples.' ??'ALICE THROWS FOOTBALLS.' ??'Carol eats 7 cats.' 7.18 实践项目 作为实践,编程完成下列任务。 7.18.1 强口令检测 写一个函数,它使用正则表达式,确保传入的口令字符串是强口令。强口令的 定义是:长度不少于8 个字符,同时包含大写和小写字符,至少有一位数字。你可 能需要用多个正则表达式来测试该字符串,以保证它的强度。 7.18.2 strip()的正则表达式版本 写一个函数,它接受一个字符串,做的事情和strip()字符串方法一样。如果只 传入了要去除的字符串,没有其他参数,那么就从该字符串首尾去除空白字符。否 则,函数第二个参数指定的字符将从该字符串中去除。 第 章 读 写 文 件 当程序运行时,变量是保存数据的好方法,但如果希望 程序结束后数据仍然保持,就需要将数据保存到文件中。你 可以认为文件的内容是一个字符串值,大小可能有几个GB。 在本章中,你将学习如何使用Python 在硬盘上创建、读取和 保存文件。 8.1 文件与文件路径 文件有两个关键属性:“文件名”(通常写成一个单词)和“路径”。路径指明了文 件在计算机上的位置。例如,我的 Windows 7 笔记本上有一个文件名为projects.docx, 它的路径在C:\Users\asweigart\Documents。文件名中,最后一个句点之后的部分称为文 件的“扩展名”,它指出了文件的类型。project.docx 是一个Word 文档,Users、asweigart 和Documents 都是指“文件夹”(也成为目录)。文件夹可以包含文件和其他文件夹。 例如,project.docx 在Documents 文件夹中,该文件夹又在asweigart 文件夹中,asweigart 文件夹又在Users 文件夹中。图8-1 展示了这个文件夹的组织结构。 路径中的C:\部分是“根文件夹”,它包含了所有其他文件夹。在Windows 中, 8 138 Python 编程快速上手——让繁琐工作自动化 根文件夹名为C:\,也称为C:盘。在OS X 和Linux 中,根文件夹是/。在本书中, 我使用Windows 风格的根文件夹,C:\。如果你在OS X 或Linux 上输入交互式环境 的例子,请用/代替。 图8-1 在文件夹层次结构中的一个文件 附加卷,诸如DVD 驱动器或USB 闪存驱动器,在不同的操作系统上显示也不 同。在Windows 上,它们表示为新的、带字符的根驱动器。诸如D:\或E:\。在OS X 上,它们表示为新的文件夹,在/Volumes 文件夹下。在Linux 上,它们表示为新的 文件夹,在/mnt("mount")文件夹下。同时也要注意,虽然文件夹名称和文件名在 Windows 和OS X 上是不区分大小写的,但在Linux 上是区分大小写的。 8.1.1 Windows 上的倒斜杠以及OS X 和Linux 上的正斜杠 在Windows 上,路径书写使用倒斜杠作为文件夹之间的分隔符。但在OS X 和 Linux 上,使用正斜杠作为它们的路径分隔符。如果想要程序运行在所有操作系统 上,在编写Python 脚本时,就必须处理这两种情况。 好在,用os.path.join()函数来做这件事很简单。如果将单个文件和路径上的文 件夹名称的字符串传递给它,os.path.join()就会返回一个文件路径的字符串,包含正 确的路径分隔符。在交互式环境中输入以下代码:
import os os.path.join('usr', 'bin', 'spam') 'usr\bin\spam' 我在Windows 上运行这些交互式环境的例子,所以,os.path .join('usr', 'bin', 'spam')返回'usr\bin\spam'(请注意,倒斜杠有两个,因为每个倒斜杠需要由另一个 倒斜杠字符来转义)。如果我在OS X 或Linux 上调用这个函数,该字符串就会是 'usr/bin/spam'。 如果需要创建文件名称的字符串,os.path.join()函数就很有用。这些字符串将传 递给几个文件相关的函数,本章将进行介绍。例如,下面的例子将一个文件名列表 中的名称,添加到文件夹名称的末尾。 第8 章 读写文件 139 myFiles = ['accounts.txt', 'details.csv', 'invite.docx'] for filename in myFiles: print(os.path.join('C:\Users\asweigart', filename)) C:\Users\asweigart\accounts.txt C:\Users\asweigart\details.csv C:\Users\asweigart\invite.docx 8.1.2 当前工作目录 每个运行在计算机上的程序,都有一个“当前工作目录”,或cwd。所有没有 从根文件夹开始的文件名或路径,都假定在当前工作目录下。利用os.getcwd()函数, 可以取得当前工作路径的字符串,并可以利用os.chdir()改变它。在交互式环境中输 入以下代码: import os os.getcwd() 'C:\Python34' os.chdir('C:\Windows\System32') os.getcwd() 'C:\Windows\System32' 这里,当前工作目录设置为C:\Python34,所以文件名project.docx 指向 C:\Python34\project.docx。如果我们将当前工作目录改为C:\Windows,文件就被解 释为C:\Windows\project.docx。 如果要更改的当前工作目录不存在,Python 就会显示一个错误。 os.chdir('C:\ThisFolderDoesNotExist') Traceback (most recent call last): File "<pyshell#18>", line 1, in os.chdir('C:\ThisFolderDoesNotExist') FileNotFoundError: [WinError 2] The system cannot find the file specified: 'C:\ThisFolderDoesNotExist' 注意 虽然文件夹是目录的更新的名称,但请注意,当前工作目录(或当前目录)是 标准术语,没有当前工作文件夹这种说法。 8.1.3 绝对路径与相对路径 有两种方法指定一个文件路径。 ? “绝对路径”,总是从根文件夹开始。 ? “相对路径”,它相对于程序的当前工作目录。 还有点(.)和点点(..)文件夹。它们不是真正的文件夹,而是可以在路径中 使用的特殊名称。单个的句点(“点”)用作文件夹目名称时,是“这个目录”的缩 写。两个句点(“点点”)意思是父文件夹。 图8-2 是一些文件夹和文件的例子。如果当前工作目录设置为C:\bacon,这些 文件夹和文件的相对目录,就设置为图8-2 所示的样子。 140 Python 编程快速上手——让繁琐工作自动化 图8-2 在工作目录C:\bacon 中的文件夹和文件的相对路径 相对路径开始处的.\是可选的。例如,.\spam.txt 和spam.txt 指的是同一个文件。 8.1.4 用os.makedirs()创建新文件夹 程序可以用os.makedirs()函数创建新文件夹(目录)。在交互式环境中输入以下 代码: import os os.makedirs('C:\delicious\walnut\waffles') 这不仅将创建C:\delicious 文件夹,也会在C:\delicious 下创建walnut 文件夹, 并在C:\delicious\walnut 中创建waffles 文件夹。也就是说,os.makedirs()将创建所有 必要的中间文件夹,目的是确保完整路径名存在。图 8-3 展示了这个文件夹的层次 结构。 图8-3 os.makedirs('C:\delicious\walnut\waffles')的结果 8.1.5 os.path 模块 os.path 模块包含了许多与文件名和文件路径相关的有用函数。例如,你已经使 用了os.path.join()来构建所有操作系统上都有效的路径。因为os.path 是os 模块中的 第8 章 读写文件 141 模块,所以只要执行import os 就可以导入它。如果你的程序需要处理文件、文件夹 或文件路径,就可以参考本节中这些简短的例子。os.path 模块的完整文档在Python 网站上:http://docs.python.org/3/library/os.path.html。 注意 本章后面的大多数例子都需要os 模块,所以要记得在每个脚本开始处导入它,或在 重新启动IDLE 时导入它。否则,就会遇到错误消息NameError: name 'os' is not defined。 8.1.6 处理绝对路径和相对路径 os.path 模块提供了一些函数,返回一个相对路径的绝对路径,以及检查给定的 路径是否为绝对路径。 ? 调用os.path.abspath(path)将返回参数的绝对路径的字符串。这是将相对路径转 换为绝对路径的简便方法。 ? 调用os.path.isabs(path),如果参数是一个绝对路径,就返回True,如果参数是 一个相对路径,就返回False。 ? 调用os.path.relpath(path, start)将返回从start 路径到path 的相对路径的字符串。 如果没有提供start,就使用当前工作目录作为开始路径。 在交互式环境中尝试以下函数: os.path.abspath('.') 'C:\Python34' os.path.abspath('.\Scripts') 'C:\Python34\Scripts' os.path.isabs('.') False os.path.isabs(os.path.abspath('.')) True 因为在os.path.abspath()调用时,当前目录是C:\Python34,所以“点”文件夹指 的是绝对路径'C:\Python34'。 注意 因为在你的系统上,文件和文件夹可能与我的不同,所以你不能完全遵照本章 中的每一个例子。但还是请尝试用你的计算机上存在的文件夹来完成例子。 在交互式环境中,输入以下对os.path.relpath()的调用: os.path.relpath('C:\Windows', 'C:\') 'Windows' os.path.relpath('C:\Windows', 'C:\spam\eggs') '..\..\Windows' os.getcwd() 'C:\Python34' 调用os.path.dirname(path)将返回一个字符串,它包含path 参数中最后一个斜杠 之前的所有内容。调用os.path.basename(path)将返回一个字符串,它包含path 参数 中最后一个斜杠之后的所有内容。一个路径的目录名称和基本名称如图8-4 所示。 142 Python 编程快速上手——让繁琐工作自动化 图8-4 基本名称跟在路径中最后一个斜杠后,它和文件名一样, 目录名称是最后一个斜杠之前的所有内容 例如,在交互式环境中输入以下代码: path = 'C:\Windows\System32\calc.exe' os.path.basename(path) 'calc.exe' os.path.dirname(path) 'C:\Windows\System32' 如果同时需要一个路径的目录名称和基本名称,就可以调用os.path.split(),获 得这两个字符串的元组,像这样: calcFilePath = 'C:\Windows\System32\calc.exe' os.path.split(calcFilePath) ('C:\Windows\System32', 'calc.exe') 请注意,可以调用os.path.dirname()和os.path.basename(),将它们的返回值放在 一个元组中,从而得到同样的元组。 (os.path.dirname(calcFilePath), os.path.basename(calcFilePath)) ('C:\Windows\System32', 'calc.exe') 但如果需要两个值,os.path.split()是很好的快捷方式。 同时也请注意,os.path.split()不会接受一个文件路径并返回每个文件夹的字符串的 列表。如果需要这样,请使用split()字符串方法,并根据os.path.sep 中的字符串进行分 割。回忆一下,根据程序运行的计算机,os.path.sep 变量设置为正确的文件夹分割斜杠。 例如,在交互式环境中输入以下代码: calcFilePath.split(os.path.sep) ['C:', 'Windows', 'System32', 'calc.exe'] 在OS X 和Linux 系统上,返回的列表头上有一个空字符串: '/usr/bin'.split(os.path.sep) ['', 'usr', 'bin'] split()字符串方法将返回一个列表,包含该路径的所有部分。如果向它传递 os.path.sep,就能在所有操作系统上工作。 8.1.7 查看文件大小和文件夹内容 一旦有办法处理文件路径,就可以开始搜集特定文件和文件夹的信息。os.path 模 块提供了一些函数,用于查看文件的字节数以及给定文件夹中的文件和子文件夹。 ? 调用os.path.getsize(path)将返回path 参数中文件的字节数。 第8 章 读写文件 143 ? 调用os.listdir(path)将返回文件名字符串的列表,包含path 参数中的每个文件 (请注意,这个函数在os 模块中,而不是os.path)。 下面是我在交互式环境中尝试这些函数的结果: os.path.getsize('C:\Windows\System32\calc.exe') 776192 os.listdir('C:\Windows\System32') ['0409', '12520437.cpx', '12520850.cpx', '5U877.ax', 'aaclient.dll', --snip-- 'xwtpdui.dll', 'xwtpw32.dll', 'zh-CN', 'zh-HK', 'zh-TW', 'zipfldr.dll'] 可以看到,我的计算机上的calc.exe 程序是776192 字节。在我的C:\Windows
system32 下有许多文件。如果想知道这个目录下所有文件的总字节数,就可以同时 使用os.path.getsize()和os.listdir()。 totalSize = 0 for filename in os.listdir('C:\Windows\System32'): totalSize = totalSize + os.path.getsize(os.path.join('C:\Windows\System32', filename)) print(totalSize) 1117846456 当循环遍历C:\Windows\System32 文件夹中的每个文件时,totalSize 变量依次增加 每个文件的字节数。请注意,我在调用os.path.getsize()时,使用了os.path.join()来连接 文件夹名称和当前的文件名。os.path.getsize()返回的整数添加到totalSize 中。在循环遍 历所有文件后,我打印出totalSize,看看C:\Windows\System32 文件夹的总字节数。 8.1.8 检查路径有效性 如果你提供的路径不存在,许多Python 函数就会崩溃并报错。os.path 模块提 供了一些函数,用于检测给定的路径是否存在,以及它是文件还是文件夹。 ? 如果path 参数所指的文件或文件夹存在,调用os.path.exists(path)将返回True, 否则返回False。 ? 如果path 参数存在,并且是一个文件,调用os.path.isfile(path)将返回True,否 则返回False。 ? 如果path 参数存在,并且是一个文件夹,调用os.path.isdir(path)将返回True, 否则返回False。 下面是我在交互式环境中尝试这些函数的结果: os.path.exists('C:\Windows') True os.path.exists('C:\some_made_up_folder') False os.path.isdir('C:\Windows\System32') True os.path.isfile('C:\Windows\System32') False os.path.isdir('C:\Windows\System32\calc.exe') 144 Python 编程快速上手——让繁琐工作自动化 False os.path.isfile('C:\Windows\System32\calc.exe') True 利用os.path.exists()函数,可以确定DVD 或闪存盘当前是否连在计算机上。例 如,如果在Windows 计算机上,我想用卷名D:\检查一个闪存盘,可以这样做: os.path.exists('D:\') False 不好!看起来我忘记插入闪存盘了。 8.2 文件读写过程 在熟悉了处理文件夹和相对路径后,你就可以指定文件的位置,进行读写。接下 来几节介绍的函数适用于纯文本文件。“纯文本文件”只包含基本文本字符,不包含字 体、大小和颜色信息。带有.txt 扩展名的文本文件,以及带有.py 扩展名的Python 脚本 文件,都是纯文本文件的例子。它们可以被Windows 的Notepad 或OS X 的TextEdit 应用打开。你的程序可以轻易地读取纯文本文件的内容,将它们作为普通的字符串值。 “二进制文件”是所有其他文件类型,诸如字处理文档、PDF、图像、电子表格 和可执行程序。如果用Notepad 或TextEdit 打开一个二进制文件,它看起来就像乱 码,如图8-5 所示。 图8-5 在Notepad 中打开Windows 的calc.exe 程序 既然每种不同类型的二进制文件,都必须用它自己的方式来处理,本书就不会 探讨直接读写二进制文件。好在,许多模块让二进制文件的处理变得更容易。在本 章稍后,你将探索其中一个模块:shelve。 在Python 中,读写文件有3 个步骤: 1.调用open()函____________数,返回一个File 对象。 2.调用File 对象的read()或write()方法。 3.调用File 对象的close()方法,关闭该文件。 第8 章 读写文件 145 8.2.1 用open()函数打开文件 要用open()函数打开一个文件,就要向它传递一个字符串路径,表明希望打开 的文件。这既可以是绝对路径,也可以是相对路径。open()函数返回一个File 对象。 尝试一下,先用Notepad 或TextEdit 创建一个文本文件,名为hello.txt。输入 Hello world!作为该文本文件的内容,将它保存在你的用户文件夹中。然后,如果使 用Windows,在交互式环境中输入以下代码: helloFile = open('C:\Users\your_home_folder\hello.txt') 如果使用OS X,在交互式环境中输入以下代码: helloFile = open('/Users/your_home_folder/hello.txt') 请确保用你自己的计算机用户名取代your_home_folder。例如,我的用户名是 asweigart,所以我在windows 下输入'C:\Users\asweigart\hello.txt'。 这些命令都将以读取纯文本文件的模式打开文件,或简称为“读模式”。当文件 以读模式打开时,Python 只让你从文件中读取数据,你不能以任何方式写入或修改它。 在Python 中打开文件时,读模式是默认的模式。但如果你不希望依赖于Python 的默 认值,也可以明确指明该模式,向open()传入字符串'r',作为第二个参数。所以 open('/Users/asweigart/hello.txt', 'r')和open('/Users/asweigart/hello.txt')做的事情一样。 调用open()将返回一个File 对象。File 对象代表计算机中的一个文件,它只是 Python 中另一种类型的值,就像你已熟悉的列表和字典。在前面的例子中,你将File 对象保存在helloFile 变量中。现在,当你需要读取或写入该文件,就可以调用 helloFile 变量中的File 对象的方法。 8.2.2 读取文件内容 既然有了一个File 对象,就可以开始从它读取内容。如果你希望将整个文件的 内容读取为一个字符串值,就使用File 对象的read()方法。让我们继续使用保存在 helloFile 中的hello.txt File 对象。在交互式环境中输入以下代码: helloContent = helloFile.read() helloContent 'Hello world!' 如果你将文件的内容看成是单个大字符串,read()方法就返回保存在该文件中的 这个字符串。 或者,可以使用readlines()方法,从该文件取得一个字符串的列表。列表中的 每个字符串就是文本中的每一行。例如,在hello.txt 文件相同的目录下,创建一个 名为sonnet29.txt 的文件,并在其中写入以下文本: When, in disgrace with fortune and men's eyes, I all alone beweep my outcast state, 146 Python 编程快速上手——让繁琐工作自动化 And trouble deaf heaven with my bootless cries, And look upon myself and curse my fate, 确保用换行分开这4 行。然后在交互式环境中输入以下代码: sonnetFile = open('sonnet29.txt') sonnetFile.readlines() [When, in disgrace with fortune and men's eyes,\n', ' I all alone beweep my outcast state,\n', And trouble deaf heaven with my bootless cries,\n', And look upon myself and curse my fate,'] 请注意,每个字符串值都以一个换行字符\n 结束。除了文件的最后一行。与单 个大字符串相比,字符串的列表通常更容易处理。 8.2.3 写入文件 Python 允许你将内容写入文件,方式与print()函数将字符串“写”到屏幕上类 似。但是,如果打开文件时用读模式,就不能写入文件。你需要以“写入纯文本模 式”或“添加纯文本模式”打开该文件,或简称为“写模式”和“添加模式”。 写模式将覆写原有的文件,从头开始,就像你用一个新值覆写一个变量的值。 将'w'作为第二个参数传递给open(),以写模式打开该文件。不同的是,添加模式将 在已有文件的末尾添加文本。你可以认为这类似向一个变量中的列表添加内容,而 不是完全覆写该变量。将'a'作为第二个参数传递给open(),以添加模式打开该文件。 如果传递给 open()的文件名不存在,写模式和添加模式都会创建一个新的空文 件。在读取或写入文件后,调用close()方法,然后才能再次打开该文件。 让我们整合这些概念。在交互式环境中输入以下代码: baconFile = open('bacon.txt', 'w') baconFile.write('Hello world!\n') 13 baconFile.close() baconFile = open('bacon.txt', 'a') baconFile.write('Bacon is not a vegetable.') 25 baconFile.close() baconFile = open('bacon.txt') content = baconFile.read() baconFile.close() print(content) Hello world! Bacon is not a vegetable. 首先,我们以写模式打开bacon.txt。因为还没有bacon.txt,Python 就创建了一 个。在打开的文件上调用write(),并向write()传入字符串参数'Hello world! \n',将 字符串写入文件,并返回写入的字符个数,包括换行符。然后关闭该文件。 为了将文本添加到文件已有的内容,而不是取代我们刚刚写入的字符串,我们 就以添加模式打开该文件。向该文件写入'Bacon is not a vegetable.',并关闭它。最后, 为了将文件的内容打印到屏幕上,我们以默认的读模式打开该文件,调用read(), 第8 章 读写文件 147 将得到的内容保存在content 中,关闭该文件,并打印content。 请注意,write()方法不会像print()函数那样,在字符串的末尾自动添加换行字 符。必须自己添加该字符。 8.3 用shelve 模块保存变量 利用shelve 模块,你可以将Python 程序中的变量保存到二进制的shelf 文件中。 这样,程序就可以从硬盘中恢复变量的数据。shelve 模块让你在程序中添加“保存” 和“打开”功能。例如,如果运行一个程序,并输入了一些配置设置,就可以将这 些设置保存到一个shelf 文件,然后让程序下一次运行时加载它们。 在交互式环境中输入以下代码: import shelve shelfFile = shelve.open('mydata') cats = ['Zophie', 'Pooka', 'Simon'] shelfFile['cats'] = cats shelfFile.close() 要利用shelve 模块读写数据,首先要导入它。调用函数shelve.open()并传入一个文件 名,然后将返回的值保存在一个变量中。可以对这个变量的shelf 值进行修改,就像它是 一个字典一样。当你完成时,在这个值上调用close()。这里,我们的shelf 值保存在shelfFile 中。我们创建了一个列表cats,并写下shelfFile['cats'] =cats,将该列表保存在shelfFile 中, 作为键'cats'关联的值(就像在字典中一样)。然后我们在shelfFile 上调用close()。 在Windows 上运行前面的代码,你会看到在当前工作目录下有3 个新文件: mydata.bak、mydata.dat 和mydata.dir。在OS X 上,只会创建一个mydata.db 文件。 这些二进制文件包含了存储在shelf 中的数据。这些二进制文件的格式并不重 要,你只需要知道shelve 模块做了什么,而不必知道它是怎么做的。该模块让你不 用操心如何将程序的数据保存到文件中。 你的程序稍后可以使用shelve 模块,重新打开这些文件并取出数据。shelf 值不 必用读模式或写模式打开,因为它们在打开后,既能读又能写。在交互式环境中输 入以下代码: shelfFile = shelve.open('mydata') type(shelfFile) <class 'shelve.DbfilenameShelf'> shelfFile['cats'] ['Zophie', 'Pooka', 'Simon'] shelfFile.close() 这里,我们打开了shelf 文件,检查我们的数据是否正确存储。输入shelfFile['cats'] 将返回我们前面保存的同一个列表,所以我们就知道该列表得到了正确存储,然后 我们调用close()。 就像字典一样,shelf 值有keys()和values()方法,返回shelf 中键和值的类似列 148 Python 编程快速上手——让繁琐工作自动化 表的值。因为这些方法返回类似列表的值,而不是真正的列表,所以应该将它们传 递给list()函数,取得列表的形式。在交互式环境中输入以下代码: shelfFile = shelve.open('mydata') list(shelfFile.keys()) ['cats'] list(shelfFile.values()) 'Zophie', 'Pooka', 'Simon' shelfFile.close() 创建文件时,如果你需要在Notepad 或TextEdit 这样的文本编辑器中读取它们, 纯文本就非常有用。但是,如果想要保存Python 程序中的数据,那就使用shelve 模块。 8.4 用pprint.pformat()函数保存变量 回忆一下5.2 节“漂亮打印”中,pprint.pprint()函数将列表或字典中的内容“漂 亮打印”到屏幕,而pprint.pformat()函数将返回同样的文本字符串,但不是打印它。 这个字符串不仅是易于阅读的格式,同时也是语法上正确的Python 代码。假定你有 一个字典,保存在一个变量中,你希望保存这个变量和它的内容,以便将来使用。 pprint.pformat()函数将提供一个字符串,你可以将它写入.py 文件。该文件将成为你自 己的模块,如果你需要使用存储在其中的变量,就可以导入它。 例如,在交互式环境中输入以下代码: import pprint cats = [{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}] pprint.pformat(cats) "[{'desc': 'chubby', 'name': 'Zophie'}, {'desc': 'fluffy', 'name': 'Pooka'}]" fileObj = open('myCats.py', 'w') fileObj.write('cats = ' + pprint.pformat(cats) + '\n') 83 fileObj.close() 这里,我们导入了pprint,以便能使用pprint.pformat()。我们有一个字典的列表, 保存在变量cats 中。为了让cats 中的列表在关闭交互式环境后仍然可用,我们利用 pprint.pformat(),将它返回为一个字符串。当我们有了cats 中数据的字符串形式, 就很容易将该字符串写入一个文件,我们将它命名为myCats.py。 import 语句导入的模块本身就是Python 脚本。如果来自pprint.pformat()的字符 串保存为一个.py 文件,该文件就是一个可以导入的模块,像其他模块一样。 由于Python 脚本本身也是带有.py 文件扩展名的文本文件,所以你的Python 程 序甚至可以生成其他Python 程序。然后可以将这些文件导入到脚本中。 import myCats myCats.cats [{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}] myCats.cats[0] {'name': 'Zophie', 'desc': 'chubby'} myCats.cats[0]['name'] 'Zophie' 第8 章 读写文件 149 创建一个.py 文件(而不是利用shelve 模块保存变量)的好处在于,因为它是 一个文本文件,所以任何人都可以用一个简单的文本编辑器读取和修改该文件的内 容。但是,对于大多数应用,利用shelve 模块来保存数据,是将变量保存到文件的 最佳方式。只有基本数据类型,诸如整型、浮点型、字符串、列表和字典,可以作 为简单文本写入一个文件。例如,File 对象就不能够编码为文本。 8.5 项目:生成随机的测验试卷文件 假如你是一位地理老师,班上有35 名学生,你希望进行美国各州首府的一个 小测验。不妙的是,班里有几个坏蛋,你无法确信学生不会作弊。你希望随机调整 问题的次序,这样每份试卷都是独一无二的,这让任何人都不能从其他人那里抄袭 答案。当然,手工完成这件事又费时又无聊。好在,你懂一些Python。 下面是程序所做的事: ? 创建35 份不同的测验试卷。 ? 为每份试卷创建50 个多重选择题,次序随机。 ? 为每个问题提供一个正确答案和3 个随机的错误答案,次序随机。 ? 将测验试卷写到35 个文本文件中。 ? 将答案写到35 个文本文件中。 这意味着代码需要做下面的事: ? 将州和它们的首府保存在一个字典中。 ? 针对测验文本文件和答案文本文件,调用open()、write()和close()。 ? 利用random.shuffle()随机调整问题和多重选项的次序。 第1 步:将测验数据保存在一个字典中 第一步是创建一个脚本框架,并填入测验数据。创建一个名为randomQuiz Generator.py 的文件,让它看起来像这样: #! python3
? import random
? capitals = {'Alabama': 'Montgomery', 'Alaska': 'Juneau', 'Arizona': 'Phoenix', 'Arkansas': 'Little Rock', 'California': 'Sacramento', 'Colorado': 'Denver', 'Connecticut': 'Hartford', 'Delaware': 'Dover', 'Florida': 'Tallahassee', 'Georgia': 'Atlanta', 'Hawaii': 'Honolulu', 'Idaho': 'Boise', 'Illinois': 'Springfield', 'Indiana': 'Indianapolis', 'Iowa': 'Des Moines', 'Kansas': 'Topeka', 'Kentucky': 'Frankfort', 'Louisiana': 'Baton Rouge', 'Maine': 'Augusta', 'Maryland': 'Annapolis', 'Massachusetts': 'Boston', 'Michigan': 'Lansing', 'Minnesota': 'Saint Paul', 'Mississippi': 'Jackson', 'Missouri': 'Jefferson City', 'Montana': 'Helena', 'Nebraska': 'Lincoln', 'Nevada': 'Carson City', 'New Hampshire': 'Concord', 'New Jersey': 'Trenton', 'New 150 Python 编程快速上手——让繁琐工作自动化 Mexico': 'Santa Fe', 'New York': 'Albany', 'North Carolina': 'Raleigh', 'North Dakota': 'Bismarck', 'Ohio': 'Columbus', 'Oklahoma': 'Oklahoma City', 'Oregon': 'Salem', 'Pennsylvania': 'Harrisburg', 'Rhode Island': 'Providence', 'South Carolina': 'Columbia', 'South Dakota': 'Pierre', 'Tennessee': 'Nashville', 'Texas': 'Austin', 'Utah': 'Salt Lake City', 'Vermont': 'Montpelier', 'Virginia': 'Richmond', 'Washington': 'Olympia', 'West Virginia': 'Charleston', 'Wisconsin': 'Madison', 'Wyoming': 'Cheyenne'}
? for quizNum in range(35):
因为这个程序将随机安排问题和答案的次序,所以需要导入random 模块?, 以便利用其中的函数。capitals 变量?含一个字典,以美国州名作为键,以州首府作 为值。因为你希望创建35 份测验试卷,所以实际生成测验试卷和答案文件的代码 (暂时用TODO 注释标注)会放在一个for 循环中,循环35 次?(这个数字可以改 变,生成任何数目的测验试卷文件)。 第2 步:创建测验文件,并打乱问题的次序 现在是时候填入那些TODO 了。 循环中的代码将重复执行35 次(每次生成一份测验试卷),所以在循环中,你 只需要考虑一份测验试卷。首先你要创建一个实际的测验试卷文件,它需要有唯一 的文件名,并且有某种标准的标题部分,留出位置,让学生填写姓名、日期和班级。 然后需要得到随机排列的州的列表,稍后将用它来创建测验试卷的问题和答案。 在randomQuizGenerator.py 中添加以下代码行: #! python3
--snip--
for quizNum in range(35):
? quizFile = open('capitalsquiz%s.txt' % (quizNum + 1), 'w') ? answerKeyFile = open('capitalsquiz_answers%s.txt' % (quizNum + 1), 'w')
? quizFile.write('Name:\n\nDate:\n\nPeriod:\n\n') quizFile.write((' ' * 20) + 'State Capitals Quiz (Form %s)' % (quizNum + 1)) quizFile.write('\n\n')
states = list(capitals.keys()) ? random.shuffle(states)
第8 章 读写文件 151 测验试卷的文件名将是capitalsquiz.txt,其中是该测验试卷的唯一编号, 来自于quizNum,即for 循环的计数器。针对capitalsquiz.txt 的答案将保存在一 个文本文件中,名为capitalsquiz_answers.txt。每次执行循环,'capitalsquiz%s.txt' 和'capitalsquiz_answers%s.txt'中的占位符%s 都将被(quizNum + 1)取代,所以第一个 测验试卷和答案将是capitalsquiz1.txt 和capitalsquiz_answers1.txt。在?和?的open() 函数调用将创建这些文件,以'w'作为第二个参数,以写模式打开它们。 ?处write()语句创建了测验标题,让学生填写。最后,利用random.shuffle()函 数?,创建了美国州名的随机列表。该函数重新随机排列传递给它的列表中的值。 第3 步:创建答案选项 现在需要为每个问题生成答案选项,这将是A 到D 的多重选择。你需要创建 另一个for 循环,该循环生成测验试卷的50 个问题的内容。然后里面会嵌套第三个 for 循环,为每个问题生成多重选项。让你的代码看起来像这样: #! python3
--snip--
for questionNum in range(50):
? correctAnswer = capitals[states[questionNum]] ? wrongAnswers = list(capitals.values()) ? del wrongAnswers[wrongAnswers.index(correctAnswer)] ? wrongAnswers = random.sample(wrongAnswers, 3) ? answerOptions = wrongAnswers + [correctAnswer] ? random.shuffle(answerOptions)
正确的答案很容易得到,它作为一个值保存在capitals 字典中?。这个循环将 遍历打乱过的states 列表中的州,从states[0]到states[49],在capitals 中找到每个州, 将该州对应的首府保存在correctAnswer 中。 可能的错误答案列表需要一点技巧。你可以从capitals 字典中复制所有的值?, 删除正确的答案?,然后从该列表中选择3 个随机的值?。random.sample()函数使 得这种选择很容易,它的第一个参数是你希望选择的列表,第二个参数是你希望选 择的值的个数。完整的答案选项列表是这3 个错误答案与正确答案的组合?。最后, 答案需要随机排列?,这样正确的答案就不会总是选项D。 第4 步:将内容写入测验试卷和答案文件 剩下来就是将问题写入测验试卷文件,将答案写入答案文件。让你的代码看起 152 Python 编程快速上手——让繁琐工作自动化 来像这样: #! python3
--snip--
for questionNum in range(50): --snip--
quizFile.write('%s. What is the capital of %s?\n' % (questionNum + 1, states[questionNum])) ? for i in range(4): ? quizFile.write(' %s. %s\n' % ('ABCD'[i], answerOptions[i])) quizFile.write('\n')
? answerKeyFile.write('%s. %s\n' % (questionNum + 1, 'ABCD'[ answerOptions.index(correctAnswer)])) quizFile.close() answerKeyFile.close() 一个遍历整数0 到3 的for 循环,将答案选项写入answerOptions 列表?。?处 的表达式'ABCD'[i]将字符串'ABCD'看成是一个数组,它在循环的每次迭代中,将分 别求值为'A'、'B'、'C'和'D'。 在最后一行?,表达式answerOptions.index(correctAnswer)将在随机排序的答案 选项中,找到正确答案的整数下标,并且'ABCD'[answerOptions.index(correctAnswer)] 将求值为正确答案的字母,写入到答案文件中。 在运行该程序后,下面就是capitalsquiz1.txt 文件看起来的样子。但是,你的问 题和答案选项当然与这里显示的可能会不同。这取决于random.shuffle()调用的结果: Name: Date: Period: State Capitals Quiz (Form 1)
- What is the capital of West Virginia? A. Hartford B. Santa Fe C. Harrisburg D. Charleston
- What is the capital of Colorado? A. Raleigh B. Harrisburg C. Denver D. Lincoln --snip-- 对应的capitalsquiz_answers1.txt 文本文件看起来像这样: 第8 章 读写文件 153
- D
- C
- A
- C --snip-- 8.6 项目:多重剪贴板 假定你有一个无聊的任务,要填充一个网页或软件中的许多表格,其中包含一 些文本字段。剪贴板让你不必一次又一次输入同样的文本,但剪贴板上一次只有一 个内容。如果你有几段不同的文本需要拷贝粘贴,就不得不一次又一次的标记和拷 贝几个同样的内容。 可以编写一个Python 程序,追踪几段文本。这个“多重剪贴板”将被命名为 mcb.pyw(因为“mcb”比输入“multiclipboard”更简单)。.pyw 扩展名意味着Python 运行该程序时,不会显示终端窗口(详细内容请参考附录B)。 该程序将利用一个关键字保存每段剪贴板文本。例如,当运行py mcb.pyw save spam,剪贴板中当前的内容就用关键字spam 保存。通过运行py mcb.pyw spam,这 段文本稍后将重新加载到剪贴板中。如果用户忘记了都有哪些关键字,他们可以运 行py mcb.pyw list,将所有关键字的列表复制到剪贴板中。 下面是程序要做的事: ? 针对要检查的关键字,提供命令行参数。 ? 如果参数是save,那么将剪贴板的内容保存到关键字。 ? 如果参数是list,就将所有的关键字拷贝到剪贴板。 ? 否则,就将关键词对应的文本拷贝到剪贴板。 这意味着代码需要做下列事情: ? 从sys.argv 读取命令行参数。 ? 读写剪贴板。 ? 保存并加载shelf 文件。 如果你使用Windows,可以创建一个名为mcb.bat 的批处理文件,很容易地通 过“Run…”窗口运行这个脚本。该批处理文件包含如下内容: @pyw.exe C:\Python34\mcb.pyw %* 第1 步:注释和shelf 设置 我们从一个脚本框架开始,其中包含一些注释和基本设置。让你的代码看起来 像这样: #! python3
? # Usage: py.exe mcb.pyw save - Saves clipboard to keyword.
154 Python 编程快速上手——让繁琐工作自动化
? import shelve, pyperclip, sys ? mcbShelf = shelve.open('mcb')
m cbShelf.close() 将一般用法信息放在文件顶部的注释中,这是常见的做法?。如果忘了如何运 行这个脚本,就可以看看这些注释,帮助回忆起来。然后导入模块?。拷贝和粘贴 需要pyperclip 模块,读取命令行参数需要sys 模块。shelve 模块也需要准备好。当 用户希望保存一段剪贴板文本时,你需要将它保存到一个shelf 文件中。然后,当 用户希望将文本拷贝回剪贴板时,你需要打开shelf 文件,将它重新加载到程序中。 这个shlef 文件命名时带有前缀mcb?。 第2 步:用一个关键字保存剪贴板内容 根据用户希望保存文本到一个关键字,或加载文本到剪贴板,或列出已有的关键 字,该程序做的事情不一样。让我们来处理第一种情况。让你的代码看起来像这样: #! python3
--snip--
? if len(sys.argv) == 3 and sys.argv[1].lower() == 'save': ? mcbShelf[sys.argv[2]] = pyperclip.paste() elif len(sys.argv) == 2: ? # TODO: List keywords and load content. m cbShelf.close() 如果第一个命令行参数(它总是在sys.argv 列表的下标1 处)是字符串'save' ?, 第二个命令行参数就是保存剪贴板当前内容的关键字。关键字将用做 mcbShelf 中的 键,值就是当前剪贴板上的文本?。 如果只有一个命令行参数,就假定它要么是'list',要么是需要加载到剪贴板的 关键字。稍后你将实现这些代码。现在只是放上一条TODO 注释?。 第3 步:列出关键字和加载关键字的内容 最后,让我们实现剩下的两种情况。用户希望从关键字加载剪贴板文本,或希 望列出所有可用的关键字。让你的代码看起来像这样: #! python3
--snip-- 第8 章 读写文件 155
if len(sys.argv) == 3 and sys.argv[1].lower() == 'save': mcbShelf[sys.argv[2]] = pyperclip.paste() elif len(sys.argv) == 2:
? if sys.argv[1].lower() == 'list': ? pyperclip.copy(str(list(mcbShelf.keys()))) elif sys.argv[1] in mcbShelf: ? pyperclip.copy(mcbShelf[sys.argv[1]]) m cbShelf.close() 如果只有一个命令行参数,首先检查它是不是'list' ?。如果是,表示shelf 键的 列表的字符串将被拷贝到剪贴板?。用户可以将这个列表拷贝到一个打开的文本编 辑器,进行查看。 否则,你可以假定该命令行参数是一个关键字。如果这个关键字是shelf 中的 一个键,就可以将对应的值加载到剪贴板?。 齐活了!加载这个程序有几个不同步骤,这取决于你的计算机使用哪种操作系 统。请查看附录B,了解操作系统的详情。 回忆一下第6 章中创建的口令保管箱程序,它将口令保存在一个字典中。更新 口令需要更改该程序的源代码。这不太理想,因为普通用户不太适应通过更改源代 码来更新他们的软件。而且,每次修改程序的源代码时,就有可能不小心引入新的 缺陷。将程序的数据保存在不同的地方,而不是在代码中,就可以让别人更容易使 用你的程序,并且更不容易出错。 8.7 小结 文件被组织在文件夹中(也称为目录),路径描述了一个文件的位置。运行在计算 机上的每个程序都有一个当前工作目录,它让你相对于当前的位置指定文件路径,而 非总是需要完整路径(绝对路径)。os.path 模块包含许多函数,用于操作文件路径。 你的程序也可以直接操作文本文件的内容。open()函数将打开这些文件,将它 们的内容读取为一个大字符串(利用reae()方法),或读取为字符串的列表(利用方 法readlines())。Open()函数可以将文件以写模式或添加模式打开,分别创建新的文 本文件或在原有的文本文件中添加内容。 在前面几章中,你利用剪贴板在程序中获得大量文本,而不是通过手工输入。现 在你可以用程序直接读取硬盘上的文件,这是一大进步。因为文件比剪贴板更不易变 化。在下一章中,你将学习如何处理文件本身,包括复制、删除、重命名、移动等。 8.8 习题 1.相对路径是相对于什么? 2.绝对路径从什么开始? 156 Python 编程快速上手——让繁琐工作自动化 3.os.getcwd()和os.chdir()函数做什么事? 4..和..文件夹是什么? 5.在C:\bacon\eggs\spam.txt 中,哪一部分是目录名称,哪一部分是基本名称? 6.可以传递给open()函数的3 种“模式”参数是什么? 7.如果已有的文件以写模式打开,会发生什么? 8.read()和readlines()方法之间的区别是什么? 9.shelf 值与什么数据结构相似? 8.9 实践项目 作为实践,设计并编写下列程序。 8.9.1 扩展多重剪贴板 扩展本章中的多重剪贴板程序,增加一个delete 命令行参数,它将 从shelf 中删除一个关键字。然后添加一个delete 命令行参数,它将删除所有关键字。 8.9.2 疯狂填词 创建一个疯狂填词(Mad Libs)程序,它将读入文本文件,并让用户在该文本 文件中出现ADJECTIVE、NOUN、ADVERB 或VERB 等单词的地方,加上他们自 己的文本。例如,一个文本文件可能看起来像这样: The ADJECTIVE panda walked to the NOUN and then VERB. A nearby NOUN was unaffected by these events. 程序将找到这些出现的单词,并提示用户取代它们。 Enter an adjective: silly Enter a noun: chandelier Enter a verb: screamed Enter a noun: pickup truck 以下的文本文件将被创建: The silly panda walked to the chandelier and then screamed. A nearby pickup truck was unaffected by these events. 结果应该打印到屏幕上,并保存为一个新的文本文件。 8.9.3 正则表达式查找 编写一个程序,打开文件夹中所有的.txt 文件,查找匹配用户提供的正则表达 式的所有行。结果应该打印到屏幕上。 第 章 组 织 文 件 在上一章中,你学习了如何用Python 创建并写入新文件。 你的程序也可以组织硬盘上已经存在的文件。也许你曾经经历 过查找一个文件夹,里面有几十个、几百个,甚至上千个文件, 需要手工进行复制、改名、移动或压缩。或者考虑下面这样的 任务: ? 在一个文件夹及其所有子文件夹中,复制所有的pdf 文件 (且只复制pdf 文件) ? 针对一个文件夹中的所有文件,删除文件名中前导的零,该文件夹中有数百个 文件,名为spam001.txt、spam002.txt、spam003.txt 等。 ? 将几个文件夹的内容压缩到一个ZIP 文件中(这可能是一个简单的备份系统) 所有这种无聊的任务,正是在请求用Python 实现自动化。通过对电脑编程来完 成这些任务,你就把它变成了一个快速工作的文件职员,而且从不犯错。 在开始处理文件时你会发现,如果能够很快查看文件的扩展名(.txt、.pdf、.jpg 等),是很有帮助的。在OS X 和Linux 上,文件浏览器很有可能自动显示扩展名。 在Windows 上,文件扩展名可能默认是隐藏的。要显示扩展名,请点开Start?Control Panel?Appearance 和Personalization?Folder 选项。在View 选项卡中,Advanced Settings 之下,取消Hide extensions for known file types 复选框。 9 158 Python 编程快速上手——让繁琐工作自动化 9.1 shutil 模块 shutil(或称为shell 工具)模块中包含一些函数,让你在Python 程序中复制、 移动、改名和删除文件。要使用shutil 的函数,首先需要import shutil。 9.1.1 复制文件和文件夹 shutil 模块提供了一些函数,用于复制文件和整个文件夹。 调用shutil.copy(source, destination),将路径source 处的文件复制到路径destination 处的文件夹(source 和destination 都是字符串)。如果destination 是一个文件名,它将 作为被复制文件的新名字。该函数返回一个字符串,表示被复制文件的路径。 在交互式环境中输入以下代码,看看shutil.copy()的效果:
import shutil, os os.chdir('C:\') ? >>> shutil.copy('C:\spam.txt', 'C:\delicious') 'C:\delicious\spam.txt' ? >>> shutil.copy('eggs.txt', 'C:\delicious\eggs2.txt') ' C:\delicious\eggs2.txt' 第一个shutil.copy()调用将文件C:\spam.txt 复制到文件夹C:\delicious。返回值 是刚刚被复制的文件的路径。请注意,因为指定了一个文件夹作为目的地?,原来 的文件名spam.txt 就被用作新复制的文件名。第二个shutil.copy()调用?也将文件 C:\eggs.txt 复制到文件夹C:\delicious,但为新文件提供了一个名字eggs2.txt。 shutil.copy()将复制一个文件,shutil.copytree()将复制整个文件夹,以及它包含 的文件夹和文件。调用shutil.copytree(source, destination),将路径source 处的文件 夹,包括它的所有文件和子文件夹,复制到路径destination 处的文件夹。source 和 destination 参数都是字符串。该函数返回一个字符串,是新复制的文件夹的路径。 在交互式环境中输入以下代码: import shutil, os os.chdir('C:\') shutil.copytree('C:\bacon', 'C:\bacon_backup') 'C:\bacon_backup' shutil.copytree()调用创建了一个新文件夹,名为bacon_backup,其中的内容与 原来的bacon 文件夹一样。现在你已经备份了非常非常宝贵的“bacon”。 9.1.2 文件和文件夹的移动与改名 调用shutil.move(source, destination),将路径source 处的文件夹移动到路径 destination,并返回新位置的绝对路径的字符串。 如果destination 指向一个文件夹,source 文件将移动到destination 中,并保持 原来的文件名。例如,在交互式环境中输入以下代码: 第9 章 组织文件 159 import shutil shutil.move('C:\bacon.txt', 'C:\eggs') 'C:\eggs\bacon.txt' 假定在C:\目录中已存在一个名为eggs 的文件夹,这个shutil.move()调用就是 说,“将C:\bacon.txt 移动到文件夹C:\eggs 中。 如果在C:\eggs 中原来已经存在一个文件bacon.txt,它就会被覆写。因为用这 种方式很容易不小心覆写文件,所以在使用move()时应该注意。 destination 路径也可以指定一个文件名。在下面的例子中,source 文件被移动 并改名。 shutil.move('C:\bacon.txt', 'C:\eggs\new_bacon.txt') 'C:\eggs\new_bacon.txt' 这一行是说,“将C:\bacon.txt 移动到文件夹C:\eggs,完成之后,将bacon.txt 文件改名为new_bacon.txt。” 前面两个例子都假设在C:\目录下有一个文件夹eggs。但是如果没有eggs 文件 夹,move()就会将bacon.txt 改名,变成名为eggs 的文件。 shutil.move('C:\bacon.txt', 'C:\eggs') 'C:\eggs' 这里,move()在C:\目录下找不到名为eggs 的文件夹,所以假定destination 指 的是一个文件,而非文件夹。所以bacon.txt 文本文件被改名为eggs(没有.txt 文件 扩展名的文本文件),但这可能不是你所希望的!这可能是程序中很难发现的缺陷, 因为move()调用会很开心地做一些事情,但和你所期望的完全不同。这也是在使用 move()时要小心的另一个理由。 最后,构成目的地的文件夹必须已经存在,否则Python 会抛出异常。在交互式 环境中输入以下代码: shutil.move('spam.txt', 'c:\does_not_exist\eggs\ham') Traceback (most recent call last): File "C:\Python34\lib\shutil.py", line 521, in move os.rename(src, real_dst) FileNotFoundError: [WinError 3] The system cannot find the path specified: 'spam.txt' -> 'c:\does_not_exist\eggs\ham' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<pyshell#29>", line 1, in shutil.move('spam.txt', 'c:\does_not_exist\eggs\ham') File "C:\Python34\lib\shutil.py", line 533, in move copy2(src, real_dst) File "C:\Python34\lib\shutil.py", line 244, in copy2 copyfile(src, dst, follow_symlinks=follow_symlinks) File "C:\Python34\lib\shutil.py", line 108, in copyfile with open(dst, 'wb') as fdst: FileNotFoundError: [Errno 2] No such file or directory: 'c:\does_not_exist\ eggs\ham' 160 Python 编程快速上手——让繁琐工作自动化 Python 在does_not_exist 目录中寻找eggs 和ham。它没有找到不存在的目录, 所以不能将spam.txt 移动到指定的路径。 9.1.3 永久删除文件和文件夹 利用os 模块中的函数,可以删除一个文件或一个空文件夹。但利用shutil 模块, 可以删除一个文件夹及其所有的内容。 ? 用os.unlink(path)将删除path 处的文件。 ? 调用os.rmdir(path)将删除path 处的文件夹。该文件夹必须为空,其中没有任 何文件和文件夹。 ? 调用shutil.rmtree(path)将删除path 处的文件夹,它包含的所有文件和文件夹都 会被删除。 在程序中使用这些函数时要小心!可以第一次运行程序时,注释掉这些调用, 并且加上print()调用,显示会被删除的文件。这样做是一个好主意。下面有一个 Python 程序,本来打算删除具有.txt 扩展名的文件,但有一处录入错误(用粗体突 出显示),结果导致它删除了.rxt 文件。 import os for filename in os.listdir(): if filename.endswith('.rxt'): os.unlink(filename) 如果你有某些重要的文件以.rxt 结尾,它们就会被不小心永久地删除。作为替 代,你应该先运行像这样的程序: import os for filename in os.listdir(): if filename.endswith('.rxt'): #os.unlink(filename) print(filename) 现在os.unlink()调用被注释掉,所以Python 会忽略它。作为替代,你会打印出 将被删除的文件名。先运行这个版本的程序,你就会知道,你不小心告诉程序要删 除.rxt 文件,而不是.txt 文件。 在确定程序按照你的意图工作后, 删除print(filename) 代码行, 取消 os.unlink(filename)代码行的注释。然后再次运行该程序,实际删除这些文件。 9.1.4 用send2trash 模块安全地删除 因为Python 内建的shutil.rmtree()函数不可恢复地删除文件和文件夹,所以用起 来可能有危险。删除文件和文件夹的更好方法,是使用第三方的send2trash 模块。 你可以在终端窗口中运行pip install send2trash,安装该模块(参见附录A,其中更 详细地解释了如何安装第三方模块)。 利用send2trash,比Python 常规的删除函数要安全得多,因为它会将文件夹和 第9 章 组织文件 161 文件发送到计算机的垃圾箱或回收站,而不是永久删除它们。如果因程序缺陷而用 send2trash 删除了某些你不想删除的东西,稍后可以从垃圾箱恢复。 安装send2trash 后,在交互式环境中输入以下代码: import send2trash baconFile = open('bacon.txt', 'a') # creates the file baconFile.write('Bacon is not a vegetable.') 25 baconFile.close() send2trash.send2trash('bacon.txt') 一般来说,总是应该使用send2trash.send2trash()函数来删除文件和文件夹。虽 然它将文件发送到垃圾箱,让你稍后能够恢复它们,但是这不像永久删除文件,不 会释放磁盘空间。如果你希望程序释放磁盘空间,就要用os 和shutil 来删除文件和 文件夹。请注意,send2trash()函数只能将文件送到垃圾箱,不能从中恢复文件。 9.2 遍历目录树 假定你希望对某个文件夹中的所有文件改名,包括该文件夹中所有子文件夹中 的所有文件。也就是说,你希望遍历目录树,处理遇到的每个文件。写程序完成这 件事,可能需要一些技巧。好在,Python 提供了一个函数,替你处理这个过程。 请看C:\delicious 文件夹及其内容,如图9-1 所示。 图9-1 一个示例文件夹,包含3 个文件夹和4 个文件 这里有一个例子程序,针对图9-1 的目录树,使用了os.walk()函数: import os for folderName, subfolders, filenames in os.walk('C:\delicious'): 162 Python 编程快速上手——让繁琐工作自动化 print('The current folder is ' + folderName) for subfolder in subfolders: print('SUBFOLDER OF ' + folderName + ': ' + subfolder) for filename in filenames: print('FILE INSIDE ' + folderName + ': '+ filename) print('') os.walk()函数被传入一个字符串值,即一个文件夹的路径。你可以在一个for 循环语句中使用os.walk()函数,遍历目录树,就像使用range()函数遍历一个范围的 数字一样。不像range(),os.walk()在循环的每次迭代中,返回3 个值: 1.当前文件夹名称的字符串。 2.当前文件夹中子文件夹的字符串的列表。 3.当前文件夹中文件的字符串的列表。 所谓当前文件夹,是指for 循环当前迭代的文件夹。程序的当前工作目录,不 会因为os.walk()而改变。 就像你可以在代码for i in range(10):中选择变量名称i 一样,你也可以选择前面 列出来的3 个字的变量名称。我通常使用foldername、subfolders 和filenames。 运行该程序,它的输出如下: The current folder is C:\delicious SUBFOLDER OF C:\delicious: cats SUBFOLDER OF C:\delicious: walnut FILE INSIDE C:\delicious: spam.txt The current folder is C:\delicious\cats FILE INSIDE C:\delicious\cats: catnames.txt FILE INSIDE C:\delicious\cats: zophie.jpg The current folder is C:\delicious\walnut SUBFOLDER OF C:\delicious\walnut: waffles The current folder is C:\delicious\walnut\waffles FILE INSIDE C:\delicious\walnut\waffles: butter.txt. 因为os.walk()返回字符串的列表,保存在subfolder 和filename 变量中,所以你 可以在它们自己的for 循环中使用这些列表。用你自己定制的代码,取代print()函 数调用(或者如果不需要,就删除for 循环)。 9.3 用zipfile 模块压缩文件 你可能熟悉ZIP 文件(带有.zip 文件扩展名),它可以包含许多其他文件的压缩 内容。压缩一个文件会减少它的大小,这在因特网上传输时很有用。因为一个ZIP 文 件可以包含多个文件和子文件夹,所以它是一种很方便的方式,将多个文件打包成一 个文件。这个文件叫做“归档文件”,然后可以用作电子邮件的附件,或其他用途。 第9 章 组织文件 163 利用zipfile 模块中的函数,Python 程序可以创建和打开(或解压)ZIP 文件。 假定你有一个名为example.zip 的zip 文件,它的内容如图9-2 所示。 图9-2 example.zip 的内容 可以从http://nostarch.com/automatestuff/下载这个ZIP 文件,或者利用计算机上 已有的一个ZIP 文件,接着完成下面的操作。 9.3.1 读取ZIP 文件 要读取ZIP 文件的内容,首先必须创建一个ZipFile 对象(请注意大写首字母Z 和F)。ZipFile 对象在概念上与File 对象相似,你在第8 章中曾经看到open()函数 返回File 对象:它们是一些值,程序通过它们与文件打交道。要创建一个ZipFile 对象,就调用zipfile.ZipFile()函数,向它传入一个字符串,表示.zip 文件的文件名。 请注意,zipfile 是Python 模块的名称,ZipFile()是函数的名称。 例如,在交互式环境中输入以下代码: import zipfile, os os.chdir('C:\') # move to the folder with example.zip exampleZip = zipfile.ZipFile('example.zip') exampleZip.namelist() ['spam.txt', 'cats/', 'cats/catnames.txt', 'cats/zophie.jpg'] spamInfo = exampleZip.getinfo('spam.txt') spamInfo.file_size 13908 spamInfo.compress_size 3828 ? >>> 'Compressed file is %sx smaller!' % (round(spamInfo.file_size / spamInfo .compress_size, 2)) 'Compressed file is 3.63x smaller!' exampleZip.close() ZipFile 对象有一个namelist()方法,返回ZIP 文件中包含的所有文件和文件夹 的字符串的列表。这些字符串可以传递给ZipFile 对象的getinfo()方法,返回一个关 于特定文件的ZipInfo 对象。ZipInfo 对象有自己的属性,诸如表示字节数的file_size 和compress_size,它们分别表示原来文件大小和压缩后文件大小。ZipFile 对象表示 整个归档文件,而ZipInfo 对象则保存该归档文件中每个文件的有用信息。 ?处的命令计算出example.zip 压缩的效率,用压缩后文件的大小除以原来文件 164 Python 编程快速上手——让繁琐工作自动化 的大小,并以%s 字符串格式打印出这一信息。 9.3.2 从ZIP 文件中解压缩 ZipFile 对象的extractall()方法从ZIP 文件中解压缩所有文件和文件夹,放到当 前工作目录中。 import zipfile, os os.chdir('C:\') # move to the folder with example.zip exampleZip = zipfile.ZipFile('example.zip') ? >>> exampleZip.extractall() exampleZip.close() 运行这段代码后,example.zip 的内容将被解压缩到C:\。或者,你可以向 extractall()传递的一个文件夹名称,它将文件解压缩到那个文件夹,而不是当前工作 目录。如果传递给extractall()方法的文件夹不存在,它会被创建。例如,如果你用 exampleZip.extractall('C:\ delicious')取代?处的调用,代码就会从example.zip 中解压 缩文件,放到新创建的C:\delicious 文件夹中。 ZipFile 对象的extract()方法从ZIP 文件中解压缩单个文件。继续交互式环境中 的例子: exampleZip.extract('spam.txt') 'C:\spam.txt' exampleZip.extract('spam.txt', 'C:\some\new\folders') 'C:\some\new\folders\spam.txt' exampleZip.close() 传递给extract()的字符串,必须匹配namelist()返回的字符串列表中的一个。或 者,你可以向extract()传递第二个参数,将文件解压缩到指定的文件夹,而不是当 前工作目录。如果第二个参数指定的文件夹不存在,Python 就会创建它。extract() 的返回值是被压缩后文件的绝对路径。 9.3.3 创建和添加到ZIP 文件 要创建你自己的压缩ZIP 文件,必须以“写模式”打开ZipFile 对象,即传入'w' 作为第二个参数(这类似于向open()函数传入'w',以写模式打开一个文本文件)。 如果向ZipFile 对象的write()方法传入一个路径,Python 就会压缩该路径所指 的文件,将它加到ZIP 文件中。write()方法的第一个参数是一个字符串,代表要添 加的文件名。第二个参数是“压缩类型”参数,它告诉计算机使用怎样的算法来压 缩文件。可以总是将这个值设置为zipfile.ZIP_DEFLATED(这指定了deflate 压缩 算法,它对各种类型的数据都很有效)。在交互式环境中输入以下代码: import zipfile newZip = zipfile.ZipFile('new.zip', 'w') newZip.write('spam.txt', compress_type=zipfile.ZIP_DEFLATED) newZip.close() 第9 章 组织文件 165 这段代码将创建一个新的ZIP 文件,名为new.zip,它包含spam.txt 压缩后的内容。 要记住,就像写入文件一样,写模式将擦除ZIP 文件中所有原有的内容。如果 只是希望将文件添加到原有的ZIP 文件中,就要向zipfile.ZipFile()传入'a'作为第二 个参数,以添加模式打开ZIP 文件。 9.4 项目:将带有美国风格日期的文件改名为欧洲风格日期 假定你的老板用电子邮件发给你上千个文件,文件名包含美国风格的日期 (MM-DD-YYYY),需要将它们改名为欧洲风格的日期(DD-MM-YYYY)。手工完 成这个无聊的任务可能需要几天时间!让我们写一个程序来完成它。 下面是程序要做的事: ? 检查当前工作目录的所有文件名,寻找美国风格的日期。 ? 如果找到,将该文件改名,交换月份和日期的位置,使之成为欧洲风格。 这意味着代码需要做下面的事情: ? 创建一个正则表达式,可以识别美国风格日期的文本模式。 ? 调用os.listdir(),找出工作目录中的所有文件。 ? 循环遍历每个文件名,利用该正则表达式检查它是否包含日期。 ? 如果它包含日期,用shutil.move()对该文件改名。 对于这个项目,打开一个新的文件编辑器窗口,将代码保存为renameDates.py。 第1 步:为美国风格的日期创建一个正则表达式 程序的第一部分需要导入必要的模块,并创建一个正则表达式,它能识别 MM-DD-YYYY 格式的日期。TODO 注释将提醒你,这个程序还要写什么。将它们作 为TODO,就很容易利用IDLE 的Ctrl-F 查找功能找到它们。让你的代码看起来像这样: #! python3
? import shutil, os, re
? datePattern = re.compile(r"""^(.?) # all text before the date ((0|1)?\d)- # one or two digits for the month ((0|1|2|3)?\d)- # one or two digits for the day ((19|20)\d\d) # four digits for the year (.?)$ # all text after the date ? """, re.VERBOSE)
166 Python 编程快速上手——让繁琐工作自动化
通过本章,你知道shutil.move()函数可以用于文件改名:它的参数是要改名的文件 名,以及新的文件名。因为这个函数存在于shutil 模块中,所以你必须导入该模块?。 在为这些文件改名之前,需要确定哪些文件要改名。文件名如果包含 spam4-4-1984.txt 和01-03-2014eggs.zip 这样的日期,就应该改名,而文件名不包含 日期的应该忽略,诸如littlebrother.epub。 可以用正则表达式来识别该模式。在开始导入re 模块后,调用re.compile()创 建一个Regex 对象?。传入re.VERBOSE 作为第二参数?,这将在正则表达式字符 串中允许空白字符和注释,让它更可读。 正则表达式字符串以^(.?)开始,匹配文件名开始处、日期出现之前的任何文本。 ((0|1)?\d)分组匹配月份。第一个数字可以是0 或1,所以正则表达式匹配12,作为 十二月份,也会匹配02,作为二月份。这个数字也是可选的,所以四月份可以是 04 或4。日期的分组是((0|1|2|3)?\d),它遵循类似的逻辑。3、03 和31 是有效的日期 数字(是的,这个正则表达式会接受一些无效的日期,诸如4-31-2014、2-29-2013 和0-15-2014。日期有许多特例,很容易被遗漏。为了简单,这个程序中的正则表 达式已经足够好了)。 虽然1885 是一个有效的年份,但你可能只在寻找20 世纪和21 世纪的年份。 这防止了程序不小心匹配非日期的文件名, 它们和日期格式类似, 诸如 10-10-1000.txt。 正则表达式的(.?)$部分,将匹配日期之后的任何文本。 第2 步:识别文件名中的日期部分 接下来,程序将循环遍历os.listdir()返回的文件名字符串列表,用这个正则表达 式匹配它们。文件名不包含日期的文件将被忽略。如果文件名包含日期,匹配的文 本将保存在几个变量中。用下面的代码代替程序中前3 个TODO: #! python3
--snip--
for amerFilename in os.listdir('.'): mo = datePattern.search(amerFilename)
? if mo == None: 第9 章 组织文件 167 ? continue ? # Get the different parts of the filename. beforePart = mo.group(1) monthPart = mo.group(2) dayPart = mo.group(4) yearPart = mo.group(6) afterPart = mo.group(8)
- -snip-- 如果search()方法返回的Match 对象是None?,那么amerFilename 中的文件名 不匹配该正则表达式。continue 语句?将跳过循环剩下的部分,转向下一个文件名。 否则,该正则表达式分组匹配的不同字符串,将保存在名为beforePart、 monthPart、dayPart、yearPart 和afterPar 的变量中?。这些变量中的字符串将在下一 步中使用,用于构成欧洲风格的文件名。 为了让分组编号直观,请尝试从头阅读该正则表达式,每遇到一个左括号就计数加 一。不要考虑代码,只是写下该正则表达式的框架。这有助于使分组变得直观,例如: datePattern = re.compile(r"""^(1) # all text before the date (2 (3) )- # one or two digits for the month (4 (5) )- # one or two digits for the day (6 (7) ) # four digits for the year (8)$ # all text after the date """, re.VERBOSE) 这里,编号1 至8 代表了该正则表达式中的分组。写出该正则表达式的框架, 其中只包含括号和分组编号。这让你更清楚地理解所写的正则表达式,然后再转向 程序中剩下的部分。 第3 步:构成新文件名,并对文件改名 作为最后一步,连接前一步生成的变量中的字符串,得到欧洲风格的日期:日 期在月份之前。用下面的代码代替程序中最后3 个TODO: #! python3
--snip--
? euroFilename = beforePart + dayPart + '-' + monthPart + '-' + yearPart + afterPart
absWorkingDir = os.path.abspath('.') amerFilename = os.path.join(absWorkingDir, amerFilename) euroFilename = os.path.join(absWorkingDir, euroFilename)
? print('Renaming "%s" to "%s"...' % (amerFilename, euroFilename)) ? #shutil.move(amerFilename, euroFilename) # uncomment after testing 168 Python 编程快速上手——让繁琐工作自动化 将连接的字符串保存在名为euroFilename 的变量中?。然后将amerFilename 中 原来的文件名和新的euroFilename 变量传递给shutil.move()函数,将该文件改名?。 这个程序将shutil.move()调用注释掉,代之以打印出将被改名的文件名?。先 像这样运行程序,你可以确认文件改名是正确的。然后取消shutil.move()调用的注 释,再次运行该程序,确实将这些文件改名。 第4 步:类似程序的想法 有很多其他的理由,导致你需要对大量的文件改名。 ? 为文件名添加前缀,诸如添加spam_,将eggs.txt 改名为spam_eggs.txt。 ? 将欧洲风格日期的文件改名为美国风格日期。 ? 删除文件名中的0,诸如spam0042.txt。 9.5 项目:将一个文件夹备份到一个ZIP 文件 假定你正在做一个项目,它的文件保存在C:\AlsPythonBook 文件夹中。你担心工作 会丢失,所以希望为整个文件夹创建一个ZIP 文件,作为“快照”。你希望保存不同的版 本,希望ZIP 文件的文件名每次创建时都有所变化。例如AlsPythonBook_1.zip、 AlsPythonBook_2.zip、AlsPythonBook_3.zip,等等。你可以手工完成,但这有点烦人, 而且可能不小心弄错ZIP 文件的编号。运行一个程序来完成这个烦人的任务会简单得多。 针对这个项目,打开一个新的文件编辑器窗口,将它保存为backupToZip.py。 第1 步:弄清楚ZIP 文件的名称 这个程序的代码将放在一个名为backupToZip()的函数中。这样就更容易将该函 数复制粘贴到其他需要这个功能的Python 程序中。在这个程序的末尾,会调用这个 函数进行备份。让你的程序看起来像这样: #! python3
? import zipfile, os def backupToZip(folder):
folder = os.path.abspath(folder) # make sure folder is absolute
? number = 1 ? while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): 第9 章 组织文件 169 break number = number + 1 ? # TODO: Create the ZIP file.
print('Done.') b ackupToZip('C:\delicious') 先完成基本任务:添加#!行,描述该程序做什么,并导入zipfile 和os 模块?。 定义backupToZip()函数,它只接收一个参数,即folder。这个参数是一个字符 串路径,指向需要备份的文件夹。该函数将决定它创建的ZIP 文件使用什么文件名, 然后创建该文件,遍历folder 文件夹,将每个子文件夹和文件添加到ZIP 文件中。 在源代码中为这些步骤写下TODO 注释,提醒你稍后来完成?。 第一部分命名这个ZIP 文件,使用folder 的绝对路径的基本名称。如果要备份 的文件夹是C:\delicious,ZIP 文件的名称就应该是delicious_N.zip,第一次运行该程 序时N=1,第二次运行时N=2,以此类推。 通过检查delicious_1.zip 是否存在,然后检查delicious_2.zip 是否存在,继续下 去,可以确定N 应该是什么。用一个名为number 的变量表示N?,在一个循环内 不断增加它,并调用os.path.exists()来检查该文件是否存在?。第一个不存在的文件 名将导致循环break,因此它就发现了新ZIP 文件的文件名。 第2 步:创建新ZIP 文件 接下来让我们创建ZIP 文件。让你的程序看起来像这样: #! python3
--snip-- while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): break number = number + 1
print('Creating %s...' % (zipFilename)) ? backupZip = zipfile.ZipFile(zipFilename, 'w')
print('Done.') b ackupToZip('C:\delicious') 既然新ZIP 文件的文件名保存在zipFilename 变量中,你就可以调用 zipfile.ZipFile(),实际创建这个ZIP 文件?。确保传入'w'作为第二个参数,这样ZIP 170 Python 编程快速上手——让繁琐工作自动化 文件以写模式打开。 第3 步:遍历目录树并添加到ZIP 文件 现在需要使用os.walk()函数,列出文件夹以及子文件夹中的每个文件。让你的 程序看起来像这样: #! python3
--snip--
? for foldername, subfolders, filenames in os.walk(folder): print('Adding files in %s...' % (foldername))
? backupZip.write(foldername)
? for filename in filenames: newBase / os.path.basename(folder) + '_' if filename.startswith(newBase) and filename.endswith('.zip') continue # don't backup the backup ZIP files backupZip.write(os.path.join(foldername, filename)) backupZip.close() print('Done.') b ackupToZip('C:\delicious') 可以在for 循环中使用os.walk()?,在每次迭代中,它将返回这次迭代当前的 文件夹名称、这个文件夹中的子文件夹,以及这个文件夹中的文件名。 在这个for 循环中,该文件夹被添加到ZIP 文件?。嵌套的for 循环将遍历 filenames 列表中的每个文件?。每个文件都被添加到ZIP 文件中,以前生成的备份 ZIP 文件除外。 如果运行该程序,它产生的输出看起来像这样: Creating delicious_1.zip... Adding files in C:\delicious... Adding files in C:\delicious\cats... Adding files in C:\delicious\waffles... Adding files in C:\delicious\walnut... Adding files in C:\delicious\walnut\waffles... Done. 第二次运行它时,它将C:\delicious 中的所有文件放进一个ZIP 文件,命名为 delicious_2.zip,以此类推。 第4 步:类似程序的想法 你可以在其他程序中遍历一个目录树,将文件添加到压缩的ZIP 归档文件中。 例如,你可以编程做下面的事情: ? 遍历一个目录树,将特定扩展名的文件归档,诸如.txt 或.py,并排除其他文件。 第9 章 组织文件 171 ? 遍历一个目录树,将除.txt 和.py 文件以外的其他文件归档。 ? 在一个目录树中查找文件夹,它包含的文件数最多,或者使用的磁盘空间最大。 9.6 小结 即使你是一个有经验的计算机用户,可能也会用鼠标和键盘手工处理文件。现 在的文件浏览器使得处理少量文件的工作很容易。但有时候,如果用计算机的浏览 器,你需要完成的任务可能要花几个小时。 os 和shutil 模块提供了一些函数,用于复制、移动、改名和删除文件。在删除 文件时,你可能希望使用send2trash 模块,将文件移动到回收站或垃圾箱,而不是 永久地删除它们。在编程处理文件时,最好是先注释掉实际会复制/移动/改名/删除 文件的代码,添加print()调用,这样你就可以运行该程序,验证它实际会做什么。 通常,你不仅需要对一个文件夹中的文件执行这些操作,而是对所有下级子文 件夹执行操作。os.walk()函数将处理这个艰苦工作,遍历文件夹,这样你就可以专 注于程序需要对其中的文件做什么。 zipfile 模块提供了一种方法,用Python 压缩和解压ZIP 归档文件。和os 和shutil 模块中的文件处理函数一起使用,很容易将硬盘上任意位置的一些文件打包。和许 多独立的文件相比,这些ZIP 文件更容易上传到网站,或作为E-mail 附件发送。 本书前面几章提供了源代码让你拷贝。但如果你编写自己的程序,可能在第一 次编写时不会完美无缺。下一章将聚焦于一些Python 模块,它们帮助你分析和调试 程序,这样就能让程序很快正确运行。 9.7 习题 1.shutil.copy()和shutil.copytree()之间的区别是什么? 2.什么函数用于文件改名? 3.send2trash 和shutil 模块中的删除函数之间的区别是什么? 4.ZipFile 对象有一个close()方法,就像File 对象的close()方法。ZipFile 对象 的什么方法等价于File 对象的open()方法? 9.8 实践项目 作为实践,编程完成下面的任务。 9.8.1 选择性拷贝 编写一个程序,遍历一个目录树,查找特定扩展名的文件(诸如.pdf 或.jpg)。 172 Python 编程快速上手——让繁琐工作自动化 不论这些文件的位置在哪里,将它们拷贝到一个新的文件夹中。 9.8.2 删除不需要的文件 一些不需要的、巨大的文件或文件夹占据了硬盘的空间,这并不少见。如果你 试图释放计算机上的空间,那么删除不想要的巨大文件效果最好。但首先你必须找 到它们。 编写一个程序,遍历一个目录树,查找特别大的文件或文件夹,比方说,超过 100MB 的文件(回忆一下,要获得文件的大小,可以使用 os 模块的os.path.getsize())。 将这些文件的绝对路径打印到屏幕上。 9.8.3 消除缺失的编号 编写一个程序,在一个文件夹中,找到所有带指定前缀的文件,诸如spam001.txt, spam002.txt 等,并定位缺失的编号(例如存在spam001.txt 和spam003.txt,但不存 在spam002.txt)。让该程序对所有后面的文件改名,消除缺失的编号。 作为附加的挑战,编写另一个程序,在一些连续编号的文件中,空出一些编号, 以便加入新的文件。 第 章 调 试 既然你已学习了足够的内容,可以编写更复杂的程序,可 能就会在程序中发现不那么简单的缺陷。本章介绍了一些工具 和技巧,用于寻找程序中缺陷的根源,帮助你更快更容易地修 复缺陷。 程序员之间流传着一个老笑话:“编码占了编程工作量的 90%,调试占了另外90%。” 计算机只会做你告诉它做的事情,它不会读懂你的心思,做你 想要它做的事情。即使专业的程序员也一直在制造缺陷,所以如果你的程序有问题, 不必感到沮丧。 好在,有一些工具和技巧可以确定你的代码在做什么,以及哪儿出了问题。首 先,你要查看日志和断言。这两项功能可以帮助你尽早发现缺陷。一般来说,缺陷发 现的越早,就越容易修复。 其次,你要学习如何使用调试器。调试器是IDLE 的一项功能,它可以一次执 行一条指令,在代码运行时,让你有机会检查变量的值,并追踪程序运行时值的变 化。这比程序全速运行要慢得多,但可以帮助你查看程序运行时其中实际的值,而 不是通过源代码推测值可能是什么。 10 174 Python 编程快速上手——让繁琐工作自动化 10.1 抛出异常 当Python 试图执行无效代码时,就会抛出异常。在第 3 章中,你已看到如何使 用try 和except 语句来处理Python 的异常,这样程序就可以从你预期的异常中恢复。 但你也可以在代码中抛出自己的异常。抛出异常相当于是说:“停止运行这个函数 中的代码,将程序执行转到except 语句”。 抛出异常使用raise 语句。在代码中,raise 语句包含以下部分: ? raise 关键字; ? 对Exception 函数的调用; ? 传递给Exception 函数的字符串,包含有用的出错信息。 例如,在交互式环境中输入以下代码:
raise Exception('This is the error message.') Traceback (most recent call last): File "<pyshell#191>", line 1, in raise Exception('This is the error message.') Exception: This is the error message. 如果没有try 和except 语句覆盖抛出异常的raise 语句,该程序就会崩溃,并显 示异常的出错信息。 通常是调用该函数的代码知道如何处理异常,而不是该函数本身。所以你常常 会看到raise 语句在一个函数中,try 和except 语句在调用该函数的代码中。例如, 打开一个新的文件编辑器窗口,输入以下代码,并保存为boxPrint.py: def boxPrint(symbol, width, height): if len(symbol) != 1: ? raise Exception('Symbol must be a single character string.') if width <= 2: ? raise Exception('Width must be greater than 2.') if height <= 2: ? raise Exception('Height must be greater than 2.') print(symbol * width) for i in range(height - 2): print(symbol + (' ' * (width - 2)) + symbol) print(symbol * width) for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)): try: boxPrint(sym, w, h) ? except Exception as err: ? print('An exception happened: ' + str(err)) 这里我们定义了一个boxPrint() 函数,它接受一个字符、一个宽度和一个高度。它 按照指定的宽度和高度,用该字符创建了一个小盒子的图像。这个盒子被打印到屏幕上。 假定我们希望该字符是一个字符,宽度和高度要大于2。我们添加了if 语句, 第10 章 调试 175 如果这些条件没有满足,就抛出异常。稍后,当我们用不同的参数调用boxPrint() 时,try/except 语句就会处理无效的参数。 这个程序使用了except 语句的except Exception as err 形式?。如果boxPrint() 返回一个Exception 对象???,这条语句就会将它保存在名为err 的变量中。 Exception 对象可以传递给str(),将它转换为一个字符串,得到用户友好的出错信息?。 运行boxPrint.py,输出看起来像这样:
OOOOOOOOOOOOOOOOOOOO O O O O O O OOOOOOOOOOOOOOOOOOOO An exception happened: Width must be greater than 2. An exception happened: Symbol must be a single character string. 使用try 和except 语句,你可以更优雅地处理错误,而不是让整个程序崩溃。 10.2 取得反向跟踪的字符串 如果Python 遇到错误,它就会生成一些错误信息,称为“反向跟踪”。反向跟踪 包含了出错消息、导致该错误的代码行号,以及导致该错误的函数调用的序列。这 个序列称为“调用栈”。 在IDLE 中打开一个新的文件编辑器窗口,输入以下程序,并保存为error Example.py: def spam(): bacon() def bacon(): raise Exception('This is the error message.') spam() 如果运行errorExample.py,输出看起来像这样: Traceback (most recent call last): File "errorExample.py", line 7, in spam() File "errorExample.py", line 2, in spam bacon() File "errorExample.py", line 5, in bacon raise Exception('This is the error message.') Exception: This is the error message. 通过反向跟踪,可以看到该错误发生在第5 行,在bacon() 函数中。这次特定的 bacon() 调用来自第2 行,在spam() 函数中,它又在第7 行被调用的。在从多个位 置调用函数的程序中,调用栈就能帮助你确定哪次调用导致了错误。 176 Python 编程快速上手——让繁琐工作自动化 只要抛出的异常没有被处理,Python 就会显示反向跟踪。但你也可以调用 traceback.format_exc(),得到它的字符串形式。如果你希望得到异常的反向跟踪的信 息,但也希望except 语句优雅地处理该异常,这个函数就很有用。在调用该函数之 前,需要导入Python 的traceback 模块。 例如,不是让程序在异常发生时就崩溃,可以将反向跟踪信息写入一个日志文 件,并让程序继续运行。稍后,在准备调试程序时,可以检查该日志文件。在交互 式环境中输入以下代码:
import traceback try: raise Exception('This is the error message.') except: errorFile = open('errorInfo.txt', 'w') errorFile.write(traceback.format_exc()) errorFile.close() print('The traceback info was written to errorInfo.txt.') 116 The traceback info was written to errorInfo.txt. write() 方法的返回值是116,因为116 个字符被写入到文件中。反向跟踪文本 被写入errorInfo.txt。 Traceback (most recent call last): File "<pyshell#28>", line 2, in Exception: This is the error message. 10.3 断言 “断言”是一个心智正常的检查,确保代码没有做什么明显错误的事情。这些 心智正常的检查由assert 语句执行。如果检查失败,就会抛出异常。在代码中,assert 语句包含以下部分: ? assert 关键字; ? 条件(即求值为True 或False 的表达式); ? 逗号; ? 当条件为False 时显示的字符串。 例如,在交互式环境中输入以下代码: podBayDoorStatus = 'open' assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".' podBayDoorStatus = 'I'm sorry, Dave. I'm afraid I can't do that.'' assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".' Traceback (most recent call last): File "<pyshell#10>", line 1, in assert podBayDoorStatus == 'open', 'The pod bay doors need to be "open".' AssertionError: The pod bay doors need to be "open". 这里将podBayDoorStatus 设置为 'open',所以从此以后,我们充分期望这个变 第10 章 调试 177 量的值是 'open'。在使用这个变量的程序中,基于这个值是 'open' 的假定,我们可能 写下了大量的代码,即这些代码依赖于它是 'open',才能按照期望工作。所以添加了 一个断言,确保假定podBayDoorStatus 是 'open' 是对的。这里,我们加入了信息 'The pod bay doors need to be "open".',这样如果断言失败,就很容易看到哪里出了错。 稍后,假如我们犯了一个明显的错误,把另外的值赋给podBayDoorStatus,但 在很多行代码中,我们并没有意识到这一点。这个断言会抓住这个错误,清楚地告 诉我们出了什么错。 在日常英语中,assert 语句是说:“我断言这个条件为真,如果不为真,程序中什 么地方就有一个缺陷。”不像异常,代码不应该用try 和except 处理assert 语句。如果 assert 失败,程序就应该崩溃。通过这样的快速失败,产生缺陷和你第一次注意到该缺 陷之间的时间就缩短了。这将减少为了寻找导致该缺陷的代码,而需要检查的代码量。 断言针对的是程序员的错误,而不是用户的错误。对于那些可以恢复的错误(诸如 文件没有找到,或用户输入了无效的数据),请抛出异常,而不是用assert 语句检测它。 10.3.1 在交通灯模拟中使用断言 假定你在编写一个交通信号灯的模拟程序。代表路口信号灯的数据结构是一个 字典,以 'ns' 和 'ew' 为键,分别表示南北向和东西向的信号灯。这些键的值可以 是 'green'、'yellow' 或 'red' 之一。代码看起来可能像这样: market_2nd = {'ns': 'green', 'ew': 'red'} mission_16th = {'ns': 'red', 'ew': 'green'} 这两个变量将针对Market 街和第2 街路口,以及Mission 街和第16 街路口。作 为项目启动,你希望编写一个switchLights() 函数,它接受一个路口字典作为参数, 并切换红绿灯。 开始你可能认为,switchLights() 只要将每一种灯按顺序切换到下一种顔色: 'green' 值应该切换到 'yellow','yellow' 应该切换到 'red','red' 应该切换到'green'。实 现这个思想的代码看起来像这样: def switchLights(stoplight): for key in stoplight.keys(): if stoplight[key] == 'green': stoplight[key] = 'yellow' elif stoplight[key] == 'yellow': stoplight[key] = 'red' elif stoplight[key] == 'red': stoplight[key] = 'green' switchLights(market_2nd) 你可能已经发现了这段代码的问题,但假设你编写了剩下的模拟代码,有几千 行,但没有注意到这个问题。当最后运行时,程序没有崩溃,但虚拟的汽车撞车了! 178 Python 编程快速上手——让繁琐工作自动化 因为你已经编写了剩下的程序,所以不知道缺陷在哪里。也许在模拟汽车的代 码中,或者在模拟司机的代码中。可能需要花几个小时追踪缺陷,才能找到 switchLights() 函数。 但如果在编写switchLights() 时,你添加了断言,确保至少一个交通灯是红色, 可能在函数的底部添加这样的代码: assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) 有了这个断言,程序就会崩溃,并提供这样的出错信息: Traceback (most recent call last): File "carSim.py", line 14, in switchLights(market_2nd) File "carSim.py", line 13, in switchLights assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) ? AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'} 这里重要的一行是AssertionError?。虽然程序崩溃并非如你所愿,但它马上指 出了心智正常检查失败:两个方向都没有红灯,这意味着两个方向的车都可以走。 在程序执行中尽早快速失败,可以省去将来大量的调试工作。 10.3.2 禁用断言 在运行Python 时传入-O 选项,可以禁用断言。如果你已完成了程序的编写和 测试,不希望执行心智正常检测,从而减慢程序的速度,这样就很好(尽管大多数 断言语句所花的时间,不会让你觉察到速度的差异)。断言是针对开发的,不是针 对最终产品。当你将程序交给其他人运行时,它应该没有缺陷,不需要进行心智正 常检查。如何用-O 选项启动也许并不疯狂的程序,详细内容请参考附录B。 10.4 日志 如果你曾经在代码中加入print() 语句,在程序运行时输出某些变量的值,你 就使用了记日志的方式来调试代码。记日志是一种很好的方式,可以理解程序中 发生的事,以及事情发生的顺序。Python 的logging 模块使得你很容易创建自定义 的消息记录。这些日志消息将描述程序执行何时到达日志函数调用,并列出你指 定的任何变量当时的值。另一方面,缺失日志信息表明有一部分代码被跳过,从 未执行。 10.4.1 使用日志模块 要启用logging 模块,在程序运行时将日志信息显示在屏幕上,请将下面的代 码复制到程序顶部(但在Python 的#!行之下): 第10 章 调试 179 import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s
- %(message)s') 你不需要过于担心它的工作原理,但基本上,当 Python 记录一个事件的日志时, 它会创建一个LogRecord 对象,保存关于该事件的信息。logging 模块的函数让你 指定想看到的这个LogRecord 对象的细节,以及希望的细节展示方式。 假如你编写了一个函数,计算一个数的阶乘。在数学上, 4 的阶乘是 1 × 2 × 3 × 4,即24。7 的阶乘是1 × 2 × 3 × 4 × 5 × 6 × 7,即5040。打开一个新的 文件编辑器窗口,输入以下代码。其中有一个缺陷,但你也会输入一些日志信息, 帮助你弄清楚哪里出了问题。将该程序保存为factorialLog.py。 import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s
- %(message)s') logging.debug('Start of program') def factorial(n): logging.debug('Start of factorial(%s%%)' % (n)) total = 1 for i in range(n + 1): total *= i logging.debug('i is ' + str(i) + ', total is ' + str(total)) logging.debug('End of factorial(%s%%)' % (n)) return total print(factorial(5)) logging.debug('End of program') 这里,我们在想打印日志信息时,使用logging.debug() 函数。这个debug() 函数 将调用basicConfig(),打印一行信息。这行信息的格式是我们在 basicConfig()函数 中指定的,并且包括我们传递给 debug() 的消息。print(factorial(5))调用是原来 程序的一部分,所以就算禁用日志信息,结果仍会显示。 这个程序的输出就像这样: 2015-05-23 16:20:12,664 - DEBUG - Start of program 2015-05-23 16:20:12,664 - DEBUG - Start of factorial(5) 2015-05-23 16:20:12,665 - DEBUG - i is 0, total is 0 2015-05-23 16:20:12,668 - DEBUG - i is 1, total is 0 2015-05-23 16:20:12,670 - DEBUG - i is 2, total is 0 2015-05-23 16:20:12,673 - DEBUG - i is 3, total is 0 2015-05-23 16:20:12,675 - DEBUG - i is 4, total is 0 2015-05-23 16:20:12,678 - DEBUG - i is 5, total is 0 2015-05-23 16:20:12,680 - DEBUG - End of factorial(5) 0 2015-05-23 16:20:12,684 - DEBUG - End of program factorial() 函数返回0 作为5 的阶乘,这是不对的。for 循环应该用从1 到5 的数,乘以total 的值。但logging.debug() 显示的日志信息表明,i 变量从0 开始, 而不是1。因为0 乘任何数都是0,所以接下来的迭代中,total 的值都是错的。日 180 Python 编程快速上手——让繁琐工作自动化 志消息提供了可以追踪的痕迹,帮助你弄清楚何时事情开始不对。 将代码行for i in range(n + 1):改为for i in range(1,n + 1):,再次运行程序。 输出看起来像这样: 2015-05-23 17:13:40,650 - DEBUG - Start of program 2015-05-23 17:13:40,651 - DEBUG - Start of factorial(5) 2015-05-23 17:13:40,651 - DEBUG - i is 1, total is 1 2015-05-23 17:13:40,654 - DEBUG - i is 2, total is 2 2015-05-23 17:13:40,656 - DEBUG - i is 3, total is 6 2015-05-23 17:13:40,659 - DEBUG - i is 4, total is 24 2015-05-23 17:13:40,661 - DEBUG - i is 5, total is 120 2015-05-23 17:13:40,661 - DEBUG - End of factorial(5) 120 2015-05-23 17:13:40,666 - DEBUG - End of program factorial(5)调用正确地返回120。日志消息表明循环内发生了什么,这直接 指向了缺陷。 你可以看到,logging.debug() 调用不仅打印出了传递给它的字符串,而且包含 一个时间戳和单词DEBUG。 10.4.2 不要用print()调试 输入import logging 和logging.basicConfig(level=logging.DEBUG, format='% (asctime)s - %(levelname)s - %(message)s')有一点不方便。你可能想使用print() 调用 代替,但不要屈服于这种诱惑!在调试完成后,你需要花很多时间,从代码中清除每 条日志消息的print() 调用。你甚至有可能不小心删除一些print() 调用,而它们不是用 来产生日志消息的。日志消息的好处在于,你可以随心所欲地在程序中想加多少就加 多少,稍后只要加入一次logging.disable(logging.CRITICAL)调用,就可以禁止日 志。不像print(),logging 模块使得显示和隐藏日志信息之间的切换变得很容易。 日志消息是给程序员的,不是给用户的。用户不会因为你便于调试,而想看到 的字典值的内容。请将日志信息用于类似这样的目的。对于用户希望看到的消息, 例如“文件未找到”或者“无效的输入,请输入一个数字”,应该使用print() 调用。 我们不希望禁用日志消息之后,让用户看不到有用的信息。 10.4.3 日志级别 “日志级别”提供了一种方式,按重要性对日志消息进行分类。5 个日志级别如表 10-1 所示,从最不重要到最重要。利用不同的日志函数,消息可以按某个级别记入日志。 表10-1 Python 中的日志级别 级别 日志函数 描述 DEBUG logging.debug() 最低级别。用于小细节。通常只有在诊断问题时,你才会关心这些消息 INFO logging.info() 用于记录程序中一般事件的信息,或确认一切工作正常 第10 章 调试 181 续表 级别 日志函数 描述 WARNING logging.warning() 用于表示可能的问题,它不会阻止程序的工作,但将来可能会 ERROR logging.error() 用于记录错误,它导致程序做某事失败 CRITICAL logging.critical() 最高级别。用于表示致命的错误,它导致或将要导致程序完全停止 工作 日志消息作为一个字符串,传递给这些函数。日志级别是一种建议。归根到底, 还是由你来决定日志消息属于哪一种类型。在交互式环境中输入以下代码:
import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') logging.debug('Some debugging details.') 2015-05-18 19:04:26,901 - DEBUG - Some debugging details. logging.info('The logging module is working.') 2015-05-18 19:04:35,569 - INFO - The logging module is working. logging.warning('An error message is about to be logged.') 2015-05-18 19:04:56,843 - WARNING - An error message is about to be logged. logging.error('An error has occurred.') 2015-05-18 19:05:07,737 - ERROR - An error has occurred. logging.critical('The program is unable to recover!') 2015-05-18 19:05:45,794 - CRITICAL - The program is unable to recover! 日志级别的好处在于,你可以改变想看到的日志消息的优先级。向basicConfig()函数 传入logging.DEBUG 作为level 关键字参数,这将显示所有日志级别的消息(DEBUG 是最低的级别)。但在开发了更多的程序后,你可能只对错误感兴趣。在这种情况 下,可以将basicConfig() 的level 参数设置为logging.ERROR,这将只显示ERROR 和CRITICAL 消息,跳过DEBUG、INFO 和WARNING 消息。 10.4.4 禁用日志 在调试完程序后,你可能不希望所有这些日志消息出现在屏幕上。logging. disable() 函数禁用了这些消息,这样就不必进入到程序中,手工删除所有的日志调 用。只要向logging.disable() 传入一个日志级别,它就会禁止该级别和更低级别的所 有日志消息。所以,如果想要禁用所有日志,只要在程序中添加logging. disable (logging.CRITICAL)。例如,在交互式环境中输入以下代码: import logging logging.basicConfig(level=logging.INFO, format=' %(asctime)s - %(levelname)s - %(message)s') logging.critical('Critical error! Critical error!') 2015-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error! logging.disable(logging.CRITICAL) logging.critical('Critical error! Critical error!') logging.error('Error! Error!') 因为logging.disable() 将禁用它之后的所有消息,你可能希望将它添加到程序中 182 Python 编程快速上手——让繁琐工作自动化 接近import logging 代码行的位置。这样就很容易找到它,根据需要注释掉它,或 取消注释,从而启用或禁用日志消息。 10.4.5 将日志记录到文件 除了将日志消息显示在屏幕上,还可以将它们写入文本文件。logging.basic Config() 函数接受filename 关键字参数,像这样: import logging logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') 日志信息将被保存到myProgramLog.txt 文件中。虽然日志消息很有用,但它们 可能塞满屏幕,让你很难读到程序的输出。将日志信息写入到文件,让屏幕保持干 净,又能保存信息,这样在运行程序后,可以阅读这些信息。可以用任何文件编辑 器打开这个文本文件,诸如Notepad 或TextEdit。 10.5 IDLE 的调试器 “调试器”是IDLE 的一项功能,让你每次执行一行程序。调试器将运行一行代 码,然后等待你告诉它继续。像这样让程序运行“在调试器之下”,你可以随便花 多少时间,检查程序运行时任意一个时刻的变量的值。对于追踪缺陷,这是一个很 有价值的工具。 要启用IDLE 的调试器,就在交互式环境窗口中点击Debug?Debugger。这将打 开调试控制(Debug Control)窗口,如图10-1 所示。 图10-1 调试控制窗口 第10 章 调试 183 当调试控制窗口出现后,勾选全部4 个复选框:Stack、Locals、Source 和Globals。 这样窗口将显示全部的调试信息。调试控制窗口显示时,只要你从文件编辑器运行程 序,调试器就会在第一条指令之前暂停执行,并显示下面的信息: ? 将要执行的代码行; ? 所有局部变量及其值的列表; ? 所有全局变量及其值的列表。 你会注意到,在全局变量列表中,有一些变量你没有定义,诸如__builtins__、 doc、file,等等。它们是Python 在运行程序时,自动设置的变量。这些变 量的含义超出了本书的范围,你可以暂时忽略它们。 程序将保持暂停,直到你按下调试控制窗口的5 个按钮中的一个:Go、Step、 Over、Out 或Quit。 10.5.1 Go 点击Go 按钮将导致程序正常执行至终止,或到达一个“断点”(断点在本章稍 后介绍)。如果你完成了调试,希望程序正常继续,就点击Go 按钮。 10.5.2 Step 点击Step 按钮将导致调试器执行下一行代码,然后再次暂停。如果变量的值发 生了变化,调试控制窗口的全局变量和局部变量列表就会更新。如果下一行代码是一 个函数调用,调试器就会“步入”那个函数,跳到该函数的第一行代码。 10.5.3 Over 点击Over 按扭将执行下一行代码,与Step 按钮类似。但是,如果下一行代码 是函数调用,Over 按钮将“跨过”该函数的代码。该函数的代码将以全速执行,调 试器将在该函数返回后暂停。例如,如果下一行代码是print() 调用,你实际上不关 心内建print() 函数中的代码,只希望传递给它的字符串打印在屏幕上。出于这个原 因,使用Over 按钮比使用Step 按钮更常见。 10.5.4 Out 点击Out 按钮将导致调试器全速执行代码行,直到它从当前函数返回。如果你 用Step 按钮进入了一个函数,现在只想继续执行指令,直到该函数返回,那就点击 Out 按钮,从当前的函数调用“走出来”。 10.5.5 Quit 如果你希望完全停止调试,不必继续执行剩下的程序,就点击Quit 按钮。Quite 按 钮将马上终止该程序。如果你希望再次正常运行你的程序,就再次选择Debug?Debugger, 184 Python 编程快速上手——让繁琐工作自动化 禁用调试器。 10.5.6 调试一个数字相加的程序 打开一个新的文件编辑器窗口,输入以下代码: print('Enter the first number to add:') first = input() print('Enter the second number to add:') second = input() print('Enter the third number to add:') third = input() print('The sum is ' + first + second + third) 将它保存为buggyAddingProgram.py,不启用调试器,第一次运行它。程序的 输出像这样: Enter the first number to add: 5 Enter the second number to add: 3 Enter the third number to add: 42 The sum is 5342 这个程序没有崩溃,但求和显然是错的。让我们启用调试控制窗口,再次运行 它,这次在调试器控制之下。 当你按下F5 或选择Run?Run Module(启用Debug?Debugger,选中调试控制窗 口的所有4 个复选框),程序启动时将暂停在第1 行。调试器总是会暂停在它将要 执行的代码行上。调试控制窗口看起来如图10-2 所示。 图10-2 程序第一次在调试器下运行时的调试控制窗口 第10 章 调试 185 点击一次Over 按钮,执行第一个print() 调用。这里应该使用Over 按钮,而不 是Step,因为你不希望进入到print() 函数的代码中。调试控制窗口将更新到第2 行, 文件编辑器窗口的第2 行将高亮显示,如图10-3 所示。这告诉你程序当前执行到 哪里。 图10-3 点击Over 按钮后的调试控制窗口 再次点击Over 按钮,执行input() 函数调用,当IDLE 等待你在交互式环境窗 口中为input() 调用输入内容时,调试控制窗口中的按钮将被禁用。输入5 并按回车。 调试控制窗口按钮将重新启用。 继续点击Over 按钮,输入3 和42 作为接下来的两个数,直到调试器位于 第7 行,程序中最后的print() 调用。调试控制窗口应该如图10-4 所示。可以看 到,在全局变量的部分,第一个、第二个和第三个变量设置为字符串值,而不 是整型值。当最后一行执行时,这些字符串连接起来,而不是加起来,导致了 这个缺陷。 用调试器单步执行程序很有用,但也可能很慢。你常常希望程序正常运行,直 到它到达特定的代码行。你可以使用断点,让调试器做到这一点。 10.5.7 断点 “断点”可以设置在特定的代码行上,当程序执行到达该行时,它迫使调试器 暂停。在一个新的文件编辑器窗口中,输入以下程序,它模拟投掷1000 次硬币。 186 Python 编程快速上手——让繁琐工作自动化 将它保存为coinFlip.py。 图10-4 在最后一行的调试控制窗口。这些变量被设置为字符串,导致了这个缺陷 import random heads = 0 for i in range(1, 1001): ? if random.randint(0, 1) == 1: heads = heads + 1 if i == 500: ? print('Halfway done!') p rint('Heads came up ' + str(heads) + ' times.') 在半数时间里,random.randint(0,1)调用?将返回0,在另外半数时间将返 回1。这可以用来模拟50/50 的硬币投掷,其中1 代表正面。当不用调试器运行该 程序时,它很快输出下面的内容: Halfway done! Heads came up 490 times. 如果启用调试器运行这个程序,就必须点击几千次Over 按钮,程序才能结束。 如果你对程序执行到一半时heads 的值感兴趣,等1000 次硬币投掷完500 次,可 以在代码行print('Halfway done!')?上设置断点。要设置断点,在文件编辑器中该 行代码上点击右键,并选择Set Breakpoint,如图10-5 所示。 第10 章 调试 187 图10-5 设置断点 你不会在if 语句上设置断点,因为if 语句会在循环的每次迭代中都执行。通过 在if 语句内的代码上设置断点,调试器就会只在执行进入if 语句时才中断。 带有断点的代码行会在文件编辑器中以黄色高亮显示。如果在调试器下运行该程 序,开始它会暂停在第一行,像平时一样。但如果点击Go,程序将全速运行,直 到设置了断点的代码行。然后可以点击Go、Over、Step 或Out,正常继续。 如果希望清除断点,在文件编辑器中该行代码上点击右键,并从菜单中选择 Clear Breakpoint。黄色高亮消失,以后调试器将不会在该行代码上中断。 10.6 小结 断言、异常、日志和调试器,都是在程序中发现和预防缺陷的有用工具。用 Python 语句实现的断言,是实现心智正常检查的好方式。如果必要的条件没有保持 为True,它将尽早给出警告。断言所针对的错误,是程序不应该尝试恢复的,而是 应该快速失败。否则,你应该抛出异常。 异常可以由try 和except 语句捕捉和处理。logging 模块是一种很好的方式,可以 在运行时查看代码的内部,它比使用print() 函数要方便得多,因为它有不同的日志 级别,并能将日志写入文本文件。 调试器让你每次单步执行一行代码。或者,可以用正常速度运行程序,并让 调试器暂停在设置了断点的代码行上。利用调试器,你可以看到程序在运行期间, 任何时候所有变量的值。 这些调试工具和技术将帮助你编写正确工作的程序。不小心在代码中引入缺 陷,这是不可避免的,不论你有多少年的编码经验。 10.7 习题 1.写一条 assert 语句,如果变量 spam 是一个小于 10 的整数,就触发 188 Python 编程快速上手——让繁琐工作自动化 AssertionError。 2.编写一条assert 语句,如果eggs 和bacon 包含的字符串彼此相同,而且不 论大小写如何,就触发AssertionError(也就是说,'hello' 和 'hello' 被认为相同, 'goodbye' 和 'GOODbye' 也被认为相同)。 3.编写一条assert 语句,总是触发AssertionError。 4.为了能调用logging.debug(),程序中必须加入哪两行代码? 5.为了让logging.debug() 将日志消息发送到名为programLog.txt 的文件中,程 序必须加入哪两行代码? 6.5 个日志级别是什么? 7.你可以加入哪一行代码,禁用程序中所有的日志消息? 8.显示同样的消息,为什么使用日志消息比使用print() 要好? 9.调试控制窗口中的Step、Over 和Out 按钮有什么区别? 10.在点击调试控制窗口中的Go 按钮后,调试器何时会停下来? 11.什么是断点? 12.在IDLE 中,如何在一行代码上设置断点? 10.8 实践项目 作为实践,编程完成下面的任务。 调试硬币抛掷 下面程序的意图是一个简单的硬币抛掷猜测游戏。玩家有两次猜测机会(这 是一个简单的游戏)。但是,程序中有一些缺陷。让程序运行几次,找出缺陷,使 该程序能正确运行。 import random guess = '' while guess not in ('heads', 'tails'): print('Guess the coin toss! Enter heads or tails:') guess = input() toss = random.randint(0, 1) # 0 is tails, 1 is heads if toss == guess: print('You got it!') else: print('Nope! Guess again!') guesss = input() if toss == guess: print('You got it!') else: print('Nope. You are really bad at this game.') 第 章 从Web 抓取信息 少数可怕的时候,我没有 Wi-Fi。这时才意识到,我在计 算机上所做的事,有多少实际上是在因特网上做的事。完全出 于习惯,我会发现自己尝试收邮件、阅读朋友的推特,或回答 问题:“在Kurtwood Smith演出1987 年的机械战警之前,曾经 演过主角吗?”1 因为计算机上如此多的工作都与因特网有关,所以如果程序 能上网就太好了。“Web 抓取”是一个术语,即利用程序下载并 处理来自Web 的内容。例如,Google 运行了许多web 抓取程序,对网页进行索引, 实现它的搜索引擎。在本章中,你将学习几个模块,让在Python 中抓取网页变得很 容易。 webbrowser:是Python 自带的,打开浏览器获取指定页面。 requests:从因特网上下载文件和网页。 Beautiful Soup:解析HTML,即网页编写的格式。 selenium:启动并控制一个Web 浏览器。selenium 能够填写表单,并模拟鼠标 在这个浏览器中点击。 1 答案是没有。 11 190 Python 编程快速上手——让繁琐工作自动化 11.1 项目:利用webbrowser 模块的mapIt.py webbrowser 模块的open()函数可以启动一个新浏览器,打开指定的URL。在交 互式环境中输入以下代码: import webbrowser webbrowser.open('http://inventwithpython.com/') Web 浏览器的选项卡将打开URL http://inventwithpython.com/。这大概就是 webbrowser 模块能做的唯一的事情。既使如此,open()函数确实让一些有趣的事情成为可 能。例如,将一条街道的地址拷贝到剪贴板,并在Google 地图上打开它的地图,这是很 繁琐的事。你可以让这个任务减少几步,写一个简单的脚本,利用剪贴板中的内容在浏 览器中自动加载地图。这样,你只要将地址拷贝到剪贴板,运行该脚本,地图就会加载。 你的程序需要做到: ? 从命令行参数或剪贴板中取得街道地址。 ? 打开Web 浏览器,指向该地址的Google 地图页面。 这意味着代码需要做下列事情: ? 从sys.argv 读取命令行参数。 ? 读取剪贴板内容。 ? 调用webbrowser.open()函数打开外部浏览器。 打开一个新的文件编辑器窗口,将它保存为mapIt.py。 第1 步:弄清楚URL 根据附录B 中的指导,建立mapIt.py,这样当你从命令行运行它时,例如 C:> mapit 870 Valencia St, San Francisco, CA 94110 该脚本将使用命令行参数,而不是剪贴板。如果没有命令行参数,程序就知道 要使用剪贴板的内容。 首先你需要弄清楚,对于指定的街道地址,要使用怎样的URL。你在浏览器中打 开http://maps.google.com/并查找一个地址时,地址栏中的URL 看起来就像这样:https:// www.google.com/maps/place/870+Valencia+St/@37.7590311,-122.4215096, 17z/data= !3m1!4b1!4m2!3m1!1s0x808f7e3dadc07a37:0xc86b0b2bb93b73d8. 地址就在URL 中,但其中还有许多附加的文本。网站常常在URL 中添加额外 的数据,帮助追踪访问者或定制网站。但如果你尝试使用https://www.google. com/maps/place/870+Valencia+St+San+Francisco+CA/,会发现仍然可以到达正确的页 面。所以你的程序可以设置为打开一个浏览器,访问'https://www.google.com/ maps/place/your_address_string'(其中your_address_string 是想查看地图的地址)。 第11 章 从Web 抓取信息 191 第2 步:处理命令行参数 让你的代码看起来像这样: #! python3
import webbrowser, sys if len(sys.argv) > 1:
address = ' '.join(sys.argv[1:])
在程序的#!行之后,需要导入webbrowser 模块,用于加载浏览器;导入sys 模 块,用于读入可能的命令行参数。sys.argv 变量保存了程序的文件名和命令行参数 的列表。如果这个列表中不只有文件名,那么len(sys.argv)的返回值就会大于1,这 意味着确实提供了命令行参数。 命令行参数通常用空格分隔,但在这个例子中,你希望将所有参数解释为一个字符串。 因为sys.argv 是字符串的列表,所以你可以将它传递给join()方法,这将返回一个字符串。 你不希望程序的名称出现在这个字符串中,所以不是使用sys.argv,而是使用sys.argv[1:], 砍掉这个数组的第一个元素。这个表达式求值得到的字符串,保存在address 变量中。 如果运行程序时在命令行中输入以下内容: mapit 870 Valencia St, San Francisco, CA 94110 …sys.argv 变量将包含这样的列表值: ['mapIt.py', '870', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110'] address 变量将包含字符串'870 Valencia St, San Francisco, CA 94110'。 第3 步:处理剪贴板内容,加载浏览器 让你的代码看起来像这样: #! python3
import webbrowser, sys, pyperclip if len(sys.argv) > 1:
address = ' '.join(sys.argv[1:]) else:
address = pyperclip.paste() webbrowser.open('https://www.google.com/maps/place/' + address) 如果没有命令行参数,程序将假定地址保存在剪贴板中。可以用pyperclip.paste()取 192 Python 编程快速上手——让繁琐工作自动化 得剪贴板的内容,并将它保存在名为address 的变量中。最后,启动外部浏览器访 问Google 地图的URL,调用webbrowser.open()。 虽然你写的某些程序将完成大型任务,为你节省数小时的时间,但使用一个程序, 在每次执行一个常用任务时节省几秒钟时间,比如取得一个地址的地图,这同样令人 满意。表11-1 比较了有mapIt.py 和没有它时,显示地图所需的步骤。 表11-1 不用和利用mapIt.py 取得地图 手工取得地图 利用mapIt.py 高亮标记地址 高亮标记地址 拷贝地址 拷贝地址 打开Web 浏览器 运行mapIt.py 打开http://maps.google.com/ 点击地址文本字段 拷贝地址 按回车 看到程序让这个任务变得不那么繁琐了吗? 第4 步:类似程序的想法 只要你有一个URL,webbrowser 模块就让用户不必打开浏览器,而直接加载一 个网站。其他程序可以利用这项功能完成以下任务: ? 在独立的浏览器标签中,打开一个页面中的所有链接。 ? 用浏览器打开本地天气的URL。 ? 打开你经常查看的几个社交网站。 11.2 用requests 模块从Web 下载文件 requests 模块让你很容易从Web 下载文件,不必担心一些复杂的问题,诸如网 络错误、连接问题和数据压缩。requests 模块不是Python 自带的,所以必须先安装。 通过命令行,运行pip install requests(附录A 详细介绍了如何安装第三方模块)。 编写requests 模块是因为Python 的urllib2 模块用起来太复杂。实际上,请拿一 支记号笔涂黑这一段。忘记我曾提到urllib2。如果你需要从Web 下载东西,使用 requests 模块就好了。 接下来,做一个简单的测试,确保requests 模块已经正确安装。在交互式环境 中输入以下代码:
<title>The Website Title</title>import requests 如果没有错误信息显示,requests 模块就已经安装成功了。 第11 章 从Web 抓取信息 193 11.2.1 用requests.get()函数下载一个网页 requests.get()函数接受一个要下载的URL 字符串。通过在requests.get()的返回 值上调用type(),你可以看到它返回一个Response 对象,其中包含了Web 服务器对 你的请求做出的响应。稍后我将更详细地解释Response 对象,但现在请在交互式环 境中输入以下代码,并保持计算机与因特网的连接: import requests ? >>> res = requests.get('http://www.gutenberg.org/cache/epub/1112/pg1112.txt') type(res) <class 'requests.models.Response'> ? >>> res.status_code == requests.codes.ok True len(res.text) 178981 print(res.text[:250]) The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare This eBook is for the use of anyone anywhere at no cost and with almost no restrictions whatsoever. You may copy it, give it away or re-use it under the terms of the Proje 该URL 指向一个文本页面,其中包含整部罗密欧与朱丽叶,它是由古登堡计 划?提供的。通过检查Response 对象的status_code 属性,你可以了解对这个网页的 请求是否成功。如果该值等于requests.codes.ok,那么一切都好?(顺便说一下,HTTP 协议中“OK”的状态码是200。你可能已经熟悉404 状态码,它表示“没找到”)。 如果请求成功,下载的页面就作为一个字符串,保存在Response 对象的text 变量中。这个变量保存了包含整部戏剧的一个大字符串,调用len(res.text)表明, 它的长度超过178000 个字符。最后,调用print(res.text[:250])显示前250 个字符。 11.2.2 检查错误 正如你看到的,Response 对象有一个status_code 属性,可以检查它是否等于 requests.codes.ok,了解下载是否成功。检查成功有一种简单的方法,就是在Response 对象上调用raise_for_status()方法。如果下载文件出错,这将抛出异常。如果下载成 功,就什么也不做。在交互式环境中输入以下代码: res = requests.get('http://inventwithpython.com/page_that_does_not_exist') res.raise_for_status() Traceback (most recent call last): File "<pyshell#138>", line 1, in res.raise_for_status() File "C:\Python34\lib\site-packages\requests\models.py", line 773, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 404 Client Error: Not Found raise_for_status()方法是一种很好的方式,确保程序在下载失败时停止。这是一 件好事:你希望程序在发生未预期的错误时,马上停止。如果下载失败对程序来说 194 Python 编程快速上手——让繁琐工作自动化 不够严重,可以用try 和except 语句将raise_for_status()代码行包裹起来,处理这一 错误,不让程序崩溃。 import requests res = requests.get('http://inventwithpython.com/page_that_does_not_exist') try: res.raise_for_status() except Exception as exc: print('There was a problem: %s' % (exc)) 这次raise_for_status()方法调用导致程序输出以下内容: There was a problem: 404 Client Error: Not Found 总是在调用requests.get()之后再调用raise_for_status()。你希望确保下载确实成 功,然后再让程序继续。 11.3 将下载的文件保存到硬盘 现在,可以用标准的open()函数和write()方法,将Web 页面保存到硬盘中的一 个文件。但是,这里稍稍有一点不同。首先,必须用“写二进制”模式打开该文件, 即向函数传入字符串'wb',作为open()的第二参数。即使该页面是纯文本的(例如 前面下载的罗密欧与朱丽叶的文本),你也需要写入二进制数据,而不是文本数据, 目的是为了保存该文本中的“Unicode 编码”。 Unicode 编码 Unicode 编码超出了本书的范围,但你可以通过以下网页了解更多的相关内容: ? Joel on Software: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!): http://www.joelonsoftware.com/articles/Unicode.html ? Pragmatic Unicode: http://nedbatchelder.com/text/unipain.html 为了将Web 页面写入到一个文件,可以使用for 循环和Response 对象的 iter_content()方法。 import requests res = requests.get('http://www.gutenberg.org/cache/epub/1112/pg1112.txt') res.raise_for_status() playFile = open('RomeoAndJuliet.txt', 'wb') for chunk in res.iter_content(100000): playFile.write(chunk) 100000 78981 playFile.close() iter_content()方法在循环的每次迭代中,返回一段内容。每一段都是bytes 数据 第11 章 从Web 抓取信息 195 类型,你需要指定一段包含多少字节。10 万字节通常是不错的选择,所以将100000 作为参数传递给iter_content()。 文件RomeoAndJuliet.txt 将存在于当前工作目录。请注意,虽然在网站上文件 名是pg1112.txt,但在你的硬盘上,该文件的名字不同。requests 模块只处理下载网 页内容。一旦网页下载后,它就只是程序中的数据。即使在下载该网页后断开了因 特网连接,该页面的所有数据仍然会在你的计算机中。 write()方法返回一个数字,表示写入文件的字节数。在前面的例子中,第一段 包含100000 个字节,文件剩下的部分只需要78981 个字节。 回顾一下,下载并保存到文件的完整过程如下: 1.调用requests.get()下载该文件。 2.用'wb'调用open(),以写二进制的方式打开一个新文件。 3.利用Respose 对象的iter_content()方法做循环。 4.在每次迭代中调用write(),将内容写入该文件。 5.调用close()关闭该文件。 这就是关于requests 模块的全部内容!相对于写入文本文件的open()/write()/ close()工作步骤,for 循环和iter_content()的部分可能看起来比较复杂,但这是为了 确保requests 模块即使在下载巨大的文件时也不会消耗太多内存。你可以访问 http://requests.readthedocs.org/,了解requests 模块的其他功能。 11.4 HTML 在你拆解网页之前,需要学习一些HTML 的基本知识。你也会看到如何利用 Web 浏览器的强大开发者工具,它们使得从Web 抓取信息更容易。 11.4.1 学习HTML 的资源 超文本标记语言(HTML)是编写Web 页面的格式。本章假定你对HTML 有 一些基本经验,但如果你需要初学者指南,我推荐以下站点: ? http://htmldog.com/guides/html/beginner/ ? http://www.codecademy.com/tracks/web/ ? https://developer.mozilla.org/en-US/learn/html/ 11.4.2 快速复习 假定你有一段时间没有看过HTML 了,这里是对基本知识的快速复习。HTML 文件是一个纯文本文件,带有.html 文件扩展名。这种文件中的文本被“标签”环绕, 标签是尖括号包围的单词。标签告诉浏览器以怎样的格式显示该页面。一个开始标 签和一个结束标签可以包围某段文本,形成一个“元素”。“文本”(或“内部的 196 Python 编程快速上手——让繁琐工作自动化 HTML”)是在开始标签和结束标签之间的内容。例如,下面的HTML 在浏览器中 显示Hello world!,其中Hello 用粗体显示。 Hello world! 这段HTML 在浏览器中看起来如图11-1 所示。 图11-1 浏览器渲染的Hello world! 开始标签表明,标签包围的文本将使用粗体。结束标签告诉 浏览器,粗体文本到此结束。 HTML 中有许多不同的标签。有一些标签具有额外的特性,在尖括号内以“属 性”的方式展现。例如,标签包含一段文本,它应该是一个链接。这段文本链 接的URL 是由href 属性确定的。下面是一个例子: Al's free Python books. 这段HTML 在浏览器中看起来如图11-2 所示。 图11-2 浏览器中渲染的链接 某些元素具有id 属性,可以用来在页面上唯一地确定该元素。你常常会告诉程 序,根据元素的id 属性来寻找它。所以利用浏览器的开发者工具,弄清楚元素的id 属性,这是编写Web 抓取程序常见的任务。 11.4.3 查看网页的HTML 源代码 对于程序要处理的网页,你需要查看它的HTML 源代码。要做到这一点,在浏 览器的任意网页上点击右键(或在OS X 上Ctrl-点击),选择View Source 或View page source,查看该页的 HTML 文本(参见图 11-3)。这是浏览器实际接收到的文本。 浏览器知道如何通过这个HTML 显示或渲染网页。 第11 章 从Web 抓取信息 197 图11-3 查看网页的源代码 我强烈建议你查看一些自己喜欢的网站的HTML 源代码。在查看源代码时,如果你 不能完全理解,也没有关系。你不需要完全掌握HTML,也能编写简单的Web 抓取程序, 毕竟你不是要编写自己的网站。只需要足够的知识,就能从已有的网站中挑选数据。 11.4.4 打开浏览器的开发者工具 除了查看网页的源代码,你还可以利用浏览器的开发者工具,来检查页面的 HTML。在Windows 版的Chrome 和IE 中,开发者工具已经安装了。可以按下F12, 让它们出现(参见图11-4)。再次按下F12,可以让开发者工具消失。在Chrome 中, 也可以选择View?Developer?Developer Tools,调出开发者工具。在OS X 中按下- Option-I,将打开Chrome 的开发者工具。 对于Firefox,可以在Windows 和Linux 中需要按下Ctrl-Shift-C,或在OS X 中按 下-option-C,调出开发者工具查看器。它的布局几乎与Chrome 的开发者工具一样。 198 Python 编程快速上手——让繁琐工作自动化 图11-4 Chrome 浏览器中的开发者工具窗口 在Safari 中,打开Preferences 窗口,并在Advanced pane 选中Show Develop menu in the menu bar 选项。在它启用后,你可以按下-option-I,调出开发者工具。 在浏览器中启用或安装了开发者工具之后,可以在网页中任何部分点击右键, 在弹出菜单中选择Inspect Element,查看页面中这一部分对应的HTML。如果需要 在Web 抓取程序中解析HTML,这很有帮助。 不要用正则表达式来解析HTML 在一个字符串中定位特定的一段HTML,这似乎很适合使用正则表达式。但 是,我建议你不要这么做。HTML 的格式可以有许多不同的方式,并且仍然被认 为是有效的HTML,但尝试用正则表达式来捕捉所有这些可能的变化,将非常繁 琐,并且容易出错。专门用于解析HTML 的模块,诸如Beautiful Soup,将更不容 易导致缺陷。在http://stackoverflow.com/a/1732454/1893164/,你会看到更充分的 讨论,了解为什么不应该用正则表达式来解析HTML。 11.4.5 使用开发者工具来寻找HTML 元素 程序利用requests 模块下载了一个网页之后,你会得到该页的HTML 内容,作为一 个字符串值。现在你需要弄清楚,这段HTML的哪个部分对应于网页上你感兴趣的信息。 这就是可以利用浏览器的开发者工具的地方。假定你需要编写一个程序,从 http://weather.gov/获取天气预报数据。在写代码之前,先做一点调查。如果你访问该网 站,并查找邮政编码94105,该网站将打开一个页面,显示该地区的天气预报。 第11 章 从Web 抓取信息 199 如果你想抓取那个邮政编码对应的气温信息,怎么办?右键点击它在页面的位 置(或在OS X 上用Control-点击),在弹出的菜单中选择Inspect Element。这将打 开开发者工具窗口,其中显示产生这部分网页的HTML。图11-5 展示了开发者工具 打开显示气温的HTML。 图11-5 用开发者工具查看包含温度文本的元素 通过开发者工具,可以看到网页中负责气温部分的 HTML 是
57°F
。这正是你要找的东西!看起来气温信息包含在一个元素 中,带有myforecast-current-lrg 类。既然你知道了要找的是什么,BeautifulSoup 模 块就可以帮助你在这个字符串中找到它。 11.5 用BeautifulSoup 模块解析HTML Beautiful Soup 是一个模块,用于从HTML 页面中提取信息(用于这个目的时, 它比正则表达式好很多)。BeautifulSoup 模块的名称是bs4(表示Beautiful Soup, 第4 版)。要安装它,需要在命令行中运行pip install beautifulsoup4(关于安装第三 方模块的指导,请查看附录 A)。虽然安装时使用的名字是beautifulsoup4,但要导 入它,就使用import bs4。 在本章中,Beautiful Soup 的例子将解析(即分析并确定其中的一些部分)硬盘 上的一个HTML 文件。在IDLE 中打开一个新的文件编辑器窗口,输入以下代码,并 200 Python 编程快速上手——让繁琐工作自动化 保存为example.html。或者,从http://nostarch.com/automatestuff/下载它。
Download my Python book from my website.
Learn Python the easy way!
By Al Sweigart
你可以看到,既使一个简单的HTML 文件,也包含许多不同的标签和属性。对 于复杂的网站,事情很快就变得令人困惑。好在,Beautiful Soup 让处理HTML 变 得容易很多。 11.5.1 从HTML 创建一个BeautifulSoup 对象 bs4.BeautifulSoup()函数调用时需要一个字符串,其中包含将要解析的HTML。 bs4.BeautifulSoup()函数返回一个BeautifulSoup 对象。在交互式环境中输入以下代 码,同时保持计算机与因特网的连接: >>> import requests, bs4 >>> res = requests.get('http://nostarch.com') >>> res.raise_for_status() >>> noStarchSoup = bs4.BeautifulSoup(res.text) >>> type(noStarchSoup) 这段代码利用requests.get()函数从No Starch Press 网站下载主页,然后将响应 结果的text 属性传递给bs4.BeautifulSoup()。它返回的BeautifulSoup 对象保存在变 量noStarchSoup 中。 也可以向bs4.BeautifulSoup()传递一个File 对象,从硬盘加载一个HTML 文件。 在交互式环境中输入以下代码(确保example.html 文件在工作目录中): >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile) >>> type(exampleSoup) 有了BeautifulSoup 对象之后,就可以利用它的方法,定位HTML文档中的特定部分。 11.5.2 用select()方法寻找元素 针对你要寻找的元素,调用method()方法,传入一个字符串作为CSS“选择器”, 这样就可以取得Web 页面元素。选择器就像正则表达式:它们指定了要寻找的模 式,在这个例子中,是在HTML 页面中寻找,而不是普通的文本字符串。 完整地讨论 CSS 选择器的语法超出了本书的范围(在 http://nostarch.com/ automatestuff/的资源中,有很好的选择器指南),但这里有一份选择器的简单介绍。 第11 章 从Web 抓取信息 201 表11-2 举例展示了大多数常用CSS 选择器的模式。 表11-2 CSS 选择器的例子 传递给select()方法的选择器 将匹配… soup.select('div') 所有名为元素之内。 select()方法将返回一个Tag 对象的列表,这是Beautiful Soup 表示一个HTML 元素的方式。针对BeautifulSoup 对象中的HTML 的每次匹配,列表中都有一个Tag 对象。Tag 值可以传递给str()函数,显示它们代表的HTML 标签。Tag 值也可以有 attrs 属性,它将该Tag 的所有HTML 属性作为一个字典。利用前面的example.html 文件,在交互式环境中输入以下代码: >>> import bs4 >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile.read()) >>> elems = exampleSoup.select('#author') >>> type(elems) >>> len(elems) 1 >>> type(elems[0]) >>> elems[0].getText() 'Al Sweigart' >>> str(elems[0]) 'Al Sweigart' >>> elems[0].attrs {'id': 'author'} 这段代码将带有 id="author"的元素,从示例 HTML 中找出来。我们使用 select('#author')返回一个列表,其中包含所有带有id="author"的元素。我们将这个 Tag 对象的列表保存在变量中elems,len(elems)告诉我们列表中只有一个Tag 对象, 只有一次匹配。在该元素上调用getText()方法,返回该元素的文本,或内部的HTML。 一个元素的文本是在开始和结束标签之间的内容:在这个例子中,就是'Al Sweigart'。 将该元素传递给str(),这将返回一个字符串,其中包含开始和结束标签,以及该元 素的文本。最后,attrs 给了我们一个字典,包含该元素的属性'id',以及id 属性的值'author'。 也可以从BeautifulSoup 对象中找出
元素。在交互式环境中输入以下代码: 202 Python 编程快速上手——让繁琐工作自动化 >>> pElems = exampleSoup.select('p') >>> str(pElems[0]) '
Download my Python book from my website.
' >>> pElems[0].getText() 'Download my Python book from my website.' >>> str(pElems[1]) 'Learn Python the easy way!
' >>> pElems[1].getText() 'Learn Python the easy way!' >>> str(pElems[2]) 'By Al Sweigart
' >>> pElems[2].getText() 'By Al Sweigart' 这一次,select()给我们一个列表,包含3 次匹配,我们将它保存在pElems 中。 在pElems[0]、pElems[1]和pElems[2]上使用str(),将每个元素显示为一个字符串, 并在每个元素上使用getText(),显示它的文本。 11.5.3 通过元素的属性获取数据 Tag 对象的get()方法让我们很容易从元素中获取属性值。向该方法传入一个属性名 称的字符串,它将返回该属性的值。利用example.html,在交互式环境中输入以下代码: >>> import bs4 >>> soup = bs4.BeautifulSoup(open('example.html')) >>> spanElem = soup.select('span')[0] >>> str(spanElem) 'Al Sweigart' >>> spanElem.get('id') 'author' >>> spanElem.get('some_nonexistent_addr') == None True >>> spanElem.attrs {'id': 'author'} 这里,我们使用select()来寻找所有元素,然后将第一个匹配的元素保存 在spanElem 中。将属性名'id'传递给get(),返回该属性的值'author'。 11.6 项目:“I’m Feeling Lucky”Google 查找 每次我在 Google 上搜索一个主题时,都不会一次只看一个搜索结果。通过鼠 标中键点击搜索结果链接,或在点击时按住CTRL 键,我会在一些新的选项卡中打 开前几个链接,稍后再来查看。我经常搜索Google,所以这个工作流程(开浏览器, 查找一个主题,依次用中键点击几个链接)变得很乏味。如果我只要在命令行中输 入查找主题,就能让计算机自动打开浏览器,并在新的选项卡中显示前面几项查询 结果,那就太好了。让我们写一个脚本来完成这件事。 下面是程序要做的事: 第11 章 从Web 抓取信息 203 ? 从命令行参数中获取查询关键字。 ? 取得查询结果页面。 ? 为每个结果打开一个浏览器选项卡。 这意味着代码需要完成以下工作: ? 从sys.argv 中读取命令行参数。 ? 用requests 模块取得查询结果页面。 ? 找到每个查询结果的链接。 ? 调用webbrowser.open()函数打开Web 浏览器。 打开一个新的文件编辑器窗口,并保存为lucky.py。 第1步:获取命令行参数,并请求查找页面 开始编码之前,你首先要知道查找结果页面的URL。在进行Google 查找后,你 看浏览器地址栏,就会发现结果页面的URL 类似于https://www.google.com/ search?q=SEARCH_TERM_HERE。requests 模块可以下载这个页面,然后可以用 Beautiful Soup,找到HTML 中的查询结果的链接。最后,用webbrowser 模块,在浏 览器选项卡中打开这些链接。 让你的代码看起来像这样: #! python3 # lucky.py - Opens several Google search results. import requests, sys, webbrowser, bs4 print('Googling...') # display text while downloading the Google page res = requests.get('http://google.com/search?q=' + ' '.join(sys.argv[1:])) res.raise_for_status() # TODO: Retrieve top search result links. # TODO: Open a browser tab for each result. 用户运行该程序时,将通过命令行参数指定查找的主题。这些参数将作为字符 串,保存在sys.argv 列表中。 第2步:找到所有的结果 现在你需要使用Beautiful Soup,从下载的HTML 中,提取排名靠前的查找结果链 接。但如何知道完成这项工作需要怎样的选择器?例如,你不能只查找所有的标 签,因为在这个HTML 中,有许多链接你是不关心的。因此,必须用浏览器的开发者 工具来检查这个查找结果页面,尝试寻找一个选择器,它将挑选出你想要的链接。 在针对Beautiful Soup 进行Google 查询后,你可以打开浏览器的开发者工具, 查看该页面上的一些链接元素。它们看起来复杂得难以置信,大概像这样: BeautifulSoup: We called him Tortoise because he taught us. 该元素看起来复杂得难以置信,但这没有关系。只需要找到查询结果链接都具有的 模式。但这个元素没有什么特殊,难以和该页面上非查询结果的元素区分开来。 确保你的代码看起来像这样: #! python3 # lucky.py - Opens several google search results. import requests, sys, webbrowser, bs4 --snip-- # Retrieve top search result links. soup = bs4.BeautifulSoup(res.text) # Open a browser tab for each result. linkElems = soup.select('.r a') 但是,如果从元素向上看一点,就会发现这样一个元素: