技能补全之正则表达式
正则表达式
- 〇、前言
- 一、基本概念
- 1、基本运算单元
- 2、元字符
- 2.1、量词
- 2.2、字符集
- 2.3、边界匹配
- 2.4、分组
- 2.5、特殊字符
- 2.6、实践案例
- 3、标志
- 二、运算优先级
〇、前言
对于程序员职业,无论是全职的开发,还是自动化测试,都少不了进行文本校验操作。而诸多文本校验操作,有笨拙的,也有高效的。高效的文本校验操作,少不了使用 正则表达式,因为它本身就是根据字符串内容的组成规律而概括得来的模式
。
因为正则表达式的魅力,Python 内置相应能力进行支持,比如 re
模块,再比如 unittest
框架的 assertRegex 方法和 assertNotRegex 方法。下面,就结合 Python 的这两个模块,带领屏幕前的你,进行正则表达式的系统性学习。
一、基本概念
1、基本运算单元
模式
,即pattern
,是正则表达式运作的基本单元。模式串可简单也可复杂,简单的往往是不使用任何限制的字符串字面量,比如 『“once”』,就是一个简单的模式串,待校验的文本中只要包含同样内容,不论位置都算符合规则;复杂的模式串,往往会通过一个或多个元字符
结合文本字符进行组装,比如『“^http|https”』可用来校验字符串内容是否以 http 或 https 开头。
在Python中,正则表达式使用字符串进行表达,为了与普通字符串进行区分,通常会在引号前使用 r
进行修饰:
pattern = r"^http|https"
此外,也可以使用 re 模块的 compile 方法去获得一个模式串,尤其是当模式串需要带上特定标志时。
2、元字符
清楚了基本概念之后,下面开始学习正则表达式语法中的元字符,所谓元字符,就是在整个正则表达式中起到特定功能的特定字符,可以理解为它们就是正则表达式中的预留关键字。
比较常用的元字符,主要有以下几类:
- 量词
- 字符集
- 边界匹配
- 分组
- 特殊字符
2.1、量词
量词的功能在于校验次数,即预期字符出现的次数,目前一共有如下:
元字符 | 功能意义 |
---|---|
* | 匹配前面的模式零次或多次 |
+ | 匹配前面的模式一次或多次 |
? | 匹配前面的模式零次或一次 |
{n} | 匹配前面的模式恰好 n 次 |
{n,} | 匹配前面的模式至少 n 次 |
{n,m} | 匹配前面的模式至少 n 次且不超过 m 次 |
2.2、字符集
元字符 | 功能 |
---|---|
[] | 匹配括号内的任意一个字符 |
[^] | 匹配除了括号内的字符以外的任意一个字符 |
2.3、边界匹配
元字符 | 功能 |
---|---|
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
2.4、分组
元字符 | 功能 |
---|---|
() | 用于定义子表达式或捕获组 |
| | 表示『或』关系 |
2.5、特殊字符
特殊字符主要有 \
和 .
、-
,前者表示转义,用以匹配特殊字符本身;中者匹配除了换行符以外的任意字符,后者表示此到彼的意思,例如a-z
就表示从a到z的所有字母。
2.6、实践案例
下面使用 unittest 框架对上述元字符进行逐一实践:
import re
import unittestclass MetaChar(unittest.TestCase):def test_matchAppearTimes(self):"""测试正则表达式语法中的量词:return:"""regex_endWithStar = r"^[a-zA-z0-9]*$"text = "aabbccddeefff1111223998"self.assertRegex(text, regex_endWithStar)regex_endWithAdd = r"^[a-zA-z]+$"self.assertNotRegex(text, regex_endWithAdd)text = "abcd"self.assertRegex(text, regex_endWithAdd)regex_endWithQuestion = r"^[A-z0-9]?$"self.assertNotRegex(text, regex_endWithQuestion)text = "A"self.assertRegex(text, regex_endWithQuestion)self.assertRegex("", regex_endWithQuestion)regex_A3Times = r"^A{3}"self.assertNotRegex(text, regex_A3Times)text = "AAA"self.assertRegex(text, regex_A3Times)regex_BMoreThan4Times = r"^B{4,}"self.assertNotRegex(text, regex_BMoreThan4Times)text = "BBBBBB"self.assertRegex(text, regex_BMoreThan4Times)regex_CMoreThan4LessThan7Times = r"^C{4,7}"self.assertNotRegex(text, regex_CMoreThan4LessThan7Times)text = "CCCCCC"self.assertRegex(text, regex_CMoreThan4LessThan7Times)text = "CC"self.assertNotRegex(text, regex_CMoreThan4LessThan7Times)def test_matchCharSet(self):regex_include = r"^[abcABC]$"text_A = "A"self.assertRegex(text_A, regex_include)text_d = "d"self.assertNotRegex(text_d, regex_include)regex_uninclude = r"^[^abcABC]$"self.assertNotRegex(text_A, regex_uninclude)self.assertRegex(text_d, regex_uninclude)def test_matchBoundary(self):regex_welcome = r"^welcome\b$"self.assertRegex("welcome", regex_welcome)self.assertNotRegex("welcome to", regex_welcome)regex_difference = r"^different\B"self.assertRegex("different1", regex_difference)self.assertNotRegex("different", regex_difference)def test_matchGroup(self):# 分组regex_group = r"^(ab)+"self.assertRegex("abab", regex_group)self.assertRegex("abcd", regex_group)self.assertNotRegex("cdab", regex_group)regex_group = r"^apple|orange"self.assertRegex("apple", regex_group)self.assertRegex("orange", regex_group)self.assertNotRegex("pear", regex_group)# 捕获分组regex_fullDate = r"^(\d{4})-(\d{2})-(\d{2})$"self.assertRegex("2020-01-01", regex_fullDate)self.assertNotRegex("2020-1-1", regex_fullDate)# 非捕获分组regex_MrMsMrs = r"^(?:Mr|Ms|Mrs)\. (\w+)"self.assertRegex("Mr. Smith", regex_MrMsMrs)self.assertRegex("Ms. Smith", regex_MrMsMrs)self.assertRegex("Mrs. Smith", regex_MrMsMrs)self.assertNotRegex("Mrs.Smith", regex_MrMsMrs)# 命名分组regex_namedCaptureGroup = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"self.assertRegex("2020-01-01", regex_namedCaptureGroup)self.assertEqual(2020, int(re.search(regex_namedCaptureGroup, "2020-01-01").group("year")))self.assertEqual(1, int(re.search(regex_namedCaptureGroup, "2020-01-01").group("month")))self.assertEqual(1, int(re.search(regex_namedCaptureGroup, "2020-01-01").group("day")))self.assertNotRegex("2020-1-1", regex_namedCaptureGroup)# 分组引用# 反向引用regex_reverseReference = r"^(?P<name>\w+) (?P=name)$"self.assertRegex("Tom Tom", regex_reverseReference)self.assertNotRegex("Tom Tom Tom", regex_reverseReference)# 命名反向引用regex_namedReverseReference = r"(?P<word>\w+) (?P=word)"self.assertRegex("Pob Pob", regex_namedReverseReference)self.assertNotRegex("Pob", regex_namedReverseReference)# 替换引用text = "2023-05-15"new_text = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', text)self.assertEqual("05/15/2023", new_text)def test_matchSpecialChar(self):regex_dot = r"^a.c$"self.assertRegex("a和c", regex_dot)self.assertNotRegex("a\nd", regex_dot)regex_backslash = r"^a\\b$"self.assertRegex("a\\b", regex_backslash)self.assertNotRegex("a\b", regex_backslash)
3、标志
正则表达式标志,也称为修饰符,是用于改变正则表达式匹配行为的特殊指令,起到指定额外的匹配策略的功能。在 Python,字符串字面量形式的正则表达式,无法直接使用标志,必须借助 re.compile
方法实现。
目前,re 模块支持的正则表达式标志,一共有:
- re.IGNORECASE (对应 i 标志)
- re.MULTILINE (对应 m 标志)
- re.DOTALL (对应 s 标志)
- re.VERBOSE (对应 x 标志)
- re.ASCII (对应 a 标志)
- re.LOCALE (对应 L 标志)
- re.UNICODE (对应 u 标志)
特别说明,Python 中不支持全局匹配的 g 标志和粘性模式的 y 标志。
import re
import unittestclass TestFlag(unittest.TestCase):def test_ignoreCase(self):"""测试忽略大小写的匹配"""regex_ignore_case = re.compile(r"abc", re.IGNORECASE)regex_not_ignore_case = r"abc"self.assertRegex("aBc", regex_ignore_case)self.assertNotRegex("aBc", regex_not_ignore_case)def test_globalMatch(self):"""测试全局匹配"""text = "abc ABC abc"result = re.search(r"abc", text, re.IGNORECASE)self.assertEqual(result.group(), "abc")self.assertEqual(re.findall(r"abc", text, re.IGNORECASE), ["abc", "ABC", "abc"])result = re.sub(r"abc", "xyz", text,)self.assertEqual(result, "xyz ABC xyz")match_list = re.finditer(r"abc", text)self.assertEqual(2, len(list(match_list)))def test_multilineMode(self):"""测试多行模式:return:"""text = "abc\nABC\nabc"pattern = re.compile(r"abc", re.MULTILINE|re.IGNORECASE)self.assertEqual(pattern.findall(text), ["abc", "ABC", "abc"])def test_dotallModel(self):"""测试单行模式:return:"""text = "abc\nABC\nabc"pattern = re.compile(r"abc", re.DOTALL)self.assertEqual(["abc", "abc"], pattern.findall(text))def test_unicodeMode(self):"""测试unicode模式:return:"""text="αβγ"pattern = re.compile(r"αβγ", re.UNICODE)self.assertRegex(text, pattern)self.assertNotRegex("abc", pattern)def test_verboseMode(self):"""测试VERBOSE模式"""text = "Price: 12.99 and 5.50"pattern = re.compile(r"""\d+ # 一个或多个数字\. # 小数点\d* # 零个或多个数字""", re.VERBOSE | re.IGNORECASE)result = pattern.findall(text)self.assertEqual(result, ["12.99", "5.50"])
对于全局匹配,可以使用 re.search 方法、re.findall 方法、re.sub 方法以及 re.finditer 方法进行替代。
二、运算优先级
使用元字符的正则表达式,如果想实现符合预期的校验功能,必须遵循正则表达式语法所规定的运算优先级。
相同优先级的从左到右进行运算,不同优先级的运算先高后低。下表从最高到最低说明了各种正则表达式运算符的优先级顺序:
运算符 | 描述 |
---|---|
\ | 转义符 |
(), (?😃, (?=), [] | 圆括号和方括号 |
*, +, ?, {n}, {n,}, {n,m} | 限定符 |
^, $, \任何元字符、任何字符 | 定位点和序列(即:位置和顺序) |
| | 替换,“或"操作 字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food”。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。 |
以下是一些常见正则表达式运算符按照优先级从高到低的顺序:
-
转义符号:
\
是用于转义其他特殊字符的转义符号。它具有最高的优先级。示例:
\d
、\.
等,其中\d
匹配数字,\.
匹配点号。 -
括号: 圆括号
()
用于创建子表达式,具有高于其他运算符的优先级。示例:
(abc)+
匹配 “abc” 一次或多次。 -
量词: 量词指定前面的元素可以重复的次数。
示例:
a*
匹配零个或多个 “a”。 -
字符类: 字符类使用方括号
[]
表示,用于匹配括号内的任意字符。示例:
[aeiou]
匹配任何一个元音字母。 -
断言: 断言是用于检查字符串中特定位置的条件的元素。
示例:
^
表示行的开头,$
表示行的结尾。 -
连接: 连接在没有其他运算符的情况下表示字符之间的简单连接。
示例:
abc
匹配 “abc”。 -
管道: 管道符号
|
表示"或"关系,用于在多个模式之间选择一个。示例:
cat|dog
匹配 “cat” 或 “dog”。
接下来我们看下以下正则表达式的优先级说明:
\d{2,3}|[a-z]+(abc)*
\d{2,3}
匹配两到三个数字。|
表示或。[a-z]+
匹配一个或多个小写字母。(abc)*
匹配零个或多个 “abc”。