正则表达式
大约 8 分钟
正则表达式
- 正则表达式
Regular Expression,也叫作正则,是一种字符串匹配模式,用于匹配字符串中的内容,绝大多数主流语言都支持正则表达式
1. 正则表达式语法
1.1 元字符
元字符是正则表达式中的特殊字符,用于匹配字符串中的内容
匹配单个字符
表达式 含义 示例 .任意一个字符(除了换行) a.c匹配abc和axc\d任意一位数字(0-9) \d\d匹配12或45\D非数字 \D\D匹配ab或@#\w单词字符,包括字母数字下划线 \w\w匹配ab、A1或_x\W非单词字符 \W\W匹配!}或@$\s空白字符,如空格、Tab等 a\sb匹配a b中的空格\S非空白字符 \S\S匹配ab或12[]表示匹配字符集合中的某一个字符 [abc]匹配a或b或c[^]表示匹配除字符集合内容外的字符 [^abc]匹配除了a或b或c以外的字符|表示匹配两个中的任意一个字符 a|b匹配a或b重复匹配
表达式 含义 示例 *匹配前面字符0次或多次 a*b匹配b、ab或aaab+匹配前面字符1次或多次 a+b匹配ab或aaab(不匹配b)?匹配前面字符0次或1次 a?b匹配b或ab(不匹配aab){n}匹配前面字符n次 a{3}匹配aaa(不匹配aa){n,m}匹配n到m次 a{2,3}匹配aa或aaa(不匹配a)一般情况下,正则会尽可能多得重复匹配,也称为贪婪匹配,如果希望尽可能少得匹配,可以在
*和+后面添加?其他
表达式 含义 示例 ^匹配字符串开头 ^a匹配abc开头(不匹配ba开头)$匹配字符串结尾 a$匹配ba结尾(不匹配ab结尾)\b单词边界 \bword\b匹配a word中的word(而不匹配keyword)
重要
- 许多语言中,使用
\w这些字符,仅能匹配ASCII字符。处理有中文和其他字符的文本时,要注意:- 如果原生支持
\p匹配对应分区,使用\p{Han}等匹配分区方法,不同语言写法也不同- JAVA要使用
\p{IsHan} - Ruby要使用
\p{Han} - JavaScript(ES2018+且开启
u标志)要使用\p{Script=Han}
- JAVA要使用
- 如果不支持
\p,但支持读取unicode字符串,使用[\u4E00-\u9FFF]匹配中文 - 同时要确定
\w是否会匹配中文,比如python 3.9测试会将中文作为单词字符,JavaScript和JAVA则不会
经过测试,大部分语言支持
\p,不过python re库不支持\p,可以使用regex包代替,\p的相关用法描述见regular expression - 如果原生支持
1.2 分组
在正则表达式中,
()可以捕获一个分组,之后使用索引方法,获取正则中每个分组的结果通过分组,可以获取其中每个分组的匹配结果
分组支持命名复用,可以在正则中要求复用的分组值是一致的,用于更精确的匹配
可以方便地进行分组替换
()的用法实际上,括号在普通的正则表达式中也是会使用的,用于表示这段内容是一个整体,比如
(abc){2}表示匹配abcabc,在需要分组的场景中,可能会导致部分不需要的分组也被捕获,因此括号中有更细致的非捕获和捕获语法- 默认将捕获分组:
(ab) - 创建非捕获分组:
(?:ab)
- 默认将捕获分组:
分组命名语法:不同语言的分组命名语法不相同
pythonimport re # 创建非捕获分组 m = re.match(r'(?:\w)(\w+)', 'abc123') print(m.group(1)) # 输出:bc123 # 创建捕获分组 m = re.match(r'(\w)(\w+)', 'abc123') print(m.group(1)) # 输出:a # 命名分组,默认名称是\数字,编号从1开始,可以使用?P<name>命名 # \1表示第一个分组 m = re.match(r"<([a-zA-Z]*)>\w*</\1>", "<xml>test</xml>") print(m.group(1),3) # 输出:xml # ?P<name>命名,?P=name用于引用 m = re.match(r"<(?P<TEST>[a-zA-Z]*)>\w*</(?P=TEST)>", "<xml>test</xml>") print(m.group(1),4) # 输出:xmljavaimport java.util.regex.*; public class RegexGroupsJava { public static void main(String[] args) { // 非捕获分组 (?:...) Matcher m1 = Pattern.compile("(?:\\w)(\\w+)").matcher("abc123"); if (m1.find()) { System.out.println(m1.group(1)); // 输出: bc123 } // 捕获分组 (...) Matcher m2 = Pattern.compile("(\\w)(\\w+)").matcher("abc123"); if (m2.find()) { System.out.println(m2.group(1)); // 输出: a } // 分组引用 \1 Matcher m3 = Pattern.compile("<([a-zA-Z]*)>\\w*</\\1>").matcher("<xml>test</xml>"); if (m3.find()) { System.out.println(m3.group(1)); // 输出: xml } // 命名分组 (?<name>...) 和 \k<name> 引用 Matcher m4 = Pattern.compile("<(?<TEST>[a-zA-Z]*)>\\w*</\\k<TEST>>").matcher("<xml>test</xml>"); if (m4.find()) { System.out.println(m4.group("TEST")); // 输出: xml } } }javascript// 非捕获分组 let m1 = /(?:\w)(\w+)/.exec("abc123"); if (m1) { console.log(m1[1]); // 输出: bc123 } // 捕获分组 let m2 = /(\w)(\w+)/.exec("abc123"); if (m2) { console.log(m2[1]); // 输出: a } // 分组引用 \1 let m3 = /<([a-zA-Z]*)>\w*<\/\1>/.exec("<xml>test</xml>"); if (m3) { console.log(m3[1]); // 输出: xml } // 命名分组和引用(ES2018+) let m4 = /<(?<TEST>[a-zA-Z]*)>\w*<\/\k<TEST>>/.exec("<xml>test</xml>"); if (m4 && m4.groups) { console.log(m4.groups["TEST"]); // 输出: xml }
1.3 断言
正则表达式中的断言是一种匹配机制,用来判断某个位置是否满足某种条件,但本身不消耗字符(不会把内容写入最终匹配结果中)
类型 语法 说明 示例 正向肯定断言 (?=...)后面是... \w+(?=\d)匹配后面是数字的单词正向否定断言 (?!...)后面不是... foo(?!bar)匹配后面不是"bar"的"foo"反向肯定断言 (?<=...)前面是... (?<=\$)\d+匹配前面是"$"的数字反向否定断言 (?<!...)前面不是... (?<!@)\w+匹配前面不是"@"的单词在
Java和JavaScript中使用反向断言存在要求长度固定的限制,也就是仅支持长度固定的反向断言,即无法使用(?<=\w{1,3})\d+来匹配前面有1-3个字母的数字,python是中这种操作可以的
2. 常用匹配结构
3. python re库
- re库是内置的支持正则匹配的库,支持多种模式标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
re.I:忽略大小写re.L:表示特殊字符集\w等依赖于当前环境re.M:多行模式re.S:即为.并且包括换行符在内的任意字符(原本不包括换行符)re.U:表示特殊字符集\w等依赖于Unicode字符属性数据库re.X:为了增加可读性,忽略空格和#后面的注释
- 正则字符串:在正则表达式中使用
\作为转义字符,如果是在一般字符串中,需要使用\\来转义\,为此python提供了r前缀,这样就可以不使用\\转义,比如r'\['代表匹配一个[ - 贪婪匹配:
re库默认是贪婪匹配,即尽可能在一个位置匹配满足条件的最长字符串
3.1 常用匹配方法
re模块导入import re预编译正则,提高多次匹配的性能
p = re.compile(r'\d+') # 之后使用p代替re使用接下来的函数,不需要再传入正则表达式match():视图匹配字符串开头,返回匹配对象或Nonem = re.match(r'\d+', '123abc') print(m) # <_sre.SRE_Match object; span=(0, 3), match='123'> m1 = re.match(r'\W+', '123abc') print(m1) # None # match对象 # 方法都可以传入1开始的分组索引或自定义的分组名称,获取对应分组情况 m.group() # 返回匹配结果 m.start() # 返回匹配的起始位置 m.end() # 返回匹配的结束位置 m.span() # 返回匹配的位置区间search():搜索字符串,返回第一个匹配对象或Nonem = re.search(r'\d+', "12a3aa4af5678c90") print(m.group())findall()和finditer():匹配所有结果,返回一个字符串列表或匹配对象迭代器print(re.findall(r'\d+', "12a3aa4af5678c90")) for m in re.finditer(r'\d+', "12a3aa4af5678c90"): print(m.group())sub():替换字符串,支持字符串、函数和正则进行替换# 将第二个字符串中匹配的内容替换为第一个字符串的内容,最多两次 print(re.sub(r'\d+', 'N', "12a3aa4af5678c90", count=2)) # 使用一个函数完成替换,函数会接收到匹配对象,返回字符串结果 print(re.sub(r'\d+', lambda m: str(len(m.group())), "12a3aa4af5678c90")) # 使用正则表达式进行替换 print(re.sub(r'(\d+)([a-z]+)', r'\2\1', "12a3aa4af5678c90")) # 对应变体subn() # 接受参数一致,但返回结果是一个元组,元组为(替换后的字符串,替换次数) print(re.subn(r'\d+', 'N', "12a3aa4af5678c90", count=2))split():根据匹配位置,将字符串分割lst = re.split(r'\s+', "Hello World ! Hello Python!") print(lst) # ['Hello', 'World', '!', 'Hello', 'Python!'] # 支持传入参数maxsplit,指定最大分割次数 lst = re.split(r'\s+', "Hello World !", maxsplit=1) print(lst) # ['Hello', 'World !']获取所有分组信息
# 获取分组信息 info = "name: xiaoming; age: 11; sex: man" # 命名分组 m = re.match(r'name:\s*(?P<name>\w+);\s*age:\s*(?P<age>\d+);\s*sex:\s*(?P<sex>\w+)', info) # 如果未命名,这里items()返回的结果是空的 for key, value in m.groupdict().items(): print(f"{key}: {value}") # 不命名分组 m = re.match(r'(\w+):\s*(\w+);\s*(\w+):\s*(\d+);\s*(\w+):\s*(\w+)', info) for value in m.groups(): print(value)
