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

Python正则匹配字符串起始和结尾及单词边界

2024-04-054.8k 阅读

Python正则匹配字符串起始和结尾及单词边界

正则表达式基础回顾

在深入探讨Python中字符串起始、结尾及单词边界的正则匹配之前,先来回顾一下正则表达式的基础知识。正则表达式是一种描述字符串模式的工具,在Python中,通过re模块来使用正则表达式。

例如,简单的字符匹配,假设我们要匹配字符串中的字母a,在Python中可以这样写:

import re

text = "apple"
match = re.search('a', text)
if match:
    print("匹配到了")

这里re.search函数在字符串text中搜索模式'a',如果找到就返回一个匹配对象,否则返回None

匹配字符串起始和结尾

^ 匹配字符串起始位置

在正则表达式中,^符号用于匹配字符串的起始位置。比如,我们只想匹配以特定字符开头的字符串。假设我们要匹配以"hello"开头的字符串:

import re

text1 = "hello world"
text2 = "world hello"

match1 = re.search('^hello', text1)
match2 = re.search('^hello', text2)

if match1:
    print(f"在 {text1} 中匹配到以 'hello' 开头的字符串")
if not match2:
    print(f"在 {text2} 中未匹配到以 'hello' 开头的字符串")

在上述代码中,re.search('^hello', text1)能够匹配成功,因为text1是以"hello"开头的;而re.search('^hello', text2)则匹配失败,因为text2不是以"hello"开头。

如果我们有更复杂的需求,比如匹配以数字开头,后面跟着任意字符的字符串。可以这样写:

import re

text3 = "123abc"
text4 = "abc123"

match3 = re.search('^\d', text3)
match4 = re.search('^\d', text4)

if match3:
    print(f"在 {text3} 中匹配到以数字开头的字符串")
if not match4:
    print(f"在 {text4} 中未匹配到以数字开头的字符串")

这里^\d表示匹配字符串起始位置的数字,\d是正则表达式中表示数字的字符类。

$ 匹配字符串结尾位置

^相对应,$符号用于匹配字符串的结尾位置。例如,我们要匹配以"world"结尾的字符串:

import re

text5 = "hello world"
text6 = "world hello"

match5 = re.search('world$', text5)
match6 = re.search('world$', text6)

if match5:
    print(f"在 {text5} 中匹配到以 'world' 结尾的字符串")
if not match6:
    print(f"在 {text6} 中未匹配到以 'world' 结尾的字符串")

在这个例子中,re.search('world$', text5)能够匹配成功,因为text5是以"world"结尾的;而re.search('world$', text6)匹配失败,因为text6不是以"world"结尾。

同样,如果要匹配以字母结尾的字符串,可以这样:

import re

text7 = "abc123"
text8 = "123abc"

match7 = re.search('[a-zA-Z]$', text7)
match8 = re.search('[a-zA-Z]$', text8)

if not match7:
    print(f"在 {text7} 中未匹配到以字母结尾的字符串")
if match8:
    print(f"在 {text8} 中匹配到以字母结尾的字符串")

这里[a-zA-Z]$表示匹配字符串结尾位置的字母,[a-zA-Z]是表示所有英文字母的字符类。

同时使用 ^ 和 $ 匹配完整字符串

当我们同时使用^$时,就可以精确匹配整个字符串。例如,要匹配字符串"python"且必须是完整的"python",不能是包含"python"的其他更长字符串:

import re

text9 = "python"
text10 = "python programming"

match9 = re.search('^python$', text9)
match10 = re.search('^python$', text10)

if match9:
    print(f"在 {text9} 中匹配到完整的 'python' 字符串")
if not match10:
    print(f"在 {text10} 中未匹配到完整的 'python' 字符串")

在这个例子中,re.search('^python$', text9)能够匹配成功,因为text9就是完整的"python"字符串;而re.search('^python$', text10)匹配失败,因为text10除了"python"还有其他字符。

单词边界匹配

\b 匹配单词边界

