正则表达式

ddl好像过去好几天了……

Posted by Lin_Xuan on February 5, 2021

正则表达式

正则表达式入门

参考资料: 正则表达式必知必会(修订版) 度盘提取码:ga2g

正则表达式是用于字符串匹配和替换的语言,但是没有单独的编译器/解释器,集成在了很多语言的内部。虽然在不同的语言中使用会有一点差距,但是核心语法相同。

单字符

确定字符:正则表达式可以是区分大小写的纯文本。

任意字符:对于不确定或无意义的字符,可以使用占位符.来代替。.可以连续出现,每一个.都代表任意一个字符(包括.本身)。如果需要特指字符.,可以使用转义字符\,写成\.的形式。

一组字符

例如想要得到文本中形式为”dot*“或”bot*“的四字符字符串(*代指任意字符),可以用特殊字符[]来代替,[]中填写所有的目标字符。在上例中,对应的正则表达式为[db]ot.。d和b的顺序无影响。

字符区间:有时候,我们希望[]去匹配一系列字符,如数字0-9,大写字母A-F。虽然可以将符合要求的所有字符填入[]内,但是还可以用另一种表达。即正则表达式的连字符-。连字符与ASCII码有关。[a-F]会匹配ASCII在a和F之间的所有字符。[]内可以填入不止一对区间,例如a-fA-F会匹配所有的字母。

取非匹配:[]还可以填写不希望匹配的字符。则其他字符都可以匹配。使用元字符^[^0-9]将匹配所有的非数字。^的效果作用于[]内的所有字符区间而不仅是其后一个。

元字符

正则表达式里的特殊字符,如果仅作为普通字符使用的话需要进行转义。创建的元字符:

Backspace 换页符 换行符 回车符 制表符 垂直制表符
[\b] \f \n \r \t \v
数字字符 非数字字符 字母数字下划线 非字母数字下划线 任何空白字符(\f\n\r\t\v,无\b) 任何非空白字符
\d \D \w \W \s \S

正则表达式支持直接使用ASCII码来匹配字符,使用十六进制/八进制,如\x0A(十六进制,对应10)、\011(八进制,对应9)

另外,还有POXSIX字符类,可以直接代指特定的字符集合。但是并不是所有的语言都支持。例如javaScript就不支持POXISIX字符类。

字符类 说明
[:alnum:] 字母或数字,[a-zA-Z0-9]
[:alpha:] 字母,[a-zA-Z]
[:blank:] 空格或制表符,[\t ]
[:cntrl:] 控制符,ASCII0-31和127
[:digit:] 数字,[0-9]
[:graph:] 可打印字符,不包括空格
[:lower:] 小写字母[a-z]
[:print:] 可打印字符,包括空格
[:punct:] 不属于alnum和cntrl的任意字符
[:space:] 任何空白字符,包括空格[\f\n\r\t\v]
[:upper:] 大写字母[A-Z]
[:xdigit] 十六进制数字,[a-fA-F0-9]

具体使用时,需要套上[]。即使用[[xdigit]]去匹配。

重复匹配

在字符或字符集的后面添加+表示重复匹配一个或多个该字符/字符集。例如使用[[:digit]]+@w+\.[a-fA-F]+可以匹配**@**.**格式的电子邮箱。+也是元字符之一。

在字符/字符集后面添加*表示字符/字符集出现0次或多次的情况。

在字符/字符集后面添加表示字符/字符集出现0次或1次的情况。

在字符/字符集后面添加{n}(n为数字)表示字符/字符集必须连续出现n次的情况。n也可以写成用,分割的区间的形式。如a{2,4}将匹配2~4个连续的字母a。[[:digit:]]{3, }将匹配最少连续出现3次或以上的数字。

贪婪形和懒惰型* ,+,{n, }将匹配尽可能多的字符。但是如果在后面加上字符?可以将模式改为最小匹配。如写成*?,+?,{n, }?

