文章目录
- 假设我们有一段杂乱的文本,需要提取其中所有的邮箱地址和日期。手动使用 split 或 find 会非常痛苦,而正则表达式则能轻松应对。 案例:从日志中提取关键信息 原始日志片段:[2023-10-27 10:05:23] ERROR: User 'user@example.com' failed to login from IP 192.168.1.1. 我们需要提取: 时间戳 2023-10-27 10:05:23 邮箱 user@example.com Python 代码实现: import re log_line = “[2023-10-27 10:05:23] ERROR: User ‘user@example.com’ failed to login from IP 192.168.1.1.” # 编译正则模式(预编译能提升性能) date_pattern = re.compile(r'[(d{4}-d{2}-d{2} d{2}:d{2}:d{2})]’) email_pattern = re.compile(r”([w.-]+@[w.-]+)”) # 查找匹配 date_match = date_pattern.search(log_line) email_match = email_pattern.search(log_line) if date_match: print(f”发现时间: {date_match.group(1)}”) # group(1) 获取括号内捕获的内容 if email_match: print(f”发现邮箱: {email_match.group(1)}”)
- 在处理 HTML 标签或成对符号时,正则的默认“贪婪模式”(Greedy)往往会匹配过多内容。例如 .* 会尽可能多地匹配字符。使用 ? 变为非贪婪模式(Lazy),可以精准匹配最近的一组字符。 贪婪:<div>.*</div> 匹配 <div>A</div><div>B</div> -> 整个字符串 非贪婪:<div>.*?</div> 匹配 <div>A</div><div>B</div> -> <div>A</div> 和 <div>B</div> (配合 findall) 掌握正则,意味着你拥有了从混乱中建立秩序的能力。
- 假设我们通过正则提取出了大量的 IP 地址,现在需要统计当天有多少独立访客。 低效的做法(使用列表): ip_list = [‘192.168.1.1’, ‘10.0.0.1’, ‘192.168.1.1’, ‘172.16.0.5’] unique_ips = [] for ip in ip_list: if ip not in unique_ips: # 每次都要遍历列表,效率极低 unique_ips.append(ip) 优雅且高效的做法(集合推导): ip_list = [‘192.168.1.1’, ‘10.0.0.1’, ‘192.168.1.1’, ‘172.16.0.5’] # 直接转换为集合,自动去重 unique_ips = {ip for ip in ip_list} print(unique_ips) # 输出: {‘10.0.0.1’, ‘172.16.0.5’, ‘192.168.1.1’} print(len(unique_ips)) # 输出: 3
- 我们还可以在推导式中加入条件判断,甚至结合正则表达式。 案例:提取特定域名的邮箱并去重 假设我们有一个包含各种邮箱的列表,只想保留 company.com 后缀的邮箱,并去除重复项。 emails = [‘admin@company.com’, ‘user@gmail.com’, ‘ceo@company.com’, ‘admin@company.com’] # 集合推导 + 条件过滤 company_emails = {email for email in emails if email.endswith(‘@company.com’)} print(company_emails) # 输出: {‘admin@company.com’, ‘ceo@company.com’} 集合推导不仅代码量少,而且执行速度通常比先转列表再转集合更快,因为它直接在 C 语言层面构建了集合结构。
- import re def clean_log_data(file_path): # 1. 使用 with 安全打开文件 with open(file_path, ‘r’, encoding=’utf-8′) as f: lines = f.readlines() # 定义正则:匹配包含 “ERROR” 的行,并提取其中的邮箱 # 这是一个复合模式:先匹配 ERROR,再贪婪匹配直到行尾,最后提取邮箱 error_email_pattern = re.compile(r”ERROR.*?([w.-]+@[w.-]+)”) # 2. 集合推导 + 正则提取 # 遍历每一行,如果匹配到模式,则提取邮箱,并直接存入集合 # 这里使用了集合推导的逻辑,但在循环内部处理复杂的正则匹配 # 或者我们可以先生成一个生成器,再传给 set() def extract_emails_generator(lines): for line in lines: match = error_email_pattern.search(line) if match: yield match.group(1) # 3. 生成集合并自动去重 # 将生成器传递给 set(),这是处理大数据流的最佳内存优化方式 bad_emails = set(extract_emails_generator(lines)) return bad_emails # 模拟数据写入 sample_data = “”” [INFO] System started [ERROR] Connection failed for user ‘bad.actor@spam.com’ [WARN] High memory usage [ERROR] Login failed for user ‘duplicate@spam.com’ [INFO] Task completed [ERROR] Timeout for user ‘bad.actor@spam.com’ “”” # 写入临时文件 with open(‘temp_log.txt’, ‘w’) as f: f.write(sample_data) # 执行清洗 result = clean_log_data(‘temp_log.txt’) print(f”发现 {len(result)} 个异常邮箱:”) for email in result: print(f” – {email}”)
- with 的嵌套与扩展:在实际工程中,我们可能需要读取多个文件。可以使用 contextlib.ExitStack 来管理动态数量的文件资源。 正则的预编译:我们在函数外部定义了 re.compile。如果这个函数被调用多次,预编译的正则对象会被复用,极大提升性能。 生成器与集合的结合:在代码中,我使用了 yield 生成器配合 set()。这是一种“惰性求值”的策略。如果日志文件有 10GB,直接 readlines() 可能会撑爆内存。更好的做法是逐行读取(for line in f),逐行正则匹配,将结果扔进集合。这样内存占用极低。 终极优化版(针对超大文件): def clean_large_log(file_path): pattern = re.compile(r”ERROR.*?([w.-]+@[w.-]+)”) bad_emails = set() with open(file_path, ‘r’, encoding=’utf-8′) as f: for line in f: # 逐行读取,内存友好 match = pattern.search(line) if match: bad_emails.add(match.group(1)) # 直接添加到集合 return bad_emails 在这个优化版中,我们完美融合了: with:确保文件句柄安全。 for line in f:流式读取,避免内存爆炸。 re:精准定位数据。 set:实时去重,保证数据唯一性。
目录
- 第一章:告别繁琐——with语句的优雅资源管理
- 第二章:精准捕获——正则表达式(Regex)的核心力量
- 常用场景与实战技巧
- 进阶技巧:贪婪与非贪婪
- 第三章:集合推导(Set Comprehension)——去重与查找的极速方案
- 场景:统计独立访客 IP
- 集合推导的高级用法
- 第四章:融会贯通——构建高效数据清洗管道
- 完整实战代码
- 代码解析与优化点
- 结语:组合的艺术
在 Python 编程的世界里,优雅和简洁是永恒的追求。当我们处理文件读写、网络连接或数据库会话时,资源管理往往是代码中容易出错且显得冗余的部分。这就是 with 语句(上下文管理器)登场的时刻。
很多初学者习惯这样写代码:
f = open('data.txt', 'r')
try:
content = f.read()
# 处理数据...
finally:
f.close()
虽然这段代码逻辑正确,但 try...finally 结构略显沉重。Python 的 with 语句将这种模式标准化,使其成为一行代码的艺术:
with open('data.txt', 'r') as f:
content = f.read()
# 处理数据...
# 文件在此处自动关闭,即使发生异常也会安全关闭
为什么with是数据处理的基石?
在数据清洗任务中,我们通常需要读取大量原始文本文件。使用 with 语句不仅仅是为了少写几行代码,更是为了确保资源的确定性释放。
想象一下,你正在处理一个包含 100 万行数据的巨型日志文件。如果在读取过程中内存溢出导致程序崩溃,或者因为逻辑错误提前 return,未关闭的文件句柄可能会导致操作系统资源泄漏。with 语句通过调用对象的 __enter__ 和 __exit__ 方法,构建了一个安全的执行环境,是编写健壮数据处理脚本的第一道防线。
此外,with 还支持同时管理多个资源,这在后续结合正则处理数据时非常有用:
# 同时读取原始数据和配置文件
with open('raw_logs.txt', 'r') as source, open('config.json', 'r') as config:
data = source.read()
settings = config.read()
如果说 with 语句提供了安全的容器,那么正则表达式就是我们从海量文本中提取价值的精密手术刀。在数据清洗中,正则表达式是处理非结构化文本(如日志、邮件、网页源码)的终极武器。
假设我们有一段杂乱的文本,需要提取其中所有的邮箱地址和日期。手动使用 split 或 find 会非常痛苦,而正则表达式则能轻松应对。
案例:从日志中提取关键信息
原始日志片段:[2023-10-27 10:05:23] ERROR: User 'user@example.com' failed to login from IP 192.168.1.1.
我们需要提取:
- 时间戳
2023-10-27 10:05:23 - 邮箱
user@example.com
Python 代码实现:
import re
log_line = "[2023-10-27 10:05:23] ERROR: User 'user@example.com' failed to login from IP 192.168.1.1."
# 编译正则模式(预编译能提升性能)
date_pattern = re.compile(r'[(d{4}-d{2}-d{2} d{2}:d{2}:d{2})]')
email_pattern = re.compile(r"([w.-]+@[w.-]+)")
# 查找匹配
date_match = date_pattern.search(log_line)
email_match = email_pattern.search(log_line)
if date_match:
print(f"发现时间: {date_match.group(1)}") # group(1) 获取括号内捕获的内容
if email_match:
print(f"发现邮箱: {email_match.group(1)}")
在处理 HTML 标签或成对符号时,正则的默认“贪婪模式”(Greedy)往往会匹配过多内容。例如 .* 会尽可能多地匹配字符。使用 ? 变为非贪婪模式(Lazy),可以精准匹配最近的一组字符。
- 贪婪:
<div>.*</div>匹配<div>A</div><div>B</div>-> 整个字符串 - 非贪婪:
<div>.*?</div>匹配<div>A</div><div>B</div>-><div>A</div>和<div>B</div>(配合findall)
掌握正则,意味着你拥有了从混乱中建立秩序的能力。
当我们成功从文本中提取出数据后,往往面临两个问题:
- 重复数据:同一条信息可能在不同行出现多次。
- 性能:如何快速判断某个元素是否存在?
这就是 Python 集合推导(Set Comprehension) 大显身手的地方。集合(Set)在 Python 中是基于哈希表实现的,其查找和去重的时间复杂度平均为 O(1),远快于列表(List)的 O(n)。
假设我们通过正则提取出了大量的 IP 地址,现在需要统计当天有多少独立访客。
低效的做法(使用列表):
ip_list = ['192.168.1.1', '10.0.0.1', '192.168.1.1', '172.16.0.5']
unique_ips = []
for ip in ip_list:
if ip not in unique_ips: # 每次都要遍历列表,效率极低
unique_ips.append(ip)
优雅且高效的做法(集合推导):
ip_list = ['192.168.1.1', '10.0.0.1', '192.168.1.1', '172.16.0.5']
# 直接转换为集合,自动去重
unique_ips = {ip for ip in ip_list}
print(unique_ips) # 输出: {'10.0.0.1', '172.16.0.5', '192.168.1.1'}
print(len(unique_ips)) # 输出: 3
我们还可以在推导式中加入条件判断,甚至结合正则表达式。
案例:提取特定域名的邮箱并去重
假设我们有一个包含各种邮箱的列表,只想保留 company.com 后缀的邮箱,并去除重复项。
emails = ['admin@company.com', 'user@gmail.com', 'ceo@company.com', 'admin@company.com']
# 集合推导 + 条件过滤
company_emails = {email for email in emails if email.endswith('@company.com')}
print(company_emails)
# 输出: {'admin@company.com', 'ceo@company.com'}
集合推导不仅代码量少,而且执行速度通常比先转列表再转集合更快,因为它直接在 C 语言层面构建了集合结构。
现在,我们将上述三个核心技术串联起来,构建一个完整的数据清洗管道。我们将模拟一个任务:从日志文件中提取所有错误的邮箱地址,清洗并去重。
import re
def clean_log_data(file_path):
# 1. 使用 with 安全打开文件
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 定义正则:匹配包含 "ERROR" 的行,并提取其中的邮箱
# 这是一个复合模式:先匹配 ERROR,再贪婪匹配直到行尾,最后提取邮箱
error_email_pattern = re.compile(r"ERROR.*?([w.-]+@[w.-]+)")
# 2. 集合推导 + 正则提取
# 遍历每一行,如果匹配到模式,则提取邮箱,并直接存入集合
# 这里使用了集合推导的逻辑,但在循环内部处理复杂的正则匹配
# 或者我们可以先生成一个生成器,再传给 set()
def extract_emails_generator(lines):
for line in lines:
match = error_email_pattern.search(line)
if match:
yield match.group(1)
# 3. 生成集合并自动去重
# 将生成器传递给 set(),这是处理大数据流的最佳内存优化方式
bad_emails = set(extract_emails_generator(lines))
return bad_emails
# 模拟数据写入
sample_data = """
[INFO] System started
[ERROR] Connection failed for user 'bad.actor@spam.com'
[WARN] High memory usage
[ERROR] Login failed for user 'duplicate@spam.com'
[INFO] Task completed
[ERROR] Timeout for user 'bad.actor@spam.com'
"""
# 写入临时文件
with open('temp_log.txt', 'w') as f:
f.write(sample_data)
# 执行清洗
result = clean_log_data('temp_log.txt')
print(f"发现 {len(result)} 个异常邮箱:")
for email in result:
print(f" - {email}")
with 的嵌套与扩展:在实际工程中,我们可能需要读取多个文件。可以使用 contextlib.ExitStack 来管理动态数量的文件资源。
- 正则的预编译:我们在函数外部定义了
re.compile。如果这个函数被调用多次,预编译的正则对象会被复用,极大提升性能。
- 生成器与集合的结合:在代码中,我使用了
yield 生成器配合 set()。这是一种“惰性求值”的策略。如果日志文件有 10GB,直接 readlines() 可能会撑爆内存。更好的做法是逐行读取(for line in f),逐行正则匹配,将结果扔进集合。这样内存占用极低。
with 的嵌套与扩展:在实际工程中,我们可能需要读取多个文件。可以使用 contextlib.ExitStack 来管理动态数量的文件资源。re.compile。如果这个函数被调用多次,预编译的正则对象会被复用,极大提升性能。yield 生成器配合 set()。这是一种“惰性求值”的策略。如果日志文件有 10GB,直接 readlines() 可能会撑爆内存。更好的做法是逐行读取(for line in f),逐行正则匹配,将结果扔进集合。这样内存占用极低。终极优化版(针对超大文件):
def clean_large_log(file_path):
pattern = re.compile(r"ERROR.*?([w.-]+@[w.-]+)")
bad_emails = set()
with open(file_path, 'r', encoding='utf-8') as f:
for line in f: # 逐行读取,内存友好
match = pattern.search(line)
if match:
bad_emails.add(match.group(1)) # 直接添加到集合
return bad_emails
在这个优化版中,我们完美融合了:
with:确保文件句柄安全。for line in f:流式读取,避免内存爆炸。re:精准定位数据。set:实时去重,保证数据唯一性。
Python 的强大之处不在于某个单一的函数,而在于不同特性之间的组合与化学反应。
with赋予了代码安全的边界;- 正则表达式 赋予了代码穿透文本的洞察力;
- 集合推导 赋予了代码高效的去重与存储能力。
当你熟练掌握这三者的配合,原本需要数百行 C++ 或 Java 代码才能完成的数据清洗任务,在 Python 中往往只需要寥寥数行。这正是 Python 作为“胶水语言”和数据处理首选语言的魅力所在。
到此这篇关于Python使用with语句高效实现数据清洗的文章就介绍到这了,更多相关Python数据清洗内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!
您可能感兴趣的文章:
- Python批量清洗Excel数据的操作指南(去重+补缺失值+可视化)
- 使用Python实现处理和清洗CSV格式的数据文件
- Python数据分析必备的12种数据清洗技术分享
- Python中数据清洗与预处理技巧分享
- Python中数据清洗与处理的常用方法小结
- 一文详解Python中数据清洗与处理的常用方法
- python数据处理和数据清洗的示例详解