MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python正则限定范围和否定的运用

2021-10-121.8k 阅读

Python 正则限定范围和否定的运用

一、正则表达式基础回顾

在深入探讨 Python 正则表达式的限定范围和否定运用之前,我们先来简单回顾一下正则表达式的基本概念。正则表达式是一种用来匹配字符串的强大工具,它使用一种专门的语法来描述字符模式。在 Python 中,通过 re 模块来支持正则表达式的操作。

例如,简单的匹配模式:

import re

pattern = 'hello'
string = 'hello world'
match = re.search(pattern, string)
if match:
    print('匹配成功')

这里使用 re.search 函数在字符串 string 中搜索模式 pattern,如果找到则返回匹配对象,否则返回 None

二、限定范围的运用

(一)字符范围

  1. 方括号 [] 在正则表达式中,方括号 [] 用于定义一个字符集合,匹配其中的任意一个字符。例如,[abc] 表示匹配字符 abc 中的任意一个。
pattern = '[abc]'
string1 = 'a good day'
string2 = '123'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
  1. 范围表示 在方括号内,可以使用 - 来表示字符范围。比如 [a - z] 表示匹配任意一个小写英文字母,[0 - 9] 表示匹配任意一个数字。
pattern = '[a - z]'
string = 'Hello 123'
match = re.search(pattern, string)
if match:
    print('匹配到小写字母')
  1. 组合范围 可以组合多个范围和单个字符。例如 [a - zA - Z0 - 9_] 表示匹配任意一个字母(大小写)、数字或下划线,这在匹配变量名等场景中很有用。
pattern = '[a - zA - Z0 - 9_]'
string1 = 'var_name123'
string2 = '@#$'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')

(二)量词限定范围

  1. 基本量词
    • *:表示前面的字符或字符组出现 0 次或多次。例如,a* 表示匹配 0 个或多个 a
pattern = 'a*'
string1 = 'aaaa'
string2 = 'bc'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if match2:
    print('string2 匹配成功')
- **`+`**:表示前面的字符或字符组出现 1 次或多次。例如,`a+` 表示匹配 1 个或多个 `a`。
pattern = 'a+'
string1 = 'aaaa'
string2 = 'bc'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
- **`?`**:表示前面的字符或字符组出现 0 次或 1 次。例如,`a?` 表示匹配 0 个或 1 个 `a`。
pattern = 'a?'
string1 = 'a'
string2 = 'bc'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if match2:
    print('string2 匹配成功')
  1. 限定次数量词
    • {n}:表示前面的字符或字符组恰好出现 n 次。例如,a{3} 表示匹配恰好 3 个 a
pattern = 'a{3}'
string1 = 'aaa'
string2 = 'aa'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
- **`{n,}`**:表示前面的字符或字符组至少出现 `n` 次。例如,`a{3,}` 表示匹配至少 3 个 `a`。
pattern = 'a{3,}'
string1 = 'aaaa'
string2 = 'aa'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
- **`{n,m}`**:表示前面的字符或字符组出现次数在 `n` 到 `m` 次之间(包括 `n` 和 `m`)。例如,`a{2,4}` 表示匹配 2 到 4 个 `a`。
pattern = 'a{2,4}'
string1 = 'aaa'
string2 = 'a'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')

三、否定的运用

(一)字符集合的否定

  1. ^ 在方括号内 在方括号 [] 内,^ 表示否定。例如,[^abc] 表示匹配除了 abc 之外的任意一个字符。
pattern = '[^abc]'
string1 = 'd good day'
string2 = 'a bad day'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
  1. 结合范围的否定 比如 [^a - z] 表示匹配除了小写字母之外的任意字符。
pattern = '[^a - z]'
string1 = '123'
string2 = 'abc'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')

(二)环视断言(Lookaround Assertions)中的否定

  1. 否定前瞻(Negative Lookahead)
    • 语法(?!pattern),表示匹配后面不跟着 pattern 的位置。例如,a(?!b) 表示匹配 a,但前提是 a 后面不跟着 b
