跳至主要内容

Python 正则表达式教程

通过本教程探索正则表达式的强大功能。您将使用 re 库进行模式匹配,学习贪婪与非贪婪匹配等更多内容!
· 14分钟 阅读

使用 AI 探索

在线运行和编辑此教程中的代码

运行代码
banner

正则表达式(Regular Expressions,常缩写为 regex)是一串用于检查给定文本(字符串)中是否存在某种模式的字符序列。如果您曾使用过搜索引擎、文字处理器和文本编辑器的“查找并替换”功能,那么您已经见过正则表达式的应用。它们在服务器端用于在注册时验证电子邮箱或密码的格式,也用于解析文本数据文件以查找、替换或删除某些字符串等。它们有助于处理文本数据,而这往往是涉及文本挖掘的数据科学项目的前提。

本教程将带您通过 Python 讲解正则表达式的重要概念。您将从导入 re(支持正则表达式的 Python 标准库)开始。随后,您将看到如何使用基本/普通字符进行匹配,然后是通配或特殊字符。接着,您将学习如何在正则表达式中使用重复。您还将学到如何在搜索中创建分组与命名分组,以便更方便地访问匹配结果。随后,您将熟悉贪婪与非贪婪匹配的概念。

内容看起来已经不少了,因此我们还附上了一张方便的总结表,用简短定义帮助您回顾到目前为止所学内容。记得查看!

本教程还涵盖了 re 库提供的一些非常有用的函数,例如:compile()、search()、findall()、用于查找与替换的 sub()、split() 等。此外,您还将了解可用于优化正则的编译标志。

最后还有一个案例研究——您可以把学到的知识用起来!那么,让我们开始“正则”吧……

Python 中的正则表达式

在 Python 中,正则表达式由 re 模块支持。这意味着如果您想在 Python 脚本中使用它们,需要通过 import 导入该模块:

import re

Python 的 re 库提供了多种函数,值得深入掌握。您将在本教程中近距离了解其中的一些。

基本模式:普通字符

在 Python 中,您可以通过普通字符轻松处理许多基本模式。普通字符是最简单的正则表达式。它们严格匹配自身,在正则表达式语法中没有特殊含义。

例如:'A'、'a'、'X'、'5'。

普通字符可用于执行简单的精确匹配:

pattern = r"Cookie"
sequence = "Cookie"
if re.match(pattern, sequence):
    print("Match!")
else: print("Not a match!")
Match!

正如上例所示,大多数字母和字符会匹配它们自身。

如果文本与模式匹配,match() 函数会返回一个匹配对象;否则返回 None。re 模块还包含其他多个函数,您将在后文中学习其中的一些。

目前,让我们聚焦于普通字符!

您是否注意到在模式 Cookie 的开头有个 r?
这叫作原始字符串字面量。它会改变字符串字面量的解释方式。此类字面量按其原样存储。

例如,在前缀为 r 的情况下,\ 就只是反斜杠,而不会被解释为转义序列。您会在特殊字符中看到它的意义。有时语法涉及反斜杠转义的字符,为避免这些字符被当作转义序列解释,您可以使用原始字符串前缀 r。

提示:本例中其实并不必需;不过为了一致性,养成使用它的习惯是个好主意。

通配符:特殊字符

特殊字符并不按其字面进行匹配,而是在正则表达式中具有特殊含义。简单理解,它们是“保留的元字符”,所表示的含义与字面不一致。

通过一些示例来看看特殊字符的实际效果……

在此之前,下方示例会用到两个函数:search() 与 group()。
使用search 函数,您可以扫描给定的字符串/序列,查找正则表达式首次产生匹配的位置。
group 函数返回由正则匹配到的字符串。稍后您会更详细地看到这两个函数。

现在回到特殊字符。

. —— 句点。匹配除换行符之外的任意单个字符。

re.search(r'Co.k.e', 'Cookie').group()
'Cookie'

^ —— 脱字符。匹配字符串的起始位置。

当您想确保文档/句子以某些字符开头时,这很有用。

re.search(r'^Eat', "Eat cake!").group()

## 不过,下面的代码不会得到同样的结果。您可以自己试试:
# re.search(r'^eat', "Let's eat cake!").group()
'Eat'

$ —— 匹配字符串的结尾。

当您想确保文档/句子以某些字符结尾时,这很有用。

re.search(r'cake$', "Cake! Let's eat cake").group()

## 下面的搜索将返回 NONE,试试:
# re.search(r'cake$', "Let's get some cake on our way home!").group()
'cake'