在正则表达式中,\b用于匹配单词边界。单词边界是指单词和非单词字符之间的位置,或者字符串的起始/结尾位置与单词之间的位置。例如,我们要匹配单词"python",但不希望匹配到包含"python"的更长单词,如"pythonic"

import re

text11 = "I love python"
text12 = "This is a pythonic approach"

match11 = re.search(r'\bpython\b', text11)
match12 = re.search(r'\bpython\b', text12)

if match11:
    print(f"在 {text11} 中匹配到单词 'python'")
if not match12:
    print(f"在 {text12} 中未匹配到单词 'python'")

这里需要注意在Python中,要使用r'\bpython\b'这种原始字符串的写法,因为\b在普通字符串中有特殊含义(表示退格),而在原始字符串中,\b就表示正则表达式中的单词边界。

再看一个更复杂的例子,匹配以单词"start"开头,以单词"end"结尾,中间包含任意字符的字符串:

import re

text13 = "start some words end"
text14 = "startwords end"

match13 = re.search(r'\bstart\b.*\bend\b', text13)
match14 = re.search(r'\bstart\b.*\bend\b', text14)

if match13:
    print(f"在 {text13} 中匹配到符合要求的字符串")
if not match14:
    print(f"在 {text14} 中未匹配到符合要求的字符串")

text13中,\bstart\b匹配单词"start"的边界,.*匹配任意字符(包括0个字符),\bend\b匹配单词"end"的边界,所以能够匹配成功;而在text14中,startwords中间没有单词边界,所以匹配失败。

\B 匹配非单词边界

\b相反,\B用于匹配非单词边界。例如,我们要匹配包含"python"但不是独立单词的情况,如"pythonic"

import re

text15 = "I love python"
text16 = "This is a pythonic approach"

match15 = re.search(r'\Bpython\B', text15)
match16 = re.search(r'\Bpython\B', text16)

if not match15:
    print(f"在 {text15} 中未匹配到非单词边界的 'python'")
if match16:
    print(f"在 {text16} 中匹配到非单词边界的 'python'")

text15中,"python"是独立单词,不存在非单词边界的"python",所以匹配失败;而在text16中,"pythonic"中的"python"处于非单词边界位置,所以能够匹配成功。

复杂场景下的应用

从文本中提取特定格式的行

假设我们有一个文本文件,每一行是不同的字符串,我们要提取以数字开头,以字母结尾,且中间包含至少一个单词"example"的行。代码如下:

import re


def extract_lines(file_path):
    result = []
    with open(file_path, 'r', encoding='utf - 8') as file:
        for line in file.readlines():
            if re.search('^\d.*\bexample\b.*[a-zA-Z]$', line):
                result.append(line.strip())
    return result


file_path = 'test.txt'
extracted_lines = extract_lines(file_path)
for line in extracted_lines:
    print(line)

在这个代码中,^\d匹配行首的数字,.*匹配中间的任意字符,\bexample\b确保"example"是独立单词,.*[a-zA-Z]$匹配以字母结尾。

验证邮箱格式

在验证邮箱格式时,单词边界和字符串起始结尾匹配也很有用。一个简单的邮箱格式匹配正则表达式可以写成:

import re


def validate_email(email):
    pattern = r'^[a-zA - Z0 - 9_.+-]+@[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+$'
    if re.search(pattern, email):
        return True
    return False


email1 = "test@example.com"
email2 = "test.example.com"

if validate_email(email1):
    print(f"{email1} 是有效的邮箱格式")
if not validate_email(email2):
    print(f"{email2} 不是有效的邮箱格式")

这里^[a-zA - Z0 - 9_.+-]+匹配邮箱用户名部分,@是固定字符,[a-zA - Z0 - 9 -]+匹配域名部分,\.[a-zA - Z0 - 9-.]+$匹配域名后缀部分,并且通过^$确保整个字符串是一个完整的邮箱格式。

性能考虑

在使用正则表达式进行字符串起始、结尾及单词边界匹配时,性能是一个需要考虑的因素。复杂的正则表达式,尤其是包含大量回溯(例如过多的.*与其他精确匹配组合)的表达式,可能会导致匹配速度变慢。

