美文网首页
《正则表达式必知必会》学习笔记

《正则表达式必知必会》学习笔记

作者: 王远竹 | 来源:发表于2021-07-01 18:18 被阅读0次
    原书信息:
    书名: 正则表达式必知必会
    作者: Ben Forta [美]
    译者: 杨涛, 杨晓云, 王建桥
    ISBN: 978-7-115-16474-2
    豆瓣介绍: https://book.douban.com/subject/26285406/

    正则表达式(以下简称“正则”)的作用:主要用字符串查找,替换。

    1 从windows的“*”和“?”说起

    我是比较晚接触正则的,最近才刚刚开始学习它。在接触正则之前,我在windows平台搜索文件的时候,有个通配符“*”, 它表示任意长度的任意内容,“?”表示任意单个字符。这个和正则比起来,是一个功能不强,也不怎么优雅的办法。但是却非常的实用。用户用他们搜索文件的时候,可以不必记住全部文件名,只需要记住其中具有特征的部分,举个例子
    你想查找文件spring-expression-5.2.6.RELEASE.jar, 你可以这么查询:spring?expression*.jar, 甚至spring*.jar
    如果按照本书的作者所说,纯文本也是正则的话,显然“*”和“?”也能算正则,但是正则里面却不是这么实现的。为什么呢?
    实用归实用,但确实是功能比较弱,也不优雅。为什么呢?我们先看看正则怎么表达“任意长度的任意内容”,就明白了。
    在正则里面,用.*来表示任意长度的任意内容。其中.表示任意字符, *表示任意个数的匹配。第一个强大的地方是:解耦了;第二个强大的地方是:因为解耦了,所以可以各自变化。

    2 匹配字符

    .的含义太广泛了,正则有多种更严格的限制

    2.1 任选一个——集合

    集合用方括号表示:
    F[0123456789]可以匹配F0,F1......,而 F12 只能匹配到F1,后面的2匹配不上

    2.2 集合的简写形式——区间

    [0123456789]的写法太复杂,因为从0-9是连续的,所以,可以写成:[0-9],和[0123456789]意思完全一样。
    类似的还有:
    [0-5] == [012345]
    [A-Z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
    [a-z] == [abcdefghijklmnopqrstuvwxyz]
    需要注意的是:
    [A-z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz]
    这个是由他们在ASCII表中的位置来决定的

    另外,一个集合可以包含多个区间:
    [A-Za-z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]
    不同区间之间不需要分隔符

    实际上,字符集和区间是可以并存的,而且顺序也无所谓:
    [Ff0-9a] = [Ff0123456789a]

    2.3 取非

    取非的符号是^:
    [^0-9]匹配非数字
    ^虽然在[]里面,但是他表示对整个集合取非,所以会跟在[后面

    2.4 一些简化

    [^0-9]还能不能简化?能!
    正则还预定义了一些语法糖,和集合相关的有:
    \d == [0-9]
    \w == [A-Za-z0-9_] //这个基本就是编程语言的标识符命名限制了
    \s == [\f\n\r\t\v] //任意空白字符
    他们的大写形式表示取非:
    \D == [^0-9],其他依次类推

    之所以定义这些简便写法,是因为这些很常用

    3 重复匹配

    [Ff0-9a] 是匹配一个字符,而不是3个字符。当我们用这个正则对F1ab进行全局匹配(后面会讲到全局匹配)的时候,是匹配到了3个结果、分别是F,1,a,而不是一个匹配结果。
    如果我们想匹配一个结果F1a的话,应该这么写正则:
    [Ff][0-9]a

    3.1重复次数的区间

    如果我们想匹配3~6个连续的数字,如 012, 3333, 33445, 778899 的话,怎么做呢?
    好像比较发麻烦,如果次数固定的(比如3个)话,或许有办法:
    [0-9][0-9][0-9]或者\d\d\d
    这个既不灵活,也不优雅。
    正则可以定义重复匹配的次数的区间。
    集合使用[]定义,重复次数使用{}定义
    想匹配3~6个连续的数字,可以这么写:
    [0-9]{3,6} // {}定义的是一个闭区间,匹配长度为3或4或5或6

    3.2 区间的简化形式

    {3,6}这种形式在不同的情况下还可以简化
    {3,} 至少重复3次,上不封顶
    {3} == {3,3}
    {1} == {1,1} 这个可以省略不写
    和合集一样,依然有些语法糖:
    + == {1,} //一个或者多个
    * == {0,} //零个或者多个
    ? == {0,1} //零个或者1个

    3.3 贪婪和懒惰

    假设用正则[0-9]{3,6}去匹配 012345,结果是012还是012345?
    答案是012345。默认是贪婪模式。
    懒惰模式的正则为[0-9]{3,6}?
    同样,+*?也都有懒惰模式:+?,*?,??

    4 元字符和转义

    所谓元字符就是在正则里面有特殊含义的字符,比如前面接触到的[, ], ., *等
    如果要把元字符当普通字符使用,就要转义,转义使用"\",因此"\"也是元字符,做普通字符时,也需要转义:"\\"
    在有些解析器中,如果[]没有匹配到[,不管是没出现,或者是被转义\[,那么]是不需要转义的,但是加上转义也可以。写的时候还是都转义,读的时候要注意。

    4.1 反向转义

    有些时候,为了表示不可见字符或者为了方便灵活,用转义字符"\"后面跟普通字符,表示一个特殊含义,我称之为反向转义。

    下表是不可见字符的情况:

    元字符 说明
    [\b] 回退(并删除)一个字符(Backspace键)
    \f 换页符
    \n 换行符
    \r 回车符
    \t 制表符(Tab键)
    \v 垂直制表符

    下表是一些常用的简写的情况:

    元字符 说明
    \d d表示digital,任何一个数字字符(等价于[0-9])
    \D 等价于[^0-9]
    \w w表示word:
        任何一个字母数字字符(大小写均可)
        或下划线字符(等价于[a-zA-Z0-9_])
    \W 等价于[^a-zA-Z0-9_]
    \s 任何一个空白字符(等价于[\f\n\r\t\v])
    \S 任何一个非空白字符,等价于[^\f\n\r\t\v]

    下表是表示数字进制的情况:

    元字符 说明
    \x 16进制表示,范围:0~FF(10进制0~255)
    \0 8进制表示,\后面是数字0,不是字母o,范围:0~77(10进制0~63)

    5 位置和边界

    边界并不包含在匹配结果里面。

    5.1单词边界\b

    表示单词的开头或者结尾,
    首先理解下什么是单词。正则不理解“单词”这个术语。单词可以理解为两个空白字符之间的非空白字符组成的部分。
    然后要注意“或”。\babc表示匹配abc开头的单词,abc\b表示匹配abc结尾的单词,要只匹配单词abc,则\babc\b
    \b还有取非的模式。也许你从前面注意到了,大写就是取非。所以\b取非就是\B
    其含义如下:
    \babc\b 能匹配ff abc ff中的abc,不能匹配ffabcff中的abc;

    \Babc\B能匹配ffabcff中的abc,不能匹配ff abc ff中的abc

    5.2 字符串边界

    ^匹配字符串开头,分行匹配模式下,匹配每一行的开头
    $匹配字符串结尾,分行匹配模式下,匹配每一行的结尾,写在表达式尾部

    6 子表达式 和 一致性匹配

    6.1子表达式的定义

    从语法上看,子表达式就是让表达式中的某些部分“优先计算”。和编程语言的算术表达式一样,使用()来实现。

    举个简单的例子:
    我们要匹配abab, 不能写ad{2}, 这种只能匹配abb,而应该写(ab){2}

    再举个例子说明“优先计算”:
    19|20\d{2},这个可以匹配2021,不能匹配1992,为什么呢?因为|的优先级比较低,这个正则的意思是:匹配19或者20\d{2},如果要搜索年份,同时匹配 1992 和 2021,应该写(19|20)\d{2}

    子表达式是可以嵌套的
    如:((ab){2}c){2} 匹配 ababcababc

    6.2 回溯

    从语法层面来说,用()定义的子表达式,不仅仅提高了计算的优先级,我们还可以像引用变量那样引用它,引用方法是反斜杠\后面跟一个数字,表示第几个,编号从1开始。

    关于引用子表达式的编号:
    (1)从1开始是因为0通常表示整个表达式
    (2)对于嵌套的子表达式,从外向内编号,依次为:外层子表达式、内层子表达式、和外层平级的下一个子表达式

    引用的规则不是说这里的子表达式和前面的子表达式一样,如果这样的话,直接重新写一遍就行了。引用的规则是:应用部分的内容应该和被引用表达式匹配到的内容完全一致。比较拗口,试举例说明:
    ([a-z])\d+\1 可以匹配 a21a, D47D, 不能匹配A53B, 也不能匹配D38d
    因此这种引用被称为回溯引用。这种匹配被称为一致性匹配

    6.3 替换

    子表达式的一个重要用途就是用来替换。
    在替换的时候,引用子表达式,和查询略有不同。如何引用和实现引擎有关。以JS为例:
    假设要给下面一段话中的Spring Boot加粗
    With Spring Boot, your microservices can start small and iterate fast.
    查询:(\bSpring \bBoot)
    替换:<b>$1</b>
    没错,在JS中替换时引用子表达式,用$, 而不是\。

    替换的时候,可以改变大小写。如下表所示:

    元字符 说明
    \u 把下一个字符转换为大写
    \l 把下一个字符转换为小写
    \U...\E 把\U到\E之间的字符全部转换为小写
    \L...\E 把\L到\E之间的字符全部转换为小写

    或许你已经注意到了,这里大写字母不是小写字母取非的意思。

    例如,要把
    micrOSErVice architectures are the ‘new normal’.
    变成
    Microservice architectures are the ‘new normal’.
    查询:(^[a-z])(\w*)
    替换:\u$1\L$2\E

    7 前后查找

    先理解下“消费”的概念。
    我们写一个正则去匹配一段文本。被匹配到的文本如果在匹配结果里面,那就是我们“消费”了它,如果不在,那就是没有“消费”,举个生活中的例子,假设我借室友的钥匙去寝室拿了一鼠标,回来以后,钥匙还给了室友。那我消费的是鼠标,没有消费钥匙。
    回到书中的例子,下面是个html文档,省略了部分内容:
    <html lang='en'><head>...<title>Spring | Microservices</title>...</head><body>...</body></html>
    我们要从中找出title的内容,如果没有前后查找的话,我们可以先用一个正则把<title>Spring | Microservices</title>出来,然后再用程序处理掉<title>和</title>,取中中间的内容。听起来就很累,还不如用DOM呢?
    如果我们有办法用正则,一次就把Spring | Microservices拿到的话,我们就是使用了<title>,但没有消费<title>(</title>也一样)。
    我们来一步一步做,看看能不能完成这个任务。

    7.1 向前查找

    向前查找使用?=,后面跟需要匹配但不消费的文本,并且用()包裹起来,返回的是匹配文本前面的内容,所以叫向前查找。
    上例,我们可以这写:<title>.*(?=<\/title>)
    注:实际中,根据html的规范,还要考虑title大小写的问题,此处假定原始文本全部小写
    我们得到的是:<title>Spring | Microservices,</title>并没有返回。但这还不是我们想要的结果。

    7.2 向后查找

    这个根据“向前查找”自然而然就能想到的。向后查找使用?<=。
    正则(?<=<title>).*<\/title>匹配到的是:Spring | Microservices</title>。

    把他们结合起来怎么样?
    (?<=<title>).*(?=<\/title>),大功告成!

    7.3 负向前、负向后查找

    ?=的英文解释是Positive lookahead
    ?<=的英文解释是Positive lookbehind
    有Positive就有Negative
    ?!:Negative lookahead
    ?<!:Negative lookbehind

    ?! 和 ?<!解释起来太拗口,还是用作者的例子吧:
    假设要从下面的文本中获取数量:
    I paid 30 for 100 apples, 50 oranges, and 60 pears. I saved5 on this order.
    这么写正则:(?<!\$\d*)\d+
    解释下: 这个意图就是找出不是在后面的数字。d+表示至少以为的数字,\表示本身,\后面加\d*。

    书作者特别提醒了:
    (1)不是所有正则的实现都支持向后查找
    (2)向后查找模式只能是固定长度——这是一条几乎所有的正则表达式实现都遵守的限制。

    8 嵌入条件

    嵌入条件就是先定义一个条件,在后面引用这个条件,如果条件成立的话,就怎样怎样。

    8.1 回溯引用条件

    为了聚焦重点,我们简化下作者的例子:
    假设有如下文本:
    <A href="/home"><IMG src="/images/home.gif"></A>
    <IMG src="/images/spacer.gif">
    <A href="/search"><IMG src="/images/spacer.gif">
    我们的意图是:是找出IMG元素,或者包含IMG元素的完整的A元素。说人话就是匹配前两行,不匹配第3行(缺少</A>)
    正则是这样的:^(<A.*>)?<IMG.*>(?(1)<\/A>)
    解释下:
    (1) 首先,为了更聚焦于条件,没有去兼容大小写。事实上,全局的大小写可以通过参数设定
    (2) (<A.*>)?定义了一个条件A元素的开始,有更好的正则来匹配合法元素定义,这里忽略了
    (3) ?(1)是引用前面条件,1表示第一个,如果成立的话,必须要有</A>,这个要优先计算,所以整体作为一个子表达式:(?(1)<\/A>)
    (4) 用?()引用条件的时候,不需要转义成\1,因为这里没有歧义
    (5) 最后说明下^,这个是从行首开始检查,否则第3行的IMG元素会以和第2行一样的规则被匹配

    8.2 回溯引用匹配

    假设有如下文本:
    11111
    22222
    33333-
    44444-4444
    我们的意图是:匹配正点的电话号码,5个数字,或者后面再跟-和四个数字,匹配124,排除第3行
    正则是这样的: \d{5}(?(?=-)-\d{4})
    解释下:
    (1) ?=- 是一个向前查找,找-,假设定义为C
    (2) (?=-)前面的?表示条件,就像if一样
    (3) (?=-)后面的-\d{4}在外层()里面,表示如果找到-(条件成立)的话,匹配-\d{4},假设定义为E
    (4) 上面的前查找条件就是if(C)E

    9 如何解读正则

    现在有很多在线的正则工具帮我们解读正则,但是自己解读有助于熟练掌握正则,从而快速编写正则。理由如下:
    有些时候,我们并不从头开始编写正则,而是拿一个正则来改一下。修改之前,要弄明白别人写的正则的含义。解读就很重要。
    同时,解读多有有助于理解别人代码中正则的意图。
    另外,也可以提高编写正则的能力。

    解读的步骤:
    (1)找出表达式中的元字符
    正则复杂的一点是,元字符并不总是元字符。比如]在和[连用的时候才是元字符,单独使用,就是字符本身,可以转义,也可以不转义
    (2)根据元字符的组合特征,将整个表达式拆成若干部分
    (3)一次解读各部分,再拼起来
    (4)对于复杂的子表达式,递归进行前面3步
    试举1例
    https?://(\w*:\w*@)?[-\w.]+(:\d+)?(/([\w/-.]*(\?\S+)?)?)?

    先拆分

    Line Parts Comments
    1 http 普通字符
    2 s? 字符s是可选的
    3 :// 普通字符
    4 (\w*:\w*@)? 可选的。
    5     \w* 任意数量的字母数字下划线的任意组合
    6     : 普通字符
    7     \w* 同上
    8     @ 普通字符
    9 [-\w.]+ 1个以上(含)的-、字母、数字、下划线的任意组合
    10 (:\d+)? :后面跟数字
    11 (/([\w/_.]*(\?\S+)?)?)?
    12     / 普通字符
    13     ([\w/-.]*(\?\S+)?)?
    14         [\w/-.]* 任意数量的字母、数字、下划线、/、-的任意组合
    15         (\?\S+)? 整体加了可选
    16             \? 字符?
    17             \S+ 一个以上(含)的非空白字符

    说明:
    (1) 第1、2、3行,可以看出这个很可能是个url的正则,支持http或者https协议
    (2) 第4行比较少见,如果在URL中,又有:和@,可能是用户名和密码
    (3) 第9行是域名或者ip地址,类似www.abc.com这种
    (4) 第10行从上下文看是端口,是可选项
    (5) 第14行是路径,是可选项
    (6) 第15行是查询条件,是可选的。如果有的话,必须是从?开始。这里也可以用查找条件表达式,但因为需求简单,现在的表达式更简洁,也更加高效

    -------- (完) --------

    相关文章

      网友评论

          本文标题:《正则表达式必知必会》学习笔记

          本文链接:https://www.haomeiwen.com/subject/sszeultx.html