[abc] —— 匹配 a 或 b 或 c。
[a-zA-Z0-9] —— 匹配任意字母(a 到 z 或 A 到 Z)或数字(0 到 9)。

提示:可通过对集合取补来匹配“非此范围”的字符。如果集合的第一个字符是 ^,则会匹配所有不在集合中的字符。

re.search(r'[0-6]', 'Number: 5').group()
'5'
## 匹配除 5 之外的任意字符
re.search(r'Number: [^5]', 'Number: 0').group()

## 这不会匹配,因此会返回 NONE 值
#re.search(r'Number: [^5]', 'Number: 5').group()
'Number: 0'

\ —— 反斜杠。
也许是最灵活多变的元字符!

  • 如果反斜杠后面的字符是已识别的转义字符,则采用该转义字符的特殊含义(情形 1)。
  • 否则,如果 \ 后的字符不是已识别的转义字符,则该 \ 被视为普通字符,原样传递(情形 2)。
  • \ 可放在所有元字符前以移除其特殊含义(情形 3)。
##(情形 1)将 '\s' 视为转义字符,'\s' 表示空格
re.search(r'Not a\sregular character', 'Not a regular character').group()
'Not a regular character'
##(情形 2)由于 '\r' 不是已识别的转义字符,'\' 被视为普通字符
re.search(r'Just a \regular character', 'Just a \regular character').group()
'Just a \regular character'
##(情形 3)通过额外的 `\` 转义 '\s',使其被解释为字面字符串 '\s'
re.search(r'Just a \\sregular character', 'Just a \sregular character').group()
'Just a \\sregular character'

有一组以 '\' 开头的预定义特殊序列,在搜索与匹配时非常有用。我们来近距离看看其中的一些……

\w —— 小写 w。匹配任意单个字母、数字或下划线。
\W —— 大写 W。匹配不属于小写 \w 的任意字符。

print("Lowercase w:", re.search(r'Co\wk\we', 'Cookie').group())

## 匹配除单个字母、数字或下划线之外的任意字符
print("Uppercase W:", re.search(r'C\Wke', 'C@ke').group())

## 大写 W 不会匹配单个字母或数字
print("Uppercase W won't match, and return:", re.search(r'Co\Wk\We', 'Cookie'))
Lowercase w: Cookie
Uppercase W: C@ke
Uppercase W won't match, and return: None

\s —— 小写 s。匹配单个空白字符,如空格、换行、制表、回车。
\S —— 大写 S。匹配不属于小写 \s 的任意字符。

print("Lowercase s:", re.search(r'Eat\scake', 'Eat cake').group())
print("Uppercase S:", re.search(r'cook\Se', "Let's eat cookie").group())
Lowercase s: Eat cake
Uppercase S: cookie

\d —— 小写 d。匹配十进制数字 0-9。
\D —— 大写 D。匹配任何非十进制数字的字符。

# \d 的示例
print("How many cookies do you want? ", re.search(r'\d+', '100 cookies').group())
How many cookies do you want?  100

上例中用在 \d 后的 + 符号用于重复。您将在“重复”一节中更详细地看到它……

\t —— 小写 t。匹配制表符。
\n —— 小写 n。匹配换行。
\r —— 小写 r。匹配回车。
\A —— 大写 A。仅匹配字符串开头。跨多行同样有效。
\Z —— 大写 Z。仅匹配字符串结尾。
提示:^ 与 \A 在效果上等同,$ 与 \Z 也一样。除非在多行(MULTILINE)模式下。更多内容见标志一节。

\b —— 小写 b。仅匹配单词的开头或结尾。

# \t 的示例
print("\\t (TAB) example: ", re.search(r'Eat\tcake', 'Eat    cake').group())

# \b 的示例
print("\\b match gives: ",re.search(r'\b[A-E]ookie', 'Cookie').group())
\t (TAB) example:  Eat    cake
\b match gives:  Cookie

重复

当您要在序列中查找较长的模式时,手动匹配会很繁琐。幸运的是,re 模块可以通过以下特殊字符处理重复:

+ —— 检查前一个字符从该位置开始是否出现一次或多次。

re.search(r'Co+kie', 'Cooookie').group()
'Cooookie'

* —— 检查前一个字符从该位置开始是否出现零次或多次。

# 检查给定序列中 a 或 o 或两者的任意出现次数
re.search(r'Ca*o*kie', 'Cookie').group()
'Cookie'