pattern = 'a(?!b)'
string1 = 'ac'
string2 = 'ab'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')
  1. 否定后顾(Negative Lookbehind)
    • 语法(?<!pattern),表示匹配前面不跟着 pattern 的位置。例如,(?<!b)a 表示匹配 a,但前提是 a 前面不跟着 b。在 Python 中,后顾断言要求 pattern 的长度必须是固定的。
pattern = '(?<!b)a'
string1 = 'ca'
string2 = 'ba'
match1 = re.search(pattern, string1)
match2 = re.search(pattern, string2)
if match1:
    print('string1 匹配成功')
if not match2:
    print('string2 匹配失败')

四、综合运用案例

(一)验证邮箱格式

  1. 简单邮箱格式匹配 一般邮箱格式为用户名@域名,用户名可以包含字母、数字、下划线,域名部分可以包含字母、数字和点。
import re


def validate_email(email):
    pattern = '^[a - zA - Z0 - 9_]+@[a - zA - Z0 - 9]+\.[a - zA - Z0 - 9]+$'
    return re.match(pattern, email) is not None


email1 = 'user123@example.com'
email2 = 'user@.com'
if validate_email(email1):
    print(email1, '格式正确')
if not validate_email(email2):
    print(email2, '格式错误')
  1. 增强邮箱格式匹配(考虑否定情况) 有时候我们可能需要排除一些特殊字符在用户名或域名中。比如用户名不能以数字开头,域名不能以点开头。
import re


def validate_email_advanced(email):
    pattern = '^(?![0 - 9])[a - zA - Z0 - 9_]+@(?![.])[a - zA - Z0 - 9]+\.[a - zA - Z0 - 9]+$'
    return re.match(pattern, email) is not None


email1 = 'user123@example.com'
email2 = '1user@example.com'
email3 = 'user@.example.com'
if validate_email_advanced(email1):
    print(email1, '格式正确')
if not validate_email_advanced(email2):
    print(email2, '格式错误')
if not validate_email_advanced(email3):
    print(email3, '格式错误')

(二)提取 HTML 标签内的文本(不包含特定标签)

假设我们有一段 HTML 代码,我们想提取所有 <p> 标签内的文本,但不包括 <script> 标签内的内容。

import re


html = """
<html>
<head>
<title>页面标题</title>
</head>
<body>
<p>这是一个段落。</p>
<script>
console.log('这是脚本内容');
</script>
<p>这是另一个段落。</p>
</body>
</html>
"""
pattern = '<p>(.*?)</p>(?![^<]*<script>)'
matches = re.findall(pattern, html, re.DOTALL)
for match in matches:
    print(match)

这里使用了否定前瞻 (?![^<]*<script>) 来确保匹配的 <p> 标签之后不会紧接着出现 <script> 标签,re.DOTALL 标志使得 . 可以匹配包括换行符在内的所有字符。

五、性能考虑

在使用正则表达式的限定范围和否定时,性能是一个需要考虑的因素。复杂的限定范围和否定条件可能会导致正则表达式引擎花费更多的时间来进行匹配。

  1. 量词的贪婪与非贪婪模式
    • 贪婪模式:默认情况下,量词(如 *+{n,} 等)是贪婪的,它们会尽可能多地匹配字符。例如,a.*b 在字符串 a123b456b 中会匹配 a123b456b
pattern = 'a.*b'
string = 'a123b456b'
match = re.search(pattern, string)
if match:
    print(match.group())
- **非贪婪模式**:在量词后面加上 `?` 可以使其变为非贪婪模式,它们会尽可能少地匹配字符。例如,`a.*?b` 在字符串 `a123b456b` 中会匹配 `a123b`。
pattern = 'a.*?b'
string = 'a123b456b'
match = re.search(pattern, string)
if match:
    print(match.group())
  1. 避免过度复杂的否定条件 复杂的否定环视断言(如多层嵌套的否定前瞻或后顾)可能会显著降低匹配效率。在可能的情况下,尝试简化否定条件,或者通过其他方式来达到相同的逻辑目的。