例如,在一个非常长的字符串中使用如下表达式:re.search('^.*\bexample\b.*$', long_text),由于.*会贪婪地匹配尽可能多的字符,然后再尝试匹配\bexample\b,如果匹配失败就会回溯,这在长字符串中会消耗大量时间。

为了提高性能,可以尽量避免不必要的贪婪匹配,使用非贪婪匹配符.*?,并且将精确匹配部分尽量提前。比如:re.search('^.*?\bexample\b.*$', long_text),这样.*?会尽可能少地匹配字符,先尝试匹配\bexample\b,减少回溯的可能性。

同时,如果需要在大量数据上进行重复的正则匹配操作,可以考虑使用re.compile函数将正则表达式编译成模式对象,然后多次使用该对象进行匹配,这样可以提高一定的性能。例如:

import re

pattern = re.compile('^.*?\bexample\b.*$')
text_list = ["text1", "text2 with example", "text3"]
for text in text_list:
    if pattern.search(text):
        print(f"在 {text} 中匹配到")

通过re.compile编译正则表达式后,Python会对其进行优化,在多次匹配时可以减少编译的开销。

实际应用案例

网页爬虫中的数据提取

在网页爬虫中,我们经常需要从HTML文本中提取特定信息。假设我们要从网页中提取所有链接,链接通常以<a href="开头,以"</a>结尾,并且链接地址在"之间。可以使用如下代码:

import re
import requests


def extract_links(url):
    response = requests.get(url)
    html_text = response.text
    pattern = r'<a href="(.*?)"'
    links = re.findall(pattern, html_text)
    return links


url = "http://example.com"
extracted_links = extract_links(url)
for link in extracted_links:
    print(link)

这里虽然没有直接使用字符串起始和结尾的严格匹配(因为HTML文本结构复杂),但r'<a href="(.*?)"'这种模式利用了类似匹配起始和限定中间内容的思路,(.*?)采用非贪婪匹配来获取href属性中的链接地址。

日志文件分析

在日志文件分析中,我们可能需要提取特定格式的日志记录。比如,日志记录格式为[时间] [级别] [消息],我们要提取所有[级别]"ERROR"的记录。代码如下:

import re


def extract_error_logs(log_file_path):
    result = []
    with open(log_file_path, 'r', encoding='utf - 8') as file:
        for line in file.readlines():
            if re.search(r'^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \[ERROR\]', line):
                result.append(line.strip())
    return result


log_file_path = 'app.log'
error_logs = extract_error_logs(log_file_path)
for log in error_logs:
    print(log)

这里^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\] \[ERROR\]通过匹配字符串起始位置,精确提取出以特定时间格式开头且级别为"ERROR"的日志记录。

总结常见错误及解决方法

正则表达式语法错误

这是最常见的错误之一。例如,在写字符类时遗漏了],如[a - z,这会导致语法错误。解决方法是仔细检查正则表达式的语法,确保每个字符类、分组等都有正确的起止符号。

匹配结果不符合预期

有时候我们写的正则表达式看似正确,但匹配结果却不是我们想要的。这可能是因为对^$\b等符号的理解不准确,或者是贪婪/非贪婪匹配的问题。例如,原本想匹配独立单词"test",写成了'test'而不是r'\btest\b',就可能匹配到包含"test"的更长单词。解决这类问题需要深入理解各个正则表达式符号的含义,并且通过在不同测试字符串上尝试来调试正则表达式。

性能问题导致程序卡顿

如前文提到的,复杂的正则表达式可能导致性能问题。解决方法是优化正则表达式,避免不必要的贪婪匹配,合理使用re.compile等。如果正则表达式过于复杂,可以考虑是否有其他更高效的算法来实现相同的功能。

通过深入理解和掌握Python中字符串起始、结尾及单词边界的正则匹配,我们能够更加灵活和高效地处理各种字符串处理任务,无论是在文本处理、数据验证还是网络爬虫等领域都能发挥重要作用。同时,要注意性能和常见错误的处理,以确保代码的健壮性和高效性。