更新了部分文档
|
@ -1,10 +1,8 @@
|
|||
## Python语言基础50课
|
||||
|
||||
由于之前发布的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”](<https://zhuanlan.zhihu.com/c_1216656665569013760>)专栏,专栏也在持续更新中,还有大家比较期待的“数据分析”和“面试宝典”的内容也即将上线。**有需要的小伙伴可以关注我在知乎的专栏、文章和回答**,当然,也**欢迎大家评论、收藏和点赞**。
|
||||
由于之前发布的 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”](<https://zhuanlan.zhihu.com/c_1216656665569013760>)专栏,两边同步更新。有需要的小伙伴可以关注我在知乎的专栏、文章和回答,当然,也欢迎大家评论、收藏和点赞。
|
||||
|
||||
近期我们公司组织了大量免费的线上公开课和体验课,对于新手来说,上手一门新语言的时候,如果**有人指导可以少走很多的弯路**。我们的体验课几乎每周都有,我也会**每周更新体验课的信息**,需要参加的小伙伴可以直接**扫描下方的二维码联系我们**,体验课的**班级讨论群**会一直保留,也会有**老师答疑**,希望能够帮助到大家。最为关键的是**这些都是免费的**,没有任何人强制你消费一毛钱。在收到体验账号后,如果愿意你可以屏蔽掉付费课程销售人员的电话和QQ,我们并不会因此将你从学习群中移除掉,仍然会一如既往的帮助你。所以希望大家能够理解公司行为和我个人行为的区别,我就是一个单纯的程序员,我分享这些知识是为了让更多的人学习编程并用它来改变自己的生活。**当键盘侠和五毛党很容易,创作不易,分享自己知识和经验更加不易**。
|
||||
|
||||
> **温馨提示**:最近,国内访问GitHub会因为DNS(域名解析服务)的问题出现**图片无法显示**的情况,如果你也遇到了这样的问题,可以通过**修改本机的`hosts`文件**直接对GitHub的资源链接进行域名解析来加以解决。使用macOS系统的读者可以参考[《macOS 下三种修改 hosts 文件的方法》](<https://www.jianshu.com/p/752211238c1b>)一文来修改`hosts`文件;使用Windows系统的读者可以参考[《在 Windows 上如何管理 hosts 文件》](<https://sspai.com/post/43248>)一文来进行操作。我们可以把下面的内容添加到`hosts`文件的末尾,这样就可以解决GitHub上图片无法显示的问题。
|
||||
最近,国内访问 GitHub 会因为 DNS(域名解析服务)的问题出现**图片无法显示**的情况,如果你也遇到了这样的问题,可以通过**修改本机的 hosts 文件**直接对 GitHub 的资源链接进行域名解析来加以解决。使用 macOS 系统的读者可以参考[《macOS 下三种修改 hosts 文件的方法》](<https://www.jianshu.com/p/752211238c1b>)一文来修改 hosts 文件;使用 Windows 系统的读者可以参考[《在 Windows 上如何管理 hosts 文件》](<https://sspai.com/post/43248>)一文来进行操作。我们可以把下面的内容添加到 hosts 文件的末尾,这样就可以解决 GitHub 上图片无法显示的问题。
|
||||
|
||||
```INI
|
||||
151.101.184.133 assets-cdn.github.com
|
||||
|
@ -14,4 +12,4 @@
|
|||
151.101.184.133 camo.githubusercontent.com
|
||||
```
|
||||
|
||||
![](res/qrcode-group.png)
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210822092730.png" alt="20933D073D8CCF2D8537AD1E666330E4" width="35%">
|
||||
|
|
BIN
res/ENIAC.jpg
Before Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 352 KiB |
Before Width: | Height: | Size: 393 KiB |
Before Width: | Height: | Size: 206 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 335 KiB |
Before Width: | Height: | Size: 310 KiB |
Before Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 177 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 456 KiB |
Before Width: | Height: | Size: 723 KiB |
Before Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 288 KiB |
BIN
res/equation.png
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 200 KiB |
Before Width: | Height: | Size: 904 KiB |
Before Width: | Height: | Size: 717 KiB |
Before Width: | Height: | Size: 520 KiB |
Before Width: | Height: | Size: 716 KiB |
Before Width: | Height: | Size: 682 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 142 KiB |
Before Width: | Height: | Size: 268 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 594 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 304 KiB |
BIN
res/qrcode.png
Before Width: | Height: | Size: 198 KiB |
BIN
res/set.png
Before Width: | Height: | Size: 286 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 26 KiB |
BIN
res/vscode.png
Before Width: | Height: | Size: 86 KiB |
BIN
res/winver.png
Before Width: | Height: | Size: 139 KiB |
|
@ -74,4 +74,3 @@ macOS自带了Python 2,但是我们需要安装和使用的是Python 3。可
|
|||
### 总结
|
||||
|
||||
Python语言可以做很多的事情,也值得我们去学习。要使用Python语言,首先需要在自己的计算机上安装Python环境,也就是运行Python程序的Python解释器。
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ else:
|
|||
如果要构造出更多的分支,可以使用`if...elif...else...`结构或者嵌套的`if...else...`结构,下面的代码演示了如何利用多分支结构实现分段函数求值。
|
||||
|
||||
$$
|
||||
f(x) = \begin{cases} 3x - 5, & x \gt 1 \\ x + 2, & -1 \le x \le 1 \\ 5x + 3, & x \lt -1 \end{cases}
|
||||
f(x) = \begin{cases} 3x - 5, & (x \gt 1) \\ x + 2, & (-1 \le x \le 1) \\ 5x + 3, & (x \lt -1) \end{cases}
|
||||
$$
|
||||
|
||||
```Python
|
||||
|
@ -144,4 +144,3 @@ else:
|
|||
### 简单的总结
|
||||
|
||||
学会了Python中的分支结构和循环结构,我们就可以用Python程序来解决很多实际的问题了。这一节课相信已经帮助大家记住了`if`、`elif`、`else`这几个关键字以及如何使用它们来构造分支结构,下一节课我们为大家介绍循环结构,学完这两次课你一定会发现,你能写出很多很多非常有意思的代码。继续加油!
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ person3 = {'王大锤', 55, 60, '科华北路62号', '中同仁路8号', '131223
|
|||
|
||||
说到字典这个词,大家一定不陌生,读小学的时候每个人基本上都有一本《新华字典》,如下图所示。
|
||||
|
||||
![](res/dictionary.jpg)
|
||||
![dictionary](https://gitee.com/jackfrued/mypic/raw/master/20210820204829.jpg)
|
||||
|
||||
Python程序中的字典跟现实生活中的字典很像,它以键值对(键和值的组合)的方式把数据组织到一起,我们可以通过键找到与之对应的值并进行操作。就像《新华字典》中,每个字(键)都有与它对应的解释(值)一样,每个字和它的解释合在一起就是字典中的一个条目,而字典中通常包含了很多个这样的条目。
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ def calc(*args, **kwargs):
|
|||
for value in kwargs.values():
|
||||
if type(value) in (int, float):
|
||||
result += value
|
||||
return total
|
||||
return result
|
||||
|
||||
|
||||
print(calc()) # 0
|
||||
|
|
|
@ -227,8 +227,7 @@ print('程序执行结束.')
|
|||
|
||||
```Python
|
||||
try:
|
||||
with open('guido.jpg', 'rb') as file1, \
|
||||
open('吉多.jpg', 'wb') as file2:
|
||||
with open('guido.jpg', 'rb') as file1, open('吉多.jpg', 'wb') as file2:
|
||||
data = file1.read(512)
|
||||
while data:
|
||||
file2.write(data)
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
## 第22课:对象的序列化和反序列化
|
||||
|
||||
###读写JSON格式的数据
|
||||
###JSON概述
|
||||
|
||||
通过上面的讲解,我们已经知道如何将文本数据和二进制数据保存到文件中,那么这里还有一个问题,如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢?在Python中,我们可以将程序中的数据以JSON格式进行保存。JSON是“JavaScript Object Notation”的缩写,它本来是JavaScript语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨语言跨平台的数据交换。使用JSON的原因非常简单,因为它结构紧凑而且是纯文本,任何操作系统和编程语言都能处理纯文本,这就是**实现跨语言跨平台数据交换**的前提条件。目前JSON基本上已经取代了XML(可扩展标记语言)作为**异构系统间交换数据的事实标准**。可以在[JSON的官方网站](https://www.json.org/json-zh.html)找到更多关于JSON的知识,这个网站还提供了每种语言处理JSON数据格式可以使用的工具或三方库。
|
||||
|
||||
下面是JSON格式的一个简单例子,大家可能已经注意到了,它跟Python中的字典非常类似而且支持嵌套结构,就像Python字典中的值可以是字典。如果我们把下面的代码输入到浏览器控制台中,它会创建出一个JavaScript中的对象。
|
||||
|
||||
```JSON
|
||||
```JavaScript
|
||||
{
|
||||
"name": "骆昊",
|
||||
"age": 40,
|
||||
"friends": ["王大锤", "白元芳"],
|
||||
"cars": [
|
||||
name: "骆昊",
|
||||
age: 40,
|
||||
friends: ["王大锤", "白元芳"],
|
||||
cars: [
|
||||
{"brand": "BMW", "max_speed": 240},
|
||||
{"brand": "Benz", "max_speed": 280},
|
||||
{"brand": "Audi", "max_speed": 280}
|
||||
|
@ -19,29 +17,79 @@
|
|||
}
|
||||
```
|
||||
|
||||
![](https://gitee.com/jackfrued/mypic/raw/master/20210803201758.png)
|
||||
上面是JSON的一个简单例子,大家可能已经注意到了,它跟Python中的字典非常类似而且支持嵌套结构,就好比Python字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台(对于Chrome浏览器,可以通过“更多工具”菜单找到“开发者工具”子菜单,就可以打开浏览器的控制台),浏览器的控制台提供了一个运行JavaScript代码的交互式环境(类似于Python的交互式环境),下面的代码会帮我们创建出一个JavaScript的对象,我们将其赋值给名为`obj`的变量。
|
||||
|
||||
JSON格式的数据类型和Python中的数据类型也是很容易找到对应关系的,正如下面的两张表所示。
|
||||
```JavaScript
|
||||
let obj = {
|
||||
name: "骆昊",
|
||||
age: 40,
|
||||
friends: ["王大锤", "白元芳"],
|
||||
cars: [
|
||||
{"brand": "BMW", "max_speed": 240},
|
||||
{"brand": "Benz", "max_speed": 280},
|
||||
{"brand": "Audi", "max_speed": 280}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| JSON | Python |
|
||||
| ------------------- | ------------ |
|
||||
| object | dict |
|
||||
| array | list |
|
||||
| string | str |
|
||||
| number (int / real) | int / float |
|
||||
| true / false | True / False |
|
||||
| null | None |
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820143803.png" alt="image-20210820143756353" width="80%">
|
||||
|
||||
| Python | JSON |
|
||||
| -------------------------------------- | ------------ |
|
||||
| dict | object |
|
||||
| list, tuple | array |
|
||||
| str | string |
|
||||
| int, float, int- & float-derived Enums | number |
|
||||
| True / False | true / false |
|
||||
| None | null |
|
||||
上面的`obj`就是JavaScript中的一个对象,我们可以通过`obj.name`或`obj["name"]`两种方式获取到`name`对应的值,如下图所示。可以注意到,`obj["name"]`这种获取数据的方式跟Python字典通过键获取值的索引操作是完全一致的,而Python中也通过名为`json`的模块提供了字典与JSON双向转换的支持。
|
||||
|
||||
在Python中,我们可以使用`json`模块将字典或列表以JSON格式写入到文件中,代码如下所示。
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820144411.png" width="85%">
|
||||
|
||||
我们在JSON中使用的数据类型(JavaScript数据类型)和Python中的数据类型也是很容易找到对应关系的,大家可以看看下面的两张表。
|
||||
|
||||
表1:JavaScript数据类型(值)对应的Python数据类型(值)
|
||||
|
||||
| JSON | Python |
|
||||
| ------------ | ------------ |
|
||||
| `object` |`dict`|
|
||||
| `array` |`list`|
|
||||
| `string` | `str` |
|
||||
| `number ` |`int` / `float`|
|
||||
| `number` (real) |`float`|
|
||||
| `boolean` (`true` / `false`) | `bool` (`True` / `False`) |
|
||||
| `null` | `None` |
|
||||
|
||||
表2:Python数据类型(值)对应的JavaScript数据类型(值)
|
||||
|
||||
| Python | JSON |
|
||||
| --------------------------- | ---------------------------- |
|
||||
| `dict` | `object` |
|
||||
| `list` / `tuple` | `array` |
|
||||
| `str` | `string` |
|
||||
| `int` / `float` | `number` |
|
||||
| `bool` (`True` / `False`) | `boolean` (`true` / `false`) |
|
||||
| `None` | `null` |
|
||||
|
||||
### 读写JSON格式的数据
|
||||
|
||||
在Python中,如果要将字典处理成JSON格式(以字符串形式存在),可以使用`json`模块的`dumps`函数,代码如下所示。
|
||||
|
||||
```Python
|
||||
import json
|
||||
|
||||
my_dict = {
|
||||
'name': '骆昊',
|
||||
'age': 40,
|
||||
'friends': ['王大锤', '白元芳'],
|
||||
'cars': [
|
||||
{'brand': 'BMW', 'max_speed': 240},
|
||||
{'brand': 'Audi', 'max_speed': 280},
|
||||
{'brand': 'Benz', 'max_speed': 280}
|
||||
]
|
||||
}
|
||||
print(json.dumps(my_dict))
|
||||
```
|
||||
|
||||
运行上面的代码,输出如下所示,可以注意到中文字符都是用Unicode编码显示的。
|
||||
|
||||
```JSON
|
||||
{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}
|
||||
```
|
||||
|
||||
如果要将字典处理成JSON格式并写入文本文件,只需要将`dumps`函数换成`dump`函数并传入文件对象即可,代码如下所示。
|
||||
|
||||
```Python
|
||||
import json
|
||||
|
@ -58,14 +106,9 @@ my_dict = {
|
|||
}
|
||||
with open('data.json', 'w') as file:
|
||||
json.dump(my_dict, file)
|
||||
print('字典已经保存到data.json文件中')
|
||||
```
|
||||
|
||||
执行上面的代码,会创建`data.json`文件,文件的内容如下所示,中文是用Unicode编码书写的。
|
||||
|
||||
```JSON
|
||||
{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}
|
||||
```
|
||||
执行上面的代码,会创建`data.json`文件,文件的内容跟上面代码的输出是一样的。
|
||||
|
||||
`json`模块有四个比较重要的函数,分别是:
|
||||
|
||||
|
@ -76,7 +119,7 @@ print('字典已经保存到data.json文件中')
|
|||
|
||||
这里出现了两个概念,一个叫序列化,一个叫反序列化,[维基百科](https://zh.wikipedia.org/)上的解释是:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
|
||||
|
||||
我们可以通过下面的代码,从上面创建的`data.json`文件中读取JSON格式的数据并还原成字典。
|
||||
我们可以通过下面的代码,读取上面创建的`data.json`文件,将JSON格式的数据还原成Python中的字典。
|
||||
|
||||
```Python
|
||||
import json
|
||||
|
@ -100,7 +143,7 @@ pip install ujson
|
|||
在默认情况下,pip会访问`https://pypi.org/simple/`来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网提供的镜像来替代这个默认的下载源,操作如下所示。
|
||||
|
||||
```Bash
|
||||
pip install ujson -i https://pypi.doubanio.com/simple
|
||||
pip install ujson
|
||||
```
|
||||
|
||||
可以通过`pip search`命令根据名字查找需要的三方库,可以通过`pip list`命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用`pip install -U`或`pip install --upgrade`;如果要删除某个三方库,可以使用`pip uninstall`命令。
|
||||
|
@ -109,6 +152,7 @@ pip install ujson -i https://pypi.doubanio.com/simple
|
|||
|
||||
```Bash
|
||||
pip search ujson
|
||||
|
||||
micropython-cpython-ujson (0.2) - MicroPython module ujson ported to CPython
|
||||
pycopy-cpython-ujson (0.2) - Pycopy module ujson ported to CPython
|
||||
ujson (3.0.0) - Ultra fast JSON encoder and decoder for Python
|
||||
|
@ -131,6 +175,7 @@ decimal-monkeypatch (0.4.3) - Python 2 performance patches: decimal to cdec
|
|||
|
||||
```Bash
|
||||
pip list
|
||||
|
||||
Package Version
|
||||
----------------------------- ----------
|
||||
aiohttp 3.5.4
|
||||
|
@ -143,21 +188,7 @@ amqp 2.4.2
|
|||
更新`ujson`三方库。
|
||||
|
||||
```Bash
|
||||
pip install -U ujson -i https://pypi.doubanio.com/simple
|
||||
```
|
||||
|
||||
如果要更新pip本身,可以使用下面的命令。
|
||||
|
||||
macOS系统:
|
||||
|
||||
```Bash
|
||||
pip3 install -U pip
|
||||
```
|
||||
|
||||
Windows系统:
|
||||
|
||||
```Bash
|
||||
python -m pip install -U pip
|
||||
pip install -U ujson
|
||||
```
|
||||
|
||||
删除`ujson`三方库。
|
||||
|
@ -166,16 +197,20 @@ python -m pip install -U pip
|
|||
pip uninstall -y ujson
|
||||
```
|
||||
|
||||
> **提示**:如果要更新`pip`自身,对于macOS系统来说,可以使用命令`pip install -U pip`。在Windows系统上,可以将命令替换为`python -m pip install -U --user pip`。
|
||||
|
||||
### 使用网络API获取数据
|
||||
|
||||
如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络API)都是基于[HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)提供JSON格式的数据,在Python程序中,我们可以发送HTTP请求给指定的URL(统一资源定位符),这个URL就是所谓的网络API,如果请求成功,它会返回HTTP响应,而HTTP响应的消息体中就有我们需要的JSON格式的数据。关于HTTP的相关知识,可以看看阮一峰的[《HTTP协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)一文。
|
||||
如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络API)都是基于[HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)或HTTPS提供JSON格式的数据,我们可以通过Python程序发送HTTP请求给指定的URL(统一资源定位符),这个URL就是所谓的网络API,如果请求成功,它会返回HTTP响应,而HTTP响应的消息体中就有我们需要的JSON格式的数据。关于HTTP的相关知识,可以看看阮一峰的[《HTTP协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)一文。
|
||||
|
||||
国内有很多提供网络API接口的网站,例如[聚合数据](https://www.juhe.cn/)、[阿凡达数据](http://www.avatardata.cn/)等,这些网站上有免费的和付费的数据接口,国外的[{API}Search](http://apis.io/)网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用[`requests`](http://docs.python-requests.org/zh_CN/latest/)库(基于HTTP进行网络资源访问的三方库)访问网络API获取国内新闻并显示新闻标题和链接,这个例子使用了[天行数据](https://www.tianapi.com/)提供的国内新闻数据接口,其中的APIKey需要自己到网站上注册申请。
|
||||
国内有很多提供网络API接口的网站,例如[聚合数据](https://www.juhe.cn/)、[阿凡达数据](http://www.avatardata.cn/)等,这些网站上有免费的和付费的数据接口,国外的[{API}Search](http://apis.io/)网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用[`requests`](http://docs.python-requests.org/zh_CN/latest/)库(基于HTTP进行网络资源访问的三方库)访问网络API获取国内新闻并显示新闻标题和链接。在这个例子中,我们使用了名为[天行数据](https://www.tianapi.com/)的网站提供的国内新闻数据接口,其中的APIKey需要自己到网站上注册申请。在天行数据网站注册账号后会自动分配APIKey,但是要访问接口获取数据,需要绑定验证邮箱或手机,然后还要申请需要使用的接口,如下图所示。
|
||||
|
||||
安装`requests`库。
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820151134.png" alt="image-20210820151134034" width="100%">
|
||||
|
||||
Python通过URL接入网络,我们推荐大家使用`requests`三方库,它简单且强大,但需要自行安装。
|
||||
|
||||
```Bash
|
||||
pip install requests -i https://pypi.doubanio.com/simple/
|
||||
pip install requests
|
||||
```
|
||||
|
||||
获取国内新闻并显示新闻标题和链接。
|
||||
|
@ -192,7 +227,11 @@ if resp.status_code == 200:
|
|||
print('-' * 60)
|
||||
```
|
||||
|
||||
> **注意**:上面代码中的APIKey需要换成自己在天行数据网站申请的APIKey,同时还要申请开通国内新闻的接口才能获取到JSON格式的数据。这个网站上还有很多非常有意思的网络API接口,例如:垃圾分类、舔狗日记、周公解梦等等,大家可以仿照上面的代码来调用这些接口。
|
||||
上面的代码通过`requests`模块的`get`函数向天行数据的国内新闻接口发起了一次请求,如果请求过程没有出现问题,`get`函数会返回一个`Response`对象,通过该对象的`status_code`属性表示HTTP响应状态码,如果不理解没关系,你只需要关注它的值,如果值等于`200`或者其他`2`字头的值,那么我们的请求是成功的。通过`Response`对象的`json()`方法可以将返回的JSON格式的数据直接处理成Python字典,非常方便。天行数据国内新闻接口返回的JSON格式的数据(部分)如下图所示。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820154455.png" width="100%">
|
||||
|
||||
> **提示**:上面代码中的APIKey需要换成自己在天行数据网站申请的APIKey。天行数据网站上还有提供了很多非常有意思的API接口,例如:垃圾分类、周公解梦等,大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档,文档中有关于如何使用接口的详细说明。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
|
|
|
@ -57,6 +57,8 @@ print(sheet.row_values(0))
|
|||
print(sheet.row_slice(3, 0, 5))
|
||||
```
|
||||
|
||||
> **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xls”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
|
||||
|
||||
相信通过上面的代码,大家已经了解到了如何读取一个Excel文件,如果想知道更多关于`xlrd`模块的知识,可以阅读它的[官方文档](https://xlrd.readthedocs.io/en/latest/)。
|
||||
|
||||
### 写Excel文件
|
||||
|
@ -183,7 +185,7 @@ sheet2.write(nrows, 6, xlwt.Formula(f'sum(G2:G{nrows})'))
|
|||
wb_for_write.save('阿里巴巴2020年股票数据汇总.xls')
|
||||
```
|
||||
|
||||
> **说明**:上面的代码有一些小瑕疵,有兴趣的读者可以自行探索如何解决。
|
||||
> **说明**:上面的代码有一些小瑕疵,有兴趣的读者可以自行探索并思考如何解决。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
|
|
|
@ -55,6 +55,8 @@ for row_ch in range(2, sheet.max_row + 1):
|
|||
print()
|
||||
```
|
||||
|
||||
> **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xlsx”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
|
||||
|
||||
需要提醒大家一点,`openpyxl`获取指定的单元格有两种方式,一种是通过`cell`方法,需要注意,该方法的行索引和列索引都是从`1`开始的,这是为了照顾用惯了Excel的人的习惯;另一种是通过索引运算,通过指定单元格的坐标,例如`C3`、`G255`,也可以取得对应的单元格,再通过单元格对象的`value`属性,就可以获取到单元格的值。通过上面的代码,相信大家还注意到了,可以通过类似`sheet['A2:C5']`或`sheet['A2':'C5']`这样的切片操作获取多个单元格,该操作将返回嵌套的元组,相当于获取到了多行多列。
|
||||
|
||||
### 写Excel文件
|
||||
|
|
|
@ -83,6 +83,8 @@ document.add_page_break()
|
|||
document.save('demo.docx')
|
||||
```
|
||||
|
||||
> **提示**:上面代码第7行中的注释`# type: Doc`是为了在PyCharm中获得代码补全提示,因为如果不清楚对象具体的数据类型,PyCharm无法在后续代码中给出`Doc`对象的代码补全提示。
|
||||
|
||||
执行上面的代码,打开生成的Word文档,效果如下图所示。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820002742.png" alt="image-20210820002742341" width="40%"> <img src="https://gitee.com/jackfrued/mypic/raw/master/20210820002843.png" alt="image-20210820002843696" width="40%">
|
||||
|
@ -98,7 +100,7 @@ for no, p in enumerate(doc.paragraphs):
|
|||
print(no, p.text)
|
||||
```
|
||||
|
||||
> **提示**:如果需要上面代码中的Word文件,可以通过下面的百度云盘地址进行获取。
|
||||
> **提示**:如果需要上面代码中的Word文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
|
||||
|
||||
读取到的内容如下所示。
|
||||
|
||||
|
@ -238,3 +240,6 @@ pres.save('test.pptx')
|
|||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820010306.png" alt="image-20210820010306008" width="75%" />
|
||||
|
||||
### 简单的总结
|
||||
|
||||
用Python程序解决办公自动化的问题真的非常酷,它可以将我们从繁琐乏味的劳动中解放出来。写这类代码就是去做一件一劳永逸的事情,写代码的过程即便不怎么愉快,使用这些代码的时候应该是非常开心的。
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
## 第27课:用Python操作PDF文件
|
||||
|
||||
PDF是Portable Document Format的缩写,这类文件通常使用`.pdf`作为其扩展名。在日常开发工作中,最容易遇到的就是从PDF中读取文本内容以及用已有的内容生成PDF文档这两个任务。
|
||||
|
||||
### 从PDF中提取文本
|
||||
|
||||
在Python中,可以使用名为`PyPDF2`的三方库来读取PDF文件,可以使用下面的命令来安装它。
|
||||
|
||||
```Bash
|
||||
pip install PyPDF2
|
||||
```
|
||||
|
||||
`PyPDF2`没有办法从PDF文档中提取图像、图表或其他媒体,但它可以提取文本,并将其返回为Python字符串。
|
||||
|
||||
```Python
|
||||
import PyPDF2
|
||||
|
||||
reader = PyPDF2.PdfFileReader('test.pdf')
|
||||
page = reader.getPage(0)
|
||||
print(page.extractText())
|
||||
```
|
||||
|
||||
> **提示**:上面代码中使用的PDF文件“test.pdf”以及下面的代码中需要用到的PDF文件,都可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
|
||||
|
||||
当然,`PyPDF2`并不是什么样的PDF文档都能提取出文字来,这个问题就我所知并没有什么特别好的解决方法,尤其是在提取中文的时候。网上也有很多讲解从PDF中提取文字的文章,推荐大家自行阅读[《三大神器助力Python提取pdf文档信息》](https://cloud.tencent.com/developer/article/1395339)一文进行了解。
|
||||
|
||||
要从PDF文件中提取文本也可以直接使用三方的命令行工具,具体的做法如下所示。
|
||||
|
||||
```Bash
|
||||
pip install pdfminer.six
|
||||
pdf2text.py test.pdf
|
||||
```
|
||||
|
||||
### 旋转和叠加页面
|
||||
|
||||
上面的代码中通过创建`PdfFileReader`对象的方式来读取PDF文档,该对象的`getPage`方法可以获得PDF文档的指定页并得到一个`PageObject`对象,通过`PageObject`对象的`rotateClockwise`和`rotateCounterClockwise`方法可以实现页面的顺时针和逆时针方向旋转,通过`PageObject`对象的`addBlankPage`方法可以添加一个新的空白页,代码如下所示。
|
||||
|
||||
```Python
|
||||
import PyPDF2
|
||||
|
||||
from PyPDF2.pdf import PageObject
|
||||
|
||||
# 创建一个读PDF文件的Reader对象
|
||||
reader = PyPDF2.PdfFileReader('resources/XGBoost.pdf')
|
||||
# 创建一个写PDF文件的Writer对象
|
||||
writer = PyPDF2.PdfFileWriter()
|
||||
# 对PDF文件所有页进行循环遍历
|
||||
for page_num in range(reader.numPages):
|
||||
# 获取指定页码的Page对象
|
||||
current_page = reader.getPage(page_num) # type: PageObject
|
||||
if page_num % 2 == 0:
|
||||
# 奇数页顺时针旋转90度
|
||||
current_page.rotateClockwise(90)
|
||||
else:
|
||||
# 偶数页反时针旋转90度
|
||||
current_page.rotateCounterClockwise(90)
|
||||
writer.addPage(current_page)
|
||||
# 最后添加一个空白页并旋转90度
|
||||
page = writer.addBlankPage() # type: PageObject
|
||||
page.rotateClockwise(90)
|
||||
# 通过Writer对象的write方法将PDF写入文件
|
||||
with open('resources/XGBoost-modified.pdf', 'wb') as file:
|
||||
writer.write(file)
|
||||
```
|
||||
|
||||
### 加密PDF文件
|
||||
|
||||
使用`PyPDF2`中的`PdfFileWrite`对象可以为PDF文档加密,如果需要给一系列的PDF文档设置统一的访问口令,使用Python程序来处理就会非常的方便。
|
||||
|
||||
```Python
|
||||
import PyPDF2
|
||||
|
||||
reader = PyPDF2.PdfFileReader('resources/XGBoost.pdf')
|
||||
writer = PyPDF2.PdfFileWriter()
|
||||
for page_num in range(reader.numPages):
|
||||
writer.addPage(reader.getPage(page_num))
|
||||
# 通过encrypt方法加密PDF文件,方法的参数就是设置的密码
|
||||
writer.encrypt('foobared')
|
||||
with open('resources/XGBoost-encrypted.pdf', 'wb') as file:
|
||||
writer.write(file)
|
||||
```
|
||||
|
||||
### 批量添加水印
|
||||
|
||||
上面提到的`PageObject`对象还有一个名为`mergePage`的方法,可以两个PDF页面进行叠加,通过这个操作,我们很容易实现给PDF文件添加水印的功能。例如要给上面的“XGBoost.pdf”文件添加一个水印,我们可以先准备好一个提供水印页面的PDF文件,然后将包含水印的`PageObject`读取出来,然后再循环遍历“XGBoost.pdf”文件的每个页,获取到`PageObject`对象,然后通过`mergePage`方法实现水印页和原始页的合并,代码如下所示。
|
||||
|
||||
```Python
|
||||
import PyPDF2
|
||||
|
||||
from PyPDF2.pdf import PageObject
|
||||
|
||||
reader1 = PyPDF2.PdfFileReader('resources/XGBoost.pdf')
|
||||
reader2 = PyPDF2.PdfFileReader('resources/watermark.pdf')
|
||||
writer = PyPDF2.PdfFileWriter()
|
||||
# 获取水印页
|
||||
watermark_page = reader2.getPage(0)
|
||||
for page_num in range(reader1.numPages):
|
||||
current_page = reader1.getPage(page_num) # type: PageObject
|
||||
current_page.mergePage(watermark_page)
|
||||
# 将原始页和水印页进行合并
|
||||
writer.addPage(current_page)
|
||||
# 将PDF写入文件
|
||||
with open('resources/XGBoost-watermarked.pdf', 'wb') as file:
|
||||
writer.write(file)
|
||||
```
|
||||
|
||||
如果愿意,还可以让奇数页和偶数页使用不同的水印,大家可以自己思考下应该怎么做。
|
||||
|
||||
### 创建PDF文件
|
||||
|
||||
创建PDF文档需要三方库`reportlab`的支持,安装的方法如下所示。
|
||||
|
||||
```Bash
|
||||
pip install reportlab
|
||||
```
|
||||
|
||||
下面通过一个例子为大家展示`reportlab`的用法。
|
||||
|
||||
```Python
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
pdf_canvas = canvas.Canvas('resources/demo.pdf', pagesize=A4)
|
||||
width, height = A4
|
||||
|
||||
# 绘图
|
||||
image = canvas.ImageReader('resources/guido.jpg')
|
||||
pdf_canvas.drawImage(image, 20, height - 395, 250, 375)
|
||||
|
||||
# 显示当前页
|
||||
pdf_canvas.showPage()
|
||||
|
||||
# 注册字体文件
|
||||
pdfmetrics.registerFont(TTFont('Font1', 'resources/fonts/Vera.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('Font2', 'resources/fonts/青呱石头体.ttf'))
|
||||
|
||||
# 写字
|
||||
pdf_canvas.setFont('Font2', 40)
|
||||
pdf_canvas.setFillColorRGB(0.9, 0.5, 0.3, 1)
|
||||
pdf_canvas.drawString(width // 2 - 120, height // 2, '你好,世界!')
|
||||
pdf_canvas.setFont('Font1', 40)
|
||||
pdf_canvas.setFillColorRGB(0, 1, 0, 0.5)
|
||||
pdf_canvas.rotate(18)
|
||||
pdf_canvas.drawString(250, 250, 'hello, world!')
|
||||
|
||||
# 保存
|
||||
pdf_canvas.save()
|
||||
```
|
||||
|
||||
上面的代码如果不太理解也没有关系,等真正需要用Python创建PDF文档的时候,再好好研读一下`reportlab`的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)就可以了。
|
||||
|
||||
> **提示**:上面代码中用到的图片和字体,可以在后面的百度云盘链接中获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
在学习完上面的内容之后,相信大家已经知道像合并多个PDF文件这样的工作应该如何用Python代码来处理了,赶紧自己动手试一试吧。
|
|
@ -0,0 +1,171 @@
|
|||
## 第28课:用Python处理图像
|
||||
|
||||
### 入门知识
|
||||
|
||||
1. 颜色。如果你有使用颜料画画的经历,那么一定知道混合红、黄、蓝三种颜料可以得到其他的颜色,事实上这三种颜色就是美术中的三原色,它们是不能再分解的基本颜色。在计算机中,我们可以将红、绿、蓝三种色光以不同的比例叠加来组合成其他的颜色,因此这三种颜色就是色光三原色。在计算机系统中,我们通常会将一个颜色表示为一个RGB值或RGBA值(其中的A表示Alpha通道,它决定了透过这个图像的像素,也就是透明度)。
|
||||
|
||||
| 名称 | RGB值 | 名称 | RGB值 |
|
||||
| :---------: | :-------------: | :----------: | :-----------: |
|
||||
| White(白) | (255, 255, 255) | Red(红) | (255, 0, 0) |
|
||||
| Green(绿) | (0, 255, 0) | Blue(蓝) | (0, 0, 255) |
|
||||
| Gray(灰) | (128, 128, 128) | Yellow(黄) | (255, 255, 0) |
|
||||
| Black(黑) | (0, 0, 0) | Purple(紫) | (128, 0, 128) |
|
||||
|
||||
2. 像素。对于一个由数字序列表示的图像来说,最小的单位就是图像上单一颜色的小方格,这些小方块都有一个明确的位置和被分配的色彩数值,而这些一小方格的颜色和位置决定了该图像最终呈现出来的样子,它们是不可分割的单位,我们通常称之为像素(pixel)。每一个图像都包含了一定量的像素,这些像素决定图像在屏幕上所呈现的大小,大家如果爱好拍照或者自拍,对像素这个词就不会陌生。
|
||||
|
||||
### 用Pillow处理图像
|
||||
|
||||
Pillow是由从著名的Python图像处理库PIL发展出来的一个分支,通过Pillow可以实现图像压缩和图像处理等各种操作。可以使用下面的命令来安装Pillow。
|
||||
|
||||
```Shell
|
||||
pip install pillow
|
||||
```
|
||||
|
||||
Pillow中最为重要的是`Image`类,可以通过`Image`模块的`open`函数来读取图像并获得`Image`类型的对象。
|
||||
|
||||
1. 读取和显示图像
|
||||
|
||||
```Python
|
||||
from PIL import Image
|
||||
|
||||
# 读取图像获得Image对象
|
||||
image = Image.open('guido.jpg')
|
||||
# 通过Image对象的format属性获得图像的格式
|
||||
print(image.format) # JPEG
|
||||
# 通过Image对象的size属性获得图像的尺寸
|
||||
print(image.size) # (500, 750)
|
||||
# 通过Image对象的mode属性获取图像的模式
|
||||
print(image.mode) # RGB
|
||||
# 通过Image对象的show方法显示图像
|
||||
image.show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202628.png" width="80%">
|
||||
|
||||
2. 剪裁图像
|
||||
|
||||
```Python
|
||||
# 通过Image对象的crop方法指定剪裁区域剪裁图像
|
||||
image.crop((80, 20, 310, 360)).show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202701.png" width="80%">
|
||||
|
||||
3. 生成缩略图
|
||||
|
||||
```Python
|
||||
# 通过Image对象的thumbnail方法生成指定尺寸的缩略图
|
||||
image.thumbnail((128, 128))
|
||||
image.show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202722.png" width="100%">
|
||||
|
||||
4. 缩放和黏贴图像
|
||||
|
||||
```Python
|
||||
# 读取骆昊的照片获得Image对象
|
||||
luohao_image = Image.open('luohao.png')
|
||||
# 读取吉多的照片获得Image对象
|
||||
guido_image = Image.open('guido.jpg')
|
||||
# 从吉多的照片上剪裁出吉多的头
|
||||
guido_head = guido_image.crop((80, 20, 310, 360))
|
||||
width, height = guido_head.size
|
||||
# 使用Image对象的resize方法修改图像的尺寸
|
||||
# 使用Image对象的paste方法将吉多的头粘贴到骆昊的照片上
|
||||
luohao_image.paste(guido_head.resize((int(width / 1.5), int(height / 1.5))), (172, 40))
|
||||
luohao_image.show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202749.png" width="80%">
|
||||
|
||||
5. 旋转和翻转
|
||||
|
||||
```Python
|
||||
image = Image.open('guido.jpg')
|
||||
# 使用Image对象的rotate方法实现图像的旋转
|
||||
image.rotate(45).show()
|
||||
# 使用Image对象的transpose方法实现图像翻转
|
||||
# Image.FLIP_LEFT_RIGHT - 水平翻转
|
||||
# Image.FLIP_TOP_BOTTOM - 垂直翻转
|
||||
image.transpose(Image.FLIP_TOP_BOTTOM).show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202829.png" width="80%">
|
||||
|
||||
6. 操作像素
|
||||
|
||||
```Python
|
||||
for x in range(80, 310):
|
||||
for y in range(20, 360):
|
||||
# 通过Image对象的putpixel方法修改图像指定像素点
|
||||
image.putpixel((x, y), (128, 128, 128))
|
||||
image.show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202932.png" width="80%">
|
||||
|
||||
7. 滤镜效果
|
||||
|
||||
```Python
|
||||
from PIL import ImageFilter
|
||||
|
||||
# 使用Image对象的filter方法对图像进行滤镜处理
|
||||
# ImageFilter模块包含了诸多预设的滤镜也可以自定义滤镜
|
||||
image.filter(ImageFilter.CONTOUR).show()
|
||||
```
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803202953.png" width="80%">
|
||||
|
||||
### 使用Pillow绘图
|
||||
|
||||
Pillow中有一个名为`ImageDraw`的模块,该模块的`Draw`函数会返回一个`ImageDraw`对象,通过`ImageDraw`对象的`arc`、`line`、`rectangle`、`ellipse`、`polygon`等方法,可以在图像上绘制出圆弧、线条、矩形、椭圆、多边形等形状,也可以通过该对象的`text`方法在图像上添加文字。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803203016.png" width="80%">
|
||||
|
||||
要绘制如上图所示的图像,完整的代码如下所示。
|
||||
|
||||
```Python
|
||||
import random
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
def random_color():
|
||||
"""生成随机颜色"""
|
||||
red = random.randint(0, 255)
|
||||
green = random.randint(0, 255)
|
||||
blue = random.randint(0, 255)
|
||||
return red, green, blue
|
||||
|
||||
|
||||
width, height = 800, 600
|
||||
# 创建一个800*600的图像,背景色为白色
|
||||
image = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
|
||||
# 创建一个ImageDraw对象
|
||||
drawer = ImageDraw.Draw(image)
|
||||
# 通过指定字体和大小获得ImageFont对象
|
||||
font = ImageFont.truetype('Kongxin.ttf', 32)
|
||||
# 通过ImageDraw对象的text方法绘制文字
|
||||
drawer.text((300, 50), 'Hello, world!', fill=(255, 0, 0), font=font)
|
||||
# 通过ImageDraw对象的line方法绘制两条对角直线
|
||||
drawer.line((0, 0, width, height), fill=(0, 0, 255), width=2)
|
||||
drawer.line((width, 0, 0, height), fill=(0, 0, 255), width=2)
|
||||
xy = width // 2 - 60, height // 2 - 60, width // 2 + 60, height // 2 + 60
|
||||
# 通过ImageDraw对象的rectangle方法绘制矩形
|
||||
drawer.rectangle(xy, outline=(255, 0, 0), width=2)
|
||||
# 通过ImageDraw对象的ellipse方法绘制椭圆
|
||||
for i in range(4):
|
||||
left, top, right, bottom = 150 + i * 120, 220, 310 + i * 120, 380
|
||||
drawer.ellipse((left, top, right, bottom), outline=random_color(), width=8)
|
||||
# 显示图像
|
||||
image.show()
|
||||
# 保存图像
|
||||
image.save('result.png')
|
||||
```
|
||||
|
||||
> **注意**:上面代码中使用的字体文件需要根据自己准备,可以选择自己喜欢的字体文件并放置在代码目录下。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
使用Python语言做开发,除了可以用Pillow来处理图像外,还可以使用更为强大的OpenCV库来完成图形图像的处理,OpenCV(**Open** Source **C**omputer **V**ision Library)是一个跨平台的计算机视觉库,可以用来开发实时图像处理、计算机视觉和模式识别程序。在我们的日常工作中,有很多繁琐乏味的任务其实都可以通过Python程序来处理,编程的目的就是让计算机帮助我们解决问题,减少重复乏味的劳动。通过本章节的学习,相信大家已经感受到了使用Python程序绘图P图的乐趣,其实Python能做的事情还远不止这些,继续你的学习吧。
|
|
@ -0,0 +1,201 @@
|
|||
## 第29课:用Python发送邮件和短信
|
||||
|
||||
在前面的课程中,我们已经教会大家如何用Python程序自动的生成Excel、Word、PDF文档,接下来我们还可以更进一步,就是通过邮件将生成好的文档发送给指定的收件人,然后用短信告知对方我们发出了邮件。这些事情利用Python程序也可以轻松愉快的解决。
|
||||
|
||||
### 发送电子邮件
|
||||
|
||||
在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。
|
||||
|
||||
我们可以用HTTP(超文本传输协议)来访问网站资源,HTTP是一个应用级协议,它建立在TCP(传输控制协议)之上,TCP为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件,需要使用SMTP(简单邮件传输协议),它也是建立在TCP之上的应用级协议,规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python通过名为`smtplib`的模块将这些操作简化成了`SMTP_SSL`对象,通过该对象的`login`和`send_mail`方法,就能够完成发送邮件的操作。
|
||||
|
||||
我们先尝试一下发送一封极为简单的邮件,该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器,我们可以自己架设邮件服务器,这件事情对新手并不友好,但是我们可以选择使用第三方提供的邮件服务。例如,我在<www.126.com>已经注册了账号,登录成功之后,就可以在设置中开启SMTP服务,这样就相当于获得了邮件服务器,具体的操作如下所示。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820190307.png" alt="image-20210820190306861" width="95%">
|
||||
|
||||
![image-20210820190816557](https://gitee.com/jackfrued/mypic/raw/master/20210820190816.png)
|
||||
|
||||
用手机扫码上面的二维码可以通过发送短信的方式来获取授权码,短信发送成功后,点击“我已发送”就可以获得授权码。授权码需要妥善保管,因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来,我们就可以编写发送邮件的代码了,如下所示。
|
||||
|
||||
```Python
|
||||
import smtplib
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
# 创建邮件主体对象
|
||||
email = MIMEMultipart()
|
||||
# 设置发件人、收件人和主题
|
||||
email['From'] = 'xxxxxxxxx@126.com'
|
||||
email['To'] = 'yyyyyy@qq.com;zzzzzz@1000phone.com'
|
||||
email['Subject'] = Header('上半年工作情况汇报', 'utf-8')
|
||||
# 添加邮件正文内容
|
||||
content = """据德国媒体报道,当地时间9日,德国火车司机工会成员进行了投票,
|
||||
定于当地时间10日起进行全国性罢工,货运交通方面的罢工已于当地时间10日19时开始。
|
||||
此后,从11日凌晨2时到13日凌晨2时,德国全国范围内的客运和铁路基础设施将进行48小时的罢工。"""
|
||||
|
||||
# 创建SMTP_SSL对象(连接邮件服务器)
|
||||
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
|
||||
# 通过用户名和授权码进行登录
|
||||
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
|
||||
# 发送邮件(发件人、收件人、邮件内容(字符串))
|
||||
smtp_obj.sendmail(
|
||||
'xxxxxxxxx@126.com',
|
||||
['yyyyyy@qq.com', 'zzzzzz@1000phone.com'],
|
||||
email.as_string()
|
||||
)
|
||||
```
|
||||
|
||||
如果要发送带有附件的邮件,只需要将附件的内容处理成BASE64编码,那么它就和普通的文本内容几乎没有什么区别。BASE64是一种基于64个可打印字符来表示二进制数据的表示方法,常用于某些需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。对这种编码方式不理解的同学,推荐阅读[《Base64笔记》](http://www.ruanyifeng.com/blog/2008/06/base64.html)一文。在之前的内容中,我们也提到过,Python标准库的`base64`模块提供了对BASE64编解码的支持。
|
||||
|
||||
下面的代码演示了如何发送带附件的邮件。
|
||||
|
||||
```Python
|
||||
import smtplib
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from urllib.parse import quote
|
||||
|
||||
# 创建邮件主体对象
|
||||
email = MIMEMultipart()
|
||||
# 设置发件人、收件人和主题
|
||||
email['From'] = 'xxxxxxxxx@126.com'
|
||||
email['To'] = 'zzzzzzzz@1000phone.com'
|
||||
email['Subject'] = Header('请查收离职证明文件', 'utf-8')
|
||||
# 添加邮件正文内容(带HTML标签排版的内容)
|
||||
content = """<p>亲爱的前同事:</p>
|
||||
<p>你需要的离职证明在附件中,请查收!</p>
|
||||
<br>
|
||||
<p>祝,好!</p>
|
||||
<hr>
|
||||
<p>孙美丽 即日</p>"""
|
||||
email.attach(MIMEText(content, 'html', 'utf-8'))
|
||||
# 读取作为附件的文件
|
||||
with open(f'resources/王大锤离职证明.docx', 'rb') as file:
|
||||
attachment = MIMEText(file.read(), 'base64', 'utf-8')
|
||||
# 指定内容类型
|
||||
attachment['content-type'] = 'application/octet-stream'
|
||||
# 将中文文件名处理成百分号编码
|
||||
filename = quote('王大锤离职证明.docx')
|
||||
# 指定如何处置内容
|
||||
attachment['content-disposition'] = f'attachment; filename="{filename}"'
|
||||
|
||||
# 创建SMTP_SSL对象(连接邮件服务器)
|
||||
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
|
||||
# 通过用户名和授权码进行登录
|
||||
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
|
||||
# 发送邮件(发件人、收件人、邮件内容(字符串))
|
||||
smtp_obj.sendmail(
|
||||
'xxxxxxxxx@126.com',
|
||||
'zzzzzzzz@1000phone.com',
|
||||
email.as_string()
|
||||
)
|
||||
```
|
||||
|
||||
为了方便大家用Python实现邮件发送,我将上面的代码封装成了函数,使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。
|
||||
|
||||
```Python
|
||||
import smtplib
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from urllib.parse import quote
|
||||
|
||||
# 邮件服务器域名(自行修改)
|
||||
EMAIL_HOST = 'smtp.126.com'
|
||||
# 邮件服务端口(通常是465)
|
||||
EMAIL_PORT = 465
|
||||
# 登录邮件服务器的账号(自行修改)
|
||||
EMAIL_USER = 'xxxxxxxxx@126.com'
|
||||
# 开通SMTP服务的授权码(自行修改)
|
||||
EMAIL_AUTH = '邮件服务器的授权码'
|
||||
|
||||
|
||||
def send_email(*, from_user, to_users, subject='', content='', filenames=[]):
|
||||
"""发送邮件
|
||||
|
||||
:param from_user: 发件人
|
||||
:param to_users: 收件人,多个收件人用英文分号进行分隔
|
||||
:param subject: 邮件的主题
|
||||
:param content: 邮件正文内容
|
||||
:param filenames: 附件要发送的文件路径
|
||||
"""
|
||||
email = MIMEMultipart()
|
||||
email['From'] = from_user
|
||||
email['To'] = to_users
|
||||
email['Subject'] = subject
|
||||
|
||||
message = MIMEText(content, 'plain', 'utf-8')
|
||||
email.attach(message)
|
||||
for filename in filenames:
|
||||
with open(filename, 'rb') as file:
|
||||
pos = filename.rfind('/')
|
||||
display_filename = filename[pos + 1:] if pos >= 0 else filename
|
||||
display_filename = quote(display_filename)
|
||||
attachment = MIMEText(file.read(), 'base64', 'utf-8')
|
||||
attachment['content-type'] = 'application/octet-stream'
|
||||
attachment['content-disposition'] = f'attachment; filename="{display_filename}"'
|
||||
email.attach(attachment)
|
||||
|
||||
smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
|
||||
smtp.login(EMAIL_USER, EMAIL_AUTH)
|
||||
smtp.sendmail(from_user, to_users, email.as_string())
|
||||
```
|
||||
|
||||
### 发送短信
|
||||
|
||||
发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台的支持,下面我们以[螺丝帽平台](https://luosimao.com/)为例,为大家介绍如何用Python程序发送短信。注册账号和购买短信服务的细节我们不在这里进行赘述,大家可以咨询平台的客服。
|
||||
|
||||
![image-20210820194420911](https://gitee.com/jackfrued/mypic/raw/master/20210820194421.png)
|
||||
|
||||
接下来,我们可以通过`requests`库向平台提供的短信网关发起一个HTTP请求,通过将接收短信的手机号和短信内容作为参数,就可以发送短信,代码如下所示。
|
||||
|
||||
```Python
|
||||
import random
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def send_message_by_luosimao(tel, message):
|
||||
"""发送短信(调用螺丝帽短信网关)"""
|
||||
resp = requests.post(
|
||||
url='http://sms-api.luosimao.com/v1/send.json',
|
||||
auth=('api', 'key-注册成功后平台分配的KEY'),
|
||||
data={
|
||||
'mobile': tel,
|
||||
'message': message
|
||||
},
|
||||
timeout=10,
|
||||
verify=False
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
|
||||
def gen_mobile_code(length=6):
|
||||
"""生成指定长度的手机验证码"""
|
||||
return ''.join(random.choices('0123456789', k=length))
|
||||
|
||||
|
||||
def main():
|
||||
code = gen_mobile_code()
|
||||
message = f'您的短信验证码是{code},打死也不能告诉别人哟!【Python小课】'
|
||||
print(send_message_by_luosimao('13500112233', message))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
上面请求螺丝帽的短信网关`http://sms-api.luosimao.com/v1/send.json`会返回JSON格式的数据,如果返回`{'error': 0, 'msg': 'OK'}`就说明短信已经发送成功了,如果`error`的值不是`0`,可以通过查看官方的[开发文档](https://luosimao.com/docs/api/)了解到底哪个环节出了问题。螺丝帽平台常见的错误类型如下图所示。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210820195505.png" alt="image-20210820195505761" style="zoom:50%;">
|
||||
|
||||
目前,大多数短信平台都会要求短信内容必须附上签名,下图是我在螺丝帽平台配置的短信签名“【Python小课】”。有些涉及到敏感内容的短信,还需要提前配置短信模板,有兴趣的读者可以自行研究。一般情况下,平台为了防范短信被盗用,还会要求设置“IP白名单”,不清楚如何配置的可以咨询平台客服。
|
||||
|
||||
![image-20210820194653785](https://gitee.com/jackfrued/mypic/raw/master/20210820194653.png)
|
||||
|
||||
当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
其实,发送邮件和发送短信一样,也可以通过调用三方服务来完成,在实际的商业项目中,建议自己架设邮件服务器或购买三方服务来发送邮件,这个才是比较靠谱的选择。
|
|
@ -0,0 +1,159 @@
|
|||
## 第30课:正则表达式的应用
|
||||
|
||||
### 正则表达式相关知识
|
||||
|
||||
在编写处理字符串的程时,经常会遇到在一段文本中查找符合某些规则的字符串的需求,正则表达式就是用于描述这些规则的工具,换句话说,我们可以使用正则表达式来定义字符串的匹配模式,即如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉。
|
||||
|
||||
举一个简单的例子,如果你在Windows操作系统中使用过文件查找并且在指定文件名时使用过通配符(`*`和`?`),那么正则表达式也是与之类似的用 来进行文本匹配的工具,只不过比起通配符正则表达式更强大,它能更精确地描述你的需求,当然你付出的代价是书写一个正则表达式比使用通配符要复杂得多,因为任何给你带来好处的东西都需要你付出对应的代价。
|
||||
|
||||
再举一个例子,我们从某个地方(可能是一个文本文件,也可能是网络上的一则新闻)获得了一个字符串,希望在字符串中找出手机号和座机号。当然我们可以设定手机号是11位的数字(注意并不是随机的11位数字,因为你没有见过“25012345678”这样的手机号),而座机号则是类似于“区号-号码”这样的模式,如果不使用正则表达式要完成这个任务就会比较麻烦。最初计算机是为了做数学运算而诞生的,处理的信息基本上都是数值,而今天我们在日常工作中处理的信息很多都是文本数据,我们希望计算机能够识别和处理符合某些模式的文本,正则表达式就显得非常重要了。今天几乎所有的编程语言都提供了对正则表达式操作的支持,Python通过标准库中的`re`模块来支持正则表达式操作。
|
||||
|
||||
关于正则表达式的相关知识,大家可以阅读一篇非常有名的博文叫[《正则表达式30分钟入门教程》](https://deerchao.net/tutorials/regex/regex.htm),读完这篇文章后你就可以看懂下面的表格,这是我们对正则表达式中的一些基本符号进行的扼要总结。
|
||||
|
||||
| 符号 | 解释 | 示例 | 说明 |
|
||||
| -------------- | -------------------------------- | ------------------ | ------------------------------------------------------------ |
|
||||
| `.` | 匹配任意字符 | `b.t` | 可以匹配bat / but / b#t / b1t等 |
|
||||
| `\w` | 匹配字母/数字/下划线 | `b\wt` | 可以匹配bat / b1t / b_t等<br>但不能匹配b#t |
|
||||
| `\s` | 匹配空白字符(包括\r、\n、\t等) | `love\syou` | 可以匹配love you |
|
||||
| `\d` | 匹配数字 | `\d\d` | 可以匹配01 / 23 / 99等 |
|
||||
| `\b` | 匹配单词的边界 | `\bThe\b` | |
|
||||
| `^` | 匹配字符串的开始 | `^The` | 可以匹配The开头的字符串 |
|
||||
| `$` | 匹配字符串的结束 | `.exe$` | 可以匹配.exe结尾的字符串 |
|
||||
| `\W` | 匹配非字母/数字/下划线 | `b\Wt` | 可以匹配b#t / b@t等<br>但不能匹配but / b1t / b_t等 |
|
||||
| `\S` | 匹配非空白字符 | `love\Syou` | 可以匹配love#you等<br>但不能匹配love you |
|
||||
| `\D` | 匹配非数字 | `\d\D` | 可以匹配9a / 3# / 0F等 |
|
||||
| `\B` | 匹配非单词边界 | `\Bio\B` | |
|
||||
| `[]` | 匹配来自字符集的任意单一字符 | `[aeiou]` | 可以匹配任一元音字母字符 |
|
||||
| `[^]` | 匹配不在字符集中的任意单一字符 | `[^aeiou]` | 可以匹配任一非元音字母字符 |
|
||||
| `*` | 匹配0次或多次 | `\w*` | |
|
||||
| `+` | 匹配1次或多次 | `\w+` | |
|
||||
| `?` | 匹配0次或1次 | `\w?` | |
|
||||
| `{N}` | 匹配N次 | `\w{3}` | |
|
||||
| `{M,}` | 匹配至少M次 | `\w{3,}` | |
|
||||
| `{M,N}` | 匹配至少M次至多N次 | `\w{3,6}` | |
|
||||
| `|` | 分支 | `foo|bar` | 可以匹配foo或者bar |
|
||||
| `(?#)` | 注释 | | |
|
||||
| `(exp)` | 匹配exp并捕获到自动命名的组中 | | |
|
||||
| `(?<name>exp)` | 匹配exp并捕获到名为name的组中 | | |
|
||||
| `(?:exp)` | 匹配exp但是不捕获匹配的文本 | | |
|
||||
| `(?=exp)` | 匹配exp前面的位置 | `\b\w+(?=ing)` | 可以匹配I'm dancing中的danc |
|
||||
| `(?<=exp)` | 匹配exp后面的位置 | `(?<=\bdanc)\w+\b` | 可以匹配I love dancing and reading中的第一个ing |
|
||||
| `(?!exp)` | 匹配后面不是exp的位置 | | |
|
||||
| `(?<!exp)` | 匹配前面不是exp的位置 | | |
|
||||
| `*?` | 重复任意次,但尽可能少重复 | `a.*b`<br>`a.*?b` | 将正则表达式应用于aabab,前者会匹配整个字符串aabab,后者会匹配aab和ab两个字符串 |
|
||||
| `+?` | 重复1次或多次,但尽可能少重复 | | |
|
||||
| `??` | 重复0次或1次,但尽可能少重复 | | |
|
||||
| `{M,N}?` | 重复M到N次,但尽可能少重复 | | |
|
||||
| `{M,}?` | 重复M次以上,但尽可能少重复 | | |
|
||||
|
||||
> **说明:** 如果需要匹配的字符是正则表达式中的特殊字符,那么可以使用`\`进行转义处理,例如想匹配小数点可以写成`\.`就可以了,因为直接写`.`会匹配任意字符;同理,想匹配圆括号必须写成`\(`和`\)`,否则圆括号被视为正则表达式中的分组。
|
||||
|
||||
### Python对正则表达式的支持
|
||||
|
||||
Python提供了`re`模块来支持正则表达式相关操作,下面是`re`模块中的核心函数。
|
||||
|
||||
| 函数 | 说明 |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `compile(pattern, flags=0)` | 编译正则表达式返回正则表达式对象 |
|
||||
| `match(pattern, string, flags=0)` | 用正则表达式匹配字符串 成功返回匹配对象 否则返回`None` |
|
||||
| `search(pattern, string, flags=0)` | 搜索字符串中第一次出现正则表达式的模式 成功返回匹配对象 否则返回`None` |
|
||||
| `split(pattern, string, maxsplit=0, flags=0)` | 用正则表达式指定的模式分隔符拆分字符串 返回列表 |
|
||||
| `sub(pattern, repl, string, count=0, flags=0)` | 用指定的字符串替换原字符串中与正则表达式匹配的模式 可以用`count`指定替换的次数 |
|
||||
| `fullmatch(pattern, string, flags=0)` | `match`函数的完全匹配(从字符串开头到结尾)版本 |
|
||||
| `findall(pattern, string, flags=0)` | 查找字符串所有与正则表达式匹配的模式 返回字符串的列表 |
|
||||
| `finditer(pattern, string, flags=0)` | 查找字符串所有与正则表达式匹配的模式 返回一个迭代器 |
|
||||
| `purge()` | 清除隐式编译的正则表达式的缓存 |
|
||||
| `re.I` / `re.IGNORECASE` | 忽略大小写匹配标记 |
|
||||
| `re.M` / `re.MULTILINE` | 多行匹配标记 |
|
||||
|
||||
> **说明:** 上面提到的`re`模块中的这些函数,实际开发中也可以用正则表达式对象(`Pattern`对象)的方法替代对这些函数的使用,如果一个正则表达式需要重复的使用,那么先通过`compile`函数编译正则表达式并创建出正则表达式对象无疑是更为明智的选择。
|
||||
|
||||
下面我们通过一系列的例子来告诉大家在Python中如何使用正则表达式。
|
||||
|
||||
#### 例子1:验证输入用户名和QQ号是否有效并给出对应的提示信息。
|
||||
|
||||
```Python
|
||||
"""
|
||||
要求:用户名必须由字母、数字或下划线构成且长度在6~20个字符之间,QQ号是5~12的数字且首位不能为0
|
||||
"""
|
||||
import re
|
||||
|
||||
username = input('请输入用户名: ')
|
||||
qq = input('请输入QQ号: ')
|
||||
# match函数的第一个参数是正则表达式字符串或正则表达式对象
|
||||
# match函数的第二个参数是要跟正则表达式做匹配的字符串对象
|
||||
m1 = re.match(r'^[0-9a-zA-Z_]{6,20}$', username)
|
||||
if not m1:
|
||||
print('请输入有效的用户名.')
|
||||
# fullmatch函数要求字符串和正则表达式完全匹配
|
||||
# 所以正则表达式没有写起始符和结束符
|
||||
m2 = re.fullmatch(r'[1-9]\d{4,11}', qq)
|
||||
if not m2:
|
||||
print('请输入有效的QQ号.')
|
||||
if m1 and m2:
|
||||
print('你输入的信息是有效的!')
|
||||
```
|
||||
|
||||
> **提示:** 上面在书写正则表达式时使用了“原始字符串”的写法(在字符串前面加上了`r`),所谓“原始字符串”就是字符串中的每个字符都是它原始的意义,说得更直接一点就是字符串中没有所谓的转义字符啦。因为正则表达式中有很多元字符和需要进行转义的地方,如果不使用原始字符串就需要将反斜杠写作`\\`,例如表示数字的`\d`得书写成`\\d`,这样不仅写起来不方便,阅读的时候也会很吃力。
|
||||
|
||||
#### 例子2:从一段文字中提取出国内手机号码。
|
||||
|
||||
下面这张图是截止到2017年底,国内三家运营商推出的手机号段。
|
||||
|
||||
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210803203134.png">
|
||||
|
||||
```Python
|
||||
import re
|
||||
|
||||
# 创建正则表达式对象,使用了前瞻和回顾来保证手机号前后不应该再出现数字
|
||||
pattern = re.compile(r'(?<=\D)1[34578]\d{9}(?=\D)')
|
||||
sentence = '''重要的事情说8130123456789遍,我的手机号是13512346789这个靓号,
|
||||
不是15600998765,也是110或119,王大锤的手机号才是15600998765。'''
|
||||
# 方法一:查找所有匹配并保存到一个列表中
|
||||
tels_list = re.findall(pattern, sentence)
|
||||
for tel in tels_list:
|
||||
print(tel)
|
||||
print('--------华丽的分隔线--------')
|
||||
|
||||
# 方法二:通过迭代器取出匹配对象并获得匹配的内容
|
||||
for temp in pattern.finditer(sentence):
|
||||
print(temp.group())
|
||||
print('--------华丽的分隔线--------')
|
||||
|
||||
# 方法三:通过search函数指定搜索位置找出所有匹配
|
||||
m = pattern.search(sentence)
|
||||
while m:
|
||||
print(m.group())
|
||||
m = pattern.search(sentence, m.end())
|
||||
```
|
||||
|
||||
> **说明:** 上面匹配国内手机号的正则表达式并不够好,因为像14开头的号码只有145或147,而上面的正则表达式并没有考虑这种情况,要匹配国内手机号,更好的正则表达式的写法是:`(?<=\D)(1[38]\d{9}|14[57]\d{8}|15[0-35-9]\d{8}|17[678]\d{8})(?=\D)`,国内好像已经有19和16开头的手机号了,但是这个暂时不在我们考虑之列。
|
||||
|
||||
#### 例子3:替换字符串中的不良内容
|
||||
|
||||
```Python
|
||||
import re
|
||||
|
||||
sentence = 'Oh, shit! 你是傻逼吗? Fuck you.'
|
||||
purified = re.sub('fuck|shit|[傻煞沙][比笔逼叉缺吊碉雕]',
|
||||
'*', sentence, flags=re.IGNORECASE)
|
||||
print(purified) # Oh, *! 你是*吗? * you.
|
||||
```
|
||||
|
||||
> **说明:**` re`模块的正则表达式相关函数中都有一个`flags`参数,它代表了正则表达式的匹配标记,可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为flags参数指定多个值,可以使用[按位或运算符](http://www.runoob.com/python/python-operators.html#ysf5)进行叠加,如`flags=re.I | re.M`。
|
||||
|
||||
#### 例子4:拆分长字符串
|
||||
|
||||
```Python
|
||||
import re
|
||||
|
||||
poem = '窗前明月光,疑是地上霜。举头望明月,低头思故乡。'
|
||||
sentences_list = re.split(r'[,。]', poem)
|
||||
sentences_list = [sentence for sentence in sentences_list if sentence]
|
||||
for sentence in sentences_list:
|
||||
print(sentence)
|
||||
```
|
||||
|
||||
### 简单的总结
|
||||
|
||||
正则表达式在字符串的处理和匹配上真的非常强大,通过上面的例子相信大家已经感受到了正则表达式的魅力,当然写一个正则表达式对新手来说并不是那么容易,但是很多事情都是熟能生巧,大胆的去尝试就行了,有一个在线的[正则表达式测试工具](https://c.runoob.com/front-end/854)相信能够在一定程度上帮到大家。
|
|
@ -1,89 +0,0 @@
|
|||
## 第030课:用Python获取网络数据
|
||||
|
||||
对于Python语言来说,一个较为擅长的领域就是网络数据采集,实现网络数据采集的程序通常称之为网络爬虫或蜘蛛程序。即便是在大数据时代,数据对于中小企业来说仍然是硬伤和短板,有些数据需要通过开放或付费的数据接口来获得,其他的行业数据则必须要通过网络数据采集的方式来获得。不管使用哪种方式获取网络数据资源,Python语言都是非常好的选择,因为Python的标准库和三方库都对获取网络数据提供了良好的支持。
|
||||
|
||||
### HTTP和requests库
|
||||
|
||||
要使用Python获取网络数据,我们可以先安装一个名为`requests` 的三方库,这个库我们在第24课中已经使用过了。按照官方网站的解释,`requests`是基于Python标准库进行了封装,简化了通过[HTTP](https://www.ruanyifeng.com/blog/2016/08/http.html)访问网络资源的操作。说到HTTP相信大家不会陌生,通常我们打开浏览器浏览网页时,我们就是使用了HTTP或[HTTPS](https://zhuanlan.zhihu.com/p/100591696)。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响应,服务器给浏览器的数据就包含在这个响应中。我们可以使用浏览器提供的“开发者工具”或是“抓包工具”(如:Fiddler、Charles等)来了解HTTP请求和响应到底是什么样子的,如下图所示。
|
||||
|
||||
![](res/http-request-response.png)
|
||||
|
||||
通过`requests`库,我们可以让程序向浏览器一样向Web服务器发起请求,并接收到服务器返回的响应,从响应中我们就可以提取出我们想要的数据。下面通过两个例子来演示如何获取网页代码和网络资源(如:图片),浏览器呈现给我们的网页是用[HTML](https://developer.mozilla.org/zh-CN/docs/Web/HTML)编写的,浏览器相当于是HTML的解释器环境,我们看到的网页中的内容都包含在HTML的标签中。在获取到HTML代码后,就可以从标签的属性或标签体中提取我们需要的内容。
|
||||
|
||||
获取搜狐网首页。
|
||||
|
||||
```Python
|
||||
import requests
|
||||
|
||||
# requests的get函数会返回一个Response对象
|
||||
resp = requests.get('https://www.sohu.com/')
|
||||
if resp.status_code == 200:
|
||||
# 通过Response对象的text属性获取服务器返回的文本内容
|
||||
print(resp.text)
|
||||
```
|
||||
|
||||
获取百度Logo并保存到名为`baidu.png`的本地文件中。首先在百度的首页上,右键点击百度Logo,并通过“复制图片地址”菜单获取图片的URL。
|
||||
|
||||
```Python
|
||||
import requests
|
||||
|
||||
resp = requests.get('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png')
|
||||
with open('baidu.png', 'wb') as file:
|
||||
# 通过Response对象的content属性获取服务器返回的二进制内容
|
||||
file.write(resp.content)
|
||||
```
|
||||
|
||||
> **说明**:关于`requests`库的详细使用方法,大家可以参考[官方文档](https://requests.readthedocs.io/zh_CN/latest/)的内容。
|
||||
|
||||
### 访问网络数据接口
|
||||
|
||||
国内外的很多网站都提供了开放数据接口,在开发商业项目时,如果有些事情我们自己无法解决,就可以借助这些开放的数据接口来处理。例如要根据用户或企业上传的资料进行实名认证或企业认证,我们就可以调用第三方提供的开放接口来识别用户或企业信息的真伪;例如要获取某个城市的天气信息,我们不可能直接从气象卫星拿到数据然后自己进行运算,只能通过第三方提供的数据接口来得到对应的天气信息。通常,提供有商业价值的数据的接口都是需要支付费用后才能访问的,在访问接口时还需要提供身份标识,便于服务器判断用户是不是付费用户以及进行费用扣除等相关操作。当然,还有些接口是可以免费使用的,但是必须先提供个人或者公司的信息才能访问,例如:[深圳市政府数据开放平台](https://opendata.sz.gov.cn/data/api/toApi)、[蜻蜓FM开放平台](https://open.qingting.fm/)等。如果查找自己需要的数据接口,可以访问[聚合数据](https://www.juhe.cn/)这类型的网站。
|
||||
|
||||
目前,我们访问的网络数据接口大多会返回JSON格式的数据,我们在第24课讲解序列化和反序列的时候,提到过JSON格式的字符串跟Python中的字典如何进行转换,并以[天行数据](https://www.tianapi.com/)为例讲解过网络数据接口访问的相关知识,这里我们就不再进行赘述了。
|
||||
|
||||
### 开发爬虫/蜘蛛程序
|
||||
|
||||
有的时候,我们需要的数据并不能通过开放数据接口来获得,但是可能在某些网页上能够获取到,这个时候就需要我们开发爬虫程序通过爬取页面来获得需要的内容。我们可以按照上面提供的方法,使用`requests`先获取到网页的HTML代码,我们可以将整个代码看成一个长字符串,这样我们就可以使用正则表达式的捕获组从字符串提取我们需要的内容。下面我们通过代码演示如何从[豆瓣电影](https://movie.douban.com/)获取排前250名的电影的名称。豆瓣电影Top250页面的结构和对应的代码如下图所示。
|
||||
|
||||
![](res/douban-movie-top250.png)
|
||||
|
||||
```Python
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
for page in range(1, 11):
|
||||
resp = requests.get(
|
||||
# 请求https://movie.douban.com/top250时,start参数代表了从哪一部电影开始
|
||||
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
|
||||
# 如果不设置HTTP请求头中的User-Agent,豆瓣会检测出爬虫程序而阻止我们的请求
|
||||
# User-Agent可以设置为浏览器的标识(可以在浏览器的开发者工具查看HTTP请求头找到)
|
||||
# 由于豆瓣网允许百度爬虫获取它的数据,因此直接将我们的爬虫伪装成百度的爬虫
|
||||
headers={'User-Agent': 'BaiduSpider'}
|
||||
)
|
||||
# 创建正则表达式对象,通过捕获组捕获span标签中的电影标题
|
||||
pattern = re.compile(r'\<span class="title"\>([^&]*?)\<\/span\>')
|
||||
# 通过正则表达式获取class属性为title且标签内容不以&符号开头的span标签
|
||||
results = pattern.findall(resp.text)
|
||||
# 循环变量列表中所有的电影标题
|
||||
for result in results:
|
||||
print(result)
|
||||
# 随机休眠1-3秒,避免获取页面过于频繁
|
||||
time.sleep(random.randint(1, 3))
|
||||
```
|
||||
|
||||
编写爬虫程序比较重要的一点就是让爬虫程序隐匿自己的身份,因为一般的网站都比较反感爬虫。隐匿身份除了像上面的代码中修改`User-Agent`之外,还可以使用**商业IP代理**(如:[蘑菇代理](http://www.moguproxy.com/)、[芝麻代理](http://www.zhimaruanjian.com/)等),让被爬取的网站无法得知爬虫程序的真实IP地址,也就无法通过IP地址对爬虫程序进行封禁。当然,爬虫本身也是一个处于灰色地带的东西,没有谁说它是违法的,但也没有谁说它是合法的,本着**法不禁止即为许可**的精神,我们可以根据自己工作的需要去编写爬虫程序,但是如果被爬取的网站能够**举证你的爬虫程序有破坏动产的行为**,那么在打官司的时候,基本上是要败诉的,这一点需要注意。
|
||||
|
||||
另外,用编写正则表达式的方式从网页中提取内容虽然可行,但是写出一个能够满足需求的正则表达式本身也不是件容易的事情,这一点对于新手来说尤为明显。在下一节课中,我们将会为大家介绍另外两种从页面中提取数据的方法,虽然从性能上来讲,它们可能不如正则表达式,但是却降低了编码的复杂性,相信大家会喜欢上它们的。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
Python语言能做的事情真的很多,就获取网络数据这一项而言,Python几乎是一枝独秀的,大量的企业和个人都会使用Python从网络上获取自己需要的数据,相信这一点在现在或者将来也会是你工作中的一部分。
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,161 +0,0 @@
|
|||
## 第031课:用Python解析HTML页面
|
||||
|
||||
在上一课中我们讲到了使用Python获取网络资源,如果我们获取到一个或多个页面,需要从页面中提取出指定的信息,首先得掌握解析HTML页面的技术。上一课中我们把整个HTML页面当成一个字符串,使用正则表达式的捕获组提取出了需要的内容。但是,写出一个正确的正则表达式经常也是一件让人头疼的事情。为此,我们可以先了解HTML页面的结构,在此基础上就可以掌握其他的解析HTML页面的方法。
|
||||
|
||||
### HTML页面的结构
|
||||
|
||||
我们在浏览器中打开任意一个网站,然后通过鼠标右键菜单,选择“显示网页源代码”菜单项,就可以看到网页对应的HTML代码。
|
||||
|
||||
![](res/html_page_code.png)
|
||||
|
||||
代码的第1行是文档类型声明,第2行的`<html>`标签是整个页面根标签的开始标签,最后一行是根标签的结束标签`</html>`。`<html>`标签下面有两个子标签`<head>`和`<body>`,放在`<body>`标签下的内容会显示在浏览器窗口中,这部分内容是网页的主体;放在`<head>`标签下的内容不会显示在浏览器窗口中,但是却包含了页面重要的元信息,通常称之为网页的头部。HTML页面大致的代码结构如下所示。
|
||||
|
||||
```HTML
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- 页面的元信息,如字符编码、标题、关键字、媒体查询等 -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- 页面的主体,显示在浏览器窗口中的内容 -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
标签、层叠样式表(CSS)、JavaScript是构成HTML页面的三要素,其中标签用来承载页面要显示的内容,CSS负责对页面的渲染,而JavaScript用来控制页面的交互式行为。对HTML页面的解析可以使用一种名为XPath的语法,根据HTML标签的层次结构提取标签中的内容或标签属性;除此之外,也可以使用CSS选择器来定位页面元素,如果不清楚什么是CSS选择器,可以移步到我的[《Web前端概述》](https://github.com/jackfrued/Python-100-Days/blob/master/Day21-30/21-30.Web%E5%89%8D%E7%AB%AF%E6%A6%82%E8%BF%B0.md)一文进行了解。
|
||||
|
||||
### XPath解析
|
||||
|
||||
XPath是在XML(eXtensible Markup Language)文档中查找信息的一种语法,XML跟HTML类似也是一种用标签承载数据的标签语言,不同之处在于XML的标签是可扩展的,可以自定义的,而且XML对语法有更严格的要求。XPath使用路径表达式来选取XML文档中的节点或者节点集,这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。下面我们通过一个例子来说明如何使用XPath对页面进行解析。
|
||||
|
||||
```XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bookstore>
|
||||
<book>
|
||||
<title lang="eng">Harry Potter</title>
|
||||
<price>29.99</price>
|
||||
</book>
|
||||
<book>
|
||||
<title lang="eng">Learning XML</title>
|
||||
<price>39.95</price>
|
||||
</book>
|
||||
</bookstore>
|
||||
```
|
||||
|
||||
对于上面的XML文件,我们可以用如下所示的XPath语法获取文档中的节点。
|
||||
|
||||
| 路径表达式 | 结果 |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| /bookstore | 选取根元素 bookstore。**注意**:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
|
||||
| //book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
|
||||
| //@lang | 选取名为 lang 的所有属性。 |
|
||||
| /bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
|
||||
| /bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
|
||||
| /bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
|
||||
| /bookstore/book[position()<3] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
|
||||
| //title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
|
||||
| //title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
|
||||
| /bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
|
||||
| /bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
|
||||
|
||||
XPath还支持通配符用法,如下所示。
|
||||
|
||||
| 路径表达式 | 结果 |
|
||||
| ------------ | --------------------------------- |
|
||||
| /bookstore/* | 选取 bookstore 元素的所有子元素。 |
|
||||
| //* | 选取文档中的所有元素。 |
|
||||
| //title[@*] | 选取所有带有属性的 title 元素。 |
|
||||
|
||||
如果要选取多个节点,可以使用如下所示的方法。
|
||||
|
||||
| 路径表达式 | 结果 |
|
||||
| -------------------------------- | ------------------------------------------------------------ |
|
||||
| //book/title \| //book/price | 选取 book 元素的所有 title 和 price 元素。 |
|
||||
| //title \| //price | 选取文档中的所有 title 和 price 元素。 |
|
||||
| /bookstore/book/title \| //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
|
||||
|
||||
> **说明**:上面的例子来自于“菜鸟教程”网站上的[XPath教程](<https://www.runoob.com/xpath/xpath-tutorial.html>),有兴趣的读者可以自行阅读原文。
|
||||
|
||||
当然,如果不理解或不熟悉XPath语法,可以在浏览器的开发者工具中按照如下所示的方法查看元素的XPath语法,下图是在Chrome浏览器的开发者工具中查看豆瓣网电影详情信息中影片标题的XPath语法。
|
||||
|
||||
![](res/douban-xpath.png)
|
||||
|
||||
实现XPath解析需要三方库`lxml` 的支持,可以使用下面的命令安装`lxml`。
|
||||
|
||||
```Bash
|
||||
pip install lxml
|
||||
```
|
||||
|
||||
下面我们用XPath解析方式改写之前获取豆瓣电影Top250的代码,如下所示。
|
||||
|
||||
```Python
|
||||
import random
|
||||
import time
|
||||
|
||||
from lxml import etree
|
||||
import requests
|
||||
|
||||
for page in range(1, 11):
|
||||
resp = requests.get(
|
||||
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
|
||||
headers={
|
||||
'User-Agent': 'BaiduSpider',
|
||||
}
|
||||
)
|
||||
tree = etree.HTML(resp.text)
|
||||
# 通过XPath语法从页面中提取需要的数据
|
||||
spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]')
|
||||
for span in spans:
|
||||
print(span.text)
|
||||
time.sleep(random.randint(1, 3))
|
||||
```
|
||||
|
||||
### CSS选择器解析
|
||||
|
||||
对于熟悉CSS选择器和JavaScript的开发者来说,通过CSS选择器获取页面元素可能是更为简单的选择,因为浏览器中运行的JavaScript本身就可以`document`对象的`querySelector()`和`querySelectorAll()`方法基于CSS选择器获取页面元素。在Python中,我们可以利用三方库`bs4`(BeautifulSoup)或`pyquery`来做同样的事情。BeautifulSoup可以用来解析HTML和XML文档,修复含有未闭合标签等错误的文档,通过为待解析的页面在内存中创建一棵树结构,实现对从页面中提取数据操作的封装。可以用下面的命令来安装BeautifulSoup。
|
||||
|
||||
```Python
|
||||
pip install beautifulsoup4
|
||||
```
|
||||
|
||||
下面是使用`bs4`改写的获取豆瓣电影Top250电影名称的代码。
|
||||
|
||||
```Python
|
||||
import random
|
||||
import time
|
||||
|
||||
import bs4
|
||||
import requests
|
||||
|
||||
for page in range(1, 11):
|
||||
resp = requests.get(
|
||||
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
|
||||
headers={
|
||||
'User-Agent': 'BaiduSpider',
|
||||
}
|
||||
)
|
||||
soup = bs4.BeautifulSoup(resp.text, 'lxml')
|
||||
# 通过CSS选择器从页面中提取需要的数据
|
||||
spans = soup.select('div.info > div.hd > a > span:nth-child(1)')
|
||||
for span in spans:
|
||||
print(span.text)
|
||||
time.sleep(random.randint(1, 3))
|
||||
```
|
||||
|
||||
关于BeautifulSoup更多的知识,可以参考它的[官方网站](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)。
|
||||
|
||||
### 简单的总结
|
||||
|
||||
下面我们对三种解析方式做一个简单比较。
|
||||
|
||||
| 解析方式 | 对应的模块 | 速度 | 使用难度 |
|
||||
| -------------- | ---------------- | ------ | -------- |
|
||||
| 正则表达式解析 | `re` | 快 | 困难 |
|
||||
| XPath解析 | `lxml` | 快 | 一般 |
|
||||
| CSS选择器解析 | `bs4`或`pyquery` | 不确定 | 简单 |
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,82 +0,0 @@
|
|||
## 第032课:Python中的并发编程(1)
|
||||
|
||||
现如今,我们使用的计算机早已是多CPU或多核的计算机,为此我们使用的操作系统基本都是支持“多任务”的操作系统,这样的操作系统使得我们我们可以同时运行多个程序,也可以将一个程序分解为若干个相对独立的子任务,让多个子任务“齐头并进”的执行,从而缩短程序的执行时间,同时也让用户获得更好的体验。因此,当下不管用什么编程语言进行开发,实现让一个程序同时执行多个任务已经成为程序员的标配技能。为此,我们需要先了解两个重要的概念:多进程和多线程。
|
||||
|
||||
### 线程和进程
|
||||
|
||||
我们通过操作系统运行一个程序会创建出一个或多个进程,进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。简单的说,进程是操作系统分配存储空间的基本单位,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据;操作系统管理所有进程的执行,为它们合理的分配资源。一个进程可以通过fork或spawn的方式创建新的进程来执行其他的任务,不过新的进程也有自己独立的内存空间,因此两个进程如果要共享数据,必须通过进程间通信机制(IPC)来实现,具体的方式包括管道、信号、套接字等。
|
||||
|
||||
一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当然在单核CPU系统中,多个线程不可能同时执行,因为在某个时刻只有一个线程能够获得CPU,多个线程通过共享了CPU执行时间的方式来达到并发的效果。在程序中使用多线程技术通常都会带来不言而喻的好处,最主要的体现在提升程序的性能和改善用户体验,今天我们使用的软件几乎都用到了多线程技术,这一点可以利用系统自带的进程监控工具(如macOS中的“活动监视器”、Windows中的“任务管理器”)来证实,如下图所示。
|
||||
|
||||
![](res/macos-monitor.png)
|
||||
|
||||
这里,我们还需要跟大家说说另外两个概念:**并发**(concurrency)和**并行**(parallel)。并发是指同一时刻只能有一条指令执行,但是多个线程对应的指令被快速轮换地执行。比如一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。由于处理器执行指令的速度和切换的速度非常非常快,人完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行,但微观上其实只有一个线程在执行。并行是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器,不论是从宏观上还是微观上,多个线程都是在同一时刻一起执行的。在我们的课程中,其实并不用严格区分并发和并行两个词,所以我们把Python中的多线程、多进程以及异步I/O都视为实现并发编程的手段,但实际上前面两者也可以实现并行编程,当然这里还有一个全局解释器锁(GIL)的问题,我们稍后讨论。
|
||||
|
||||
### 多线程编程
|
||||
|
||||
Python标准库中`threading`模块的`Thread`类可以帮助我们非常轻松的实现多线程编程。我们用一个联网下载文件的例子来对比使用多线程和不使用多线程到底有什么区别,代码如下所示。
|
||||
|
||||
不使用多线程的下载。
|
||||
|
||||
```Python
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
def download(*, filename):
|
||||
start = time.time()
|
||||
print(f'开始下载{filename}.')
|
||||
time.sleep(random.randint(3, 6))
|
||||
print(f'{filename}下载完成.')
|
||||
end = time.time()
|
||||
print(f'下载耗时: {end - start:.3f}秒.')
|
||||
|
||||
|
||||
start = time.time()
|
||||
download(filename='Python从入门到住院.pdf')
|
||||
download(filename='MySQL从删库到跑路.avi')
|
||||
download(filename='Linux从精通到放弃.mp3')
|
||||
end = time.time()
|
||||
print(f'总耗时: {end - start:.3f}秒.')
|
||||
```
|
||||
|
||||
> **说明**:上面的代码并没有真正实现联网下载的功能,而是通过`time.sleep()`休眠指定的时间模拟下载文件需要花费一段时间。
|
||||
|
||||
运行上面的代码,可以得到如下所示的运行结果。可以看出,当我们的程序只有一个工作线程时,每个下载任务都需要等待上一个下载任务执行结束才能开始,所以程序执行的总耗时是三个下载任务各自执行时间的总和。
|
||||
|
||||
```
|
||||
开始下载Python从入门到住院.pdf.
|
||||
Python从入门到住院.pdf下载完成.
|
||||
下载耗时: 3.005秒.
|
||||
开始下载MySQL从删库到跑路.avi.
|
||||
MySQL从删库到跑路.avi下载完成.
|
||||
下载耗时: 5.006秒.
|
||||
开始下载Linux从精通到放弃.mp3.
|
||||
Linux从精通到放弃.mp3下载完成.
|
||||
下载耗时: 6.007秒.
|
||||
总耗时: 14.018秒.
|
||||
```
|
||||
|
||||
事实上,上面的三个下载任务之间并没有逻辑上的因果关系,三者是可以并发的,没有必要等待上一个下载任务结束,为此,我们可以使用多线程编程来改写上面的代码。
|
||||
|
||||
```Python
|
||||
|
||||
```
|
||||
|
||||
再次运行程序,我们可以发现,整个程序的执行时间几乎等于耗时最长的一个下载任务的执行时间,这也就意味着,三个下载任务是并发执行的,没有一个等一个,这样做很显然提高了程序的执行效率。
|
||||
|
||||
通过上面的代码可以看出,直接使用`Thread`类的构造器就可以创建线程对象,而线程对象的`start()`方法可以启动一个线程。线程启动后会执行`target`参数指定的函数,当然前提是获得CPU的调度;如果`target`指定的线程要执行的目标函数有参数,需要通过`args`参数为其进行指定。
|
||||
|
||||
除了上面的代码展示的创建线程的方式外,还可以通过继承`Thread`类并重写`run()`方法的方式来自定义线程,具体的代码如下所示。
|
||||
|
||||
```Python
|
||||
|
||||
```
|
||||
|
||||
通过上面的例子可以看出,如果程序中有非常耗时的执行单元,而这些耗时的执行单元之间又没有逻辑上的因果关系,即B单元的执行不依赖于A单元的执行结果,那么A和B两个单元就可以放到两个不同的线程中,让他们并发的执行。这样做的好处除了减少程序执行的等待时间,还可以带来更好的用户体验,因为一个单元的阻塞不会造成程序的“假死”,因为程序中还有其他的单元是可以运转的。
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,17 +0,0 @@
|
|||
## 第033课:Python中的并发编程(2)
|
||||
|
||||
|
||||
|
||||
#### 守护线程
|
||||
|
||||
|
||||
|
||||
#### 资源竞争
|
||||
|
||||
|
||||
|
||||
#### 线程池
|
||||
|
||||
|
||||
|
||||
#### GIL问题
|
|
@ -1,90 +0,0 @@
|
|||
## 第034课:Python中的并发编程(中)
|
||||
|
||||
###创建进程
|
||||
|
||||
|
||||
|
||||
### 多进程和多线程的比较
|
||||
|
||||
由于GIL问题,CPython中的多线程是不能很好发挥多核优势的,如果想要发挥多核优势,可以考虑使用多进程。对于多进程的程序,每个进程都有属于自己的GIL,所以多进程不会受GIL的影响,能够很好的发挥多核CPU的优势。对于爬虫这类I/O密集型任务来说,多线程和多进程影响差别并不大。对于计算密集型任务来说,多进程相比多线程,在效率上会有成倍的提升。
|
||||
|
||||
|
||||
|
||||
我们通过下面的代码来为大家证实多进程的优势。
|
||||
|
||||
```Python
|
||||
"""
|
||||
time python3 example22.py
|
||||
real 0m11.512s
|
||||
user 0m39.319s
|
||||
sys 0m0.169s
|
||||
使用多进程后实际执行时间为11.512秒,而用户时间39.319秒约为实际执行时间的4倍
|
||||
这就证明我们的程序通过多进程使用了CPU的多核特性,而且这台计算机配置了4核的CPU
|
||||
"""
|
||||
import concurrent.futures
|
||||
import math
|
||||
|
||||
PRIMES = [
|
||||
1116281,
|
||||
1297337,
|
||||
104395303,
|
||||
472882027,
|
||||
533000389,
|
||||
817504243,
|
||||
982451653,
|
||||
112272535095293,
|
||||
112582705942171,
|
||||
112272535095293,
|
||||
115280095190773,
|
||||
115797848077099,
|
||||
1099726899285419
|
||||
] * 5
|
||||
|
||||
|
||||
def is_prime(n):
|
||||
"""判断素数"""
|
||||
if n % 2 == 0:
|
||||
return False
|
||||
|
||||
sqrt_n = int(math.floor(math.sqrt(n)))
|
||||
for i in range(3, sqrt_n + 1, 2):
|
||||
if n % i == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
|
||||
print('%d is prime: %s' % (number, prime))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 进程间通信
|
||||
|
||||
|
||||
|
||||
### 简单的总结
|
||||
|
||||
对于Python开发者来说,以下情况需要考虑使用多线程:
|
||||
|
||||
1. 程序需要维护许多共享的状态(尤其是可变状态),Python中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
|
||||
2. 程序会花费大量时间在I/O操作上,没有太多并行计算的需求且不需占用太多的内存。
|
||||
|
||||
那么在遇到下列情况时,应该考虑使用多进程:
|
||||
|
||||
1. 程序执行计算密集型任务(如:字节码操作、数据处理、科学计算)。
|
||||
2. 程序的输入可以并行的分成块,并且可以将运算结果合并。
|
||||
3. 程序在内存使用方面没有任何限制且不强依赖于I/O操作(如:读写文件、套接字等)。
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,15 +0,0 @@
|
|||
## 第035课:Python中的并发编程(4)
|
||||
|
||||
### 异步编程
|
||||
|
||||
|
||||
|
||||
### 简单的总结
|
||||
|
||||
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,15 +0,0 @@
|
|||
## 第036课:Python中的并发编程(5)
|
||||
|
||||
### 异步编程
|
||||
|
||||
|
||||
|
||||
### 简单的总结
|
||||
|
||||
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|
|
@ -1,37 +0,0 @@
|
|||
## 第035课:用Python程序操作MySQL
|
||||
|
||||
如果要在程序中实现数据持久化,一个特别常见的方案就是接入关系型数据库。我们之前提到过持久化这个概念,简单的说就是将内存中的数据转移到硬盘上以便长久的保存数据。接下来我们以MySQL数据库为例,来讲解在Python程序中如何使用MySQL实现数据持久化,我们这里不会介绍MySQL和关系型数据库的知识,如果没有对应的背景知识,大家可以跳过这一课,或者移步到我的Python-100-Days项目中[《关系型数据库MySQL》](https://github.com/jackfrued/Python-100-Days/blob/master/Day36-40/36-38.%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93MySQL.md)一文来了解关于关系型数据库和MySQL的知识。如果想了解在Windows或macOS环境下如何安装MySQL数据库,可以点击[传送门1](https://cloud.tencent.com/developer/article/1636375)或[传送门2](https://juejin.im/post/6844903831298375693)查看对应的文章。
|
||||
|
||||
### 准备工作
|
||||
|
||||
1. 创建名为`hrs`的数据库。
|
||||
|
||||
```SQL
|
||||
|
||||
```
|
||||
|
||||
2. 创建部门表(`tb_dept`)和员工表(`tb_emp`)。
|
||||
|
||||
```SQL
|
||||
|
||||
```
|
||||
|
||||
3. 插入测试数据。
|
||||
|
||||
```SQL
|
||||
|
||||
```
|
||||
|
||||
### Python程序接入
|
||||
|
||||
|
||||
|
||||
### 简单的总结
|
||||
|
||||
|
||||
|
||||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||||
>
|
||||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||||
>
|
||||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。
|