正则表达式基础

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里面找到一个匹配aaaaa被匹配了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)(后面一定是ingfly pig is flying, haha!
    pig can fly, right?
负向先行断言(后面不能出现什么) fly(?!ing)(后面不是ingfly 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
 

2. Python里面的正则

Python内置库 re 提供了正则表达式的实现,我们可以直接使用。

我们首先import进来

 

import re
2.1 匹配(match)

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 用来实现从起始到结束的完全匹配,函数签名为:

re.fullmatch(pattern, string, flags=0)

re.fullmatch('abc', 'abc')  # 完全匹配,成功
# 非完全匹配,失败
print(re.fullmatch('abc', '123abc')) 
print(re.fullmatch('abc', 'abc123'))
 
None
None

 

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.6 编译正则(compile)

re.compile 用来编译正则,被编译后的正则运行起来速度更快。

我们来看下函数签名

re.compile(pattern, flags=0)

regex = re.compile('abc')
print(type(regex))  # 正则被编译后,生成一个`Pattern`对象
<class 're.Pattern'>


# Pattern对象几乎拥有上面我们所使用的re下的所有方法
regex.match, regex.fullmatch, regex.search, regex.sub, regex.findall, regex.finditer, regex.split
(<function Pattern.match(string, pos=0, endpos=9223372036854775807)>,
 <function Pattern.fullmatch(string, pos=0, endpos=9223372036854775807)>,
 <function Pattern.search(string, pos=0, endpos=9223372036854775807)>,
 <function Pattern.sub(repl, string, count=0)>,
 <function Pattern.findall(string, pos=0, endpos=9223372036854775807)>,
 <function Pattern.finditer(string, pos=0, endpos=9223372036854775807)>,
 <function Pattern.split(string, maxsplit=0)>)

 
# Pattern对象的方法 使用方式与上面(re调用)基本没有差别,除了无需再声明正则,因为在compile的时候已经声明过了
regex.match('abc123')
<re.Match object; span=(0, 3), match='abc'>
 

我们来比较下:编译和不编译的速度差距

import time
​
content = '我来到了深圳科技有限公司玩;' * 1000  # 重复1千次
regex_str = '(深圳)(.{1,5})(有限公司)'  # 检测公司名
run_times = 1000  # 运行1千次
​
regex = re.compile(regex_str)  # 预编译正则
# 计算并打印不编译耗时
init_time = time.time()
for _ in range(run_times):
    re.findall(regex_str, content);
print('不编译耗时: {}s'.format(time.time()-init_time))
​
# 计算并打印编译耗时
init_time = time.time()
for _ in range(run_times):
    regex.findall(content)
print('编译耗时: {}s'.format(time.time()-init_time))
 
 
 
不编译耗时: 0.43161606788635254s
编译耗时: 0.3776540756225586s

实践经验

  • 如果你的正则后面不只使用一次,那记得一定要提前编译,养成编写高效代码的好习惯。
 

2.7 更强大的正则库——regex

  • 第三方库 regex 提供了额外的一些功能,以及对unicode的支持更加彻底,并且基本兼容内置库re模块
  • 如果发现内置re库不能满足你的需求,可以尝试使用regex

下次整理后再发

 

posted @ 2021-03-10 10:40  momomoi  阅读(203)  评论(0编辑  收藏  举报