位置匹配

字符边界: 当我们想匹配的字符位于特定位置时,我们使用边界来限定。如搜索cat时不想搜索到scatter,就可以使用\b来限定开头和结尾(匹配一个非字母数字下划线的位置)。\bcat\b将只匹配到cat。这里的\b匹配位置,不匹配字符。(也可以使用\B来限定没有边界)

字符串边界: 当我们想匹配字符串的特定位置时,有两个元字符可供选择。^,$^有取非的意思,但是只有出现在字符集的开头部分时才表这一含义。当它出现在正则表达式的开头时,表示的是匹配字符串的开头。而$会匹配字符串的末尾。(正则表达式对字符串的定义是传进来的所有字符,都当作一个字符串)。

分行匹配: 正则表达式可以使用一个元字符来改变另一个元字符的含义。在正则表达式的开头使用(?m)将开启分行匹配模式。在这一模式下,正则表达式将使用换行符来作为字符串的划分。不再将所有的字符作为一个字符串。这时^$将匹配更多的位置。(有些正则表达式不支持分行匹配)

子表达式

()括起来的部分将作为一个整体,再匹配时被看作“一个字符”。这样,原本只能作用以一个字符/字符集的?, *, +, {a,b}等将可以作用与字符串片段。

为了增加代码的可读性,可以使用子表达式来对正则表达式做一定的划分。虽然一般情况下无影响,在有些实现方式里会影响执行效率。

或 元字符:在我们想要匹配年份的时候,我们寻找前两位时19或20的四位数字。可以使用|表示“或”的意思。但是|将左右两边的整体作为两个部分。因此可以使用()来限定|的使用范围。(19|20)\d{2}可以达成目的。

子表达式支持嵌套。

回溯引用

子表达式的另一个重要的应用。

匹配

在想要使用正则表达式中取寻找两个连续的相同单词时,后面的表达式需要知道前面匹配到了什么。再这种情况下,就需要使用回溯引用。例如[ ]+(\w+)[ ]+\1。前面和后面的[ ]+匹配若干个空格,中间匹配任意一个单词。关键在于后面的\1。它就是回溯引用,代指正则表则式中出现的第一个子表达式。数字可以为1,2,3(第一个、第二个、第三个……一次类推)。再有些实现里,\0可以代指整个正则表达式。

替换

在之前的应用中,正则表达式都是用来搜索。正则表达式的另一个重要作用是替换。而在替换操作中,最基本的就是用使用回溯引用。

例如,在文本中匹配到一个邮箱,改成<A HREF="mailto:user@address.com">user@address.com</A>(HTML)的格式。具体实现代码:

(\w+{\w\.}*@{\w\.}+\.\w+)//搜索模式表达式
<A HREF="mailto:$1">$1</A>//替换模式

正则表达式可以跨模式使用。用()将搜索模式的表达式写成子表达式,就可以在替换模式中使用了。$1\1效果相同。在不同的语言里又不同的要求。

大小写转换

部分正则表达式可以使用元字符来改变字符的大小写。

元字符 说明
\E 结束\L或\U转换
\l 下一个字符转小写
\L \L到\E之间转小写
\u 下一个转大写
\U \U到\E之间转大写

前后查找

当我们需要去标记待匹配的文本的位置时,比如查找<>括住的文本,我本可以这样</w+>但是,这样会多匹配出我们不需要的<>。从然我们可以自己剔除不需要的部分,但是,使用前后查找的方法,我们可以直接返回我们需要的部分。

前后查找分为向前查找和向后查找。常见的语言都支持向前查找,而支持向后查找的就没有那么多了。

**向前查找 **

向前查找在语法上是一个以?=开头的子表达式。例如.+(?=:)可以在一堆网址中搜索出协议名,但是省略掉写一名后面的’:’字符。向前查找的子表达式可以出现在总表达式的任意位置。

