🔎大家好,我是ZTLJQ,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流

📝个人主页-ZTLJQ的主页

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​📣系列果你对这个系列感兴趣的话

专栏 - ​​​​​​Python从零到企业级应用:短时间成为市场抢手的程序员

✔说明⇢本人讲解主要包括Python爬虫、JS逆向、Python的企业级应用

如果你对这个系列感兴趣的话,可以关注订阅哟👋

为什么需要正则表达式?

想象一下,你需要完成以下任务:

  • 验证用户输入的电话号码格式是否正确。
  • 从一篇包含成百上千个单词的文章中,找出所有以“http”或“https”开头的网址链接。
  • 将一段文本中所有的日期格式从 MM/DD/YYYY 统一转换为 YYYY-MM-DD
  • 清理日志文件,移除其中所有的IP地址。

使用传统的字符串方法(如find()split()replace())来实现这些需求,代码会变得极其复杂和脆弱。例如,要找网址,你可能需要写一个循环,检查每个子串是否以“http”开头,然后找到下一个空格...

正则表达式(Regex) 提供了一种强大的“模式”语言。你可以用一个紧凑的字符串(称为“模式”)来描述你想要匹配的文本规则,然后让引擎去自动搜索、替换或分割文本。

Python通过内置的re模块提供了完整的正则表达式支持。


第一部分:核心概念与基本语法
1.1 基础元字符
元字符 含义 示例
. 匹配除换行符外的任意单个字符 a.c 匹配 "abc", "a2c", "a c"
^ 匹配字符串的开始 ^Hello 匹配 "Hello World" 的开头
$ 匹配字符串的结束 world$ 匹配 "Hello world" 的结尾
* 匹配前面的字符零次或多次 ab*c 匹配 "ac", "abc", "abbc", "abbbc"...
+ 匹配前面的字符一次或多次 ab+c 匹配 "abc", "abbc",但不匹配 "ac"
? 匹配前面的字符零次或一次 ab?c 匹配 "ac", "abc",但不匹配 "abbc"
{m,n} 匹配前面的字符m到n次 a{2,4} 匹配 "aa", "aaa", "aaaa"
\ 转义字符,将特殊字符变为普通字符 \. 匹配字面量的点号 "."
1.2 字符集 ([]) 与预定义字符类
  • 字符集 [...]: 匹配方括号内的任意一个字符。

    • [aeiou]: 匹配任何一个元音字母。
    • [a-z]: 匹配任何一个小写字母。
    • [0-9A-F]: 匹配任何十六进制数字。
    • [^0-9]^ 在开头表示否定,匹配任何非数字字符。
  • 预定义字符类 (更简洁):

    • \d: 等价于 [0-9],匹配任何数字。
    • \D: 等价于 [^0-9],匹配任何非数字。
    • \w: 匹配任何“单词字符”,等价于 [a-zA-Z0-9_]
    • \W: 匹配任何“非单词字符”。
    • \s: 匹配任何空白字符(空格、制表符\t、换行符\n等)。
    • \S: 匹配任何非空白字符。
1.3 分组 (()) 与捕获

圆括号()用于创建分组,可以对分组应用量词,并且可以“捕获”匹配到的文本以便后续使用。

import re

# 使用分组和量词
pattern = r"(ab)+" # 匹配一个或多个连续的 "ab"
text = "ababab cd ab"
match = re.search(pattern, text)
if match:
    print(f"找到: {match.group()}") # 找到: ababab
    # group(0) 或 group() 是整个匹配
    # group(1) 是第一个分组的内容
    # 注意: 这里只有一个分组 (ab),所以 group(1) 返回最后一次成功的捕获 "ab"

注意re.search() 只返回第一个匹配项。re.findall() 返回所有匹配项的列表。


第二部分:re 模块实战案例

我们将通过一系列实际案例来展示re模块的强大功能。

案例一:验证与匹配 (re.matchre.searchre.findall)
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("user@example.com")) # True
print(is_valid_email("invalid.email"))   # False

