├── res ├── qrcode.JPG ├── qq_groups.JPG ├── pay_qr_code.png └── 20211121225327.png ├── README.md ├── 第44课.SQL详解之DCL.md ├── 第23课:用Python读写CSV文件.md ├── 第01课:初识Python.md ├── 第14课:函数的应用.md ├── 第06课:循环结构.md ├── 第03课:Python语言元素之变量.md ├── 第05课:分支结构.md ├── 第27课:用Python操作PDF文件.md ├── 第07课:分支和循环结构的应用.md ├── 第02课:第一个Python程序.md ├── 第09课:常用数据结构之元组.md ├── 第45课.索引.md ├── 第04课:Python语言元素之运算符.md ├── 第47课.MySQL新特性.md ├── 第37课:并发编程在爬虫中的应用.md ├── 第25课:用Python读写Excel文件-2.md ├── 第28课:用Python处理图像.md ├── 第42课.SQL详解之DML.md ├── 第33课:用Python解析HTML页面.md ├── 第11课:常用数据结构之集合.md ├── 第32课:用Python获取网络资源.md ├── 第24课:用Python读写Excel文件-1.md ├── 第31课:网络数据采集概述.md ├── 第15课:函数使用进阶.md ├── 第12课:常用数据结构之字典.md ├── .gitignore ├── 第18课:面向对象编程进阶.md ├── 第36课:Python中的并发编程-3.md ├── 第16课:函数的高级应用.md ├── 第29课:用Python发送邮件和短信.md ├── 第26课:用Python操作Word文件和PowerPoint.md ├── 第19课:面向对象编程应用.md ├── 第35课:Python中的并发编程-2.md ├── 第48课.Python程序接入MySQL数据库.md ├── 第13课:函数和模块.md ├── 第39课:爬虫框架Scrapy简介.md └── 第17课:面向对象编程入门.md /res/qrcode.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackfrued/Python-Core-50-Courses/HEAD/res/qrcode.JPG -------------------------------------------------------------------------------- /res/qq_groups.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackfrued/Python-Core-50-Courses/HEAD/res/qq_groups.JPG -------------------------------------------------------------------------------- /res/pay_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackfrued/Python-Core-50-Courses/HEAD/res/pay_qr_code.png -------------------------------------------------------------------------------- /res/20211121225327.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackfrued/Python-Core-50-Courses/HEAD/res/20211121225327.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python语言基础50课 2 | 3 | 由于之前发布的 Python 学习项目 [Python-100-Days](https://github.com/jackfrued/Python-100-Days) 对初学者来说上手还是有一定难度,所以花了点之间把原来项目中 Python 语言基础部分单独剥离出来,做成了现在这个名为“Python语言基础50课”的项目。现在这个项目用更为简单通俗的方式重写了原来“Python100天”项目中第1天到第15天的部分,**有删减也有补充**,力求**对初学者更加友好**,也欢迎大家关注这个持续更新中的项目。国内用户如果访问 GitHub 比较慢的话,也可以关注我的知乎号 [Python-Jack](https://www.zhihu.com/people/jackfrued) 上的[“从零开始学Python”]()专栏,两边同步更新。有需要的小伙伴可以关注我在知乎的专栏、文章和回答,当然,也欢迎大家评论、收藏和点赞。如果需要**视频教程**,可以到“B站”上搜索[《Python零基础快速上手》](https://www.bilibili.com/video/BV1FT4y1R7sz)。 4 | 5 | 最近,国内访问 GitHub 会因为 DNS(域名解析服务)的问题出现**图片无法显示**的情况,如果你也遇到了这样的问题,可以通过**修改本机的 hosts 文件**直接对 GitHub 的资源链接进行域名解析来加以解决。使用 macOS 系统的读者可以参考[《macOS 下三种修改 hosts 文件的方法》]()一文来修改 hosts 文件;使用 Windows 系统的读者可以参考[《在 Windows 上如何管理 hosts 文件》]()一文来进行操作。我们可以把下面的内容添加到 hosts 文件的末尾,这样就可以解决 GitHub 上图片无法显示的问题。 6 | 7 | ```INI 8 | 151.101.184.133 assets-cdn.github.com 9 | 151.101.184.133 raw.githubusercontent.com 10 | 151.101.184.133 gist.githubusercontent.com 11 | 151.101.184.133 cloud.githubusercontent.com 12 | 151.101.184.133 camo.githubusercontent.com 13 | ``` 14 | 15 | ### 视频资源 16 | 17 | 视频在抖音和B站都可以找到,有兴趣的小伙伴可以关注我的抖音或B站账号,刚刚起号,还希望大家多多支持,非常感谢! 18 | 19 | > **说明**:抖音对学习类的视频并不友好,我自己也不懂抖音的账号运营,目前基本不做抖音更新了,大家想看我的视频还是关注B站账号(下图左边的二维码),感谢大家的点赞关注,有什么想看的内容可以给我留言。 20 | 21 | 22 | 23 | ### 文件资源 24 | 25 | 教程和视频中用到的文件、代码等内容,请统一访问百度网盘获取。 26 | 27 | 链接:,提取码:swg1。 28 | 29 | ### 付费学习 30 | 31 | 之前创建的免费学习交流群(QQ群)都已经满员了,有学习意向的小伙伴可以加入付费交流群,新用户可以通过下方二维码付费之后添加我的私人微信(微信号:**jackfrued**),然后邀请大家进入付费学习打卡群,添加微信时请备注好自己的称呼和需求,我会为大家提供力所能及的帮助。 32 | 33 | 34 | -------------------------------------------------------------------------------- /第44课.SQL详解之DCL.md: -------------------------------------------------------------------------------- 1 | ## 第44课:SQL详解之DCL 2 | 3 | 数据库服务器通常包含了非常重要的数据,可以通过访问控制来确保这些数据的安全,而 DCL 就是解决这一问题的,它可以为指定的用户授予访问权限或者从指定用户处召回指定的权限。DCL 对数据库管理员来说非常重要,因为用户权限的管理关系到数据库的安全。简单的说,我们可以通过 DCL 允许受信任的用户访问数据库,阻止不受信任的用户访问数据库,同时还可以通过 DCL 将每个访问者的的权限最小化(让访问者的权限刚刚够用)。 4 | 5 | ### 创建用户 6 | 7 | 我们可以使用下面的 SQL 来创建一个用户并为其指定访问口令。 8 | 9 | ```SQL 10 | create user 'wangdachui'@'%' identified by 'Wang.618'; 11 | ``` 12 | 13 | 上面的 SQL 创建了名为 wangdachui 的用户,它的访问口令是 Wang.618,该用户可以从任意主机访问数据库服务器,因为 @ 后面使用了可以表示任意多个字符的通配符 %。如果要限制 wangdachui 这个用户只能从 192.168.0.x 这个网段的主机访问数据库服务器,可以按照下面的方式来修改 SQL 语句。 14 | 15 | ```SQL 16 | drop user if exists 'wangdachui'@'%'; 17 | 18 | create user 'wangdachui'@'192.168.0.%' identified by 'Wang.618'; 19 | ``` 20 | 21 | 此时,如果我们使用 wangdachui 这个账号访问数据库服务器,我们几乎不能做任何操作,因为该账号没有任何操作权限。 22 | 23 | ### 授予权限 24 | 25 | 我们用下面的语句为 wangdachui 授予查询 school 数据库学院表(`tb_college`)的权限。 26 | 27 | ```SQL 28 | grant select on `school`.`tb_college` to 'wangdachui'@'192.168.0.%'; 29 | ``` 30 | 31 | 我们也可以让 wangdachui 对 school 数据库的所有对象都具有查询权限,代码如下所示。 32 | 33 | ```SQL 34 | grant select on `school`.* to 'wangdachui'@'192.168.0.%'; 35 | ``` 36 | 37 | 如果我们希望 wangdachui 还有 insert、delete 和 update 权限,可以使用下面的方式进行操作。 38 | 39 | ```SQL 40 | grant insert, delete, update on `school`.* to 'wangdachui'@'192.168.0.%'; 41 | ``` 42 | 43 | 如果我们还想授予 wangdachui 执行 DDL 的权限,可以使用如下所示的 SQL。 44 | 45 | ```SQL 46 | grant create, drop, alter on `school`.* to 'wangdachui'@'192.168.0.%'; 47 | ``` 48 | 49 | 如果我们希望 wangdachui 账号对所有数据库的所有对象都具备所有的操作权限,可以执行如下所示的操作,但是一般情况下,我们不会这样做,因为我们之前说过,权限刚刚够用就行,一个普通的账号不应该拥有这么大的权限。 50 | 51 | ```SQL 52 | grant all privileges on *.* to 'wangdachui'@'192.168.0.%'; 53 | ``` 54 | 55 | ### 召回权限 56 | 57 | 如果要召回 wangdachui 对 school 数据库的 insert、delete 和 update 权限,可以使用下面的操作。 58 | 59 | ```SQL 60 | revoke insert, delete, update on `school`.* from 'wangdachui'@'192.168.0.%'; 61 | ``` 62 | 63 | 如果要召回所有的权限,可以按照如下所示的方式进行操作。 64 | 65 | ```SQL 66 | revoke all privileges on *.* from 'wangdachui'@'192.168.0.%'; 67 | ``` 68 | 69 | 需要说明的是,由于数据库可能会缓存用户的权限,可以在授予或召回权限后执行下面的语句使新的权限即时生效。 70 | 71 | ```SQL 72 | flush privileges; 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /第23课:用Python读写CSV文件.md: -------------------------------------------------------------------------------- 1 | ## 第23课:用Python读写CSV文件 2 | 3 | ### CSV文件介绍 4 | 5 | CSV(Comma Separated Values)全称逗号分隔值文件是一种简单、通用的文件格式,被广泛的应用于应用程序(数据库、电子表格等)数据的导入和导出以及异构系统之间的数据交换。因为CSV是纯文本文件,不管是什么操作系统和编程语言都是可以处理纯文本的,而且很多编程语言中都提供了对读写CSV文件的支持,因此CSV格式在数据处理和数据科学中被广泛应用。 6 | 7 | CSV文件有以下特点: 8 | 9 | 1. 纯文本,使用某种字符集(如[ASCII](https://zh.wikipedia.org/wiki/ASCII)、[Unicode](https://zh.wikipedia.org/wiki/Unicode)、[GB2312](https://zh.wikipedia.org/wiki/GB2312))等); 10 | 2. 由一条条的记录组成(典型的是每行一条记录); 11 | 3. 每条记录被分隔符(如逗号、分号、制表符等)分隔为字段(列); 12 | 4. 每条记录都有同样的字段序列。 13 | 14 | CSV文件可以使用文本编辑器或类似于Excel电子表格这类工具打开和编辑,当使用Excel这类电子表格打开CSV文件时,你甚至感觉不到CSV和Excel文件的区别。很多数据库系统都支持将数据导出到CSV文件中,当然也支持从CSV文件中读入数据保存到数据库中,这些内容并不是现在要讨论的重点。 15 | 16 | ### 将数据写入CSV文件 17 | 18 | 现有五个学生三门课程的考试成绩需要保存到一个CSV文件中,要达成这个目标,可以使用Python标准库中的`csv`模块,该模块的`writer`函数会返回一个`csvwriter`对象,通过该对象的`writerow`或`writerows`方法就可以将数据写入到CSV文件中,具体的代码如下所示。 19 | 20 | ```Python 21 | import csv 22 | import random 23 | 24 | with open('scores.csv', 'w') as file: 25 | writer = csv.writer(file) 26 | writer.writerow(['姓名', '语文', '数学', '英语']) 27 | names = ['关羽', '张飞', '赵云', '马超', '黄忠'] 28 | for name in names: 29 | scores = [random.randrange(50, 101) for _ in range(3)] 30 | scores.insert(0, name) 31 | writer.writerow(scores) 32 | ``` 33 | 34 | 生成的CSV文件的内容。 35 | 36 | ``` 37 | 姓名,语文,数学,英语 38 | 关羽,98,86,61 39 | 张飞,86,58,80 40 | 赵云,95,73,70 41 | 马超,83,97,55 42 | 黄忠,61,54,87 43 | ``` 44 | 45 | 需要说明的是上面的`writer`函数,除了传入要写入数据的文件对象外,还可以`dialect`参数,它表示CSV文件的方言,默认值是`excel`。除此之外,还可以通过`delimiter`、`quotechar`、`quoting`参数来指定分隔符(默认是逗号)、包围值的字符(默认是双引号)以及包围的方式。其中,包围值的字符主要用于当字段中有特殊符号时,通过添加包围值的字符可以避免二义性。大家可以尝试将上面第5行代码修改为下面的代码,然后查看生成的CSV文件。 46 | 47 | ```Python 48 | writer = csv.writer(file, delimiter='|', quoting=csv.QUOTE_ALL) 49 | ``` 50 | 51 | 生成的CSV文件的内容。 52 | 53 | ``` 54 | "姓名"|"语文"|"数学"|"英语" 55 | "关羽"|"88"|"64"|"65" 56 | "张飞"|"76"|"93"|"79" 57 | "赵云"|"78"|"55"|"76" 58 | "马超"|"72"|"77"|"68" 59 | "黄忠"|"70"|"72"|"51" 60 | ``` 61 | 62 | ### 从CSV文件读取数据 63 | 64 | 如果要读取刚才创建的CSV文件,可以使用下面的代码,通过`csv`模块的`reader`函数可以创建出`csvreader`对象,该对象是一个迭代器,可以通过`next`函数或`for-in`循环读取到文件中的数据。 65 | 66 | ```Python 67 | import csv 68 | 69 | with open('scores.csv', 'r') as file: 70 | reader = csv.reader(file, delimiter='|') 71 | for data_list in reader: 72 | print(reader.line_num, end='\t') 73 | for elem in data_list: 74 | print(elem, end='\t') 75 | print() 76 | ``` 77 | 78 | > **注意**:上面的代码对`csvreader`对象做`for`循环时,每次会取出一个列表对象,该列表对象包含了一行中所有的字段。 79 | 80 | ### 简单的总结 81 | 82 | 将来如果大家使用Python做数据分析,很有可能会用到名为`pandas`的三方库,它是Python数据分析的神器之一。`pandas`中封装了名为`read_csv`和`to_csv`的函数用来读写CSV文件,其中`read_CSV`会将读取到的数据变成一个`DataFrame`对象,而`DataFrame`就是`pandas`库中最重要的类型,它封装了一系列用于数据处理的方法(清洗、转换、聚合等);而`to_csv`会将`DataFrame`对象中的数据写入CSV文件,完成数据的持久化。`read_csv`函数和`to_csv`函数远远比原生的`csvreader`和`csvwriter`强大。 83 | -------------------------------------------------------------------------------- /第01课:初识Python.md: -------------------------------------------------------------------------------- 1 | ## 第01课:初识Python 2 | 3 | ### Python简介 4 | 5 | Python是由荷兰人吉多·范罗苏姆(Guido von Rossum)发明的一种编程语言,是目前世界上最受欢迎和拥有最多用户群体的编程语言。 6 | 7 | 8 | 9 | #### Python的历史 10 | 11 | 1. 1989年圣诞节:Guido开始写Python语言的编译器。 12 | 2. 1991年2月:第一个Python解释器诞生,它是用C语言实现的,可以调用C语言的库函数。 13 | 3. 1994年1月:Python 1.0正式发布。 14 | 4. 2000年10月:Python 2.0发布,Python的整个开发过程更加透明,生态圈开始慢慢形成。 15 | 5. 2008年12月:Python 3.0发布,引入了诸多现代编程语言的新特性,但并不完全兼容之前的Python代码。 16 | 6. 2020年1月:在Python 2和Python 3共存了11年之后,官方停止了对Python 2的更新和维护,希望用户尽快过渡到Python 3。 17 | 18 | > **说明**:大多数软件的版本号一般分为三段,形如A.B.C,其中A表示大版本号,当软件整体重写升级或出现不向后兼容的改变时,才会增加A;B表示功能更新,出现新功能时增加B;C表示小的改动(例如:修复了某个Bug),只要有修改就增加C。 19 | 20 | #### Python的优缺点 21 | 22 | Python的优点很多,简单为大家列出几点。 23 | 24 | 1. 简单明确,跟其他很多语言相比,Python更容易上手。 25 | 2. 能用更少的代码做更多的事情,提升开发效率。 26 | 3. 开放源代码,拥有强大的社区和生态圈。 27 | 4. 能够做的事情非常多,有极强的适应性。 28 | 5. 能够在Windows、macOS、Linux等各种系统上运行。 29 | 30 | Python最主要的缺点是执行效率低,但是当我们更看重产品的开发效率而不是执行效率的时候,Python就是很好的选择。 31 | 32 | #### Python的应用领域 33 | 34 | 目前Python在Web服务器应用开发、云基础设施开发、**网络数据采集**(爬虫)、**数据分析**、量化交易、**机器学习**、**深度学习**、自动化测试、自动化运维等领域都有用武之地。 35 | 36 | ### 安装Python环境 37 | 38 | 想要开始你的Python编程之旅,首先得在计算机上安装Python环境,简单的说就是得安装运行Python程序的工具,通常也称之为Python解释器。我们强烈建议大家安装Python 3的环境,很明显它是目前更好的选择。 39 | 40 | #### Windows环境 41 | 42 | 可以在[Python官方网站](https://www.python.org/downloads/)找到下载链接并下载Python 3的安装程序。 43 | 44 | ![](https://github.com/jackfrued/mypic/raw/master/20210719222940.png) 45 | 46 | 对于Windows操作系统,可以下载“executable installer”。需要注意的是,如果在Windows 7环境下安装Python 3,需要先安装Service Pack 1补丁包,大家可以在Windows的“运行”中输入`winver`命令,从弹出的窗口上可以看到你的系统是否安装了该补丁包。如果没有该补丁包,一定要先通过“Windows Update”或者类似“CCleaner”这样的工具自动安装该补丁包,安装完成后通常需要重启你的Windows系统,然后再开始安装Python环境。 47 | 48 | ![](https://github.com/jackfrued/mypic/raw/master/20210719222956.png) 49 | 50 | 双击运行刚才下载的安装程序,会打开Python环境的安装向导。在执行安装向导的时候,记得勾选“Add Python 3.x to PATH”选项,这个选项会帮助我们将Python的解释器添加到PATH环境变量中(不理解没关系,照做就行),具体的步骤如下图所示。 51 | 52 | ![](https://github.com/jackfrued/mypic/raw/master/20210719223007.png) 53 | 54 | ![](https://github.com/jackfrued/mypic/raw/master/20210719223021.png) 55 | 56 | ![](https://github.com/jackfrued/mypic/raw/master/20210719223317.png) 57 | 58 | ![](https://github.com/jackfrued/mypic/raw/master/20210719223332.png) 59 | 60 | 安装完成后可以打开Windows的“命令行提示符”工具(或“PowerShell”)并输入`python --version`或`python -V`来检查安装是否成功,命令行提示符可以在“运行”中输入`cmd`来打开或者在“开始菜单”的附件中找到它。如果看了Python解释器对应的版本号(如:Python 3.7.8),说明你的安装已经成功了,如下图所示。 61 | 62 | ![](https://github.com/jackfrued/mypic/raw/master/20210719223350.png) 63 | 64 | > **说明**:如果安装过程显示安装失败或执行上面的命令报错,很有可能是因为你的Windows系统缺失了一些动态链接库文件或C构建工具导致的问题。可以在[微软官网](https://www.microsoft.com/zh-cn/download/details.aspx?id=48145)下载Visual C++ Redistributable for Visual Studio 2015文件进行修复,64位的系统需要下载有x64标记的安装文件。也可以通过下面的百度云盘地址获取修复工具,运行修复工具,按照如下图所示的方式进行修复,链接: https://pan.baidu.com/s/1iNDnU5UVdDX5sKFqsiDg5Q 提取码: cjs3。 65 | > 66 | > ![QQ20210711-0](https://github.com/jackfrued/mypic/raw/master/20210816234614.png) 67 | 68 | 除此之外,你还应该检查一下Python的包管理工具是否已经可用,对应的命令是`pip --version`。 69 | 70 | #### macOS环境 71 | 72 | macOS自带了Python 2,但是我们需要安装和使用的是Python 3。可以通过Python官方网站提供的[下载链接]()找到适合macOS的“macOS installer”来安装Python 3,安装过程基本不需要做任何勾选,直接点击“下一步”即可。安装完成后,可以在macOS的“终端”工具中输入`python3`命令来调用Python 3解释器,因为如果直接输入`python`,将会调用Python 2的解释器。 73 | 74 | ### 总结 75 | 76 | Python语言可以做很多的事情,也值得我们去学习。要使用Python语言,首先需要在自己的计算机上安装Python环境,也就是运行Python程序的Python解释器。 77 | -------------------------------------------------------------------------------- /第14课:函数的应用.md: -------------------------------------------------------------------------------- 1 | ## 第14课:函数的应用 2 | 3 | 接下来我们通过一些案例来为大家讲解函数的应用。 4 | 5 | ### 经典小案例 6 | 7 | #### 案例1:设计一个生成验证码的函数。 8 | 9 | > **说明**:验证码由数字和英文大小写字母构成,长度可以用参数指定。 10 | 11 | ```Python 12 | import random 13 | import string 14 | 15 | ALL_CHARS = string.digits + string.ascii_letters 16 | 17 | 18 | def generate_code(code_len=4): 19 | """生成指定长度的验证码 20 | 21 | :param code_len: 验证码的长度(默认4个字符) 22 | :return: 由大小写英文字母和数字构成的随机验证码字符串 23 | """ 24 | return ''.join(random.choices(ALL_CHARS, k=code_len)) 25 | ``` 26 | 27 | 可以用下面的代码生成10组随机验证码来测试上面的函数。 28 | 29 | ```Python 30 | for _ in range(10): 31 | print(generate_code()) 32 | ``` 33 | 34 | > **说明**:`random`模块的`sample`和`choices`函数都可以实现随机抽样,`sample`实现无放回抽样,这意味着抽样取出的字符是不重复的;`choices`实现有放回抽样,这意味着可能会重复选中某些字符。这两个函数的第一个参数代表抽样的总体,而参数`k`代表抽样的数量。 35 | 36 | #### 案例2:设计一个函数返回给定文件的后缀名。 37 | 38 | > **说明**:文件名通常是一个字符串,而文件的后缀名指的是文件名中最后一个`.`后面的部分,也称为文件的扩展名,它是某些操作系统用来标记文件类型的一种机制,例如在Windows系统上,后缀名`exe`表示这是一个可执行程序,而后缀名`txt`表示这是一个纯文本文件。需要注意的是,在Linux和macOS系统上,文件名可以以`.`开头,表示这是一个隐藏文件,像`.gitignore`这样的文件名,`.`后面并不是后缀名,这个文件没有后缀名或者说后缀名为`''`。 39 | 40 | ```Python 41 | def get_suffix(filename, ignore_dot=True): 42 | """获取文件名的后缀名 43 | 44 | :param filename: 文件名 45 | :param ignore_dot: 是否忽略后缀名前面的点 46 | :return: 文件的后缀名 47 | """ 48 | # 从字符串中逆向查找.出现的位置 49 | pos = filename.rfind('.') 50 | # 通过切片操作从文件名中取出后缀名 51 | if pos <= 0: 52 | return '' 53 | return filename[pos + 1:] if ignore_dot else filename[pos:] 54 | ``` 55 | 56 | 可以用下面的代码对上面的函数做一个简单的测验。 57 | 58 | ```Python 59 | print(get_suffix('readme.txt')) # txt 60 | print(get_suffix('readme.txt.md')) # md 61 | print(get_suffix('.readme')) # 62 | print(get_suffix('readme.')) # 63 | print(get_suffix('readme')) # 64 | ``` 65 | 66 | 上面的`get_suffix`函数还有一个更为便捷的实现方式,就是直接使用`os.path`模块的`splitext`函数,这个函数会将文件名拆分成带路径的文件名和扩展名两个部分,然后返回一个二元组,二元组中的第二个元素就是文件的后缀名(包含`.`),如果要去掉后缀名中的`.`,可以做一个字符串的切片操作,代码如下所示。 67 | 68 | ```Python 69 | from os.path import splitext 70 | 71 | 72 | def get_suffix(filename, ignore_dot=True): 73 | return splitext(filename)[1][1:] 74 | ``` 75 | 76 | > **思考**:如果要给上面的函数增加一个参数,用来控制文件的后缀名是否包含`.`,应该怎么做? 77 | 78 | #### 案例3:写一个判断给定的正整数是不是质数的函数。 79 | 80 | ```Python 81 | def is_prime(num: int) -> bool: 82 | """判断一个正整数是不是质数 83 | 84 | :param num: 正整数 85 | :return: 如果是质数返回True,否则返回False 86 | """ 87 | for i in range(2, int(num ** 0.5) + 1): 88 | if num % i == 0: 89 | return False 90 | return num != 1 91 | ``` 92 | 93 | #### 案例4:写出计算两个正整数最大公约数和最小公倍数的函数。 94 | 95 | 代码一: 96 | 97 | ```Python 98 | def gcd_and_lcm(x: int, y: int) -> int: 99 | """求最大公约数和最小公倍数""" 100 | a, b = x, y 101 | while b % a != 0: 102 | a, b = b % a, a 103 | return a, x * y // a 104 | ``` 105 | 106 | 代码二: 107 | 108 | ```Python 109 | def gcd(x: int, y: int) -> int: 110 | """求最大公约数""" 111 | while y % x != 0: 112 | x, y = y % x, x 113 | return x 114 | 115 | 116 | def lcm(x: int, y: int) -> int: 117 | """求最小公倍数""" 118 | return x * y // gcd(x, y) 119 | ``` 120 | 121 | > **思考**:请比较上面的代码一和代码二,想想哪种做法是更好的选择。 122 | 123 | #### 案例5:写出计算一组样本数据描述性统计信息的函数。 124 | 125 | ```Python 126 | import math 127 | 128 | 129 | def ptp(data): 130 | """求极差(全距)""" 131 | return max(data) - min(data) 132 | 133 | 134 | def average(data): 135 | """求均值""" 136 | return sum(data) / len(data) 137 | 138 | 139 | def variance(data): 140 | """求方差""" 141 | x_bar = average(data) 142 | temp = [(num - x_bar) ** 2 for num in data] 143 | return sum(temp) / (len(temp) - 1) 144 | 145 | 146 | def standard_deviation(data): 147 | """求标准差""" 148 | return math.sqrt(variance(data)) 149 | 150 | 151 | def median(data): 152 | """找中位数""" 153 | temp, size = sorted(data), len(data) 154 | if size % 2 != 0: 155 | return temp[size // 2] 156 | else: 157 | return average(temp[size // 2 - 1:size // 2 + 1]) 158 | ``` 159 | 160 | ### 简单的总结 161 | 162 | 在写代码尤其是开发商业项目的时候,一定要有意识的**将相对独立且重复出现的功能封装成函数**,这样不管是自己还是团队的其他成员都可以通过调用函数的方式来使用这些功能。 163 | -------------------------------------------------------------------------------- /第06课:循环结构.md: -------------------------------------------------------------------------------- 1 | ## 第06课:循环结构 2 | 3 | ### 应用场景 4 | 5 | 我们在写程序的时候,一定会遇到需要重复执行某条指令或某些指令的场景。例如用程序控制机器人踢足球,如果机器人持球而且还没有进入射门范围,那么我们就要一直发出让机器人向球门方向移动的指令。在这个场景中,让机器人向球门方向移动就是一个需要重复的动作,当然这里还会用到上一课讲的分支结构来判断机器人是否持球以及是否进入射门范围。再举一个简单的例子,如果要实现每隔1秒中在屏幕上打印一次“hello, world”并持续打印一个小时,我们肯定不能够直接把`print('hello, world')`这句代码写3600遍,这里我们需要构造循环结构。 6 | 7 | 所谓循环结构,就是程序中控制某条或某些指令重复执行的结构。在Python中构造循环结构有两种做法,一种是`for-in`循环,另一种是`while`循环。 8 | 9 | ### for-in循环 10 | 11 | 如果明确的知道循环执行的次数,我们推荐使用`for-in`循环,例如输出100行的”hello, world“。 被`for-in`循环控制的语句块也是通过缩进的方式来构造的,这一点跟分支结构完全相同,大家看看下面的代码就明白了。 12 | 13 | ```Python 14 | """ 15 | 用for循环实现1~100求和 16 | 17 | Version: 0.1 18 | Author: 骆昊 19 | """ 20 | total = 0 21 | for x in range(1, 101): 22 | total += x 23 | print(total) 24 | ``` 25 | 26 | 需要说明的是上面代码中的`range(1, 101)`可以用来构造一个从`1`到`100`的范围,当我们把这样一个范围放到`for-in`循环中,就可以通过前面的循环变量`x`依次取出从`1`到`100`的整数。当然,`range`的用法非常灵活,下面给出了一个例子: 27 | 28 | - `range(101)`:可以用来产生0到100范围的整数,需要注意的是取不到101。 29 | - `range(1, 101)`:可以用来产生1到100范围的整数,相当于前面是闭区间后面是开区间。 30 | - `range(1, 101, 2)`:可以用来产生1到100的奇数,其中2是步长,即每次递增的值。 31 | - `range(100, 0, -2)`:可以用来产生100到1的偶数,其中-2是步长,即每次递减的值。 32 | 33 | 知道了这一点,我们可以用下面的代码来实现1~100之间的偶数求和。 34 | 35 | ```Python 36 | """ 37 | 用for循环实现1~100之间的偶数求和 38 | 39 | Version: 0.1 40 | Author: 骆昊 41 | """ 42 | total = 0 43 | for x in range(2, 101, 2): 44 | total += x 45 | print(total) 46 | ``` 47 | 48 | ### while循环 49 | 50 | 如果要构造不知道具体循环次数的循环结构,我们推荐使用`while`循环。`while`循环通过一个能够产生`bool`值的表达式来控制循环,当表达式的值为`True`时则继续循环,当表达式的值为`False`时则结束循环。 51 | 52 | 下面我们通过一个“猜数字”的小游戏来看看如何使用`while`循环。猜数字游戏的规则是:计算机出一个`1`到`100`之间的随机数,玩家输入自己猜的数字,计算机给出对应的提示信息(大一点、小一点或猜对了),如果玩家猜中了数字,计算机提示用户一共猜了多少次,游戏结束,否则游戏继续。 53 | 54 | ```Python 55 | """ 56 | 猜数字游戏 57 | 58 | Version: 0.1 59 | Author: 骆昊 60 | """ 61 | import random 62 | 63 | # 产生一个1-100范围的随机数 64 | answer = random.randint(1, 100) 65 | counter = 0 66 | while True: 67 | counter += 1 68 | number = int(input('请输入: ')) 69 | if number < answer: 70 | print('大一点') 71 | elif number > answer: 72 | print('小一点') 73 | else: 74 | print('恭喜你猜对了!') 75 | break 76 | # 当退出while循环的时候显示用户一共猜了多少次 77 | print(f'你总共猜了{counter}次') 78 | ``` 79 | 80 | ### break和continue 81 | 82 | 上面的代码中使用`while True`构造了一个条件恒成立的循环,也就意味着如果不做特殊处理,循环是不会结束的,这也就是常说的“死循环”。为了在用户猜中数字时能够退出循环结构,我们使用了`break`关键字,它的作用是提前结束循环。需要注意的是,`break`只能终止它所在的那个循环,这一点在使用嵌套循环结构时需要引起注意,下面的例子我们会讲到什么是嵌套的循环结构。除了`break`之外,还有另一个关键字是`continue`,它可以用来放弃本次循环后续的代码直接让循环进入下一轮。 83 | 84 | ### 嵌套的循环结构 85 | 86 | 和分支结构一样,循环结构也是可以嵌套的,也就是说在循环中还可以构造循环结构。下面的例子演示了如何通过嵌套的循环来输出一个乘法口诀表(九九表)。 87 | 88 | ```Python 89 | """ 90 | 打印乘法口诀表 91 | 92 | Version: 0.1 93 | Author: 骆昊 94 | """ 95 | for i in range(1, 10): 96 | for j in range(1, i + 1): 97 | print(f'{i}*{j}={i * j}', end='\t') 98 | print() 99 | ``` 100 | 101 | 很显然,在上面的代码中,外层循环用来控制一共会产生`9`行的输出,而内层循环用来控制每一行会输出多少列。内层循环中的输出就是九九表一行中的所有列,所以在内层循环完成时,有一个`print()`来实现换行输出的效果。 102 | 103 | ### 循环的例子 104 | 105 | #### 例子1:输入一个正整数判断它是不是素数。 106 | 107 | > **提示**:素数指的是只能被1和自身整除的大于1的整数。 108 | 109 | ```Python 110 | """ 111 | 输入一个正整数判断它是不是素数 112 | 113 | Version: 0.1 114 | Author: 骆昊 115 | """ 116 | num = int(input('请输入一个正整数: ')) 117 | end = int(num ** 0.5) 118 | is_prime = True 119 | for x in range(2, end + 1): 120 | if num % x == 0: 121 | is_prime = False 122 | break 123 | if is_prime and num != 1: 124 | print(f'{num}是素数') 125 | else: 126 | print(f'{num}不是素数') 127 | ``` 128 | 129 | #### 例子2:输入两个正整数,计算它们的最大公约数和最小公倍数。 130 | 131 | > **提示**:两个数的最大公约数是两个数的公共因子中最大的那个数;两个数的最小公倍数则是能够同时被两个数整除的最小的那个数。 132 | 133 | ```Python 134 | """ 135 | 输入两个正整数计算它们的最大公约数和最小公倍数 136 | 137 | Version: 0.1 138 | Author: 骆昊 139 | """ 140 | 141 | x = int(input('x = ')) 142 | y = int(input('y = ')) 143 | for factor in range(x, 0, -1): 144 | if x % factor == 0 and y % factor == 0: 145 | print(f'{x}和{y}的最大公约数是{factor}') 146 | print(f'{x}和{y}的最小公倍数是{x * y // factor}') 147 | break 148 | ``` 149 | 150 | ### 简单的总结 151 | 152 | 学会了Python中的分支结构和循环结构,我们就可以解决很多实际的问题了。通过这节课的学习,大家应该已经知道了可以用`for`和`while`关键字来构造循环结构。**如果知道循环的次数,我们通常使用**`for`**循环**;如果**循环次数不能确定,可以用**`while`**循环**。在循环中还**可以使用**`break`**来提前结束循环**。 153 | -------------------------------------------------------------------------------- /第03课:Python语言元素之变量.md: -------------------------------------------------------------------------------- 1 | ## 第03课:Python语言元素之变量 2 | 3 | 作为一个程序员,可能经常会被外行问到两个问题,其一是“什么是(计算机)程序”,其二是“写(计算机)程序能做什么”,这里我先对这两个问题做一个回答。**程序是指令的集合**,**写程序就是用指令控制计算机做我们想让它做的事情**。那么,为什么要用Python语言来写程序呢?因为**Python语言简单优雅**,相比C、C++、Java这样的编程语言,**Python对初学者更加友好**,当然这并不是说Python不像其他语言那样强大,**Python几乎是无所不能的**,在第一节课的时候,我们就说到了Python可以用于服务器程序开发、云平台开发、数据分析、机器学习等各个领域。当然,Python语言还可以用来粘合其他语言开发的系统,所以也经常被戏称为“**胶水语言**”。 4 | 5 | ### 一些计算机常识 6 | 7 | 在开始系统的学习编程之前,我们先来科普一些计算机的基础知识。计算机的硬件系统通常由五大部件构成,包括:**运算器**、**控制器**、**存储器**、**输入设备**和**输出设备**。其中,运算器和控制器放在一起就是我们常说的**中央处理器**,它的功能是执行各种运算和控制指令。刚才我们提到过程序是指令的集合,写程序就是将一系列的指令按照某种方式组织到一起,然后通过这些指令去控制计算机做我们想让它做的事情。目前,我们使用的计算机基本都是“冯·诺依曼体系结构”的计算机,这种计算机有两个关键点:一是要将**存储设备与中央处理器分开**;二是将**数据以二进制方式编码**。 8 | 9 | 二进制是一种“逢二进一”的计数法,跟我们人类使用的“逢十进一”的计数法本质是一样的。人类因为有十根手指所以使用了十进制,因为在计数时十根手指用完之后就只能用进位的方式来表示更大的数值。当然凡事都有例外,玛雅人可能是因为长年光着脚的原因,把脚趾头也都用上了,于是他们使用了二十进制的计数法。在这种计数法的指导下,玛雅人的历法就与我们平常使用的历法并不相同。按照玛雅人的历法,2012年是上一个所谓的“太阳纪”的最后一年,而2013年则是新的“太阳纪”的开始,后来这件事情被以讹传讹的方式误传为”2012年是玛雅人预言的世界末日“的荒诞说法。今天很多人都在猜测,玛雅文明之所以发展缓慢跟使用了二十进制是有关系的。对于计算机来说,二进制在物理器件上最容易实现的,因为可以用高电压表示1,用低电压表示0。不是所有写程序的人都需要知道十进制与二进制如何转换,大多数时候我们即便不了解这些知识也能写出程序,但是我们必须要知道**计算机是使用二进制计数的**,不管什么**数据到了计算机内存中都是以二进制形式存在的**。 10 | 11 | ### 变量和类型 12 | 13 | 要想在计算机内存中保存数据,首先就得说一说变量这个概念。在编程语言中,**变量是数据的载体**,简单的说就是一块用来保存数据的内存空间,**变量的值可以被读取和修改**,这是所有计算和控制的基础。计算机能处理的数据有很多种类型,最常见的就是数值,除了数值之外还有文本、图形、音频、视频等各种各样的数据。虽然数据在计算机中都是以二进制形态存在的,但是我们可以用不同类型的变量来表示数据类型的差异。**Python中的数据类型很多**,而且也**允许我们自定义新的数据类型**(这一点在后面会讲到),这里我们需要先了解几种常用的数据类型。 14 | 15 | - 整型(`int`):Python中可以处理任意大小的整数,而且支持二进制(如`0b100`,换算成十进制是4)、八进制(如`0o100`,换算成十进制是64)、十进制(`100`)和十六进制(`0x100`,换算成十进制是256)的表示法。 16 | - 浮点型(`float`):浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如`123.456`)之外还支持科学计数法(如`1.23456e2`)。 17 | - 字符串型(`str`):字符串是以单引号或双引号括起来的任意文本,比如`'hello'`和`"hello"`。 18 | - 布尔型(`bool`):布尔值只有`True`、`False`两种值,要么是`True`,要么是`False`。 19 | 20 | ### 变量命名 21 | 22 | 对于每个变量我们需要给它取一个名字,就如同我们每个人都有自己的名字一样。在Python中,变量命名需要遵循以下这些规则,这些规则又分为必须遵守的硬性规则和建议遵守的非硬性规则。 23 | 24 | - 硬性规则: 25 | - 规则1:变量名由**字母**、数字和**下划线**构成,数字不能开头。需要说明的是,这里说的字母指的是Unicode字符,Unicode称为万国码,囊括了世界上大部分的文字系统,这也就意味着中文、日文、希腊字母等都可以作为变量名中的字符,但是像`!`、`@`、`#`这些特殊字符是不能出现在变量名中的,而且我们强烈建议大家**尽可能使用英文字母**。 26 | - 规则2:**大小写敏感**,简单的说就是大写的`A`和小写的`a`是两个不同的变量。 27 | - 规则3:变量名**不要跟Python语言的关键字**(有特殊含义的单词,后面会讲到)和**保留字**(如已有的函数、模块等的名字)**发生重名的冲突**。 28 | - 非硬性规则: 29 | - 规则1:变量名通常使用小写英文字母,多个单词用下划线进行连接。 30 | - 规则2:受保护的变量用单个下划线开头。 31 | - 规则3:私有的变量用两个下划线开头。 32 | 33 | 规则2和规则3大家暂时不用理解,后面自然会明白的。当然,作为一个专业的程序员,给变量(事实上应该是所有的标识符)命名时做到**见名知意**也非常重要。 34 | 35 | ### 变量的使用 36 | 37 | 下面通过例子来说明变量的类型和变量的使用。 38 | 39 | ```Python 40 | """ 41 | 使用变量保存数据并进行加减乘除运算 42 | 43 | Version: 0.1 44 | Author: 骆昊 45 | """ 46 | a = 45 # 变量a保存了45 47 | b = 12 # 变量b保存了12 48 | print(a + b) # 57 49 | print(a - b) # 33 50 | print(a * b) # 540 51 | print(a / b) # 3.75 52 | ``` 53 | 54 | 在Python中可以使用`type`函数对变量的类型进行检查。程序设计中函数的概念跟数学上函数的概念基本一致,数学上的函数相信大家并不陌生,它包括了函数名、自变量和因变量。如果暂时不理解函数这个概念也不要紧,我们会在后续的内容中专门讲解函数的定义和使用。 55 | 56 | ```Python 57 | """ 58 | 使用type()检查变量的类型 59 | 60 | Version: 0.1 61 | Author: 骆昊 62 | """ 63 | a = 100 64 | b = 12.345 65 | c = 'hello, world' 66 | d = True 67 | print(type(a)) # 68 | print(type(b)) # 69 | print(type(c)) # 70 | print(type(d)) # 71 | ``` 72 | 73 | 不同类型的变量可以相互转换,这一点可以通过Python的内置函数来实现。 74 | 75 | - `int()`:将一个数值或字符串转换成整数,可以指定进制。 76 | - `float()`:将一个字符串转换成浮点数。 77 | - `str()`:将指定的对象转换成字符串形式,可以指定编码。 78 | - `chr()`:将整数转换成该编码对应的字符串(一个字符)。 79 | - `ord()`:将字符串(一个字符)转换成对应的编码(整数)。 80 | 81 | 下面的例子为大家演示了Python中类型转换的操作。 82 | 83 | ```Python 84 | """ 85 | Python中的类型转换操作 86 | 87 | Version: 0.1 88 | Author: 骆昊 89 | """ 90 | a = 100 91 | b = 12.345 92 | c = 'hello, world' 93 | d = True 94 | # 整数转成浮点数 95 | print(float(a)) # 100.0 96 | # 浮点型转成字符串 (输出字符串时不会看到引号哟) 97 | print(str(b)) # 12.345 98 | # 字符串转成布尔型 (有内容的字符串都会变成True) 99 | print(bool(c)) # True 100 | # 布尔型转成整数 (True会转成1,False会转成0) 101 | print(int(d)) # 1 102 | # 将整数变成对应的字符 (97刚好对应字符表中的字母a) 103 | print(chr(97)) # a 104 | # 将字符转成整数 (Python中字符和字符串表示法相同) 105 | print(ord('a')) # 97 106 | ``` 107 | 108 | ### 总结 109 | 110 | 在Python程序中,我们可以**使用变量来保存数据**,**变量有不同的类型**,**变量可以做运算**(下一课会有详细的讲解),**也可以通过内置函数来转换变量类型**。 111 | -------------------------------------------------------------------------------- /第05课:分支结构.md: -------------------------------------------------------------------------------- 1 | ## 第05课:分支结构 2 | 3 | ### 应用场景 4 | 5 | 迄今为止,我们写的Python代码都是一条一条语句顺序执行,这种代码结构通常称之为顺序结构。然而仅有顺序结构并不能解决所有的问题,比如我们设计一个游戏,游戏第一关的通关条件是玩家获得1000分,那么在完成本局游戏后,我们要根据玩家得到分数来决定究竟是进入第二关,还是告诉玩家“Game Over”,这里就会产生两个分支,而且这两个分支只有一个会被执行。类似的场景还有很多,我们将这种结构称之为“分支结构”或“选择结构”。给大家一分钟的时间,你应该可以想到至少5个以上这样的例子,赶紧试一试。 6 | 7 | ### if语句的使用 8 | 9 | 在Python中,要构造分支结构可以使用`if`、`elif`和`else`关键字。所谓**关键字**就是有特殊含义的单词,像`if`和`else`就是专门用于构造分支结构的关键字,很显然你不能够使用它作为变量名。下面的例子中演示了如何构造一个分支结构。 10 | 11 | ```Python 12 | """ 13 | 用户身份验证 14 | 15 | Version: 0.1 16 | Author: 骆昊 17 | """ 18 | username = input('请输入用户名: ') 19 | password = input('请输入口令: ') 20 | # 用户名是admin且密码是123456则身份验证成功否则身份验证失败 21 | if username == 'admin' and password == '123456': 22 | print('身份验证成功!') 23 | else: 24 | print('身份验证失败!') 25 | ``` 26 | 27 | 需要说明的是,不同于C++、Java等编程语言,Python中没有用花括号来构造代码块而是**使用了缩进的方式来表示代码的层次结构**,如果`if`条件成立的情况下需要执行多条语句,只要保持多条语句具有相同的缩进就可以了。换句话说**连续的代码如果又保持了相同的缩进那么它们属于同一个代码块**,相当于是一个执行的整体。**缩进**可以使用任意数量的空格,但**通常使用4个空格**,强烈建议大家**不要使用制表键来缩进代码**,如果你已经习惯了这么做,可以**设置代码编辑工具将1个制表键自动变成4个空格**,很多的代码编辑工具都支持这项功能。 28 | 29 | > **提示**:`if`和`else` 的最后面有一个`:`,它是用英文输入法输入的冒号;程序中输入的`'`、`"`、`=`、`(`、`)`等特殊字符,都是在英文输入法状态下输入的。有很多初学者经常不注意这一点,结果运行代码的时候就会遇到很多莫名其妙的错误提示。**强烈建议**大家在写代码的时候都**打开英文输入法**(注意是英文输入法而不是中文输入法的英文输入模式),这样可以避免很多不必要的麻烦。 30 | 31 | 如果要构造出更多的分支,可以使用`if...elif...else...`结构或者嵌套的`if...else...`结构,下面的代码演示了如何利用多分支结构实现分段函数求值。 32 | 33 | $$ 34 | f(x) = \begin{cases} 3x - 5, & (x \gt 1) \\\\ x + 2, & (-1 \le x \le 1) \\\\ 5x + 3, & (x \lt -1) \end{cases} 35 | $$ 36 | 37 | ```Python 38 | """ 39 | 分段函数求值 40 | 41 | Version: 0.1 42 | Author: 骆昊 43 | """ 44 | x = float(input('x = ')) 45 | if x > 1: 46 | y = 3 * x - 5 47 | elif x >= -1: 48 | y = x + 2 49 | else: 50 | y = 5 * x + 3 51 | print(f'f({x}) = {y}') 52 | ``` 53 | 54 | 当然根据实际开发的需要,分支结构是可以嵌套的,例如判断是否通关以后还要根据你获得的宝物或者道具的数量对你的表现给出等级(比如点亮两颗或三颗星星),那么我们就需要在`if`的内部构造出一个新的分支结构,同理`elif`和`else`中也可以再构造新的分支,我们称之为嵌套的分支结构,也就是说上面的代码也可以写成下面的样子。 55 | 56 | ```Python 57 | """ 58 | 分段函数求值 59 | 60 | Version: 0.1 61 | Author: 骆昊 62 | """ 63 | x = float(input('x = ')) 64 | if x > 1: 65 | y = 3 * x - 5 66 | else: 67 | if x >= -1: 68 | y = x + 2 69 | else: 70 | y = 5 * x + 3 71 | print(f'f({x}) = {y}') 72 | ``` 73 | 74 | > **说明:** 大家可以自己感受和评判一下这两种写法到底是哪一种更好。在[**Python之禅**](https://zhuanlan.zhihu.com/p/111843067)中有这么一句话:“**Flat is better than nested**”,之所以提倡代码“扁平化”,是因为代码嵌套的层次如果很多,会严重的影响代码的可读性,所以使用更为扁平化的结构在很多场景下都是较好的选择。 75 | 76 | ### 一些例子 77 | 78 | #### 例子1:英制单位英寸与公制单位厘米互换。 79 | 80 | ```Python 81 | """ 82 | 英制单位英寸和公制单位厘米互换 83 | 84 | Version: 0.1 85 | Author: 骆昊 86 | """ 87 | value = float(input('请输入长度: ')) 88 | unit = input('请输入单位: ') 89 | if unit == 'in' or unit == '英寸': 90 | print('%f英寸 = %f厘米' % (value, value * 2.54)) 91 | elif unit == 'cm' or unit == '厘米': 92 | print('%f厘米 = %f英寸' % (value, value / 2.54)) 93 | else: 94 | print('请输入有效的单位') 95 | ``` 96 | 97 | #### 例子2:百分制成绩转换为等级制成绩。 98 | 99 | > **要求**:如果输入的成绩在90分以上(含90分)输出A;80分-90分(不含90分)输出B;70分-80分(不含80分)输出C;60分-70分(不含70分)输出D;60分以下输出E。 100 | 101 | ```Python 102 | """ 103 | 百分制成绩转换为等级制成绩 104 | 105 | Version: 0.1 106 | Author: 骆昊 107 | """ 108 | score = float(input('请输入成绩: ')) 109 | if score >= 90: 110 | grade = 'A' 111 | elif score >= 80: 112 | grade = 'B' 113 | elif score >= 70: 114 | grade = 'C' 115 | elif score >= 60: 116 | grade = 'D' 117 | else: 118 | grade = 'E' 119 | print('对应的等级是:', grade) 120 | ``` 121 | #### 例子3:输入三条边长,如果能构成三角形就计算周长和面积。 122 | 123 | ```Python 124 | """ 125 | 判断输入的边长能否构成三角形,如果能则计算出三角形的周长和面积 126 | 127 | Version: 0.1 128 | Author: 骆昊 129 | """ 130 | a = float(input('a = ')) 131 | b = float(input('b = ')) 132 | c = float(input('c = ')) 133 | if a + b > c and a + c > b and b + c > a: 134 | peri = a + b + c 135 | print(f'周长: {peri}') 136 | half = peri / 2 137 | area = (half * (half - a) * (half - b) * (half - c)) ** 0.5 138 | print(f'面积: {area}') 139 | else: 140 | print('不能构成三角形') 141 | ``` 142 | > **说明:** 上面通过边长计算三角形面积的公式叫做[海伦公式](https://zh.wikipedia.org/zh-hans/海伦公式)。 143 | 144 | ### 简单的总结 145 | 146 | 学会了Python中的分支结构和循环结构,我们就可以用Python程序来解决很多实际的问题了。这一节课相信已经帮助大家记住了`if`、`elif`、`else`这几个关键字以及如何使用它们来构造分支结构,下一节课我们为大家介绍循环结构,学完这两次课你一定会发现,你能写出很多很多非常有意思的代码。继续加油! 147 | -------------------------------------------------------------------------------- /第27课:用Python操作PDF文件.md: -------------------------------------------------------------------------------- 1 | ## 第27课:用Python操作PDF文件 2 | 3 | PDF是Portable Document Format的缩写,这类文件通常使用`.pdf`作为其扩展名。在日常开发工作中,最容易遇到的就是从PDF中读取文本内容以及用已有的内容生成PDF文档这两个任务。 4 | 5 | ### 从PDF中提取文本 6 | 7 | 在Python中,可以使用名为`PyPDF2`的三方库来读取PDF文件,可以使用下面的命令来安装它。 8 | 9 | ```Bash 10 | pip install PyPDF2 11 | ``` 12 | 13 | `PyPDF2`没有办法从PDF文档中提取图像、图表或其他媒体,但它可以提取文本,并将其返回为Python字符串。 14 | 15 | ```Python 16 | import PyPDF2 17 | 18 | reader = PyPDF2.PdfReader('test.pdf') 19 | for page in reader.pages: 20 | print(page.extract_text()) 21 | ``` 22 | 23 | > **提示**:上面代码中使用的PDF文件“test.pdf”以及下面的代码中需要用到的PDF文件,也可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。 24 | 25 | 当然,`PyPDF2`并不是什么样的PDF文档都能提取出文字来,这个问题就我所知并没有什么特别好的解决方法,尤其是在提取中文的时候。网上也有很多讲解从PDF中提取文字的文章,推荐大家自行阅读[《三大神器助力Python提取pdf文档信息》](https://cloud.tencent.com/developer/article/1395339)一文进行了解。 26 | 27 | 要从PDF文件中提取文本也可以直接使用三方的命令行工具,具体的做法如下所示。 28 | 29 | ```Bash 30 | pip install pdfminer.six 31 | pdf2text.py test.pdf 32 | ``` 33 | 34 | ### 旋转和叠加页面 35 | 36 | 上面的代码中通过创建`PdfFileReader`对象的方式来读取PDF文档,该对象的`getPage`方法可以获得PDF文档的指定页并得到一个`PageObject`对象,通过`PageObject`对象的`rotateClockwise`和`rotateCounterClockwise`方法可以实现页面的顺时针和逆时针方向旋转,通过`PageObject`对象的`addBlankPage`方法可以添加一个新的空白页,代码如下所示。 37 | 38 | ```Python 39 | reader = PyPDF2.PdfReader('XGBoost.pdf') 40 | writer = PyPDF2.PdfWriter() 41 | 42 | for no, page in enumerate(reader.pages): 43 | if no % 2 == 0: 44 | new_page = page.rotate(-90) 45 | else: 46 | new_page = page.rotate(90) 47 | writer.add_page(new_page) 48 | 49 | with open('temp.pdf', 'wb') as file_obj: 50 | writer.write(file_obj) 51 | ``` 52 | 53 | ### 加密PDF文件 54 | 55 | 使用`PyPDF2`中的`PdfFileWrite`对象可以为PDF文档加密,如果需要给一系列的PDF文档设置统一的访问口令,使用Python程序来处理就会非常的方便。 56 | 57 | ```Python 58 | import PyPDF2 59 | 60 | reader = PyPDF2.PdfReader('XGBoost.pdf') 61 | writer = PyPDF2.PdfWriter() 62 | 63 | for page in reader.pages: 64 | writer.add_page(page) 65 | 66 | writer.encrypt('foobared') 67 | 68 | with open('temp.pdf', 'wb') as file_obj: 69 | writer.write(file_obj) 70 | ``` 71 | 72 | ### 批量添加水印 73 | 74 | 上面提到的`PageObject`对象还有一个名为`mergePage`的方法,可以两个PDF页面进行叠加,通过这个操作,我们很容易实现给PDF文件添加水印的功能。例如要给上面的“XGBoost.pdf”文件添加一个水印,我们可以先准备好一个提供水印页面的PDF文件,然后将包含水印的`PageObject`读取出来,然后再循环遍历“XGBoost.pdf”文件的每个页,获取到`PageObject`对象,然后通过`mergePage`方法实现水印页和原始页的合并,代码如下所示。 75 | 76 | ```Python 77 | reader1 = PyPDF2.PdfReader('XGBoost.pdf') 78 | reader2 = PyPDF2.PdfReader('watermark.pdf') 79 | writer = PyPDF2.PdfWriter() 80 | watermark_page = reader2.pages[0] 81 | 82 | for page in reader1.pages: 83 | page.merge_page(watermark_page) 84 | writer.add_page(page) 85 | 86 | with open('temp.pdf', 'wb') as file_obj: 87 | writer.write(file_obj) 88 | ``` 89 | 90 | 如果愿意,还可以让奇数页和偶数页使用不同的水印,大家可以自己思考下应该怎么做。 91 | 92 | ### 创建PDF文件 93 | 94 | 创建PDF文档需要三方库`reportlab`的支持,安装的方法如下所示。 95 | 96 | ```Bash 97 | pip install reportlab 98 | ``` 99 | 100 | 下面通过一个例子为大家展示`reportlab`的用法。 101 | 102 | ```Python 103 | from reportlab.lib.pagesizes import A4 104 | from reportlab.pdfbase import pdfmetrics 105 | from reportlab.pdfbase.ttfonts import TTFont 106 | from reportlab.pdfgen import canvas 107 | 108 | pdf_canvas = canvas.Canvas('resources/demo.pdf', pagesize=A4) 109 | width, height = A4 110 | 111 | # 绘图 112 | image = canvas.ImageReader('resources/guido.jpg') 113 | pdf_canvas.drawImage(image, 20, height - 395, 250, 375) 114 | 115 | # 显示当前页 116 | pdf_canvas.showPage() 117 | 118 | # 注册字体文件 119 | pdfmetrics.registerFont(TTFont('Font1', 'resources/fonts/Vera.ttf')) 120 | pdfmetrics.registerFont(TTFont('Font2', 'resources/fonts/青呱石头体.ttf')) 121 | 122 | # 写字 123 | pdf_canvas.setFont('Font2', 40) 124 | pdf_canvas.setFillColorRGB(0.9, 0.5, 0.3, 1) 125 | pdf_canvas.drawString(width // 2 - 120, height // 2, '你好,世界!') 126 | pdf_canvas.setFont('Font1', 40) 127 | pdf_canvas.setFillColorRGB(0, 1, 0, 0.5) 128 | pdf_canvas.rotate(18) 129 | pdf_canvas.drawString(250, 250, 'hello, world!') 130 | 131 | # 保存 132 | pdf_canvas.save() 133 | ``` 134 | 135 | 上面的代码如果不太理解也没有关系,等真正需要用Python创建PDF文档的时候,再好好研读一下`reportlab`的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)就可以了。 136 | 137 | > **提示**:上面代码中用到的图片和字体,也可以通过下面的百度云盘链接获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。 138 | 139 | ### 简单的总结 140 | 141 | 在学习完上面的内容之后,相信大家已经知道像合并多个PDF文件这样的工作应该如何用Python代码来处理了,赶紧自己动手试一试吧。 142 | -------------------------------------------------------------------------------- /第07课:分支和循环结构的应用.md: -------------------------------------------------------------------------------- 1 | ## 第07课:分支和循环结构的应用 2 | 3 | 通过上两节课的学习,大家对Python中的分支和循环结构已经有了感性的认识。**分支和循环结构**的重要性不言而喻,它**是构造程序逻辑的基础**,对于初学者来说也是比较困难的部分。大部分初学者在学习了分支和循环结构后都能理解它们的用途和用法,但是遇到实际问题的时候又无法下手;**看懂别人的代码很容易,但是要自己写出同样的代码却又很难**。如果你也有同样的问题和困惑,千万不要沮丧,这只是因为你才刚刚开始编程之旅,**你的练习量还没有达到让你可以随心所欲的写出代码的程度**,只要加强编程练习,这个问题迟早都会解决的。下面我们就为大家讲解一些经典的案例。 4 | 5 | ### 经典小案例 6 | 7 | #### 例子1:寻找水仙花数。 8 | 9 | > **说明**:水仙花数也被称为超完全数字不变数、自恋数、自幂数、阿姆斯特朗数,它是一个3位数,该数字每个位上数字的立方之和正好等于它本身,例如:$ 153=1^3+5^3+3^3 $。 10 | 11 | 这个题目的关键是将一个三位数拆分为个位、十位、百位,这一点利用Python中的`//`(整除)和`%`(求模)运算符其实很容易做到,代码如下所示。 12 | 13 | ```Python 14 | """ 15 | 找出所有水仙花数 16 | 17 | Version: 0.1 18 | Author: 骆昊 19 | """ 20 | for num in range(100, 1000): 21 | low = num % 10 22 | mid = num // 10 % 10 23 | high = num // 100 24 | if num == low ** 3 + mid ** 3 + high ** 3: 25 | print(num) 26 | ``` 27 | 28 | 上面利用`//`和`%`拆分一个数的小技巧在写代码的时候还是很常用的。我们要将一个不知道有多少位的正整数进行反转,例如将`12345`变成`54321`,也可以利用这两个运算来实现,代码如下所示。 29 | 30 | ```Python 31 | """ 32 | 正整数的反转 33 | 34 | Version: 0.1 35 | Author: 骆昊 36 | """ 37 | num = int(input('num = ')) 38 | reversed_num = 0 39 | while num > 0: 40 | reversed_num = reversed_num * 10 + num % 10 41 | num //= 10 42 | print(reversed_num) 43 | ``` 44 | 45 | #### 例子2:百钱百鸡问题。 46 | 47 | > **说明**:百钱百鸡是我国古代数学家[张丘建](https://baike.baidu.com/item/%E5%BC%A0%E4%B8%98%E5%BB%BA/10246238)在《算经》一书中提出的数学问题:鸡翁一值钱五,鸡母一值钱三,鸡雏三值钱一。百钱买百鸡,问鸡翁、鸡母、鸡雏各几何?翻译成现代文是:公鸡5元一只,母鸡3元一只,小鸡1元三只,用100块钱买一百只鸡,问公鸡、母鸡、小鸡各有多少只? 48 | 49 | ```Python 50 | """ 51 | 《百钱百鸡》问题 52 | 53 | Version: 0.1 54 | Author: 骆昊 55 | """ 56 | # 假设公鸡的数量为x,x的取值范围是0到20 57 | for x in range(0, 21): 58 | # 假设母鸡的数量为y,y的取值范围是0到33 59 | for y in range(0, 34): 60 | z = 100 - x - y 61 | if 5 * x + 3 * y + z // 3 == 100 and z % 3 == 0: 62 | print(f'公鸡: {x}只, 母鸡: {y}只, 小鸡: {z}只') 63 | ``` 64 | 65 | 上面使用的方法叫做**穷举法**,也称为**暴力搜索法**,这种方法通过一项一项的列举备选解决方案中所有可能的候选项并检查每个候选项是否符合问题的描述,最终得到问题的解。这种方法看起来比较笨拙,但对于运算能力非常强大的计算机来说,通常都是一个可行的甚至是不错的选择,只要问题的解存在就能够找到它。 66 | 67 | #### 例子3:CRAPS赌博游戏。 68 | 69 | > **说明**:CRAPS又称花旗骰,是美国拉斯维加斯非常受欢迎的一种的桌上赌博游戏。该游戏使用两粒骰子,玩家通过摇两粒骰子获得点数进行游戏。简化后的规则是:玩家第一次摇骰子如果摇出了7点或11点,玩家胜;玩家第一次如果摇出2点、3点或12点,庄家胜;玩家如果摇出其他点数则玩家继续摇骰子,如果玩家摇出了7点,庄家胜;如果玩家摇出了第一次摇的点数,玩家胜;其他点数玩家继续摇骰子,直到分出胜负。 70 | 71 | ```Python 72 | """ 73 | Craps赌博游戏 74 | 我们设定游戏开始时玩家有1000元的赌注 75 | 游戏结束的条件是玩家破产(输光所有的赌注) 76 | 77 | Version: 0.1 78 | Author: 骆昊 79 | """ 80 | from random import randint 81 | 82 | money = 1000 83 | while money > 0: 84 | print(f'你的总资产为: {money}元') 85 | go_on = False 86 | # 下注金额必须大于0小于等于玩家总资产 87 | while True: 88 | debt = int(input('请下注: ')) 89 | if 0 < debt <= money: 90 | break 91 | # 第一次摇色子 92 | # 用1到6均匀分布的随机数模拟摇色子得到的点数 93 | first = randint(1, 6) + randint(1, 6) 94 | print(f'\n玩家摇出了{first}点') 95 | if first == 7 or first == 11: 96 | print('玩家胜!\n') 97 | money += debt 98 | elif first == 2 or first == 3 or first == 12: 99 | print('庄家胜!\n') 100 | money -= debt 101 | else: 102 | go_on = True 103 | # 第一次摇色子没有分出胜负游戏继续 104 | while go_on: 105 | go_on = False 106 | current = randint(1, 6) + randint(1, 6) 107 | print(f'玩家摇出了{current}点') 108 | if current == 7: 109 | print('庄家胜!\n') 110 | money -= debt 111 | elif current == first: 112 | print('玩家胜!\n') 113 | money += debt 114 | else: 115 | go_on = True 116 | print('你破产了, 游戏结束!') 117 | ``` 118 | 119 | #### 例子4:斐波那契数列。 120 | 121 | > **说明**:斐波那契数列(Fibonacci sequence),通常也被称作黄金分割数列,是意大利数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)在《计算之书》中研究在理想假设条件下兔子成长率问题而引入的数列,因此这个数列也常被戏称为“兔子数列”。斐波那契数列的特点是数列的前两个数都是1,从第三个数开始,每个数都是它前面两个数的和,按照这个规律,斐波那契数列的前10个数是:`1, 1, 2, 3, 5, 8, 13, 21, 34, 55`。斐波那契数列在现代物理、准晶体结构、化学等领域都有直接的应用。 122 | 123 | ```Python 124 | """ 125 | 输出斐波那契数列前20个数 126 | 127 | Version: 0.1 128 | Author: 骆昊 129 | """ 130 | 131 | a, b = 0, 1 132 | for _ in range(20): 133 | a, b = b, a + b 134 | print(a) 135 | ``` 136 | 137 | #### 例子5:打印100以内的素数。 138 | 139 | > **说明**:素数指的是只能被1和自身整除的正整数(不包括1)。 140 | 141 | ```Python 142 | """ 143 | 输出100以内的素数 144 | 145 | Version: 0.1 146 | Author: 骆昊 147 | """ 148 | for num in range(2, 100): 149 | # 假设num是素数 150 | is_prime = True 151 | # 在2到num-1之间找num的因子 152 | for factor in range(2, num): 153 | # 如果找到了num的因子,num就不是素数 154 | if num % factor == 0: 155 | is_prime = False 156 | break 157 | # 如果布尔值为True在num是素数 158 | if is_prime: 159 | print(num) 160 | ``` 161 | 162 | ### 简单的总结 163 | 164 | 还是那句话:**分支结构和循环结构非常重要**,是构造程序逻辑的基础,**一定要通过大量的练习来达到融会贯通**。刚才讲到的CRAPS赌博游戏那个例子可以作为一个标准,如果你能很顺利的完成这段代码,那么分支和循环结构的知识你就已经掌握了。 165 | 166 | -------------------------------------------------------------------------------- /第02课:第一个Python程序.md: -------------------------------------------------------------------------------- 1 | ## 第02课:第一个Python程序 2 | 3 | 在上一课中,我们已经了解了Python语言并安装了运行Python程序所需的环境,相信大家已经迫不及待的想开始自己的Python编程之旅了。首先我们来看看应该在哪里编写我们的Python程序。 4 | 5 | ### 编写代码的工具 6 | 7 | #### 交互式环境 8 | 9 | 我们打开Windows的“命令提示符”工具,输入命令`python`然后回车就可以进入到Python的交互式环境中。所谓交互式环境,就是我们输入一行代码回车,代码马上会被执行,如果代码有产出结果,那么结果会被显示在窗口中。例如: 10 | 11 | ```Bash 12 | Python 3.7.6 13 | Type "help", "copyright", "credits" or "license" for more information. 14 | >>> 2 * 3 15 | 6 16 | >>> 2 + 3 17 | 5 18 | ``` 19 | 20 | > **提示**:使用macOS系统的用户需要打开“终端”工具,输入`python3`进入交互式环境。 21 | 22 | 如果希望退出交互式环境,可以在交互式环境中输入`quit()`,如下所示。 23 | 24 | ```Bash 25 | >>> quit() 26 | ``` 27 | 28 | #### 更好的交互式环境 - IPython 29 | 30 | Python默认的交互式环境用户体验并不怎么好,我们可以用IPython来替换掉它,因为IPython提供了更为强大的编辑和交互功能。我们可以使用Python的包管理工具`pip`来安装IPython,如下所示。 31 | 32 | ```bash 33 | pip install ipython 34 | ``` 35 | 36 | > **温馨提示**:在使用上面的命令安装IPython之前,可以先通过`pip config set global.index-url https://pypi.doubanio.com/simple`命令将`pip`的下载源修改为国内的豆瓣网,否则下载安装的过程可能会非常的缓慢。 37 | 38 | 可以使用下面的命令启动IPython,进入交互式环境。 39 | 40 | ```bash 41 | ipython 42 | ``` 43 | 44 | #### 文本编辑器 - Visual Studio Code 45 | 46 | Visual Studio Code(通常简称为VSCode)是一个由微软开发能够在Windows、 Linux和macOS等操作系统上运行的代码编辑神器。它支持语法高亮、自动补全、多点编辑、运行调试等一系列便捷功能,而且能够支持多种编程语言。如果大家要选择一款高级文本编辑工具,强烈建议使用VSCode。关于VSCode的[下载](https://code.visualstudio.com/)、安装和使用,推荐大家阅读一篇名为[《VScode安装使用》]()的文章。 47 | 48 | #### 集成开发环境 - PyCharm 49 | 50 | 如果用Python开发商业项目,我们推荐大家使用更为专业的工具PyCharm。PyCharm是由捷克一家名为[JetBrains](https://www.jetbrains.com/)的公司开发的用于Python项目开发的集成开发环境(IDE)。所谓集成开发环境,通常是指工具中提供了编写代码、运行代码、调试代码、分析代码、版本控制等一系列功能,因此特别适合商业项目的开发。在JetBrains的官方网站上提供了PyCharm的[下载链接](),其中社区版(Community)是免费的但功能相对弱小(其实已经足够强大了),专业版(Professional)功能非常强大,但需要按年或月付费使用,新用户可以试用30天时间。 51 | 52 | 运行PyCharm,可以看到如下图所示的欢迎界面,可以选择“New Project”来创建一个新的项目。 53 | 54 | 55 | 56 | 创建项目的时候需要指定项目的路径并创建运行项目的”虚拟环境“,如下图所示。 57 | 58 | 59 | 60 | 项目创建好以后会出现如下图所示的画面,我们可以通过在项目文件夹上点击鼠标右键,选择“New”菜单下的“Python File”来创建一个Python文件,创建好的Python文件会自动打开进入可编辑的状态。 61 | 62 | ![image-20210720133621079](https://github.com/jackfrued/mypic/raw/master/20210720133621.png) 63 | 64 | 写好代码后,可以在编辑代码的窗口点击鼠标右键,选择“Run”菜单项来运行代码,下面的“Run”窗口会显示代码的执行结果,如下图所示。 65 | 66 | ![image-20210720134039848](https://github.com/jackfrued/mypic/raw/master/20210720134039.png) 67 | 68 | PyCharm常用的快捷键如下表所示,我们也可以在“File”菜单的“Settings”中定制PyCharm的快捷键(macOS系统是在“PyCharm”菜单的“Preferences”中对快捷键进行设置)。 69 | 70 | 表1. PyCharm常用快捷键。 71 | 72 | | 快捷键 | 作用 | 73 | | --------------------------------------- | -------------------------------------- | 74 | | `ctrl + j` | 显示可用的代码模板 | 75 | | `ctrl + b` | 查看函数、类、方法的定义 | 76 | | `ctrl + alt + l` | 格式化代码 | 77 | | `alt + enter` | 万能代码修复快捷键 | 78 | | `ctrl + /` | 注释/反注释代码 | 79 | | `shift + shift` | 万能搜索快捷键 | 80 | | `ctrl + d` / `ctrl + y` | 复制/删除一行代码 | 81 | | `ctrl + shift + -` / `ctrl + shift + +` | 折叠/展开所有代码 | 82 | | `F2` | 快速定位到错误代码 | 83 | | `ctrl + alt + F7` | 查看哪些地方用到了指定的函数、类、方法 | 84 | 85 | > **说明**:使用macOS系统,可以将上面的`ctrl`键换成`command`键,在macOS系统上,可以使用`ctrl + space`组合键来获得万能提示,在Windows系统上不能使用该快捷键,因为它跟Windows默认的切换输入法的快捷键是冲突的,需要重新设置。 86 | 87 | ### hello, world 88 | 89 | 按照行业惯例,我们学习任何一门编程语言写的第一个程序都是输出`hello, world`,因为这段代码是伟大的丹尼斯·里奇(C语言之父,和肯·汤普森一起开发了Unix操作系统)和布莱恩·柯尼汉(awk语言的发明者)在他们的不朽著作*The C Programming Language*中写的第一段代码。 90 | 91 | ```Python 92 | print('hello, world') 93 | ``` 94 | 95 | ### 运行程序 96 | 97 | 如果不使用PyCharm这样的集成开发环境,我们可以将上面的代码命名为`hello.py`,对于Windows操作系统,可以在你保存代码的目录下先按住键盘上的`shift`键再点击鼠标右键,这时候鼠标右键菜单中会出现“命令提示符”选项,点击该选项就可以打开“命令提示符”工具,我们输入下面的命令。 98 | 99 | ```Shell 100 | python hello.py 101 | ``` 102 | 103 | > **提醒**:我们也可以在任意位置打开“命令提示符”,然后将需要执行的Python代码通过拖拽的方式拖入到“命令提示符”中,这样相当于指定了文件的绝对路径来运行该文件中的Python代码。再次提醒,macOS系统要通过`python3`命令来运行该程序。 104 | 105 | 你可以尝试将上面程序单引号中的`hello, world`换成其他内容;你也可以尝试着多写几个这样的语句,看看会运行出怎样的结果。需要提醒大家,上面代码中的`print('hello, world')`就是一条完整的语句,我们用Python写程序,最好每一行代码中只有一条语句。虽然使用`;`分隔符可以将多个语句写在一行代码中,但是最好不要这样做,因为代码会变得非常难看。 106 | 107 | ### 注释你的代码 108 | 109 | 注释是编程语言的一个重要组成部分,用于在源代码中解释代码的作用从而增强程序的可读性。当然,我们也可以将源代码中暂时不需要运行的代码段通过注释来去掉,这样当你需要重新使用这些代码的时候,去掉注释符号就可以了。简单的说,**注释会让代码更容易看懂但不会影响程序的执行结果**。 110 | 111 | Python中有两种形式的注释: 112 | 113 | 1. 单行注释:以`#`和空格开头,可以注释掉从`#`开始后面一整行的内容。 114 | 2. 多行注释:三个引号开头,三个引号结尾,通常用于添加多行说明性内容。 115 | 116 | ```Python 117 | """ 118 | 第一个Python程序 - hello, world 119 | 120 | Version: 0.1 121 | Author: 骆昊 122 | """ 123 | # print('hello, world') 124 | print("你好,世界!") 125 | ``` 126 | 127 | ### 总结 128 | 129 | 到这里,我们已经把第一个Python程序运行起来了,是不是很有成就感?只要你坚持学习下去,再过一段时间,我们就可以用Python制作小游戏、编写爬虫程序、完成办公自动化操作等。**写程序本身就是一件很酷的事情**,在未来编程就像英语一样,**对很多人来说或都是必须要掌握的技能**。 130 | 131 | -------------------------------------------------------------------------------- /第09课:常用数据结构之元组.md: -------------------------------------------------------------------------------- 1 | ## 第09课:常用数据结构之元组 2 | 3 | 上一节课为大家讲解了Python中的列表,它是一种容器型数据类型,我们可以通过定义列表类型的变量来保存和操作多个元素。当然,Python中容器型的数据类型肯定不止列表一种,接下来我们为大家讲解另一种重要的容器型数据类型,它的名字叫元组(tuple)。 4 | 5 | ### 定义和使用元组 6 | 7 | 在Python中,元组也是多个元素按照一定的顺序构成的序列。元组和列表的不同之处在于,元组是不可变类型,这就意味着元组类型的变量一旦定义,其中的元素不能再添加或删除,而且元素的值也不能进行修改。定义元组通常使用`()`字面量语法,也建议大家使用这种方式来创建元组。元组类型支持的运算符跟列表是一样。下面的代码演示了元组的定义和运算。 8 | 9 | ```Python 10 | # 定义一个三元组 11 | t1 = (30, 10, 55) 12 | # 定义一个四元组 13 | t2 = ('骆昊', 40, True, '四川成都') 14 | 15 | # 查看变量的类型 16 | print(type(t1), type(t2)) # 17 | # 查看元组中元素的数量 18 | print(len(t1), len(t2)) # 3 4 19 | 20 | # 通过索引运算获取元组中的元素 21 | print(t1[0], t1[-3]) # 30 30 22 | print(t2[3], t2[-1]) # 四川成都 四川成都 23 | 24 | # 循环遍历元组中的元素 25 | for member in t2: 26 | print(member) 27 | 28 | # 成员运算 29 | print(100 in t1) # False 30 | print(40 in t2) # True 31 | 32 | # 拼接 33 | t3 = t1 + t2 34 | print(t3) # (30, 10, 55, '骆昊', 40, True, '四川成都') 35 | 36 | # 切片 37 | print(t3[::3]) # (30, '骆昊', '四川成都') 38 | 39 | # 比较运算 40 | print(t1 == t3) # False 41 | print(t1 >= t3) # False 42 | print(t1 < (30, 11, 55)) # True 43 | ``` 44 | 45 | 一个元组中如果有两个元素,我们就称之为二元组;一个元组中如果五个元素,我们就称之为五元组。需要提醒大家注意的是,`()`表示空元组,但是如果元组中只有一个元素,需要加上一个逗号,否则`()`就不是代表元组的字面量语法,而是改变运算优先级的圆括号,所以`('hello', )`和`(100, )`才是一元组,而`('hello')`和`(100)`只是字符串和整数。我们可以通过下面的代码来加以验证。 46 | 47 | ```Python 48 | # 空元组 49 | a = () 50 | print(type(a)) # 51 | # 不是元组 52 | b = ('hello') 53 | print(type(b)) # 54 | c = (100) 55 | print(type(c)) # 56 | # 一元组 57 | d = ('hello', ) 58 | print(type(d)) # 59 | e = (100, ) 60 | print(type(e)) # 61 | ``` 62 | 63 | ### 元组的应用场景 64 | 65 | 讲到这里,相信大家一定迫切的想知道元组有哪些应用场景,我们给大家举几个例子。 66 | 67 | #### 例子1:打包和解包操作。 68 | 69 | 当我们把多个用逗号分隔的值赋给一个变量时,多个值会打包成一个元组类型;当我们把一个元组赋值给多个变量时,元组会解包成多个值然后分别赋给对应的变量,如下面的代码所示。 70 | 71 | ```Python 72 | # 打包 73 | a = 1, 10, 100 74 | print(type(a), a) # (1, 10, 100) 75 | # 解包 76 | i, j, k = a 77 | print(i, j, k) # 1 10 100 78 | ``` 79 | 80 | 在解包时,如果解包出来的元素个数和变量个数不对应,会引发`ValueError`异常,错误信息为:`too many values to unpack`(解包的值太多)或`not enough values to unpack`(解包的值不足)。 81 | 82 | ```Python 83 | a = 1, 10, 100, 1000 84 | # i, j, k = a # ValueError: too many values to unpack (expected 3) 85 | # i, j, k, l, m, n = a # ValueError: not enough values to unpack (expected 6, got 4) 86 | ``` 87 | 88 | 有一种解决变量个数少于元素的个数方法,就是使用星号表达式,我们之前讲函数的可变参数时使用过星号表达式。有了星号表达式,我们就可以让一个变量接收多个值,代码如下所示。需要注意的是,用星号表达式修饰的变量会变成一个列表,列表中有0个或多个元素。还有在解包语法中,星号表达式只能出现一次。 89 | 90 | ```Python 91 | a = 1, 10, 100, 1000 92 | i, j, *k = a 93 | print(i, j, k) # 1 10 [100, 1000] 94 | i, *j, k = a 95 | print(i, j, k) # 1 [10, 100] 1000 96 | *i, j, k = a 97 | print(i, j, k) # [1, 10] 100 1000 98 | *i, j = a 99 | print(i, j) # [1, 10, 100] 1000 100 | i, *j = a 101 | print(i, j) # 1 [10, 100, 1000] 102 | i, j, k, *l = a 103 | print(i, j, k, l) # 1 10 100 [1000] 104 | i, j, k, l, *m = a 105 | print(i, j, k, l, m) # 1 10 100 1000 [] 106 | ``` 107 | 108 | 需要说明一点,解包语法对所有的序列都成立,这就意味着对列表以及我们之前讲到的`range`函数返回的范围序列都可以使用解包语法。大家可以尝试运行下面的代码,看看会出现怎样的结果。 109 | 110 | ```Python 111 | a, b, *c = range(1, 10) 112 | print(a, b, c) 113 | a, b, c = [1, 10, 100] 114 | print(a, b, c) 115 | a, *b, c = 'hello' 116 | print(a, b, c) 117 | ``` 118 | 119 | #### 例子2:交换两个变量的值。 120 | 121 | 交换两个变量的值是编程语言中的一个经典案例,在很多编程语言中,交换两个变量的值都需要借助一个中间变量才能做到,如果不用中间变量就需要使用比较晦涩的位运算来实现。在Python中,交换两个变量`a`和`b`的值只需要使用如下所示的代码。 122 | 123 | ```Python 124 | a, b = b, a 125 | ``` 126 | 127 | 同理,如果要将三个变量`a`、`b`、`c`的值互换,即`b`赋给`a`,`c`赋给`b`,`a`赋给`c`,也可以如法炮制。 128 | 129 | ```Python 130 | a, b, c = b, c, a 131 | ``` 132 | 133 | 需要说明的是,上面并没有用到打包和解包语法,Python的字节码指令中有`ROT_TWO`和`ROT_THREE`这样的指令可以实现这个操作,效率是非常高的。但是如果有多于三个变量的值要依次互换,这个时候没有直接可用的字节码指令,执行的原理就是我们上面讲解的打包和解包操作。 134 | 135 | ### 元组和列表的比较 136 | 137 | 这里还有一个非常值得探讨的问题,Python中已经有了列表类型,为什么还需要元组这样的类型呢?这个问题对于初学者来说似乎有点困难,不过没有关系,我们先抛出观点,大家可以一边学习一边慢慢体会。 138 | 139 | 1. 元组是不可变类型,**不可变类型更适合多线程环境**,因为它降低了并发访问变量的同步化开销。关于这一点,我们会在后面讲解多线程的时候为大家详细论述。 140 | 141 | 2. 元组是不可变类型,通常**不可变类型在创建时间和占用空间上面都优于对应的可变类型**。我们可以使用`sys`模块的`getsizeof`函数来检查保存相同元素的元组和列表各自占用了多少内存空间。我们也可以使用`timeit`模块的`timeit`函数来看看创建保存相同元素的元组和列表各自花费的时间,代码如下所示。 142 | 143 | ```Python 144 | import sys 145 | import timeit 146 | 147 | a = list(range(100000)) 148 | b = tuple(range(100000)) 149 | print(sys.getsizeof(a), sys.getsizeof(b)) # 900120 800056 150 | 151 | print(timeit.timeit('[1, 2, 3, 4, 5, 6, 7, 8, 9]')) 152 | print(timeit.timeit('(1, 2, 3, 4, 5, 6, 7, 8, 9)')) 153 | ``` 154 | 155 | 3. Python中的元组和列表是可以相互转换的,我们可以通过下面的代码来做到。 156 | 157 | ```Python 158 | # 将元组转换成列表 159 | info = ('骆昊', 175, True, '四川成都') 160 | print(list(info)) # ['骆昊', 175, True, '四川成都'] 161 | # 将列表转换成元组 162 | fruits = ['apple', 'banana', 'orange'] 163 | print(tuple(fruits)) # ('apple', 'banana', 'orange') 164 | ``` 165 | 166 | ### 简单的总结 167 | 168 | **列表和元组都是容器型的数据类型**,即一个变量可以保存多个数据。**列表是可变数据类型**,**元组是不可变数据类型**,所以列表添加元素、删除元素、清空、排序等方法对于元组来说是不成立的。但是列表和元组都可以进行**拼接**、**成员运算**、**索引和切片**这些操作,后面我们要讲到的字符串类型也是这样,因为字符串就是字符按一定顺序构成的序列,在这一点上三者并没有什么区别。我们**推荐大家使用列表的生成式语法来创建列表**,它很好用,也是Python中非常有特色的语法。 169 | -------------------------------------------------------------------------------- /第45课.索引.md: -------------------------------------------------------------------------------- 1 | ## 第45课:索引 2 | 3 | 索引是关系型数据库中用来提升查询性能最为重要的手段。关系型数据库中的索引就像一本书的目录,我们可以想象一下,如果要从一本书中找出某个知识点,但是这本书没有目录,这将是一件多么可怕的事情!我们估计得一篇一篇的翻下去,才能确定这个知识点到底在什么位置。创建索引虽然会带来存储空间上的开销,就像一本书的目录会占用一部分篇幅一样,但是在牺牲空间后换来的查询时间的减少也是非常显著的。 4 | 5 | MySQL 数据库中所有数据类型的列都可以被索引。对于MySQL 8.0 版本的 InnoDB 存储引擎来说,它支持三种类型的索引,分别是 B+ 树索引、全文索引和 R 树索引。这里,我们只介绍使用得最为广泛的 B+ 树索引。使用 B+ 树的原因非常简单,因为它是目前在基于磁盘进行海量数据存储和排序上最有效率的数据结构。B+ 树是一棵[平衡树](https://zh.wikipedia.org/zh-cn/%E5%B9%B3%E8%A1%A1%E6%A0%91),树的高度通常为3或4,但是却可以保存从百万级到十亿级的数据,而从这些数据里面查询一条数据,只需要3次或4次 I/O 操作。 6 | 7 | B+ 树由根节点、中间节点和叶子节点构成,其中叶子节点用来保存排序后的数据。由于记录在索引上是排序过的,因此在一个叶子节点内查找数据时可以使用二分查找,这种查找方式效率非常的高。当数据很少的时候,B+ 树只有一个根节点,数据也就保存在根节点上。随着记录越来越多,B+ 树会发生分裂,根节点不再保存数据,而是提供了访问下一层节点的指针,帮助快速确定数据在哪个叶子节点上。 8 | 9 | 在创建二维表时,我们通常都会为表指定主键列,主键列上默认会创建索引,而对于 MySQL InnoDB 存储引擎来说,因为它使用的是索引组织表这种数据存储结构,所以主键上的索引就是整张表的数据,而这种索引我们也将其称之为**聚集索引**(clustered index)。很显然,一张表只能有一个聚集索引,否则表的数据岂不是要保存多次。我们自己创建的索引都是二级索引(secondary index),更常见的叫法是**非聚集索引**(non-clustered index)。通过我们自定义的非聚集索引只能定位记录的主键,在获取数据时可能需要再通过主键上的聚集索引进行查询,这种现象称为“回表”,因此通过非聚集索引检索数据通常比使用聚集索引检索数据要慢。 10 | 11 | 接下来我们通过一个简单的例子来说明索引的意义,比如我们要根据学生的姓名来查找学生,这个场景在实际开发中应该经常遇到,就跟通过商品名称查找商品是一个道理。我们可以使用 MySQL 的`explain`关键字来查看 SQL 的执行计划(数据库执行 SQL 语句的具体步骤)。 12 | 13 | ```SQL 14 | explain select * from tb_student where stuname='林震南'\G 15 | ``` 16 | 17 | ``` 18 | *************************** 1. row *************************** 19 | id: 1 20 | select_type: SIMPLE 21 | table: tb_student 22 | partitions: NULL 23 | type: ALL 24 | possible_keys: NULL 25 | key: NULL 26 | key_len: NULL 27 | ref: NULL 28 | rows: 11 29 | filtered: 10.00 30 | Extra: Using where 31 | 1 row in set, 1 warning (0.00 sec) 32 | ``` 33 | 34 | 在上面的 SQL 执行计划中,有几项值得我们关注: 35 | 36 | 1. `select_type`:查询的类型。 37 | - `SIMPLE`:简单 SELECT,不需要使用 UNION 操作或子查询。 38 | - `PRIMARY`:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。 39 | - `UNION`:UNION 操作中第二个或后面的 SELECT 语句。 40 | - `SUBQUERY`:子查询中的第一个 SELECT。 41 | - `DERIVED`:派生表的 SELECT 子查询。 42 | 2. `table`:查询对应的表。 43 | 3. `type`:MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:`ALL`(全表扫描)、`index`(索引全扫描,只遍历索引树)、`range`(索引范围扫描)、`ref`(非唯一索引扫描)、`eq_ref`(唯一索引扫描)、`const` / `system`(常量级查询)、`NULL`(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。 44 | 4. `possible_keys`:MySQL 可以选择的索引,但是**有可能不会使用**。 45 | 5. `key`:MySQL 真正使用的索引,如果为`NULL`就表示没有使用索引。 46 | 6. `key_len`:使用的索引的长度,在不影响查询的情况下肯定是长度越短越好。 47 | 7. `rows`:执行查询需要扫描的行数,这是一个**预估值**。 48 | 8. `extra`:关于查询额外的信息。 49 | - `Using filesort`:MySQL 无法利用索引完成排序操作。 50 | - `Using index`:只使用索引的信息而不需要进一步查表来获取更多的信息。 51 | - `Using temporary`:MySQL 需要使用临时表来存储结果集,常用于分组和排序。 52 | - `Impossible where`:`where`子句会导致没有符合条件的行。 53 | - `Distinct`:MySQL 发现第一个匹配行后,停止为当前的行组合搜索更多的行。 54 | - `Using where`:查询的列未被索引覆盖,筛选条件并不是索引的前导列。 55 | 56 | 从上面的执行计划可以看出,当我们通过学生名字查询学生时实际上是进行了全表扫描,不言而喻这个查询性能肯定是非常糟糕的,尤其是在表中的行很多的时候。如果我们需要经常通过学生姓名来查询学生,那么就应该在学生姓名对应的列上创建索引,通过索引来加速查询。 57 | 58 | ```SQL 59 | create index idx_student_name on tb_student(stuname); 60 | ``` 61 | 62 | 再次查看刚才的 SQL 对应的执行计划。 63 | 64 | ```SQL 65 | explain select * from tb_student where stuname='林震南'\G 66 | ``` 67 | 68 | ``` 69 | *************************** 1. row *************************** 70 | id: 1 71 | select_type: SIMPLE 72 | table: tb_student 73 | partitions: NULL 74 | type: ref 75 | possible_keys: idx_student_name 76 | key: idx_student_name 77 | key_len: 62 78 | ref: const 79 | rows: 1 80 | filtered: 100.00 81 | Extra: NULL 82 | 1 row in set, 1 warning (0.00 sec) 83 | ``` 84 | 85 | 可以注意到,在对学生姓名创建索引后,刚才的查询已经不是全表扫描而是基于索引的查询,而且扫描的行只有唯一的一行,这显然大大的提升了查询的性能。MySQL 中还允许创建前缀索引,即对索引字段的前N个字符创建索引,这样的话可以减少索引占用的空间(但节省了空间很有可能会浪费时间,**时间和空间是不可调和的矛盾**),如下所示。 86 | 87 | ```SQL 88 | create index idx_student_name_1 on tb_student(stuname(1)); 89 | ``` 90 | 91 | 上面的索引相当于是根据学生姓名的第一个字来创建的索引,我们再看看 SQL 执行计划。 92 | 93 | ```SQL 94 | explain select * from tb_student where stuname='林震南'\G 95 | ``` 96 | 97 | ``` 98 | *************************** 1. row *************************** 99 | id: 1 100 | select_type: SIMPLE 101 | table: tb_student 102 | partitions: NULL 103 | type: ref 104 | possible_keys: idx_student_name 105 | key: idx_student_name 106 | key_len: 5 107 | ref: const 108 | rows: 2 109 | filtered: 100.00 110 | Extra: Using where 111 | 1 row in set, 1 warning (0.00 sec) 112 | ``` 113 | 114 | 不知道大家是否注意到,这一次扫描的行变成了2行,因为学生表中有两个姓“林”的学生,我们只用姓名的第一个字作为索引的话,在查询时通过索引就会找到这两行。 115 | 116 | 如果要删除索引,可以使用下面的SQL。 117 | 118 | ```SQL 119 | alter table tb_student drop index idx_student_name; 120 | ``` 121 | 122 | 或者 123 | 124 | ```SQL 125 | drop index idx_student_name on tb_student; 126 | ``` 127 | 128 | 在创建索引时,我们还可以使用复合索引、函数索引(MySQL 5.7 开始支持),用好复合索引实现**索引覆盖**可以减少不必要的排序和回表操作,这样就会让查询的性能成倍的提升,有兴趣的读者可以自行研究。 129 | 130 | 我们简单的为大家总结一下索引的设计原则: 131 | 132 | 1. **最适合**索引的列是出现在**WHERE子句**和连接子句中的列。 133 | 2. 索引列的基数越大(取值多、重复值少),索引的效果就越好。 134 | 3. 使用**前缀索引**可以减少索引占用的空间,内存中可以缓存更多的索引。 135 | 4. **索引不是越多越好**,虽然索引加速了读操作(查询),但是写操作(增、删、改)都会变得更慢,因为数据的变化会导致索引的更新,就如同书籍章节的增删需要更新目录一样。 136 | 5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,提升索引的缓存效果。 137 | 138 | 最后,还有一点需要说明,InnoDB 使用的 B-tree 索引,数值类型的列除了等值判断时索引会生效之外,使用`>`、`<`、`>=`、`<=`、`BETWEEN...AND... `、`<>`时,索引仍然生效;对于字符串类型的列,如果使用不以通配符开头的模糊查询,索引也是起作用的,但是其他的情况会导致索引失效,这就意味着很有可能会做全表查询。 139 | -------------------------------------------------------------------------------- /第04课:Python语言元素之运算符.md: -------------------------------------------------------------------------------- 1 | ## 第04课:Python语言元素之运算符 2 | 3 | Python语言支持很多种运算符,我们先用一个表格为大家列出这些运算符,然后选择一些马上就会用到的运算符为大家进行讲解。 4 | 5 | | 运算符 | 描述 | 6 | | ------------------------------------------------------------ | ------------------------------ | 7 | | `[]` `[:]` | 下标,切片 | 8 | | `**` | 指数 | 9 | | `~` `+` `-` | 按位取反, 正负号 | 10 | | `*` `/` `%` `//` | 乘,除,模,整除 | 11 | | `+` `-` | 加,减 | 12 | | `>>` `<<` | 右移,左移 | 13 | | `&` | 按位与 | 14 | | `^` `\|` | 按位异或,按位或 | 15 | | `<=` `<` `>` `>=` | 小于等于,小于,大于,大于等于 | 16 | | `==` `!=` | 等于,不等于 | 17 | | `is` `is not` | 身份运算符 | 18 | | `in` `not in` | 成员运算符 | 19 | | `not` `or` `and` | 逻辑运算符 | 20 | | `=` `+=` `-=` `*=` `/=` `%=` `//=` `**=` `&=` `\|=` `^=` `>>=` `<<=` | (复合)赋值运算符 | 21 | 22 | >**说明:** 上面这个表格实际上是按照运算符的优先级从上到下列出了各种运算符。所谓优先级就是在一个运算的表达式中,如果出现了多个运算符,应该先执行哪个运算再执行哪个运算的顺序。在实际开发中,如果搞不清楚运算符的优先级,可以使用圆括号来确保运算的执行顺序。 23 | 24 | ### 算术运算符 25 | 26 | Python中的算术运算符非常丰富,除了大家最为熟悉的加减乘除之外,还有整除运算符、求模(求余数)运算符和求幂运算符。下面的例子为大家展示了算术运算符的使用。 27 | 28 | ```Python 29 | """ 30 | 算术运算符 31 | 32 | Version: 0.1 33 | Author: 骆昊 34 | """ 35 | print(321 + 123) # 加法运算 36 | print(321 - 123) # 减法运算 37 | print(321 * 123) # 乘法运算 38 | print(321 / 123) # 除法运算 39 | print(321 % 123) # 求模运算 40 | print(321 // 123) # 整除运算 41 | print(321 ** 123) # 求幂运算 42 | ``` 43 | 44 | ### 赋值运算符 45 | 46 | 赋值运算符应该是最为常见的运算符,它的作用是将右边的值赋给左边的变量。下面的例子演示了赋值运算符和复合赋值运算符的使用。 47 | 48 | ```Python 49 | """ 50 | 赋值运算符和复合赋值运算符 51 | 52 | Version: 0.1 53 | Author: 骆昊 54 | """ 55 | a = 10 56 | b = 3 57 | a += b # 相当于:a = a + b 58 | a *= a + 2 # 相当于:a = a * (a + 2) 59 | print(a) # 算一下这里会输出什么 60 | ``` 61 | 62 | ###比较运算符和逻辑运算符 63 | 64 | 比较运算符有的地方也称为关系运算符,包括`==`、`!=`、`<`、`>`、`<=`、`>=`,我相信没有什么好解释的,大家一看就能懂,需要提醒的是比较相等用的是`==`,请注意这里是两个等号,因为`=`是赋值运算符,我们在上面刚刚讲到过,`==`才是比较相等的运算符;比较不相等用的是`!=`,这不同于数学上的不等号,Python 2中曾经使用过`<>`来表示不等关系,大家知道就可以了。比较运算符会产生布尔值,要么是`True`要么是`False`。 65 | 66 | 逻辑运算符有三个,分别是`and`、`or`和`not`。`and`字面意思是“而且”,所以`and`运算符会连接两个布尔值,如果两个布尔值都是`True`,那么运算的结果就是`True`;左右两边的布尔值有一个是`False`,最终的运算结果就是`False`。相信大家已经想到了,如果`and`左边的布尔值是`False`,不管右边的布尔值是什么,最终的结果都是`False`,所以在做运算的时候右边的值会被跳过(短路处理),这也就意味着在`and`运算符左边为`False`的情况下,右边的表达式根本不会执行。`or`字面意思是“或者”,所以`or`运算符也会连接两个布尔值,如果两个布尔值有任意一个是`True`,那么最终的结果就是`True`。当然,`or`运算符也是有短路功能的,在它左边的布尔值为`True`的情况下,右边的表达式根本不会执行。`not`运算符的后面会跟上一个布尔值,它的作用是得到与该布尔值相反的值,也就是说,`not`后面的布尔值如果是`True`,运算结果就是`False`;而`not`后面的布尔值如果是`False`,运算结果就是`True`。 67 | 68 | ```Python 69 | """ 70 | 比较运算符和逻辑运算符的使用 71 | 72 | Version: 0.1 73 | Author: 骆昊 74 | """ 75 | flag0 = 1 == 1 76 | flag1 = 3 > 2 77 | flag2 = 2 < 1 78 | flag3 = flag1 and flag2 79 | flag4 = flag1 or flag2 80 | flag5 = not (1 != 2) 81 | print('flag0 =', flag0) # flag0 = True 82 | print('flag1 =', flag1) # flag1 = True 83 | print('flag2 =', flag2) # flag2 = False 84 | print('flag3 =', flag3) # flag3 = False 85 | print('flag4 =', flag4) # flag4 = True 86 | print('flag5 =', flag5) # flag5 = False 87 | ``` 88 | 89 | > **说明**:比较运算符的优先级高于赋值运算符,所以`flag0 = 1 == 1`先做`1 == 1`产生布尔值`True`,再将这个值赋值给变量`flag0`。`print`函数可以输出多个值,多个值之间可以用`,`进行分隔,输出的内容之间默认以空格分开。 90 | 91 | ### 运算符的例子 92 | 93 | #### 例子1:华氏温度转换为摄氏温度。 94 | 95 | > **提示**:华氏温度到摄氏温度的转换公式为:`C = (F - 32) / 1.8`。 96 | 97 | ```Python 98 | """ 99 | 将华氏温度转换为摄氏温度 100 | 101 | Version: 0.1 102 | Author: 骆昊 103 | """ 104 | f = float(input('请输入华氏温度: ')) 105 | c = (f - 32) / 1.8 106 | print('%.1f华氏度 = %.1f摄氏度' % (f, c)) 107 | ``` 108 | 109 | > **说明**:在使用`print`函数输出时,也可以对字符串内容进行格式化处理,上面`print`函数中的字符串`%.1f`是一个占位符,稍后会由一个`float`类型的变量值替换掉它。同理,如果字符串中有`%d`,后面可以用一个`int`类型的变量值替换掉它,而`%s`会被字符串的值替换掉。除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串,其中`{f:.1f}`和`{c:.1f}`可以先看成是`{f}`和`{c}`,表示输出时会用变量`f`和变量`c`的值替换掉这两个占位符,后面的`:.1f`表示这是一个浮点数,小数点后保留1位有效数字。 110 | > 111 | > ```Python 112 | > print(f'{f:.1f}华氏度 = {c:.1f}摄氏度') 113 | > ``` 114 | 115 | #### 例子2:输入圆的半径计算计算周长和面积。 116 | 117 | ```Python 118 | """ 119 | 输入半径计算圆的周长和面积 120 | 121 | Version: 0.1 122 | Author: 骆昊 123 | """ 124 | radius = float(input('请输入圆的半径: ')) 125 | perimeter = 2 * 3.1416 * radius 126 | area = 3.1416 * radius * radius 127 | print('周长: %.2f' % perimeter) 128 | print('面积: %.2f' % area) 129 | ``` 130 | 131 | #### 例子3:输入年份判断是不是闰年。 132 | 133 | ```Python 134 | """ 135 | 输入年份 如果是闰年输出True 否则输出False 136 | 137 | Version: 0.1 138 | Author: 骆昊 139 | """ 140 | year = int(input('请输入年份: ')) 141 | is_leap = year % 4 == 0 and year % 100 != 0 or year % 400 == 0 142 | print(is_leap) 143 | ``` 144 | 145 | > **说明**:比较运算符会产生布尔值,而逻辑运算符`and`和`or`会对这些布尔值进行组合,最终也是得到一个布尔值,闰年输出`True`,平年输出`False`。 146 | 147 | ### 总结 148 | 149 | 通过上面的例子相信大家感受到了,学会使用运算符以及由运算符构成的表达式,就可以帮助我们解决很多实际的问题,**运算符和表达式对于任何一门编程语言都是非常重要的**。 150 | -------------------------------------------------------------------------------- /第47课.MySQL新特性.md: -------------------------------------------------------------------------------- 1 | ## 第47课:MySQL 新特性 2 | 3 | #### JSON类型 4 | 5 | 很多开发者在使用关系型数据库做数据持久化的时候,常常感到结构化的存储缺乏灵活性,因为必须事先设计好所有的列以及对应的数据类型。在业务发展和变化的过程中,如果需要修改表结构,这绝对是比较麻烦和难受的事情。从 MySQL 5.7 版本开始,MySQL引入了对 JSON 数据类型的支持(MySQL 8.0 解决了 JSON 的日志性能瓶颈问题),用好 JSON 类型,其实就是打破了关系型数据库和非关系型数据库之间的界限,为数据持久化操作带来了更多的便捷。 6 | 7 | JSON 类型主要分为 JSON 对象和 JSON数组两种,如下所示。 8 | 9 | 1. JSON 对象 10 | 11 | ```JSON 12 | {"name": "骆昊", "tel": "13122335566", "QQ": "957658"} 13 | ``` 14 | 15 | 2. JSON 数组 16 | 17 | ```JSON 18 | [1, 2, 3] 19 | ``` 20 | 21 | ```JSON 22 | [{"name": "骆昊", "tel": "13122335566"}, {"name": "王大锤", "QQ": "123456"}] 23 | ``` 24 | 25 | 哪些地方需要用到JSON类型呢?举一个简单的例子,现在很多产品的用户登录都支持多种方式,例如手机号、微信、QQ、新浪微博等,但是一般情况下我们又不会要求用户提供所有的这些信息,那么用传统的设计方式,就需要设计多个列来对应多种登录方式,可能还需要允许这些列存在空值,这显然不是很好的选择;另一方面,如果产品又增加了一种登录方式,那么就必然要修改之前的表结构,这就更让人痛苦了。但是,有了 JSON 类型,刚才的问题就迎刃而解了,我们可以做出如下所示的设计。 26 | 27 | ```SQL 28 | create table `tb_test` 29 | ( 30 | `user_id` bigint unsigned, 31 | `login_info` json, 32 | primary key (`user_id`) 33 | ) engine=innodb; 34 | 35 | insert into `tb_test` values 36 | (1, '{"tel": "13122335566", "QQ": "654321", "wechat": "jackfrued"}'), 37 | (2, '{"tel": "13599876543", "weibo": "wangdachui123"}'); 38 | ``` 39 | 40 | 如果要查询用户的手机和微信号,可以用如下所示的 SQL 语句。 41 | 42 | ```SQL 43 | select 44 | `user_id`, 45 | json_unquote(json_extract(`login_info`, '$.tel')) as 手机号, 46 | json_unquote(json_extract(`login_info`, '$.wechat')) as 微信 47 | from `tb_test`; 48 | ``` 49 | 50 | ``` 51 | +---------+-------------+-----------+ 52 | | user_id | 手机号 | 微信 | 53 | +---------+-------------+-----------+ 54 | | 1 | 13122335566 | jackfrued | 55 | | 2 | 13599876543 | NULL | 56 | +---------+-------------+-----------+ 57 | ``` 58 | 59 | 因为支持 JSON 类型,MySQL 也提供了配套的处理 JSON 数据的函数,就像上面用到的`json_extract`和`json_unquote`。当然,上面的 SQL 还有更为便捷的写法,如下所示。 60 | 61 | ```SQL 62 | select 63 | `user_id`, 64 | `login_info` ->> '$.tel' as 手机号, 65 | `login_info` ->> '$.wechat' as 微信 66 | from `tb_test`; 67 | ``` 68 | 69 | 再举个例子,如果我们的产品要实现用户画像功能(给用户打标签),然后基于用户画像给用户推荐平台的服务或消费品之类的东西,我们也可以使用 JSON 类型来保存用户画像数据,示意代码如下所示。 70 | 71 | 创建画像标签表。 72 | 73 | ```SQL 74 | create table `tb_tags` 75 | ( 76 | `tag_id` int unsigned not null comment '标签ID', 77 | `tag_name` varchar(20) not null comment '标签名', 78 | primary key (`tag_id`) 79 | ) engine=innodb; 80 | 81 | insert into `tb_tags` (`tag_id`, `tag_name`) 82 | values 83 | (1, '70后'), 84 | (2, '80后'), 85 | (3, '90后'), 86 | (4, '00后'), 87 | (5, '爱运动'), 88 | (6, '高学历'), 89 | (7, '小资'), 90 | (8, '有房'), 91 | (9, '有车'), 92 | (10, '爱看电影'), 93 | (11, '爱网购'), 94 | (12, '常点外卖'); 95 | ``` 96 | 97 | 为用户打标签。 98 | 99 | ```SQL 100 | create table `tb_users_tags` 101 | ( 102 | `user_id` bigint unsigned not null comment '用户ID', 103 | `user_tags` json not null comment '用户标签' 104 | ) engine=innodb; 105 | 106 | insert into `tb_users_tags` values 107 | (1, '[2, 6, 8, 10]'), 108 | (2, '[3, 10, 12]'), 109 | (3, '[3, 8, 9, 11]'); 110 | ``` 111 | 112 | 接下来,我们通过一组查询来了解 JSON 类型的巧妙之处。 113 | 114 | 1. 查询爱看电影(有`10`这个标签)的用户ID。 115 | 116 | ```SQL 117 | select `user_id` from `tb_users_tags` where 10 member of (`user_tags`->'$'); 118 | ``` 119 | 120 | 2. 查询爱看电影(有`10`这个标签)的80后(有`2`这个标签)用户ID。 121 | 122 | ```SQL 123 | select `user_id` from `tb_users_tags` where json_contains(`user_tags`->'$', '[2, 10]'); 124 | ``` 125 | 126 | 3. 查询爱看电影或80后或90后的用户ID。 127 | 128 | ```SQL 129 | select `user_id` from `tb_users_tags` where json_overlaps(user_tags->'$', '[2, 3, 10]'); 130 | ``` 131 | 132 | > **说明**:上面的查询用到了`member of`谓词和两个 JSON 函数,`json_contains`可以检查 JSON 数组是否包含了指定的元素,而`json_overlaps`可以检查 JSON 数组是否与指定的数组有重叠部分。 133 | 134 | #### 窗口函数 135 | 136 | MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源数据库早已提供了对窗口函数的支持,有的也将其称之为 OLAP(联机分析和处理)函数,听名字就知道跟统计和分析相关。为了帮助大家理解窗口函数,我们先说说窗口的概念。 137 | 138 | 窗口可以理解为记录的集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数。窗口函数和我们上面讲到的聚合函数比较容易混淆,二者的区别主要在于聚合函数是将多条记录聚合为一条记录,窗口函数是每条记录都会执行,执行后记录条数不会变。窗口函数不仅仅是几个函数,它是一套完整的语法,函数只是该语法的一部分,基本语法如下所示: 139 | 140 | ```SQL 141 | <窗口函数> over (partition by <用于分组的列名> order by <用户排序的列名>) 142 | ``` 143 | 144 | 上面语法中,窗口函数的位置可以放以下两种函数: 145 | 146 | 1. 专用窗口函数,包括:`lead`、`lag`、`first_value`、`last_value`、`rank`、`dense_rank`和`row_number`等。 147 | 2. 聚合函数,包括:`sum`、`avg`、`max`、`min`和`count`等。 148 | 149 | 下面为大家举几个使用窗口函数的简单例子,我们直接使用上一课创建的 hrs 数据库。 150 | 151 | 例子1:查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。 152 | 153 | ```SQL 154 | select * from ( 155 | select 156 | `ename`, `sal`, 157 | row_number() over (order by `sal` desc) as `rank` 158 | from `tb_emp` 159 | ) `temp` where `rank` between 4 and 6; 160 | ``` 161 | 162 | 上面使用的函数`row_number()`可以为每条记录生成一个行号,在实际工作中可以根据需要将其替换为`rank()`或`dense_rank()`函数,三者的区别可以参考官方文档或阅读[《通俗易懂的学会:SQL窗口函数》](https://zhuanlan.zhihu.com/p/92654574)进行了解。在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。 163 | 164 | ```SQL 165 | select `rank`, `ename`, `sal` from ( 166 | select @a:=@a+1 as `rank`, `ename`, `sal` 167 | from `tb_emp`, (select @a:=0) as t1 order by `sal` desc 168 | ) as `temp` where `rank` between 4 and 6; 169 | ``` 170 | 171 | 例子2:查询每个部门月薪最高的两名的员工的姓名和部门名称。 172 | 173 | ```SQL 174 | select `ename`, `sal`, `dname` 175 | from ( 176 | select 177 | `ename`, `sal`, `dno`, 178 | rank() over (partition by `dno` order by `sal` desc) as `rank` 179 | from `tb_emp` 180 | ) as `temp` natural join `tb_dept` where `rank`<=2; 181 | ``` 182 | 183 | 说明:在MySQL 8以前的版本,我们可以通过下面的方式来完成类似的操作。 184 | 185 | ```SQL 186 | select `ename`, `sal`, `dname` from `tb_emp` as `t1` 187 | natural join `tb_dept` 188 | where ( 189 | select count(*) from `tb_emp` as `t2` 190 | where `t1`.`dno`=`t2`.`dno` and `t2`.`sal`>`t1`.`sal` 191 | )<2 order by `dno` asc, `sal` desc; 192 | ``` -------------------------------------------------------------------------------- /第37课:并发编程在爬虫中的应用.md: -------------------------------------------------------------------------------- 1 | ## 第37课:并发编程在爬虫中的应用 2 | 3 | 之前的课程,我们已经为大家介绍了 Python 中的多线程、多进程和异步编程,通过这三种手段,我们可以实现并发或并行编程,这一方面可以加速代码的执行,另一方面也可以带来更好的用户体验。爬虫程序是典型的 I/O 密集型任务,对于 I/O 密集型任务来说,多线程和异步 I/O 都是很好的选择,因为当程序的某个部分因 I/O 操作阻塞时,程序的其他部分仍然可以运转,这样我们不用在等待和阻塞中浪费大量的时间。下面我们以爬取“[360图片](https://image.so.com/)”网站的图片并保存到本地为例,为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别,同时也对它们的执行效率进行简单的对比。 4 | 5 | “360图片”网站的页面使用了 [Ajax](https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX) 技术,这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说,页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的,而且整个页面还使用了瀑布式加载(一边向下滚动,一边加载更多的图片)。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口,如下图所示,我们需要的图片信息就在服务器返回的 JSON 数据中。 6 | 7 | 8 | 9 | 例如,要获取“美女”频道的图片,我们可以请求如下所示的URL,其中参数`ch`表示请求的频道,`=`后面的参数值`beauty`就代表了“美女”频道,参数`sn`相当于是页码,`0`表示第一页(共`30`张图片),`30`表示第二页,`60`表示第三页,以此类推。 10 | 11 | ``` 12 | https://image.so.com/zjl?ch=beauty&sn=0 13 | ``` 14 | 15 | ### 单线程版本 16 | 17 | 通过上面的 URL 下载“美女”频道共`90`张图片。 18 | 19 | ```Python 20 | """ 21 | example04.py - 单线程版本爬虫 22 | """ 23 | import os 24 | 25 | import requests 26 | 27 | 28 | def download_picture(url): 29 | filename = url[url.rfind('/') + 1:] 30 | resp = requests.get(url) 31 | if resp.status_code == 200: 32 | with open(f'images/beauty/{filename}', 'wb') as file: 33 | file.write(resp.content) 34 | 35 | 36 | def main(): 37 | if not os.path.exists('images/beauty'): 38 | os.makedirs('images/beauty') 39 | for page in range(3): 40 | resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}') 41 | if resp.status_code == 200: 42 | pic_dict_list = resp.json()['list'] 43 | for pic_dict in pic_dict_list: 44 | download_picture(pic_dict['qhimg_url']) 45 | 46 | if __name__ == '__main__': 47 | main() 48 | ``` 49 | 50 | 在 macOS 或 Linux 系统上,我们可以使用`time`命令来了解上面代码的执行时间以及 CPU 的利用率,如下所示。 51 | 52 | ```Bash 53 | time python3 example04.py 54 | ``` 55 | 56 | 下面是单线程爬虫代码在我的电脑上执行的结果。 57 | 58 | ``` 59 | python3 example04.py 2.36s user 0.39s system 12% cpu 21.578 total 60 | ``` 61 | 62 | 这里我们只需要关注代码的总耗时为`21.578`秒,CPU 利用率为`12%`。 63 | 64 | ### 多线程版本 65 | 66 | 我们使用之前讲到过的线程池技术,将上面的代码修改为多线程版本。 67 | 68 | ```Python 69 | """ 70 | example05.py - 多线程版本爬虫 71 | """ 72 | import os 73 | from concurrent.futures import ThreadPoolExecutor 74 | 75 | import requests 76 | 77 | 78 | def download_picture(url): 79 | filename = url[url.rfind('/') + 1:] 80 | resp = requests.get(url) 81 | if resp.status_code == 200: 82 | with open(f'images/beauty/{filename}', 'wb') as file: 83 | file.write(resp.content) 84 | 85 | 86 | def main(): 87 | if not os.path.exists('images/beauty'): 88 | os.makedirs('images/beauty') 89 | with ThreadPoolExecutor(max_workers=16) as pool: 90 | for page in range(3): 91 | resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}') 92 | if resp.status_code == 200: 93 | pic_dict_list = resp.json()['list'] 94 | for pic_dict in pic_dict_list: 95 | pool.submit(download_picture, pic_dict['qhimg_url']) 96 | 97 | 98 | if __name__ == '__main__': 99 | main() 100 | ``` 101 | 102 | 执行如下所示的命令。 103 | 104 | ```Bash 105 | time python3 example05.py 106 | ``` 107 | 108 | 代码的执行结果如下所示: 109 | 110 | ``` 111 | python3 example05.py 2.65s user 0.40s system 95% cpu 3.193 total 112 | ``` 113 | 114 | ### 异步I/O版本 115 | 116 | 我们使用`aiohttp`将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库`aiohttp`和`aiofile`,命令如下所示。 117 | 118 | ```Bash 119 | pip install aiohttp aiofile 120 | ``` 121 | 122 | `aiohttp` 的用法在之前的课程中已经做过简要介绍,`aiofile`模块中的`async_open`函数跟 Python 内置函数`open`的用法大致相同,只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。 123 | 124 | ```Python 125 | """ 126 | example06.py - 异步I/O版本爬虫 127 | """ 128 | import asyncio 129 | import json 130 | import os 131 | 132 | import aiofile 133 | import aiohttp 134 | 135 | 136 | async def download_picture(session, url): 137 | filename = url[url.rfind('/') + 1:] 138 | async with session.get(url, ssl=False) as resp: 139 | if resp.status == 200: 140 | data = await resp.read() 141 | async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file: 142 | await file.write(data) 143 | 144 | 145 | async def fetch_json(): 146 | async with aiohttp.ClientSession() as session: 147 | for page in range(3): 148 | async with session.get( 149 | url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}', 150 | ssl=False 151 | ) as resp: 152 | if resp.status == 200: 153 | json_str = await resp.text() 154 | result = json.loads(json_str) 155 | for pic_dict in result['list']: 156 | await download_picture(session, pic_dict['qhimg_url']) 157 | 158 | 159 | def main(): 160 | if not os.path.exists('images/beauty'): 161 | os.makedirs('images/beauty') 162 | loop = asyncio.get_event_loop() 163 | loop.run_until_complete(fetch_json()) 164 | loop.close() 165 | 166 | 167 | if __name__ == '__main__': 168 | main() 169 | ``` 170 | 171 | 执行如下所示的命令。 172 | 173 | ```Bash 174 | time python3 example06.py 175 | ``` 176 | 177 | 代码的执行结果如下所示: 178 | 179 | ``` 180 | python3 example06.py 0.82s user 0.21s system 27% cpu 3.782 total 181 | ``` 182 | 183 | ### 总结 184 | 185 | 通过上面三段代码执行结果的比较,我们可以得出一个结论,使用多线程和异步 I/O 都可以改善爬虫程序的性能,因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上,而`time`命令的执行结果也告诉我们,单线程的代码 CPU 利用率仅仅只有`12%`,而多线程版本的 CPU 利用率则高达`95%`;单线程版本的爬虫执行时间约`21`秒,而多线程和异步 I/O 的版本仅执行了`3`秒钟。另外,在运行时间差别不大的情况下,多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源,这是因为多线程的调度和切换也需要花费 CPU 时间。至此,三种方式在 I/O 密集型任务上的优劣已经一目了然,当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢,那么使用多线程和异步 I/O 的优势将更为明显,有兴趣的读者可以自行试验。 186 | -------------------------------------------------------------------------------- /第25课:用Python读写Excel文件-2.md: -------------------------------------------------------------------------------- 1 | ## 第25课:用Python读写Excel文件-2 2 | 3 | ### Excel简介 4 | 5 | Excel是Microsoft(微软)为使用Windows和macOS操作系统开发的一款电子表格软件。Excel凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel也有很多竞品,例如Google Sheets、LibreOffice Calc、Numbers等,这些竞品基本上也能够兼容Excel,至少能够读写较新版本的Excel文件,当然这些不是我们讨论的重点。掌握用Python程序操作Excel文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出Excel文件都是特别常见的功能。 6 | 7 | 本章我们继续讲解基于另一个三方库`openpyxl`如何进行Excel文件操作,首先需要先安装它。 8 | 9 | ```Bash 10 | pip install openpyxl 11 | ``` 12 | 13 | `openpyxl`的优点在于,当我们打开一个Excel文件后,既可以对它进行读操作,又可以对它进行写操作,而且在操作的便捷性上是优于`xlwt`和`xlrd`的。此外,如果要进行样式编辑和公式计算,使用`openpyxl`也远比上一个章节我们讲解的方式更为简单,而且`openpyxl`还支持数据透视和插入图表等操作,功能非常强大。有一点需要再次强调,`openpyxl`并不支持操作Office 2007以前版本的Excel文件。 14 | 15 | ### 读取Excel文件 16 | 17 | 例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xlsx”的Excel文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。 18 | 19 | ```Python 20 | import datetime 21 | 22 | import openpyxl 23 | 24 | # 加载一个工作簿 ---> Workbook 25 | wb = openpyxl.load_workbook('阿里巴巴2020年股票数据.xlsx') 26 | # 获取工作表的名字 27 | print(wb.sheetnames) 28 | # 获取工作表 ---> Worksheet 29 | sheet = wb.worksheets[0] 30 | # 获得单元格的范围 31 | print(sheet.dimensions) 32 | # 获得行数和列数 33 | print(sheet.max_row, sheet.max_column) 34 | 35 | # 获取指定单元格的值 36 | print(sheet.cell(3, 3).value) 37 | print(sheet['C3'].value) 38 | print(sheet['G255'].value) 39 | 40 | # 获取多个单元格(嵌套元组) 41 | print(sheet['A2:C5']) 42 | 43 | # 读取所有单元格的数据 44 | for row_ch in range(2, sheet.max_row + 1): 45 | for col_ch in 'ABCDEFG': 46 | value = sheet[f'{col_ch}{row_ch}'].value 47 | if type(value) == datetime.datetime: 48 | print(value.strftime('%Y年%m月%d日'), end='\t') 49 | elif type(value) == int: 50 | print(f'{value:<10d}', end='\t') 51 | elif type(value) == float: 52 | print(f'{value:.4f}', end='\t') 53 | else: 54 | print(value, end='\t') 55 | print() 56 | ``` 57 | 58 | > **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xlsx”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。 59 | 60 | 需要提醒大家一点,`openpyxl`获取指定的单元格有两种方式,一种是通过`cell`方法,需要注意,该方法的行索引和列索引都是从`1`开始的,这是为了照顾用惯了Excel的人的习惯;另一种是通过索引运算,通过指定单元格的坐标,例如`C3`、`G255`,也可以取得对应的单元格,再通过单元格对象的`value`属性,就可以获取到单元格的值。通过上面的代码,相信大家还注意到了,可以通过类似`sheet['A2:C5']`或`sheet['A2':'C5']`这样的切片操作获取多个单元格,该操作将返回嵌套的元组,相当于获取到了多行多列。 61 | 62 | ### 写Excel文件 63 | 64 | 下面我们使用`openpyxl`来进行写Excel操作。 65 | 66 | ```Python 67 | import random 68 | 69 | import openpyxl 70 | 71 | # 第一步:创建工作簿(Workbook) 72 | wb = openpyxl.Workbook() 73 | 74 | # 第二步:添加工作表(Worksheet) 75 | sheet = wb.active 76 | sheet.title = '期末成绩' 77 | 78 | titles = ('姓名', '语文', '数学', '英语') 79 | for col_index, title in enumerate(titles): 80 | sheet.cell(1, col_index + 1, title) 81 | 82 | names = ('关羽', '张飞', '赵云', '马超', '黄忠') 83 | for row_index, name in enumerate(names): 84 | sheet.cell(row_index + 2, 1, name) 85 | for col_index in range(2, 5): 86 | sheet.cell(row_index + 2, col_index, random.randrange(50, 101)) 87 | 88 | # 第四步:保存工作簿 89 | wb.save('考试成绩表.xlsx') 90 | ``` 91 | 92 | #### 调整样式和公式计算 93 | 94 | 在使用`openpyxl`操作Excel时,如果要调整单元格的样式,可以直接通过单元格对象(`Cell`对象)的属性进行操作。单元格对象的属性包括字体(`font`)、对齐(`alignment`)、边框(`border`)等,具体的可以参考`openpyxl`的[官方文档](https://openpyxl.readthedocs.io/en/stable/index.html)。在使用`openpyxl`时,如果需要做公式计算,可以完全按照Excel中的操作方式来进行,具体的代码如下所示。 95 | 96 | ```Python 97 | import openpyxl 98 | from openpyxl.styles import Font, Alignment, Border, Side 99 | 100 | # 对齐方式 101 | alignment = Alignment(horizontal='center', vertical='center') 102 | # 边框线条 103 | side = Side(color='ff7f50', style='mediumDashed') 104 | 105 | wb = openpyxl.load_workbook('考试成绩表.xlsx') 106 | sheet = wb.worksheets[0] 107 | 108 | # 调整行高和列宽 109 | sheet.row_dimensions[1].height = 30 110 | sheet.column_dimensions['E'].width = 120 111 | 112 | sheet['E1'] = '平均分' 113 | # 设置字体 114 | sheet.cell(1, 5).font = Font(size=18, bold=True, color='ff1493', name='华文楷体') 115 | # 设置对齐方式 116 | sheet.cell(1, 5).alignment = alignment 117 | # 设置单元格边框 118 | sheet.cell(1, 5).border = Border(left=side, top=side, right=side, bottom=side) 119 | for i in range(2, 7): 120 | # 公式计算每个学生的平均分 121 | sheet[f'E{i}'] = f'=average(B{i}:D{i})' 122 | sheet.cell(i, 5).font = Font(size=12, color='4169e1', italic=True) 123 | sheet.cell(i, 5).alignment = alignment 124 | 125 | wb.save('考试成绩表.xlsx') 126 | ``` 127 | 128 | ### 生成统计图表 129 | 130 | 通过`openpyxl`库,可以直接向Excel中插入统计图表,具体的做法跟在Excel中插入图表大体一致。我们可以创建指定类型的图表对象,然后通过该对象的属性对图表进行设置。当然,最为重要的是为图表绑定数据,即横轴代表什么,纵轴代表什么,具体的数值是多少。最后,可以将图表对象添加到表单中,具体的代码如下所示。 131 | 132 | ```Python 133 | from openpyxl import Workbook 134 | from openpyxl.chart import BarChart, Reference 135 | 136 | wb = Workbook(write_only=True) 137 | sheet = wb.create_sheet() 138 | 139 | rows = [ 140 | ('类别', '销售A组', '销售B组'), 141 | ('手机', 40, 30), 142 | ('平板', 50, 60), 143 | ('笔记本', 80, 70), 144 | ('外围设备', 20, 10), 145 | ] 146 | 147 | # 向表单中添加行 148 | for row in rows: 149 | sheet.append(row) 150 | 151 | # 创建图表对象 152 | chart = BarChart() 153 | chart.type = 'col' 154 | chart.style = 10 155 | # 设置图表的标题 156 | chart.title = '销售统计图' 157 | # 设置图表纵轴的标题 158 | chart.y_axis.title = '销量' 159 | # 设置图表横轴的标题 160 | chart.x_axis.title = '商品类别' 161 | # 设置数据的范围 162 | data = Reference(sheet, min_col=2, min_row=1, max_row=5, max_col=3) 163 | # 设置分类的范围 164 | cats = Reference(sheet, min_col=1, min_row=2, max_row=5) 165 | # 给图表添加数据 166 | chart.add_data(data, titles_from_data=True) 167 | # 给图表设置分类 168 | chart.set_categories(cats) 169 | chart.shape = 4 170 | # 将图表添加到表单指定的单元格中 171 | sheet.add_chart(chart, 'A10') 172 | 173 | wb.save('demo.xlsx') 174 | ``` 175 | 176 | 运行上面的代码,打开生成的Excel文件,效果如下图所示。 177 | 178 | image-20210819235009026 179 | 180 | ### 简单的总结 181 | 182 | 掌握了Python程序操作Excel的方法,可以解决日常办公中很多繁琐的处理Excel电子表格工作,最常见就是将多个数据格式相同的Excel文件合并到一个文件以及从多个Excel文件或表单中提取指定的数据。如果数据体量较大或者处理数据的方式比较复杂,我们还是推荐大家使用Python数据分析神器之一的`pandas`库。 183 | -------------------------------------------------------------------------------- /第28课:用Python处理图像.md: -------------------------------------------------------------------------------- 1 | ## 第28课:用Python处理图像 2 | 3 | ### 入门知识 4 | 5 | 1. 颜色。如果你有使用颜料画画的经历,那么一定知道混合红、黄、蓝三种颜料可以得到其他的颜色,事实上这三种颜色就是美术中的三原色,它们是不能再分解的基本颜色。在计算机中,我们可以将红、绿、蓝三种色光以不同的比例叠加来组合成其他的颜色,因此这三种颜色就是色光三原色。在计算机系统中,我们通常会将一个颜色表示为一个RGB值或RGBA值(其中的A表示Alpha通道,它决定了透过这个图像的像素,也就是透明度)。 6 | 7 | | 名称 | RGB值 | 名称 | RGB值 | 8 | | :---------: | :-------------: | :----------: | :-----------: | 9 | | White(白) | (255, 255, 255) | Red(红) | (255, 0, 0) | 10 | | Green(绿) | (0, 255, 0) | Blue(蓝) | (0, 0, 255) | 11 | | Gray(灰) | (128, 128, 128) | Yellow(黄) | (255, 255, 0) | 12 | | Black(黑) | (0, 0, 0) | Purple(紫) | (128, 0, 128) | 13 | 14 | 2. 像素。对于一个由数字序列表示的图像来说,最小的单位就是图像上单一颜色的小方格,这些小方块都有一个明确的位置和被分配的色彩数值,而这些一小方格的颜色和位置决定了该图像最终呈现出来的样子,它们是不可分割的单位,我们通常称之为像素(pixel)。每一个图像都包含了一定量的像素,这些像素决定图像在屏幕上所呈现的大小,大家如果爱好拍照或者自拍,对像素这个词就不会陌生。 15 | 16 | ### 用Pillow处理图像 17 | 18 | Pillow是由从著名的Python图像处理库PIL发展出来的一个分支,通过Pillow可以实现图像压缩和图像处理等各种操作。可以使用下面的命令来安装Pillow。 19 | 20 | ```Shell 21 | pip install pillow 22 | ``` 23 | 24 | Pillow中最为重要的是`Image`类,可以通过`Image`模块的`open`函数来读取图像并获得`Image`类型的对象。 25 | 26 | 1. 读取和显示图像 27 | 28 | ```Python 29 | from PIL import Image 30 | 31 | # 读取图像获得Image对象 32 | image = Image.open('guido.jpg') 33 | # 通过Image对象的format属性获得图像的格式 34 | print(image.format) # JPEG 35 | # 通过Image对象的size属性获得图像的尺寸 36 | print(image.size) # (500, 750) 37 | # 通过Image对象的mode属性获取图像的模式 38 | print(image.mode) # RGB 39 | # 通过Image对象的show方法显示图像 40 | image.show() 41 | ``` 42 | 43 | 44 | 45 | 2. 剪裁图像 46 | 47 | ```Python 48 | # 通过Image对象的crop方法指定剪裁区域剪裁图像 49 | image.crop((80, 20, 310, 360)).show() 50 | ``` 51 | 52 | 53 | 54 | 3. 生成缩略图 55 | 56 | ```Python 57 | # 通过Image对象的thumbnail方法生成指定尺寸的缩略图 58 | image.thumbnail((128, 128)) 59 | image.show() 60 | ``` 61 | 62 | 63 | 64 | 4. 缩放和黏贴图像 65 | 66 | ```Python 67 | # 读取骆昊的照片获得Image对象 68 | luohao_image = Image.open('luohao.png') 69 | # 读取吉多的照片获得Image对象 70 | guido_image = Image.open('guido.jpg') 71 | # 从吉多的照片上剪裁出吉多的头 72 | guido_head = guido_image.crop((80, 20, 310, 360)) 73 | width, height = guido_head.size 74 | # 使用Image对象的resize方法修改图像的尺寸 75 | # 使用Image对象的paste方法将吉多的头粘贴到骆昊的照片上 76 | luohao_image.paste(guido_head.resize((int(width / 1.5), int(height / 1.5))), (172, 40)) 77 | luohao_image.show() 78 | ``` 79 | 80 | 81 | 82 | 5. 旋转和翻转 83 | 84 | ```Python 85 | image = Image.open('guido.jpg') 86 | # 使用Image对象的rotate方法实现图像的旋转 87 | image.rotate(45).show() 88 | # 使用Image对象的transpose方法实现图像翻转 89 | # Image.FLIP_LEFT_RIGHT - 水平翻转 90 | # Image.FLIP_TOP_BOTTOM - 垂直翻转 91 | image.transpose(Image.FLIP_TOP_BOTTOM).show() 92 | ``` 93 | 94 | 95 | 96 | 6. 操作像素 97 | 98 | ```Python 99 | for x in range(80, 310): 100 | for y in range(20, 360): 101 | # 通过Image对象的putpixel方法修改图像指定像素点 102 | image.putpixel((x, y), (128, 128, 128)) 103 | image.show() 104 | ``` 105 | 106 | 107 | 108 | 7. 滤镜效果 109 | 110 | ```Python 111 | from PIL import ImageFilter 112 | 113 | # 使用Image对象的filter方法对图像进行滤镜处理 114 | # ImageFilter模块包含了诸多预设的滤镜也可以自定义滤镜 115 | image.filter(ImageFilter.CONTOUR).show() 116 | ``` 117 | 118 | 119 | 120 | ### 使用Pillow绘图 121 | 122 | Pillow中有一个名为`ImageDraw`的模块,该模块的`Draw`函数会返回一个`ImageDraw`对象,通过`ImageDraw`对象的`arc`、`line`、`rectangle`、`ellipse`、`polygon`等方法,可以在图像上绘制出圆弧、线条、矩形、椭圆、多边形等形状,也可以通过该对象的`text`方法在图像上添加文字。 123 | 124 | 125 | 126 | 要绘制如上图所示的图像,完整的代码如下所示。 127 | 128 | ```Python 129 | import random 130 | 131 | from PIL import Image, ImageDraw, ImageFont 132 | 133 | 134 | def random_color(): 135 | """生成随机颜色""" 136 | red = random.randint(0, 255) 137 | green = random.randint(0, 255) 138 | blue = random.randint(0, 255) 139 | return red, green, blue 140 | 141 | 142 | width, height = 800, 600 143 | # 创建一个800*600的图像,背景色为白色 144 | image = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255)) 145 | # 创建一个ImageDraw对象 146 | drawer = ImageDraw.Draw(image) 147 | # 通过指定字体和大小获得ImageFont对象 148 | font = ImageFont.truetype('Kongxin.ttf', 32) 149 | # 通过ImageDraw对象的text方法绘制文字 150 | drawer.text((300, 50), 'Hello, world!', fill=(255, 0, 0), font=font) 151 | # 通过ImageDraw对象的line方法绘制两条对角直线 152 | drawer.line((0, 0, width, height), fill=(0, 0, 255), width=2) 153 | drawer.line((width, 0, 0, height), fill=(0, 0, 255), width=2) 154 | xy = width // 2 - 60, height // 2 - 60, width // 2 + 60, height // 2 + 60 155 | # 通过ImageDraw对象的rectangle方法绘制矩形 156 | drawer.rectangle(xy, outline=(255, 0, 0), width=2) 157 | # 通过ImageDraw对象的ellipse方法绘制椭圆 158 | for i in range(4): 159 | left, top, right, bottom = 150 + i * 120, 220, 310 + i * 120, 380 160 | drawer.ellipse((left, top, right, bottom), outline=random_color(), width=8) 161 | # 显示图像 162 | image.show() 163 | # 保存图像 164 | image.save('result.png') 165 | ``` 166 | 167 | > **注意**:上面代码中使用的字体文件需要根据自己准备,可以选择自己喜欢的字体文件并放置在代码目录下。 168 | 169 | ### 简单的总结 170 | 171 | 使用Python语言做开发,除了可以用Pillow来处理图像外,还可以使用更为强大的OpenCV库来完成图形图像的处理,OpenCV(**Open** Source **C**omputer **V**ision Library)是一个跨平台的计算机视觉库,可以用来开发实时图像处理、计算机视觉和模式识别程序。在我们的日常工作中,有很多繁琐乏味的任务其实都可以通过Python程序来处理,编程的目的就是让计算机帮助我们解决问题,减少重复乏味的劳动。通过本章节的学习,相信大家已经感受到了使用Python程序绘图P图的乐趣,其实Python能做的事情还远不止这些,继续你的学习吧。 172 | -------------------------------------------------------------------------------- /第42课.SQL详解之DML.md: -------------------------------------------------------------------------------- 1 | ## 第42课:SQL详解之DML 2 | 3 | 我们接着上一课中创建的学校选课系统数据库,为大家讲解 DML 的使用。DML 可以帮助将数据插入到二维表(`insert`操作)、从二维表删除数据(`delete`操作)以及更新二维表的数据(`update`操作)。在执行 DML 之前,我们先通过下面的`use`命令切换到`school`数据库。 4 | 5 | ```SQL 6 | use `school`; 7 | ``` 8 | 9 | ### insert操作 10 | 11 | 顾名思义,`insert`是用来插入行到二维表中的,插入的方式包括:插入完整的行、插入行的一部分、插入多行、插入查询的结果。我们通过如下所示的 SQL 向学院表中添加一个学院。 12 | 13 | ```SQL 14 | insert into `tb_college` values (default, '计算机学院', '学习计算机科学与技术的地方'); 15 | ``` 16 | 17 | 其中,由于学院表的主键是一个自增字段,因此上面的 SQL 中用`default`表示该列使用默认值,我们也可以使用下面的方式完成同样的操作。 18 | 19 | ```SQL 20 | insert into `tb_college` (`col_name`, `col_intro`) values ('计算机学院', '学习计算机科学与技术的地方'); 21 | ``` 22 | 23 | 我们推荐大家使用下面这种做法,指定为哪些字段赋值,这样做可以不按照建表时设定的字段顺序赋值,可以按照`values`前面的元组中给定的字段顺序为字段赋值,但是需要注意,除了允许为`null`和有默认值的字段外,其他的字段都必须要一一列出并在`values`后面的元组中为其赋值。如果希望一次性插入多条记录,我们可以在`values`后面跟上多个元组来实现批量插入,代码如下所示。 24 | 25 | ```SQL 26 | insert into `tb_college` 27 | (`col_name`, `col_intro`) 28 | values 29 | ('外国语学院', '学习歪果仁的语言的学院'), 30 | ('经济管理学院', '经世济民,治理国家;管理科学,兴国之道'), 31 | ('体育学院', '发展体育运动,增强人民体质'); 32 | ``` 33 | 34 | 在插入数据时,要注意主键是不能重复的,如果插入的数据与表中已有记录主键相同,那么`insert`操作将会产生 Duplicated Entry 的报错信息。再次提醒大家,如果`insert`操作省略了某些列,那么这些列要么有默认值,要么允许为`null`,否则也将产生错误。在业务系统中,为了让`insert`操作不影响其他操作(主要是后面要讲的`select`操作)的性能,可以在`insert`和`into`之间加一个`low_priority`来降低`insert`操作的优先级,这个做法也适用于下面要讲的`delete`和`update`操作。 35 | 36 | 假如有一张名为`tb_temp`的表中有`a`和`b`两个列,分别保存了学院的名称和学院的介绍,我们也可以通过查询操作获得`tb_temp`表的数据并插入到学院表中,如下所示,其中的`select`就是我们之前提到的 DQL,在下一课中会详细讲解。 37 | 38 | ```SQL 39 | insert into `tb_college` 40 | (`col_name`, `col_intro`) 41 | select `a`, `b` from `tb_temp`; 42 | ``` 43 | 44 | ### delete 操作 45 | 46 | 如果需要从表中删除数据,可以使用`delete`操作,它可以帮助我们删除指定行或所有行,例如我们要删除编号为`1`的学院,就可以使用如下所示的 SQL。 47 | 48 | ```SQL 49 | delete from `tb_college` where col_id=1; 50 | ``` 51 | 52 | 注意,上面的`delete`操作中的`where`子句是用来指定条件的,只有满足条件的行会被删除。如果我们不小心写出了下面的 SQL,就会删除学院表中所有的记录,这是相当危险的,在实际工作中通常也不会这么做。 53 | 54 | ```SQL 55 | delete from `tb_college`; 56 | ``` 57 | 58 | 需要说明的是,即便删除了所有的数据,`delete`操作不会删除表本身,也不会让 AUTO_INCREMENT 字段的值回到初始值。如果需要删除所有的数据而且让 AUTO_INCREMENT 字段回到初始值,可以使用`truncate table`执行截断表操作,`truncate`的本质是删除原来的表并重新创建一个表,它的速度其实更快,因为不需要逐行删除数据。但是请大家记住一点,用`truncate table`删除数据是非常危险的,因为它会删除所有的数据,而且由于原来的表已经被删除了,要想恢复误删除的数据也会变得极为困难。 59 | 60 | ### update 操作 61 | 62 | 如果要修改表中的数据,可以使用`update`操作,它可以用来删除指定的行或所有的行。例如,我们将学生表中的“杨过”修改为“杨逍”,这里我们假设“杨过”的学号为`1001`,代码如下所示。 63 | 64 | ```SQL 65 | update `tb_student` set `stu_name`='杨逍' where `stu_id`=1001; 66 | ``` 67 | 68 | 注意上面 SQL 中的`where`子句,我们使用学号作为条件筛选出对应的学生,然后通过前面的赋值操作将其姓名修改为“杨逍”。这里为什么不直接使用姓名作为筛选条件,那是因为学生表中可能有多个名为“杨过”的学生,如果使用 stu_name 作为筛选条件,那么我们的`update`操作有可能会一次更新多条数据,这显然不是我们想要看到的。还有一个需要注意的地方是`update`操作中的`set`关键字,因为 SQL 中的`=`并不表示赋值,而是判断相等的运算符,只有出现在`set` 关键字后面的`=`,才具备赋值的能力。 69 | 70 | 如果要同时修改学生的姓名和生日,我们可以对上面的`update`语句稍作修改,如下所示。 71 | 72 | ```SQL 73 | update `tb_student` set `stu_name`='杨逍', `stu_birth`='1975-12-29' where `stu_id`=1001; 74 | ``` 75 | 76 | `update`语句中也可以使用查询的方式获得数据并以此来更新指定的表数据,有兴趣的读者可以自行研究。在书写`update`语句时,通常都会有`where`子句,因为实际工作中几乎不太会用到更新全表的操作,这一点大家一定要注意。 77 | 78 | ### 完整的数据 79 | 80 | 下面我们给出完整的向 school 数据库的五张表中插入数据的 SQL。 81 | 82 | ```SQL 83 | use `school`; 84 | 85 | -- 插入学院数据 86 | insert into `tb_college` 87 | (`col_name`, `col_intro`) 88 | values 89 | ('计算机学院', '计算机学院1958年设立计算机专业,1981年建立计算机科学系,1998年设立计算机学院,2005年5月,为了进一步整合教学和科研资源,学校决定,计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系:计算机科学与技术系、物联网工程系、计算金融系;两个研究所:图象图形研究所、网络空间安全研究院(2015年成立);三个教学实验中心:计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。'), 90 | ('外国语学院', '外国语学院设有7个教学单位,6个文理兼收的本科专业;拥有1个一级学科博士授予点,3个二级学科博士授予点,5个一级学科硕士学位授权点,5个二级学科硕士学位授权点,5个硕士专业授权领域,同时还有2个硕士专业学位(MTI)专业;有教职员工210余人,其中教授、副教授80余人,教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。'), 91 | ('经济管理学院', '经济学院前身是创办于1905年的经济科;已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光,以及当代学者刘诗白等曾先后在此任教或学习。'); 92 | 93 | -- 插入学生数据 94 | insert into `tb_student` 95 | (`stu_id`, `stu_name`, `stu_sex`, `stu_birth`, `stu_addr`, `col_id`) 96 | values 97 | (1001, '杨过', 1, '1990-3-4', '湖南长沙', 1), 98 | (1002, '任我行', 1, '1992-2-2', '湖南长沙', 1), 99 | (1033, '王语嫣', 0, '1989-12-3', '四川成都', 1), 100 | (1572, '岳不群', 1, '1993-7-19', '陕西咸阳', 1), 101 | (1378, '纪嫣然', 0, '1995-8-12', '四川绵阳', 1), 102 | (1954, '林平之', 1, '1994-9-20', '福建莆田', 1), 103 | (2035, '东方不败', 1, '1988-6-30', null, 2), 104 | (3011, '林震南', 1, '1985-12-12', '福建莆田', 3), 105 | (3755, '项少龙', 1, '1993-1-25', '四川成都', 3), 106 | (3923, '杨不悔', 0, '1985-4-17', '四川成都', 3); 107 | 108 | -- 插入老师数据 109 | insert into `tb_teacher` 110 | (`tea_id`, `tea_name`, `tea_title`, `col_id`) 111 | values 112 | (1122, '张三丰', '教授', 1), 113 | (1133, '宋远桥', '副教授', 1), 114 | (1144, '杨逍', '副教授', 1), 115 | (2255, '范遥', '副教授', 2), 116 | (3366, '韦一笑', default, 3); 117 | 118 | -- 插入课程数据 119 | insert into `tb_course` 120 | (`cou_id`, `cou_name`, `cou_credit`, `tea_id`) 121 | values 122 | (1111, 'Python程序设计', 3, 1122), 123 | (2222, 'Web前端开发', 2, 1122), 124 | (3333, '操作系统', 4, 1122), 125 | (4444, '计算机网络', 2, 1133), 126 | (5555, '编译原理', 4, 1144), 127 | (6666, '算法和数据结构', 3, 1144), 128 | (7777, '经贸法语', 3, 2255), 129 | (8888, '成本会计', 2, 3366), 130 | (9999, '审计学', 3, 3366); 131 | 132 | -- 插入选课数据 133 | insert into `tb_record` 134 | (`stu_id`, `cou_id`, `sel_date`, `score`) 135 | values 136 | (1001, 1111, '2017-09-01', 95), 137 | (1001, 2222, '2017-09-01', 87.5), 138 | (1001, 3333, '2017-09-01', 100), 139 | (1001, 4444, '2018-09-03', null), 140 | (1001, 6666, '2017-09-02', 100), 141 | (1002, 1111, '2017-09-03', 65), 142 | (1002, 5555, '2017-09-01', 42), 143 | (1033, 1111, '2017-09-03', 92.5), 144 | (1033, 4444, '2017-09-01', 78), 145 | (1033, 5555, '2017-09-01', 82.5), 146 | (1572, 1111, '2017-09-02', 78), 147 | (1378, 1111, '2017-09-05', 82), 148 | (1378, 7777, '2017-09-02', 65.5), 149 | (2035, 7777, '2018-09-03', 88), 150 | (2035, 9999, '2019-09-02', null), 151 | (3755, 1111, '2019-09-02', null), 152 | (3755, 8888, '2019-09-02', null), 153 | (3755, 9999, '2017-09-01', 92); 154 | ``` 155 | 156 | > **注意**:上面的`insert`语句使用了批处理的方式来插入数据,这种做法插入数据的效率比较高。 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /第33课:用Python解析HTML页面.md: -------------------------------------------------------------------------------- 1 | ## 第33课:用Python解析HTML页面 2 | 3 | 在前面的课程中,我们讲到了使用`request`三方库获取网络资源,还介绍了一些前端的基础知识。接下来,我们继续探索如何解析 HTML 代码,从页面中提取出有用的信息。之前,我们尝试过用正则表达式的捕获组操作提取页面内容,但是写出一个正确的正则表达式也是一件让人头疼的事情。为了解决这个问题,我们得先深入的了解一下 HTML 页面的结构,并在此基础上研究另外的解析页面的方法。 4 | 5 | ### HTML 页面的结构 6 | 7 | 我们在浏览器中打开任意一个网站,然后通过鼠标右键菜单,选择“显示网页源代码”菜单项,就可以看到网页对应的 HTML 代码。 8 | 9 | ![](https://github.com/jackfrued/mypic/raw/master/20210822094218.png) 10 | 11 | 代码的第`1`行是文档类型声明,第`2`行的``标签是整个页面根标签的开始标签,最后一行是根标签的结束标签``。``标签下面有两个子标签``和``,放在``标签下的内容会显示在浏览器窗口中,这部分内容是网页的主体;放在``标签下的内容不会显示在浏览器窗口中,但是却包含了页面重要的元信息,通常称之为网页的头部。HTML 页面大致的代码结构如下所示。 12 | 13 | ```HTML 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | 标签、层叠样式表(CSS)、JavaScript 是构成 HTML 页面的三要素,其中标签用来承载页面要显示的内容,CSS 负责对页面的渲染,而 JavaScript 用来控制页面的交互式行为。要实现 HTML 页面的解析,可以使用 XPath 的语法,它原本是 XML 的一种查询语法,可以根据 HTML 标签的层次结构提取标签中的内容或标签属性;此外,也可以使用 CSS 选择器来定位页面元素,就跟用 CSS 渲染页面元素是同样的道理。 26 | 27 | ### XPath 解析 28 | 29 | XPath 是在 XML(eXtensible Markup Language)文档中查找信息的一种语法,XML 跟 HTML 类似也是一种用标签承载数据的标签语言,不同之处在于 XML 的标签是可扩展的,可以自定义的,而且 XML 对语法有更严格的要求。XPath 使用路径表达式来选取 XML 文档中的节点或者节点集,这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。下面我们通过一个例子来说明如何使用 XPath 对页面进行解析。 30 | 31 | ```XML 32 | 33 | 34 | 35 | Harry Potter 36 | 29.99 37 | 38 | 39 | Learning XML 40 | 39.95 41 | 42 | 43 | ``` 44 | 45 | 对于上面的 XML 文件,我们可以用如下所示的 XPath 语法获取文档中的节点。 46 | 47 | | 路径表达式 | 结果 | 48 | | --------------- | ------------------------------------------------------------ | 49 | | `/bookstore` | 选取根元素 bookstore。**注意**:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! | 50 | | `//book` | 选取所有 book 子元素,而不管它们在文档中的位置。 | 51 | | `//@lang` | 选取名为 lang 的所有属性。 | 52 | | `/bookstore/book[1]` | 选取属于 bookstore 子元素的第一个 book 元素。 | 53 | | `/bookstore/book[last()]` | 选取属于 bookstore 子元素的最后一个 book 元素。 | 54 | | `/bookstore/book[last()-1]` | 选取属于 bookstore 子元素的倒数第二个 book 元素。 | 55 | | `/bookstore/book[position()<3]` | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 | 56 | | `//title[@lang]` | 选取所有拥有名为 lang 的属性的 title 元素。 | 57 | | `//title[@lang='eng']` | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 | 58 | | `/bookstore/book[price>35.00]` | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 | 59 | | `/bookstore/book[price>35.00]/title` | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 | 60 | 61 | XPath还支持通配符用法,如下所示。 62 | 63 | | 路径表达式 | 结果 | 64 | | -------------- | --------------------------------- | 65 | | `/bookstore/*` | 选取 bookstore 元素的所有子元素。 | 66 | | `//*` | 选取文档中的所有元素。 | 67 | | `//title[@*]` | 选取所有带有属性的 title 元素。 | 68 | 69 | 如果要选取多个节点,可以使用如下所示的方法。 70 | 71 | | 路径表达式 | 结果 | 72 | | ---------------------------------- | ------------------------------------------------------------ | 73 | | `//book/title \| //book/price` | 选取 book 元素的所有 title 和 price 元素。 | 74 | | `//title \| //price` | 选取文档中的所有 title 和 price 元素。 | 75 | | `/bookstore/book/title \| //price` | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 | 76 | 77 | > **说明**:上面的例子来自于“菜鸟教程”网站上的 [XPath 教程](),有兴趣的读者可以自行阅读原文。 78 | 79 | 当然,如果不理解或不熟悉 XPath 语法,可以在浏览器的开发者工具中按照如下所示的方法查看元素的 XPath 语法,下图是在 Chrome 浏览器的开发者工具中查看豆瓣网电影详情信息中影片标题的 XPath 语法。 80 | 81 | ![](https://github.com/jackfrued/mypic/raw/master/20210822093707.png) 82 | 83 | 实现 XPath 解析需要三方库`lxml` 的支持,可以使用下面的命令安装`lxml`。 84 | 85 | ```Bash 86 | pip install lxml 87 | ``` 88 | 89 | 下面我们用 XPath 解析方式改写之前获取豆瓣电影 Top250的代码,如下所示。 90 | 91 | ```Python 92 | from lxml import etree 93 | import requests 94 | 95 | for page in range(1, 11): 96 | resp = requests.get( 97 | url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', 98 | headers={'User-Agent': 'BaiduSpider'} 99 | ) 100 | tree = etree.HTML(resp.text) 101 | # 通过XPath语法从页面中提取电影标题 102 | title_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]') 103 | # 通过XPath语法从页面中提取电影评分 104 | rank_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]') 105 | for title_span, rank_span in zip(title_spans, rank_spans): 106 | print(title_span.text, rank_span.text) 107 | ``` 108 | 109 | ### CSS 选择器解析 110 | 111 | 对于熟悉 CSS 选择器和 JavaScript 的开发者来说,通过 CSS 选择器获取页面元素可能是更为简单的选择,因为浏览器中运行的 JavaScript 本身就可以`document`对象的`querySelector()`和`querySelectorAll()`方法基于 CSS 选择器获取页面元素。在 Python 中,我们可以利用三方库`beautifulsoup4`或`pyquery`来做同样的事情。Beautiful Soup 可以用来解析 HTML 和 XML 文档,修复含有未闭合标签等错误的文档,通过为待解析的页面在内存中创建一棵树结构,实现对从页面中提取数据操作的封装。可以用下面的命令来安装 Beautiful Soup。 112 | 113 | ```Python 114 | pip install beautifulsoup4 115 | ``` 116 | 117 | 下面是使用`bs4`改写的获取豆瓣电影Top250电影名称的代码。 118 | 119 | ```Python 120 | import bs4 121 | import requests 122 | 123 | for page in range(1, 11): 124 | resp = requests.get( 125 | url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', 126 | headers={'User-Agent': 'BaiduSpider'} 127 | ) 128 | # 创建BeautifulSoup对象 129 | soup = bs4.BeautifulSoup(resp.text, 'lxml') 130 | # 通过CSS选择器从页面中提取包含电影标题的span标签 131 | title_spans = soup.select('div.info > div.hd > a > span:nth-child(1)') 132 | # 通过CSS选择器从页面中提取包含电影评分的span标签 133 | rank_spans = soup.select('div.info > div.bd > div > span.rating_num') 134 | for title_span, rank_span in zip(title_spans, rank_spans): 135 | print(title_span.text, rank_span.text) 136 | ``` 137 | 138 | 关于 BeautifulSoup 更多的知识,可以参考它的[官方文档](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)。 139 | 140 | ### 简单的总结 141 | 142 | 下面我们对三种解析方式做一个简单比较。 143 | 144 | | 解析方式 | 对应的模块 | 速度 | 使用难度 | 145 | | -------------- | ---------------- | ------ | -------- | 146 | | 正则表达式解析 | `re` | 快 | 困难 | 147 | | XPath 解析 | `lxml` | 快 | 一般 | 148 | | CSS 选择器解析 | `bs4`或`pyquery` | 不确定 | 简单 | 149 | 150 | -------------------------------------------------------------------------------- /第11课:常用数据结构之集合.md: -------------------------------------------------------------------------------- 1 | ## 第11课:常用数据结构之集合 2 | 3 | 在学习了列表和元组之后,我们再来学习一种容器型的数据类型,它的名字叫集合(set)。说到集合这个词大家一定不会陌生,在数学课本上就有这个概念。通常我们对集合的定义是“**把一定范围的、确定的、可以区别的事物当作一个整体来看待**”,集合中的各个事物通常称为集合的**元素**。集合应该满足以下特性: 4 | 5 | 1. **无序性**:一个集合中,每个元素的地位都是相同的,元素之间是无序的。 6 | 2. **互异性**:一个集合中,任何两个元素都是不相同的,即元素在集合中只能出现一次。 7 | 3. **确定性**:给定一个集合和一个任意元素,该元素要么属这个集合,要么不属于这个集合,二者必居其一,不允许有模棱两可的情况出现。 8 | 9 | Python程序中的集合跟数学上的集合是完全一致的,需要强调的是上面所说的无序性和互异性。无序性说明集合中的元素并不像列中的元素那样一个挨着一个,可以通过索引实现随机访问(随机访问指的是给定一个有效的范围,随机抽取出一个数字,然后通过这个数字可以获取到对应的元素),所以Python中的**集合肯定不能够支持索引运算**。另外,集合的互异性决定了**集合中不能有重复元素**,这一点也是集合区别于列表的关键,说得更直白一些就是,Python中的集合类型会对其中的元素做去重处理。Python中的集合一定是支持`in`和`not in`成员运算的,这样就可以确定一个元素是否属于集合,也就是上面所说的集合的确定性。**集合的成员运算在性能上要优于列表的成员运算**,这是集合的底层存储特性(哈希存储)决定的,此处我们暂时不做讨论,大家可以先记下这个结论。 10 | 11 | ### 创建集合 12 | 13 | 在Python中,创建集合可以使用`{}`字面量语法,`{}`中需要至少有一个元素,因为没有元素的`{}`并不是空集合而是一个空字典,我们下一节课就会大家介绍字典的知识。当然,也可以使用内置函数`set`来创建一个集合,准确的说`set`并不是一个函数,而是创建集合对象的构造器,这个知识点我们很快也会讲到,现在不理解跳过它就可以了。要创建空集合可以使用`set()`;也可以将其他序列转换成集合,例如:`set('hello')`会得到一个包含了4个字符的集合(重复的`l`会被去掉)。除了这两种方式,我们还可以使用生成式语法来创建集合,就像我们之前用生成式创建列表那样。要知道集合中有多少个元素,还是使用内置函数`len`;使用`for`循环可以实现对集合元素的遍历。 14 | 15 | ```Python 16 | # 创建集合的字面量语法(重复元素不会出现在集合中) 17 | set1 = {1, 2, 3, 3, 3, 2} 18 | print(set1) # {1, 2, 3} 19 | print(len(set1)) # 3 20 | 21 | # 创建集合的构造器语法(后面会讲到什么是构造器) 22 | set2 = set('hello') 23 | print(set2) # {'h', 'l', 'o', 'e'} 24 | 25 | # 将列表转换成集合(可以去掉列表中的重复元素) 26 | set3 = set([1, 2, 3, 3, 2, 1]) 27 | print(set3) # {1, 2, 3} 28 | 29 | # 创建集合的生成式语法(将列表生成式的[]换成{}) 30 | set4 = {num for num in range(1, 20) if num % 3 == 0 or num % 5 == 0} 31 | print(set4) # {3, 5, 6, 9, 10, 12, 15, 18} 32 | 33 | # 集合元素的循环遍历 34 | for elem in set4: 35 | print(elem) 36 | ``` 37 | 38 | 需要提醒大家,集合中的元素必须是`hashable`类型。所谓`hashable`类型指的是能够计算出哈希码的数据类型,大家可以暂时将哈希码理解为和变量对应的唯一的ID值。通常不可变类型都是`hashable`类型,如整数、浮点、字符串、元组等,而可变类型都不是`hashable`类型,因为可变类型无法确定唯一的ID值,所以也就不能放到集合中。集合本身也是可变类型,所以集合不能够作为集合中的元素,这一点在使用集合的时候一定要注意。 39 | 40 | ### 集合的运算 41 | 42 | Python为集合类型提供了非常丰富的运算符,主要包括:成员运算、交集运算、并集运算、差集运算、比较运算(相等性、子集、超集)等。 43 | 44 | #### 成员运算 45 | 46 | 可以通过成员运算`in`和`not in `检查元素是否在集合中,代码如下所示。 47 | 48 | ```Python 49 | set1 = {11, 12, 13, 14, 15} 50 | print(10 in set1) # False 51 | print(15 in set1) # True 52 | set2 = {'Python', 'Java', 'Go', 'Swift'} 53 | print('Ruby' in set2) # False 54 | print('Java' in set2) # True 55 | ``` 56 | 57 | #### 交并差运算 58 | 59 | Python中的集合跟数学上的集合一样,可以进行交集、并集、差集等运算,而且可以通过运算符和方法调用两种方式来进行操作,代码如下所示。 60 | 61 | ```Python 62 | set1 = {1, 2, 3, 4, 5, 6, 7} 63 | set2 = {2, 4, 6, 8, 10} 64 | 65 | # 交集 66 | # 方法一: 使用 & 运算符 67 | print(set1 & set2) # {2, 4, 6} 68 | # 方法二: 使用intersection方法 69 | print(set1.intersection(set2)) # {2, 4, 6} 70 | 71 | # 并集 72 | # 方法一: 使用 | 运算符 73 | print(set1 | set2) # {1, 2, 3, 4, 5, 6, 7, 8, 10} 74 | # 方法二: 使用union方法 75 | print(set1.union(set2)) # {1, 2, 3, 4, 5, 6, 7, 8, 10} 76 | 77 | # 差集 78 | # 方法一: 使用 - 运算符 79 | print(set1 - set2) # {1, 3, 5, 7} 80 | # 方法二: 使用difference方法 81 | print(set1.difference(set2)) # {1, 3, 5, 7} 82 | 83 | # 对称差 84 | # 方法一: 使用 ^ 运算符 85 | print(set1 ^ set2) # {1, 3, 5, 7, 8, 10} 86 | # 方法二: 使用symmetric_difference方法 87 | print(set1.symmetric_difference(set2)) # {1, 3, 5, 7, 8, 10} 88 | # 方法三: 对称差相当于两个集合的并集减去交集 89 | print((set1 | set2) - (set1 & set2)) # {1, 3, 5, 7, 8, 10} 90 | ``` 91 | 92 | 通过上面的代码可以看出,对两个集合求交集,`&`运算符和`intersection`方法的作用是完全相同的,使用运算符的方式更直观而且代码也比较简短。相信大家对交集、并集、差集、对称差这几个概念是比较清楚的,如果没什么印象了可以看看下面的图。 93 | 94 | 95 | 96 | 集合的交集、并集、差集运算还可以跟赋值运算一起构成复合赋值运算,如下所示。 97 | 98 | ```Python 99 | set1 = {1, 3, 5, 7} 100 | set2 = {2, 4, 6} 101 | # 将set1和set2求并集再赋值给set1 102 | # 也可以通过set1.update(set2)来实现 103 | set1 |= set2 104 | print(set1) # {1, 2, 3, 4, 5, 6, 7} 105 | set3 = {3, 6, 9} 106 | # 将set1和set3求交集再赋值给set1 107 | # 也可以通过set1.intersection_update(set3)来实现 108 | set1 &= set3 109 | print(set1) # {3, 6} 110 | ``` 111 | 112 | #### 比较运算 113 | 114 | 两个集合可以用`==`和`!=`进行相等性判断,如果两个集合中的元素完全相同,那么`==`比较的结果就是`True`,否则就是`False`。如果集合`A`的任意一个元素都是集合`B`的元素,那么集合`A`称为集合`B`的子集,即对于 $ \forall{a} \in {A}$ ,均有 $ {a} \in {B} $ ,则 $ {A} \subseteq {B} $ ,`A`是`B`的子集,反过来也可以称`B`是`A`的超集。如果`A`是`B`的子集且`A`不等于`B`,那么`A`就是`B`的真子集。Python为集合类型提供了判断子集和超集的运算符,其实就是我们非常熟悉的`<`和`>`运算符,代码如下所示。 115 | 116 | ```Python 117 | set1 = {1, 3, 5} 118 | set2 = {1, 2, 3, 4, 5} 119 | set3 = set2 120 | # <运算符表示真子集,<=运算符表示子集 121 | print(set1 < set2, set1 <= set2) # True True 122 | print(set2 < set3, set2 <= set3) # False True 123 | # 通过issubset方法也能进行子集判断 124 | print(set1.issubset(set2)) # True 125 | 126 | # 反过来可以用issuperset或>运算符进行超集判断 127 | print(set2.issuperset(set1)) # True 128 | print(set2 > set1) # True 129 | ``` 130 | 131 | 132 | ### 集合的方法 133 | 134 | Python中的集合是可变类型,我们可以通过集合类型的方法为集合添加或删除元素。 135 | 136 | ```Python 137 | # 创建一个空集合 138 | set1 = set() 139 | 140 | # 通过add方法添加元素 141 | set1.add(33) 142 | set1.add(55) 143 | set1.update({1, 10, 100, 1000}) 144 | print(set1) # {33, 1, 100, 55, 1000, 10} 145 | 146 | # 通过discard方法删除指定元素 147 | set1.discard(100) 148 | set1.discard(99) 149 | print(set1) # {1, 10, 33, 55, 1000} 150 | 151 | # 通过remove方法删除指定元素,建议先做成员运算再删除 152 | # 否则元素如果不在集合中就会引发KeyError异常 153 | if 10 in set1: 154 | set1.remove(10) 155 | print(set1) # {33, 1, 55, 1000} 156 | 157 | # pop方法可以从集合中随机删除一个元素并返回该元素 158 | print(set1.pop()) 159 | 160 | # clear方法可以清空整个集合 161 | set1.clear() 162 | 163 | print(set1) # set() 164 | ``` 165 | 166 | 如果要判断两个集合有没有相同的元素可以使用`isdisjoint`方法,没有相同元素返回`True`,否则返回`False`,代码如下所示。 167 | 168 | ```Python 169 | set1 = {'Java', 'Python', 'Go', 'Kotlin'} 170 | set2 = {'Kotlin', 'Swift', 'Java', 'Objective-C', 'Dart'} 171 | set3 = {'HTML', 'CSS', 'JavaScript'} 172 | print(set1.isdisjoint(set2)) # False 173 | print(set1.isdisjoint(set3)) # True 174 | ``` 175 | 176 | ### 不可变集合 177 | 178 | Python中还有一种不可变类型的集合,名字叫`frozenset`。`set`跟`frozenset`的区别就如同`list`跟`tuple`的区别,`frozenset`由于是不可变类型,能够计算出哈希码,因此它可以作为`set`中的元素。除了不能添加和删除元素,`frozenset`在其他方面跟`set`基本是一样的,下面的代码简单的展示了`frozenset`的用法。 179 | 180 | ```Python 181 | set1 = frozenset({1, 3, 5, 7}) 182 | set2 = frozenset(range(1, 6)) 183 | print(set1 & set2) # frozenset({1, 3, 5}) 184 | print(set1 | set2) # frozenset({1, 2, 3, 4, 5, 7}) 185 | print(set1 - set2) # frozenset({7}) 186 | print(set1 < set2) # False 187 | ``` 188 | 189 | ### 简单的总结 190 | 191 | Python中的集合底层使用了**哈希存储**的方式,对于这一点我们暂时不做介绍,在后面的课程有需要的时候再为大家讲解集合的底层原理,现阶段大家只需要知道**集合是一种容器**,元素必须是`hashable`类型,与列表不同的地方在于集合中的元素**没有序**、**不能用索引运算**、**不能重复**。 192 | -------------------------------------------------------------------------------- /第32课:用Python获取网络资源.md: -------------------------------------------------------------------------------- 1 | ## 第32课:用Python获取网络数据 2 | 3 | 网络数据采集是 Python 语言非常擅长的领域,上节课我们讲到,实现网络数据采集的程序通常称之为网络爬虫或蜘蛛程序。即便是在大数据时代,数据对于中小企业来说仍然是硬伤和短板,有些数据需要通过开放或付费的数据接口来获得,其他的行业数据和竞对数据则必须要通过网络数据采集的方式来获得。不管使用哪种方式获取网络数据资源,Python 语言都是非常好的选择,因为 Python 的标准库和三方库都对网络数据采集提供了良好的支持。 4 | 5 | ### requests库 6 | 7 | 要使用 Python 获取网络数据,我们推荐大家使用名为`requests` 的三方库,这个库我们在之前的课程中其实已经使用过了。按照官方网站的解释,`requests`是基于 Python 标准库进行了封装,简化了通过 HTTP 或 HTTPS 访问网络资源的操作。上课我们提到过,HTTP 是一个请求响应式的协议,当我们在浏览器中输入正确的 [URL](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL)(通常也称为网址)并按下 Enter 键时,我们就向网络上的 [Web 服务器](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_web_server)发送了一个 HTTP 请求,服务器在收到请求后会给我们一个 HTTP 响应。在 Chrome 浏览器中的菜单中打开“开发者工具”切换到“Network”选项卡就能够查看 HTTP 请求和响应到底是什么样子的,如下图所示。 8 | 9 | ![](http://localhost/mypic/20210822093434.png) 10 | 11 | 通过`requests`库,我们可以让 Python 程序向浏览器一样向 Web 服务器发起请求,并接收服务器返回的响应,从响应中我们就可以提取出想要的数据。浏览器呈现给我们的网页是用 [HTML](https://developer.mozilla.org/zh-CN/docs/Web/HTML) 编写的,浏览器相当于是 HTML 的解释器环境,我们看到的网页中的内容都包含在 HTML 的标签中。在获取到 HTML 代码后,就可以从标签的属性或标签体中提取内容。下面例子演示了如何获取网页 HTML 代码,我们通过`requests`库的`get`函数,获取了搜狐首页的代码。 12 | 13 | ```Python 14 | import requests 15 | 16 | resp = requests.get('https://www.sohu.com/') 17 | if resp.status_code == 200: 18 | print(resp.text) 19 | ``` 20 | 21 | > **说明**:上面代码中的变量`resp`是一个`Response`对象(`requests`库封装的类型),通过该对象的`status_code`属性可以获取响应状态码,而该对象的`text`属性可以帮我们获取到页面的 HTML 代码。 22 | 23 | 由于`Response`对象的`text`是一个字符串,所以我们可以利用之前讲过的正则表达式的知识,从页面的 HTML 代码中提取新闻的标题和链接,代码如下所示。 24 | 25 | ```Python 26 | import re 27 | 28 | import requests 29 | 30 | pattern = re.compile(r'') 31 | resp = requests.get('https://www.sohu.com/') 32 | if resp.status_code == 200: 33 | all_matches = pattern.findall(resp.text) 34 | for href, title in all_matches: 35 | print(href) 36 | print(title) 37 | ``` 38 | 39 | 除了文本内容,我们也可以使用`requests`库通过 URL 获取二进制资源。下面的例子演示了如何获取百度 Logo 并保存到名为`baidu.png`的本地文件中。可以在百度的首页上右键点击百度Logo,并通过“复制图片地址”菜单项获取图片的 URL。 40 | 41 | ```Python 42 | import requests 43 | 44 | resp = requests.get('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png') 45 | with open('baidu.png', 'wb') as file: 46 | file.write(resp.content) 47 | ``` 48 | 49 | > **说明**:`Response`对象的`content`属性可以获得服务器响应的二进制数据。 50 | 51 | `requests`库非常好用而且功能上也比较强大和完整,具体的内容我们在使用的过程中为大家一点点剖析。想解锁关于`requests`库更多的知识,可以阅读它的[官方文档](https://docs.python-requests.org/zh_CN/latest/)。 52 | 53 | ### 编写爬虫代码 54 | 55 | 接下来,我们以“豆瓣电影”为例,为大家讲解如何编写爬虫代码。按照上面提供的方法,我们先使用`requests`获取到网页的HTML代码,然后将整个代码看成一个长字符串,这样我们就可以使用正则表达式的捕获组从字符串提取我们需要的内容。下面的代码演示了如何从[豆瓣电影](https://movie.douban.com/)获取排前250名的电影的名称。[豆瓣电影Top250](https://movie.douban.com/top250)的页面结构和对应代码如下图所示,可以看出,每页共展示了25部电影,如果要获取到 Top250 数据,我们共需要访问10个页面,对应的地址是,这里的`xxx`如果为`0`就是第一页,如果`xxx`的值是`100`,那么我们可以访问到第五页。为了代码简单易读,我们只获取电影的标题和评分。 56 | 57 | ![](http://localhost/mypic/20210822093447.png) 58 | 59 | ```Python 60 | import random 61 | import re 62 | import time 63 | 64 | import requests 65 | 66 | for page in range(1, 11): 67 | resp = requests.get( 68 | url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', 69 | # 如果不设置HTTP请求头中的User-Agent,豆瓣会检测出不是浏览器而阻止我们的请求。 70 | # 通过get函数的headers参数设置User-Agent的值,具体的值可以在浏览器的开发者工具查看到。 71 | # 用爬虫访问大部分网站时,将爬虫伪装成来自浏览器的请求都是非常重要的一步。 72 | headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'} 73 | ) 74 | # 通过正则表达式获取class属性为title且标签体不以&开头的span标签并用捕获组提取标签内容 75 | pattern1 = re.compile(r'([^&]*?)') 76 | titles = pattern1.findall(resp.text) 77 | # 通过正则表达式获取class属性为rating_num的span标签并用捕获组提取标签内容 78 | pattern2 = re.compile(r'(.*?)') 79 | ranks = pattern2.findall(resp.text) 80 | # 使用zip压缩两个列表,循环遍历所有的电影标题和评分 81 | for title, rank in zip(titles, ranks): 82 | print(title, rank) 83 | # 随机休眠1-5秒,避免爬取页面过于频繁 84 | time.sleep(random.random() * 4 + 1) 85 | ``` 86 | 87 | > **说明**:通过分析豆瓣网的robots协议,我们发现豆瓣网并不拒绝百度爬虫获取它的数据,因此我们也可以将爬虫伪装成百度的爬虫,将`get`函数的`headers`参数修改为:`headers={'User-Agent': 'BaiduSpider'}`。 88 | 89 | ### 使用 IP 代理 90 | 91 | 让爬虫程序隐匿自己的身份对编写爬虫程序来说是比较重要的,很多网站对爬虫都比较反感的,因为爬虫会耗费掉它们很多的网络带宽并制造很多无效的流量。要隐匿身份通常需要使用**商业 IP 代理**(如蘑菇代理、芝麻代理、快代理等),让被爬取的网站无法获取爬虫程序来源的真实 IP 地址,也就无法简单的通过 IP 地址对爬虫程序进行封禁。 92 | 93 | 下面以[蘑菇代理](http://www.moguproxy.com/)为例,为大家讲解商业 IP 代理的使用方法。首先需要在该网站注册一个账号,注册账号后就可以[购买](http://www.moguproxy.com/buy)相应的套餐来获得商业 IP 代理。作为商业用途,建议大家购买不限量套餐,这样可以根据实际需要获取足够多的代理 IP 地址;作为学习用途,可以购买包时套餐或根据自己的需求来决定。蘑菇代理提供了两种接入代理的方式,分别是 API 私密代理和 HTTP 隧道代理,前者是通过请求蘑菇代理的 API 接口获取代理服务器地址,后者是直接使用统一的入口(蘑菇代理提供的域名)进行接入。 94 | 95 | 96 | 97 | 下面,我们以HTTP隧道代理为例,为大家讲解接入 IP 代理的方式,大家也可以直接参考蘑菇代理官网提供的代码来为爬虫设置代理。 98 | 99 | ```Python 100 | import requests 101 | 102 | APP_KEY = 'Wnp******************************XFx' 103 | PROXY_HOST = 'secondtransfer.moguproxy.com:9001' 104 | 105 | for page in range(1, 11): 106 | resp = requests.get( 107 | url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', 108 | # 需要在HTTP请求头设置代理的身份认证方式 109 | headers={ 110 | 'Proxy-Authorization': f'Basic {APP_KEY}', 111 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36', 112 | 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4' 113 | }, 114 | # 设置代理服务器 115 | proxies={ 116 | 'http': f'http://{PROXY_HOST}', 117 | 'https': f'https://{PROXY_HOST}' 118 | }, 119 | verify=False 120 | ) 121 | pattern1 = re.compile(r'([^&]*?)') 122 | titles = pattern1.findall(resp.text) 123 | pattern2 = re.compile(r'(.*?)') 124 | ranks = pattern2.findall(resp.text) 125 | for title, rank in zip(titles, ranks): 126 | print(title, rank) 127 | ``` 128 | 129 | > **说明**:上面的代码需要修改`APP_KEY`为自己创建的订单对应的`Appkey`值,这个值可以在用户中心用户订单中查看到。蘑菇代理提供了免费的 API 代理和 HTTP 隧道代理试用,但是试用的代理接通率不能保证,建议大家还是直接购买一个在自己支付能力范围内的代理服务来体验。 130 | > 131 | > **另注**:蘑菇代理目前已经停止服务了,大家可以按照上面讲解的方式使用其他商业代理即可。 132 | 133 | ### 简单的总结 134 | 135 | Python 语言能做的事情真的很多,就网络数据采集这一项而言,Python 几乎是一枝独秀的,大量的企业和个人都在使用 Python 从网络上获取自己需要的数据,这可能也是你将来日常工作的一部分。另外,用编写正则表达式的方式从网页中提取内容虽然可行,但是写出一个能够满足需求的正则表达式本身也不是件容易的事情,这一点对于新手来说尤为明显。在下一节课中,我们将会为大家介绍另外两种从页面中提取数据的方法,虽然从性能上来讲,它们可能不如正则表达式,但是却降低了编码的复杂性,相信大家会喜欢上它们的。 136 | -------------------------------------------------------------------------------- /第24课:用Python读写Excel文件-1.md: -------------------------------------------------------------------------------- 1 | ## 第24课:用Python读写Excel文件-1 2 | 3 | ### Excel简介 4 | 5 | Excel是Microsoft(微软)为使用Windows和macOS操作系统开发的一款电子表格软件。Excel凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel也有很多竞品,例如Google Sheets、LibreOffice Calc、Numbers等,这些竞品基本上也能够兼容Excel,至少能够读写较新版本的Excel文件,当然这些不是我们讨论的重点。掌握用Python程序操作Excel文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出Excel文件都是特别常见的功能。 6 | 7 | Python操作Excel需要三方库的支持,如果要兼容Excel 2007以前的版本,也就是`xls`格式的Excel文件,可以使用三方库`xlrd`和`xlwt`,前者用于读Excel文件,后者用于写Excel文件。如果使用较新版本的Excel,即操作`xlsx`格式的Excel文件,可以使用`openpyxl`库,当然这个库不仅仅可以操作Excel,还可以操作其他基于Office Open XML的电子表格文件。 8 | 9 | 本章我们先讲解基于`xlwt`和`xlrd`操作Excel文件,大家可以先使用下面的命令安装这两个三方库以及配合使用的工具模块`xlutils`。 10 | 11 | ```Bash 12 | pip install xlwt xlrd xlutils 13 | ``` 14 | 15 | ### 读Excel文件 16 | 17 | 例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xls”的Excel文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。 18 | 19 | ```Python 20 | import xlrd 21 | 22 | # 使用xlrd模块的open_workbook函数打开指定Excel文件并获得Book对象(工作簿) 23 | wb = xlrd.open_workbook('阿里巴巴2020年股票数据.xls') 24 | # 通过Book对象的sheet_names方法可以获取所有表单名称 25 | sheetnames = wb.sheet_names() 26 | print(sheetnames) 27 | # 通过指定的表单名称获取Sheet对象(工作表) 28 | sheet = wb.sheet_by_name(sheetnames[0]) 29 | # 通过Sheet对象的nrows和ncols属性获取表单的行数和列数 30 | print(sheet.nrows, sheet.ncols) 31 | for row in range(sheet.nrows): 32 | for col in range(sheet.ncols): 33 | # 通过Sheet对象的cell方法获取指定Cell对象(单元格) 34 | # 通过Cell对象的value属性获取单元格中的值 35 | value = sheet.cell(row, col).value 36 | # 对除首行外的其他行进行数据格式化处理 37 | if row > 0: 38 | # 第1列的xldate类型先转成元组再格式化为“年月日”的格式 39 | if col == 0: 40 | # xldate_as_tuple函数的第二个参数只有0和1两个取值 41 | # 其中0代表以1900-01-01为基准的日期,1代表以1904-01-01为基准的日期 42 | value = xlrd.xldate_as_tuple(value, 0) 43 | value = f'{value[0]}年{value[1]:>02d}月{value[2]:>02d}日' 44 | # 其他列的number类型处理成小数点后保留两位有效数字的浮点数 45 | else: 46 | value = f'{value:.2f}' 47 | print(value, end='\t') 48 | print() 49 | # 获取最后一个单元格的数据类型 50 | # 0 - 空值,1 - 字符串,2 - 数字,3 - 日期,4 - 布尔,5 - 错误 51 | last_cell_type = sheet.cell_type(sheet.nrows - 1, sheet.ncols - 1) 52 | print(last_cell_type) 53 | # 获取第一行的值(列表) 54 | print(sheet.row_values(0)) 55 | # 获取指定行指定列范围的数据(列表) 56 | # 第一个参数代表行索引,第二个和第三个参数代表列的开始(含)和结束(不含)索引 57 | print(sheet.row_slice(3, 0, 5)) 58 | ``` 59 | 60 | > **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xls”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。 61 | 62 | 相信通过上面的代码,大家已经了解到了如何读取一个Excel文件,如果想知道更多关于`xlrd`模块的知识,可以阅读它的[官方文档](https://xlrd.readthedocs.io/en/latest/)。 63 | 64 | ### 写Excel文件 65 | 66 | 写入Excel文件可以通过`xlwt` 模块的`Workbook`类创建工作簿对象,通过工作簿对象的`add_sheet`方法可以添加工作表,通过工作表对象的`write`方法可以向指定单元格中写入数据,最后通过工作簿对象的`save`方法将工作簿写入到指定的文件或内存中。下面的代码实现了将`5`个学生`3`门课程的考试成绩写入Excel文件的操作。 67 | 68 | ```Python 69 | import random 70 | 71 | import xlwt 72 | 73 | student_names = ['关羽', '张飞', '赵云', '马超', '黄忠'] 74 | scores = [[random.randrange(50, 101) for _ in range(3)] for _ in range(5)] 75 | # 创建工作簿对象(Workbook) 76 | wb = xlwt.Workbook() 77 | # 创建工作表对象(Worksheet) 78 | sheet = wb.add_sheet('一年级二班') 79 | # 添加表头数据 80 | titles = ('姓名', '语文', '数学', '英语') 81 | for index, title in enumerate(titles): 82 | sheet.write(0, index, title) 83 | # 将学生姓名和考试成绩写入单元格 84 | for row in range(len(scores)): 85 | sheet.write(row + 1, 0, student_names[row]) 86 | for col in range(len(scores[row])): 87 | sheet.write(row + 1, col + 1, scores[row][col]) 88 | # 保存Excel工作簿 89 | wb.save('考试成绩表.xls') 90 | ``` 91 | 92 | #### 调整单元格样式 93 | 94 | 在写Excel文件时,我们还可以为单元格设置样式,主要包括字体(Font)、对齐方式(Alignment)、边框(Border)和背景(Background)的设置,`xlwt`对这几项设置都封装了对应的类来支持。要设置单元格样式需要首先创建一个`XFStyle`对象,再通过该对象的属性对字体、对齐方式、边框等进行设定,例如在上面的例子中,如果希望将表头单元格的背景色修改为黄色,可以按照如下的方式进行操作。 95 | 96 | ```Python 97 | header_style = xlwt.XFStyle() 98 | pattern = xlwt.Pattern() 99 | pattern.pattern = xlwt.Pattern.SOLID_PATTERN 100 | # 0 - 黑色、1 - 白色、2 - 红色、3 - 绿色、4 - 蓝色、5 - 黄色、6 - 粉色、7 - 青色 101 | pattern.pattern_fore_colour = 5 102 | header_style.pattern = pattern 103 | titles = ('姓名', '语文', '数学', '英语') 104 | for index, title in enumerate(titles): 105 | sheet.write(0, index, title, header_style) 106 | ``` 107 | 108 | 如果希望为表头设置指定的字体,可以使用`Font`类并添加如下所示的代码。 109 | 110 | ```Python 111 | font = xlwt.Font() 112 | # 字体名称 113 | font.name = '华文楷体' 114 | # 字体大小(20是基准单位,18表示18px) 115 | font.height = 20 * 18 116 | # 是否使用粗体 117 | font.bold = True 118 | # 是否使用斜体 119 | font.italic = False 120 | # 字体颜色 121 | font.colour_index = 1 122 | header_style.font = font 123 | ``` 124 | 125 | > **注意**:上面代码中指定的字体名(`font.name`)应当是本地系统有的字体,例如在我的电脑上有名为“华文楷体”的字体。 126 | 127 | 如果希望表头垂直居中对齐,可以使用下面的代码进行设置。 128 | 129 | ```Python 130 | align = xlwt.Alignment() 131 | # 垂直方向的对齐方式 132 | align.vert = xlwt.Alignment.VERT_CENTER 133 | # 水平方向的对齐方式 134 | align.horz = xlwt.Alignment.HORZ_CENTER 135 | header_style.alignment = align 136 | ``` 137 | 138 | 如果希望给表头加上黄色的虚线边框,可以使用下面的代码来设置。 139 | 140 | ```Python 141 | borders = xlwt.Borders() 142 | props = ( 143 | ('top', 'top_colour'), ('right', 'right_colour'), 144 | ('bottom', 'bottom_colour'), ('left', 'left_colour') 145 | ) 146 | # 通过循环对四个方向的边框样式及颜色进行设定 147 | for position, color in props: 148 | # 使用setattr内置函数动态给对象指定的属性赋值 149 | setattr(borders, position, xlwt.Borders.DASHED) 150 | setattr(borders, color, 5) 151 | header_style.borders = borders 152 | ``` 153 | 154 | 如果要调整单元格的宽度(列宽)和表头的高度(行高),可以按照下面的代码进行操作。 155 | 156 | ```Python 157 | # 设置行高为40px 158 | sheet.row(0).set_style(xlwt.easyxf(f'font:height {20 * 40}')) 159 | titles = ('姓名', '语文', '数学', '英语') 160 | for index, title in enumerate(titles): 161 | # 设置列宽为200px 162 | sheet.col(index).width = 20 * 200 163 | # 设置单元格的数据和样式 164 | sheet.write(0, index, title, header_style) 165 | ``` 166 | 167 | #### 公式计算 168 | 169 | 对于前面打开的“阿里巴巴2020年股票数据.xls”文件,如果要统计全年收盘价(Close字段)的平均值以及全年交易量(Volume字段)的总和,可以使用Excel的公式计算即可。我们可以先使用`xlrd`读取Excel文件夹,然后通过`xlutils`三方库提供的`copy`函数将读取到的Excel文件转成`Workbook`对象进行写操作,在调用`write`方法时,可以将一个`Formula`对象写入单元格。 170 | 171 | 实现公式计算的代码如下所示。 172 | 173 | ```Python 174 | import xlrd 175 | import xlwt 176 | from xlutils.copy import copy 177 | 178 | wb_for_read = xlrd.open_workbook('阿里巴巴2020年股票数据.xls') 179 | sheet1 = wb_for_read.sheet_by_index(0) 180 | nrows, ncols = sheet1.nrows, sheet1.ncols 181 | wb_for_write = copy(wb_for_read) 182 | sheet2 = wb_for_write.get_sheet(0) 183 | sheet2.write(nrows, 4, xlwt.Formula(f'average(E2:E{nrows})')) 184 | sheet2.write(nrows, 6, xlwt.Formula(f'sum(G2:G{nrows})')) 185 | wb_for_write.save('阿里巴巴2020年股票数据汇总.xls') 186 | ``` 187 | 188 | > **说明**:上面的代码有一些小瑕疵,有兴趣的读者可以自行探索并思考如何解决。 189 | 190 | ### 简单的总结 191 | 192 | 掌握了Python程序操作Excel的方法,可以解决日常办公中很多繁琐的处理Excel电子表格工作,最常见就是将多个数据格式相同的Excel文件合并到一个文件以及从多个Excel文件或表单中提取指定的数据。当然,如果要对表格数据进行处理,使用Python数据分析神器之一的`pandas`库可能更为方便。 193 | -------------------------------------------------------------------------------- /第31课:网络数据采集概述.md: -------------------------------------------------------------------------------- 1 | ## 第31课:网络数据采集概述 2 | 3 | 爬虫(crawler)也经常被称为网络蜘蛛(spider),是按照一定的规则自动浏览网站并获取所需信息的机器人程序(自动化脚本代码),被广泛的应用于互联网搜索引擎和数据采集。使用过互联网和浏览器的人都知道,网页中除了供用户阅读的文字信息之外,还包含一些超链接,网络爬虫正是通过网页中的超链接信息,不断获得网络上其它页面的地址,然后持续的进行数据采集。正因如此,网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游,所以才被形象的称为爬虫或者网络蜘蛛。 4 | 5 | ### 爬虫的应用领域 6 | 7 | 在理想的状态下,所有 ICP(Internet Content Provider)都应该为自己的网站提供 API 接口来共享它们允许其他程序获取的数据,在这种情况下就根本不需要爬虫程序。国内比较有名的电商平台(如淘宝、京东等)、社交平台(如微博、微信等)等都提供了自己的 API 接口,但是这类 API 接口通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言,及时的获取行业数据和竞对数据是企业生存的重要环节之一,然而对大部分企业来说,数据都是其与生俱来的短板。在这种情况下,合理的利用爬虫来获取数据并从中提取出有商业价值的信息对这些企业来说就显得至关重要的。 8 | 9 | 爬虫的应用领域其实非常广泛,下面我们列举了其中的一部分,有兴趣的读者可以自行探索相关内容。 10 | 11 | 1. 搜索引擎 12 | 2. 新闻聚合 13 | 3. 社交应用 14 | 4. 舆情监控 15 | 5. 行业数据 16 | 17 | ### 爬虫合法性探讨 18 | 19 | 经常听人说起“爬虫写得好,牢饭吃到饱”,那么编程爬虫程序是否违法呢?关于这个问题,我们可以从以下几个角度进行解读。 20 | 21 | 1. 网络爬虫这个领域目前还属于拓荒阶段,虽然互联网世界已经通过自己的游戏规则建立起了一定的道德规范,即 Robots 协议(全称是“网络爬虫排除标准”),但法律部分还在建立和完善中,也就是说,现在这个领域暂时还是灰色地带。 22 | 2. “法不禁止即为许可”,如果爬虫就像浏览器一样获取的是前端显示的数据(网页上的公开信息)而不是网站后台的私密敏感信息,就不太担心法律法规的约束,因为目前大数据产业链的发展速度远远超过了法律的完善程度。 23 | 3. 在爬取网站的时候,需要限制自己的爬虫遵守 Robots 协议,同时控制网络爬虫程序的抓取数据的速度;在使用数据的时候,必须要尊重网站的知识产权(从Web 2.0时代开始,虽然Web上的数据很多都是由用户提供的,但是网站平台是投入了运营成本的,当用户在注册和发布内容时,平台通常就已经获得了对数据的所有权、使用权和分发权)。如果违反了这些规定,在打官司的时候败诉几率相当高。 24 | 4. 适当的隐匿自己的身份在编写爬虫程序时必要的,而且最好不要被对方举证你的爬虫有破坏别人动产(例如服务器)的行为。 25 | 5. 不要在公网(如代码托管平台)上去开源或者展示你的爬虫代码,这些行为通常会给自己带来不必要的麻烦。 26 | 27 | #### Robots协议 28 | 29 | 大多数网站都会定义`robots.txt`文件,这是一个君子协议,并不是所有爬虫都必须遵守的游戏规则。下面以淘宝的[`robots.txt`](http://www.taobao.com/robots.txt)文件为例,看看淘宝网对爬虫有哪些限制。 30 | 31 | ``` 32 | User-agent: Baiduspider 33 | Disallow: / 34 | 35 | User-agent: baiduspider 36 | Disallow: / 37 | ``` 38 | 39 | 通过上面的文件可以看出,淘宝禁止百度爬虫爬取它任何资源,因此当你在百度搜索“淘宝”的时候,搜索结果下方会出现:“由于该网站的`robots.txt`文件存在限制指令(限制搜索引擎抓取),系统无法提供该页面的内容描述”。百度作为一个搜索引擎,至少在表面上遵守了淘宝网的`robots.txt`协议,所以用户不能从百度上搜索到淘宝内部的产品信息。 40 | 41 | 图1. 百度搜索淘宝的结果 42 | 43 | ![](http://localhost/mypic/20210824004320.png) 44 | 45 | 下面是豆瓣网的[`robots.txt`](https://www.douban.com/robots.txt)文件,大家可以自行解读,看看它做出了什么样的限制。 46 | 47 | ``` 48 | User-agent: * 49 | Disallow: /subject_search 50 | Disallow: /amazon_search 51 | Disallow: /search 52 | Disallow: /group/search 53 | Disallow: /event/search 54 | Disallow: /celebrities/search 55 | Disallow: /location/drama/search 56 | Disallow: /forum/ 57 | Disallow: /new_subject 58 | Disallow: /service/iframe 59 | Disallow: /j/ 60 | Disallow: /link2/ 61 | Disallow: /recommend/ 62 | Disallow: /doubanapp/card 63 | Disallow: /update/topic/ 64 | Disallow: /share/ 65 | Allow: /ads.txt 66 | Sitemap: https://www.douban.com/sitemap_index.xml 67 | Sitemap: https://www.douban.com/sitemap_updated_index.xml 68 | # Crawl-delay: 5 69 | 70 | User-agent: Wandoujia Spider 71 | Disallow: / 72 | 73 | User-agent: Mediapartners-Google 74 | Disallow: /subject_search 75 | Disallow: /amazon_search 76 | Disallow: /search 77 | Disallow: /group/search 78 | Disallow: /event/search 79 | Disallow: /celebrities/search 80 | Disallow: /location/drama/search 81 | Disallow: /j/ 82 | ``` 83 | 84 | ### 超文本传输协议(HTTP) 85 | 86 | 在开始讲解爬虫之前,我们稍微对超文本传输协议(HTTP)做一些回顾,因为我们在网页上看到的内容通常是浏览器执行 HTML (超文本标记语言)得到的结果,而 HTTP 就是传输 HTML 数据的协议。HTTP 和其他很多应用级协议一样是构建在 TCP(传输控制协议)之上的,它利用了 TCP 提供的可靠的传输服务实现了 Web 应用中的数据交换。按照维基百科上的介绍,设计 HTTP 最初的目的是为了提供一种发布和接收 [HTML](https://zh.wikipedia.org/wiki/HTML) 页面的方法,也就是说,这个协议是浏览器和 Web 服务器之间传输的数据的载体。关于 HTTP 的详细信息以及目前的发展状况,大家可以阅读[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)、[《互联网协议入门》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)、[《图解 HTTPS 协议》](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)等文章进行了解。 87 | 88 | 下图是我在四川省网络通信技术重点实验室工作期间用开源协议分析工具 Ethereal(WireShark 的前身)截取的访问百度首页时的 HTTP 请求和响应的报文(协议数据),由于 Ethereal 截取的是经过网络适配器的数据,因此可以清晰的看到从物理链路层到应用层的协议数据。 89 | 90 | 图2. HTTP请求 91 | 92 | ![http-request](http://localhost/mypic/20210824003915.png) 93 | 94 | HTTP 请求通常是由请求行、请求头、空行、消息体四个部分构成,如果没有数据发给服务器,消息体就不是必须的部分。请求行中包含了请求方法(GET、POST 等,如下表所示)、资源路径和协议版本;请求头由若干键值对构成,包含了浏览器、编码方式、首选语言、缓存策略等信息;请求头的后面是空行和消息体。 95 | 96 | 97 | 98 | 图3. HTTP响应 99 | 100 | ![http-response](http://localhost/mypic/20210824234158.png) 101 | 102 | HTTP 响应通常是由响应行、响应头、空行、消息体四个部分构成,其中消息体是服务响应的数据,可能是 HTML 页面,也有可能是JSON或二进制数据等。响应行中包含了协议版本和响应状态码,响应状态码有很多种,常见的如下表所示。 103 | 104 | 105 | 106 | #### 相关工具 107 | 108 | 下面我们先介绍一些开发爬虫程序的辅助工具,这些工具相信能帮助你事半功倍。 109 | 110 | 1. Chrome Developer Tools:谷歌浏览器内置的开发者工具。该工具最常用的几个功能模块是: 111 | 112 | - 元素(ELements):用于查看或修改 HTML 元素的属性、CSS 属性、监听事件等。CSS 可以即时修改,即时显示,大大方便了开发者调试页面。 113 | - 控制台(Console):用于执行一次性代码,查看 JavaScript 对象,查看调试日志信息或异常信息。控制台其实就是一个执行 JavaScript 代码的交互式环境。 114 | - 源代码(Sources):用于查看页面的 HTML 文件源代码、JavaScript 源代码、CSS 源代码,此外最重要的是可以调试 JavaScript 源代码,可以给代码添加断点和单步执行。 115 | - 网络(Network):用于 HTTP 请求、HTTP 响应以及与网络连接相关的信息。 116 | - 应用(Application):用于查看浏览器本地存储、后台任务等内容,本地存储主要包括Cookie、Local Storage、Session Storage等。 117 | 118 | ![chrome-developer-tools](http://localhost/mypic/20210824004034.png) 119 | 120 | 2. Postman:功能强大的网页调试与 RESTful 请求工具。Postman可以帮助我们模拟请求,非常方便的定制我们的请求以及查看服务器的响应。 121 | 122 | ![postman](http://localhost/mypic/20210824004048.png) 123 | 124 | 3. HTTPie:命令行HTTP客户端。 125 | 126 | 安装。 127 | 128 | ```Bash 129 | pip install httpie 130 | ``` 131 | 132 | 使用。 133 | 134 | ```Bash 135 | http --header http --header https://movie.douban.com/ 136 | 137 | HTTP/1.1 200 OK 138 | Connection: keep-alive 139 | Content-Encoding: gzip 140 | Content-Type: text/html; charset=utf-8 141 | Date: Tue, 24 Aug 2021 16:48:00 GMT 142 | Keep-Alive: timeout=30 143 | Server: dae 144 | Set-Cookie: bid=58h4BdKC9lM; Expires=Wed, 24-Aug-22 16:48:00 GMT; Domain=.douban.com; Path=/ 145 | Strict-Transport-Security: max-age=15552000 146 | Transfer-Encoding: chunked 147 | X-Content-Type-Options: nosniff 148 | X-DOUBAN-NEWBID: 58h4BdKC9lM 149 | ``` 150 | 151 | 4. `builtwith`库:识别网站所用技术的工具。 152 | 153 | 安装。 154 | 155 | ```Bash 156 | pip install builtwith 157 | ``` 158 | 159 | 使用。 160 | 161 | ```Python 162 | import ssl 163 | 164 | import builtwith 165 | 166 | ssl._create_default_https_context = ssl._create_unverified_context 167 | print(builtwith.parse('http://www.bootcss.com/')) 168 | ``` 169 | 170 | 5. `python-whois`库:查询网站所有者的工具。 171 | 172 | 安装。 173 | 174 | ```Bash 175 | pip3 install python-whois 176 | ``` 177 | 178 | 使用。 179 | 180 | ```Python 181 | import whois 182 | 183 | print(whois.whois('https://www.bootcss.com')) 184 | ``` 185 | 186 | ### 爬虫的基本工作流程 187 | 188 | 一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容,当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术,这就需要有调度器(安排线程或进程执行对应的任务)、后台管理程序(监控爬虫的工作状态以及检查数据抓取的结果)等的参与。 189 | 190 | ![](http://localhost/mypic/20210824004107.png) 191 | 192 | 一般来说,爬虫的工作流程包括以下几个步骤: 193 | 194 | 1. 设定抓取目标(种子页面/起始页面)并获取网页。 195 | 2. 当服务器无法访问时,按照指定的重试次数尝试重新下载页面。 196 | 3. 在需要的时候设置用户代理或隐藏真实IP,否则可能无法访问页面。 197 | 4. 对获取的页面进行必要的解码操作然后抓取出需要的信息。 198 | 5. 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息。 199 | 6. 对链接进行进一步的处理(获取页面并重复上面的动作)。 200 | 7. 将有用的信息进行持久化以备后续的处理。 201 | -------------------------------------------------------------------------------- /第15课:函数使用进阶.md: -------------------------------------------------------------------------------- 1 | ## 第15课:函数使用进阶 2 | 3 | 前面我们讲到了关于函数的知识,我们还讲到过Python中常用的数据类型,这些类型的变量都可以作为函数的参数或返回值,用好函数还可以让我们做更多的事情。 4 | 5 | ### 关键字参数 6 | 7 | 下面是一个判断传入的三条边长能否构成三角形的函数,在调用函数传入参数时,我们可以指定参数名,也可以不指定参数名,代码如下所示。 8 | 9 | ```Python 10 | def is_triangle(a, b, c): 11 | print(f'a = {a}, b = {b}, c = {c}') 12 | return a + b > c and b + c > a and a + c > b 13 | 14 | 15 | # 调用函数传入参数不指定参数名按位置对号入座 16 | print(is_triangle(1, 2, 3)) 17 | # 调用函数通过“参数名=参数值”的形式按顺序传入参数 18 | print(is_triangle(a=1, b=2, c=3)) 19 | # 调用函数通过“参数名=参数值”的形式不按顺序传入参数 20 | print(is_triangle(c=3, a=1, b=2)) 21 | ``` 22 | 23 | 在没有特殊处理的情况下,函数的参数都是**位置参数**,也就意味着传入参数的时候对号入座即可,如上面代码的第7行所示,传入的参数值`1`、`2`、`3`会依次赋值给参数`a`、`b`、`c`。当然,也可以通过`参数名=参数值`的方式传入函数所需的参数,因为指定了参数名,传入参数的顺序可以进行调整,如上面代码的第9行和第11行所示。 24 | 25 | 调用函数时,如果希望函数的调用者必须以`参数名=参数值`的方式传参,可以用**命名关键字参数**(keyword-only argument)取代位置参数。所谓命名关键字参数,是在函数的参数列表中,写在`*`之后的参数,代码如下所示。 26 | 27 | ```Python 28 | def is_triangle(*, a, b, c): 29 | print(f'a = {a}, b = {b}, c = {c}') 30 | return a + b > c and b + c > a and a + c > b 31 | 32 | 33 | # TypeError: is_triangle() takes 0 positional arguments but 3 were given 34 | # print(is_triangle(3, 4, 5)) 35 | # 传参时必须使用“参数名=参数值”的方式,位置不重要 36 | print(is_triangle(a=3, b=4, c=5)) 37 | print(is_triangle(c=5, b=4, a=3)) 38 | ``` 39 | 40 | > **注意**:上面的`is_triangle`函数,参数列表中的`*`是一个分隔符,`*`前面的参数都是位置参数,而`*`后面的参数就是命名关键字参数。 41 | 42 | 我们之前讲过在函数的参数列表中可以使用**可变参数**`*args`来接收任意数量的参数,但是我们需要看看,`*args`是否能够接收带参数名的参数。 43 | 44 | ```Python 45 | def calc(*args): 46 | result = 0 47 | for arg in args: 48 | if type(arg) in (int, float): 49 | result += arg 50 | return result 51 | 52 | 53 | print(calc(a=1, b=2, c=3)) 54 | ``` 55 | 56 | 执行上面的代码会引发`TypeError`错误,错误消息为`calc() got an unexpected keyword argument 'a'`,由此可见,`*args`并不能处理带参数名的参数。我们在设计函数时,如果既不知道调用者会传入的参数个数,也不知道调用者会不会指定参数名,那么同时使用可变参数和**关键字参数**。关键字参数会将传入的带参数名的参数组装成一个字典,参数名就是字典中键值对的键,而参数值就是字典中键值对的值,代码如下所示。 57 | 58 | ```Python 59 | def calc(*args, **kwargs): 60 | result = 0 61 | for arg in args: 62 | if type(arg) in (int, float): 63 | result += arg 64 | for value in kwargs.values(): 65 | if type(value) in (int, float): 66 | result += value 67 | return result 68 | 69 | 70 | print(calc()) # 0 71 | print(calc(1, 2, 3)) # 6 72 | print(calc(a=1, b=2, c=3)) # 6 73 | print(calc(1, 2, c=3, d=4)) # 10 74 | ``` 75 | 76 | > **提示**:**不带参数名的参数(位置参数)必须出现在带参数名的参数(关键字参数)之前**,否则将会引发异常。例如,执行`calc(1, 2, c=3, d=4, 5)`将会引发`SyntaxError`错误,错误消息为`positional argument follows keyword argument`,翻译成中文意思是“位置参数出现在关键字参数之后”。 77 | 78 | ### 高阶函数的用法 79 | 80 | 在前面几节课中,我们讲到了面向对象程序设计,在面向对象的世界中,一切皆为对象,所以类和函数也是对象。函数的参数和返回值可以是任意类型的对象,这就意味着**函数本身也可以作为函数的参数或返回值**,这就是所谓的**高阶函数**。 81 | 82 | 如果我们希望上面的`calc`函数不仅仅可以做多个参数求和,还可以做多个参数求乘积甚至更多的二元运算,我们就可以使用高阶函数的方式来改写上面的代码,将加法运算从函数中移除掉,具体的做法如下所示。 83 | 84 | ```Python 85 | def calc(*args, init_value, op, **kwargs): 86 | result = init_value 87 | for arg in args: 88 | if type(arg) in (int, float): 89 | result = op(result, arg) 90 | for value in kwargs.values(): 91 | if type(value) in (int, float): 92 | result = op(result, value) 93 | return result 94 | ``` 95 | 96 | 注意,上面的函数增加了两个参数,其中`init_value`代表运算的初始值,`op`代表二元运算函数。经过改造的`calc`函数不仅仅可以实现多个参数的累加求和,也可以实现多个参数的累乘运算,代码如下所示。 97 | 98 | ```Python 99 | def add(x, y): 100 | return x + y 101 | 102 | 103 | def mul(x, y): 104 | return x * y 105 | 106 | 107 | print(calc(1, 2, 3, init_value=0, op=add, x=4, y=5)) # 15 108 | print(calc(1, 2, x=3, y=4, z=5, init_value=1, op=mul)) # 120 109 | ``` 110 | 111 | 通过对高阶函数的运用,`calc`函数不再和加法运算耦合,所以灵活性和通用性会变强,这是一种解耦合的编程技巧,但是最初学者来说可能会稍微有点难以理解。需要注意的是,将函数作为参数和调用函数是有显著的区别的,**调用函数需要在函数名后面跟上圆括号,而把函数作为参数时只需要函数名即可**。上面的代码也可以不用定义`add`和`mul`函数,因为Python标准库中的`operator`模块提供了代表加法运算的`add`和代表乘法运算的`mul`函数,我们直接使用即可,代码如下所示。 112 | 113 | ```Python 114 | import operator 115 | 116 | print(calc(1, 2, 3, init_value=0, op=operator.add, x=4, y=5)) # 15 117 | print(calc(1, 2, x=3, y=4, z=5, init_value=1, op=operator.mul)) # 120 118 | ``` 119 | 120 | Python内置函数中有不少高阶函数,我们前面提到过的`filter`和`map`函数就是高阶函数,前者可以实现对序列中元素的过滤,后者可以实现对序列中元素的映射,例如我们要去掉一个整数列表中的奇数,并对所有的偶数求平方得到一个新的列表,就可以直接使用这两个函数来做到,具体的做法是如下所示。 121 | 122 | ```Python 123 | def is_even(num): 124 | return num % 2 == 0 125 | 126 | 127 | def square(num): 128 | return num ** 2 129 | 130 | 131 | numbers1 = [35, 12, 8, 99, 60, 52] 132 | numbers2 = list(map(square, filter(is_even, numbers1))) 133 | print(numbers2) # [144, 64, 3600, 2704] 134 | ``` 135 | 136 | 当然,要完成上面代码的功能,也可以使用列表生成式,列表生成式的做法更为简单优雅。 137 | 138 | ```Python 139 | numbers1 = [35, 12, 8, 99, 60, 52] 140 | numbers2 = [num ** 2 for num in numbers1 if num % 2 == 0] 141 | print(numbers2) # [144, 64, 3600, 2704] 142 | ``` 143 | 144 | ### Lambda函数 145 | 146 | 在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,那么我们可以使用**Lambda函数**来表示。Python中的Lambda函数是没有的名字函数,所以很多人也把它叫做**匿名函数**,匿名函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。上面代码中的`is_even`和`square`函数都只有一行代码,我们可以用Lambda函数来替换掉它们,代码如下所示。 147 | 148 | ```Python 149 | numbers1 = [35, 12, 8, 99, 60, 52] 150 | numbers2 = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers1))) 151 | print(numbers2) # [144, 64, 3600, 2704] 152 | ``` 153 | 154 | 通过上面的代码可以看出,定义Lambda函数的关键字是`lambda`,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是Lambda函数的返回值,不需要写`return` 关键字。 155 | 156 | 如果需要使用加减乘除这种简单的二元函数,也可以用Lambda函数来书写,例如调用上面的`calc`函数时,可以通过传入Lambda函数来作为`op`参数的参数值。当然,`op`参数也可以有默认值,例如我们可以用一个代表加法运算的Lambda函数来作为`op`参数的默认值。 157 | 158 | ```Python 159 | def calc(*args, init_value=0, op=lambda x, y: x + y, **kwargs): 160 | result = init_value 161 | for arg in args: 162 | if type(arg) in (int, float): 163 | result = op(result, arg) 164 | for value in kwargs.values(): 165 | if type(value) in (int, float): 166 | result = op(result, value) 167 | return result 168 | 169 | 170 | # 调用calc函数,使用init_value和op的默认值 171 | print(calc(1, 2, 3, x=4, y=5)) # 15 172 | # 调用calc函数,通过lambda函数给op参数赋值 173 | print(calc(1, 2, 3, x=4, y=5, init_value=1, op=lambda x, y: x * y)) # 120 174 | ``` 175 | 176 | > **提示**:注意上面的代码中的`calc`函数,它同时使用了可变参数、关键字参数、命名关键字参数,其中命名关键字参数要放在可变参数和关键字参数之间,传参时先传入可变参数,关键字参数和命名关键字参数的先后顺序并不重要。 177 | 178 | 有很多函数在Python中用一行代码就能实现,我们可以用Lambda函数来定义这些函数,调用Lambda函数就跟调用普通函数一样,代码如下所示。 179 | 180 | ```Python 181 | import operator, functools 182 | 183 | # 一行代码定义求阶乘的函数 184 | fac = lambda num: functools.reduce(operator.mul, range(1, num + 1), 1) 185 | # 一行代码定义判断素数的函数 186 | is_prime = lambda x: x > 1 and all(map(lambda f: x % f, range(2, int(x ** 0.5) + 1))) 187 | 188 | # 调用Lambda函数 189 | print(fac(10)) # 3628800 190 | print(is_prime(9)) # False 191 | ``` 192 | 193 | > **提示1**:上面使用的`reduce`函数是Python标准库`functools`模块中的函数,它可以实现对数据的归约操作,通常情况下,**过滤**(filter)、**映射**(map)和**归约**(reduce)是处理数据中非常关键的三个步骤,而Python的标准库也提供了对这三个操作的支持。 194 | > 195 | > **提示2**:上面使用的`all`函数是Python内置函数,如果传入的序列中所有布尔值都是`True`,`all`函数就返回`True`,否则`all`函数就返回`False`。 196 | 197 | ### 简单的总结 198 | 199 | Python中的函数可以使用可变参数`*args`和关键字参数`**kwargs`来接收任意数量的参数,而且传入参数时可以带上参数名也可以没有参数名,可变参数会被处理成一个元组,而关键字参数会被处理成一个字典。**Python中的函数是一等函数,可以赋值给变量,也可以作为函数的参数和返回值**,这也就意味着我们可以在Python中使用高阶函数。如果我们要定义的函数非常简单,只有一行代码且不需要函数名,可以使用Lambda函数(匿名函数)。 200 | -------------------------------------------------------------------------------- /第12课:常用数据结构之字典.md: -------------------------------------------------------------------------------- 1 | ## 第12课:常用数据结构之字典 2 | 3 | 迄今为止,我们已经为大家介绍了Python中的三种容器型数据类型,但是这些数据类型仍然不足以帮助我们解决所有的问题。例如,我们要保存一个人的信息,包括姓名、年龄、体重、单位地址、家庭住址、本人手机号、紧急联系人手机号等信息,你会发现我们之前学过的列表、元组和集合都不是最理想的选择。 4 | 5 | ```Python 6 | person1 = ['王大锤', 55, 60, '科华北路62号', '中同仁路8号', '13122334455', '13800998877'] 7 | person2 = ('王大锤', 55, 60, '科华北路62号', '中同仁路8号', '13122334455', '13800998877') 8 | person3 = {'王大锤', 55, 60, '科华北路62号', '中同仁路8号', '13122334455', '13800998877'} 9 | ``` 10 | 11 | 集合肯定是最不合适的,因为集合有去重特性,如果一个人的年龄和体重相同,那么集合中就会少一项信息;同理,如果这个人的家庭住址和单位地址是相同的,那么集合中又会少一项信息。另一方面,虽然列表和元组可以把一个人的所有信息都保存下来,但是当你想要获取这个人的手机号时,你得先知道他的手机号是列表或元组中的第6个还是第7个元素;当你想获取一个人的家庭住址时,你还得知道家庭住址是列表或元组中的第几项。总之,在遇到上述的场景时,列表、元组、字典都不是最合适的选择,我们还需字典(dictionary)类型,这种数据类型最适合把相关联的信息组装到一起,并且可以帮助我们解决程序中为真实事物建模的问题。 12 | 13 | 说到字典这个词,大家一定不陌生,读小学的时候每个人基本上都有一本《新华字典》,如下图所示。 14 | 15 | ![dictionary](https://github.com/jackfrued/mypic/raw/master/20210820204829.jpg) 16 | 17 | Python程序中的字典跟现实生活中的字典很像,它以键值对(键和值的组合)的方式把数据组织到一起,我们可以通过键找到与之对应的值并进行操作。就像《新华字典》中,每个字(键)都有与它对应的解释(值)一样,每个字和它的解释合在一起就是字典中的一个条目,而字典中通常包含了很多个这样的条目。 18 | 19 | ### 创建和使用字典 20 | 21 | 在Python中创建字典可以使用`{}`字面量语法,这一点跟上一节课讲的集合是一样的。但是字典的`{}`中的元素是以键值对的形式存在的,每个元素由`:`分隔的两个值构成,`:`前面是键,`:`后面是值,代码如下所示。 22 | 23 | ```Python 24 | xinhua = { 25 | '麓': '山脚下', 26 | '路': '道,往来通行的地方;方面,地区:南~货,外~货;种类:他俩是一~人', 27 | '蕗': '甘草的别名', 28 | '潞': '潞水,水名,即今山西省的浊漳河;潞江,水名,即云南省的怒江' 29 | } 30 | print(xinhua) 31 | person = { 32 | 'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号', 33 | 'home': '中同仁路8号', 'tel': '13122334455', 'econtact': '13800998877' 34 | } 35 | print(person) 36 | ``` 37 | 38 | 通过上面的代码,相信大家已经看出来了,用字典来保存一个人的信息远远优于使用列表或元组,因为我们可以用`:`前面的键来表示条目的含义,而`:`后面就是这个条目所对应的值。 39 | 40 | 当然,如果愿意,我们也可以使用内置函数`dict`或者是字典的生成式语法来创建字典,代码如下所示。 41 | 42 | ```Python 43 | # dict函数(构造器)中的每一组参数就是字典中的一组键值对 44 | person = dict(name='王大锤', age=55, weight=60, home='中同仁路8号') 45 | print(person) # {'name': '王大锤', 'age': 55, 'weight': 60, 'home': '中同仁路8号'} 46 | 47 | # 可以通过Python内置函数zip压缩两个序列并创建字典 48 | items1 = dict(zip('ABCDE', '12345')) 49 | print(items1) # {'A': '1', 'B': '2', 'C': '3', 'D': '4', 'E': '5'} 50 | items2 = dict(zip('ABCDE', range(1, 10))) 51 | print(items2) # {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5} 52 | 53 | # 用字典生成式语法创建字典 54 | items3 = {x: x ** 3 for x in range(1, 6)} 55 | print(items3) # {1: 1, 2: 8, 3: 27, 4: 64, 5: 125} 56 | ``` 57 | 58 | 想知道字典中一共有多少组键值对,仍然是使用`len`函数;如果想对字典进行遍历,可以用`for`循环,但是需要注意,`for`循环只是对字典的键进行了遍历,不过没关系,在讲完字典的运算后,我们可以通过字典的键获取到和这个键对应的值。 59 | 60 | ```Python 61 | person = {'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号'} 62 | print(len(person)) # 4 63 | for key in person: 64 | print(key) 65 | ``` 66 | 67 | ### 字典的运算 68 | 69 | 对于字典类型来说,成员运算和索引运算肯定是最为重要的,前者可以判定指定的键在不在字典中,后者可以通过键获取对应的值或者向字典中加入新的键值对。值得注意的是,字典的索引不同于列表的索引,列表中的元素因为有属于自己有序号,所以列表的索引是一个整数;字典中因为保存的是键值对,所以字典的索引是键值对中的键,通过索引操作可以修改原来的值或者向字典中存入新的键值对。需要**特别提醒**大家注意的是,**字典中的键必须是不可变类型**,例如整数(`int`)、浮点数(`float`)、字符串(`str`)、元组(`tuple`)等类型的值;显然,列表(`list`)和集合(`set`)是不能作为字典中的键的,当然字典类型本身也不能再作为字典中的键,因为字典也是可变类型,但是字典可以作为字典中的值。关于可变类型不能作为字典中的键的原因,我们在后面的课程中再为大家详细说明。这里,我们先看看下面的代码,了解一下字典的成员运算和索引运算。 70 | 71 | ```Python 72 | person = {'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号'} 73 | # 检查name和tel两个键在不在person字典中 74 | print('name' in person, 'tel' in person) # True False 75 | # 通过age修将person字典中对应的值修改为25 76 | if 'age' in person: 77 | person['age'] = 25 78 | # 通过索引操作向person字典中存入新的键值对 79 | person['tel'] = '13122334455' 80 | person['signature'] = '你的男朋友是一个盖世垃圾,他会踏着五彩祥云去迎娶你的闺蜜' 81 | print('name' in person, 'tel' in person) # True True 82 | # 检查person字典中键值对的数量 83 | print(len(person)) # 6 84 | # 对字典的键进行循环并通索引运算获取键对应的值 85 | for key in person: 86 | print(f'{key}: {person[key]}') 87 | ``` 88 | 89 | 需要注意,在通过索引运算获取字典中的值时,如指定的键没有在字典中,将会引发`KeyError`异常。 90 | 91 | ### 字典的方法 92 | 93 | 字典类型的方法基本上都跟字典的键值对操作相关,可以通过下面的例子来了解这些方法的使用。例如,我们要用一个字典来保存学生的信息,我们可以使用学生的学号作为字典中的键,通过学号做索引运算就可以得到对应的学生;我们可以把字典的值也做成一个字典,这样就可以用多组键值对分别存储学生的姓名、性别、年龄、籍贯等信息,代码如下所示。 94 | 95 | ```Python 96 | # 字典中的值又是一个字典(嵌套的字典) 97 | students = { 98 | 1001: {'name': '狄仁杰', 'sex': True, 'age': 22, 'place': '山西大同'}, 99 | 1002: {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'}, 100 | 1003: {'name': '武则天', 'sex': False, 'age': 20, 'place': '四川广元'} 101 | } 102 | 103 | # 使用get方法通过键获取对应的值,如果取不到不会引发KeyError异常而是返回None或设定的默认值 104 | print(students.get(1002)) # {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'} 105 | print(students.get(1005)) # None 106 | print(students.get(1005, {'name': '无名氏'})) # {'name': '无名氏'} 107 | 108 | # 获取字典中所有的键 109 | print(students.keys()) # dict_keys([1001, 1002, 1003]) 110 | # 获取字典中所有的值 111 | print(students.values()) # dict_values([{...}, {...}, {...}]) 112 | # 获取字典中所有的键值对 113 | print(students.items()) # dict_items([(1001, {...}), (1002, {....}), (1003, {...})]) 114 | # 对字典中所有的键值对进行循环遍历 115 | for key, value in students.items(): 116 | print(key, '--->', value) 117 | 118 | # 使用pop方法通过键删除对应的键值对并返回该值 119 | stu1 = students.pop(1002) 120 | print(stu1) # {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'} 121 | print(len(students)) # 2 122 | # stu2 = students.pop(1005) # KeyError: 1005 123 | stu2 = students.pop(1005, {}) 124 | print(stu2) # {} 125 | 126 | # 使用popitem方法删除字典中最后一组键值对并返回对应的二元组 127 | # 如果字典中没有元素,调用该方法将引发KeyError异常 128 | key, value = students.popitem() 129 | print(key, value) # 1003 {'name': '武则天', 'sex': False, 'age': 20, 'place': '四川广元'} 130 | 131 | # 如果这个键在字典中存在,setdefault返回原来与这个键对应的值 132 | # 如果这个键在字典中不存在,向字典中添加键值对,返回第二个参数的值,默认为None 133 | result = students.setdefault(1005, {'name': '方启鹤', 'sex': True}) 134 | print(result) # {'name': '方启鹤', 'sex': True} 135 | print(students) # {1001: {...}, 1005: {...}} 136 | 137 | # 使用update更新字典元素,相同的键会用新值覆盖掉旧值,不同的键会添加到字典中 138 | others = { 139 | 1005: {'name': '乔峰', 'sex': True, 'age': 32, 'place': '北京大兴'}, 140 | 1010: {'name': '王语嫣', 'sex': False, 'age': 19}, 141 | 1008: {'name': '钟灵', 'sex': False} 142 | } 143 | students.update(others) 144 | print(students) # {1001: {...}, 1005: {...}, 1010: {...}, 1008: {...}} 145 | ``` 146 | 147 | 跟列表一样,从字典中删除元素也可以使用`del`关键字,在删除元素的时候如果指定的键索引不到对应的值,一样会引发`KeyError`异常,具体的做法如下所示。 148 | 149 | ```Python 150 | person = {'name': '王大锤', 'age': 25, 'sex': True} 151 | del person['age'] 152 | print(person) # {'name': '王大锤', 'sex': True} 153 | ``` 154 | 155 | ### 字典的应用 156 | 157 | 我们通过几个简单的例子来讲解字典的应用。 158 | 159 | **例子1**:输入一段话,统计每个英文字母出现的次数。 160 | 161 | ```Python 162 | sentence = input('请输入一段话: ') 163 | counter = {} 164 | for ch in sentence: 165 | if 'A' <= ch <= 'Z' or 'a' <= ch <= 'z': 166 | counter[ch] = counter.get(ch, 0) + 1 167 | for key, value in counter.items(): 168 | print(f'字母{key}出现了{value}次.') 169 | ``` 170 | 171 | **例子2**:在一个字典中保存了股票的代码和价格,找出股价大于100元的股票并创建一个新的字典。 172 | 173 | > **说明**:可以用字典的生成式语法来创建这个新字典。 174 | 175 | ```Python 176 | stocks = { 177 | 'AAPL': 191.88, 178 | 'GOOG': 1186.96, 179 | 'IBM': 149.24, 180 | 'ORCL': 48.44, 181 | 'ACN': 166.89, 182 | 'FB': 208.09, 183 | 'SYMC': 21.29 184 | } 185 | stocks2 = {key: value for key, value in stocks.items() if value > 100} 186 | print(stocks2) 187 | ``` 188 | 189 | ### 简单的总结 190 | 191 | Python程序中的字典跟现实生活中字典非常像,允许我们**以键值对的形式保存数据**,再**通过键索引对应的值**。这是一种非常**有利于数据检索**的数据类型,底层原理我们在后续的课程中为大家讲解。再次提醒大家注意,**字典中的键必须是不可变类型**,字典中的值可以是任意类型。 192 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/linux,macos,python,pycharm,windows,sublimetext,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=linux,macos,python,pycharm,windows,sublimetext,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### PyCharm ### 48 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 49 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 50 | 51 | # User-specific stuff 52 | .idea/**/workspace.xml 53 | .idea/**/tasks.xml 54 | .idea/**/usage.statistics.xml 55 | .idea/**/dictionaries 56 | .idea/**/shelf 57 | 58 | # Generated files 59 | .idea/**/contentModel.xml 60 | 61 | # Sensitive or high-churn files 62 | .idea/**/dataSources/ 63 | .idea/**/dataSources.ids 64 | .idea/**/dataSources.local.xml 65 | .idea/**/sqlDataSources.xml 66 | .idea/**/dynamic.xml 67 | .idea/**/uiDesigner.xml 68 | .idea/**/dbnavigator.xml 69 | 70 | # Gradle 71 | .idea/**/gradle.xml 72 | .idea/**/libraries 73 | 74 | # Gradle and Maven with auto-import 75 | # When using Gradle or Maven with auto-import, you should exclude module files, 76 | # since they will be recreated, and may cause churn. Uncomment if using 77 | # auto-import. 78 | # .idea/modules.xml 79 | # .idea/*.iml 80 | # .idea/modules 81 | # *.iml 82 | # *.ipr 83 | 84 | # CMake 85 | cmake-build-*/ 86 | 87 | # Mongo Explorer plugin 88 | .idea/**/mongoSettings.xml 89 | 90 | # File-based project format 91 | *.iws 92 | 93 | # IntelliJ 94 | out/ 95 | 96 | # mpeltonen/sbt-idea plugin 97 | .idea_modules/ 98 | 99 | # JIRA plugin 100 | atlassian-ide-plugin.xml 101 | 102 | # Cursive Clojure plugin 103 | .idea/replstate.xml 104 | 105 | # Crashlytics plugin (for Android Studio and IntelliJ) 106 | com_crashlytics_export_strings.xml 107 | crashlytics.properties 108 | crashlytics-build.properties 109 | fabric.properties 110 | 111 | # Editor-based Rest Client 112 | .idea/httpRequests 113 | 114 | # Android studio 3.1+ serialized cache file 115 | .idea/caches/build_file_checksums.ser 116 | 117 | ### PyCharm Patch ### 118 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 119 | 120 | # *.iml 121 | # modules.xml 122 | # .idea/misc.xml 123 | # *.ipr 124 | 125 | # Sonarlint plugin 126 | .idea/**/sonarlint/ 127 | 128 | # SonarQube Plugin 129 | .idea/**/sonarIssues.xml 130 | 131 | # Markdown Navigator plugin 132 | .idea/**/markdown-navigator.xml 133 | .idea/**/markdown-navigator/ 134 | 135 | ### Python ### 136 | # Byte-compiled / optimized / DLL files 137 | __pycache__/ 138 | *.py[cod] 139 | *$py.class 140 | 141 | # C extensions 142 | *.so 143 | 144 | # Distribution / packaging 145 | .Python 146 | build/ 147 | develop-eggs/ 148 | dist/ 149 | downloads/ 150 | eggs/ 151 | .eggs/ 152 | lib/ 153 | lib64/ 154 | parts/ 155 | sdist/ 156 | var/ 157 | wheels/ 158 | pip-wheel-metadata/ 159 | share/python-wheels/ 160 | *.egg-info/ 161 | .installed.cfg 162 | *.egg 163 | MANIFEST 164 | 165 | # PyInstaller 166 | # Usually these files are written by a python script from a template 167 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 168 | *.manifest 169 | *.spec 170 | 171 | # Installer logs 172 | pip-log.txt 173 | pip-delete-this-directory.txt 174 | 175 | # Unit test / coverage reports 176 | htmlcov/ 177 | .tox/ 178 | .nox/ 179 | .coverage 180 | .coverage.* 181 | .cache 182 | nosetests.xml 183 | coverage.xml 184 | *.cover 185 | .hypothesis/ 186 | .pytest_cache/ 187 | 188 | # Translations 189 | *.mo 190 | *.pot 191 | 192 | # Scrapy stuff: 193 | .scrapy 194 | 195 | # Sphinx documentation 196 | docs/_build/ 197 | 198 | # PyBuilder 199 | target/ 200 | 201 | # pyenv 202 | .python-version 203 | 204 | # pipenv 205 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 206 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 207 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 208 | # install all needed dependencies. 209 | #Pipfile.lock 210 | 211 | # celery beat schedule file 212 | celerybeat-schedule 213 | 214 | # SageMath parsed files 215 | *.sage.py 216 | 217 | # Spyder project settings 218 | .spyderproject 219 | .spyproject 220 | 221 | # Rope project settings 222 | .ropeproject 223 | 224 | # Mr Developer 225 | .mr.developer.cfg 226 | .project 227 | .pydevproject 228 | 229 | # mkdocs documentation 230 | /site 231 | 232 | # mypy 233 | .mypy_cache/ 234 | .dmypy.json 235 | dmypy.json 236 | 237 | # Pyre type checker 238 | .pyre/ 239 | 240 | ### SublimeText ### 241 | # Cache files for Sublime Text 242 | *.tmlanguage.cache 243 | *.tmPreferences.cache 244 | *.stTheme.cache 245 | 246 | # Workspace files are user-specific 247 | *.sublime-workspace 248 | 249 | # Project files should be checked into the repository, unless a significant 250 | # proportion of contributors will probably not be using Sublime Text 251 | # *.sublime-project 252 | 253 | # SFTP configuration file 254 | sftp-config.json 255 | 256 | # Package control specific files 257 | Package Control.last-run 258 | Package Control.ca-list 259 | Package Control.ca-bundle 260 | Package Control.system-ca-bundle 261 | Package Control.cache/ 262 | Package Control.ca-certs/ 263 | Package Control.merged-ca-bundle 264 | Package Control.user-ca-bundle 265 | oscrypto-ca-bundle.crt 266 | bh_unicode_properties.cache 267 | 268 | # Sublime-github package stores a github token in this file 269 | # https://packagecontrol.io/packages/sublime-github 270 | GitHub.sublime-settings 271 | 272 | ### VisualStudioCode ### 273 | .vscode/* 274 | !.vscode/settings.json 275 | !.vscode/tasks.json 276 | !.vscode/launch.json 277 | !.vscode/extensions.json 278 | 279 | ### VisualStudioCode Patch ### 280 | # Ignore all local history of files 281 | .history 282 | 283 | ### Windows ### 284 | # Windows thumbnail cache files 285 | Thumbs.db 286 | Thumbs.db:encryptable 287 | ehthumbs.db 288 | ehthumbs_vista.db 289 | 290 | # Dump file 291 | *.stackdump 292 | 293 | # Folder config file 294 | [Dd]esktop.ini 295 | 296 | # Recycle Bin used on file shares 297 | $RECYCLE.BIN/ 298 | 299 | # Windows Installer files 300 | *.cab 301 | *.msi 302 | *.msix 303 | *.msm 304 | *.msp 305 | 306 | # Windows shortcuts 307 | *.lnk 308 | 309 | # End of https://www.gitignore.io/api/linux,macos,python,pycharm,windows,sublimetext,visualstudiocode 310 | -------------------------------------------------------------------------------- /第18课:面向对象编程进阶.md: -------------------------------------------------------------------------------- 1 | ## 第18课:面向对象编程进阶 2 | 3 | 上一节课我们讲解了Python面向对象编程的基础知识,这一节课我们继续来讨论面向对象编程相关的内容。 4 | 5 | ### 可见性和属性装饰器 6 | 7 | 在很多面向对象编程语言中,对象的属性通常会被设置为私有(private)或受保护(protected)的成员,简单的说就是不允许直接访问这些属性;对象的方法通常都是公开的(public),因为公开的方法是对象能够接受的消息,也是对象暴露给外界的调用接口,这就是所谓的访问可见性。在Python中,可以通过给对象属性名添加前缀下划线的方式来说明属性的访问可见性,例如,可以用`__name`表示一个私有属性,`_name`表示一个受保护属性,代码如下所示。 8 | 9 | ```Python 10 | class Student: 11 | 12 | def __init__(self, name, age): 13 | self.__name = name 14 | self.__age = age 15 | 16 | def study(self, course_name): 17 | print(f'{self.__name}正在学习{course_name}.') 18 | 19 | 20 | stu = Student('王大锤', 20) 21 | stu.study('Python程序设计') 22 | print(stu.__name) 23 | ``` 24 | 25 | 上面代码的最后一行会引发`AttributeError`(属性错误)异常,异常消息为:`'Student' object has no attribute '__name'`。由此可见,以`__`开头的属性`__name`是私有的,在类的外面无法直接访问,但是类里面的`study`方法中可以通过`self.__name`访问该属性。 26 | 27 | 需要提醒大家的是,Python并没有从语法上严格保证私有属性的私密性,它只是给私有的属性和方法换了一个名字来阻挠对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们,我们可以对上面的代码稍作修改就可以访问到私有的属性。 28 | 29 | ```Python 30 | class Student: 31 | 32 | def __init__(self, name, age): 33 | self.__name = name 34 | self.__age = age 35 | 36 | def study(self, course_name): 37 | print(f'{self.__name}正在学习{course_name}.') 38 | 39 | 40 | stu = Student('王大锤', 20) 41 | stu.study('Python程序设计') 42 | print(stu._Student__name, stu._Student__age) 43 | ``` 44 | 45 | Python中有一句名言:“**We are all consenting adults here**”(大家都是成年人)。Python语言的设计者认为程序员要为自己的行为负责,而不是由Python语言本身来严格限制访问可见性,而大多数的程序员都认为**开放比封闭要好**,把对象的属性私有化并不是编程语言必须的东西,所以Python并没有从语法上做出最严格的限定。 46 | 47 | Python中可以通过`property`装饰器为“私有”属性提供读取和修改的方法,之前我们提到过,装饰器通常会放在类、函数或方法的声明之前,通过一个`@`符号表示将装饰器应用于类、函数或方法。 48 | 49 | ```Python 50 | class Student: 51 | 52 | def __init__(self, name, age): 53 | self.__name = name 54 | self.__age = age 55 | 56 | # 属性访问器(getter方法) - 获取__name属性 57 | @property 58 | def name(self): 59 | return self.__name 60 | 61 | # 属性修改器(setter方法) - 修改__name属性 62 | @name.setter 63 | def name(self, name): 64 | # 如果name参数不为空就赋值给对象的__name属性 65 | # 否则将__name属性赋值为'无名氏',有两种写法 66 | # self.__name = name if name else '无名氏' 67 | self.__name = name or '无名氏' 68 | 69 | @property 70 | def age(self): 71 | return self.__age 72 | 73 | 74 | stu = Student('王大锤', 20) 75 | print(stu.name, stu.age) # 王大锤 20 76 | stu.name = '' 77 | print(stu.name) # 无名氏 78 | # stu.age = 30 # AttributeError: can't set attribute 79 | ``` 80 | 81 | 在实际项目开发中,我们并不经常使用私有属性,属性装饰器的使用也比较少,所以上面的知识点大家简单了解一下就可以了。 82 | 83 | ### 动态属性 84 | 85 | Python是一门动态语言,维基百科对动态语言的解释是:“在运行时可以改变其结构的语言,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。动态语言非常灵活,目前流行的Python和JavaScript都是动态语言,除此之外如PHP、Ruby等也都属于动态语言,而C、C++等语言则不属于动态语言”。 86 | 87 | 在Python中,我们可以动态为对象添加属性,这是Python作为动态类型语言的一项特权,代码如下所示。需要提醒大家的是,对象的方法其实本质上也是对象的属性,如果给对象发送一个无法接收的消息,引发的异常仍然是`AttributeError`。 88 | 89 | ```Python 90 | class Student: 91 | 92 | def __init__(self, name, age): 93 | self.name = name 94 | self.age = age 95 | 96 | 97 | stu = Student('王大锤', 20) 98 | # 为Student对象动态添加sex属性 99 | stu.sex = '男' 100 | ``` 101 | 102 | 如果不希望在使用对象时动态的为对象添加属性,可以使用Python的`__slots__`魔法。对于`Student`类来说,可以在类中指定`__slots__ = ('name', 'age')`,这样`Student`类的对象只能有`name`和`age`属性,如果想动态添加其他属性将会引发异常,代码如下所示。 103 | 104 | ```Python 105 | class Student: 106 | __slots__ = ('name', 'age') 107 | 108 | def __init__(self, name, age): 109 | self.name = name 110 | self.age = age 111 | 112 | 113 | stu = Student('王大锤', 20) 114 | # AttributeError: 'Student' object has no attribute 'sex' 115 | stu.sex = '男' 116 | ``` 117 | 118 | ### 静态方法和类方法 119 | 120 | 之前我们在类中定义的方法都是对象方法,换句话说这些方法都是对象可以接收的消息。除了对象方法之外,类中还可以有静态方法和类方法,这两类方法是发给类的消息,二者并没有实质性的区别。在面向对象的世界里,一切皆为对象,我们定义的每一个类其实也是一个对象,而静态方法和类方法就是发送给类对象的消息。那么,什么样的消息会直接发送给类对象呢? 121 | 122 | 举一个例子,定义一个三角形类,通过传入三条边的长度来构造三角形,并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法,这一点毫无疑问。但是在创建三角形对象时,传入的三条边长未必能构造出三角形,为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形,这种方法很显然就不是对象方法,因为在调用这个方法时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法,也就是说这类方法不是发送给三角形对象的消息,而是发送给三角形类的消息,代码如下所示。 123 | 124 | ```Python 125 | class Triangle(object): 126 | """三角形类""" 127 | 128 | def __init__(self, a, b, c): 129 | """初始化方法""" 130 | self.a = a 131 | self.b = b 132 | self.c = c 133 | 134 | @staticmethod 135 | def is_valid(a, b, c): 136 | """判断三条边长能否构成三角形(静态方法)""" 137 | return a + b > c and b + c > a and a + c > b 138 | 139 | # @classmethod 140 | # def is_valid(cls, a, b, c): 141 | # """判断三条边长能否构成三角形(类方法)""" 142 | # return a + b > c and b + c > a and a + c > b 143 | 144 | def perimeter(self): 145 | """计算周长""" 146 | return self.a + self.b + self.c 147 | 148 | def area(self): 149 | """计算面积""" 150 | p = self.perimeter() / 2 151 | return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5 152 | ``` 153 | 154 | 上面的代码使用`staticmethod`装饰器声明了`is_valid`方法是`Triangle`类的静态方法,如果要声明类方法,可以使用`classmethod`装饰器。可以直接使用`类名.方法名`的方式来调用静态方法和类方法,二者的区别在于,类方法的第一个参数是类对象本身,而静态方法则没有这个参数。简单的总结一下,**对象方法、类方法、静态方法都可以通过`类名.方法名`的方式来调用,区别在于方法的第一个参数到底是普通对象还是类对象,还是没有接受消息的对象**。静态方法通常也可以直接写成一个独立的函数,因为它并没有跟特定的对象绑定。 155 | 156 | ### 继承和多态 157 | 158 | 面向对象的编程语言支持在已有类的基础上创建新类,从而减少重复代码的编写。提供继承信息的类叫做父类(超类、基类),得到继承信息的类叫做子类(派生类、衍生类)。例如,我们定义一个学生类和一个老师类,我们会发现他们有大量的重复代码,而这些重复代码都是老师和学生作为人的公共属性和行为,所以在这种情况下,我们应该先定义人类,再通过继承,从人类派生出老师类和学生类,代码如下所示。 159 | 160 | ```Python 161 | class Person: 162 | """人类""" 163 | 164 | def __init__(self, name, age): 165 | self.name = name 166 | self.age = age 167 | 168 | def eat(self): 169 | print(f'{self.name}正在吃饭.') 170 | 171 | def sleep(self): 172 | print(f'{self.name}正在睡觉.') 173 | 174 | 175 | class Student(Person): 176 | """学生类""" 177 | 178 | def __init__(self, name, age): 179 | # super(Student, self).__init__(name, age) 180 | super().__init__(name, age) 181 | 182 | def study(self, course_name): 183 | print(f'{self.name}正在学习{course_name}.') 184 | 185 | 186 | class Teacher(Person): 187 | """老师类""" 188 | 189 | def __init__(self, name, age, title): 190 | # super(Teacher, self).__init__(name, age) 191 | super().__init__(name, age) 192 | self.title = title 193 | 194 | def teach(self, course_name): 195 | print(f'{self.name}{self.title}正在讲授{course_name}.') 196 | 197 | 198 | 199 | stu1 = Student('白元芳', 21) 200 | stu2 = Student('狄仁杰', 22) 201 | teacher = Teacher('武则天', 35, '副教授') 202 | stu1.eat() 203 | stu2.sleep() 204 | teacher.teach('Python程序设计') 205 | stu1.study('Python程序设计') 206 | ``` 207 | 208 | 继承的语法是在定义类的时候,在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定它的父类是谁,那么默认的父类是`object`类。`object`类是Python中的顶级类,这也就意味着所有的类都是它的子类,要么直接继承它,要么间接继承它。Python语言允许多重继承,也就是说一个类可以有一个或多个父类,关于多重继承的问题我们在后面会有更为详细的讨论。在子类的初始化方法中,我们可以通过`super().__init__()`来调用父类初始化方法,`super`函数是Python内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出,子类除了可以通过继承得到父类提供的属性和方法外,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力。在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,也叫做“里氏替换原则”(Liskov Substitution Principle)。 209 | 210 | 子类继承父类的方法后,还可以对方法进行重写(重新实现该方法),不同的子类可以对父类的同一个方法给出不同的实现版本,这样的方法在程序运行时就会表现出多态行为(调用相同的方法,做了不同的事情)。多态是面向对象编程中最精髓的部分,当然也是对初学者来说最难以理解和灵活运用的部分,我们会在下一节课中用专门的例子来讲解多态这个知识点。 211 | 212 | ### 简单的总结 213 | 214 | Python是动态语言,Python中的对象可以动态的添加属性。在面向对象的世界中,**一切皆为对象**,我们定义的类也是对象,所以**类也可以接收消息**,对应的方法是类方法或静态方法。通过继承,我们**可以从已有的类创建新类**,实现对已有类代码的复用。 215 | -------------------------------------------------------------------------------- /第36课:Python中的并发编程-3.md: -------------------------------------------------------------------------------- 1 | ## 第36课:Python中的并发编程-3 2 | 3 | 爬虫是典型的 I/O 密集型任务,I/O 密集型任务的特点就是程序会经常性的因为 I/O 操作而进入阻塞状态,比如我们之前使用`requests`获取页面代码或二进制内容,发出一个请求之后,程序必须要等待网站返回响应之后才能继续运行,如果目标网站不是很给力或者网络状况不是很理想,那么等待响应的时间可能会很久,而在这个过程中整个程序是一直阻塞在那里,没有做任何的事情。通过前面的课程,我们已经知道了可以通过多线程的方式为爬虫提速,使用多线程的本质就是,当一个线程阻塞的时候,程序还有其他的线程可以继续运转,因此整个程序就不会在阻塞和等待中浪费了大量的时间。 4 | 5 | 事实上,还有一种非常适合 I/O 密集型任务的并发编程方式,我们称之为异步编程,你也可以将它称为异步 I/O。这种方式并不需要启动多个线程或多个进程来实现并发,它是通过多个子程序相互协作的方式来提升 CPU 的利用率,解决了 I/O 密集型任务 CPU 利用率很低的问题,我一般将这种方式称为“协作式并发”。这里,我不打算探讨操作系统的各种 I/O 模式,因为这对很多读者来说都太过抽象;但是我们得先抛出两组概念给大家,一组叫做“阻塞”和“非阻塞”,一组叫做“同步”和“异步”。 6 | 7 | ### 基本概念 8 | 9 | #### 阻塞 10 | 11 | 阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续处理其他的事情,则称该程序在该操作上是阻塞的。阻塞随时都可能发生,最典型的就是 I/O 中断(包括网络 I/O 、磁盘 I/O 、用户输入等)、休眠操作、等待某个线程执行结束,甚至包括在 CPU 切换上下文时,程序都无法真正的执行,这就是所谓的阻塞。 12 | 13 | #### 非阻塞 14 | 15 | 程序在等待某操作过程中,自身不被阻塞,可以继续处理其他的事情,则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。显然,某个操作的阻塞可能会导程序耗时以及效率低下,所以我们会希望把它变成非阻塞的。 16 | 17 | #### 同步 18 | 19 | 不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,我们称这些程序单元是同步执行的。例如前面讲过的给银行账户存钱的操作,我们在代码中使用了“锁”作为通信信号,让多个存钱操作强制排队顺序执行,这就是所谓的同步。 20 | 21 | #### 异步 22 | 23 | 不同程序单元在执行过程中无需通信协调,也能够完成一个任务,这种方式我们就称之为异步。例如,使用爬虫下载页面时,调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是不相关的,也无需相互通知协调。很显然,异步操作的完成时刻和先后顺序并不能确定。 24 | 25 | 很多人都不太能准确的把握这几个概念,这里我们简单的总结一下,同步与异步的关注点是**消息通信机制**,最终表现出来的是“有序”和“无序”的区别;阻塞和非阻塞的关注点是**程序在等待消息时状态**,最终表现出来的是程序在等待时能不能做点别的。如果想深入理解这些内容,推荐大家阅读经典著作[《UNIX网络编程》](https://item.jd.com/11880047.html),这本书非常的赞。 26 | 27 | ### 生成器和协程 28 | 29 | 前面我们说过,异步编程是一种“协作式并发”,即通过多个子程序相互协作的方式提升 CPU 的利用率,从而减少程序在阻塞和等待中浪费的时间,最终达到并发的效果。我们可以将多个相互协作的子程序称为“协程”,它是实现异步编程的关键。在介绍协程之前,我们先通过下面的代码,看看什么是生成器。 30 | 31 | ```Python 32 | def fib(max_count): 33 | a, b = 0, 1 34 | for _ in range(max_count): 35 | a, b = b, a + b 36 | yield a 37 | ``` 38 | 39 | 上面我们编写了一个生成斐波那契数列的生成器,调用上面的`fib`函数并不是执行该函数获得返回值,因为`fib`函数中有一个特殊的关键字`yield`。这个关键字使得`fib`函数跟普通的函数有些区别,调用该函数会得到一个生成器对象,我们可以通过下面的代码来验证这一点。 40 | 41 | ```Python 42 | gen_obj = fib(20) 43 | print(gen_obj) 44 | ``` 45 | 46 | 输出: 47 | 48 | ``` 49 | 50 | ``` 51 | 52 | 我们可以使用内置函数`next`从生成器对象中获取斐波那契数列的值,也可以通过`for-in`循环对生成器能够提供的值进行遍历,代码如下所示。 53 | 54 | ```Python 55 | for value in gen_obj: 56 | print(value) 57 | ``` 58 | 59 | 生成器经过预激活,就是一个协程,它可以跟其他子程序协作。 60 | 61 | ```Python 62 | def calc_average(): 63 | total, counter = 0, 0 64 | avg_value = None 65 | while True: 66 | curr_value = yield avg_value 67 | total += curr_value 68 | counter += 1 69 | avg_value = total / counter 70 | 71 | 72 | def main(): 73 | obj = calc_average() 74 | # 生成器预激活 75 | obj.send(None) 76 | for _ in range(5): 77 | print(obj.send(float(input()))) 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | ``` 83 | 84 | 上面的`main`函数首先通过生成器对象的`send`方法发送一个`None`值来将其激活为协程,也可以通过`next(obj)`达到同样的效果。接下来,协程对象会接收`main`函数发送的数据并产出(`yield`)数据的平均值。通过上面的例子,不知道大家是否看出两段子程序是怎么“协作”的。 85 | 86 | ### 异步函数 87 | 88 | Python 3.5版本中,引入了两个非常有意思的元素,一个叫`async`,一个叫`await`,它们在Python 3.7版本中成为了正式的关键字。通过这两个关键字,可以简化协程代码的编写,可以用更为简单的方式让多个子程序很好的协作起来。我们通过一个例子来加以说明,请大家先看看下面的代码。 89 | 90 | ```Python 91 | import time 92 | 93 | 94 | def display(num): 95 | time.sleep(1) 96 | print(num) 97 | 98 | 99 | def main(): 100 | start = time.time() 101 | for i in range(1, 10): 102 | display(i) 103 | end = time.time() 104 | print(f'{end - start:.3f}秒') 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | ``` 110 | 111 | 上面的代码每次执行都会依次输出`1`到`9`的数字,每个间隔`1`秒钟,整个代码需要执行大概需要`9`秒多的时间,这一点我相信大家都能看懂。不知道大家是否意识到,这段代码就是以同步和阻塞的方式执行的,同步可以从代码的输出看出来,而阻塞是指在调用`display`函数发生休眠时,整个代码的其他部分都不能继续执行,必须等待休眠结束。 112 | 113 | 接下来,我们尝试用异步的方式改写上面的代码,让`display`函数以异步的方式运转。 114 | 115 | ```Python 116 | import asyncio 117 | import time 118 | 119 | 120 | async def display(num): 121 | await asyncio.sleep(1) 122 | print(num) 123 | 124 | 125 | def main(): 126 | start = time.time() 127 | objs = [display(i) for i in range(1, 10)] 128 | loop = asyncio.get_event_loop() 129 | loop.run_until_complete(asyncio.wait(objs)) 130 | loop.close() 131 | end = time.time() 132 | print(f'{end - start:.3f}秒') 133 | 134 | 135 | if __name__ == '__main__': 136 | main() 137 | ``` 138 | 139 | Python 中的`asyncio`模块提供了对异步 I/O 的支持。上面的代码中,我们首先在`display`函数前面加上了`async`关键字使其变成一个异步函数,调用异步函数不会执行函数体而是获得一个协程对象。我们将`display`函数中的`time.sleep(1)`修改为`await asyncio.sleep(1)`,二者的区别在于,后者不会让整个代码陷入阻塞,因为`await`操作会让其他协作的子程序有获得 CPU 资源而得以运转的机会。为了让这些子程序可以协作起来,我们需要将他们放到一个事件循环(实现消息分派传递的系统)上,因为**当协程遭遇 I/O 操作阻塞时,就会到事件循环中监听 I/O 操作是否完成,并注册自身的上下文以及自身的唤醒函数(以便恢复执行),之后该协程就变为阻塞状态**。上面的第12行代码创建了`9`个协程对象并放到一个列表中,第13行代码通过`asyncio`模块的`get_event_loop`函数获得了系统的事件循环,第14行通过`asyncio`模块的`run_until_complete`函数将协程对象挂载到事件循环上。执行上面的代码会发现,`9`个分别会阻塞`1`秒钟的协程总共只阻塞了约`1`秒种的时间,因为**阻塞的协程对象会放弃对 CPU 的占有而不是让 CPU 处于闲置状态,这种方式大大的提升了 CPU 的利用率**。而且我们还会注意到,数字并不是按照从`1`到`9`的顺序打印输出的,这正是我们想要的结果,说明它们是**异步执行**的。对于爬虫这样的 I/O 密集型任务来说,这种协作式并发在很多场景下是比使用多线程更好的选择,因为这种做法减少了管理和维护多个线程以及多个线程切换所带来的开销。 140 | 141 | ### aiohttp库 142 | 143 | 我们之前使用的`requests`三方库并不支持异步 I/O,如果希望使用异步 I/O 的方式来加速爬虫代码的执行,我们可以安装和使用名为`aiohttp`的三方库。 144 | 145 | 安装`aiohttp`。 146 | 147 | ```Bash 148 | pip install aiohttp 149 | ``` 150 | 151 | 下面的代码使用`aiohttp`抓取了`10`个网站的首页并解析出它们的标题。 152 | 153 | ```Python 154 | import asyncio 155 | import re 156 | 157 | import aiohttp 158 | from aiohttp import ClientSession 159 | 160 | TITLE_PATTERN = re.compile(r'(.*?)', re.DOTALL) 161 | 162 | 163 | async def fetch_page_title(url): 164 | async with aiohttp.ClientSession(headers={ 165 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36', 166 | }) as session: # type: ClientSession 167 | async with session.get(url, ssl=False) as resp: 168 | if resp.status == 200: 169 | html_code = await resp.text() 170 | matcher = TITLE_PATTERN.search(html_code) 171 | title = matcher.group(1).strip() 172 | print(title) 173 | 174 | 175 | def main(): 176 | urls = [ 177 | 'https://www.python.org/', 178 | 'https://www.jd.com/', 179 | 'https://www.baidu.com/', 180 | 'https://www.taobao.com/', 181 | 'https://git-scm.com/', 182 | 'https://www.sohu.com/', 183 | 'https://gitee.com/', 184 | 'https://www.amazon.com/', 185 | 'https://www.usa.gov/', 186 | 'https://www.nasa.gov/' 187 | ] 188 | objs = [fetch_page_title(url) for url in urls] 189 | loop = asyncio.get_event_loop() 190 | loop.run_until_complete(asyncio.wait(objs)) 191 | loop.close() 192 | 193 | 194 | if __name__ == '__main__': 195 | main() 196 | ``` 197 | 198 | 输出: 199 | 200 | ``` 201 | 京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物! 202 | 搜狐 203 | 淘宝网 - 淘!我喜欢 204 | 百度一下,你就知道 205 | Gitee - 基于 Git 的代码托管和研发协作平台 206 | Git 207 | NASA 208 | Official Guide to Government Information and Services | USAGov 209 | Amazon.com. Spend less. Smile more. 210 | Welcome to Python.org 211 | ``` 212 | 213 | 从上面的输出可以看出,网站首页标题的输出顺序跟它们的 URL 在列表中的顺序没有关系。代码的第11行到第13行创建了`ClientSession`对象,通过它的`get`方法可以向指定的 URL 发起请求,如第14行所示,跟`requests`中的`Session`对象并没有本质区别,唯一的区别是这里使用了异步上下文。代码第16行的`await`会让因为 I/O 操作阻塞的子程序放弃对 CPU 的占用,这使得其他的子程序可以运转起来去抓取页面。代码的第17行和第18行使用了正则表达式捕获组操作解析网页标题。`fetch_page_title`是一个被`async`关键字修饰的异步函数,调用该函数会获得协程对象,如代码第35行所示。后面的代码跟之前的例子没有什么区别,相信大家能够理解。 214 | 215 | 大家可以尝试将`aiohttp`换回到`requests`,看看不使用异步 I/O 也不使用多线程,到底和上面的代码有什么区别,相信通过这样的对比,大家能够更深刻的理解我们之前强调的几个概念:同步和异步,阻塞和非阻塞。 216 | -------------------------------------------------------------------------------- /第16课:函数的高级应用.md: -------------------------------------------------------------------------------- 1 | ## 第16课:函数的高级应用 2 | 3 | 在上一节课中,我们已经对函数进行了更为深入的研究,还探索了Python中的高阶函数和Lambda函数。在这些知识的基础上,这节课我们为大家分享两个和函数相关的内容,一个是装饰器,一个是函数的递归调用。 4 | 5 | ### 装饰器 6 | 7 | 装饰器是Python中**用一个函数装饰另外一个函数或类并为其提供额外功能**的语法现象。装饰器本身是一个函数,它的参数是被装饰的函数或类,它的返回值是一个带有装饰功能的函数。很显然,装饰器是一个高阶函数,它的参数和返回值都是函数。下面我们先通过一个简单的例子来说明装饰器的写法和作用,假设已经有名为`downlaod`和`upload`的两个函数,分别用于文件的上传和下载,下面的代码用休眠一段随机时间的方式模拟了下载和上传需要花费的时间,并没有联网做上传下载。 8 | 9 | > **说明**:用Python语言实现联网的上传下载也很简单,继续你的学习,这个环节很快就会来到。 10 | 11 | ```Python 12 | import random 13 | import time 14 | 15 | 16 | def download(filename): 17 | print(f'开始下载{filename}.') 18 | time.sleep(random.randint(2, 6)) 19 | print(f'{filename}下载完成.') 20 | 21 | 22 | def upload(filename): 23 | print(f'开始上传{filename}.') 24 | time.sleep(random.randint(4, 8)) 25 | print(f'{filename}上传完成.') 26 | 27 | 28 | download('MySQL从删库到跑路.avi') 29 | upload('Python从入门到住院.pdf') 30 | ``` 31 | 32 | 现在我们希望知道调用`download`和`upload`函数做文件上传下载到底用了多少时间,这个应该如何实现呢?相信很多小伙伴已经想到了,我们可以在函数开始执行的时候记录一个时间,在函数调用结束后记录一个时间,两个时间相减就可以计算出下载或上传的时间,代码如下所示。 33 | 34 | ```Python 35 | start = time.time() 36 | download('MySQL从删库到跑路.avi') 37 | end = time.time() 38 | print(f'花费时间: {end - start:.3f}秒') 39 | start = time.time() 40 | upload('Python从入门到住院.pdf') 41 | end = time.time() 42 | print(f'花费时间: {end - start:.3f}秒') 43 | ``` 44 | 45 | 通过上面的代码,我们可以得到下载和上传花费的时间,但不知道大家是否注意到,上面记录时间、计算和显示执行时间的代码都是重复代码。有编程经验的人都知道,**重复的代码是万恶之源**,那么有没有办法在不写重复代码的前提下,用一种简单优雅的方式记录下函数的执行时间呢?在Python中,装饰器就是解决这类问题的最佳选择。我们可以把记录函数执行时间的功能封装到一个装饰器中,在有需要的地方直接使用这个装饰器就可以了,代码如下所示。 46 | 47 | ```Python 48 | import time 49 | 50 | 51 | # 定义装饰器函数,它的参数是被装饰的函数或类 52 | def record_time(func): 53 | 54 | # 定义一个带装饰功能(记录被装饰函数的执行时间)的函数 55 | # 因为不知道被装饰的函数有怎样的参数所以使用*args和**kwargs接收所有参数 56 | # 在Python中函数可以嵌套的定义(函数中可以再定义函数) 57 | def wrapper(*args, **kwargs): 58 | # 在执行被装饰的函数之前记录开始时间 59 | start = time.time() 60 | # 执行被装饰的函数并获取返回值 61 | result = func(*args, **kwargs) 62 | # 在执行被装饰的函数之后记录结束时间 63 | end = time.time() 64 | # 计算和显示被装饰函数的执行时间 65 | print(f'{func.__name__}执行时间: {end - start:.3f}秒') 66 | # 返回被装饰函数的返回值(装饰器通常不会改变被装饰函数的执行结果) 67 | return result 68 | 69 | # 返回带装饰功能的wrapper函数 70 | return wrapper 71 | ``` 72 | 73 | 使用上面的装饰器函数有两种方式,第一种方式就是直接调用装饰器函数,传入被装饰的函数并获得返回值,我们可以用这个返回值直接覆盖原来的函数,那么在调用时就已经获得了装饰器提供的额外的功能(记录执行时间),大家可以试试下面的代码就明白了。 74 | 75 | ```Python 76 | download = record_time(download) 77 | upload = record_time(upload) 78 | download('MySQL从删库到跑路.avi') 79 | upload('Python从入门到住院.pdf') 80 | ``` 81 | 82 | 上面的代码中已经没有重复代码了,虽然写装饰器会花费一些心思,但是这是一个一劳永逸的骚操作,如果还有其他的函数也需要记录执行时间,按照上面的代码如法炮制即可。 83 | 84 | 在Python中,使用装饰器很有更为便捷的**语法糖**(编程语言中添加的某种语法,这种语法对语言的功能没有影响,但是使用更加方法,代码的可读性也更强,我们将其称之为“语法糖”或“糖衣语法”),可以用`@装饰器函数`将装饰器函数直接放在被装饰的函数上,效果跟上面的代码相同,下面是完整的代码。 85 | 86 | ```Python 87 | import random 88 | import time 89 | 90 | 91 | def record_time(func): 92 | 93 | def wrapper(*args, **kwargs): 94 | start = time.time() 95 | result = func(*args, **kwargs) 96 | end = time.time() 97 | print(f'{func.__name__}执行时间: {end - start:.3f}秒') 98 | return result 99 | 100 | return wrapper 101 | 102 | 103 | @record_time 104 | def download(filename): 105 | print(f'开始下载{filename}.') 106 | time.sleep(random.randint(2, 6)) 107 | print(f'{filename}下载完成.') 108 | 109 | 110 | @record_time 111 | def upload(filename): 112 | print(f'开始上传{filename}.') 113 | time.sleep(random.randint(4, 8)) 114 | print(f'{filename}上传完成.') 115 | 116 | 117 | download('MySQL从删库到跑路.avi') 118 | upload('Python从入门到住院.pdf') 119 | ``` 120 | 121 | 上面的代码,我们通过装饰器语法糖为`download`和`upload`函数添加了装饰器,这样调用`download`和`upload`函数时,会记录下函数的执行时间。事实上,被装饰后的`download`和`upload`函数是我们在装饰器`record_time`中返回的`wrapper`函数,调用它们其实就是在调用`wrapper`函数,所以拥有了记录函数执行时间的功能。 122 | 123 | 如果希望取消装饰器的作用,那么在定义装饰器函数的时候,需要做一些额外的工作。Python标准库`functools`模块的`wraps`函数也是一个装饰器,我们将它放在`wrapper`函数上,这个装饰器可以帮我们保留被装饰之前的函数,这样在需要取消装饰器时,可以通过被装饰函数的`__wrapped__`属性获得被装饰之前的函数。 124 | 125 | ```Python 126 | import random 127 | import time 128 | 129 | from functools import wraps 130 | 131 | 132 | def record_time(func): 133 | 134 | @wraps(func) 135 | def wrapper(*args, **kwargs): 136 | start = time.time() 137 | result = func(*args, **kwargs) 138 | end = time.time() 139 | print(f'{func.__name__}执行时间: {end - start:.3f}秒') 140 | return result 141 | 142 | return wrapper 143 | 144 | 145 | @record_time 146 | def download(filename): 147 | print(f'开始下载{filename}.') 148 | time.sleep(random.randint(2, 6)) 149 | print(f'{filename}下载完成.') 150 | 151 | 152 | @record_time 153 | def upload(filename): 154 | print(f'开始上传{filename}.') 155 | time.sleep(random.randint(4, 8)) 156 | print(f'{filename}上传完成.') 157 | 158 | 159 | download('MySQL从删库到跑路.avi') 160 | upload('Python从入门到住院.pdf') 161 | # 取消装饰器 162 | download.__wrapped__('MySQL必知必会.pdf') 163 | upload = upload.__wrapped__ 164 | upload('Python从新手到大师.pdf') 165 | ``` 166 | 167 | **装饰器函数本身也可以参数化**,简单的说就是通过我们的装饰器也是可以通过调用者传入的参数来定制的,这个知识点我们在后面用到它的时候再为大家讲解。 168 | 169 | ### 递归调用 170 | 171 | Python中允许函数嵌套定义,也允许函数之间相互调用,而且一个函数还可以直接或间接的调用自身。函数自己调用自己称为递归调用,那么递归调用有什么用处呢?现实中,有很多问题的定义本身就是一个递归定义,例如我们之前讲到的阶乘,非负整数`N`的阶乘是`N`乘以`N-1`的阶乘,即 $ N! = N \times (N-1)! $ ,定义的左边和右边都出现了阶乘的概念,所以这是一个递归定义。既然如此,我们可以使用递归调用的方式来写一个求阶乘的函数,代码如下所示。 172 | 173 | ```Python 174 | def fac(num): 175 | if num in (0, 1): 176 | return 1 177 | return num * fac(num - 1) 178 | ``` 179 | 180 | 上面的代码中,`fac`函数中又调用了`fac`函数,这就是所谓的递归调用。代码第2行的`if`条件叫做递归的收敛条件,简单的说就是什么时候要结束函数的递归调用,在计算阶乘时,如果计算到`0`或`1`的阶乘,就停止递归调用,直接返回`1`;代码第4行的`num * fac(num - 1)`是递归公式,也就是阶乘的递归定义。下面,我们简单的分析下,如果用`fac(5)`计算`5`的阶乘,整个过程会是怎样的。 181 | 182 | ```Python 183 | # 递归调用函数入栈 184 | # 5 * fac(4) 185 | # 5 * (4 * fac(3)) 186 | # 5 * (4 * (3 * fac(2))) 187 | # 5 * (4 * (3 * (2 * fac(1)))) 188 | # 停止递归函数出栈 189 | # 5 * (4 * (3 * (2 * 1))) 190 | # 5 * (4 * (3 * 2)) 191 | # 5 * (4 * 6) 192 | # 5 * 24 193 | # 120 194 | print(fac(5)) # 120 195 | ``` 196 | 197 | 注意,函数调用会通过内存中称为“栈”(stack)的数据结构来保存当前代码的执行现场,函数调用结束后会通过这个栈结构恢复之前的执行现场。栈是一种先进后出的数据结构,这也就意味着最早入栈的函数最后才会返回,而最后入栈的函数会最先返回。例如调用一个名为`a`的函数,函数`a`的执行体中又调用了函数`b`,函数`b`的执行体中又调用了函数`c`,那么最先入栈的函数是`a`,最先出栈的函数是`c`。每进入一个函数调用,栈就会增加一层栈帧(stack frame),栈帧就是我们刚才提到的保存当前代码执行现场的结构;每当函数调用结束后,栈就会减少一层栈帧。通常,内存中的栈空间很小,因此递归调用的次数如果太多,会导致栈溢出(stack overflow),所以**递归调用一定要确保能够快速收敛**。我们可以尝试执行`fac(5000)`,看看是不是会提示`RecursionError`错误,错误消息为:`maximum recursion depth exceeded in comparison`(超出最大递归深度),其实就是发生了栈溢出。 198 | 199 | 我们使用的Python官方解释器,默认将函数调用的栈结构最大深度设置为`1000`层。如果超出这个深度,就会发生上面说的`RecursionError`。当然,我们可以使用`sys`模块的`setrecursionlimit`函数来改变递归调用的最大深度,例如:`sys.setrecursionlimit(10000)`,这样就可以让上面的`fac(5000)`顺利执行出结果,但是我们不建议这样做,因为让递归快速收敛才是我们应该做的事情,否则就应该考虑使用循环递推而不是递归。 200 | 201 | 再举一个之前讲过的生成斐波那契数列的例子,因为斐波那契数列前两个数都是`1`,从第3个数开始,每个数是前两个数相加的和,可以记为`f(n) = f(n - 1) + f(n - 2)`,很显然这又是一个递归的定义,所以我们可以用下面的递归调用函数来计算第​`n`个斐波那契数。 202 | 203 | ```Python 204 | def fib(n): 205 | if n in (1, 2): 206 | return 1 207 | return fib(n - 1) + fib(n - 2) 208 | 209 | 210 | # 打印前20个斐波那契数 211 | for i in range(1, 21): 212 | print(fib(i)) 213 | ``` 214 | 215 | 需要提醒大家,上面计算斐波那契数的代码虽然看起来非常简单明了,但执行性能是比较糟糕的,原因大家可以自己思考一下,更好的做法还是之前讲过的使用循环递推的方式,代码如下所示。 216 | 217 | ```Python 218 | def fib(n): 219 | a, b = 0, 1 220 | for _ in range(n): 221 | a, b = b, a + b 222 | return a 223 | ``` 224 | 225 | ### 简单的总结 226 | 227 | 装饰器是Python中的特色语法,**可以通过装饰器来增强现有的函数**,这是一种非常有用的编程技巧。一些复杂的问题用函数递归调用的方式写起来真的很简单,但是**函数的递归调用一定要注意收敛条件和递归公式**,找到递归公式才有机会使用递归调用,而收敛条件确定了递归什么时候停下来。函数调用通过内存中的栈空间来保存现场和恢复现场,栈空间通常都很小,所以**递归如果不能迅速收敛,很可能会引发栈溢出错误,从而导致程序的崩溃**。 228 | -------------------------------------------------------------------------------- /第29课:用Python发送邮件和短信.md: -------------------------------------------------------------------------------- 1 | ## 第29课:用Python发送邮件和短信 2 | 3 | 在前面的课程中,我们已经教会大家如何用Python程序自动的生成Excel、Word、PDF文档,接下来我们还可以更进一步,就是通过邮件将生成好的文档发送给指定的收件人,然后用短信告知对方我们发出了邮件。这些事情利用Python程序也可以轻松愉快的解决。 4 | 5 | ### 发送电子邮件 6 | 7 | 在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。 8 | 9 | 我们可以用HTTP(超文本传输协议)来访问网站资源,HTTP是一个应用级协议,它建立在TCP(传输控制协议)之上,TCP为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件,需要使用SMTP(简单邮件传输协议),它也是建立在TCP之上的应用级协议,规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python通过名为`smtplib`的模块将这些操作简化成了`SMTP_SSL`对象,通过该对象的`login`和`send_mail`方法,就能够完成发送邮件的操作。 10 | 11 | 我们先尝试一下发送一封极为简单的邮件,该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器,我们可以自己架设邮件服务器,这件事情对新手并不友好,但是我们可以选择使用第三方提供的邮件服务。例如,我在已经注册了账号,登录成功之后,就可以在设置中开启SMTP服务,这样就相当于获得了邮件服务器,具体的操作如下所示。 12 | 13 | image-20210820190306861 14 | 15 | ![image-20210820190816557](https://github.com/jackfrued/mypic/raw/master/20210820190816.png) 16 | 17 | 用手机扫码上面的二维码可以通过发送短信的方式来获取授权码,短信发送成功后,点击“我已发送”就可以获得授权码。授权码需要妥善保管,因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来,我们就可以编写发送邮件的代码了,如下所示。 18 | 19 | ```Python 20 | import smtplib 21 | from email.header import Header 22 | from email.mime.multipart import MIMEMultipart 23 | from email.mime.text import MIMEText 24 | 25 | # 创建邮件主体对象 26 | email = MIMEMultipart() 27 | # 设置发件人、收件人和主题 28 | email['From'] = 'xxxxxxxxx@126.com' 29 | email['To'] = 'yyyyyy@qq.com;zzzzzz@1000phone.com' 30 | email['Subject'] = Header('上半年工作情况汇报', 'utf-8') 31 | # 添加邮件正文内容 32 | content = """据德国媒体报道,当地时间9日,德国火车司机工会成员进行了投票, 33 | 定于当地时间10日起进行全国性罢工,货运交通方面的罢工已于当地时间10日19时开始。 34 | 此后,从11日凌晨2时到13日凌晨2时,德国全国范围内的客运和铁路基础设施将进行48小时的罢工。""" 35 | email.attach(MIMEText(content, 'plain', 'utf-8')) 36 | 37 | # 创建SMTP_SSL对象(连接邮件服务器) 38 | smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465) 39 | # 通过用户名和授权码进行登录 40 | smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码') 41 | # 发送邮件(发件人、收件人、邮件内容(字符串)) 42 | smtp_obj.sendmail( 43 | 'xxxxxxxxx@126.com', 44 | ['yyyyyy@qq.com', 'zzzzzz@1000phone.com'], 45 | email.as_string() 46 | ) 47 | ``` 48 | 49 | 如果要发送带有附件的邮件,只需要将附件的内容处理成BASE64编码,那么它就和普通的文本内容几乎没有什么区别。BASE64是一种基于64个可打印字符来表示二进制数据的表示方法,常用于某些需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。对这种编码方式不理解的同学,推荐阅读[《Base64笔记》](http://www.ruanyifeng.com/blog/2008/06/base64.html)一文。在之前的内容中,我们也提到过,Python标准库的`base64`模块提供了对BASE64编解码的支持。 50 | 51 | 下面的代码演示了如何发送带附件的邮件。 52 | 53 | ```Python 54 | import smtplib 55 | from email.header import Header 56 | from email.mime.multipart import MIMEMultipart 57 | from email.mime.text import MIMEText 58 | from urllib.parse import quote 59 | 60 | # 创建邮件主体对象 61 | email = MIMEMultipart() 62 | # 设置发件人、收件人和主题 63 | email['From'] = 'xxxxxxxxx@126.com' 64 | email['To'] = 'zzzzzzzz@1000phone.com' 65 | email['Subject'] = Header('请查收离职证明文件', 'utf-8') 66 | # 添加邮件正文内容(带HTML标签排版的内容) 67 | content = """

亲爱的前同事:

68 |

你需要的离职证明在附件中,请查收!

69 |
70 |

祝,好!

71 |
72 |

孙美丽 即日

""" 73 | email.attach(MIMEText(content, 'html', 'utf-8')) 74 | # 读取作为附件的文件 75 | with open(f'resources/王大锤离职证明.docx', 'rb') as file: 76 | attachment = MIMEText(file.read(), 'base64', 'utf-8') 77 | # 指定内容类型 78 | attachment['content-type'] = 'application/octet-stream' 79 | # 将中文文件名处理成百分号编码 80 | filename = quote('王大锤离职证明.docx') 81 | # 指定如何处置内容 82 | attachment['content-disposition'] = f'attachment; filename="{filename}"' 83 | 84 | # 创建SMTP_SSL对象(连接邮件服务器) 85 | smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465) 86 | # 通过用户名和授权码进行登录 87 | smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码') 88 | # 发送邮件(发件人、收件人、邮件内容(字符串)) 89 | smtp_obj.sendmail( 90 | 'xxxxxxxxx@126.com', 91 | 'zzzzzzzz@1000phone.com', 92 | email.as_string() 93 | ) 94 | ``` 95 | 96 | 为了方便大家用Python实现邮件发送,我将上面的代码封装成了函数,使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。 97 | 98 | ```Python 99 | import smtplib 100 | from email.header import Header 101 | from email.mime.multipart import MIMEMultipart 102 | from email.mime.text import MIMEText 103 | from urllib.parse import quote 104 | 105 | # 邮件服务器域名(自行修改) 106 | EMAIL_HOST = 'smtp.126.com' 107 | # 邮件服务端口(通常是465) 108 | EMAIL_PORT = 465 109 | # 登录邮件服务器的账号(自行修改) 110 | EMAIL_USER = 'xxxxxxxxx@126.com' 111 | # 开通SMTP服务的授权码(自行修改) 112 | EMAIL_AUTH = '邮件服务器的授权码' 113 | 114 | 115 | def send_email(*, from_user, to_users, subject='', content='', filenames=[]): 116 | """发送邮件 117 | 118 | :param from_user: 发件人 119 | :param to_users: 收件人,多个收件人用英文分号进行分隔 120 | :param subject: 邮件的主题 121 | :param content: 邮件正文内容 122 | :param filenames: 附件要发送的文件路径 123 | """ 124 | email = MIMEMultipart() 125 | email['From'] = from_user 126 | email['To'] = to_users 127 | email['Subject'] = subject 128 | 129 | message = MIMEText(content, 'plain', 'utf-8') 130 | email.attach(message) 131 | for filename in filenames: 132 | with open(filename, 'rb') as file: 133 | pos = filename.rfind('/') 134 | display_filename = filename[pos + 1:] if pos >= 0 else filename 135 | display_filename = quote(display_filename) 136 | attachment = MIMEText(file.read(), 'base64', 'utf-8') 137 | attachment['content-type'] = 'application/octet-stream' 138 | attachment['content-disposition'] = f'attachment; filename="{display_filename}"' 139 | email.attach(attachment) 140 | 141 | smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT) 142 | smtp.login(EMAIL_USER, EMAIL_AUTH) 143 | smtp.sendmail(from_user, to_users.split(';'), email.as_string()) 144 | ``` 145 | 146 | ### 发送短信 147 | 148 | 发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台的支持,下面我们以[螺丝帽平台](https://luosimao.com/)为例,为大家介绍如何用Python程序发送短信。注册账号和购买短信服务的细节我们不在这里进行赘述,大家可以咨询平台的客服。 149 | 150 | ![image-20210820194420911](https://github.com/jackfrued/mypic/raw/master/20210820194421.png) 151 | 152 | 接下来,我们可以通过`requests`库向平台提供的短信网关发起一个HTTP请求,通过将接收短信的手机号和短信内容作为参数,就可以发送短信,代码如下所示。 153 | 154 | ```Python 155 | import random 156 | 157 | import requests 158 | 159 | 160 | def send_message_by_luosimao(tel, message): 161 | """发送短信(调用螺丝帽短信网关)""" 162 | resp = requests.post( 163 | url='http://sms-api.luosimao.com/v1/send.json', 164 | auth=('api', 'key-注册成功后平台分配的KEY'), 165 | data={ 166 | 'mobile': tel, 167 | 'message': message 168 | }, 169 | timeout=10, 170 | verify=False 171 | ) 172 | return resp.json() 173 | 174 | 175 | def gen_mobile_code(length=6): 176 | """生成指定长度的手机验证码""" 177 | return ''.join(random.choices('0123456789', k=length)) 178 | 179 | 180 | def main(): 181 | code = gen_mobile_code() 182 | message = f'您的短信验证码是{code},打死也不能告诉别人哟!【Python小课】' 183 | print(send_message_by_luosimao('13500112233', message)) 184 | 185 | 186 | if __name__ == '__main__': 187 | main() 188 | ``` 189 | 190 | 上面请求螺丝帽的短信网关`http://sms-api.luosimao.com/v1/send.json`会返回JSON格式的数据,如果返回`{'error': 0, 'msg': 'OK'}`就说明短信已经发送成功了,如果`error`的值不是`0`,可以通过查看官方的[开发文档](https://luosimao.com/docs/api/)了解到底哪个环节出了问题。螺丝帽平台常见的错误类型如下图所示。 191 | 192 | image-20210820195505761 193 | 194 | 目前,大多数短信平台都会要求短信内容必须附上签名,下图是我在螺丝帽平台配置的短信签名“【Python小课】”。有些涉及到敏感内容的短信,还需要提前配置短信模板,有兴趣的读者可以自行研究。一般情况下,平台为了防范短信被盗用,还会要求设置“IP白名单”,不清楚如何配置的可以咨询平台客服。 195 | 196 | ![image-20210820194653785](https://github.com/jackfrued/mypic/raw/master/20210820194653.png) 197 | 198 | 当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。 199 | 200 | ### 简单的总结 201 | 202 | 其实,发送邮件和发送短信一样,也可以通过调用三方服务来完成,在实际的商业项目中,建议自己架设邮件服务器或购买三方服务来发送邮件,这个才是比较靠谱的选择。 203 | -------------------------------------------------------------------------------- /第26课:用Python操作Word文件和PowerPoint.md: -------------------------------------------------------------------------------- 1 | ## 第26课:用Python操作Word和PowerPoint 2 | 3 | 在日常工作中,有很多简单重复的劳动其实完全可以交给Python程序,比如根据样板文件(模板文件)批量的生成很多个Word文件或PowerPoint文件。Word是微软公司开发的文字处理程序,相信大家都不陌生,日常办公中很多正式的文档都是用Word进行撰写和编辑的,目前使用的Word文件后缀名一般为`.docx`。PowerPoint是微软公司开发的演示文稿程序,是微软的Office系列软件中的一员,被商业人士、教师、学生等群体广泛使用,通常也将其称之为“幻灯片”。在Python中,可以使用名为`python-docx` 的三方库来操作Word,可以使用名为`python-pptx`的三方库来生成PowerPoint。 4 | 5 | ### 操作Word文档 6 | 7 | 我们可以先通过下面的命令来安装`python-docx`三方库。 8 | 9 | ```bash 10 | pip install python-docx 11 | ``` 12 | 13 | 按照[官方文档](https://python-docx.readthedocs.io/en/latest/)的介绍,我们可以使用如下所示的代码来生成一个简单的Word文档。 14 | 15 | ```Python 16 | from docx import Document 17 | from docx.shared import Cm, Pt 18 | 19 | from docx.document import Document as Doc 20 | 21 | # 创建代表Word文档的Doc对象 22 | document = Document() # type: Doc 23 | # 添加大标题 24 | document.add_heading('快快乐乐学Python', 0) 25 | # 添加段落 26 | p = document.add_paragraph('Python是一门非常流行的编程语言,它') 27 | run = p.add_run('简单') 28 | run.bold = True 29 | run.font.size = Pt(18) 30 | p.add_run('而且') 31 | run = p.add_run('优雅') 32 | run.font.size = Pt(18) 33 | run.underline = True 34 | p.add_run('。') 35 | 36 | # 添加一级标题 37 | document.add_heading('Heading, level 1', level=1) 38 | # 添加带样式的段落 39 | document.add_paragraph('Intense quote', style='Intense Quote') 40 | # 添加无序列表 41 | document.add_paragraph( 42 | 'first item in unordered list', style='List Bullet' 43 | ) 44 | document.add_paragraph( 45 | 'second item in ordered list', style='List Bullet' 46 | ) 47 | # 添加有序列表 48 | document.add_paragraph( 49 | 'first item in ordered list', style='List Number' 50 | ) 51 | document.add_paragraph( 52 | 'second item in ordered list', style='List Number' 53 | ) 54 | 55 | # 添加图片(注意路径和图片必须要存在) 56 | document.add_picture('resources/guido.jpg', width=Cm(5.2)) 57 | 58 | # 添加分节符 59 | document.add_section() 60 | 61 | records = ( 62 | ('骆昊', '男', '1995-5-5'), 63 | ('孙美丽', '女', '1992-2-2') 64 | ) 65 | # 添加表格 66 | table = document.add_table(rows=1, cols=3) 67 | table.style = 'Dark List' 68 | hdr_cells = table.rows[0].cells 69 | hdr_cells[0].text = '姓名' 70 | hdr_cells[1].text = '性别' 71 | hdr_cells[2].text = '出生日期' 72 | # 为表格添加行 73 | for name, sex, birthday in records: 74 | row_cells = table.add_row().cells 75 | row_cells[0].text = name 76 | row_cells[1].text = sex 77 | row_cells[2].text = birthday 78 | 79 | # 添加分页符 80 | document.add_page_break() 81 | 82 | # 保存文档 83 | document.save('demo.docx') 84 | ``` 85 | 86 | > **提示**:上面代码第7行中的注释`# type: Doc`是为了在PyCharm中获得代码补全提示,因为如果不清楚对象具体的数据类型,PyCharm无法在后续代码中给出`Doc`对象的代码补全提示。 87 | 88 | 执行上面的代码,打开生成的Word文档,效果如下图所示。 89 | 90 | image-20210820002742341  image-20210820002843696 91 | 92 | 对于一个已经存在的Word文件,我们可以通过下面的代码去遍历它所有的段落并获取对应的内容。 93 | 94 | ```Python 95 | from docx import Document 96 | from docx.document import Document as Doc 97 | 98 | doc = Document('resources/离职证明.docx') # type: Doc 99 | for no, p in enumerate(doc.paragraphs): 100 | print(no, p.text) 101 | ``` 102 | 103 | > **提示**:如果需要上面代码中的Word文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。 104 | 105 | 读取到的内容如下所示。 106 | 107 | ``` 108 | 0 109 | 1 离 职 证 明 110 | 2 111 | 3 兹证明 王大锤 ,身份证号码: 100200199512120001 ,于 2018 年 8 月 7 日至 2020 年 6 月 28 日在我单位 开发部 部门担任 Java开发工程师 职务,在职期间无不良表现。因 个人 原因,于 2020 年 6 月 28 日起终止解除劳动合同。现已结清财务相关费用,办理完解除劳动关系相关手续,双方不存在任何劳动争议。 112 | 4 113 | 5 特此证明! 114 | 6 115 | 7 116 | 8 公司名称(盖章):成都风车车科技有限公司 117 | 9 2020 年 6 月 28 日 118 | ``` 119 | 120 | 讲到这里,相信很多读者已经想到了,我们可以把上面的离职证明制作成一个模板文件,把姓名、身份证号、入职和离职日期等信息用占位符代替,这样通过对占位符的替换,就可以根据实际需要写入对应的信息,这样就可以批量的生成Word文档。 121 | 122 | 按照上面的思路,我们首先编辑一个离职证明的模板文件,如下图所示。 123 | 124 | image-20210820004223731 125 | 126 | 接下来我们读取该文件,将占位符替换为真实信息,就可以生成一个新的Word文档,如下所示。 127 | 128 | ```Python 129 | from docx import Document 130 | from docx.document import Document as Doc 131 | 132 | # 将真实信息用字典的方式保存在列表中 133 | employees = [ 134 | { 135 | 'name': '骆昊', 136 | 'id': '100200198011280001', 137 | 'sdate': '2008年3月1日', 138 | 'edate': '2012年2月29日', 139 | 'department': '产品研发', 140 | 'position': '架构师', 141 | 'company': '成都华为技术有限公司' 142 | }, 143 | { 144 | 'name': '王大锤', 145 | 'id': '510210199012125566', 146 | 'sdate': '2019年1月1日', 147 | 'edate': '2021年4月30日', 148 | 'department': '产品研发', 149 | 'position': 'Python开发工程师', 150 | 'company': '成都谷道科技有限公司' 151 | }, 152 | { 153 | 'name': '李元芳', 154 | 'id': '2102101995103221599', 155 | 'sdate': '2020年5月10日', 156 | 'edate': '2021年3月5日', 157 | 'department': '产品研发', 158 | 'position': 'Java开发工程师', 159 | 'company': '同城企业管理集团有限公司' 160 | }, 161 | ] 162 | # 对列表进行循环遍历,批量生成Word文档 163 | for emp_dict in employees: 164 | # 读取离职证明模板文件 165 | doc = Document('resources/离职证明模板.docx') # type: Doc 166 | # 循环遍历所有段落寻找占位符 167 | for p in doc.paragraphs: 168 | if '{' not in p.text: 169 | continue 170 | # 不能直接修改段落内容,否则会丢失样式 171 | # 所以需要对段落中的元素进行遍历并进行查找替换 172 | for run in p.runs: 173 | if '{' not in run.text: 174 | continue 175 | # 将占位符换成实际内容 176 | start, end = run.text.find('{'), run.text.find('}') 177 | key, place_holder = run.text[start + 1:end], run.text[start:end + 1] 178 | run.text = run.text.replace(place_holder, emp_dict[key]) 179 | # 每个人对应保存一个Word文档 180 | doc.save(f'{emp_dict["name"]}离职证明.docx') 181 | ``` 182 | 183 | 执行上面的代码,会在当前路径下生成三个Word文档,如下图所示。 184 | 185 | image-20210820004825183 186 | 187 | ### 生成PowerPoint 188 | 189 | 首先我们需要安装名为`python-pptx`的三方库,命令如下所示。 190 | 191 | ```Bash 192 | pip install python-pptx 193 | ``` 194 | 195 | 用Python操作PowerPoint的内容,因为实际应用场景不算很多,我不打算在这里进行赘述,有兴趣的读者可以自行阅读`python-pptx`的[官方文档](https://python-pptx.readthedocs.io/en/latest/),下面仅展示一段来自于官方文档的代码。 196 | 197 | ```Python 198 | from pptx import Presentation 199 | 200 | # 创建幻灯片对象 201 | pres = Presentation() 202 | 203 | # 选择母版添加一页 204 | title_slide_layout = pres.slide_layouts[0] 205 | slide = pres.slides.add_slide(title_slide_layout) 206 | # 获取标题栏和副标题栏 207 | title = slide.shapes.title 208 | subtitle = slide.placeholders[1] 209 | # 编辑标题和副标题 210 | title.text = "Welcome to Python" 211 | subtitle.text = "Life is short, I use Python" 212 | 213 | # 选择母版添加一页 214 | bullet_slide_layout = pres.slide_layouts[1] 215 | slide = pres.slides.add_slide(bullet_slide_layout) 216 | # 获取页面上所有形状 217 | shapes = slide.shapes 218 | # 获取标题和主体 219 | title_shape = shapes.title 220 | body_shape = shapes.placeholders[1] 221 | # 编辑标题 222 | title_shape.text = 'Introduction' 223 | # 编辑主体内容 224 | tf = body_shape.text_frame 225 | tf.text = 'History of Python' 226 | # 添加一个一级段落 227 | p = tf.add_paragraph() 228 | p.text = 'X\'max 1989' 229 | p.level = 1 230 | # 添加一个二级段落 231 | p = tf.add_paragraph() 232 | p.text = 'Guido began to write interpreter for Python.' 233 | p.level = 2 234 | 235 | # 保存幻灯片 236 | pres.save('test.pptx') 237 | ``` 238 | 239 | 运行上面的代码,生成的PowerPoint文件如下图所示。 240 | 241 | image-20210820010306008 242 | 243 | ### 简单的总结 244 | 245 | 用Python程序解决办公自动化的问题真的非常酷,它可以将我们从繁琐乏味的劳动中解放出来。写这类代码就是去做一件一劳永逸的事情,写代码的过程即便不怎么愉快,使用这些代码的时候应该是非常开心的。 246 | -------------------------------------------------------------------------------- /第19课:面向对象编程应用.md: -------------------------------------------------------------------------------- 1 | ## 第19课:面向对象编程应用 2 | 3 | 面向对象编程对初学者来说不难理解但很难应用,虽然我们为大家总结过面向对象的三步走方法(定义类、创建对象、给对象发消息),但是说起来容易做起来难。**大量的编程练习**和**阅读优质的代码**可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识,同时也通过这些案例为大家讲解如何运用之前学过的Python知识。 4 | 5 | ### 经典案例 6 | 7 | #### 案例1:扑克游戏。 8 | 9 | > **说明**:简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将52张牌发到4个玩家的手上,每个玩家手上有13张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。 10 | 11 | 使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。当然,这件事情并不是特别困难,我们可以从需求的描述中找出名词和动词,名词通常就是对象或者是对象的属性,而动词通常是对象的行为。扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家,牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为**is-a关系(继承)**、**has-a关系(关联)**和**use-a关系(依赖)**。很显然扑克和牌是has-a关系,因为一副扑克有(has-a)52张牌;玩家和牌之间不仅有关联关系还有依赖关系,因为玩家手上有(has-a)牌而且玩家使用了(use-a)牌。 12 | 13 | 牌的属性显而易见,有花色和点数。我们可以用0到3的四个数字来代表四种不同的花色,但是这样的代码可读性会非常糟糕,因为我们并不知道黑桃、红心、草花、方块跟0到3的数字的对应关系。如果一个变量的取值只有有限多个选项,我们可以使用枚举。与C、Java等语言不同的是,Python中没有声明枚举类型的关键字,但是可以通过继承`enum`模块的`Enum`类来创建枚举类型,代码如下所示。 14 | 15 | ```Python 16 | from enum import Enum 17 | 18 | 19 | class Suite(Enum): 20 | """花色(枚举)""" 21 | SPADE, HEART, CLUB, DIAMOND = range(4) 22 | ``` 23 | 24 | 通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如`SPADE`、`HEART`等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字`0`,而是用`Suite.SPADE`;同理,表示方块可以不用数字`3`, 而是用`Suite.DIAMOND`。注意,使用符号常量肯定是优于使用字面常量的,因为能够读懂英文就能理解符号常量的含义,代码的可读性会提升很多。Python中的枚举类型是可迭代类型,简单的说就是可以将枚举类型放到`for-in`循环中,依次取出每一个符号常量及其对应的值,如下所示。 25 | 26 | ```Python 27 | for suite in Suite: 28 | print(f'{suite}: {suite.value}') 29 | ``` 30 | 31 | 接下来我们可以定义牌类。 32 | 33 | ```Python 34 | class Card: 35 | """牌""" 36 | 37 | def __init__(self, suite, face): 38 | self.suite = suite 39 | self.face = face 40 | 41 | def __repr__(self): 42 | suites = '♠♥♣♦' 43 | faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] 44 | # 根据牌的花色和点数取到对应的字符 45 | return f'{suites[self.suite.value]}{faces[self.face]}' 46 | ``` 47 | 48 | 可以通过下面的代码来测试下`Card`类。 49 | 50 | ```Python 51 | card1 = Card(Suite.SPADE, 5) 52 | card2 = Card(Suite.HEART, 13) 53 | print(card1, card2) # ♠5 ♥K 54 | ``` 55 | 56 | 接下来我们定义扑克类。 57 | 58 | ```Python 59 | import random 60 | 61 | 62 | class Poker: 63 | """扑克""" 64 | 65 | def __init__(self): 66 | # 通过列表的生成式语法创建一个装52张牌的列表 67 | self.cards = [Card(suite, face) for suite in Suite 68 | for face in range(1, 14)] 69 | # current属性表示发牌的位置 70 | self.current = 0 71 | 72 | def shuffle(self): 73 | """洗牌""" 74 | self.current = 0 75 | # 通过random模块的shuffle函数实现列表的随机乱序 76 | random.shuffle(self.cards) 77 | 78 | def deal(self): 79 | """发牌""" 80 | card = self.cards[self.current] 81 | self.current += 1 82 | return card 83 | 84 | @property 85 | def has_next(self): 86 | """还有没有牌可以发""" 87 | return self.current < len(self.cards) 88 | ``` 89 | 90 | 可以通过下面的代码来测试下`Poker`类。 91 | 92 | ```Python 93 | poker = Poker() 94 | poker.shuffle() 95 | print(poker.cards) 96 | ``` 97 | 98 | 定义玩家类。 99 | 100 | ```Python 101 | class Player: 102 | """玩家""" 103 | 104 | def __init__(self, name): 105 | self.name = name 106 | self.cards = [] 107 | 108 | def get_one(self, card): 109 | """摸牌""" 110 | self.cards.append(card) 111 | 112 | def arrange(self): 113 | self.cards.sort() 114 | ``` 115 | 116 | 创建四个玩家并将牌发到玩家的手上。 117 | 118 | ```Python 119 | poker = Poker() 120 | poker.shuffle() 121 | players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')] 122 | for _ in range(13): 123 | for player in players: 124 | player.get_one(poker.deal()) 125 | for player in players: 126 | player.arrange() 127 | print(f'{player.name}: ', end='') 128 | print(player.cards) 129 | ``` 130 | 131 | 执行上面的代码会在`player.arrange()`那里出现异常,因为`Player`的`arrange`方法使用了列表的`sort`对玩家手上的牌进行排序,排序需要比较两个`Card`对象的大小,而`<`运算符又不能直接作用于`Card`类型,所以就出现了`TypeError`异常,异常消息为:`'<' not supported between instances of 'Card' and 'Card'`。 132 | 133 | 为了解决这个问题,我们可以对`Card`类的代码稍作修改,使得两个`Card`对象可以直接用`<`进行大小的比较。这里用到技术叫**运算符重载**,Python中要实现对`<`运算符的重载,需要在类中添加一个名为`__lt__`的魔术方法。很显然,魔术方法`__lt__`中的`lt`是英文单词“less than”的缩写,以此类推,魔术方法`__gt__`对应`>`运算符,魔术方法`__le__`对应`<=`运算符,`__ge__`对应`>=`运算符,`__eq__`对应`==`运算符,`__ne__`对应`!=`运算符。 134 | 135 | 修改后的`Card`类代码如下所示。 136 | 137 | ```Python 138 | class Card: 139 | """牌""" 140 | 141 | def __init__(self, suite, face): 142 | self.suite = suite 143 | self.face = face 144 | 145 | def __repr__(self): 146 | suites = '♠♥♣♦' 147 | faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'] 148 | # 根据牌的花色和点数取到对应的字符 149 | return f'{suites[self.suite.value]}{faces[self.face]}' 150 | 151 | def __lt__(self, other): 152 | # 花色相同比较点数的大小 153 | if self.suite == other.suite: 154 | return self.face < other.face 155 | # 花色不同比较花色对应的值 156 | return self.suite.value < other.suite.value 157 | ``` 158 | 159 | >**说明:** 大家可以尝试在上面代码的基础上写一个简单的扑克游戏,如21点游戏(Black Jack),游戏的规则可以自己在网上找一找。 160 | 161 | #### 案例2:工资结算系统。 162 | 163 | > **要求**:某公司有三种类型的员工,分别是部门经理、程序员和销售员。需要设计一个工资结算系统,根据提供的员工信息来计算员工的月薪。其中,部门经理的月薪是固定15000元;程序员按工作时间(以小时为单位)支付月薪,每小时200元;销售员的月薪由1800元底薪加上销售额5%的提成两部分构成。 164 | 165 | 通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为`Employee`的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然,后续的代码不会创建`Employee` 类的对象,因为我们需要的是具体的员工对象,所以这个类可以设计成专门用于继承的抽象类。Python中没有定义抽象类的关键字,但是可以通过`abc`模块中名为`ABCMeta` 的元类来定义抽象类。关于元类的知识,后面的课程中会有专门的讲解,这里不用太纠结这个概念,记住用法即可。 166 | 167 | ```Python 168 | from abc import ABCMeta, abstractmethod 169 | 170 | 171 | class Employee(metaclass=ABCMeta): 172 | """员工""" 173 | 174 | def __init__(self, name): 175 | self.name = name 176 | 177 | @abstractmethod 178 | def get_salary(self): 179 | """结算月薪""" 180 | pass 181 | ``` 182 | 183 | 在上面的员工类中,有一个名为`get_salary`的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用`abstractmethod`装饰器将其声明为抽象方法,所谓**抽象方法就是只有声明没有实现的方法**,**声明这个方法是为了让子类去重写这个方法**。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。 184 | 185 | ```Python 186 | class Manager(Employee): 187 | """部门经理""" 188 | 189 | def get_salary(self): 190 | return 15000.0 191 | 192 | 193 | class Programmer(Employee): 194 | """程序员""" 195 | 196 | def __init__(self, name, working_hour=0): 197 | super().__init__(name) 198 | self.working_hour = working_hour 199 | 200 | def get_salary(self): 201 | return 200 * self.working_hour 202 | 203 | 204 | class Salesman(Employee): 205 | """销售员""" 206 | 207 | def __init__(self, name, sales=0): 208 | super().__init__(name) 209 | self.sales = sales 210 | 211 | def get_salary(self): 212 | return 1800 + self.sales * 0.05 213 | ``` 214 | 215 | 上面的`Manager`、`Programmer`、`Salesman`三个类都继承自`Employee`,三个类都分别重写了`get_salary`方法。**重写就是子类对父类已有的方法重新做出实现**。相信大家已经注意到了,三个子类中的`get_salary`各不相同,所以这个方法在程序运行时会产生**多态行为**,多态简单的说就是**调用相同的方法**,**不同的子类对象做不同的事情**。 216 | 217 | 我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了Python内置的`isinstance`函数来判断员工对象的类型。我们之前讲过的`type`函数也能识别对象的类型,但是`isinstance`函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简答的理解为`type`函数是对对象类型的精准匹配,而`isinstance`函数是对对象类型的模糊匹配。 218 | 219 | ```Python 220 | emps = [ 221 | Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'), 222 | Programmer('荀彧'), Salesman('吕布'), Programmer('张辽'), 223 | ] 224 | for emp in emps: 225 | if isinstance(emp, Programmer): 226 | emp.working_hour = int(input(f'请输入{emp.name}本月工作时间: ')) 227 | elif isinstance(emp, Salesman): 228 | emp.sales = float(input(f'请输入{emp.name}本月销售额: ')) 229 | print(f'{emp.name}本月工资为: ¥{emp.get_salary():.2f}元') 230 | ``` 231 | 232 | ### 简单的总结 233 | 234 | 面向对象的编程思想非常的好,也符合人类的正常思维习惯,但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀,这件事情无法一蹴而就,属于“路漫漫其修远兮,吾将上下而求索”的东西。 235 | -------------------------------------------------------------------------------- /第35课:Python中的并发编程-2.md: -------------------------------------------------------------------------------- 1 | ## 第35课:Python中的并发编程-2 2 | 3 | 在上一课中我们说过,由于 GIL 的存在,CPython 中的多线程并不能发挥 CPU 的多核优势,如果希望突破 GIL 的限制,可以考虑使用多进程。对于多进程的程序,每个进程都有一个属于自己的 GIL,所以多进程不会受到 GIL 的影响。那么,我们应该如何在 Python 程序中创建和使用多进程呢? 4 | 5 | ###创建进程 6 | 7 | 在 Python 中可以基于`Process`类来创建进程,虽然进程和线程有着本质的差别,但是`Process`类和`Thread`类的用法却非常类似。在使用`Process`类的构造器创建对象时,也是通过`target`参数传入一个函数来指定进程要执行的代码,而`args`和`kwargs`参数可以指定该函数使用的参数值。 8 | 9 | ```Python 10 | from multiprocessing import Process, current_process 11 | from time import sleep 12 | 13 | 14 | def sub_task(content, nums): 15 | # 通过current_process函数获取当前进程对象 16 | # 通过进程对象的pid和name属性获取进程的ID号和名字 17 | print(f'PID: {current_process().pid}') 18 | print(f'Name: {current_process().name}') 19 | # 通过下面的输出不难发现,每个进程都有自己的nums列表,进程之间本就不共享内存 20 | # 在创建子进程时复制了父进程的数据结构,三个进程从列表中pop(0)得到的值都是20 21 | counter, total = 0, nums.pop(0) 22 | print(f'Loop count: {total}') 23 | sleep(0.5) 24 | while counter < total: 25 | counter += 1 26 | print(f'{counter}: {content}') 27 | sleep(0.01) 28 | 29 | 30 | def main(): 31 | nums = [20, 30, 40] 32 | # 创建并启动进程来执行指定的函数 33 | Process(target=sub_task, args=('Ping', nums)).start() 34 | Process(target=sub_task, args=('Pong', nums)).start() 35 | # 在主进程中执行sub_task函数 36 | sub_task('Good', nums) 37 | 38 | 39 | if __name__ == '__main__': 40 | main() 41 | ``` 42 | 43 | > **说明**:上面的代码通过`current_process`函数获取当前进程对象,再通过进程对象的`pid`属性获取进程ID。在 Python 中,使用`os`模块的`getpid`函数也可以达到同样的效果。 44 | 45 | 如果愿意,也可以使用`os`模块的`fork`函数来创建进程,调用该函数时,操作系统自动把当前进程(父进程)复制一份(子进程),父进程的`fork`函数会返回子进程的ID,而子进程中的`fork`函数会返回`0`,也就是说这个函数调用一次会在父进程和子进程中得到两个不同的返回值。需要注意的是,Windows 系统并不支持`fork`函数,如果你使用的是 Linux 或 macOS 系统,可以试试下面的代码。 46 | 47 | ```Python 48 | import os 49 | 50 | print(f'PID: {os.getpid()}') 51 | pid = os.fork() 52 | if pid == 0: 53 | print(f'子进程 - PID: {os.getpid()}') 54 | print('Todo: 在子进程中执行的代码') 55 | else: 56 | print(f'父进程 - PID: {os.getpid()}') 57 | print('Todo: 在父进程中执行的代码') 58 | ``` 59 | 60 | 简而言之,我们还是推荐大家通过直接使用`Process`类、继承`Process`类和使用进程池(`ProcessPoolExecutor`)这三种方式来创建和使用多进程,这三种方式不同于上面的`fork`函数,能够保证代码的兼容性和可移植性。具体的做法跟之前讲过的创建和使用多线程的方式比较接近,此处不再进行赘述。 61 | 62 | ### 多进程和多线程的比较 63 | 64 | 对于爬虫这类 I/O 密集型任务来说,使用多进程并没有什么优势;但是对于计算密集型任务来说,多进程相比多线程,在效率上会有显著的提升,我们可以通过下面的代码来加以证明。下面的代码会通过多线程和多进程两种方式来判断一组大整数是不是质数,很显然这是一个计算密集型任务,我们将任务分别放到多个线程和多个进程中来加速代码的执行,让我们看看多线程和多进程的代码具体表现有何不同。 65 | 66 | 我们先实现一个多线程的版本,代码如下所示。 67 | 68 | ```Python 69 | import concurrent.futures 70 | 71 | PRIMES = [ 72 | 1116281, 73 | 1297337, 74 | 104395303, 75 | 472882027, 76 | 533000389, 77 | 817504243, 78 | 982451653, 79 | 112272535095293, 80 | 112582705942171, 81 | 112272535095293, 82 | 115280095190773, 83 | 115797848077099, 84 | 1099726899285419 85 | ] * 5 86 | 87 | 88 | def is_prime(n): 89 | """判断素数""" 90 | for i in range(2, int(n ** 0.5) + 1): 91 | if n % i == 0: 92 | return False 93 | return n != 1 94 | 95 | 96 | def main(): 97 | """主函数""" 98 | with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: 99 | for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)): 100 | print('%d is prime: %s' % (number, prime)) 101 | 102 | 103 | if __name__ == '__main__': 104 | main() 105 | ``` 106 | 107 | 假设上面的代码保存在名为`example.py`的文件中,在 Linux 或 macOS 系统上,可以使用`time python example.py`命令执行程序并获得操作系统关于执行时间的统计,在我的 macOS 上,某次的运行结果的最后一行输出如下所示。 108 | 109 | ``` 110 | python example09.py 38.69s user 1.01s system 101% cpu 39.213 total 111 | ``` 112 | 113 | 从运行结果可以看出,多线程的代码只能让 CPU 利用率达到100%,这其实已经证明了多线程的代码无法利用 CPU 多核特性来加速代码的执行,我们再看看多进程的版本,我们将上面代码中的线程池(`ThreadPoolExecutor`)更换为进程池(`ProcessPoolExecutor`)。 114 | 115 | 多进程的版本。 116 | 117 | ```Python 118 | import concurrent.futures 119 | 120 | PRIMES = [ 121 | 1116281, 122 | 1297337, 123 | 104395303, 124 | 472882027, 125 | 533000389, 126 | 817504243, 127 | 982451653, 128 | 112272535095293, 129 | 112582705942171, 130 | 112272535095293, 131 | 115280095190773, 132 | 115797848077099, 133 | 1099726899285419 134 | ] * 5 135 | 136 | 137 | def is_prime(n): 138 | """判断素数""" 139 | for i in range(2, int(n ** 0.5) + 1): 140 | if n % i == 0: 141 | return False 142 | return n != 1 143 | 144 | 145 | def main(): 146 | """主函数""" 147 | with concurrent.futures.ProcessPoolExecutor(max_workers=16) as executor: 148 | for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)): 149 | print('%d is prime: %s' % (number, prime)) 150 | 151 | 152 | if __name__ == '__main__': 153 | main() 154 | ``` 155 | 156 | > **提示**:运行上面的代码时,可以通过操作系统的任务管理器(资源监视器)来查看是否启动了多个 Python 解释器进程。 157 | 158 | 我们仍然通过`time python example.py`的方式来执行上述代码,运行结果的最后一行如下所示。 159 | 160 | ``` 161 | python example09.py 106.63s user 0.57s system 389% cpu 27.497 total 162 | ``` 163 | 164 | 可以看出,多进程的版本在我使用的这台电脑上,让 CPU 的利用率达到了将近400%,而运行代码时用户态耗费的 CPU 的时间(106.63秒)几乎是代码运行总时间(27.497秒)的4倍,从这两点都可以看出,我的电脑使用了一款4核的 CPU。当然,要知道自己的电脑有几个 CPU 或几个核,可以直接使用下面的代码。 165 | 166 | ```Python 167 | import os 168 | 169 | print(os.cpu_count()) 170 | ``` 171 | 172 | 综上所述,多进程可以突破 GIL 的限制,充分利用 CPU 多核特性,对于计算密集型任务,这一点是相当重要的。常见的计算密集型任务包括科学计算、图像处理、音视频编解码等,如果这些计算密集型任务本身是可以并行的,那么使用多进程应该是更好的选择。 173 | 174 | ### 进程间通信 175 | 176 | 在讲解进程间通信之前,先给大家一个任务:启动两个进程,一个输出“Ping”,一个输出“Pong”,两个进程输出的“Ping”和“Pong”加起来一共有50个时,就结束程序。听起来是不是非常简单,但是实际编写代码时,由于多个进程之间不能够像多个线程之间直接通过共享内存的方式交换数据,所以下面的代码是达不到我们想要的结果的。 177 | 178 | ```Python 179 | from multiprocessing import Process 180 | from time import sleep 181 | 182 | counter = 0 183 | 184 | 185 | def sub_task(string): 186 | global counter 187 | while counter < 50: 188 | print(string, end='', flush=True) 189 | counter += 1 190 | sleep(0.01) 191 | 192 | 193 | def main(): 194 | Process(target=sub_task, args=('Ping', )).start() 195 | Process(target=sub_task, args=('Pong', )).start() 196 | 197 | 198 | if __name__ == '__main__': 199 | main() 200 | ``` 201 | 202 | 上面的代码看起来没毛病,但是最后的结果是“Ping”和“Pong”各输出了50个。再次提醒大家,当我们在程序中创建进程的时候,子进程会复制父进程及其所有的数据结构,每个子进程有自己独立的内存空间,这也就意味着两个子进程中各有一个`counter`变量,它们都会从`0`加到`50`,所以结果就可想而知了。要解决这个问题比较简单的办法是使用`multiprocessing`模块中的`Queue`类,它是可以被多个进程共享的队列,底层是通过操作系统底层的管道和信号量(semaphore)机制来实现的,代码如下所示。 203 | 204 | ```Python 205 | import time 206 | from multiprocessing import Process, Queue 207 | 208 | 209 | def sub_task(content, queue): 210 | counter = queue.get() 211 | while counter < 50: 212 | print(content, end='', flush=True) 213 | counter += 1 214 | queue.put(counter) 215 | time.sleep(0.01) 216 | counter = queue.get() 217 | 218 | 219 | def main(): 220 | queue = Queue() 221 | queue.put(0) 222 | p1 = Process(target=sub_task, args=('Ping', queue)) 223 | p1.start() 224 | p2 = Process(target=sub_task, args=('Pong', queue)) 225 | p2.start() 226 | while p1.is_alive() and p2.is_alive(): 227 | pass 228 | queue.put(50) 229 | 230 | 231 | if __name__ == '__main__': 232 | main() 233 | ``` 234 | 235 | > **提示**:`multiprocessing.Queue`对象的`get`方法默认在队列为空时是会阻塞的,直到获取到数据才会返回。如果不希望该方法阻塞以及需要指定阻塞的超时时间,可以通过指定`block`和`timeout`参数进行设定。 236 | 237 | 上面的代码通过`Queue`类的`get`和`put`方法让三个进程(`p1`、`p2`和主进程)实现了数据的共享,这就是所谓的进程间的通信,通过这种方式,当`Queue`中取出的值已经大于等于`50`时,`p1`和`p2`就会跳出`while`循环,从而终止进程的执行。代码第22行的循环是为了等待`p1`和`p2`两个进程中的一个结束,这时候主进程还需要向`Queue`中放置一个大于等于`50`的值,这样另一个尚未结束的进程也会因为读到这个大于等于`50`的值而终止。 238 | 239 | 进程间通信的方式还有很多,比如使用套接字也可以实现两个进程的通信,甚至于这两个进程并不在同一台主机上,有兴趣的读者可以自行了解。 240 | 241 | ### 简单的总结 242 | 243 | 在 Python 中,我们还可以通过`subprocess`模块的`call`函数执行其他的命令来创建子进程,相当于就是在我们的程序中调用其他程序,这里我们暂不探讨这些知识,有兴趣的读者可以自行研究。 244 | 245 | 对于Python开发者来说,以下情况需要考虑使用多线程: 246 | 247 | 1. 程序需要维护许多共享的状态(尤其是可变状态),Python 中的列表、字典、集合都是线程安全的(多个线程同时操作同一个列表、字典或集合,不会引发错误和数据问题),所以使用线程而不是进程维护共享状态的代价相对较小。 248 | 2. 程序会花费大量时间在 I/O 操作上,没有太多并行计算的需求且不需占用太多的内存。 249 | 250 | 那么在遇到下列情况时,应该考虑使用多进程: 251 | 252 | 1. 程序执行计算密集型任务(如:音视频编解码、数据压缩、科学计算等)。 253 | 2. 程序的输入可以并行的分成块,并且可以将运算结果合并。 254 | 3. 程序在内存使用方面没有任何限制且不强依赖于 I/O 操作(如读写文件、套接字等)。 255 | -------------------------------------------------------------------------------- /第48课.Python程序接入MySQL数据库.md: -------------------------------------------------------------------------------- 1 | ## 第48课:Python程序接入MySQL数据库 2 | 3 | 在 Python3 中,我们可以使用`mysqlclient`或者`pymysql`三方库来接入 MySQL 数据库并实现数据持久化操作。二者的用法完全相同,只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库`pymysql`,因为它更容易安装成功。下面我们仍然以之前创建的名为`hrs`的数据库为例,为大家演示如何通过 Python 程序操作 MySQL 数据库实现数据持久化操作。 4 | 5 | ### 接入MySQL 6 | 7 | 首先,我们可以在命令行或者 PyCharm 的终端中通过下面的命令安装`pymysql`,如果需要接入 MySQL 8,还需要安装一个名为`cryptography`的三方库来支持 MySQL 8 的密码认证方式。 8 | 9 | ```Shell 10 | pip install pymysql cryptography 11 | ``` 12 | 13 | 使用`pymysql`操作 MySQL 的步骤如下所示: 14 | 15 | 1. 创建连接。MySQL 服务器启动后,提供了基于 TCP (传输控制协议)的网络服务。我们可以通过`pymysql`模块的`connect`函数连接 MySQL 服务器。在调用`connect`函数时,需要指定主机(`host`)、端口(`port`)、用户名(`user`)、口令(`password`)、数据库(`database`)、字符集(`charset`)等参数,该函数会返回一个`Connection`对象。 16 | 2. 获取游标。连接 MySQL 服务器成功后,接下来要做的就是向数据库服务器发送 SQL 语句,MySQL 会执行接收到的 SQL 并将执行结果通过网络返回。要实现这项操作,需要先通过连接对象的`cursor`方法获取游标(`Cursor`)对象。 17 | 3. 发出 SQL。通过游标对象的`execute`方法,我们可以向数据库发出 SQL 语句。 18 | 4. 如果执行`insert`、`delete`或`update`操作,需要根据实际情况提交或回滚事务。因为创建连接时,默认开启了事务环境,在操作完成后,需要使用连接对象的`commit`或`rollback`方法,实现事务的提交或回滚,`rollback`方法通常会放在异常捕获代码块`except`中。如果执行`select`操作,需要通过游标对象抓取查询的结果,对应的方法有三个,分别是:`fetchone`、`fetchmany`和`fetchall`。其中`fetchone`方法会抓取到一条记录,并以元组或字典的方式返回;`fetchmany`和`fetchall`方法会抓取到多条记录,以嵌套元组或列表装字典的方式返回。 19 | 5. 关闭连接。在完成持久化操作后,请不要忘记关闭连接,释放外部资源。我们通常会在`finally`代码块中使用连接对象的`close`方法来关闭连接。 20 | 21 | ### 代码实操 22 | 23 | 下面,我们通过代码实操的方式为大家演示上面说的五个步骤。 24 | 25 | #### 插入数据 26 | 27 | ```Python 28 | import pymysql 29 | 30 | no = int(input('部门编号: ')) 31 | name = input('部门名称: ') 32 | location = input('部门所在地: ') 33 | 34 | # 1. 创建连接(Connection) 35 | conn = pymysql.connect(host='127.0.0.1', port=3306, 36 | user='guest', password='Guest.618', 37 | database='hrs', charset='utf8mb4') 38 | try: 39 | # 2. 获取游标对象(Cursor) 40 | with conn.cursor() as cursor: 41 | # 3. 通过游标对象向数据库服务器发出SQL语句 42 | affected_rows = cursor.execute( 43 | 'insert into `tb_dept` values (%s, %s, %s)', 44 | (no, name, location) 45 | ) 46 | if affected_rows == 1: 47 | print('新增部门成功!!!') 48 | # 4. 提交事务(transaction) 49 | conn.commit() 50 | except pymysql.MySQLError as err: 51 | # 4. 回滚事务 52 | conn.rollback() 53 | print(type(err), err) 54 | finally: 55 | # 5. 关闭连接释放资源 56 | conn.close() 57 | ``` 58 | 59 | > **说明**:上面的`127.0.0.1`称为回环地址,它代表的是本机。下面的`guest`是我提前创建好的用户,该用户拥有对`hrs`数据库的`insert`、`delete`、`update`和`select`权限。我们不建议大家在项目中直接使用`root`超级管理员账号访问数据库,这样做实在是太危险了。我们可以使用下面的命令创建名为`guest`的用户并为其授权。 60 | > 61 | > ```SQL 62 | > create user 'guest'@'%' identified by 'Guest.618'; 63 | > grant insert, delete, update, select on `hrs`.* to 'guest'@'%'; 64 | > ``` 65 | 66 | 如果要插入大量数据,建议使用游标对象的`executemany`方法做批处理(一个`insert`操作后面跟上多组数据),大家可以尝试向一张表插入10000条记录,然后看看不使用批处理一条条的插入和使用批处理有什么差别。游标对象的`executemany`方法第一个参数仍然是 SQL 语句,第二个参数可以是包含多组数据的列表或元组。 67 | 68 | #### 删除数据 69 | 70 | ```Python 71 | import pymysql 72 | 73 | no = int(input('部门编号: ')) 74 | 75 | # 1. 创建连接(Connection) 76 | conn = pymysql.connect(host='127.0.0.1', port=3306, 77 | user='guest', password='Guest.618', 78 | database='hrs', charset='utf8mb4', 79 | autocommit=True) 80 | try: 81 | # 2. 获取游标对象(Cursor) 82 | with conn.cursor() as cursor: 83 | # 3. 通过游标对象向数据库服务器发出SQL语句 84 | affected_rows = cursor.execute( 85 | 'delete from `tb_dept` where `dno`=%s', 86 | (no, ) 87 | ) 88 | if affected_rows == 1: 89 | print('删除部门成功!!!') 90 | finally: 91 | # 5. 关闭连接释放资源 92 | conn.close() 93 | ``` 94 | 95 | > **说明**:如果不希望每次 SQL 操作之后手动提交或回滚事务,可以`connect`函数中加一个名为`autocommit`的参数并将它的值设置为`True`,表示每次执行 SQL 成功后自动提交。但是我们建议大家手动提交或回滚,这样可以根据实际业务需要来构造事务环境。如果不愿意捕获异常并进行处理,可以在`try`代码块后直接跟`finally`块,省略`except`意味着发生异常时,代码会直接崩溃并将异常栈显示在终端中。 96 | 97 | #### 更新数据 98 | 99 | ```Python 100 | import pymysql 101 | 102 | no = int(input('部门编号: ')) 103 | name = input('部门名称: ') 104 | location = input('部门所在地: ') 105 | 106 | # 1. 创建连接(Connection) 107 | conn = pymysql.connect(host='127.0.0.1', port=3306, 108 | user='guest', password='Guest.618', 109 | database='hrs', charset='utf8mb4') 110 | try: 111 | # 2. 获取游标对象(Cursor) 112 | with conn.cursor() as cursor: 113 | # 3. 通过游标对象向数据库服务器发出SQL语句 114 | affected_rows = cursor.execute( 115 | 'update `tb_dept` set `dname`=%s, `dloc`=%s where `dno`=%s', 116 | (name, location, no) 117 | ) 118 | if affected_rows == 1: 119 | print('更新部门信息成功!!!') 120 | # 4. 提交事务 121 | conn.commit() 122 | except pymysql.MySQLError as err: 123 | # 4. 回滚事务 124 | conn.rollback() 125 | print(type(err), err) 126 | finally: 127 | # 5. 关闭连接释放资源 128 | conn.close() 129 | ``` 130 | 131 | #### 查询数据 132 | 133 | 1. 查询部门表的数据。 134 | 135 | ```Python 136 | import pymysql 137 | 138 | # 1. 创建连接(Connection) 139 | conn = pymysql.connect(host='127.0.0.1', port=3306, 140 | user='guest', password='Guest.618', 141 | database='hrs', charset='utf8mb4') 142 | try: 143 | # 2. 获取游标对象(Cursor) 144 | with conn.cursor() as cursor: 145 | # 3. 通过游标对象向数据库服务器发出SQL语句 146 | cursor.execute('select `dno`, `dname`, `dloc` from `tb_dept`') 147 | # 4. 通过游标对象抓取数据 148 | row = cursor.fetchone() 149 | while row: 150 | print(row) 151 | row = cursor.fetchone() 152 | except pymysql.MySQLError as err: 153 | print(type(err), err) 154 | finally: 155 | # 5. 关闭连接释放资源 156 | conn.close() 157 | ``` 158 | >**说明**:上面的代码中,我们通过构造一个`while`循环实现了逐行抓取查询结果的操作。这种方式特别适合查询结果有非常多行的场景。因为如果使用`fetchall`一次性将所有记录抓取到一个嵌套元组中,会造成非常大的内存开销,这在很多场景下并不是一个好主意。如果不愿意使用`while`循环,还可以考虑使用`iter`函数构造一个迭代器来逐行抓取数据,有兴趣的读者可以自行研究。 159 | 160 | 2. 分页查询员工表的数据。 161 | 162 | ```Python 163 | import pymysql 164 | 165 | page = int(input('页码: ')) 166 | size = int(input('大小: ')) 167 | 168 | # 1. 创建连接(Connection) 169 | con = pymysql.connect(host='127.0.0.1', port=3306, 170 | user='guest', password='Guest.618', 171 | database='hrs', charset='utf8') 172 | try: 173 | # 2. 获取游标对象(Cursor) 174 | with con.cursor(pymysql.cursors.DictCursor) as cursor: 175 | # 3. 通过游标对象向数据库服务器发出SQL语句 176 | cursor.execute( 177 | 'select `eno`, `ename`, `job`, `sal` from `tb_emp` order by `sal` desc limit %s,%s', 178 | ((page - 1) * size, size) 179 | ) 180 | # 4. 通过游标对象抓取数据 181 | for emp_dict in cursor.fetchall(): 182 | print(emp_dict) 183 | finally: 184 | # 5. 关闭连接释放资源 185 | con.close() 186 | ``` 187 | 188 | ### 案例讲解 189 | 190 | 下面我们为大家讲解一个将数据库表数据导出到 Excel 文件的例子,我们需要先安装`openpyxl`三方库,命令如下所示。 191 | 192 | ```Bash 193 | pip install openpyxl 194 | ``` 195 | 196 | 接下来,我们通过下面的代码实现了将数据库`hrs`中所有员工的编号、姓名、职位、月薪、补贴和部门名称导出到一个 Excel 文件中。 197 | 198 | ```Python 199 | import openpyxl 200 | import pymysql 201 | 202 | # 创建工作簿对象 203 | workbook = openpyxl.Workbook() 204 | # 获得默认的工作表 205 | sheet = workbook.active 206 | # 修改工作表的标题 207 | sheet.title = '员工基本信息' 208 | # 给工作表添加表头 209 | sheet.append(('工号', '姓名', '职位', '月薪', '补贴', '部门')) 210 | # 创建连接(Connection) 211 | conn = pymysql.connect(host='127.0.0.1', port=3306, 212 | user='guest', password='Guest.618', 213 | database='hrs', charset='utf8mb4') 214 | try: 215 | # 获取游标对象(Cursor) 216 | with conn.cursor() as cursor: 217 | # 通过游标对象执行SQL语句 218 | cursor.execute( 219 | 'select `eno`, `ename`, `job`, `sal`, coalesce(`comm`, 0), `dname` ' 220 | 'from `tb_emp` natural join `tb_dept`' 221 | ) 222 | # 通过游标抓取数据 223 | row = cursor.fetchone() 224 | while row: 225 | # 将数据逐行写入工作表中 226 | sheet.append(row) 227 | row = cursor.fetchone() 228 | # 保存工作簿 229 | workbook.save('hrs.xlsx') 230 | except pymysql.MySQLError as err: 231 | print(err) 232 | finally: 233 | # 关闭连接释放资源 234 | conn.close() 235 | ``` 236 | 237 | 大家可以参考上面的例子,试一试把 Excel 文件的数据导入到指定数据库的指定表中,看看是否可以成功。 -------------------------------------------------------------------------------- /第13课:函数和模块.md: -------------------------------------------------------------------------------- 1 | ## 第13课:函数和模块 2 | 3 | 在讲解本节课的内容之前,我们先来研究一道数学题,请说出下面的方程有多少组正整数解。 4 | 5 | $$ 6 | x_1 + x_2 + x_3 + x_4 = 8 7 | $$ 8 | 9 | 你可能已经想到了,这个问题其实等同于将`8`个苹果分成四组且每组至少一个苹果有多少种方案,因此该问题还可以进一步等价于在分隔`8`个苹果的`7`个空隙之间插入三个隔板将苹果分成四组有多少种方案,也就是从`7`个空隙选出`3`个空隙放入隔板的组合数,所以答案是 $C_7^3=35$ 。组合数的计算公式如下所示。 10 | 11 | $$ 12 | C_M^N = \frac {M!} {N!(M-N)!} 13 | $$ 14 | 15 | 根据我们前面学习的知识,可以用循环做累乘的方式来计算阶乘,那么通过下面的 Python 代码我们就可以计算出组合数 $C_M^N$ 的值,代码如下所示。 16 | 17 | ```Python 18 | """ 19 | 输入M和N计算C(M,N) 20 | 21 | Version: 0.1 22 | Author: 骆昊 23 | """ 24 | m = int(input('m = ')) 25 | n = int(input('n = ')) 26 | # 计算m的阶乘 27 | fm = 1 28 | for num in range(1, m + 1): 29 | fm *= num 30 | # 计算n的阶乘 31 | fn = 1 32 | for num in range(1, n + 1): 33 | fn *= num 34 | # 计算m-n的阶乘 35 | fk = 1 36 | for num in range(1, m - n + 1): 37 | fk *= num 38 | # 计算C(M,N)的值 39 | print(fm // fn // fk) 40 | ``` 41 | 42 | ### 函数的作用 43 | 44 | 不知大家是否注意到,上面的代码中我们做了三次求阶乘,虽然`m`、`n`、`m - n`的值各不相同,但是三段代码并没有实质性的区别,属于重复代码。世界级的编程大师*Martin Fowler*先生曾经说过:“**代码有很多种坏味道,重复是最坏的一种!**”。要写出高质量的代码首先要解决的就是重复代码的问题。对于上面的代码来说,我们可以将计算阶乘的功能封装到一个称为“函数”的代码块中,在需要计算阶乘的地方,我们只需要“调用函数”就可以了。 45 | 46 | ### 定义函数 47 | 48 | 数学上的函数通常形如`y = f(x)`或者`z = g(x, y)`这样的形式,在`y = f(x)`中,`f`是函数的名字,`x`是函数的自变量,`y`是函数的因变量;而在`z = g(x, y)`中,`g`是函数名,`x`和`y`是函数的自变量,`z`是函数的因变量。Python中的函数跟这个结构是一致的,每个函数都有自己的名字、自变量和因变量。我们通常把Python中函数的自变量称为函数的参数,而因变量称为函数的返回值。 49 | 50 | 在Python中可以使用`def`关键字来定义函数,和变量一样每个函数也应该有一个漂亮的名字,命名规则跟变量的命名规则是一致的(赶紧想一想我们之前讲过的变量的命名规则)。在函数名后面的圆括号中可以放置传递给函数的参数,就是我们刚才说到的函数的自变量,而函数执行完成后我们会通过`return`关键字来返回函数的执行结果,就是我们刚才说的函数的因变量。一个函数要执行的代码块(要做的事情)也是通过缩进的方式来表示的,跟之前分支和循环结构的代码块是一样的。大家不要忘了`def`那一行的最后面还有一个`:`,之前提醒过大家,那是在英文输入法状态下输入的冒号。 51 | 52 | 我们可以通过函数对上面的代码进行重构。**所谓重构,是在不影响代码执行结果的前提下对代码的结构进行调整。**重构之后的代码如下所示。 53 | 54 | ```Python 55 | """ 56 | 输入M和N计算C(M,N) 57 | 58 | Version: 0.1 59 | Author: 骆昊 60 | """ 61 | 62 | 63 | # 定义函数:def是定义函数的关键字、fac是函数名,num是参数(自变量) 64 | def fac(num): 65 | """求阶乘""" 66 | result = 1 67 | for n in range(1, num + 1): 68 | result *= n 69 | # 返回num的阶乘(因变量) 70 | return result 71 | 72 | 73 | m = int(input('m = ')) 74 | n = int(input('n = ')) 75 | # 当需要计算阶乘的时候不用再写重复的代码而是直接调用函数fac 76 | # 调用函数的语法是在函数名后面跟上圆括号并传入参数 77 | print(fac(m) // fac(n) // fac(m - n)) 78 | ``` 79 | 80 | > **说明**:事实上,Python标准库的`math`模块中有一个名为`factorial`的函数已经实现了求阶乘的功能,我们可以直接使用该函数来计算阶乘。**将来我们使用的函数,要么是自定义的函数,要么是Python标准库或者三方库中提供的函数**。 81 | 82 | ### 函数的参数 83 | 84 | #### 参数的默认值 85 | 86 | 如果函数中没有`return`语句,那么函数默认返回代表空值的`None`。另外,在定义函数时,函数也可以没有自变量,但是函数名后面的圆括号是必须有的。Python中还允许函数的参数拥有默认值,我们可以把之前讲过的一个例子“CRAPS赌博游戏”中摇色子获得点数的功能封装成函数,代码如下所示。 87 | 88 | ```Python 89 | """ 90 | 参数的默认值 91 | 92 | Version: 0.1 93 | Author: 骆昊 94 | """ 95 | from random import randint 96 | 97 | 98 | # 定义摇色子的函数,n表示色子的个数,默认值为2 99 | def roll_dice(n=2): 100 | """摇色子返回总的点数""" 101 | total = 0 102 | for _ in range(n): 103 | total += randint(1, 6) 104 | return total 105 | 106 | 107 | # 如果没有指定参数,那么n使用默认值2,表示摇两颗色子 108 | print(roll_dice()) 109 | # 传入参数3,变量n被赋值为3,表示摇三颗色子获得点数 110 | print(roll_dice(3)) 111 | ``` 112 | 113 | 我们再来看一个更为简单的例子。 114 | 115 | ```Python 116 | def add(a=0, b=0, c=0): 117 | """三个数相加求和""" 118 | return a + b + c 119 | 120 | 121 | # 调用add函数,没有传入参数,那么a、b、c都使用默认值0 122 | print(add()) # 0 123 | # 调用add函数,传入一个参数,那么该参数赋值给变量a, 变量b和c使用默认值0 124 | print(add(1)) # 1 125 | # 调用add函数,传入两个参数,1和2分别赋值给变量a和b,变量c使用默认值0 126 | print(add(1, 2)) # 3 127 | # 调用add函数,传入三个参数,分别赋值给a、b、c三个变量 128 | print(add(1, 2, 3)) # 6 129 | # 传递参数时可以不按照设定的顺序进行传递,但是要用“参数名=参数值”的形式 130 | print(add(c=50, a=100, b=200)) # 350 131 | ``` 132 | 133 | > **注意**:带默认值的参数必须放在不带默认值的参数之后,否则将产生`SyntaxError`错误,错误消息是:`non-default argument follows default argument`,翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。 134 | 135 | #### 可变参数 136 | 137 | 接下来,我们还可以实现一个对任意多个数求和的`add`函数,因为Python语言中的函数可以通过星号表达式语法来支持可变参数。所谓可变参数指的是在调用函数时,可以向函数传入`0`个或任意多个参数。将来我们以团队协作的方式开发商业项目时,很有可能要设计函数给其他人使用,但有的时候我们并不知道函数的调用者会向该函数传入多少个参数,这个时候可变参数就可以派上用场。下面的代码演示了用可变参数实现对任意多个数求和的`add`函数。 138 | 139 | ```Python 140 | """ 141 | 可变参数 142 | 143 | Version: 0.1 144 | Author: 骆昊 145 | """ 146 | 147 | 148 | # 用星号表达式来表示args可以接收0个或任意多个参数 149 | def add(*args): 150 | total = 0 151 | # 可变参数可以放在for循环中取出每个参数的值 152 | for val in args: 153 | if type(val) in (int, float): 154 | total += val 155 | return total 156 | 157 | 158 | # 在调用add函数时可以传入0个或任意多个参数 159 | print(add()) 160 | print(add(1)) 161 | print(add(1, 2)) 162 | print(add(1, 2, 3)) 163 | print(add(1, 3, 5, 7, 9)) 164 | ``` 165 | 166 | ### 用模块管理函数 167 | 168 | 不管用什么样的编程语言来写代码,给变量、函数起名字都是一个让人头疼的问题,因为我们会遇到**命名冲突**这种尴尬的情况。最简单的场景就是在同一个`.py`文件中定义了两个同名的函数,如下所示。 169 | 170 | ```Python 171 | def foo(): 172 | print('hello, world!') 173 | 174 | 175 | def foo(): 176 | print('goodbye, world!') 177 | 178 | 179 | foo() # 大家猜猜调用foo函数会输出什么 180 | ``` 181 | 182 | 当然上面的这种情况我们很容易就能避免,但是如果项目是团队协作多人开发的时候,团队中可能有多个程序员都定义了名为`foo`的函数,这种情况下怎么解决命名冲突呢?答案其实很简单,Python中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候我们通过`import`关键字导入指定的模块再使用**完全限定名**的调用方式就可以区分到底要使用的是哪个模块中的`foo`函数,代码如下所示。 183 | 184 | `module1.py` 185 | 186 | ```Python 187 | def foo(): 188 | print('hello, world!') 189 | ``` 190 | 191 | `module2.py` 192 | 193 | ```Python 194 | def foo(): 195 | print('goodbye, world!') 196 | ``` 197 | 198 | `test.py` 199 | 200 | ```Python 201 | import module1 202 | import module2 203 | 204 | # 用“模块名.函数名”的方式(完全限定名)调用函数, 205 | module1.foo() # hello, world! 206 | module2.foo() # goodbye, world! 207 | ``` 208 | 209 | 在导入模块时,还可以使用`as`关键字对模块进行别名,这样我们可以使用更为简短的完全限定名。 210 | 211 | `test.py` 212 | 213 | ```Python 214 | import module1 as m1 215 | import module2 as m2 216 | 217 | m1.foo() # hello, world! 218 | m2.foo() # goodbye, world! 219 | ``` 220 | 221 | 上面的代码我们导入了定义函数的模块,我们也可以使用`from...import...`语法从模块中直接导入需要使用的函数,代码如下所示。 222 | 223 | `test.py` 224 | 225 | ```Python 226 | from module1 import foo 227 | 228 | foo() # hello, world! 229 | 230 | from module2 import foo 231 | 232 | foo() # goodbye, world! 233 | ``` 234 | 235 | 但是,如果我们如果从两个不同的模块中导入了同名的函数,后导入的函数会覆盖掉先前的导入,就像下面的代码中,调用`foo`会输出`hello, world!`,因为我们先导入了`module2`的`foo`,后导入了`module1`的`foo` 。如果两个`from...import...`反过来写,就是另外一番光景了。 236 | 237 | `test.py` 238 | 239 | ```Python 240 | from module2 import foo 241 | from module1 import foo 242 | 243 | foo() # hello, world! 244 | ``` 245 | 246 | 如果想在上面的代码中同时使用来自两个模块中的`foo`函数也是有办法的,大家可能已经猜到了,还是用`as`关键字对导入的函数进行别名,代码如下所示。 247 | 248 | `test.py` 249 | 250 | ```Python 251 | from module1 import foo as f1 252 | from module2 import foo as f2 253 | 254 | f1() # hello, world! 255 | f2() # goodbye, world! 256 | ``` 257 | 258 | ### 标准库中的模块和函数 259 | 260 | Python标准库中提供了大量的模块和函数来简化我们的开发工作,我们之前用过的`random`模块就为我们提供了生成随机数和进行随机抽样的函数;而`time`模块则提供了和时间操作相关的函数;上面求阶乘的函数在Python标准库中的`math`模块中已经有了,实际开发中并不需要我们自己编写,而`math`模块中还包括了计算正弦、余弦、指数、对数等一系列的数学函数。随着我们进一步的学习Python编程知识,我们还会用到更多的模块和函数。 261 | 262 | Python标准库中还有一类函数是不需要`import`就能够直接使用的,我们将其称之为内置函数,这些内置函数都是很有用也是最常用的,下面的表格列出了一部分的内置函数。 263 | 264 | | 函数 | 说明 | 265 | | ------- | ------------------------------------------------------------ | 266 | | `abs` | 返回一个数的绝对值,例如:`abs(-1.3)`会返回`1.3`。 | 267 | | `bin` | 把一个整数转换成以`'0b'`开头的二进制字符串,例如:`bin(123)`会返回`'0b1111011'`。 | 268 | | `chr` | 将Unicode编码转换成对应的字符,例如:`chr(8364)`会返回`'€'`。 | 269 | | `hex` | 将一个整数转换成以`'0x'`开头的十六进制字符串,例如:`hex(123)`会返回`'0x7b'`。 | 270 | | `input` | 从输入中读取一行,返回读到的字符串。 | 271 | | `len` | 获取字符串、列表等的长度。 | 272 | | `max` | 返回多个参数或一个可迭代对象中的最大值,例如:`max(12, 95, 37)`会返回`95`。 | 273 | | `min` | 返回多个参数或一个可迭代对象中的最小值,例如:`min(12, 95, 37)`会返回`12`。 | 274 | | `oct` | 把一个整数转换成以`'0o'`开头的八进制字符串,例如:`oct(123)`会返回`'0o173'`。 | 275 | | `open` | 打开一个文件并返回文件对象。 | 276 | | `ord` | 将字符转换成对应的Unicode编码,例如:`ord('€')`会返回`8364`。 | 277 | | `pow` | 求幂运算,例如:`pow(2, 3)`会返回`8`;`pow(2, 0.5)`会返回`1.4142135623730951`。 | 278 | | `print` | 打印输出。 | 279 | | `range` | 构造一个范围序列,例如:`range(100)`会产生`0`到`99`的整数序列。 | 280 | | `round` | 按照指定的精度对数值进行四舍五入,例如:`round(1.23456, 4)`会返回`1.2346`。 | 281 | | `sum` | 对一个序列中的项从左到右进行求和运算,例如:`sum(range(1, 101))`会返回`5050`。 | 282 | | `type` | 返回对象的类型,例如:`type(10)`会返回`int`;而` type('hello')`会返回`str`。 | 283 | 284 | ### 简单的总结 285 | 286 | **函数是对功能相对独立且会重复使用的代码的封装**。学会使用定义和使用函数,就能够写出更为优质的代码。当然,Python语言的标准库中已经为我们提供了大量的模块和常用的函数,用好这些模块和函数就能够用更少的代码做更多的事情;如果这些模块和函数不能满足我们的要求,我们就需要自定义函数,然后用模块的概念来管理这些自定义函数。 -------------------------------------------------------------------------------- /第39课:爬虫框架Scrapy简介.md: -------------------------------------------------------------------------------- 1 | ## 第39课:爬虫框架Scrapy简介 2 | 3 | 当你写了很多个爬虫程序之后,你会发现每次写爬虫程序时,都需要将页面获取、页面解析、爬虫调度、异常处理、反爬应对这些代码从头至尾实现一遍,这里面有很多工作其实都是简单乏味的重复劳动。那么,有没有什么办法可以提升我们编写爬虫代码的效率呢?答案是肯定的,那就是利用爬虫框架,而在所有的爬虫框架中,Scrapy 应该是最流行、最强大的框架。 4 | 5 | ### Scrapy 概述 6 | 7 | Scrapy 是基于 Python 的一个非常流行的网络爬虫框架,可以用来抓取 Web 站点并从页面中提取结构化的数据。下图展示了 Scrapy 的基本架构,其中包含了主要组件和系统的数据处理流程(图中带数字的红色箭头)。 8 | 9 | ![](https://github.com/jackfrued/mypic/raw/master/20210824003638.png) 10 | 11 | #### Scrapy的组件 12 | 13 | 我们先来说说 Scrapy 中的组件。 14 | 15 | 1. Scrapy 引擎(Engine):用来控制整个系统的数据处理流程。 16 | 2. 调度器(Scheduler):调度器从引擎接受请求并排序列入队列,并在引擎发出请求后返还给它们。 17 | 3. 下载器(Downloader):下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)。 18 | 4. 蜘蛛程序(Spiders):蜘蛛是用户自定义的用来解析网页并抓取特定URL的类,每个蜘蛛都能处理一个域名或一组域名,简单的说就是用来定义特定网站的抓取和解析规则的模块。 19 | 5. 数据管道(Item Pipeline):管道的主要责任是负责处理有蜘蛛从网页中抽取的数据条目,它的主要任务是清理、验证和存储数据。当页面被蜘蛛解析后,将被发送到数据管道,并经过几个特定的次序处理数据。每个数据管道组件都是一个 Python 类,它们获取了数据条目并执行对数据条目进行处理的方法,同时还需要确定是否需要在数据管道中继续执行下一步或是直接丢弃掉不处理。数据管道通常执行的任务有:清理 HTML 数据、验证解析到的数据(检查条目是否包含必要的字段)、检查是不是重复数据(如果重复就丢弃)、将解析到的数据存储到数据库(关系型数据库或 NoSQL 数据库)中。 20 | 6. 中间件(Middlewares):中间件是介于引擎和其他组件之间的一个钩子框架,主要是为了提供自定义的代码来拓展 Scrapy 的功能,包括下载器中间件和蜘蛛中间件。 21 | 22 | #### 数据处理流程 23 | 24 | Scrapy 的整个数据处理流程由引擎进行控制,通常的运转流程包括以下的步骤: 25 | 26 | 1. 引擎询问蜘蛛需要处理哪个网站,并让蜘蛛将第一个需要处理的 URL 交给它。 27 | 28 | 2. 引擎让调度器将需要处理的 URL 放在队列中。 29 | 30 | 3. 引擎从调度那获取接下来进行爬取的页面。 31 | 32 | 4. 调度将下一个爬取的 URL 返回给引擎,引擎将它通过下载中间件发送到下载器。 33 | 34 | 5. 当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎;如果下载失败了,引擎会通知调度器记录这个 URL,待会再重新下载。 35 | 36 | 6. 引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。 37 | 38 | 7. 蜘蛛处理响应并返回爬取到的数据条目,此外还要将需要跟进的新的 URL 发送给引擎。 39 | 40 | 8. 引擎将抓取到的数据条目送入数据管道,把新的 URL 发送给调度器放入队列中。 41 | 42 | 上述操作中的第2步到第8步会一直重复直到调度器中没有需要请求的 URL,爬虫就停止工作。 43 | 44 | ### 安装和使用Scrapy 45 | 46 | 可以使用 Python 的包管理工具`pip`来安装 Scrapy。 47 | 48 | ```Shell 49 | pip install scrapy 50 | ``` 51 | 52 | 在命令行中使用`scrapy`命令创建名为`demo`的项目。 53 | 54 | ```Bash 55 | scrapy startproject demo 56 | ``` 57 | 58 | 项目的目录结构如下图所示。 59 | 60 | ```Shell 61 | demo 62 | |____ demo 63 | |________ spiders 64 | |____________ __init__.py 65 | |________ __init__.py 66 | |________ items.py 67 | |________ middlewares.py 68 | |________ pipelines.py 69 | |________ settings.py 70 | |____ scrapy.cfg 71 | ``` 72 | 73 | 切换到`demo` 目录,用下面的命令创建名为`douban`的蜘蛛程序。 74 | 75 | ```Bash 76 | scrapy genspider douban movie.douban.com 77 | ``` 78 | 79 | #### 一个简单的例子 80 | 81 | 接下来,我们实现一个爬取豆瓣电影 Top250 电影标题、评分和金句的爬虫。 82 | 83 | 1. 在`items.py`的`Item`类中定义字段,这些字段用来保存数据,方便后续的操作。 84 | 85 | ```Python 86 | import scrapy 87 | 88 | 89 | class DoubanItem(scrapy.Item): 90 | title = scrapy.Field() 91 | score = scrapy.Field() 92 | motto = scrapy.Field() 93 | ``` 94 | 95 | 2. 修改`spiders`文件夹中名为`douban.py` 的文件,它是蜘蛛程序的核心,需要我们添加解析页面的代码。在这里,我们可以通过对`Response`对象的解析,获取电影的信息,代码如下所示。 96 | 97 | ```Python 98 | import scrapy 99 | from scrapy import Selector, Request 100 | from scrapy.http import HtmlResponse 101 | 102 | from demo.items import MovieItem 103 | 104 | 105 | class DoubanSpider(scrapy.Spider): 106 | name = 'douban' 107 | allowed_domains = ['movie.douban.com'] 108 | start_urls = ['https://movie.douban.com/top250?start=0&filter='] 109 | 110 | def parse(self, response: HtmlResponse): 111 | sel = Selector(response) 112 | movie_items = sel.css('#content > div > div.article > ol > li') 113 | for movie_sel in movie_items: 114 | item = MovieItem() 115 | item['title'] = movie_sel.css('.title::text').extract_first() 116 | item['score'] = movie_sel.css('.rating_num::text').extract_first() 117 | item['motto'] = movie_sel.css('.inq::text').extract_first() 118 | yield item 119 | ``` 120 | 通过上面的代码不难看出,我们可以使用 CSS 选择器进行页面解析。当然,如果你愿意也可以使用 XPath 或正则表达式进行页面解析,对应的方法分别是`xpath`和`re`。 121 | 122 | 如果还要生成后续爬取的请求,我们可以用`yield`产出`Request`对象。`Request`对象有两个非常重要的属性,一个是`url`,它代表了要请求的地址;一个是`callback`,它代表了获得响应之后要执行的回调函数。我们可以将上面的代码稍作修改。 123 | 124 | ```Python 125 | import scrapy 126 | from scrapy import Selector, Request 127 | from scrapy.http import HtmlResponse 128 | 129 | from demo.items import MovieItem 130 | 131 | 132 | class DoubanSpider(scrapy.Spider): 133 | name = 'douban' 134 | allowed_domains = ['movie.douban.com'] 135 | start_urls = ['https://movie.douban.com/top250?start=0&filter='] 136 | 137 | def parse(self, response: HtmlResponse): 138 | sel = Selector(response) 139 | movie_items = sel.css('#content > div > div.article > ol > li') 140 | for movie_sel in movie_items: 141 | item = MovieItem() 142 | item['title'] = movie_sel.css('.title::text').extract_first() 143 | item['score'] = movie_sel.css('.rating_num::text').extract_first() 144 | item['motto'] = movie_sel.css('.inq::text').extract_first() 145 | yield item 146 | 147 | hrefs = sel.css('#content > div > div.article > div.paginator > a::attr("href")') 148 | for href in hrefs: 149 | full_url = response.urljoin(href.extract()) 150 | yield Request(url=full_url) 151 | ``` 152 | 153 | 到这里,我们已经可以通过下面的命令让爬虫运转起来。 154 | 155 | ```Shell 156 | scrapy crawl movie 157 | ``` 158 | 159 | 可以在控制台看到爬取到的数据,如果想将这些数据保存到文件中,可以通过`-o`参数来指定文件名,Scrapy 支持我们将爬取到的数据导出成 JSON、CSV、XML 等格式。 160 | 161 | ```Shell 162 | scrapy crawl moive -o result.json 163 | ``` 164 | 165 | 不知大家是否注意到,通过运行爬虫获得的 JSON 文件中有`275`条数据,那是因为首页被重复爬取了。要解决这个问题,可以对上面的代码稍作调整,不在`parse`方法中解析获取新页面的 URL,而是通过`start_requests`方法提前准备好待爬取页面的 URL,调整后的代码如下所示。 166 | 167 | ```Python 168 | import scrapy 169 | from scrapy import Selector, Request 170 | from scrapy.http import HtmlResponse 171 | 172 | from demo.items import MovieItem 173 | 174 | 175 | class DoubanSpider(scrapy.Spider): 176 | name = 'douban' 177 | allowed_domains = ['movie.douban.com'] 178 | 179 | def start_requests(self): 180 | for page in range(10): 181 | yield Request(url=f'https://movie.douban.com/top250?start={page * 25}') 182 | 183 | def parse(self, response: HtmlResponse): 184 | sel = Selector(response) 185 | movie_items = sel.css('#content > div > div.article > ol > li') 186 | for movie_sel in movie_items: 187 | item = MovieItem() 188 | item['title'] = movie_sel.css('.title::text').extract_first() 189 | item['score'] = movie_sel.css('.rating_num::text').extract_first() 190 | item['motto'] = movie_sel.css('.inq::text').extract_first() 191 | yield item 192 | ``` 193 | 194 | 3. 如果希望完成爬虫数据的持久化,可以在数据管道中处理蜘蛛程序产生的`Item`对象。例如,我们可以通过前面讲到的`openpyxl`操作 Excel 文件,将数据写入 Excel 文件中,代码如下所示。 195 | 196 | ```Python 197 | import openpyxl 198 | 199 | from demo.items import MovieItem 200 | 201 | 202 | class MovieItemPipeline: 203 | 204 | def __init__(self): 205 | self.wb = openpyxl.Workbook() 206 | self.sheet = self.wb.active 207 | self.sheet.title = 'Top250' 208 | self.sheet.append(('名称', '评分', '名言')) 209 | 210 | def process_item(self, item: MovieItem, spider): 211 | self.sheet.append((item['title'], item['score'], item['motto'])) 212 | return item 213 | 214 | def close_spider(self, spider): 215 | self.wb.save('豆瓣电影数据.xlsx') 216 | ``` 217 | 218 | 上面的`process_item`和`close_spider`都是回调方法(钩子函数), 简单的说就是 Scrapy 框架会自动去调用的方法。当蜘蛛程序产生一个`Item`对象交给引擎时,引擎会将该`Item`对象交给数据管道,这时我们配置好的数据管道的`parse_item`方法就会被执行,所以我们可以在该方法中获取数据并完成数据的持久化操作。另一个方法`close_spider`是在爬虫结束运行前会自动执行的方法,在上面的代码中,我们在这个地方进行了保存 Excel 文件的操作,相信这段代码大家是很容易读懂的。 219 | 220 | 总而言之,数据管道可以帮助我们完成以下操作: 221 | 222 | - 清理 HTML 数据,验证爬取的数据。 223 | - 丢弃重复的不必要的内容。 224 | - 将爬取的结果进行持久化操作。 225 | 226 | 4. 修改`settings.py`文件对项目进行配置,主要需要修改以下几个配置。 227 | 228 | ```Python 229 | # 用户浏览器 230 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' 231 | 232 | # 并发请求数量 233 | CONCURRENT_REQUESTS = 4 234 | 235 | # 下载延迟 236 | DOWNLOAD_DELAY = 3 237 | # 随机化下载延迟 238 | RANDOMIZE_DOWNLOAD_DELAY = True 239 | 240 | # 是否遵守爬虫协议 241 | ROBOTSTXT_OBEY = True 242 | 243 | # 配置数据管道 244 | ITEM_PIPELINES = { 245 | 'demo.pipelines.MovieItemPipeline': 300, 246 | } 247 | ``` 248 | 249 | > **说明**:上面配置文件中的`ITEM_PIPELINES`选项是一个字典,可以配置多个处理数据的管道,后面的数字代表了执行的优先级,数字小的先执行。 250 | 251 | -------------------------------------------------------------------------------- /第17课:面向对象编程入门.md: -------------------------------------------------------------------------------- 1 | ## 第17课:面向对象编程入门 2 | 3 | 面向对象编程是一种非常流行的**编程范式**(programming paradigm),所谓编程范式就是**程序设计的方法论**,简单的说就是程序员对程序的认知和理解以及他们编写代码的方式。 4 | 5 | 在前面的课程中,我们说过“**程序是指令的集合**”,运行程序时,程序中的语句会变成一条或多条指令,然后由CPU(中央处理器)去执行。为了简化程序的设计,我们又讲到了函数,**把相对独立且经常重复使用的代码放置到函数中**,在需要使用这些代码的时候调用函数即可。如果一个函数的功能过于复杂和臃肿,我们又可以进一步**将函数进一步拆分为多个子函数**来降低系统的复杂性。 6 | 7 | 不知大家是否发现,我们的编程工作其实是写程序的人按照计算机的工作方式通过代码控制机器完成任务。但是,计算机的工作方式与人类正常的思维模式是不同的,如果编程就必须抛弃人类正常的思维方式去迎合计算机,编程的乐趣就少了很多,而“每个人都应该学习编程”的豪言壮语也就只能喊喊口号而已。这里,我想说的并不是我们不能按照计算机的工作方式去编写代码,但是当我们需要开发一个复杂的系统时,这种方式会让代码过于复杂,从而导致开发和维护工作都变得举步维艰。 8 | 9 | 随着软件复杂性的增加,编写正确可靠的代码会变成了一项极为艰巨的任务,这也是很多人都坚信“软件开发是人类改造世界所有活动中最为复杂的活动”的原因。如何用程序描述复杂系统和解决复杂问题,就成为了所有程序员必须要思考和直面的问题。诞生于上世纪70年代的Smalltalk语言让软件开发者看到了希望,因为它引入了一种新的编程范式叫面向对象编程。在面向对象编程的世界里,程序中的**数据和操作数据的函数是一个逻辑上的整体**,我们称之为**对象**,**对象可以接收消息**,解决问题的方法就是**创建对象并向对象发出各种各样的消息**;通过消息传递,程序中的多个对象可以协同工作,这样就能构造出复杂的系统并解决现实中的问题。当然,面向对象编程的雏形还可以向前追溯到更早期的Simula语言,但这不是我们现在要讨论的重点。 10 | 11 | > **说明:** 今天我们使用的很多高级程序设计语言都支持面向对象编程,但是面向对象编程也不是解决软件开发中所有问题的“银弹”,或者说在软件开发这个行业目前还找不到这种所谓的“银弹”。关于这个问题,大家可以参考IBM360系统之父弗雷德里克·布鲁克斯所发表的论文《没有银弹:软件工程的本质性与附属性工作》或软件工程的经典著作《人月神话》一书。 12 | 13 | ### 类和对象 14 | 15 | 如果要用一句话来概括面向对象编程,我认为下面的说法是相当精辟和准确的。 16 | 17 | > **面向对象编程**:把一组数据和处理数据的方法组成**对象**,把行为相同的对象归纳为**类**,通过**封装**隐藏对象的内部细节,通过**继承**实现类的特化和泛化,通过**多态**实现基于对象类型的动态分派。 18 | 19 | 这句话对初学者来说可能不那么容易理解,但是我可以先为大家圈出几个关键词:**对象**(object)、**类**(class)、**封装**(encapsulation)、**继承**(inheritance)、**多态**(polymorphism)。 20 | 21 | 我们先说说类和对象这两个词。在面向对象编程中,**类是一个抽象的概念,对象是一个具体的概念**。我们把同一类对象的共同特征抽取出来就是一个类,比如我们经常说的人类,这是一个抽象概念,而我们每个人就是人类的这个抽象概念下的实实在在的存在,也就是一个对象。简而言之,**类是对象的蓝图和模板,对象是类的实例,是可以接受消息的实体**。 22 | 23 | 在面向对象编程的世界中,**一切皆为对象**,**对象都有属性和行为**,**每个对象都是独一无二的**,而且**对象一定属于某个类**。对象的属性是对象的静态特征,对象的行为是对象的动态特征。按照上面的说法,如果我们把拥有共同特征的对象的属性和行为都抽取出来,就可以定义出一个类。 24 | 25 | 26 | 27 | ### 定义类 28 | 29 | 在Python中,可以使用`class`关键字加上类名来定义类,通过缩进我们可以确定类的代码块,就如同定义函数那样。在类的代码块中,我们需要写一些函数,我们说过类是一个抽象概念,那么这些函数就是我们对一类对象共同的动态特征的提取。写在类里面的函数我们通常称之为**方法**,方法就是对象的行为,也就是对象可以接收的消息。方法的第一个参数通常都是`self`,它代表了接收这个消息的对象本身。 30 | 31 | ```Python 32 | class Student: 33 | 34 | def study(self, course_name): 35 | print(f'学生正在学习{course_name}.') 36 | 37 | def play(self): 38 | print(f'学生正在玩游戏.') 39 | ``` 40 | 41 | ### 创建和使用对象 42 | 43 | 在我们定义好一个类之后,可以使用构造器语法来创建对象,代码如下所示。 44 | 45 | ```Python 46 | stu1 = Student() 47 | stu2 = Student() 48 | print(stu1) # <__main__.Student object at 0x10ad5ac50> 49 | print(stu2) # <__main__.Student object at 0x10ad5acd0> 50 | print(hex(id(stu1)), hex(id(stu2))) # 0x10ad5ac50 0x10ad5acd0 51 | ``` 52 | 53 | 在类的名字后跟上圆括号就是所谓的构造器语法,上面的代码创建了两个学生对象,一个赋值给变量`stu1`,一个复制给变量`stu2`。当我们用`print`函数打印`stu1`和`stu2`两个变量时,我们会看到输出了对象在内存中的地址(十六进制形式),跟我们用`id`函数查看对象标识获得的值是相同的。现在我们可以告诉大家,我们定义的变量其实保存的是一个对象在内存中的逻辑地址(位置),通过这个逻辑地址,我们就可以在内存中找到这个对象。所以`stu3 = stu2`这样的赋值语句并没有创建新的对象,只是用一个新的变量保存了已有对象的地址。 54 | 55 | 接下来,我们尝试给对象发消息,即调用对象的方法。刚才的`Student`类中我们定义了`study`和`play`两个方法,两个方法的第一个参数`self`代表了接收消息的学生对象,`study`方法的第二个参数是学习的课程名称。Python中,给对象发消息有两种方式,请看下面的代码。 56 | 57 | ```Python 58 | # 通过“类.方法”调用方法,第一个参数是接收消息的对象,第二个参数是学习的课程名称 59 | Student.study(stu1, 'Python程序设计') # 学生正在学习Python程序设计. 60 | # 通过“对象.方法”调用方法,点前面的对象就是接收消息的对象,只需要传入第二个参数 61 | stu1.study('Python程序设计') # 学生正在学习Python程序设计. 62 | 63 | Student.play(stu2) # 学生正在玩游戏. 64 | stu2.play() # 学生正在玩游戏. 65 | ``` 66 | 67 | ### 初始化方法 68 | 69 | 大家可能已经注意到了,刚才我们创建的学生对象只有行为没有属性,如果要给学生对象定义属性,我们可以修改`Student`类,为其添加一个名为`__init__`的方法。在我们调用`Student`类的构造器创建对象时,首先会在内存中获得保存学生对象所需的内存空间,然后通过自动执行`__init__`方法,完成对内存的初始化操作,也就是把数据放到内存空间中。所以我们可以通过给`Student`类添加`__init__`方法的方式为学生对象指定属性,同时完成对属性赋初始值的操作,正因如此,`__init__`方法通常也被称为初始化方法。 70 | 71 | 我们对上面的`Student`类稍作修改,给学生对象添加`name`(姓名)和`age`(年龄)两个属性。 72 | 73 | ```Python 74 | class Student: 75 | """学生""" 76 | 77 | def __init__(self, name, age): 78 | """初始化方法""" 79 | self.name = name 80 | self.age = age 81 | 82 | def study(self, course_name): 83 | """学习""" 84 | print(f'{self.name}正在学习{course_name}.') 85 | 86 | def play(self): 87 | """玩耍""" 88 | print(f'{self.name}正在玩游戏.') 89 | ``` 90 | 91 | 修改刚才创建对象和给对象发消息的代码,重新执行一次,看看程序的执行结果有什么变化。 92 | 93 | ```Python 94 | # 由于初始化方法除了self之外还有两个参数 95 | # 所以调用Student类的构造器创建对象时要传入这两个参数 96 | stu1 = Student('骆昊', 40) 97 | stu2 = Student('王大锤', 15) 98 | stu1.study('Python程序设计') # 骆昊正在学习Python程序设计. 99 | stu2.play() # 王大锤正在玩游戏. 100 | ``` 101 | 102 | ### 打印对象 103 | 104 | 上面我们通过`__init__`方法在创建对象时为对象绑定了属性并赋予了初始值。在Python中,以两个下划线`__`(读作“dunder”)开头和结尾的方法通常都是有特殊用途和意义的方法,我们一般称之为**魔术方法**或**魔法方法**。如果我们在打印对象的时候不希望看到对象的地址而是看到我们自定义的信息,可以通过在类中放置`__repr__`魔术方法来做到,该方法返回的字符串就是用`print`函数打印对象的时候会显示的内容,代码如下所示。 105 | 106 | ```Python 107 | class Student: 108 | """学生""" 109 | 110 | def __init__(self, name, age): 111 | """初始化方法""" 112 | self.name = name 113 | self.age = age 114 | 115 | def study(self, course_name): 116 | """学习""" 117 | print(f'{self.name}正在学习{course_name}.') 118 | 119 | def play(self): 120 | """玩耍""" 121 | print(f'{self.name}正在玩游戏.') 122 | 123 | def __repr__(self): 124 | return f'{self.name}: {self.age}' 125 | 126 | 127 | stu1 = Student('骆昊', 40) 128 | print(stu1) # 骆昊: 40 129 | students = [stu1, Student('李元芳', 36), Student('王大锤', 25)] 130 | print(students) # [骆昊: 40, 李元芳: 36, 王大锤: 25] 131 | ``` 132 | 133 | 134 | ### 面向对象的支柱 135 | 136 | 面向对象编程有三大支柱,就是我们之前给大家划重点的时候圈出的三个词:**封装**、**继承**和**多态**。后面两个概念在下一节课中会详细说明,这里我们先说一下什么是封装。我自己对封装的理解是:**隐藏一切可以隐藏的实现细节,只向外界暴露简单的调用接口**。我们在类中定义的对象方法其实就是一种封装,这种封装可以让我们在创建对象之后,只需要给对象发送一个消息就可以执行方法中的代码,也就是说我们在只知道方法的名字和参数(方法的外部视图),不知道方法内部实现细节(方法的内部视图)的情况下就完成了对方法的使用。 137 | 138 | 举一个例子,假如要控制一个机器人帮我倒杯水,如果不使用面向对象编程,不做任何的封装,那么就需要向这个机器人发出一系列的指令,如站起来、向左转、向前走5步、拿起面前的水杯、向后转、向前走10步、弯腰、放下水杯、按下出水按钮、等待10秒、松开出水按钮、拿起水杯、向右转、向前走5步、放下水杯等,才能完成这个简单的操作,想想都觉得麻烦。按照面向对象编程的思想,我们可以将倒水的操作封装到机器人的一个方法中,当需要机器人帮我们倒水的时候,只需要向机器人对象发出倒水的消息就可以了,这样做不是更好吗? 139 | 140 | 在很多场景下,面向对象编程其实就是一个三步走的问题。第一步定义类,第二步创建对象,第三步给对象发消息。当然,有的时候我们是不需要第一步的,因为我们想用的类可能已经存在了。之前我们说过,Python内置的`list`、`set`、`dict`其实都不是函数而是类,如果要创建列表、集合、字典对象,我们就不用自定义类了。当然,有的类并不是Python标准库中直接提供的,它可能来自于第三方的代码,如何安装和使用三方代码在后续课程中会进行讨论。在某些特殊的场景中,我们会用到名为“内置对象”的对象,所谓“内置对象”就是说上面三步走的第一步和第二步都不需要了,因为类已经存在而且对象已然创建过了,直接向对象发消息就可以了,这也就是我们常说的“开箱即用”。 141 | 142 | ### 经典案例 143 | 144 | #### 案例1:定义一个类描述数字时钟。 145 | 146 | ```Python 147 | import time 148 | 149 | 150 | # 定义数字时钟类 151 | class Clock(object): 152 | """数字时钟""" 153 | 154 | def __init__(self, hour=0, minute=0, second=0): 155 | """初始化方法 156 | :param hour: 时 157 | :param minute: 分 158 | :param second: 秒 159 | """ 160 | self.hour = hour 161 | self.min = minute 162 | self.sec = second 163 | 164 | def run(self): 165 | """走字""" 166 | self.sec += 1 167 | if self.sec == 60: 168 | self.sec = 0 169 | self.min += 1 170 | if self.min == 60: 171 | self.min = 0 172 | self.hour += 1 173 | if self.hour == 24: 174 | self.hour = 0 175 | 176 | def show(self): 177 | """显示时间""" 178 | return f'{self.hour:0>2d}:{self.min:0>2d}:{self.sec:0>2d}' 179 | 180 | 181 | # 创建时钟对象 182 | clock = Clock(23, 59, 58) 183 | while True: 184 | # 给时钟对象发消息读取时间 185 | print(clock.show()) 186 | # 休眠1秒钟 187 | time.sleep(1) 188 | # 给时钟对象发消息使其走字 189 | clock.run() 190 | ``` 191 | 192 | #### 案例2:定义一个类描述平面上的点,要求提供计算到另一个点距离的方法。 193 | 194 | ```Python 195 | class Point(object): 196 | """屏面上的点""" 197 | 198 | def __init__(self, x=0, y=0): 199 | """初始化方法 200 | :param x: 横坐标 201 | :param y: 纵坐标 202 | """ 203 | self.x, self.y = x, y 204 | 205 | def distance_to(self, other): 206 | """计算与另一个点的距离 207 | :param other: 另一个点 208 | """ 209 | dx = self.x - other.x 210 | dy = self.y - other.y 211 | return (dx * dx + dy * dy) ** 0.5 212 | 213 | def __str__(self): 214 | return f'({self.x}, {self.y})' 215 | 216 | 217 | p1 = Point(3, 5) 218 | p2 = Point(6, 9) 219 | print(p1, p2) 220 | print(p1.distance_to(p2)) 221 | ``` 222 | 223 | ### 简单的总结 224 | 225 | 面向对象编程是一种非常流行的编程范式,除此之外还有**指令式编程**、**函数式编程**等编程范式。由于现实世界是由对象构成的,而对象是可以接收消息的实体,所以**面向对象编程更符合人类正常的思维习惯**。类是抽象的,对象是具体的,有了类就能创建对象,有了对象就可以接收消息,这就是面向对象编程的基础。定义类的过程是一个抽象的过程,找到对象公共的属性属于数据抽象,找到对象公共的方法属于行为抽象。抽象的过程是一个仁者见仁智者见智的过程,对同一类对象进行抽象可能会得到不同的结果,如下图所示。 226 | 227 | 228 | 229 | > **说明:** 本节课的插图来自于 Grady Booc 等撰写的《面向对象分析与设计》一书,该书是讲解面向对象编程的经典著作,有兴趣的读者可以购买和阅读这本书来了解更多的面向对象的相关知识。 230 | 231 | --------------------------------------------------------------------------------