第十三章: 高级数据处理 Python 提供了多种处理不同类型数据的工具和库,能够轻松处理结构化和非结构化数据。本章将深入探讨 Python 中常用的数据格式处理技术,包括 JSON、CSV、XML 和配置文件等。
13.1 JSON 处理 JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,也易于机器解析和生成。Python 通过内置的 json 模块提供了 JSON 的序列化和反序列化功能。
方法 描述 json.dump(obj, fp)将 Python 对象 obj 编码为 JSON 格式并写入文件 fp。 json.dumps(obj)将 Python 对象 obj 编码为 JSON 格式并返回字符串。 json.load(fp)从文件 fp 读取 JSON 数据并解码为 Python 对象。 json.loads(s)将字符串 s 解码为 Python 对象。
13.1.1 基本操作 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 import jsondata = { "name" : "张三" , "age" : 30 , "is_student" : False , "courses" : ["Python" , "数据分析" , "机器学习" ], "scores" : {"Python" : 95 , "数据分析" : 88 } } json_str = json.dumps(data, ensure_ascii=False , indent=4 ) print (json_str)with open ("data.json" , "w" , encoding="utf-8" ) as f: json.dump(data, f, ensure_ascii=False , indent=4 ) parsed_data = json.loads(json_str) print (parsed_data["name" ]) with open ("data.json" , "r" , encoding="utf-8" ) as f: loaded_data = json.load(f) print (loaded_data["scores" ]["Python" ])
13.1.2 重要参数说明 参数 说明 用法示例 ensure_ascii是否转义非 ASCII 字符,False 时保留原始字符 json.dumps(data, ensure_ascii=False)indent缩进格式,美化输出 json.dumps(data, indent=4)separators指定分隔符,用于紧凑输出 json.dumps(data, separators=(',', ':'))sort_keys是否按键排序 json.dumps(data, sort_keys=True)default指定序列化函数,处理不可序列化对象 json.dumps(obj, default=lambda o: o.__dict__)
13.1.3 自定义对象序列化 Python 的 json 模块默认无法直接序列化自定义类对象,但提供了多种方式解决:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import jsonclass Person : def __init__ (self, name, age ): self .name = name self .age = age def person_to_dict (person ): """将Person对象转换为字典""" return { "name" : person.name, "age" : person.age } person = Person("李四" , 25 ) json_str = json.dumps(person, default=person_to_dict, ensure_ascii=False ) print (json_str) class PersonEncoder (json.JSONEncoder): """自定义JSON编码器处理Person类""" def default (self, obj ): if isinstance (obj, Person): return {"name" : obj.name, "age" : obj.age} return super ().default(obj) json_str = json.dumps(person, cls=PersonEncoder, ensure_ascii=False ) print (json_str) class Student : def __init__ (self, name, grade ): self .name = name self .grade = grade def __repr__ (self ): return f"Student('{self.name} ', {self.grade} )" def to_json (self ): """返回可JSON序列化的字典""" return { "name" : self .name, "grade" : self .grade } students = [Student("小明" , 90 ), Student("小红" , 88 )] json_str = json.dumps([s.to_json() for s in students], ensure_ascii=False ) print (json_str)
13.1.4 JSON 解码为自定义对象 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 40 import jsonfrom typing import Dict class Person : def __init__ (self, name: str , age: int ): self .name = name self .age = age def __str__ (self ): return f"{self.name} ({self.age} )" def dict_to_person (data: Dict ) -> Person: return Person(data["name" ], data["age" ]) person_data = '{"name": "Alice", "age": 25}' person = json.loads(person_data, object_hook=dict_to_person) print (type (person)) print ([person.name, person.age]) print (person)
13.1.5 处理复杂 JSON 数据 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 nested_json = ''' { "company": "ABC Corp", "employees": [ {"name": "张三", "department": "技术", "skills": ["Python", "Java"]}, {"name": "李四", "department": "市场", "skills": ["营销", "策划"]} ], "locations": { "headquarters": "北京", "branches": ["上海", "广州", "深圳"] } } ''' data = json.loads(nested_json) print (data["employees" ][0 ]["name" ]) print (data["employees" ][0 ]["skills" ][0 ]) print (data["locations" ]["branches" ][1 ]) data["employees" ][0 ]["skills" ].append("C++" ) data["locations" ]["branches" ].append("成都" ) updated_json = json.dumps(data, ensure_ascii=False , indent=2 ) print (updated_json)
13.1.6 性能优化 处理大型 JSON 文件时,可以使用流式解析来提高性能:
1 2 3 4 5 6 7 8 import ijson with open ("large_file.json" , "rb" ) as f: for item in ijson.items(f, "items.item" ): print (item["id" ], item["name" ])
13.1.7 JSON Schema 验证 验证 JSON 数据是否符合预期格式:
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 from jsonschema import validate schema = { "type" : "object" , "properties" : { "name" : {"type" : "string" }, "age" : {"type" : "integer" , "minimum" : 0 }, "email" : {"type" : "string" , "format" : "email" } }, "required" : ["name" , "age" ] } valid_data = {"name" : "张三" , "age" : 30 , "email" : "zhangsan@example.com" } invalid_data = {"name" : "李四" , "age" : -5 } try : validate(instance=valid_data, schema=schema) print ("有效数据" ) except Exception as e: print (f"验证失败: {e} " ) try : validate(instance=invalid_data, schema=schema) print ("有效数据" ) except Exception as e: print (f"验证失败: {e} " )
13.2 CSV 处理 CSV (Comma-Separated Values) 是一种常见的表格数据格式。Python 的 csv 模块提供了读写 CSV 文件的功能,适用于处理电子表格和数据库导出数据。
在我们写入中文数据时,尽量将编码更换为 GBK 否则写入 CSV 会导致一些乱码问题
13.2.1 基本读写操作 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 import csvdata = [ ["姓名" , "年龄" , "城市" ], ["张三" , 30 , "北京" ], ["李四" , 25 , "上海" ], ["王五" , 28 , "广州" ] ] with open ("people.csv" , "w" , newline="" , encoding="gbk" ) as f: writer = csv.writer(f) writer.writerows(data) with open ("people_row.csv" , "w" , newline="" , encoding="gbk" ) as f: writer = csv.writer(f) for row in data: writer.writerow(row) with open ("people.csv" , "r" , encoding="gbk" ) as f: reader = csv.reader(f) for row in reader: print (row)
13.2.2 使用字典处理 CSV 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import csvdict_data = [ {"姓名" : "张三" , "年龄" : 30 , "城市" : "北京" }, {"姓名" : "李四" , "年龄" : 25 , "城市" : "上海" }, {"姓名" : "王五" , "年龄" : 28 , "城市" : "广州" } ] with open ("people_dict.csv" , "w" , newline="" , encoding="gbk" ) as f: fieldnames = ["姓名" , "年龄" , "城市" ] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(dict_data) with open ("people_dict.csv" , "r" , encoding="gbk" ) as f: reader = csv.DictReader(f) for row in reader: print (f"{row['姓名' ]} ({row['年龄' ]} 岁) 来自 {row['城市' ]} " )
13.2.3 CSV 方言与格式化选项 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 csv.register_dialect( 'tab_dialect' , delimiter='\t' , quotechar='"' , escapechar='\\' , doublequote=False , quoting=csv.QUOTE_MINIMAL ) with open ("tab_data.csv" , "w" , newline="" , encoding="utf-8" ) as f: writer = csv.writer(f, dialect='tab_dialect' ) writer.writerows(data) with open ("formatted.csv" , "w" , newline="" , encoding="utf-8" ) as f: writer = csv.writer( f, delimiter=',' , quotechar='"' , quoting=csv.QUOTE_NONNUMERIC, escapechar='\\' , lineterminator='\n' ) writer.writerows(data)
13.2.4 处理特殊情况 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 complex_data = [ ["产品" , "描述" , "价格" ], ["笔记本" , "14\" 高配, i7处理器" , 5999.99 ], ["手机" , "5.5\" 屏幕, 双卡双待" , 2999.50 ] ] with open ("complex.csv" , "w" , newline="" , encoding="utf-8" ) as f: writer = csv.writer(f, quoting=csv.QUOTE_ALL) writer.writerows(complex_data) with open ("complex.csv" , "r" , encoding="utf-8" ) as f: reader = csv.reader(f) next (reader) for row in reader: print (row) with open ("missing.csv" , "r" , encoding="utf-8" ) as f: reader = csv.reader(f) for row in reader: processed_row = [None if cell == '' else cell for cell in row] print (processed_row)
13.2.5 CSV 文件的高级操作 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 with open ("people.csv" , "r" , encoding="utf-8" ) as f: reader = csv.DictReader(f) filtered_data = [row for row in reader if int (row["年龄" ]) > 25 ] with open ("grades.csv" , "r" , encoding="utf-8" ) as f: reader = csv.DictReader(f) scores = [float (row["分数" ]) for row in reader] avg_score = sum (scores) / len (scores) print (f"平均分: {avg_score:.2 f} " ) import globdef merge_csv_files (file_pattern, output_file ): all_files = glob.glob(file_pattern) with open (output_file, "w" , newline="" , encoding="utf-8" ) as outfile: for i, filename in enumerate (all_files): with open (filename, "r" , encoding="utf-8" ) as infile: reader = csv.reader(infile) if i == 0 : for row in reader: csv.writer(outfile).writerow(row) else : next (reader, None ) for row in reader: csv.writer(outfile).writerow(row)
13.3 XML 处理 XML (eXtensible Markup Language) 是一种用于存储和传输数据的标记语言。Python 提供多种处理 XML 的方法,最常用的是 xml.etree.ElementTree 模块。
13.3.1 创建和写入 XML 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 import xml.etree.ElementTree as ETroot = ET.Element("data" ) items = ET.SubElement(root, "items" ) for i in range (1 , 4 ): item = ET.SubElement(items, "item" ) item.set ("id" , str (i)) item.text = f"第{i} 项" detail = ET.SubElement(item, "detail" ) detail.text = f"项目{i} 的详情" users = ET.SubElement(root, "users" ) user = ET.SubElement(users, "user" ) user.set ("name" , "张三" ) ET.SubElement(user, "age" ).text = "30" ET.SubElement(user, "city" ).text = "北京" user2 = ET.SubElement(users, "user" ) user2.set ("name" , "李四" ) ET.SubElement(user2, "age" ).text = "25" ET.SubElement(user2, "city" ).text = "上海" xml_str = ET.tostring(root, encoding="utf-8" ).decode("utf-8" ) print (xml_str)tree = ET.ElementTree(root) tree.write("data.xml" , encoding="utf-8" , xml_declaration=True )
13.3.2 解析和读取 XML 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 tree = ET.parse("data.xml" ) root = tree.getroot() xml_string = '<data><item id="1">测试</item></data>' root = ET.fromstring(xml_string) print (f"根元素标签: {root.tag} " )for child in root: print (f"子元素: {child.tag} , 属性: {child.attrib} " ) items = root.find("items" ) if items is not None : for item in items.findall("item" ): print (f"项目ID: {item.get('id' )} , 内容: {item.text} " ) detail = item.find("detail" ) if detail is not None : print (f" 详情: {detail.text} " ) users = root.findall(".//user" ) for user in users: print (f"用户: {user.get('name' )} " ) print (f" 年龄: {user.find('age' ).text} " ) print (f" 城市: {user.find('city' ).text} " ) beijing_users = root.findall(".//user[city='北京']" ) for user in beijing_users: print (f"北京用户: {user.get('name' )} " )
13.3.3 修改 XML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 user = root.find(".//user[@name='张三']" ) if user is not None : user.set ("status" , "active" ) age_elem = user.find("age" ) if age_elem is not None : age_elem.text = "31" ET.SubElement(user, "email" ).text = "zhangsan@example.com" users = root.find("users" ) if users is not None : for user in users.findall("user" ): if user.get("name" ) == "李四" : users.remove(user) break tree.write("updated_data.xml" , encoding="utf-8" , xml_declaration=True )
13.3.4 命名空间处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 root = ET.Element("data" , {"xmlns:dt" : "http://example.org/datatypes" }) item = ET.SubElement(root, "dt:item" ) item.set ("dt:type" , "special" ) item.text = "带命名空间的元素" ns_xml = ET.tostring(root, encoding="utf-8" ).decode("utf-8" ) print (ns_xml)ns_root = ET.fromstring(ns_xml) namespaces = {"dt" : "http://example.org/datatypes" } ns_items = ns_root.findall(".//dt:item" , namespaces) for item in ns_items: print (f"找到命名空间元素: {item.text} " ) print (f"类型属性: {item.get('{http://example.org/datatypes}type' )} " )
13.4 配置文件处理 配置文件是应用程序保存设置和首选项的常用方式。Python 提供了多种处理不同格式配置文件的方法。
13.4.1 INI 配置文件处理 INI 文件是一种结构简单的配置文件格式,Python 通过 configparser 模块提供支持。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import configparserconfig = configparser.ConfigParser() config["DEFAULT" ] = { "language" : "中文" , "theme" : "默认" , "auto_save" : "true" , "save_interval" : "10" } config["应用设置" ] = {} config["应用设置" ]["font_size" ] = "14" config["用户信息" ] = {} user_info = config["用户信息" ] user_info["username" ] = "张三" user_info["email" ] = "zhangsan@example.com" user_info["remember_password" ] = "false" config["数据库" ] = {} config["数据库" ]["host" ] = "localhost" config["数据库" ]["port" ] = "3306" config["数据库" ]["username" ] = "root" config["数据库" ]["password" ] = "123456" with open ("config.ini" , "w" , encoding="utf-8" ) as f: config.write(f) config = configparser.ConfigParser() config.read("config.ini" , encoding="utf-8" ) print ("所有配置节:" , config.sections()) print ("用户信息节中的所有键:" , list (config["用户信息" ].keys()))print ("用户名:" , config["用户信息" ]["username" ]) print ("默认语言:" , config.get("应用设置" , "language" )) font_size = config.getint("应用设置" , "font_size" ) auto_save = config.getboolean("DEFAULT" , "auto_save" , fallback=True ) save_interval = config.getint("DEFAULT" , "save_interval" ) print (f"字体大小: {font_size} , 类型: {type (font_size)} " ) print (f"自动保存: {auto_save} , 类型: {type (auto_save)} " ) config["用户信息" ]["username" ] = "李四" if "日志设置" not in config: config["日志设置" ] = {} config["日志设置" ]["log_level" ] = "INFO" config["日志设置" ]["log_file" ] = "app.log" config["日志设置" ]["max_size" ] = "10MB" with open ("updated_config.ini" , "w" , encoding="utf-8" ) as f: config.write(f)
13.4.2 YAML 配置文件处理 YAML 是一种人类友好的数据序列化格式,需要安装 PyYAML 库。
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 40 41 42 43 44 import yamldata = { "server" : { "host" : "example.com" , "port" : 8080 }, "database" : { "host" : "localhost" , "port" : 5432 , "username" : "admin" , "password" : "secret" }, "logging" : { "level" : "INFO" , "file" : "/var/log/app.log" }, "users" : [ {"name" : "张三" , "role" : "admin" }, {"name" : "李四" , "role" : "user" } ] } with open ("config.yaml" , "w" , encoding="utf-8" ) as f: yaml.dump(data, f, default_flow_style=False , allow_unicode=True ) with open ("config.yaml" , "r" , encoding="utf-8" ) as f: config = yaml.safe_load(f) print (f"服务器地址: {config['server' ]['host' ]} " ) print (f"第一个用户: {config['users' ][0 ]['name' ]} " ) config["server" ]["port" ] = 9090 config["users" ].append({"name" : "王五" , "role" : "user" }) with open ("updated_config.yaml" , "w" , encoding="utf-8" ) as f: yaml.dump(config, f, default_flow_style=False , allow_unicode=True )
13.4.3 使用环境变量作为配置 环境变量是一种灵活的配置方式,尤其适用于容器化应用。
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 import osfrom dotenv import load_dotenv load_dotenv() database_url = os.environ.get("DATABASE_URL" , "sqlite:///default.db" ) debug_mode = os.environ.get("DEBUG" , "False" ).lower() in ("true" , "1" , "yes" ) port = int (os.environ.get("PORT" , "8000" )) print (f"数据库URL: {database_url} " )print (f"调试模式: {debug_mode} " )print (f"端口: {port} " )env_content = """ # 数据库设置 DATABASE_URL=postgresql://user:pass@localhost/dbname # 应用设置 DEBUG=True PORT=5000 """ with open (".env.example" , "w" ) as f: f.write(env_content)
13.4.4 JSON 作为配置文件 JSON 也是一种常用的配置文件格式,尤其适合需要与 Web 应用共享配置的场景。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import jsonimport osdefault_config = { "app_name" : "MyApp" , "version" : "1.0.0" , "debug" : False , "database" : { "host" : "localhost" , "port" : 5432 , "name" : "app_db" }, "cache" : { "enabled" : True , "ttl" : 3600 } } config_path = "app_config.json" def load_config (): if os.path.exists(config_path): with open (config_path, "r" , encoding="utf-8" ) as f: return json.load(f) else : save_config(default_config) return default_config def save_config (config ): with open (config_path, "w" , encoding="utf-8" ) as f: json.dump(config, f, indent=4 , ensure_ascii=False ) def update_config (key, value ): config = load_config() if "." in key: parts = key.split("." ) current = config for part in parts[:-1 ]: if part not in current: current[part] = {} current = current[part] current[parts[-1 ]] = value else : config[key] = value save_config(config) return config config = load_config() print (f"应用名称: {config['app_name' ]} " )print (f"数据库主机: {config['database' ]['host' ]} " )update_config("database.host" , "db.example.com" ) update_config("cache.ttl" , 7200 ) config = load_config() print (f"更新后的数据库主机: {config['database' ]['host' ]} " )print (f"更新后的缓存TTL: {config['cache' ]['ttl' ]} " )
13.5 正则表达式 正则表达式 (通常缩写为 regex 或 regexp)是一种强大的文本处理工具。它使用一种专门的语法来定义 搜索模式 (pattern) ,然后可以用这个模式在文本中进行查找、匹配、提取或替换操作。正则表达式在各种编程任务中都极为有用,例如:
数据验证 : 检查用户输入是否符合特定格式(如邮箱、手机号、日期)。数据提取 : 从大量非结构化文本(如日志文件、网页内容)中精确地抽取所需信息(如 IP 地址、错误代码、特定标签内容)。文本替换 : 对文本进行复杂的查找和替换操作,例如格式化代码、屏蔽敏感信息。文本分割 : 根据复杂的模式分割字符串。Python 通过内置的 re 模块提供了对正则表达式的全面支持。
核心概念 : 正则表达式的核心在于使用 元字符 (metacharacters) 和普通字符组合来定义模式。元字符是具有特殊含义的字符,而普通字符则匹配它们自身。
13.5.1 常用元字符和语法 以下是一些最常用的正则表达式元字符及其含义:
元字符 描述 示例模式 示例匹配 .匹配 除换行符 \n 之外 的任何单个字符 (使用 re.DOTALL 标志可匹配换行符)。 a.cabc, a_c, a&c (但不匹配 ac)^匹配字符串的 开头 。在多行模式 (re.MULTILINE) 下,也匹配每行的开头。 ^HelloHello world (但不匹配 Say Hello)$匹配字符串的 结尾 。在多行模式 (re.MULTILINE) 下,也匹配每行的结尾。 world$Hello world (但不匹配 world say)*匹配前面的元素 零次或多次 (贪婪模式)。 go*dgd, god, good, goooood+匹配前面的元素 一次或多次 (贪婪模式)。 go+dgod, good, goooood (但不匹配 gd)?匹配前面的元素 零次或一次 (贪婪模式)。也用于将贪婪量词变为 非贪婪 (见后文)。 colou?rcolor, colour{n}匹配前面的元素 恰好 n 次 。 \d{3}123 (但不匹配 12 或 1234){n,}匹配前面的元素 至少 n 次 (贪婪模式)。 \d{2,}12, 123, 12345{n,m}匹配前面的元素 至少 n 次,但不超过 m 次 (贪婪模式)。 \d{2,4}12, 123, 1234 (但不匹配 1 或 12345)[]字符集 。匹配方括号中包含的 任意一个 字符。[abc]a 或 b 或 c[^...]否定字符集 。匹配 不在 方括号中包含的任何字符。[^0-9]任何非数字字符 \转义符 。用于转义元字符,使其匹配其字面含义 (如 \. 匹配句点 .),或用于引入特殊序列 (如 \d)。\$$ 字符本身` ` 或 (OR) 运算符。匹配 `` 左边或右边的表达式。 ()分组 。将括号内的表达式视为一个整体,用于应用量词、限制 `` 的范围,或 捕获 匹配的子字符串。 (ab)+
踩坑提示 :
转义 : 当需要匹配元字符本身时(如 .、*、?),必须在前面加上反斜杠 \ 进行转义。例如,要匹配 IP 地址中的点,应使用 \.。原始字符串 (Raw Strings) : 在 Python 中定义正则表达式模式时,强烈建议 使用原始字符串(在字符串前加 r),如 r"\d+"。这可以避免 Python 解释器对反斜杠进行自身的转义,从而简化正则表达式的书写,尤其是包含很多 \ 的模式。13.5.2 特殊序列 (预定义字符集) re 模块提供了一些方便的特殊序列来代表常见的字符集:
特殊序列 描述 等价于 示例 \d匹配任何 Unicode 数字 字符 (包括 [0-9] 和其他语言的数字)。 [0-9] (ASCII)1, 5\D匹配任何 非数字 字符。 [^0-9] (ASCII)a, _, \s匹配任何 Unicode 空白 字符 (包括 、\t、\n、\r、\f、\v 等)。 , \t\S匹配任何 非空白 字符。 a, 1, .\w匹配任何 Unicode 词语 字符 (字母、数字和下划线 _)。 [a-zA-Z0-9_] (ASCII)a, B, 5, _\W匹配任何 非词语 字符。 [^a-zA-Z0-9_](ASCII)!, , @\b匹配 词语边界 (word boundary)。这是一个零宽度断言,匹配词语字符 (\w) 和非词语字符 (\W) 之间,或词语字符和字符串开头/结尾之间的位置。 \bword\b\B匹配 非词语边界 。 \Bword\B
13.5.3 贪婪模式 vs. 非贪婪模式 默认情况下,量词 (*, +, ?, {n,}, {n,m}) 都是 贪婪 (Greedy) 的,它们会尽可能多地匹配字符。
场景 : 从 HTML 标签 <b>Bold Text</b> 中提取 <b>。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import retext = "<b>Bold Text</b> Regular Text <b>Another Bold</b>" greedy_pattern = r"<.*>" match_greedy = re.search(greedy_pattern, text) if match_greedy: print (f"贪婪匹配结果: {match_greedy.group(0 )} " ) non_greedy_pattern = r"<.*?>" match_non_greedy = re.search(non_greedy_pattern, text) if match_non_greedy: print (f"非贪婪匹配结果: {match_non_greedy.group(0 )} " ) all_matches_non_greedy = re.findall(non_greedy_pattern, text) print (f"所有非贪婪匹配: {all_matches_non_greedy} " )
何时使用非贪婪模式?
当需要匹配从某个开始标记到 最近的 结束标记之间的内容时,通常需要使用非贪婪量词 (*?, +?, ??, {n,}?, {n,m}?)。
13.5.4 分组与捕获 使用圆括号 () 可以将模式的一部分组合起来,形成一个 分组 (Group) 。分组有几个重要作用:
应用量词 : 将量词作用于整个分组,如 (abc)+ 匹配 abc, abcabc 等。限制 | 范围 : 如 gr(a|e)y 匹配 gray 或 grey。捕获内容 : 默认情况下,每个分组会 捕获 (Capture) 其匹配到的子字符串,以便后续引用或提取。场景 : 从 “Name: John Doe, Age: 30” 中提取姓名和年龄。
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 import retext = "Name: John Doe, Age: 30; Name: Jane Smith, Age: 25" pattern_capture = r"Name: (\w+\s+\w+), Age: (\d+)" matches = re.findall(pattern_capture, text) print (f"\n--- 使用 findall 提取分组 ---" )print (matches) print ("\n--- 使用 finditer 访问分组 ---" )for match_obj in re.finditer(pattern_capture, text): print (f"整个匹配: {match_obj.group(0 )} " ) print (f" 姓名 (组 1): {match_obj.group(1 )} " ) print (f" 年龄 (组 2): {match_obj.group(2 )} " ) print (f" 所有分组: {match_obj.groups()} " ) pattern_non_capture = r"Name: (?:\w+\s+\w+), Age: (\d+)" matches_nc = re.findall(pattern_non_capture, text) print (f"\n--- 使用非捕获组的 findall ---" )print (matches_nc)
反向引用 (Backreferences) : 可以在模式内部或替换字符串中使用 \1, \2, … 来引用前面捕获组匹配到的文本。
场景 : 查找重复的单词,如 “the the”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 text_repeat = "This is the the test sentence with repeated repeated words." pattern_repeat = r"\b(\w+)\s+\1\b" repeated_words = re.findall(pattern_repeat, text_repeat) print (f"\n--- 查找重复单词 ---" )print (f"找到的重复单词: {repeated_words} " ) corrected_text = re.sub(pattern_repeat, r"\1" , text_repeat) print (f"修正后的文本: {corrected_text} " )
13.5.5 re 模块核心函数 Python 的 re 模块提供了以下核心函数来执行正则表达式操作:
函数 描述 返回值 主要用途 re.match(p, s, flags=0)从字符串 s 的 开头 尝试匹配模式 p。 匹配成功返回 Match 对象,失败返回 None。 验证字符串是否以特定模式开始。 re.search(p, s, flags=0)在 整个 字符串 s 中搜索模式 p 的 第一个 匹配项。 匹配成功返回 Match 对象,失败返回 None。 在字符串中查找模式是否存在,并获取第一个匹配项的信息。 re.findall(p, s, flags=0)在字符串 s 中查找模式 p 的 所有非重叠 匹配项。 返回一个 列表 。如果模式无捕获组,列表元素是匹配的字符串;如果有捕获组,列表元素是包含各捕获组内容的元组。 提取字符串中所有符合模式的子串或捕获组内容。 re.finditer(p, s, flags=0)与 findall 类似,但返回一个 迭代器 (iterator) ,迭代器中的每个元素都是一个 Match 对象。 返回一个迭代器,每个元素是 Match 对象。 处理大量匹配结果时更 内存高效 ,因为不需要一次性存储所有结果。可以方便地访问每个匹配的详细信息(如位置)。 re.sub(p, repl, s, count=0, flags=0)在字符串 s 中查找模式 p 的所有匹配项,并用 repl 替换它们。repl 可以是字符串(支持 \g<name> 或 \1 等反向引用)或函数。count 指定最大替换次数。 返回替换后的 新字符串 。 执行查找和替换操作。repl 可以是函数,实现更复杂的替换逻辑。 re.split(p, s, maxsplit=0, flags=0)使用模式 p 作为分隔符来 分割 字符串 s。maxsplit 指定最大分割次数。 返回一个 列表 ,包含分割后的子字符串。如果模式中有捕获组,捕获的内容也会包含在列表中。 根据复杂的模式分割字符串。 re.compile(p, flags=0)编译 正则表达式模式 p 为一个 模式对象 (Pattern Object) 。返回一个 Pattern 对象。 当一个模式需要被 多次 使用时,预先编译可以 提高性能 。模式对象拥有与 re 模块函数同名的方法(如 pattern.search(s))。
代码示例 :
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import retext = "The quick brown fox jumps over the lazy dog. Phone: 123-456-7890. Email: test@example.com." pattern_start = r"The" match_result = re.match (pattern_start, text) if match_result: print (f"match(): 字符串以 '{pattern_start} ' 开头。匹配内容: '{match_result.group(0 )} '" ) else : print (f"match(): 字符串不以 '{pattern_start} ' 开头。" ) match_fail = re.match (r"quick" , text) print (f"match() 失败示例: {match_fail} " ) pattern_word = r"fox" search_result = re.search(pattern_word, text) if search_result: print (f"search(): 找到单词 '{pattern_word} '。 起始位置: {search_result.start()} , 结束位置: {search_result.end()} " ) else : print (f"search(): 未找到单词 '{pattern_word} '。" ) pattern_digits = r"\d+" all_digits = re.findall(pattern_digits, text) print (f"findall(): 找到的所有数字序列: {all_digits} " ) pattern_email = r"(\w+)@(\w+\.\w+)" email_parts = re.findall(pattern_email, text) print (f"findall() 捕获组: {email_parts} " ) pattern_words_o = r"\b\w*o\w*\b" print ("finditer(): 查找包含 'o' 的单词:" )for match in re.finditer(pattern_words_o, text, re.IGNORECASE): print (f" 找到: '{match .group(0 )} ' at position {match .span()} " ) pattern_phone = r"\d{3}-\d{3}-\d{4}" censored_text = re.sub(pattern_phone, "[REDACTED]" , text) print (f"sub() 替换电话号码: {censored_text} " )def mask_email (match_obj ): username = match_obj.group(1 ) domain = match_obj.group(2 ) return f"{username[0 ]} ***@{domain} " censored_email_text = re.sub(pattern_email, mask_email, text) print (f"sub() 使用函数替换邮箱: {censored_email_text} " )pattern_punct = r"[.,:;]\s*" parts = re.split(pattern_punct, text) print (f"split(): 按标点分割: {parts} " )compiled_pattern = re.compile (r"l\w*y" , re.IGNORECASE) match1 = compiled_pattern.search(text) if match1: print (f"compile() & search(): 找到 '{match1.group(0 )} '" ) match2 = compiled_pattern.findall("Actually, Lily is lovely." ) print (f"compile() & findall(): 找到 {match2} " )
13.5.6 Match 对象详解 当 re.match(), re.search() 或 re.finditer() 中的一项成功匹配时,它们会返回一个 Match 对象 。这个对象包含了关于匹配结果的详细信息。
Match 对象方法/属性 描述 示例 (假设 m = re.search(r"(\w+) (\d+)", "Order P123 45")) m.group(0) 或 m.group()返回整个匹配的字符串。 'P123 45'm.group(n)返回第 n 个捕获组匹配的字符串 (从 1 开始计数)。 m.group(1) 返回 'P123', m.group(2) 返回 '45'm.groups()返回一个包含所有捕获组匹配内容的 元组 。 ('P123', '45')m.groupdict()如果模式中使用了 命名捕获组 (?P<name>...),返回一个包含组名和匹配内容的字典。 (需要命名组,如下例) m.start([group])返回整个匹配或指定 group 的 起始索引 (包含)。 m.start() 返回 6, m.start(1) 返回 6, m.start(2) 返回 11m.end([group])返回整个匹配或指定 group 的 结束索引 (不包含)。 m.end() 返回 13, m.end(1) 返回 10, m.end(2) 返回 13m.span([group])返回一个包含 (start, end) 索引的 元组 。 m.span() 返回 (6, 13), m.span(1) 返回 (6, 10)m.string传递给 match() 或 search() 的原始字符串。 'Order P123 45'm.re匹配时使用的已编译的模式对象 (Pattern object)。
命名捕获组示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import retext = "Product ID: ABC-987, Quantity: 50" pattern_named = r"Product ID: (?P<product_id>[A-Z]+-\d+), Quantity: (?P<quantity>\d+)" match = re.search(pattern_named, text)if match : print ("\n--- 使用命名捕获组 ---" ) print (f"产品 ID: {match .group('product_id' )} " ) print (f"数量: {match .group('quantity' )} " ) print (f"捕获字典: {match .groupdict()} " )
13.5.7 正则表达式标志 (Flags) 标志可以修改正则表达式的匹配行为。可以在 re 函数的 flags 参数中指定,或在编译时指定。多个标志可以使用 | (按位或) 组合。
标志 简写 描述 re.IGNORECASEre.I进行 不区分大小写 的匹配。 re.MULTILINEre.M使 ^ 和 $ 匹配 每行的开头和结尾 ,而不仅仅是整个字符串的开头和结尾。 re.DOTALLre.S使元字符 . 能够匹配 包括换行符 \n 在内 的任何字符。 re.VERBOSEre.X详细模式 。允许在模式字符串中添加 空白和注释 以提高可读性,此时模式中的空白会被忽略,# 后到行尾的内容视为注释。re.ASCIIre.A使 \w, \W, \b, \B, \s, \S 只匹配 ASCII 字符,而不是完整的 Unicode 字符集 (Python 3 默认匹配 Unicode)。 re.UNICODE (默认)re.U使 \w, \W, \b, \B, \s, \S, \d, \D 匹配完整的 Unicode 字符集。这是 Python 3 的默认行为。
示例 :
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 import retext_multi = """first line second line THIRD line""" print (f"\n--- Flags 示例 ---" )print (f"re.I: {re.findall(r'line' , text_multi, re.IGNORECASE)} " ) print (f"re.M (^): {re.findall(r'^s.*' , text_multi, re.MULTILINE | re.IGNORECASE)} " ) print (f"re.M ($): {re.findall(r'line$' , text_multi, re.MULTILINE | re.IGNORECASE)} " ) text_dot = "Hello\nWorld" print (f"re.S (.): {re.search(r'Hello.World' , text_dot, re.DOTALL)} " ) print (f"No re.S (.): {re.search(r'Hello.World' , text_dot)} " ) pattern_verbose = r""" ^ # 匹配字符串开头 [\w\.\-]+ # 用户名部分 (字母、数字、下划线、点、连字符) @ # @ 符号 ([\w\-]+\.)+ # 域名部分 (允许子域名,如 mail.example.) [a-zA-Z]{2,7} # 顶级域名 (如 .com, .org) $ # 匹配字符串结尾 """ email = "test.user-1@sub.example.com" match_verbose = re.match (pattern_verbose, email, re.VERBOSE) print (f"re.X (VERBOSE): {'匹配成功' if match_verbose else '匹配失败' } " )
13.5.8 实际应用场景示例 场景 1: 验证中国大陆手机号 (简单示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import redef is_valid_china_mobile (phone_number: str ) -> bool : """简单验证中国大陆手机号码 (11位数字,常见号段)""" pattern = r"^(?:1[3-9])\d{9}$" if re.match (pattern, phone_number): return True else : return False print ("\n--- 手机号验证 ---" )print (f"13812345678: {is_valid_china_mobile('13812345678' )} " ) print (f"12012345678: {is_valid_china_mobile('12012345678' )} " ) print (f"1381234567: {is_valid_china_mobile('1381234567' )} " ) print (f"138123456789: {is_valid_china_mobile('138123456789' )} " )
注意 : 实际手机号验证可能需要更复杂的规则或查询号段数据库。
场景 2: 从 Apache/Nginx 日志中提取 IP 地址和请求路径
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 import relog_line = '192.168.1.101 - - [03/May/2025:17:20:01 +0900] "GET /index.html HTTP/1.1" 200 1542 "-" "Mozilla/5.0..."' pattern_log = r'^([\d\.]+) \s+-\s+-\s+ \[.*?\] \s+"(GET|POST|PUT|DELETE|HEAD)\s+([^\s"]+)\s+HTTP/[\d\.]+" .*' match = re.match (pattern_log, log_line)if match : ip_address = match .group(1 ) method = match .group(2 ) path = match .group(3 ) print ("\n--- 日志解析 ---" ) print (f"IP 地址: {ip_address} " ) print (f"请求方法: {method} " ) print (f"请求路径: {path} " ) else : print ("日志格式不匹配" )
场景 3: 将 Markdown 样式的链接 [text](url) 转换为 HTML <a> 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import remarkdown_text = "这是一个链接 [Google](https://www.google.com) 和另一个 [Python 官网](http://python.org) 的例子。" pattern_md_link = r'\[([^\]]+)\]\(([^\)]+)\)' html_text = re.sub(pattern_md_link, r'<a href="\2">\1</a>' , markdown_text) print ("\n--- Markdown 转 HTML 链接 ---" )print (f"原始 Markdown: {markdown_text} " )print (f"转换后 HTML: {html_text} " )