向后查找

向后查找模式的标志是?<=。使用方法类似,使用(?<=\$)[0-9.]+可以匹配形如$12.34的价格,但是会抛去$字符。

向前查找和向后查找意味着被抛去的字符在我们需要文本的前方/后方。计算机处理的前后与我们阅读的前后顺序**相反**。因此向后查找,被抛去的部分反而在文本的前面。理解为向……之后、向……之前查找,更容易理解

向前查找模式的长度是可变的,可以使用. +等字符。但是向后查找的长度必须固定,不能使用重复匹配的元字符。

向前查找和向后查找可以同时使用

正/负向前/后查找

向前查找和向后查找默认都是正查找(positive look-ahead and positive look-behand)。还可以使用负查找(negative look-ahead and negative look-behand),标识分别是?=~?<=!对正查找取非,意味着搜索不在标记位置出的文本,但是不太常用。

嵌入条件

正则表达式可以在表达式内部嵌入条件处理功能,限制匹配到文本的格式。

123-456-7890 //合法
(123)456-7890 //合法
(123)-456-7890 //合法
(123-456-7890 //不合法
1234567890 //不合法
123 456 7890 //不合法

比如在上述的6个电话号码中查找出符合特定格式的号码。

在之前已经接触过一些条件了。如:

  • :匹配一个字符/表达式,如果它存在的话
  • ?=?<=:匹配前面或后面的文本,如果它存在的话

因此,条件也用?字符来实现。正则表达式的条件有两种。一种是根据回溯引用,一种是根据前后查找

回溯引用条件

回溯引用的格式是((backreference)true-regex)。括号里的backreference是一个回溯引用,后面的true-regex是一个子表达式。只有当backreference存在时,后面的true-regex才会执行。

例如,我们要从html文本中查找出所有的<img>标签。但是如何这个标签是连接(被<a></a>包含)的话,就把整个连接匹配出来。

([Aa]\s+[^>]+>\s*)?<[Ii][Mm][Gg]\s+[^>]+>(?(1)\s*</[Aa]>)

这个模式不解释是不容易看明白的。其中,(<[Aa]\s+[">]+>\s*)?将匹配一个<A><a>标签(以及<A><a>标签的任意屈性)。这个标签可有可无(因为这个子表达式的最后有一个)。接下来<[Ii][Mm][Gg]\s+[">]+>匹配一个<!MG> (大小写均可)及其任意屈性。(?(1)\s*</[Aa]>)是一个回溯引用条件, ?(1)的含义是: 如果第1个回溯引用(具体到本例,就是 <A>标签)存在, 则使用\s*</[Aa]>继续进行匹配。

因此,可以写出解决上面电话号码匹配问题的代码。使用(\()?\d{3}(?(1)\|-)\d{3}-\d{4}可以解决我们的问题。中间的(?(1)\)|-在前面匹配到(的情况下去匹配)。否则,就去匹配-

前后查找条件

前后查找条件与回溯引用条件极为接近,只是把回溯引用的编号替换成一个完整的正则表达式就可以了。

举个例子,匹配下列电话号码

11111//合法
22222//合法
33333-//不合法
44444-4444//合法
\d{5}(?(?=-)\-d{4})

(?=-)成功匹配到-的情况下,才会匹配后面的-和四位数字。

PS:不知道为什么,所有的条件匹配代码在网页端测试均没有成功,目前原因未明。


正则表达式中的中文/双字节字符

入门中的部分是参考的国外的书籍,因此案例和方法都是针对单字节的英文等字符的。\w.等字符不配中文。在中文环境中使用正则表达式时,需要补充一些内容。

首先介绍一些关于非英文语系字符的”常识”:

2E80~33FFh:中日韩符号区。收容康熙字典部首、中日韩辅助部首、注音符号、日本假名、韩文音符,中日韩的符号、标点、带圈或带括符文数字、月份,以及日本的假名组合、单位、年号、月份、日期、时间等。

3400~4DFFh:中日韩认同表意文字扩充A区,总计收容6,582个中日韩汉字。

4E00~9FFFh:中日韩认同表意文字区,总计收容20,902个中日韩汉字。

A000~A4FFh:彝族文字区,收容中国南方彝族文字和字根。

AC00~D7FFh:韩文拼音组合字区,收容以韩文音符拼成的文字。

F900~FAFFh:中日韩兼容表意文字区,总计收容302个中日韩汉字。

FB00~FFFDh:文字表现形式区,收容组合拉丁文字、希伯来文、阿拉伯文、中日韩直式标点、小符号、半角符号、全角符号等。

因此,使用^[\u2E80-\u9FFF]+$(^$表示正则表达式的开始和结束)来匹配中日韩文字,(中文包括简体和繁体)。

另外,[\u4e00-\u9fa5]也经常用来匹配中文字符。简体繁体同样适用。

同时,也可以直接使用[\x00-\xff]来匹配所有的双字节字符。


Python 中的正则表达式

参考资料python3.8 re库文档

在python中使用内置的re库来使用正则表达式。

首先python同样使用\来对特殊字符进行转义,与正则表达式的转义相同,会造成一些繁琐的转义问题。如\\\\含义时匹配一个\字符。因此,在python中使用原生字符串书写正则表达式,格式为r"content"

python中使用正则表达式对象来实现搜索、替换等操作。把代表正则表达式的字符转转化为正则表达式对象需要使用函数re.compile(partern,flags=0)。这个函数会返回编译好的正则表达式对象,第一个参数为表达式内容的字符串,第二个为可选参数,可以改变对象的模式。

常用的可选参数

参数 效果  
re.IGNORECASE/re.I 不区分大小写匹配  
re.MULTILINE/re.M 开启后,’^’匹配字符串每一行的开始,$匹配每一行的结束,相当于内联标记(?m)  
re.DOTALL/re.S .可以匹配换行符。  
re.VERBOSE/re.X 忽略表达式内不在字符集的空格,且使用不在字符集内的#来进行注释,增加可读性  

RE库常用函数

re.search(pattern,string,flags=0):在string中按照正则表达式pattern搜索,返回匹配到的第一个字符串的匹配对象None

上面的函数等价于编译后的正则表达式对象pattern直接使用pattern.search(string)。之后的其他函数支持同样的用法。一般来说如果一个正则表达式要多次使用,应该优先使用编译的方法。

re.match(pattern,string,flags=0):在string中只能从第0个字符开始匹配。如果不匹配,返回None。匹配时返回一个匹配对象

re.fullmatch(pattern,string,flags=0):要求整个字符串string从头到尾与pattern匹配的话返回匹配到的对象。否则返回None。

区分matchfullmatch。对于正则表达式-\w{3},匹配一个-字符与3个字母。则在match中,-1111和以被搜索并返回-111,但是在fullmatch中会返回None。只有形如-123的字符串在能被搜索到。

re.split(pattern,string,maxsplit=0,flags=0):使用pattern匹配来作为分隔标志来分割stirng字符串,返回结果是一个列表。split的行为比较复杂:

  • 如果pattern中有有括号,则结果会包含pattern。否则pattern的内容在结果中不出现;

    >>> re.split(r'\W+', 'Words, words, words.')
    ['Words', 'words', 'words', ''] # 没有分隔符
    >>> re.split(r'(\W+)', 'Words, words, words.')
    ['Words', ', ', 'words', ', ', 'words', '.', ''] #有分隔符
    
  • 如果pattern会匹配到分割字符(换行,空格等),则结果会以一个空字符串开始,空字符串结束;

  • 样式的空匹配只在不相邻的情况下才会分割字符串,即如果pattern匹配的内容为空,不会因为连续匹配到空而死循环,任意的两个字符之间有无穷个空字符,但是只会再一个地方分割;(最迷惑的地方,仍然不是很清晰)

    # \W*会匹配0或多个非字符、数字、下划线。因此可以以空字符串为分割线。
    >>>re.split(r'\W*', '...words...')
    ['', '', 'w', 'o', 'r', 'd', 's', '', '']
    # 出现上述结果的原因:首先开头的空字符出现。,第二个空字符是被'...'划分出来的空字符,
    >>>re.split(r'(\W*)', '...words...')
    ['', '...', '', '', 'w', '', 'o', '', 'r', '', 'd', '', 's', '...', '', '', '']
    
  • maxsplit参数不为0的情况下。会限制最多的分割次数。剩下的字符穿一起出现在结果的最后。

re.findall(pattern,string,flags):找到string中不重复使用字母的所有匹配,数量大于1返回列表。

re.finditer(pattern,string,flags):功能同findall,但是将结果保存在迭代器中。

re.sub(pattern, repl, string, count=0, flags=0):正则表达式的替换操作:

  • repl为字符串时,将不重叠的匹配到的pattern替换为repl,返回替换后的字符串。
  • repl还可以为单个参数的函数,pattern将替换为rele以pattern匹配到的字符串为参数的函数的返回值。
  • count不为0时,最多执行count次替换。
  • 样式的空匹配仅在与前一个空匹配不相邻时才会被替换, sub('x*', '-', 'abxd') 返回 '-a-b--d-'

re.subn(pattern, repl, string, count=0, flags=0):行为与re.sub相同,但是返回一个元组(字符串, 替换次数)

re.escape(pattern):转义pattern中的特殊字符。如果你想对任意可能包含正则表达式元字符的文本字符串进行匹配,它就是有用的。

>>> print(re.escape('http://www.python.org'))
http://www\.python\.org

在 3.7 版更改: 只有在正则表达式中具有特殊含义的字符才会被转义。 因此, '!', '"', '%', "'", ',', '/', ':', ';', '<', '=', '>', '@'"”` 将不再会被转义。

匹配对象:

re.search()re.match()等函数返回的不是直接的字符串,而是一个匹配对象。下面介绍几个匹配对象的常用方法和属性

匹配对象在条件判断中会被判断为true

Match = re.match(pattern,string)

Match.group([group1, ...]):返回匹配对象多代表的字符串。根据参数的不同,有不同的行为。

  • 不带参数时,默认参数是一个0,返回的是全部匹配到的字符串。

  • 当正则表达式中出现子表达式时,非0参数才有意义。参数的最大值为子表达式的数量(可以嵌套。(()())这种情况的子表达式个数为3)。使用参数i,将返回从左往右第’i’个左括号对应的子表达式的匹配内容。填入多个参数时,返回元组。

    >>> re.search(r'((你)(好))',"你好").group(1) 
    '你好'
    >>> re.search(r'(你(好))',"你好").group(2) 
    '好'
    
  • Match对象重载了[]运算符,可以直接填入参数i。

Match.groups():返回所有的子表达式对应字符串的元组。

  >>> re.search(r'((你)(好))',"你好").groups() 
  ('你好', '你', '好')

Match.re:返回产生这个实例的正则对象,这个实例是由正则对象 match()或search() 方法产生的。

Match.string:传递到 match()或 search()的字符串。


c++中的正则表达式

在c++中使用正则表达式的常用方法有三种,各有优点。

首先是c语言中的<regex.h>库。支持标准的正则表达式。它的各方面性能是三种方法里最快的。

其次,从c++11开始,c++支持了<regex>库。但是目前貌似问题比较大。

最后,是以个第三方库,需要单独下载"boost/regex.hpp"。这个库是功能最多,使用人数较多,文档资料教为齐全的。在有些方面,性能甚至比c语言的<regex.h>库要快一点。从通用性上考虑,会优先学习这种。