公司动态

正则指引——匹配模式

📅 2026/8/10 8:33:12
正则指引——匹配模式
匹配模式1、不区分大小写模式与模式的指定方式2、单行模式3、多行模式4、注释模式5、补充5.1、更多的模式5.2、修饰符的作用范围5.3、失效修饰符5.4、模式与反向引用5.5、冲突策略5.6、哪种方式更好所谓匹配模式match mode​指的是匹配时遵循的规则。设置特定的模式可能会改变对正则表达式的识别也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有4种不区分大小写模式单行模式多行模式注释模式1、不区分大小写模式与模式的指定方式通常用户关心的只是文本的意义而不是它的具体形式。比如单词the在句子中写作the在句子开头写作The还可能为了强调写作THE可是用户不管这些只希望找到所有的the。为达到这个目的可以使用第1章介绍的字符组写作[tT]​[hH]​[eE]这样做确实没错但是如果单词长一些写起来就很麻烦了比如tomorrow就要写成[tT]​[oO]​[mM]​[oO][rR]​[rR]​[oO]​[wW]。更重要的是这样的表达式不够直观读代码的人很难明白[tT]​[oO]​[mM]​[oO]​[rR]​[rR]​[oO]​[wW]要匹配的是tomorrow。为解决这种问题正则表达式提供了不区分大小写的匹配模式指定此模式之后在正则表达式中可以直接写the就可以匹配the、The、THE等各种大小写形式的thetomorrow也可以匹配各种形式的tomorrow大大降低了理解的难度。在看这个模式的应用实例之前必须首先了解模式的指定方式。通常有两种办法指定匹配模式以模式修饰符指定以预定义的常量作为特殊参数传入来指定模式修饰符即模式名称对应的单个字符使用时将其填入特定结构(?modifier)中其中的modifier为模式修饰符​嵌在正则表达式的开头。比如不区分大小写的匹配模式对应的模式修饰符是icase Insensitive​对the指定此模式完整的正则表达式就是(?i)the。这个表达式几乎可以原封不动地用在任何语言中只有JavaScript是例外JavaScript不支持模式修饰符(?modifier)的记法​其意义都是相同的对the采用不区分大小写的模式。Python中的结果下例所示。用模式修饰符指定不区分大小写模式另一种指定模式的方式是使用预定义的常量作为参数传入正则函数。在Python中不区分大小写的预定义常量是Re类的静态成员re.IGNORECASE一般来说它都是某个类的静态成员​在Java中它属于Pattern类在.NET中属于RegexOptions在Ruby中属于Regexp。PHP和JavaScript是例外它们的做法其实在Ruby中也可以这么做是在正则表达式末尾的分隔符delimeter之后加上模式对应的字母比如不区分大小写模式对应的字母是i则添加字母i​。下表列出了常用语言中的写法。Golang的语法更加奇怪似乎完全不支持这种做法只能在表达式中以修饰符来指定匹配模式。各语言中用预定义常量指定不区分大小写模式的例子参见下例。各语言中用预定义常量指定不区分大小写模式比较两种指定形式模式修饰符较为通用因为在各种语言中写法基本相同而预定义常量在不同语言中写法不同。不过两种形式的效果是相同的无论以哪种方式只要指定了不区分大小写模式正则表达式在匹配时就不会区分同一个字母的大小写形式(?i)the和(?i)THE是完全等价的​。之前看到过匹配HTML中tag的例子比如匹配超链接tag的正则表达式、匹配图片tag及网页标题tag的正则表达式​虽然XHTML规范推荐tag名都用小写字母但类似IMG的tag也很有可能出现为同时兼容大写字母可以使用不区分大小写模式这样比用字符组详细列出字母的大小写形式简单很多如表所示。2、单行模式元字符点号.几乎能匹配任何字符唯有换行符\n是例外。但是有时候确实需要匹配“任何字符”​比如在处理HTML源代码时经常会遇到跨越多行的脚本代码。正则文档里一般都会说明“点号.不能匹配换行符”​不过许多人并不习惯仔细阅读文档所以认为点号.能匹配任何字符当然也就包括换行符所以直接的想法是用script\s.*?/script来匹配。因为这段JavaScript代码中出现了换行符所以.*?的匹配最多只能延伸到第一行末尾。之前提到过可以用[\s\S]之类的字符组匹配“任意字符”​所以正则表达式script\s[\s\S]*?/script能解决问题。不过对大多数人来说点号更自然也更简洁所以正则表达式提供了单行模式。在这种模式下所有文本似乎只在一行里换行符是这一行中的“普通字符”​所以可以由点号.匹配。单行模式对应的模式修饰符是sSingle line​所以如果用模式修饰符可以在表达式的开头用(?s)指定因此上面的表达式也可以改写为(?s)script\s.*?/script。使用预定义常量的写法见下表。你可能注意到了单行模式在不同语言中的称呼很不一样甚至在同一门语言中也可能有不同的记法比如在Python中​。比如在Java和Python中叫作DOTALL也就是点号通配​这个名字确实更高明因为常用的模式中还包含“多行模式”​它和“单行模式”没什么联系但是这两个名字确实很迷惑人​。不过​“单行模式”成了约定俗成的称呼通用的模式修饰符是s它难以联系上DOTALL​所以本书还是沿用“单行模式”的说法。比较奇怪的是Ruby的预定义常量Multiline它的意思是“多行”​而且它使用的模式修饰符也是m。这确实让人费解或许本意是“使用点号.的正则表达式可以跨越多行”​不管怎样请一定记住Ruby中的“多行模式”实际上是常说的“单行模式”​。如果不想使用“点号单行模式”的组合比如JavaScript完全不支持这种模式想用也没办法​也可以使用[\s\S]之类的字符组它的确可以匹配任何字符只是许多人并不习惯这样的写法​“点号单行模式”的组合看起来更顺眼一些。3、多行模式“多行模式”听起来是与“单行模式”对应的其实这两个模式没有任何联系。单行模式影响的是点号的匹配规则在默认模式下点号.可以匹配除换行符之外的任何字符在单行模式下点号.可以匹配包括换行符在内的任何字符多行模式影响的是^和$的匹配规则在默认模式下^和$匹配的是整个字符串的起始位置和结束位置但在多行模式下它们也能匹配字符串内部某一行文本的起始位置和结束位置。假设需要找到下面文本中所有数字字符开头的行。解决这个问题需要定位到每行的起始位置尝试匹配一个数字字符如果成功则匹配之后的整行文本。多行模式的模式修饰符是mMultiline​所以在表达式的开头用(?m)指定多行模式这样^可以定位到字符串内部每一行的起始位置匹配数字字符的表达式是\d因为没有指定单行模式点号.不能匹配换行符.*可以匹配“之后的整行文本”​整个表达式就是(?m)^\d.*示例见下例。用模式修饰符指定多行模式还可以利用多行模式下的$给每一行的末尾添加句号如下例所示。在多行模式下给行末添加句号上面的表达式几乎是任何语言中都“通用”的唯有JavaScript是例外因为它不支持用模式修饰符指定模式但是可以使用预定义常量来指定多行模式。在下表中列出了常用语言中预定义常量的写法。在各种语言中多行模式对应预定义常量的写法比较统一都是multiline。值得一提的是Ruby它默认就采用多行模式^和$在任何情况下都能匹配文本内部的行起始/结束位置。如果要在Ruby中“摆脱”多行模式只能用\A替代^用\Z替代$。4、注释模式有时用到的正则表达式可能非常复杂不但难以编写和阅读也难以维护。如果正则表达式也像程序源代码一样可以添加注释阅读和维护起来就容易多了。为解决这个问题许多语言支持使用(?#comment)的记法添加注释comment就是注释的内容。所以上面的表达式^\d.*?$就可以写成这样.NET、Python、Ruby、PHP、Objective-C都支持这种记法Java、JavaScript、Golang不支持。不过还有一种注释的写法是各种语言都支持的就是使用注释模式此时正则表达式对应的字符串可以跨越很多行。注释模式的代码见下例。注释模式在注释模式下正则表达式内部的空白字符都被忽略一般来说主要是ASCII编码中的空白字符Unicode编码中的空白字符情况不定​注释则以#comment的形式添加在正则表达式内部每一条注释从#开始到行末结束。许多文档中都用这种模式来解释复杂的表达式并且会使用缩进表示层级结构这样更加方便阅读和维护。比如匹配日期的正则表达式((?x)(\d{4})-(\d{2})-(\d{2}))在注释模式下可以就这样像下例这样展开。在注释模式下展开复杂正则表达式注释模式对应的模式修饰符是xextended mode扩展模式但更常见的写法是free-spacing mode宽松格式模式​。下表介绍了各语言中注释模式的预定义常量。你可能注意到了实力同时指定了两种模式多行模式和注释模式。注释模式的x与多行模式的模式修饰符m合写作(?mx)。如果需要同时使用多种模式只要在(?modifier)中将模式修饰符排列起来就可以了。如果希望同时指定多行模式和注释模式使用预定义常量该怎么做答案是使用位运算符|。通常来说匹配模式对应的预定义常量都是int类型所以多个值进行按位与的结果并不会彼此干扰。比如在Java中对应的写法就是Pattern.COMMENTS | Pattern.MULTILINE在.NET、Ruby和Python中也可以这样。如果是PHP和JavaScript则在结束的分隔符之后直接并列模式对应的修饰符比如/regex/mx这种写法在Ruby中也行得通。5、补充5.1、更多的模式上面提到的4种常用模式是各种语言中通用的但是匹配模式并不只有这4种不同的语言提供了不同的模式它们一般都在预定义常量中而且不一定有对应的模式修饰符。比如Java的Pattern还包含其他模式仅列举两种模式更多的请参看文档​。Pattern.UNIX_LINES在此模式下^、.、$识别的“行终止符”只有\n而不能是其他字符比如\r\n​它对应的模式修饰符是d。Pattern.CANON_EQ在此模式下字符的“相等”规则更加灵活Unicode字符a\u030A与\u00E5也是“相等”的都是å也就是拉丁字母a加上分音符只是前者用两个字符组合后者用单个字符​此模式可能对性能有很大影响而且没有对应的模式修饰符。Python的re也包含了其他模式仅列举两种模式更多的请参阅文档​。re.U或re.UNICODE在此模式下\w、\d、\s等字符组简记法的匹配规则会发生改变比如\w能匹配Unicode中的“单词字符”​包括中文字符\d也能匹配1、2之类的全角数字字符它有对应的模式修饰符u。re.A或re.ASCII因为在Python 3以上的版本中正则表达式默认采用Unicode匹配规则如果希望让\d、\w等字符组简记法恢复到ASCII匹配规则可以使用此模式它有对应的模式修饰符a。在.NET和PHP中也有其他模式可用。不过一般来说本章介绍的4种模式最为常用在其他模式中只有涉及Unicode或者ASCII的模式使用较多因为它们影响了字符组简记法\d、\s、\w的匹配规则。关于每种语言可用模式的具体信息请参考该语言对应的章节或文档。5.2、修饰符的作用范围常见的模式修饰符是(?modifier)形式的它表示“从现在开始使用某个模式”​。通常的做法是将它写在正则表达式的最开头表示“整个正则表达式都指定此模式”​如果它出现在正则表达式当中则表示此模式从这里开始生效​如果模式修饰符出现在某个括号内—比如((?modifier)…)—那么它的作用范围只限于括号内部此模式也可以记为(?modifier:…)。模式修饰符的作用范围见下表所示。模式修饰符对正则表达式的操控性更强因为预定义常量指定的匹配模式是对整个表达式生效的。5.3、失效修饰符(?modifier)指定了匹配模式开始作用的范围在正则表达式中另有一类失效修饰符它用来“终止”某种模式的作用范围其形式是(?-modifier)类似(?modifier)只是问号?之后多了一个减号-表示“取消模式”​也就是某个模式生效到此处为止。假设一段文本中包含了许多单词其中以bar结尾的有foobar、zeebar等而且大小写不定。现在希望找出所有这样的foobar和zeebar不论foo和zee的大小写如何只要结尾是大写的BAR就可以。FooBAR、ZEEBAR、zeeBAR都符合要求Foobar、zooBar、feeBAR等则要排除。你可能会觉得这很简单要找到的单词前面部分可能是zee或是foo不区分大小写则用字符组把大小写形式都列出来即可后面必定是BAR所以正则表达式是[fFzZ]​[oOeE][oOeE]BAR。可是这个表达式行不通因为它可以匹配feeBAR而这并不是我们需要的。真正要做的其实是准确划定不区分大小写模式的作用范围即可匹配前面部分的表达式是(foo|zee)同时必须使用不区分大小写模式匹配后面部分的BAR则必须停止使用不区分大小写模式。要满足这个要求可以使用上一节介绍的((?i)foo|zee)BAR也可以使用失效修饰符将表达式写作(?i)(foo|zee)(?-i)BAR代码见下例。因为Python不支持这种写法所以使用了Ruby​。不区分大小写模式与反向引用一般来说只要语言或工具支持模式修饰符(?modifier)都可以支持失效修饰符(?-modifier)两者配合使用可以更精确地设定模式的作用范围。不过Python和JavaScript并不支持(?-modifier)的写法。Golang的文档里虽然没有明确指出支持但实际代码测试可以支持。5.4、模式与反向引用反向引用前面介绍过它引用之前的表达式匹配的文本。如果之前的表达式使用了某种模式引用时是否会继承这种模式呢下面看看实际情况因为在Python中无法限定模式的作用范围现在以Java为例代码见下例。不区分大小写模式与反向引用在表达式((?i)abc)\1中\1引用的是(?i)abc能匹配的文本在这个例子里就是abc​因为\1不在区分大小写模式的作用范围内所以无法匹配ABC而在表达式(?i)(abc)\1中\1处在区分大小写模式的作用范围内所以可以成功匹配ABC。在下例所示的两个表达式中\1引用的是之前a.匹配的文本在单行模式下.可以匹配换行符所以a.匹配的是字符a和换行符\n因此\1也可以匹配字符a和换行符\n。单行模式与反向引用在下例的两个表达式中\1引用的是之前^a匹配的文本^a只能匹配行开头的a但是无论\1是否处在多行模式的作用范围内它能匹配的不只有行开头的a而是任何位置的字符a这一点值得注意。多行模式与反向引用5.5、冲突策略使用(?-modifier)可以终止某个模式的作用范围但是模式也可以通过预定义常量来指定它是对整个表达式生效的。这时候就可能产生冲突。如果在正则表达式中使用(?-i)取消不区分大小写的匹配模式又使用了预定义常量比如Java中的Pattern.CASE_INSENSITIVE指定整个表达式采用不区分大小写的模式这时候情况会是怎样呢从下例可以看到。冲突策略可以看到模式修饰符具有更高的优先级。严格地说如果用预定义常量指定了整个正则表达式采用某种模式同时正则表达式内部使用了关闭该模式的失效修饰符则失效修饰符之后的部分都不受预定义常量所指定模式的影响失效修饰符之前的部分则不受影响​。并不是每种语言的文档都会针对这种情况做详细讲解但大家的原则一致。下例给出了具体的例子。更详细的例子5.6、哪种方式更好模式修饰符和预定义常量都可以指定匹配模式哪种方式更好这个问题没有标准答案。用模式修饰符指定的好处之一是简洁因为通用的模式修饰符就只有ixsm虽然不那么直观但习惯之后并不难理解而且这些记法在各语言中都是通用的再者模式修饰符还可以用(?i)、(?-i)来精确控制模式的作用范围这是预定义常量做不到的另外许多正则处理函数只接受字符串形式的正则表达式此时只能以模式修饰符来标识模式。使用预定义常量的好处在于它很形象—“单行模式”​“多行模式”的说法确实很让人困惑尤其它们竟然可以同时使用互不干扰而看字面意思分明是互相矛盾的而且一些模式并没有对应的修饰符比如Java中的Pattern.LITERAL消除所有元字符的特殊含义​、.NET中的RegexOptions.ECMAScript字符组简记法采用ASCII匹配规则​。我的建议是熟练掌握常用的模式在能够使用模式修饰符的地方尽量使用修饰符。毕竟无论使用哪种编程语言(?i)是熟悉正则表达式的人都能看懂的但是不是每个人都能记得的Java中的Pattern.DOTALL等价于.NET中的RegexOptions.Singleline也不是每个人都能记得Java中的Pattern.CASE_INSENSITIVE等价于RegexOptions.IgnoreCase因此理解起来总是要多点周折。