正则表达式基础
1. 正则表达式基础
1.1 正则是什么
正则表达式,又称规则表达式,简称正则,通常被用来检索或替换符合某种模式的文本,这里的模式即规则。
正则是一种逻辑公式,主要用来对字符串进行操作。大家约定,用事先定义好的一些特定字符、及其组合,组成一个”规则字符串”,这个“规则字符串”用来表达对字符串的一种筛选逻辑。
对于搞NLP,正则表达式非常重要。正则和NLP模型一样,都是对文本进行处理;对于模型产生的一些异常结果(即badcase),我们往往需要写规则去人工干预,正则就是写规则的一种强大工具。
1.2 正则表达式的语法
大家可以使用网站 regex101.com 或者 c.runoob.com 来测试正则
普通字符
正则可以使用的普通字符包括:字母、数字、标点符号和其它符号(\s
\S
\w
.
等)。
描述 | 正则例子 | 测试字符串 |
---|---|---|
匹配单字母 | a |
abcd |
A |
ABCD |
|
匹配字母串 | abc |
abcd |
匹配单数字 | 1 |
1234 |
2 |
4567 |
|
匹配标点符号 | , |
123,456 |
匹配所有的空白符(空格/换行/Tab等)(使用\s ) |
\s |
abc 123 ABC |
匹配所有非空白符(使用\S ) |
\S |
abc 123 ABC |
匹配除换行符(\n 、\r )之外的任何单个字符(使用. ),等价于[^\n\r] |
. | abc 123 ABC |
匹配字母、数字、下划线(使用\w ),等价于[A-Za-z0-9_] |
\w | abc 123_ABC |
转义字符
正则也支持常见的转义字符:
- 换行符:
\n
- 回车符:
\r
- 制表符:
\t
- 垂直制表符:
\v
- 换页符:
\f
比如我们用正则\t
来匹配任意制表符,来测试以下文本:
word word
限定符
限定符用来指定前面匹配的次数,正则提供了6种可用的限定符:
限定符 | 描述 | 正则示例 | 测试字符串 |
---|---|---|---|
* |
匹配0次或多次 | zo* |
z zo zoo zod |
+ |
匹配1次或多次 | zo+ |
z zo zoo zod |
? |
匹配0次或1次 | zo? |
z zo zoo zod |
{n} |
匹配n次,n为非负整数 | o{2} |
z zo zoo zod zooo |
{n,} |
匹配至少n次,n为非负整数 | o{2,} |
z zo zoo zod zooo |
{n,m} |
匹配n次到m次,n和m均为非负整数,n<=m | o{2,3} |
z zo zoo zod zooo zoooo |
注意:
-
+
、*
、{}
都是贪婪的,它们尽可能多的匹配。比如:正则
a+
在字符串aaaabbbb
里面找到一个匹配aaaa
,a
被匹配了4次;
-
我们可以在其后面添加
?
,表示非贪婪,这使得它们尽量少地匹配。比如:正则
a+?
在字符串aaaabbbb
里面找到4个匹配a
,每个匹配仅匹配了一次a
;
定位符
定位符用来描述字符串或者词的边界,正则提供了4种定位符:
定位符 | 描述 | 正则示例 | 测试字符串 |
---|---|---|---|
^ |
匹配字符串开始 | ^app (匹配以app 开头的字符串) |
application mapping |
$ |
匹配字符串结束 | ing$ (匹配以ing 结尾的字符串) |
flying meanings |
\b |
匹配词(英文单词)边界 | \bword\b (匹配字符串中,包含word 单词) |
word a word my words |
\B |
匹配非词(英文单词)边界 | \Bword\B (匹配字符串中,单词中间包含word ) |
word a word my words swordfish |
逻辑或
我们可以使用|
或者[]
来表示逻辑或,
[]
主要用来匹配括号中的单个字符|
主要用来匹配两边的字符串
字符 | 描述 | 正则示例 | 测试字符串 |
---|---|---|---|
[] |
匹配某个字母 | [ad] |
abcd |
匹配任意小写字母(使用- ) |
[a-z] |
abcd |
|
匹配任意字母 | [a-zA-Z] |
abcd123ABCD |
|
匹配任意数字 | [0-9] |
abcd123ABCD |
|
[^] |
排除掉一些字符的匹配 | [^abc123] |
abcd1234 |
| |
匹配abc 或者123 |
abc|123 |
abcd1234 |
分组和断言
我们可以用()
来实现分组和断言,
- 所谓分组,是将正则分块,使得你明确知道当前匹配属于哪块
- 断言,使得你可以在匹配时限定一些上下文条件
我们通过例子来理解:
描述 | 正则例子 | 测试字符串 |
---|---|---|
分组 | (123)|(ABC)|(abc) |
123,abc,456,ABC |
正向先行断言(后面出现什么) | fly(?=ing) (后面一定是ing 的fly ) |
pig is flying, haha! |
pig can fly, right? |
||
负向先行断言(后面不能出现什么) | fly(?!ing) (后面不是ing 的fly ) |
pig is flying, haha! |
pig can fly, right? |
||
正向后行断言(前面出现什么) | (?<=can )fly (前面一定是can fly ) |
pig is flying, haha! |
pig can fly, right? |
||
负向后行断言(前面不能出现什么) | (?<!can )fly (前面不是can fly ) |
pig is flying, haha! |
pig can fly, right? |
快速记忆:
- 断言的正向 负向 先行 后行不太好记忆,我们可以有接地气一点的记忆方式
- 前文出现,
(?<=)
,放前面(前面指匹配字符串的前面) - 前文不出现,
(?<!)
,放前面 - 后文出现,
(?=)
,放后面 - 后文不出现,
(?!)
,放后面
关于元字符
- 元字符就是指那些在正则表达式中具有特殊意义的专用字符,上面介绍的很多字符都是元字符,比如:
[
]
(
)
*
等
- 完整的元字符表大家可以参考:正则表达式中的元字符
-
因为元字符已经被正则征用,如果你想匹配元字符,必须使用
\
来转义。比如正则
\(
,用来查找字符串中的左括号
运算符优先级
正则表达式的一些运算符是有优先级的,优先级如下(从高到低):
\
()
(?!)
(?=)
[]
*
+
?
{n}
{n,}
{n,m}
^
$
普通字符
|
1.3 一些复杂点的例子
描述 | 正则 | 测试字符串 |
---|---|---|
身份证 | ^(\d{15}|\d{18})$ |
110101199003073095 |
11010119900307309 (17位) |
||
IP地址 | \d+\.\d+\.\d+\.\d+ |
127.0.0.1 |
108.236.234.114 |
||
Email地址 | ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
laowang@163.com |
-laowang@163.com |
||
lao-wang@163.com |
||
laowang@163.edu.com |
re.match 尝试从字符串的起始位置匹配一个模式,
我们先看下函数签名:
re.match(pattern, string, flags=0) re.match('abc', 'abcdef') # 从头找abc,找到,返回match对象 match = re.match('abc', 'abcdef') print(match.group()) # 通过group()方法,打印匹配的字符串 print(match.span()) # 通过span()方法,打印匹配字符串的起始位置和结束位置 print(match.start(), match.end()) # 通过start()方法和end()方法,分别打印match对象的起始位置和结束位置
abc (0, 3) 0 3print(re.match('abc', '123abcdef')) # 从头找,没找到,返回None
None
re.fullmatch 用来实现从起始到结束的完全匹配,函数签名为:
2.2 搜索(search)
re.search 扫描整个字符串,并返回第一个成功的匹配,
注意search和match的区别在于:match限定只会从头开始匹配
我们来看下函数签名:
re.search(pattern, string, flags=0) re.search('abc', 'abcdef') # 搜索abc,找到,返回Match对象print(re.search('^abc', 'abcdef')) # 对比search和match print(re.match('abc', 'abcded')) # 两者等价 re.search('abc', 'abcdefabc') # 搜索abc,找到第一个,返回其match对象
假设你的正则有分组,你可以根据分组号取匹配里的相应块
match = re.search('(腾讯)(.{1,5})(有限公司)', '我来到了腾讯信息科技有限公司玩') print(match.group()) # 打印匹配,不区分子组 print(match.group(1), match.group(2), match.group(3)) # 根据子组号,分别打印三个子组 print(match.groups()) # 打印所有子组 腾讯信息科技有限公司 腾讯 信息科技 有限公司 ('腾讯', '信息科技', '有限公司')
2.3 替换(sub)
re.sub 提供将匹配到的字符串进行替换的功能,
我们来看下函数签名:
re.sub(pattern, repl, string, count=0, flags=0)
这里的count
表示替换次数
re.sub('abc', 'ABC', 'abcdefabc') # 将abc替换为ABC 'ABCdefABC' re.sub('abc', 'ABC', 'abcdefabc', count=1) # 声明仅替换一次,会替换第一次 'ABCdefabc'
2.4 查找所有(findall和finditer)
re.search
只返回第一个匹配,如果我们想返回所有匹配,可以使用 re.findall 和 re.finditer
re.findall
直接返回所有匹配的字符串re.finditer
依次返回所有匹配的Match对象
我们来看下两者的签名:
re.findall(pattern, string, flags=0) re.finditer(pattern, string, flags=0) re.findall('abc', 'abcdefabc') # 直接返回匹配的字符串 ['abc', 'abc'] re.finditer('abc', 'abcdefabc') # 默认返回一个迭代器,你需要for循环去迭代它,或者list()转下类型 <callable_iterator at 0x10e75a550> list(re.finditer('abc', 'abcdefabc')) # 返回两个匹配的Match对象 [<re.Match object; span=(0, 3), match='abc'>, <re.Match object; span=(6, 9), match='abc'>]
2.5 分割(split)
re.split 主要用来分割字符串
python的字符串函数str.split
也提供了字符串分割功能,但是它的功能过于简单,我们可以使用re.split实现更复杂的字符串分割。re.split('\s', 'a b\tc\rd\ne') # 按照空白字符切割
['a', 'b', 'c', 'd', 'e'] re.split('[;。!?\n]', '我很好。你好不好?他说他也好!没了') # 实现分句逻辑 ['我很好', '你好不好', '他说他也好', '没了'] re.split('([;。!?\n])', '我很好。你好不好?他说他也好!没了') # 在正则最外部添加括号,保留分句符 ['我很好', '。', '你好不好', '?', '他说他也好', '!', '没了']
实践经验
- 如果你的正则后面不只使用一次,那记得一定要提前编译,养成编写高效代码的好习惯。
2.7 更强大的正则库——regex
- 第三方库 regex 提供了额外的一些功能,以及对unicode的支持更加彻底,并且基本兼容内置库
re
模块 - 如果发现内置
re
库不能满足你的需求,可以尝试使用regex
库
下次整理后再发