? —— 检查前一个字符从该位置开始是否恰好出现零次或一次。

# 检查给定序列中 a 或 o 或两者恰好出现零次或一次
re.search(r'Colou?r', 'Color').group()
'Color'

但如果您想检查“恰好重复若干次”该怎么办?

例如,在应用中检查电话号码的有效性。re 模块也能很好地处理,使用以下正则表达式:

{x} —— 精确重复 x 次。
{x,} —— 至少重复 x 次或更多。
{x, y} —— 至少重复 x 次但不超过 y 次。

re.search(r'\d{9,10}', '0987654321').group()
'0987654321'

+ 与 * 限定符被称为 贪婪,您稍后会看到这是什么意思。

正则表达式中的分组

正则表达式的分组功能允许您提取匹配文本的某些部分。用括号 () 括起的正则表达式片段称为分组。括号并不会改变表达式的匹配内容,而是在匹配的序列中形成分组。您在本教程的示例中一直在使用 group() 函数。直接调用 match.group()(不带参数)仍将返回完整的匹配文本。

让我们通过一个简单示例理解该概念。想象您正在验证电子邮箱地址,并希望检查用户名与主机名。这时您就会希望在匹配文本中创建独立的分组。

statement = 'Please contact us at: [email protected]'
match = re.search(r'([\w\.-]+)@([\w\.-]+)', statement)
if statement:
  print("Email address:", match.group()) # 整个匹配文本
  print("Username:", match.group(1)) # 用户名(第 1 组)
  print("Host:", match.group(2)) # 主机(第 2 组)
Email address: support@datacamp.com
Username: support
Host: datacamp.com

另一种实现方式是使用 <> 括号来创建命名分组。命名分组会让您的代码更易读。创建命名分组的语法为:(?P<name>...)。将 name 替换为您想给分组起的名字。... 表示其余的匹配语法。使用上一示例来看看实际效果……

statement = 'Please contact us at: [email protected]'
match = re.search(r'(?P<email>(?P<username>[\w\.-]+)@(?P<host>[\w\.-]+))', statement)
if statement:
  print("Email address:", match.group('email'))
  print("Username:", match.group('username'))
  print("Host:", match.group('host'))
Email address: support@datacamp.com
Username: support
Host: datacamp.com

提示:您始终可以用编号而不是名称来访问命名分组。但随着分组数量增加,仅用编号会越来越难管理。因此,建议养成使用命名分组的习惯。

贪婪与非贪婪匹配

当特殊字符尽可能多地匹配搜索序列(字符串)时,称为“贪婪匹配”。这是正则表达式的默认行为,但有时并非我们想要的:

pattern = "cookie"
sequence = "Cake and cookie"

heading  = r'<h1>TITLE</h1>'
re.match(r'<.*>', heading).group()
'<h1>TITLE</h1>'

模式 <.*> 匹配了整个字符串,一直匹配到第二个 > 为止。

然而,如果您只想匹配第一个 <h1> 标签,可以使用修饰符 *?,它会尽可能少地匹配文本(非贪婪)。

在限定符后添加 ? 会使其以非贪婪或最小化方式执行匹配;也就是尽可能少地匹配字符。当您运行 <.*?> 时,只会匹配到 <h1>。

heading  = r'<h1>TITLE</h1>'
re.match(r'<.*?>', heading).group()
'<h1>'

总结表

到这里,您已经在正则表达式上走了很长一段路。信息量与概念都不少!下表汇总了本教程中目前为止您所见到的内容。如果暂时还不能完全理解所有元字符也别担心。随着时间与练习,您会体会到这些字符的独特之处,并学会何时用什么……

本教程未涵盖 Python 中所有的特殊序列。完整列表请查阅标准库参考。

字符作用
.句点。匹配除换行符外的任意单个字符。
^脱字符。匹配字符串开头的模式。
\A大写 A。仅匹配字符串开头。
$美元符。匹配字符串结尾。
\Z大写 Z。仅匹配字符串结尾。
[ ]匹配方括号内您指定的字符集合。
\∙ 如果反斜杠除后为已识别的转义字符,则采用其特殊含义。
∙ 否则反斜杠(\)被视为普通字符并原样通过。
∙ 可置于所有元字符前以移除其特殊含义。
\w小写 w。匹配任意单个字母、数字或下划线。
\W大写 W。匹配不属于 \w(小写 w)的任意字符。
\s小写 s。匹配单个空白字符,如空格、换行、制表、回车。
\S大写 S。匹配不属于 \s(小写 s)的任意字符。
\d小写 d。匹配十进制数字 0-9。
\D大写 D。匹配任何非十进制数字的字符。
\t小写 t。匹配制表符。
\n小写 n。匹配换行。
\r小写 r。匹配回车。
\b小写 b。仅匹配单词的开头或结尾。
+检查前一个字符是否出现一次或多次。
*检查前一个字符是否出现零次或多次。
?∙ 检查前一个字符是否恰好出现零次或一次。
∙ 指定 +、* 的非贪婪版本。
{ }检查明确的重复次数。
( )在匹配时创建分组。
< >在匹配时创建命名分组。