# ---- 从文本中提取所有邮箱 ----
text = """
Contact us at support@company.com or sales@company.org.
For jobs, email hr@company.io.
"""

# findall 返回所有匹配的字符串
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
print(emails) # ['support@company.com', 'sales@company.org', 'hr@company.io']

# ---- 查找所有以 http/https 开头的URL ----
web_text = "Visit https://www.python.org or http://example.com for more info."
urls = re.findall(r'https?://[^\s]+', web_text) # \S+ 匹配非空白字符直到遇到空白
print(urls) # ['https://www.python.org', 'http://example.com']

解析:

  • r'': 使用原始字符串(raw string),避免反斜杠被Python解释器转义。
  • https?? 表示s是可选的,因此能匹配"http"和"https"。
  • [^\s]+[^...] 表示否定字符集,[^\s] 匹配任何非空白字符,+ 表示一个或多个。
案例二:搜索与替换 (re.sub)

这是最常用的功能之一。

import re

# ---- 格式化日期:将 MM/DD/YYYY 转换为 YYYY-MM-DD ----
date_text = "The event is on 12/25/2023 and 01/01/2024."

# 使用分组捕获
# (\d{2}) 捕获月份,(\d{2}) 捕获日期,(\d{4}) 捕获年份
pattern = r'(\d{2})/(\d{2})/(\d{4})'
replacement = r'\3-\1-\2' # \3是年份,\1是月份,\2是日期

formatted_text = re.sub(pattern, replacement, date_text)
print(formatted_text) 
# The event is on 2023-12-25 and 2024-01-01.

# ---- 移除文本中的所有HTML标签 ----
html_text = "<p>This is <b>bold</b> and <i>italic</i> text.</p>"
# <[^>]+> 匹配一个左尖括号<,然后是非>的任意字符一个或多次,最后是一个右尖括号>
clean_text = re.sub(r'<[^>]+>', '', html_text)
print(clean_text) # This is bold and italic text.

# ---- 将多个连续空格替换为单个空格 ----
messy_text = "Too    many     spaces    here."
cleaned_spaces = re.sub(r'\s+', ' ', messy_text).strip()
print(cleaned_spaces) # Too many spaces here.

解析re.sub(pattern, replacement, string) 会将string中所有匹配pattern的部分替换为replacement。在replacement中,可以用\1\2...来引用前面分组捕获的内容。

案例三:分割字符串 (re.split)

str.split()更强大,可以使用复杂的模式作为分隔符。

import re

# ---- 使用多种分隔符分割 ----
text = "apple,banana;cherry|date"
# [,;|] 是一个字符集,匹配 , 或 ; 或 |
fruits = re.split(r'[,;|]', text)
print(fruits) # ['apple', 'banana', 'cherry', 'date']

# ---- 分割句子,保留标点符号 ----
sentence = "Hello world! How are you? I'm fine."
# (?<=[.!?]) 是一个“后行断言”,意思是“在 . 或 ! 或 ? 之后”
# 这样分割时,分隔符(标点)会被保留在前面的元素中
parts = re.split(r'(?<=[.!?])\s*', sentence)
print(parts) # ['Hello world!', "How are you?", "I'm fine."]

解析(?<=...) 是“正向后行断言”(positive lookbehind assertion)。它确保了分割只发生在句号、感叹号或问号之后的空白处,而不会吃掉这些标点符号。

第三部分:高级特性与技巧
3.1 编译模式 (re.compile)

如果你要重复使用同一个正则表达式,将其编译成一个Pattern对象可以提高效率。

import re