六、常见错误与调试

  1. 语法错误 在编写正则表达式时,常见的语法错误包括括号不匹配、特殊字符未转义等。例如,想要匹配 ( 字符,需要写成 \(,否则会被当作正则表达式的语法部分。
try:
    pattern = '('
    re.search(pattern, 'test')
except re.error as e:
    print('语法错误:', e)
  1. 匹配结果不符合预期 有时候正则表达式看似正确,但匹配结果却不符合预期。这可能是因为对限定范围或否定条件的理解有误。例如,在使用否定前瞻时,可能没有正确界定前瞻的范围。可以通过打印出匹配对象的详细信息(如 match.group()match.span())来调试。
pattern = 'a(?!b)c'
string = 'ac'
match = re.search(pattern, string)
if match:
    print('匹配成功,匹配内容:', match.group())
else:
    print('匹配失败')

通过仔细检查匹配结果和正则表达式的逻辑,可以逐步找出问题所在并进行修正。

七、与其他字符串处理方法的比较

  1. 与字符串内置方法比较 Python 字符串本身有一些内置方法,如 find()startswith()endswith() 等。这些方法适用于简单的字符串查找和匹配场景,性能通常比正则表达式要好,因为它们针对特定的简单操作进行了优化。例如,检查字符串是否以某个前缀开头,使用 startswith() 方法更简洁高效。
string = 'hello world'
if string.startswith('hello'):
    print('字符串以 hello 开头')

然而,对于复杂的模式匹配,如匹配复杂的邮箱格式、HTML 标签等,正则表达式具有更大的优势,能够描述更灵活的模式。 2. 与其他第三方库比较 除了 Python 内置的 re 模块,还有一些第三方库也提供字符串处理和模式匹配功能,如 regex 库。regex 库在某些方面扩展了标准 re 模块的功能,例如支持更复杂的正则表达式语法和更多的匹配模式。但在大多数情况下,re 模块已经能够满足日常开发中的正则表达式需求,并且与 Python 标准库集成得更好。

八、在不同应用场景中的选择

  1. 数据验证场景 在验证用户输入的数据格式时,如邮箱、电话号码、身份证号码等,正则表达式是非常合适的选择。通过精确地定义限定范围和否定条件,可以有效地验证数据是否符合预期格式。例如,验证中国的 11 位手机号码:
import re


def validate_phone(phone):
    pattern = '^1[3 - 9]\d{9}$'
    return re.match(pattern, phone) is not None


phone1 = '13800138000'
phone2 = '12345678901'
if validate_phone(phone1):
    print(phone1, '是有效的手机号码')
if not validate_phone(phone2):
    print(phone2, '不是有效的手机号码')
  1. 文本提取场景 当需要从文本中提取特定格式的内容时,正则表达式也很有用。比如从日志文件中提取特定格式的时间戳,或者从 HTML/XML 文件中提取特定标签内的文本。然而,如果文本结构非常规则,使用专门的解析库(如 BeautifulSoup 用于解析 HTML/XML)可能会更方便且高效。
  2. 文本替换场景 在进行文本替换时,如果替换规则比较简单,字符串的 replace() 方法就可以满足需求。但如果替换规则涉及复杂的模式匹配,正则表达式的 re.sub() 函数则更为强大。例如,将文本中的所有数字替换为 [NUMBER]
import re


text = '今天是 2023 年 10 月 5 日'
new_text = re.sub('\d+', '[NUMBER]', text)
print(new_text)

通过深入理解 Python 正则表达式的限定范围和否定运用,我们可以更加灵活和高效地处理字符串相关的任务,无论是在数据处理、文本分析还是应用开发等各个领域。同时,合理地运用这些技巧,并注意性能和调试问题,能够使我们的代码更加健壮和高效。在实际应用中,要根据具体的场景和需求,选择最合适的字符串处理方法,以达到最佳的效果。