您已经拿下了正则的基础!不过,还有一些概念可以帮助您编写更出色的正则表达式来进行搜索与匹配。

提示:尽管正则表达式非常强大和有用,但要警惕冗长而晦涩的表达式——它们既难以让他人理解,也不利于您自己长期维护。

re 提供的函数

Python 的 re 库提供了多种函数,助您更轻松地完成任务。您已经见过其中一些,例如 re.search()、re.match()。我们继续看看更多……

compile(pattern, flags=0)

在 Python 中,正则表达式以字符串形式处理。然而,通过 compile(),您可以将正则表达式模式编译为正则表达式对象。当您在同一程序中多次使用某个表达式时,使用 compile() 保存生成的正则对象以复用,比将其保存为字符串更高效。因为最近传给 compile() 和模块级匹配函数的编译版本会被缓存。

pattern = re.compile(r"cookie")
sequence = "Cake and cookie"
pattern.search(sequence).group()
'cookie'
# 等价于:
re.search(pattern, sequence).group()
'cookie'

search(pattern, string, flags=0)

使用该函数,您可以扫描给定的字符串/序列,查找正则表达式首次产生匹配的位置。若找到则返回相应的匹配对象,否则若字符串中不存在任何与模式匹配的位置则返回 None。注意,None 与在字符串某处找到长度为零的匹配不同。

pattern = "cookie"
sequence = "Cake and cookie"

re.search(pattern, sequence)
<re.Match object; span=(9, 15), match='cookie'>

match(pattern, string, flags=0)

如果字符串开头的零个或多个字符与模式匹配,则返回相应的匹配对象;否则(即字符串与给定模式不匹配)返回 None。

pattern = "C"
sequence1 = "IceCream"
sequence2 = "Cake"

# 因为 "C" 不在 "IceCream" 的开头,所以不匹配
print("Sequence 1: ", re.match(pattern, sequence1))
print("Sequence 2: ", re.match(pattern,sequence2).group())
Sequence 1:  None
Sequence 2:  C

search() 与 match() 的区别

match() 函数仅检查字符串开头(默认)的匹配,而 search() 函数会在字符串任意位置查找匹配。

findall(pattern, string, flags=0)

在整个序列中查找所有可能的匹配,并将其以字符串列表的形式返回。每个返回的字符串代表一个匹配。

statement = "Please contact us at: [email protected], [email protected]"

#'addresses' 是存储所有可能匹配的列表
addresses = re.findall(r'[\w\.-]+@[\w\.-]+', statement)
for address in addresses:
    print(address)
support@datacamp.com
xyz@datacamp.com

finditer(string, [position, end_position])

与 findall() 类似——在整个序列中查找所有可能的匹配,但以迭代器形式返回正则匹配对象。

提示:finditer() 在您希望返回更多关于搜索的信息时是极佳选择。返回的正则匹配对象不仅包含匹配的序列,还包含其在原始文本中的位置。

statement = "Please contact us at: [email protected], [email protected]"

#'addresses' 是存储所有可能匹配的列表
addresses = re.finditer(r'[\w\.-]+@[\w\.-]+', statement)
for address in addresses:
    print(address)
<re.Match object; span=(22, 42), match='[email protected]'>
<re.Match object; span=(44, 60), match='[email protected]'>

sub(pattern, repl, string, count=0, flags=0)
subn(pattern, repl, string, count=0)

sub() 是替换函数。它将字符串中最左侧、且不重叠的模式出现替换为 repl,并返回替换后的字符串。若未找到模式,则返回原字符串不变。

subn() 与 sub() 类似。不过它会返回一个元组,包含新字符串与在语句中执行的替换次数。

statement = "Please contact us at: [email protected]"
new_email_address = re.sub(r'([\w\.-]+)@([\w\.-]+)', r'[email protected]', statement)
print(new_email_address)
Please contact us at: support@datacamp.com

