写点什么

正则表达式.06 - 断言

用户头像
insight
关注
发布于: 2021 年 03 月 11 日
正则表达式.06 -  断言

简介

什么是断言呢?简单来说,断言是指对匹配到的文本位置有要求

在有些情况下,我们对要匹配的文本的位置也有一定的要求。为了解决这个问题,正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言


常见的断言有三种:

  1. 单词边界

  2. 行的开始或结束

  3. 环视。


举个例子:\d{11} 能匹配上 11 位数字,但这 11 位数字可能是 18 位身份证号中的一部分。再比如,去查找一个单词,我们要查找 tom,但其它的单词,比如 tomorrow 中也包含了 tom。所以这个时候,就需要对匹配的文本的所处位置进行限制,保证匹配出来的内容是我们想要的,这就是断言的作用了。

单词边界(Word Boundary)

在正则中,单词的组成一般可以用元字符 \w+ 来表示,\w 包括了大小写字母、下划线和数字(即 [A-Za-z0-9_])。如果我们需要找出单词边界,即\w 表示的范围以外的字符,比如引号、空格、标点、换行等这些符号,我们就可以在正则中使用\b 来表示单词的边界。 \b 中的 b 可以理解为是边界(Boundary)这个单词的首字母。

举个例子

我们想要把下面文本中的 tom 替换成 jerry。注意一下,在文本中出现了 tomorrow 这个单词,tomorrow 也是以 tom 开头的。


tom asked me if I would go fishing with him tomorrow.


如果直接替换,就会出现以下的结果。

替换前:tom asked me if I would go fishing with him tomorrow.替换后:jerry asked me if I would go fishing with him jerryorrow.
复制代码

这显然是错误的,因为明天这个英语单词里面的 tom 也被替换了。


如果用上单词边界,即 \b\w+\b 就可以实现例子中的替换了。

以 Python3 语言为例子,实现上面提到的 “tom 替换成 jerry”:

>>> import re>>> test_str = "tom asked me if I would go fishing with him tomorrow.">>> re.sub(r'\btom\b', 'jerry', test_str)'jerry asked me if I would go fishing with him tomorrow.'
复制代码

行的开始或结束

在正则中还有文本每行的开始和结束,如果我们要求匹配的内容要出现在一行文本开头或结尾,就可以使用 ^ 和 $ 来进行位置界定。


以下就是一些常见的匹配行的开始或结束的场景


日志起始行判断

最常见的例子就是日志收集,我们在收集日志的时候,通常可以指定日志行的开始规则,比如以时间开头,那些不是以时间开头的可能就是打印的堆栈信息。我来给你一个以日期开头,下面每一行都属于同一篇日志的例子。


在这种情况下,我们就通过日期时间开头来判断哪一行是日志的第一行,在日期时间后面的日志都属于同一条日志。除非我们看见下一个日期时间的出现,才是下一条日志的开始。


输入数据校验

在 Web 服务中,我们常常需要对输入的内容进行校验,比如要求输入 6 位数字,我们可以使用 \d{6} 来校验。但你需要注意到,如果用户输入的是 6 位以上的数字呢?在这种情况下,如果不去要求用户录入的 6 位数字必须是行的开头或结尾,就算验证通过了,结果也可能不对。比如下面的示例,在不加行开始和结束符号时,用户输入了 7 位数字,也是能校验通过的:

>>> import re>>> re.search('\d{6}', "1234567") is not NoneTrue    <-- 能匹配上 (包含6位数字)>>> re.search('^\d{6}', "1234567") is not NoneTrue    <-- 能匹配上 (以6位数字开头)>>> re.search('\d{6}$', "1234567") is not NoneTrue    <-- 能匹配上 (以6位数字结尾)>>> re.search('^\d{6}$', "1234567") is not NoneFalse   <-- 不能匹配上 (只能是6位数字)>>> re.search('^\d{6}$', "123456") is not NoneTrue    <-- 能匹配上 (只能是6位数字)
复制代码

在前面的匹配模式章节中,我们学习过,在多行模式下,^ 和 $ 符号可以匹配每一行的开头或结尾。大部分实现默认不是多行匹配模式,但也有例外,比如 Ruby 中默认是多行模式。所以对于校验输入数据来说,一种更严谨的做法是,使用 \A 和 \z (Python 中使用 \Z) 来匹配整个文本的开头或结尾。解决这个问题还有一种做法,我们可以在使用正则校验前,先判断一下字符串的长度,如果不满足长度要求,那就不需要再用正则去判断了。相当于你用正则解决主要的问题,而不是所有问题,这也是前面说的使用正则要克制。

环视( Look Around)

环视可以理解为看看左边,再看看右边。在正则中我们有时候也需要瞻前顾后,找准定位。环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则,有些地方也称环视为零宽断言


环视分类

环视有以下 4 种:

用口诀来记忆就是:左尖括号代表看左边,没有尖括号是看右边,感叹号是 非 的意思。


举个例子

具体什么时候我们会用到环视呢?我来举个例子。邮政编码的规则是第一位是 1-9,一共有 6 位数字组成。现在要求你写出一个正则,提取文本中的邮政编码。根据规则,我们很容易就可以写出邮编的组成 [1-9]\d{5}。我们可以使用下面的文本进行测试:

012300  不满足第一位是 1-9130400  满足要求465441  满足要求4654000 长度过长138001380002 长度过长
复制代码

我们发现,7 位数的前 6 位也能匹配上,12 位数匹配上了两次,这显然是不符合要求的。也就是说,除了文本本身组成符合这 6 位数的规则外,这 6 位数左边或右边都不能是数字。


这时候,就需要使用环视来解决这个问题了。使用环视,可以写成左边不是数字,右边也不是数字的 6 位数的正则。即 (?<!\d)[1-9]\d{5}(?!\d)

单词边界用环视表示

表示单词边界的 \b 如果用环视的方式来写,应该是怎么写呢?


这个问题其实比较简单,单词可以用 \w+ 来表示,单词的边界其实就是那些不能组成单词的字符,即左边和右边都不能是组成单词的字符。比如下面这句话:


the little cat is in the hat the


左侧是行首,右侧是空格,hat 右侧是行尾,左侧是空格,其它单词左右都是空格。所有单词左右都不是 \w。


(?<!\w) 表示左边不能是单词组成字符,(?!\w) 右边不能是单词组成字符,即 \b\w+\b 也可以写成 (?<!\w)\w+(?!\w)。


另外,根据前面学到的知识,非\w 也可以用\W 来表示。那单词的正则可以写成 (?<=\W)\w+(?=\W)。


这个例子是为了让你有更多的思考,并不推荐在日常工作中这么来表示单词的边界,因为 \b 明显更简洁,也更容易阅读和书写。

环视与子组

环视中虽然也有括号,但不会保存成子组。保存成子组的一般是匹配到的文本内容,后续用于替换等操作,而环视是表示对文本左右环境的要求,即环视只匹配位置,不匹配文本内容。


用户头像

insight

关注

不要混淆行动与进展、忙碌与多产。 2018.11.17 加入

永远都是初学者

评论

发布
暂无评论
正则表达式.06 -  断言