# 编译一个模式,用于匹配有效的IPv4地址段(简化版)
ip_pattern = re.compile(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b')

text_with_ips = "Server1: 192.168.1.1, Server2: 10.0.0.5, Invalid: 999.999.999.999"

# 多次使用编译后的模式
valid_ips = []
for ip in ip_pattern.findall(text_with_ips):
    # 进一步验证每个数字是否在0-255之间
    if all(0 <= int(part) <= 255 for part in ip.split('.')):
        valid_ips.append(ip)

print(valid_ips) # ['192.168.1.1', '10.0.0.5']

解析re.compile() 返回一个Pattern对象,它拥有search()findall()sub()等方法。

3.2 标志 (Flags)

可以改变正则表达式的匹配行为。

import re

text = "Hello\nWorld\nHELLO"

# ---- 忽略大小写 (re.IGNORECASE 或 re.I) ----
matches = re.findall(r'hello', text, re.IGNORECASE)
print(matches) # ['Hello', 'HELLO']

# ---- 让 ^ 和 $ 匹配每一行的开始和结束 (re.MULTILINE 或 re.M) ----
# 默认情况下,^ 和 $ 只匹配整个字符串的开始和结束
line_matches = re.findall(r'^\w+', text, re.MULTILINE) # 找出每行的第一个单词
print(line_matches) # ['Hello', 'World', 'HELLO']

# ---- 让 . 匹配包括换行符在内的所有字符 (re.DOTALL 或 re.S) ----
multiline_text = "Line 1\nLine 2\nLine 3"
# .* 默认不能跨行
single_line_match = re.search(r'Line 1.*Line 3', multiline_text) # None
# 使用 re.DOTALL
multi_line_match = re.search(r'Line 1.*Line 3', multiline_text, re.DOTALL)
if multi_line_match:
    print("Found across lines!") # Found across lines!
3.3 非贪婪匹配

默认情况下,*+?{m,n} 是“贪婪”的,它们会尽可能多地匹配字符。

import re

text = "<div>Content 1</div><div>Content 2</div>"

# 贪婪匹配:会匹配从第一个 <div> 到最后一个 </div> 的所有内容
greedy_pattern = r'<div>.*</div>'
greedy_match = re.search(greedy_pattern, text)
if greedy_match:
    print("Greedy:", greedy_match.group())
    # Greedy: <div>Content 1</div><div>Content 2</div>

# 非贪婪匹配:在量词后加 ?,会尽可能少地匹配
non_greedy_pattern = r'<div>.*?</div>'
non_greedy_matches = re.findall(non_greedy_pattern, text)
print("Non-greedy:", non_greedy_matches)
# Non-greedy: ['<div>Content 1</div>', '<div>Content 2</div>']

解析.*? 中的 ? 让 * 变成非贪婪(或“懒惰”)模式。它一旦找到第一个 </div> 就停止匹配,而不是继续寻找最后一个。

第四部分:最佳实践与陷阱
  1. 保持简单: 正则表达式很容易变得过于复杂而难以维护。如果一个简单的str方法就能解决,就不要用正则。
  2. 充分测试: 正则表达式是“写时容易,读时难”。务必用各种边界情况测试你的模式。
  3. 使用原始字符串: 总是在正则表达式前加上r,如r'\d+',避免反斜杠问题。
  4. 善用在线工具: 使用 regex101.com 或 pythex.org 等在线工具来编写和调试正则表达式,它们提供实时的匹配结果和解释。
  5. 性能考量: 复杂的正则表达式,尤其是涉及大量回溯(backtracking)的,可能会很慢。对于简单的查找,in 或 str.find() 通常更快。
  6. 不要过度依赖: 试图用一个正则表达式解析HTML或JSON通常是错误的选择。应使用专门的解析器(如BeautifulSoupjson模块)。
结语

正则表达式是一把双刃剑。它功能强大,能让你在几行代码内完成复杂的文本处理任务;但它也晦涩难懂,写出的“天书”会让未来的自己都头疼。

通过本篇博客的学习,你应该已经掌握了:

  • 正则表达式的核心语法和元字符。
  • 如何使用re模块进行搜索、匹配、替换和分割。
  • 编译模式、标志和非贪婪匹配等高级技巧。
  • 实际应用场景和最佳实践。

更多推荐