1. 异常类型

1.1 类型分类

总的来说,编写程序时遇到的错误可大致分为 2 类,分别为语法错误和运行时错误。

1.1.1 语法错误

语法错误,即解析代码时出现的错误。当代码不符合 Python 语法规则时,Python 解释器在解析时就会报出 SyntaxError 语法错误,与此同时还会明确指出最早探测到错误的语句。

示例:

1
print"Hello,World!"

Python3 已不再支持上面这种写法,所以在运行时,解释器会报语法错误。

1.1.2 运行时错误

运行时错误,即程序在语法上都是正确的,但在运行时发生了错误。

示例:

1
a=1/0

上面这句代码的意思是用 1 除以 0,并赋值给 a。因为 0 作除数是没有意义的,所以运行后会产生运行时错误。

1.2 常见异常类型

官网异常定义:https://docs.python.org/3/library/exceptions.html#base-dClasses

在 Python 中,把这种运行时产生错误的情况叫做异常(Exceptions),常见的几种异常情况如下。

异常类型 含义 实例
AssertionError 当 assert 关键字后的条件为假时,程序运行会停止并抛出 AssertionError 异常 demo_list = [‘hello’]assert len(demo_list) < 0
AttributeError 当试图访问的对象属性不存在时抛出的异常 age = 18age.len
IndexError 索引超出序列范围会引发此异常 demo_list = [‘hello’]demolist[3]
KeyError 字典中查找一个不存在的关键字时引发此异常 demo_dict = {‘url’: “www.baidu.com"}demo\_dict\["name"]
NameError 尝试访问一个未声明的变量时,引发此异常
TypeError 不同类型数据之间的无效操作 123 + “hello world”
ZeroDivisionError 除法运算中除数为 0 引发此异常 a = 1/0

1.3 异常处理

Python 异常相关的关键字主要有:

关键字 关键字说明
try / except 捕获异常并处理
pass 忽略异常
as 定义异常实例(exceptMyErrorase)
else 如果 try 中的语句没有引发异常,则执行 else 中的语句
finally 无论是否出现异常,都执行的代码
raise 抛出 / 引发异常

1.3.1 Try - except

当发生异常时,我们就需要对异常进行捕获,然后进行相应的处理。python 的异常捕获常用 try…except… 结构,把可能发生错误的语句放在 try 模块里,用 except 来处理异常,每一个 try,都必须至少对应一个 except。

基本语法结构如下所示:

1
2
3
4
5
6
7
8
try:
可能产生异常的代码块
except [(Error1, Error2, ... ) [as e] ]:
处理异常的代码块1
except [(Error3, Error4, ... ) [as e] ]:
处理异常的代码块2
except [Exception]:
处理其它异常

举个简单的例子,下面两种写法是等价的,a / 0 在运行时会报异常,但是我用 try…except 捕捉后,就不会抛出异常而是执行 except 中的打印语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 写法一
try:
a = 1
print(a/0)
except:
print("捕获所有异常")

# 写法二
try:
a = 1
print(a/0)
except Exception:
print("捕获所有异常")

如果我想同时捕获多种异常也是可以的,例如下面这个例子,针对不同异常情况进行相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
a = int(input("输入被除数:"))
b = int(input("输入除数:"))
c = a / b
print("您输入的两个数相除的结果是:", c)
except (ValueError, ArithmeticError) as e:
print("同时捕获多个异常!")
print("程序发生了数字格式异常、算术异常之一")
print(e)
except FileNotFoundError as e:
print("test1")
except ZeroDivisionError as e:
print("test2")
except:
print("未知异常")

1.3.2 Try - except - Else

在原本的 try except 结构的基础上,Python 异常处理机制还提供了一个 else 块,即 try except else 结构。

使用 else 包裹的代码,只有当 try 块没有捕获到任何异常时,才会得到执行;反之,如果 try 块捕获到异常,即调用对应的 except 来处理异常,else 块中的代码也不会得到执行。

1
2
3
4
5
6
7
8
9
10
11
try:
result = 20 / int(input('请输入除数:'))
print(result)
except ValueError:
print("必须输入整数")
except ArithmeticError:
print("算术错误,除数不能为 0")
else:
print('没有出现异常')

print("继续执行")

我们再来看一个完整点且复杂点的分析文本的例子,这里将使用方法 split( ) ,这里以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。

1
2
3
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice','in','Wonderland']

下面将分析多本书,并统计每本书有多少个单词。

1
2
3
4
5
6
7
8
9
10
11
12
13
def count_words(filename):
try:
with open(filename, encoding='utf-8') as f:
contents = f.read()
except FileNotFoundError:
print(f"Sorry,the file {filename} dose not exist.")
else:
words = contents.split()
num_word = len(words)
print(f"The file {filename} has about {num_words} words.")
filnames = ['alice.txt','siddhartha.txt','moby_dick.txt','little_women.txt']
for filename in filenames:
count_words(filename)

1.3.3 Try - except - Finally

Python 异常处理机制还提供了一个 finally 语句,通常用来为 try 快中的程序做扫尾清理工作。

在整个异常处理机制中,finally 语句的功能是:无论 try 块是否发生异常,最终都要进入 finally 语句,并执行其中的代码块。

1.3.4 Raise

当程序出现错误,Python 会自动引发异常,也可以通过 raise 显示地引发异常。一旦执行了 raise 语句,raise 后面的语句将不能执行。

raise 语句的基本语法格式为:

1
raise [exceptionName [(reason)]]

raise 语句有三种常用的用法:

  • raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
  • raise 异常类名称:raise 后带一个异常类名称,表示引发执行类型的异常。
  • raise 异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。

对于一些未知的异常,我们就可以通过 raise 来触指定的异常,并通过 try except 相关捕获方法捕获该异常,这样就可以通过人工自定义的异常做相应的处理。

1
2
3
4
5
6
7
8
9
def divide(a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b

try:
print(divide(5, 0))
except ZeroDivisionError as e:
print(f"捕获到异常:{e}")

1.4 获取异常信息

在实际调试程序的过程中,有时只获得异常的类型是远远不够的,还需要借助更详细的异常信息才能解决问题。

捕获异常时,有 2 种方式可获得更多的异常信息,分别是:

  • 使用 sys 模块中的 exc_info 方法
  • 使用 traceback 模块中的相关函数

使用 traceback 模块查看异常传播轨迹,首先需要将 traceback 模块引入,该模块提供了如下两个常用方法:

  • traceback.print_exc():将异常传播轨迹信息输出到控制台或指定文件中。
  • format_exc():返回一个字符串而不是打印到文件。

我们可以直接获取到错误的信息显示出来,并不会影响后续的执行。

1
2
3
4
5
6
import traceback
try:
print(1/0)
except Exception as e:
traceback.print_exc()
print("继续执行")

也可以将获取到的错误信息赋值给一个变量保存起来。

1
2
3
4
5
6
7
import traceback
try:
print(1/0)
except Exception as e:
error = traceback.format_exc()
print("format_exc 异常信息:", error)
print("继续执行")

2. 正则表达式

在线正则表达式测试:https://tool.oschina.net/regex/#

2.1 正则表达式详解

2.1.1 介绍

正则表达式是一种用于匹配和操作文本的规则或语法:

  • 正则表达式拥有强大的文本处理能力,支持匹配、查找、替换等操作
  • 广泛的应用领域,例如编程、数据处理、文本处理等领域
  • 通用性强,跨语言跨场景

2.1.2 语法

    | 匹配字符串结尾                   | abc$ 表示 abc 在一个字符串的结尾                                |                        |                              |

| () | 分组标记,内部只能使用\ | 操作符 | (abc) 表示 abc,(abc \ | def) 表示 abc、def |

| \d | 数字,等价于 [0-9] | | | |

| \w | 单词字符,等价于 [A-Za-z0-9_] | | | |

2.2 正则表达式实战

2.2.1 案例一

现在有这样一串字符,我需要根据需求查找出相应的字符信息。

1
hello world 12345 is book 123
  • 匹配出 hello 字符串
    • 方法一:hello
    • 方法二:^hello
  • 匹配出所有数字
    • 方法:\d+
  • 匹配出所有字符串
    • 方法:[a-zA-Z]+

2.2.2 案例二

要求写一个正则表达式,用来匹配邮箱,而邮箱规则如下:

  • 结构:前缀@后缀
  • 前缀:由大小写字母、数字、下划线、中划线等构成
  • 后缀:由小写字母或数字构成,并以 .com 结尾
1
2
3
123@qq.com
test@email.com
xxx@123.com

匹配邮箱的正则表达式:

  • 匹配单个邮箱
    • 方法:^[a-zA-Z0-9_-]+@[a-z0-9]+.com$
  • 匹配多个邮箱
    • 方法:[a-zA-Z0-9_-]+@[a-z0-9]+.com

2.3 Python 正则表达式用法

2.3.1 Re 模块

在字符串中搜索匹配正则表达式的第一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

text = "Hello, World!"
result = re.search('World', text)

# 找到匹配
if result:
print("找到匹配")
print(result) # <re.Match object; span=(7, 12), match='World'>
print(result.span()) # (7, 12)
print(result.group()) # World
else:
print("未找到匹配")
re.match( )

用于从字符串的起始位置匹配正则表达式,如果起始位置匹配成功,则返回一个匹配对象;否则返回 None。

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

text = "Hello World"
result = re.match('Hello', text)

# 匹配成功
if result:
print("匹配成功")
print(result) # <re.Match object; span=(0, 5), match='Hello'>
print(result.span()) # (0, 5)
print(result.group()) # Hello
else:
print("匹配失败")

Tips:

re.match() 只从字符串的起始位置进行匹配,而 re.search() 则会在字符串中搜索匹配的部分,不限于起始位置。

再看个复杂点的正则表达式,进一步理解一下 match 和 search 函数的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author:gdx
# @File:test.py.py
# @Project:test_python

import re

# 匹配1
text1 = "@#@!¥123_23134@qq.com"
result = re.match('[a-zA-Z0-9_-]+@[a-z0-9]+.com', text1)
# 匹配失败
if result:
print("匹配成功")
print(result)
print(result.group())
else:
print("匹配失败")

# 匹配2
result = re.search('[a-zA-Z0-9_-]+@[a-z0-9]+.com', text1)
# 匹配成功
if result:
print("匹配成功")
print(result) # <re.Match object; span=(5, 21), match='123_23134@qq.com'>
print(result.group()) # 123_23134@qq.com
else:
print("匹配失败")

# 匹配3
text2 = "123_23134@qq.com"
result = re.match('[a-zA-Z0-9_-]+@[a-z0-9]+.com', text2)
# 匹配成功
if result:
print("匹配成功")
print(result) # <re.Match object; span=(0, 16), match='123_23134@qq.com'>
print(result.group()) # 123_23134@qq.com
else:
print("匹配失败")
re.findall( )

返回字符串中所有匹配正则表达式的子串。

1
2
3
4
5
import re

text = "apple, banana, cherry"
matches = re.findall('a[a-z]+', text) # ['apple', 'anana']
print(matches)
re.compile( )

re.complile() 用于编译正则表达式模式,生成一个正则表达式对象。这个对象可以被多次使用,从而提高效率。它接受一个字符串形式的正则表达式作为参数,并返回一个编译后的正则表达式对象。

除了前面提到的可以直接使用编译后的对象调用 findall 等方法之外,还可以设置一些标志参数来影响正则表达式的匹配行为。

常见的标志参数有:

  • re.IGNORECASE 或 re.I :使匹配对大小写不敏感。
  • re.MULTILINE 或 re.M :多行模式,影响 ^ 和 $ 的匹配行为。
  • re.DOTALL 或 re.S :使 . 匹配包括换行符在内的所有字符。
1
2
3
4
5
6
7
8
9
10
11
import re

pattern = re.compile('hello', re.IGNORECASE)
text = "Hello World"
result = pattern.search(text)
if result:
print("找到匹配")
print(result) # <re.Match object; span=(0, 5), match='Hello'>
print(result.group()) # Hello
else:
print("未找到匹配")

可以再来看一个分割字符串的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re

data_list=[
'2小时10分20秒',
'3小时20分30秒',
'1小时10分5秒'
]
pattern = re.compile('小时|分|秒')
for i in data_list:
res = pattern.split(i)
print(res)
"""
['2', '10', '20', '']
['3', '20', '30', '']
['1', '10', '5', '']
"""
( ) 分组

使用括号 () 进行分组,可以提取匹配的子串。

1
2
3
4
5
6
7
8
9
import re

pattern = re.compile(r"(\d{3})-(\d{3})-(\d{4})")
text = "My phone number is 123-456-7890"
result = pattern.search(text)
if result:
print(result.group(1)) # 123
print(result.group(2)) # 456
print(result.group(3)) # 7890

Tips:

这里的 r 表示原始字符串,避免一些字符需要额外的转义。

re.sub( )

使用 re.sub() 函数可以进行替换操作。

1
2
3
4
5
import re

text = "Hello, World!"
new_text = re.sub('World', 'Python', text)
print(new_text) # Hello, Python!

我们再结合前面的分组方法看一个更复杂点的例子,我想要将一串手机号的中间 4 个数字进行加密处理,我们

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

# 方法一:分两组
pattern = re.compile(r"(\d{3})\d{4}(\d{3})")
text = "我有几个手机号分别是:1361234567,1331234567,1797654321"
new_text = pattern.sub(r"\1****\2", text)
print(new_text) # 我有几个手机号分别是:136****567,133****567,179****321

# 方法二:分三组
pattern = re.compile(r"(\d{3})(\d{4})(\d{3})")
text = "我有几个手机号分别是:1361234567,1331234567,1797654321"
new_text = pattern.sub(r"\1****\3", text)
print(new_text) # 我有几个手机号分别是:136****567,133****567,179****321