用Python自动化枯燥的工作 第4篇:数据结构与数据处理
摘要
本文将带你深入理解Python核心数据结构和文本处理技术,帮助你掌握高效组织数据和处理文本信息的能力。你将学到列表的高级操作、字典与集合的应用、字符串处理技巧、正则表达式基础,以及数据结构选择的最佳实践。
学习目标
阅读完本文后,你将能够:
- 列表操作:熟练使用列表的创建、访问、修改、切片等操作,理解列表推导式的强大功能
- 字典应用:掌握字典的创建、访问、遍历、嵌套等操作,能够使用字典解决实际问题
- 集合使用:理解集合的特性,能够使用集合进行去重、交集、并集等操作
- 字符串处理:熟练运用字符串的切片、查找、替换、格式化等操作
- 正则表达式:掌握正则表达式的基本语法,能够编写模式匹配复杂的文本
- 数据结构选择:根据实际需求选择最合适的数据结构,优化程序性能
一、列表:有序的集合数据结构
1.1 列表基础
列表是Python中最常用的数据结构之一,它是一个有序的、可变的集合,可以包含任意类型的元素。
# 创建列表
fruits = ['apple', 'banana', 'cherry']
numbers = [1, 2, 3, 4, 5]
mixed = [1, 'hello', 3.14, True]
empty = []
# 访问元素
print(fruits[0]) # 'apple'
print(fruits[-1]) # 'cherry'(最后一个元素)
# 获取列表长度
print(len(fruits)) # 3列表与字符串的一个重要区别是列表是可变的,而字符串是不可变的。这意味着我们可以修改列表的内容,但不能修改字符串中的字符。
1.2 列表的基本操作
列表支持多种操作来修改和管理其中的元素。
colors = ['red', 'green', 'blue']
# 修改元素
colors[1] = 'yellow'
print(colors) # ['red', 'yellow', 'blue']
# 添加元素
colors.append('purple') # 在末尾添加
colors.insert(1, 'orange') # 在指定位置插入
print(colors)
# 删除元素
colors.remove('blue') # 删除指定值的元素
popped = colors.pop() # 删除并返回最后一个元素
del colors[0] # 删除指定位置的元素
print(colors)下面的序列图展示了列表常用操作的执行过程:
sequenceDiagram participant Main as 主程序 participant List as 列表对象 Note over Main,List: 创建列表 Main->>List: fruits = ['apple', 'banana'] Note right of List: 列表初始状态<br/>['apple', 'banana'] Note over Main,List: 添加元素 Main->>List: append('cherry') Note right of List: ['apple', 'banana', 'cherry'] Main->>List: insert(1, 'orange') Note right of List: ['apple', 'orange',<br/>'banana', 'cherry'] Note over Main,List: 修改元素 Main->>List: fruits[0] = 'grape' Note right of List: ['grape', 'orange',<br/>'banana', 'cherry'] Note over Main,List: 删除元素 Main->>List: remove('banana') Note right of List: ['grape', 'orange', 'cherry'] Main->>List: pop() Note right of List: 返回 'cherry'<br/>['grape', 'orange']
图表讲解:这个序列图详细展示了列表对象如何响应各种操作命令,这是理解列表可变性的关键。
首先看创建操作:主程序使用字面量语法创建一个包含两个元素的列表fruits。列表对象在内存中被创建,初始包含'apple'和'banana'两个元素。列表记住元素的顺序,每个位置都有一个索引(从0开始)。
然后是添加元素操作:append('cherry')命令在列表末尾添加新元素,列表变为['apple', 'banana', 'cherry']。insert(1, 'orange')命令在索引1的位置插入新元素,原来索引1及之后的元素都向后移动一位,列表变为['apple', 'orange', 'banana', 'cherry']。这两个操作的区别在于:append总是在末尾添加,insert可以在任意位置插入。
修改元素操作展示了列表的可变性:fruits[0] = 'grape'直接修改索引0位置的元素,将'apple'替换为'grape'。这个操作不会改变列表的大小,只是修改了特定位置的值。
删除操作有几种方式:remove('banana')按值删除,找到第一个匹配的值并删除;pop()删除并返回最后一个元素,可以同时获取被删除的值。删除后列表会自动收缩,后面的元素向前移动填补空位。
理解这些操作对于有效使用列表非常重要,特别是在处理动态数据集合时。
1.3 列表切片
切片是Python中非常强大的功能,允许获取列表的子集。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 基本切片 [开始:结束](不包含结束位置)
print(numbers[2:5]) # [2, 3, 4]
# 省略开始或结束
print(numbers[:5]) # [0, 1, 2, 3, 4](从头开始)
print(numbers[5:]) # [5, 6, 7, 8, 9](到末尾)
print(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9](整个列表)
# 使用步长
print(numbers[::2]) # [0, 2, 4, 6, 8](每隔一个)
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0](反转)
# 负数索引
print(numbers[-3:]) # [7, 8, 9](最后三个)
print(numbers[:-3]) # [0, 1, 2, 3, 4, 5, 6](除最后三个)切片的语法是[开始:结束:步长],所有参数都是可选的。切片返回一个新的列表,不会修改原列表。
1.4 列表推导式
列表推导式是Python中创建列表的简洁方式,结合了循环和条件判断。
# 基本列表推导式
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 带条件的列表推导式
even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]
# 处理字符串
words = ['hello', 'world', 'python', 'programming']
lengths = [len(word) for word in words]
print(lengths) # [5, 5, 6, 11]
# 嵌套列表推导式
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9]列表推导式的语法结构是[表达式 for 项 in 可迭代对象 if 条件],其中if部分是可选的。相比传统的循环,列表推导式更加简洁易读。
下面的流程图展示了列表推导式的执行流程:
flowchart TD A[开始: 列表推导式] --> B[遍历可迭代对象] B --> C{还有元素?} C -->|否| D[创建完成列表] C -->|是| E[获取下一个元素] E --> F{条件满足?} F -->|否| B F -->|是| G[计算表达式值] G --> H[将值添加到结果列表] H --> B D --> I[返回结果列表] style A fill:#e1f5e1 style I fill:#e1f5ff style F fill:#fff5e1
图表讲解:这个流程图展示了列表推导式内部的工作机制,理解它有助于编写更复杂的推导式。
列表推导式从创建一个空的结果列表开始(绿色区域)。然后进入主循环,遍历输入的可迭代对象(如range(10)或另一个列表)。对于可迭代对象中的每个元素,都需要判断是否满足条件(黄色决策点)。
如果条件不满足(使用if子句时),跳过当前元素,继续处理下一个元素。这样实现了过滤功能,只有符合条件的元素才会被处理。
如果条件满足,或者没有条件子句,就计算表达式的值。表达式中可以使用当前元素进行各种运算,如x**2、len(word)等。计算出的值被添加到结果列表中。
循环继续,直到可迭代对象中的所有元素都被处理完毕。最后返回填充好的结果列表(蓝色区域)。
这个流程展示了列表推导式的两个核心功能:映射(通过表达式转换每个元素)和过滤(通过条件选择特定元素)。理解这个流程,就能灵活运用列表推导式处理各种数据转换任务。
二、字典:键值对的数据结构
2.1 字典基础
字典是Python中另一种重要的数据结构,它存储键值对(key-value pairs),通过键来快速访问对应的值。
# 创建字典
person = {
'name': '张三',
'age': 25,
'city': '北京'
}
# 访问值
print(person['name']) # '张三'
print(person.get('age')) # 25
# 添加键值对
person['email'] = '[email protected]'
# 修改值
person['age'] = 26
# 删除键值对
del person['city']
age = person.pop('age')
print(person)字典的键必须是不可变类型(如字符串、数字、元组),而值可以是任意类型。字典是可变的,可以随时添加、修改或删除键值对。
2.2 字典的常用方法
字典提供了丰富的方法来操作和访问数据。
student = {
'name': '李四',
'age': 20,
'grade': '大二',
'courses': ['数学', '英语', '计算机']
}
# 获取所有键、值或键值对
print(student.keys()) # dict_keys(['name', 'age', 'grade', 'courses'])
print(student.values()) # dict_values(['李四', 20, '大二', ['数学', '英语', '计算机']])
print(student.items()) # dict_items([('name', '李四'), ('age', 20), ...])
# 检查键是否存在
print('name' in student) # True
print('email' in student) # False
# 获取值(带默认值)
print(student.get('name')) # '李四'
print(student.get('email', '未知')) # '未知'
# 更新字典
student.update({'age': 21, 'email': '[email protected]'})
# 清空字典
student.clear()2.3 遍历字典
有多种方式遍历字典中的数据。
scores = {
'张三': 95,
'李四': 87,
'王五': 92,
'赵六': 88
}
# 遍历键
print("学生名单:")
for name in scores.keys():
print(f"- {name}")
# 遍历值
print("\n所有分数:")
for score in scores.values():
print(f"- {score}")
# 遍历键值对
print("\n成绩单:")
for name, score in scores.items():
print(f"{name}: {score}分")2.4 字典的嵌套
字典可以嵌套,创建复杂的数据结构。
# 嵌套字典示例
employees = {
'EMP001': {
'name': '张三',
'department': '技术部',
'skills': ['Python', 'Java', 'SQL']
},
'EMP002': {
'name': '李四',
'department': '市场部',
'skills': ['营销', '文案', '数据分析']
}
}
# 访问嵌套数据
print(employees['EMP001']['name']) # '张三'
print(employees['EMP002']['skills'][0]) # '营销'
# 遍历嵌套字典
for emp_id, info in employees.items():
print(f"\n员工ID: {emp_id}")
print(f"姓名: {info['name']}")
print(f"部门: {info['department']}")
print(f"技能: {', '.join(info['skills'])}")下面的类图展示了字典数据结构的组织方式:
classDiagram class Dict { +keys(): list +values(): list +items(): list +get(key, default): value +pop(key): value +update(other_dict): void +clear(): void } class Key { «不可变» +str +int +float +tuple } class Value { «任意类型» +str +int +float +list +dict +object } class NestedDict { «嵌套结构» +outer_key: Dict +inner_key: value } Dict "1" --> "*" Key : contains Dict "1" --> "*" Value : stores NestedDict --> Dict : extends Value --> Dict : can be
图表讲解:这个类图展示了字典数据结构的完整设计和能力,有助于理解字典的工作原理和适用场景。
Dict类是字典的核心,提供了丰富的方法来操作键值对。keys()方法返回所有键的集合,values()方法返回所有值的集合,items()方法返回所有键值对的元组集合。这些方法使得遍历字典变得非常方便。get()方法提供了安全访问值的方式,可以指定默认值;pop()方法删除并返回指定键的值;update()方法可以批量更新字典;clear()方法清空整个字典。
Key类展示了字典键的约束(绿色区域):键必须是不可变类型。可用的键类型包括字符串(最常用)、数字、浮点数和元组。这些类型都是不可变的,即创建后不能修改,这确保了键的稳定性。如果使用可变类型(如列表)作为键,会导致错误。
Value类展示了字典值的灵活性(蓝色区域):值可以是任意类型,包括基本类型(字符串、数字)、容器类型(列表、字典)和自定义对象。这种灵活性使得字典可以存储复杂的数据结构。值甚至可以是另一个字典,这就形成了嵌套结构。
NestedDict类表示嵌套字典(紫色区域):这是字典的一种高级用法,字典的值本身又是字典。这种结构非常适合表示层次化数据,如配置文件、JSON数据、树形结构等。通过嵌套,可以构建任意复杂度的数据结构。
理解这个设计有助于在实际编程中选择合适的数据结构。字典特别适合需要通过键快速查找值的场景,如缓存、配置管理、数据映射等。
三、集合:唯一值的数据结构
3.1 集合基础
集合是Python中存储唯一值的数据结构,类似于数学中的集合概念。集合中的元素是无序的,且不能重复。
# 创建集合
numbers = {1, 2, 3, 4, 5}
letters = set('hello') # {'h', 'e', 'l', 'o'}(重复的'l'只保留一个)
empty = set() # 空集合(注意:{}是空字典,不是空集合)
# 添加元素
numbers.add(6)
# 删除元素
numbers.remove(3) # 如果元素不存在会报错
numbers.discard(10) # 如果元素不存在不会报错
# 清空集合
numbers.clear()集合的一个重要特性是自动去重,这使得它非常适合需要唯一值的场景。
3.2 集合运算
集合支持数学中的各种集合运算。
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
# 并集(两个集合的所有元素)
union = set_a | set_b
# 或 union = set_a.union(set_b)
print(union) # {1, 2, 3, 4, 5, 6, 7, 8}
# 交集(两个集合共有的元素)
intersection = set_a & set_b
# 或 intersection = set_a.intersection(set_b)
print(intersection) # {4, 5}
# 差集(在a中但不在b中的元素)
difference = set_a - set_b
# 或 difference = set_a.difference(set_b)
print(difference) # {1, 2, 3}
# 对称差集(在任一集合中但不在两者中的元素)
symmetric_diff = set_a ^ set_b
# 或 symmetric_diff = set_a.symmetric_difference(set_b)
print(symmetric_diff) # {1, 2, 3, 6, 7, 8}3.3 集合的关系判断
可以判断集合之间的包含关系。
set_a = {1, 2, 3}
set_b = {1, 2, 3, 4, 5}
set_c = {1, 2}
# 子集判断
print(set_a.issubset(set_b)) # True(set_a是set_b的子集)
print(set_c <= set_a) # True
# 超集判断
print(set_b.issuperset(set_a)) # True(set_b是set_a的超集)
print(set_a >= set_c) # True
# 判断是否相交
print(set_a.isdisjoint({6, 7, 8})) # True(没有共同元素)
print(set_a.isdisjoint({3, 4})) # False(有共同元素)3.4 集合的应用场景
集合在许多实际场景中都非常有用。
# 去重
def remove_duplicates(items):
"""从列表中移除重复项"""
return list(set(items))
duplicated = [1, 2, 2, 3, 3, 3, 4, 5]
unique = remove_duplicates(duplicated)
print(unique) # [1, 2, 3, 4, 5](顺序可能不同)
# 查找共同好友
user1_friends = {'张三', '李四', '王五', '赵六'}
user2_friends = {'李四', '王五', '孙七', '周八'}
common_friends = user1_friends & user2_friends
print(f"共同好友: {common_friends}")
# 权限检查
admin_permissions = {'read', 'write', 'delete', 'admin'}
user_permissions = {'read', 'write'}
def has_permission(required_permission, user_perms):
"""检查用户是否拥有指定权限"""
return required_permission in user_perms
print(has_permission('write', user_permissions)) # True
print(has_permission('delete', user_permissions)) # False下面的状态图展示了集合运算的各种操作和结果:
stateDiagram-v2 [*] --> 初始化两个集合 初始化两个集合 --> 并集运算: A | B 初始化两个集合 --> 交集运算: A & B 初始化两个集合 --> 差集运算: A - B 初始化两个集合 --> 对称差运算: A ^ B 并集运算 --> 返回所有元素: 两个集合的所有元素 交集运算 --> 返回共有元素: 同时在两个集合中的元素 差集运算 --> 返回独有元素: 只在第一个集合中的元素 对称差运算 --> 返回非共有元素: 只在一个集合中的元素 返回所有元素 --> [*] 返回共有元素 --> [*] 返回独有元素 --> [*] 返回非共有元素 --> [*] note right of 初始化两个集合 A = {1, 2, 3, 4, 5} B = {4, 5, 6, 7, 8} end note note right of 返回所有元素 结果: {1, 2, 3, 4, 5, 6, 7, 8} end note note right of 返回共有元素 结果: {4, 5} end note note right of 返回独有元素 结果: {1, 2, 3} end note note right of 返回非共有元素 结果: {1, 2, 3, 6, 7, 8} end note
图表讲解:这个状态图展示了集合四大运算的功能和效果,理解这些运算对于使用集合解决实际问题非常重要。
集合运算从初始化两个集合开始(顶部节点):假设我们有两个集合A和B,A包含{1, 2, 3, 4, 5},B包含{4, 5, 6, 7, 8}。这是所有运算的起点。
并集运算(|操作符或union()方法)返回两个集合的所有元素(绿色区域)。就像把两个集合的元素都放在一起,重复的只保留一个。结果为{1, 2, 3, 4, 5, 6, 7, 8},包含了A和B的所有唯一元素。这就像合并两个列表但去除了重复项。
交集运算(&操作符或intersection()方法)返回同时存在于两个集合中的元素(蓝色区域)。这就像找两个集合的”共同点”。结果为{4, 5},因为只有4和5同时在A和B中。这对于查找共同好友、共同标签等场景非常有用。
差集运算(-操作符或difference()方法)返回只存在于第一个集合但不在第二个集合中的元素(紫色区域)。这就像”A有什么是B没有的”。结果为{1, 2, 3},这些元素在A中但不在B中。这对于比较差异、排除特定项等场景很有用。
对称差运算(^操作符或symmetric_difference()方法)返回只存在于一个集合中的元素(粉色区域)。这就像”两个集合的不对称部分”。结果为{1, 2, 3, 6, 7, 8},这些元素要么在A中,要么在B中,但不会同时在两者中。这对于找出两个集合的差异很有帮助。
理解这些运算的实际含义,就能在遇到相关问题时快速选择合适的集合运算。
四、字符串处理:文本操作的艺术
4.1 字符串基础操作
字符串是Python中处理文本数据的基本类型,提供了丰富的操作方法。
# 创建字符串
text = "Hello, World!"
# 访问字符
print(text[0]) # 'H'
print(text[-1]) # '!'
# 字符串长度
print(len(text)) # 13
# 拼接字符串
greeting = "Hello"
name = "Alice"
message = greeting + ", " + name + "!"
print(message) # "Hello, Alice!"
# 重复字符串
dashed_line = "-" * 20
print(dashed_line) # "--------------------"4.2 字符串方法
Python的字符串对象提供了大量有用的方法。
text = " Hello, Python Programming! "
# 大小写转换
print(text.upper()) # " HELLO, PYTHON PROGRAMMING! "
print(text.lower()) # " hello, python programming! "
print(text.title()) # " Hello, Python Programming! "
print(text.capitalize()) # " hello, python programming! "
# 去除空白
print(text.strip()) # "Hello, Python Programming!"
print(text.lstrip()) # "Hello, Python Programming! "
print(text.rstrip()) # " Hello, Python Programming!"
# 查找和替换
print(text.find("Python")) # 8(找到的位置)
print(text.replace("Python", "World")) # " Hello, World Programming! "
# 分割和连接
words = text.split()
print(words) # ['Hello,', 'Python', 'Programming!']
sentence = " ".join(words)
print(sentence) # "Hello, Python Programming!"4.3 字符串格式化
字符串格式化是将变量值插入字符串的重要技术。
# 方法1: 使用f-string(Python 3.6+推荐)
name = "张三"
age = 25
message = f"姓名: {name}, 年龄: {age}"
print(message)
# 方法2: 使用format()方法
message = "姓名: {}, 年龄: {}".format(name, age)
print(message)
# 方法3: 使用%操作符(旧式)
message = "姓名: %s, 年龄: %d" % (name, age)
print(message)
# 格式化选项
price = 1234.56789
print(f"价格: {price:.2f}") # "价格: 1234.57"(保留两位小数)
print(f"价格: {price:>10.2f}") # "价格: 1234.57"(右对齐,宽度10)
print(f"价格: {price:<10.2f}") # "价格: 1234.57 "(左对齐,宽度10)
print(f"百分比: {0.85:.2%}") # "百分比: 85.00%"(百分比格式)4.4 字符串切片
字符串也支持切片操作,与列表类似。
text = "Python Programming"
# 基本切片
print(text[0:6]) # "Python"
print(text[7:]) # "Programming"
print(text[:6]) # "Python"
print(text[::2]) # "Pto rgamn"(每隔一个字符)
print(text[::-1]) # "gnimmargorP nohtyP"(反转)
# 常用模式
text = "[email protected]"
# 获取用户名和域名
username = text[:text.index('@')]
domain = text[text.index('@') + 1:]
print(f"用户名: {username}")
print(f"域名: {domain}")下面的流程图展示了字符串处理的常用方法和应用场景:
flowchart TD A[字符串数据] --> B{需要做什么?} B -->|转换大小写| C[upper/lower/title<br/>转换方法] B -->|去除空白| D[strip/lstrip/rstrip<br/>去除方法] B -->|查找内容| E[find/index/count<br/>查找方法] B -->|替换内容| F[replace<br/>替换方法] B -->|分割组合| G[split/join<br/>分割连接方法] B -->|格式化输出| H[f-string/format<br/>格式化方法] C --> I[返回新字符串] D --> I E --> J[返回位置或数量] F --> I G --> K[返回列表或字符串] H --> I I --> L[用于显示或存储] J --> M[用于条件判断] K --> L style A fill:#e1f5e1 style I fill:#e1f5ff style M fill:#fff5e1
图表讲解:这个流程图展示了字符串处理的主要方法和应用方向,帮助理解如何选择合适的字符串操作方法。
字符串处理从原始字符串数据开始(绿色区域):首先需要明确操作目标,根据目标选择相应的方法类别。
如果需要转换大小写(蓝色区域):upper()将所有字符转为大写,lower()转为小写,title()将每个单词首字母大写,capitalize()将首字母大写其余小写。这些方法都返回新的字符串,不修改原字符串(因为字符串是不可变的)。常用于标准化文本格式,如统一大小写后比较。
如果需要去除空白:strip()去除两端的空白,lstrip()只去除左端,rstrip()只去除右端。这在处理用户输入或读取文件时特别有用,可以去除意外的空格、换行符等。
如果需要查找内容(黄色区域):find()返回子串的位置(找不到返回-1),index()类似但找不到会抛出异常,count()返回子串出现的次数。查找结果通常用于条件判断,如验证字符串是否包含特定内容。
如果需要替换内容:replace(old, new)将所有出现的旧子串替换为新子串。这可以用于修改文本内容,如替换敏感词、修正错误拼写等。
如果需要分割组合:split()按分隔符将字符串分割为列表,join()将列表元素连接为字符串。这两个是互补操作,常用于解析结构化文本(如CSV)或构建格式化输出。
如果需要格式化输出:f-string(推荐)、format()方法、%操作符都能将变量值插入字符串。f-string语法最简洁,支持表达式和格式化选项,是Python 3.6+的首选方法。
理解这些方法的适用场景,就能高效地处理各种文本操作任务。
五、正则表达式:强大的文本匹配工具
5.1 正则表达式基础
正则表达式是一种强大的文本模式匹配工具,可以用来查找、替换符合特定模式的文本。
import re
# 基本匹配
text = "我的电话号码是 138-1234-5678,请回电。"
pattern = r'\d{3}-\d{4}-\d{4}'
# 查找匹配
match = re.search(pattern, text)
if match:
print(f"找到电话号码: {match.group()}")
# 查找所有匹配
phones = re.findall(pattern, text)
print(f"所有电话号码: {phones}")
# 替换匹配
new_text = re.sub(pattern, "[号码已隐藏]", text)
print(new_text)5.2 常用正则表达式模式
正则表达式使用特殊字符来定义匹配模式。
import re
# 基本字符匹配
patterns = {
r'\d': '匹配任意数字', # [0-9]
r'\D': '匹配非数字字符',
r'\w': '匹配字母数字下划线', # [a-zA-Z0-9_]
r'\W': '匹配非字母数字下划线',
r'\s': '匹配空白字符', # 空格、制表符、换行等
r'\S': '匹配非空白字符',
r'.': '匹配任意字符(除换行外)'
}
# 量词
patterns.update({
r'abc*': '匹配ab,后跟0个或多个c',
r'abc+': '匹配ab,后跟1个或多个c',
r'abc?': '匹配ab,后跟0个或1个c',
r'abc{3}': '匹配ab,后跟恰好3个c',
r'abc{3,5}': '匹配ab,后跟3到5个c'
})
# 字符类
patterns.update({
r'[abc]': '匹配a、b或c',
r'[a-z]': '匹配任意小写字母',
r'[^abc]': '匹配除a、b、c外的任意字符',
r'(abc|def)': '匹配abc或def'
})5.3 实用正则表达式示例
一些常见任务的实用正则表达式模式。
import re
# 验证邮箱地址
def is_valid_email(email):
"""验证邮箱地址格式"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
print(is_valid_email("[email protected]")) # True
print(is_valid_email("invalid.email")) # False
# 提取URL
def extract_urls(text):
"""从文本中提取URL"""
pattern = r'https?://[^\s]+'
return re.findall(pattern, text)
text = "访问 https://www.example.com 或 http://test.org 获取更多信息"
urls = extract_urls(text)
print(urls) # ['https://www.example.com', 'http://test.org']
# 提取日期
def extract_dates(text):
"""提取各种格式的日期"""
patterns = [
r'\d{4}-\d{2}-\d{2}', # 2024-03-15
r'\d{4}/\d{2}/\d{2}', # 2024/03/15
r'\d{2}-\d{2}-\d{4}', # 15-03-2024
]
dates = []
for pattern in patterns:
dates.extend(re.findall(pattern, text))
return dates
text = "会议日期是 2024-03-15,或 15/03/2024,或 2024/03/15"
dates = extract_dates(text)
print(dates)5.4 正则表达式的编译和优化
对于重复使用的正则表达式,可以先编译以提高性能。
import re
# 编译正则表达式
phone_pattern = re.compile(r'\d{3}-\d{4}-\d{4}')
# 使用编译后的模式
text = "电话1: 138-1234-5678,电话2: 139-8765-4321"
phones = phone_pattern.findall(text)
print(phones) # ['138-1234-5678', '139-8765-4321']
# 带标志的编译
case_insensitive = re.compile(r'python', re.IGNORECASE)
text = "Python is great. PYTHON is powerful."
matches = case_insensitive.findall(text)
print(matches) # ['Python', 'PYTHON']
# 使用捕获组提取信息
date_pattern = re.compile(r'(\d{4})-(\d{2})-(\d{2})')
text = "出生日期: 1990-05-15"
match = date_pattern.search(text)
if match:
year, month, day = match.groups()
print(f"年: {year}, 月: {month}, 日: {day}")下面的流程图展示了正则表达式匹配的完整过程:
flowchart TD A[开始正则匹配] --> B[编译正则表达式] B --> C[从文本起始位置开始] C --> D{当前位置匹配?} D -->|是| E{还有更多模式?} D -->|否| F{模式可选?} F -->|是| E F -->|否| G[匹配失败] E -->|否| H[匹配成功] E -->|是| I[尝试匹配下一部分] I --> J{当前位置匹配?} J -->|是| I J -->|否| K{可以回溯?} K -->|是| L[回溯到之前位置] K -->|否| M[匹配失败] L --> I H --> N[返回匹配结果] G --> O[继续搜索或返回失败] M --> O style A fill:#e1f5e1 style H fill:#cffcfc style G fill:#ffcfcf style M fill:#ffcfcf style O fill:#ffcfcf
图表讲解:这个流程图展示了正则表达式引擎的工作机制,虽然复杂,但理解它有助于编写更高效的正则表达式。
正则匹配从编译正则表达式开始(绿色区域):正则表达式首先被编译成内部表示,这个过程分析模式的结构,优化匹配算法。编译后的模式可以重复使用,提高性能。
然后从文本的起始位置开始匹配:引擎逐个字符地尝试匹配模式。当前位置的字符是否与模式匹配(第一个黄色决策)?
如果当前位置匹配,检查是否还有更多模式需要匹配(第二个黄色决策)。如果没有更多模式,匹配成功(绿色区域)。如果还有更多模式,尝试匹配下一部分,这是一个递归过程。
如果当前位置不匹配,检查当前模式部分是否可选(如用?、*或{0,}量词修饰的部分)。如果可选,可以跳过这部分,继续尝试匹配后续模式。如果不可选,匹配失败(红色区域)。
在匹配过程中,引擎可能需要”回溯”(蓝色区域)。这是正则表达式的重要特性:当某个匹配路径失败时,引擎可以回退到之前的选择点,尝试其他可能的匹配方式。例如,模式a.*b匹配"abcb"时,.*首先匹配"abc",但随后找不到b,所以回溯,.*改为匹配"ab",然后成功。
理解回溯有助于编写高效的正则表达式:过多的回溯会导致性能问题,特别是在处理复杂模式和大量文本时。使用具体字符类代替.、使用非贪婪量词(*?、+?)、避免嵌套量词(如(a+)+)都可以减少不必要的回溯。
六、数据结构选择与性能优化
6.1 数据结构比较
不同的数据结构有不同的性能特点,选择合适的结构对程序性能至关重要。
# 性能测试示例
import time
# 列表 vs 集合的成员测试
def test_list_membership():
items = list(range(100000))
start = time.time()
for i in range(50000, 100000):
_ = i in items
end = time.time()
return end - start
def test_set_membership():
items = set(range(100000))
start = time.time()
for i in range(50000, 100000):
_ = i in items
end = time.time()
return end - start
list_time = test_list_membership()
set_time = test_set_membership()
print(f"列表成员测试时间: {list_time:.4f}秒")
print(f"集合成员测试时间: {set_time:.4f}秒")
print(f"集合比列表快 {list_time/set_time:.1f} 倍")6.2 操作复杂度对比
了解各种操作的复杂度有助于选择合适的数据结构。
"""
数据结构操作复杂度对比
列表 (list):
- 索引访问: O(1)
- 末尾追加: O(1)
- 中间插入: O(n)
- 中间删除: O(n)
- 成员测试: O(n)
- 排序: O(n log n)
字典 (dict):
- 键查找: O(1) 平均
- 键插入: O(1) 平均
- 键删除: O(1) 平均
- 成员测试: O(1) 平均
集合 (set):
- 添加元素: O(1) 平均
- 删除元素: O(1) 平均
- 成员测试: O(1) 平均
- 交集/并集: O(min(len(s), len(t)))
元组 (tuple):
- 索引访问: O(1)
- 成员测试: O(n)
- 不可变(创建后不能修改)
"""6.3 选择建议
根据实际需求选择最合适的数据结构。
# 场景1: 需要按索引快速访问,数据量小
# 使用列表
data = [10, 20, 30, 40, 50]
print(data[2]) # O(1) 快速访问
# 场景2: 需要频繁的成员测试
# 使用集合
allowed_users = {'alice', 'bob', 'charlie'}
if username in allowed_users: # O(1) 快速测试
print("允许访问")
# 场景3: 需要键值对存储
# 使用字典
user_scores = {
'alice': 95,
'bob': 87,
'charlie': 92
}
score = user_scores.get('alice', 0) # O(1) 快速查找
# 场景4: 数据不会改变,需要节省内存
# 使用元组
coordinates = (10.5, 20.3, 30.1)
x, y, z = coordinates # 元组解包下面的决策树展示了如何根据需求选择合适的数据结构:
flowchart TD A[需要存储数据] --> B{数据需要修改?} B -->|否| C[使用元组 tuple] B -->|是| D{需要键值对?} D -->|是| E[使用字典 dict] D -->|否| F{需要保持顺序?} F -->|是| G[使用列表 list] F -->|否| H{需要唯一值?} H -->|是| I[使用集合 set] H -->|否| G C --> J{应用场景} E --> J G --> J I --> J J --> K[配置数据<br/>函数返回多值] J --> L[缓存<br/>计数器<br/>映射关系] J --> M[序列数据<br/>堆栈<br/>队列] J --> N[去重<br/>集合运算<br/>成员测试] style A fill:#e1f5e1 style C fill:#e1f5ff style E fill:#e1f5ff style G fill:#e1f5ff style I fill:#e1f5ff
图表讲解:这个决策树提供了一个系统化的方法来选择合适的数据结构,根据具体需求逐层缩小选择范围。
首先问数据是否需要修改(第一个黄色决策):如果数据在创建后不需要修改,应该使用元组(绿色区域)。元组是不可变的,这带来了几个好处:更安全(不会意外修改)、更节省内存、可以作为字典的键。元组常用于存储配置数据、函数返回多个值、表示固定结构的数据(如坐标、RGB颜色)。
如果数据需要修改,继续问是否需要键值对存储(第二个黄色决策):如果需要通过键快速查找值,字典是最佳选择(绿色区域)。字典提供了平均O(1)的查找性能,是构建缓存、计数器、映射关系的理想选择。实际应用包括:词频统计、用户信息存储、配置管理等。
如果不需要键值对,问是否需要保持顺序(第三个黄色决策):如果需要保持元素插入顺序,使用列表(绿色区域)。列表是有序的、可变的,支持索引访问和快速追加。实际应用包括:序列数据处理、实现堆栈和队列、收集动态数据。
如果不需要保持顺序,问是否需要唯一值(第四个黄色决策):如果需要确保元素唯一,使用集合(绿色区域)。集合自动去重,提供高效的成员测试和集合运算。实际应用包括:去重、检查共同元素、权限管理等。
最右侧列出了各数据结构的典型应用场景(粉色区域):元组用于配置和函数返回值,字典用于缓存和映射,列表用于序列数据,集合用于去重和成员测试。
理解这个决策树,就能在实际编程中快速选择最合适的数据结构,提高代码效率和可读性。
七、实战案例:文本数据分析工具
7.1 需求分析
构建一个文本数据分析工具,能够:
- 统计文本的基本信息(字数、词数、段落数)
- 分析词频
- 查找特定模式
- 生成分析报告
7.2 完整实现
import re
from collections import Counter
class TextAnalyzer:
"""文本数据分析工具"""
def __init__(self, text):
"""初始化分析器"""
self.text = text
self.words = self._extract_words()
self.paragraphs = self._extract_paragraphs()
def _extract_words(self):
"""提取单词"""
# 移除标点,分割单词
cleaned = re.sub(r'[^\w\s]', ' ', self.text.lower())
words = cleaned.split()
return words
def _extract_paragraphs(self):
"""提取段落"""
# 按空行分割段落
paragraphs = [p.strip() for p in self.text.split('\n\n') if p.strip()]
return paragraphs
def get_statistics(self):
"""获取基本统计信息"""
char_count = len(self.text)
word_count = len(self.words)
paragraph_count = len(self.paragraphs)
unique_words = len(set(self.words))
return {
'字符数': char_count,
'词数': word_count,
'段落数': paragraph_count,
'唯一词数': unique_words,
'平均词长': sum(len(w) for w in self.words) / word_count if word_count > 0 else 0
}
def get_word_frequency(self, top_n=10):
"""获取词频统计"""
counter = Counter(self.words)
return counter.most_common(top_n)
def find_patterns(self, pattern):
"""查找匹配的文本模式"""
matches = re.findall(pattern, self.text, re.IGNORECASE)
return matches
def generate_report(self):
"""生成分析报告"""
stats = self.get_statistics()
top_words = self.get_word_frequency()
report = f"""
========== 文本分析报告 ==========
字符数: {stats['字符数']}
词数: {stats['词数']}
段落数: {stats['段落数']}
唯一词数: {stats['唯一词数']}
平均词长: {stats['平均词长']:.2f}
========== 高频词汇 =========="""
for word, count in top_words:
report += f"\n{word}: {count}次"
report += "\n" + "=" * 35 + "\n"
return report
# 使用示例
sample_text = """
Python是一种高级编程语言,由Guido van Rossum于1991年首次发布。
Python的设计哲学强调代码的可读性和简洁的语法。
Python支持多种编程范式,包括面向对象、命令式、函数式和过程式编程。
Python在数据科学、机器学习、Web开发、自动化等领域得到广泛应用。
Python的简单易学使得它成为编程初学者的首选语言。
Python社区活跃,拥有丰富的第三方库和工具。
"""
# 创建分析器
analyzer = TextAnalyzer(sample_text)
# 生成报告
print(analyzer.generate_report())
# 查找特定模式
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
year_pattern = r'\b(19|20)\d{2}\b'
print("\n查找模式示例:")
print(f"邮箱: {analyzer.find_patterns(email_pattern)}")
print(f"年份: {analyzer.find_patterns(year_pattern)}")7.3 扩展功能
class AdvancedTextAnalyzer(TextAnalyzer):
"""扩展的文本分析工具"""
def get_sentences(self):
"""提取句子"""
# 按句号、问号、感叹号分割
sentences = re.split(r'[.!?]+', self.text)
return [s.strip() for s in sentences if s.strip()]
def find_longest_word(self):
"""查找最长的词"""
if not self.words:
return None
return max(self.words, key=len)
def calculate_readability(self):
"""计算可读性评分(简化版)"""
sentences = self.get_sentences()
if not sentences or not self.words:
return 0
avg_sentence_length = len(self.words) / len(sentences)
# 简化的可读性公式
readability = 100 - (1.0 * avg_sentence_length)
return max(0, min(100, readability))
def extract_numbers(self):
"""提取数字"""
numbers = re.findall(r'\d+\.?\d*', self.text)
return [float(n) if '.' in n else int(n) for n in numbers]
def find_palindromes(self):
"""查找回文词"""
palindromes = []
for word in set(self.words):
if len(word) > 2 and word == word[::-1]:
palindromes.append(word)
return palindromes
def generate_advanced_report(self):
"""生成高级分析报告"""
base_report = self.generate_report()
sentences = self.get_sentences()
longest_word = self.find_longest_word()
readability = self.calculate_readability()
numbers = self.extract_numbers()
palindromes = self.find_palindromes()
advanced_info = f"""
========== 高级分析 ==========
句子数: {len(sentences)}
最长单词: {longest_word} ({len(longest_word)}字符)
可读性评分: {readability:.1f}/100
数字: {numbers}
回文词: {palindromes}
"""
return base_report + advanced_info
# 使用扩展分析器
advanced_analyzer = AdvancedTextAnalyzer(sample_text)
print(advanced_analyzer.generate_advanced_report())下面的流程图展示了文本分析工具的完整工作流程:
flowchart TD A[开始: 输入文本] --> B[创建TextAnalyzer对象] B --> C[预处理文本] C --> D[提取单词列表] C --> E[提取段落列表] D --> F[统计基本指标] E --> F F --> G[计算字符数] F --> H[计算词数] F --> I[计算段落数] F --> J[计算唯一词数] F --> K[计算平均词长] G --> L[生成统计报告] H --> L I --> L J --> L K --> L L --> M{需要高级分析?} M -->|否| N[输出基础报告] M -->|是| O[创建AdvancedAnalyzer] O --> P[提取句子] O --> Q[查找最长词] O --> R[计算可读性] O --> S[提取数字] O --> T[查找回文词] P --> U[生成高级报告] Q --> U R --> U S --> U T --> U U --> V[输出完整报告] N --> W[结束] V --> W style A fill:#e1f5e1 style W fill:#ffe1e1 style M fill:#fff5e1 style L fill:#e1f5ff style U fill:#e1f5ff
图表讲解:这个流程图展示了文本分析工具从输入到输出的完整处理流程,包括基础分析和高级分析两个层次。
处理从输入文本开始(绿色区域):首先创建TextAnalyzer对象,传入待分析的文本。然后进入预处理阶段(蓝色区域):提取单词列表和段落列表。提取单词时使用正则表达式去除标点符号,将文本转换为小写后分割;提取段落时按空行分割,并去除每段的首尾空白。
预处理完成后,进入基础统计阶段(蓝色区域):使用预处理得到的数据计算各种指标。字符数是原始文本的长度;词数是单词列表的长度;段落数是段落列表的长度;唯一词数是将单词列表转为集合后的长度;平均词长是所有单词长度的平均值。这些统计结果被汇总成基础报告。
此时有一个决策点(黄色区域):询问是否需要高级分析。如果不需要,直接输出基础报告后结束(红色区域)。
如果需要高级分析,创建AdvancedAnalyzer对象,执行更多分析任务(紫色区域):提取句子(按标点符号分割)、查找最长词(通过max函数)、计算可读性(基于句子长度的简化公式)、提取数字(使用正则表达式)、查找回文词(正读反读相同的词)。这些额外的分析结果与基础报告合并,生成完整的分析报告(绿色区域)。
这个工具展示了如何结合字符串处理、正则表达式、列表、字典等多种Python技术,构建实用的数据处理工具。通过模块化设计和类继承,代码结构清晰,易于扩展和维护。
八、核心概念总结
| 概念 | 定义 | 应用场景 | 注意事项 |
|---|---|---|---|
| 列表 | 有序可变的元素集合 | 存储序列数据、动态数据 | 修改会影响原列表 |
| 字典 | 键值对的数据结构 | 缓存、映射、快速查找 | 键必须不可变 |
| 集合 | 唯一值的无序集合 | 去重、成员测试、集合运算 | 无序,不能索引访问 |
| 元组 | 不可变的有序序列 | 固定数据、多返回值 | 创建后不能修改 |
| 字符串 | 不可变的文本序列 | 文本处理、格式化输出 | 不可变,操作返回新字符串 |
| 正则表达式 | 文本模式匹配工具 | 复杂文本查找和替换 | 语法复杂,需要转义 |
| 列表推导式 | 简洁创建列表的方式 | 数据转换和过滤 | 过于复杂会降低可读性 |
常见问题解答
Q1:列表和元组有什么区别,什么时候应该使用哪一个?
答:列表和元组的主要区别在于可变性和性能特点。
列表是可变的,创建后可以添加、删除或修改元素。这种灵活性使得列表适合存储动态变化的数据,如收集用户输入、构建动态队列、需要频繁修改的数据集合。列表使用方括号[]创建,支持丰富的修改方法如append、insert、remove等。
元组是不可变的,创建后不能修改。这种不可变性带来了几个好处:更安全(不会意外修改)、可以作为字典的键、性能稍好(内存占用更小)。元组使用圆括号()创建,或用逗号分隔的值创建。
选择建议:如果数据需要修改,使用列表;如果数据不应修改,使用元组。元组特别适合表示固定结构的数据,如坐标(x, y)、RGB颜色(r, g, b)、函数返回多个值等。当数据作为字典的键或集合的元素时,必须使用不可变类型,这时元组是唯一选择(如果列表需要作为键,可以先转为元组)。
Q2:如何高效地从列表中删除元素?
答:删除列表元素有多种方法,效率取决于具体情况。
对于已知索引位置,使用del语句或pop()方法效率最高。del list[index]直接删除指定位置的元素,list.pop(index)删除并返回被删除的元素。删除列表末尾元素是O(1)操作,但删除中间或开头的元素是O(n)操作,因为需要移动后续元素。
对于已知值,使用remove()方法删除第一次出现的匹配值。但如果值不存在会抛出ValueError,需要先检查或使用异常处理。remove()也是O(n)操作,因为需要查找。
对于批量删除,有几种高效方法:列表推导式创建新列表[x for x in old_list if condition]、使用filter()函数、就地删除时使用从后向前的循环(避免索引错位)。
# 方法1: 列表推导式(推荐)
filtered = [x for x in items if x not in to_remove]
# 方法2: filter函数
filtered = list(filter(lambda x: x not in to_remove, items))
# 方法3: 就地删除(从后向前)
for i in range(len(items)-1, -1, -1):
if items[i] in to_remove:
del items[i]对于大型列表,如果只需要处理部分元素,考虑使用生成器表达式(x for x in items if condition)避免创建临时列表,可以节省内存。
Q3:字典的键有什么限制?可以使用列表作为键吗?
答:字典的键必须是不可变类型,这是Python字典的基本要求。
可用的键类型包括:基本不可变类型如整数、浮点数、字符串、布尔值;元组(当且仅当元组本身包含的所有元素都不可变时);自定义对象(如果实现了__hash__方法且不可变)。
不能用作键的类型包括:列表、字典、集合等可变容器类型;其他可变对象。这是因为字典使用哈希表实现,键的哈希值必须在键的整个生命周期内保持不变。如果使用可变对象作为键,对象修改后哈希值会变化,导致无法找到原来的键值对。
如果需要将列表作为键,可以先将列表转换为元组:
# 错误:列表不能作为键
coordinates = {}
key = [1, 2] # 列表
# coordinates[key] = "值" # TypeError: unhashable type: 'list'
# 正确:转换为元组
key = tuple([1, 2]) # 元组
coordinates[key] = "值" # 可以
# 实际应用:网格坐标
grid = {}
position = (3, 4)
grid[position] = "玩家位置"另一个常见的解决方案是使用字符串作为键,将列表序列化为字符串表示,如使用str(list)或json.dumps(list)。但要注意序列化和反序列化的开销。
Q4:正则表达式中的贪婪匹配和非贪婪匹配有什么区别?
答:贪婪匹配和非贪婪匹配决定了量词(*、+、?、{})的匹配行为。
贪婪匹配(默认行为)会尽可能多地匹配字符。例如,模式a.*b匹配字符串"aabcb"时,.*会匹配"abc"(尽可能多的字符),然后整个模式匹配"aabcb"。贪婪匹配在某些情况下会导致问题,如从HTML中提取标签内容时可能匹配过多。
非贪婪匹配(也称为懒惰匹配)会尽可能少地匹配字符。在量词后加?使其变为非贪婪,如*?、+?、??、{n,m}?。例如,模式a.*?b匹配字符串"aabcb"时,.*?会匹配空字符串(尽可能少的字符),然后尝试匹配b,失败后回溯,最终匹配"aab"。
import re
text = "<div>内容1</div><div>内容2</div>"
# 贪婪匹配
greedy_pattern = r'<div>.*</div>'
greedy_match = re.search(greedy_pattern, text)
print(greedy_match.group()) # 匹配整个字符串
# 非贪婪匹配
lazy_pattern = r'<div>.*?</div>'
lazy_matches = re.findall(lazy_pattern, text)
print(lazy_matches) # ['<div>内容1</div>', '<div>内容2</div>']选择建议:大多数情况下,非贪婪匹配更符合预期,特别是在处理结构化文本(HTML、XML、JSON)时。但如果确定格式且想匹配最大的范围,贪婪匹配更合适。理解两者的区别有助于编写精确的正则表达式,避免过度匹配或匹配不足的问题。
Q5:如何处理大型文本文件而不消耗过多内存?
答:处理大型文件时,关键是避免一次性加载整个文件到内存。
对于逐行处理,使用文件对象的迭代器特性,逐行读取和处理:
def process_large_file(filepath):
"""逐行处理大文件"""
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
# 处理每一行
processed_line = line.strip().lower()
# 不存储整个文件,只处理当前行
yield processed_line对于需要分块读取的情况,使用read()方法指定大小:
def read_in_chunks(file, chunk_size=4096):
"""分块读取文件"""
while True:
chunk = file.read(chunk_size)
if not chunk:
break
yield chunk对于需要多次访问的数据,考虑使用数据库或磁盘缓存,而不是全部存储在内存中。Python的sqlite3模块提供了轻量级的嵌入式数据库,可以存储和查询大量数据。
使用生成器表达式代替列表推导式,避免创建临时列表:
# 列表推导式:创建完整列表
result = [process(x) for x in large_list] # 占用大量内存
# 生成器表达式:惰性求值
result = (process(x) for x in large_list) # 几乎不占内存
for item in result:
use(item)对于文本分析任务,可以边读边统计,而不是先存储再分析:
def count_words_in_large_file(filepath):
"""统计大文件中的词频"""
word_count = Counter()
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
words = line.strip().lower().split()
word_count.update(words)
return word_count这种”流式处理”的方式,无论文件多大,内存使用量都保持在可控范围内,是处理大数据集的关键技术。
总结
本文全面介绍了Python的核心数据结构和文本处理技术。我们学习了列表、字典、集合等数据结构的特性和操作,掌握了字符串处理的各种方法,理解了正则表达式的强大功能,探讨了数据结构选择的最佳实践。
数据结构是编程的基础,选择合适的数据结构可以大大提高程序效率和代码可读性。字符串处理和正则表达式是处理文本数据的利器,在数据清洗、日志分析、网页抓取等任务中不可或缺。通过灵活运用这些工具,可以构建强大的数据处理应用程序。
下篇预告
下一篇我们将深入探讨文件操作与数据存储,带你了解Python的文件读写机制、路径处理、数据序列化等核心技术。你将学会如何自动化处理文件系统,实现数据的持久化存储。