split(string, [maxsplit = 0])

在模式匹配处拆分字符串并返回列表。如果可选参数 maxsplit 非零,则最多执行 'maxsplit' 次拆分。

statement = "Please contact us at: [email protected], [email protected]"
pattern = re.compile(r'[:,]')

address = pattern.split(statement)
print(address)
['Please contact us at', ' [email protected]', ' [email protected]']

start() —— 返回匹配的起始下标。
end() —— 返回匹配结束的位置下标。
span() —— 返回包含匹配(起始, 结束)位置的元组。

pattern = re.compile('COOKIE', re.IGNORECASE)
match = pattern.search("I am not a cookie monster")

print("Start index:", match.start())
print("End index:", match.end())
print("Tuple:", match.span())
Start index: 11
End index: 17
Tuple: (11, 17)

编译标志

您是否注意到上个示例中的 re.IGNORECASE?是否理解了它的重要性?

可以通过指定标志值来修改表达式的行为。您可以将标志作为额外参数添加到本教程中见过的不同函数中。以下是一些更常用的:

IGNORECASE (I) —— 允许大小写不敏感匹配。
DOTALL (S) —— 允许 . 匹配任何字符,包括换行。
MULTILINE (M) —— 允许字符串起始(^)与结尾($)锚点匹配换行。
VERBOSE (X) —— 允许您在正则表达式中编写空白与注释,使其更易读。

statement = "Please contact us at: [email protected], [email protected]"

# 使用 VERBOSE 标志有助于理解复杂的正则表达式
pattern = re.compile(r"""
[\w\.-]+ #First part
@ #Matches @ sign within email addresses
datacamp.com #Domain
""", re.X | re.I)

addresses = re.findall(pattern, statement)                       
for address in addresses:
    print("Address: ", address)
Address:  support@DataCamp.com
Address:  xyz@DATACAMP.com

提示:您也可以通过按位或 | 来组合多个标志。

案例研究:使用正则表达式

现在您已经通过一些示例了解了 Python 中正则表达式的工作方式,是动手的时候了!在这个案例研究中,您将把所学知识付诸实践。

您将使用陀思妥耶夫斯基(Fyodor Dostoyevsky)所著、来自古腾堡计划的免费电子书《白痴》的第一部分。小说讲述了王子(Князь)列夫·尼古拉耶维奇·梅什金,一个坦率淳朴的人,他善良、单纯的本性常被错认为缺乏智慧与洞察。书名对这位年轻人形成了反讽。

您将编写一些正则表达式来解析文本并完成若干练习。

import re
import requests
the_idiot_url = 'https://www.gutenberg.org/files/2638/2638-0.txt'

def get_book(url):
    # Sends a http request to get the text from project Gutenberg
    raw = requests.get(url).text
    # Discards the metadata from the beginning of the book
    start = re.search(r"\*\*\* START OF THIS PROJECT GUTENBERG EBOOK .* \*\*\*",raw ).end()
    # Discards the text starting Part 2 of the book
    stop = re.search(r"II", raw).start()
    # Keeps the relevant text
    text = raw[start:stop]
    return text

def preprocess(sentence):
    return re.sub('[^A-Za-z0-9.]+' , ' ', sentence).lower()

book = get_book(the_idiot_url)
processed_book = preprocess(book)
#print(processed_book)
  • 练习:统计代词 "the" 在语料中的出现次数。提示:使用 len() 函数。
len(re.findall(r'the', processed_book))
302
  • 练习:尝试将语料中每个独立成词的“小写 'i'”替换为大写 'I'。确保不要更改出现在单词内部的 'i':
processed_book = re.sub(r'\si\s', " I ", processed_book)
#print(processed_book)
  • 练习:统计语料中出现引号("")的次数。
len(re.findall(r'\”', book))
0
  • 练习:语料中由 '--' 连接的词有哪些?
    请自己动手试试!欢迎在下方评论中分享您的答案。

恭喜!

您已完成本次 Python 正则表达式教程!在使用 Python 的数据科学旅程中,还有更多内容等待您探索。

正则在数据预处理阶段能发挥重要作用。欢迎查看 DataCamp 的 Cleaning Data in Python 课程。该课程将教您如何通过整理与清洗数据,更好地探索数据以支持分析。课程末尾还包含一个案例研究,您可以把新学到的知识用起来。

也可以看看我们的 Python 字符串替换教程。

主题