news 2026/8/10 7:55:12

正则指引——转义、处理形式、优先级、回车和换行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
正则指引——转义、处理形式、优先级、回车和换行

转义、处理形式、优先级、回车和换行

    • 1、转义
      • 1.1、字符串转义与正则转义
      • 1.2、元字符的转义
      • 1.3、彻底消除元字符的特殊含义
      • 1.4、字符组中的转义
    • 2、正则表达式的处理形式
      • 2.1、函数式处理
      • 2.2、面向对象式处理
      • 2.3、比较
      • 2.4、线程安全性
    • 3、表达式中的优先级
    • 4、回车和换行

1、转义

正则表达式中的转义是一个麻烦的问题,经常会搞得人头疼。虽然之前各章都提到了转义,仍然有必要专门介绍。

1.1、字符串转义与正则转义

理解转义的基础是,明白字符串与正则表达式的关系。通常说的string(字符串)中,string称为字符串文字(String Literal)​​,它是某个字符串的值在源代码中的表现形式。比如字符串文字\n,它包含\n两个字符,意义(或者说它的值)是一个换行符(为方便观察,表示为NL​。在生成字符串时,应当进行“字符串转义”​,才能准确识别字符串文字中\n的意思,如下表所示。


另一方面,在源代码中看到的“正则表达式”regex,其中的regex称为正则表达式文字(Regular ExpressionLiteral,以下简称正则文字)​,是正则表达式的表现形式。比如正则表达式\d,其正则文字包含\和d两个字符,它的意义(或者说值)是匹配数字字符的字符组简记法。在生成正则表达式时应当进行“正则转义”​,才能将正则文字中的\d识别为字符组简记法。

这两点都不难理解,但从字符串转到正则表达式的过程就比较复杂。因为不少正则表达式都是以字符串形式提供的,所以必须经过从字符串文字到正则表达式的转换。根据上面的介绍,字符串文字首先必须经过“字符串转义”​,才能得到真正的字符串;接下来,这个字符串作为正则文字,经过“正则转义”​,最终得到正则表达式。下图说明了从字符串文字到正则表达式的转义过程,下表则列出了若干常用字符串的转义。



换行符和制表符的情况比较容易理解,它们的正则文字是\n\t,也就是字符串的值。但是从字符串文字必须经过字符串转义才能得到作为正则文字的\n\t,根据字符串转义的规则,反斜线字符\必须写成\,所以字符串文字必须写成\n\t`。

如果要表示正则表达式中的\,必须使用正则文字\\,这样在正则转义时才能正确识别。同时,正则文字中的每个\都必须用字符串文字\\表示,所以正则表达式\对应的字符串文字就是\\\\。网络上常有人问,某个正则表达式中为什么需要连续用到4个反斜线字符,原因就在这里。

\的转义


有时候情况更复杂:正则表达式中的换行符或者制表符,在字符串文字中必须写成\\n\\t;但是,用\n\t也没有问题。原因在于,在处理字符串转义时,它们已经被解释为换行符或制表符,所以传递给正则表达式的字符串中就包含了换行符或者制表符,具体情况如下表所示。

从下例可以看到,字符串文字中的\\n\n表示的是同一个字符,只是前者先做字符串转义,再做正则转义,而后者只做了字符串转义。看来,似乎还是\n更符合直觉一些—如果你确实能够理解,为什么字符串文字中\\n\n的结果竟然一样,使用\n确实可行。但是,正则表达式中\的字符串文字还是必须写成\\\\

\n和\t的转义

要特别注意的是\b。在一般字符串中,\b是预定义的转义序列,表示退格符(backspace,表示为BS​;但是在正则表达式中,它表示单词边界(记为\B)​。如果在字符串文字中写\b,字符串转义为退格符,作为正则表达式交给正则表达式,正则表达式真正得到的就是退格符\BS,而不是单词边界\B,如下表和下例所示。

所以,如果用到了单词边界,在字符串文字中一定要写成\\b。所以,最保险的办法是:正则表达式中的每一个\,在字符串文字中都要写成\\


\b的转义

在Python中也可以这样,如下例所示。

Python中 \d的转义

看起来足够奇怪,因为在字符串中\d\(都不是合法的转义序列,所以如果在Java中这样写,是会报编译错误的。为什么Python中可以呢?这是因为Python对字符串有特殊的规定:如果遇到无法识别的转义序列,则将它原封不动地保存下来,具体的处理见下表。

在PHP中也有同样的规定,所以在PHP的字符串文字中也可以直接写\d、\w、\s等,效果和\d、\w、\s是一样的。

这样看来,使用字符串形式的正则表达式,转义的处理确实比较复杂。最好是能省去这些麻烦—正则表达式是怎样的,正则文字中就怎样写。要做到这一点,有两种办法:第一,使用原生字符串(Raw String)​,也就是完全忽略字符串转义的特殊字符串,在Python、Ruby、.NET、Golang中都提供了原生字符串。

第二,直接使用正则文字,在Ruby和JavaScript中可以这么做。下面以正则表达式a\.\nb\b为例。这种方法一般是直接在正则表达式两端添加分隔符/。此时,字符串转义会被略过,不过另外还有一个字符需要转义,就是分隔符/


总的来看,转义的情况确实很复杂。我推荐的做法是:

  • 第一,如果可以使用原生字符串或者正则文字直接表示正则表达式,应当尽量使用这种做法,因为它简单直观,方便理解;​而且,本书从此开始,如果能使用原生字符串表示正则表达式,则使用原生字符串,在Python示例代码中使用r"regex"的形式。
  • 第二,如果确定必须使用字符串文字,请尽量坚持这条原则:正则表达式中的每个反斜线\,在字符串文字中都必须写成\,只有\n、\t中的反斜线例外(但是\n和\t也不难理解)​;我不推荐使用语言自身提供的“字符串转义时将无法识别的转义序列保留下来”的规定—比如要在正则表达式中使用字符组简记法\d,正则文字中要写为\d,而不是\d—这样有利于使用其他语言的程序员阅读。

1.2、元字符的转义

了解了字符串与表达式的关系,再来看正则表达式中元字符的转义。元字符是有特殊含义的字符,如果要匹配“元字符”自身则必须转义,也就是在元字符之前添加反斜线\。比如元字符点号.,可以匹配除换行符以外的任何字符,如果要准确匹配字符串中的点号.,正则表达式中就必须写\.

也有些时候,匹配元字符自身并非一定要转义,下表列出了各种结构的转义。



从表中可以看到,对称出现的元字符在转义时并不是“对称”的,比如与开方括号[对应的闭方括号],与开花括号{对应的闭花括号},这两个字符是否是元字符,取决于之前是否出现了开方括号或开花括号。如果出现了,则作为元字符出现;否则,作为普通字符出现。下表以方括号为例,说明了这一点。


字符组内部的闭方括号]在任何情况下都要转义,否则类似[​]​]的正则表达式会出现二义性,造成识别错误。所以能匹配字符a、字符b、字符]的字符组,应当写为[ab\]​],而不是[ab]​]。同样道理,括号内部的任何闭括号)都要转义,比如包含ab和b)的多选结构的正则表达式就应当写为(ab|c\)),而不是(ab|c))

另一点容易忽略的是,在进行正则表达式替换时,replacement字符串中也可能出现转义。比如在Java中,replacement里通过$num引用对应的捕获分组。如果想在替换时使用一个单独的$符号,而不是引用分组(比如生成价格字符串)​,则会报错,必须做转义才可以解决问题,在Java中使用\$,代码见下例。

Java中$的转义

.NET中的replacement中同样使用$num引用对应的捕获分组,但是.NET中$的转义并不是\$,而是$$,代码见下例。

.NET中$的转义

Condoe.Write(Regex.Replace("the price is 12.99", "\\d+\\.\\d{0, 2}", "$$$0")); the price is $12.99

1.3、彻底消除元字符的特殊含义

有些时候需要消除所有元字符的特殊含义,全部作为普通字符。这种情况经常发生在处理用户输入的场合:用户输入了某个字符串,需要根据这个字符串进行正则表达式查找。

假设某个文件中包含多个文本片段,用空格分隔,现在需要查找包含用户输入内容的片段:用户输入cat,查找包含cat的行,直接的思路是^.*cat.*$(使用多行模式,同时不能指定单行模式)​,假设用户输入的内容保存在变量userInput中,就应当用"^.*" + userInput + ".*$"得到查找用的正则表达式。

如果用户输入cat,这么做当然没有问题。如果用户输入的是ca*t,得到的正则表达式就是^.*ca*t.*$:本意是查找包含字符串ca*t的行,但*是正则表达式中的元字符,正则表达式ca*t能匹配字符串cat、caat、caaat,却不能匹配ca*t,这样就会产生错误。更麻烦的是,恶意用户可能会输入a(b*b*)b*之类的字符串,这样的正则表达式匹配起来会消耗非常多的资源,这就是正则表达式拒绝服务攻击​,严重时可能把服务程序打垮。

为解决这类问题,一些语言中的正则表达式提供了特殊结构,彻底消除元字符的特殊含义,提供真正“安全”的表达式(也就是普通字符串)​。最常见的做法是在需要消除元字符特殊含义的正则表达式两端添加\Q\E,也就是\Q…\E(其中Q表示“引用文本Quoted”​,而E表示“引用文本结束End”​)​,像下例那样。此时正则表达式\Qca*t\E就不再能匹配cat、caat、caaat,只能匹配ca*t\Q…\E只对在它内部的子表达式生效,所以在正则表达式^.*\Qca*t\E.*$中,^.$.*$会按照正则表达式的默认规则匹配,只有中间的ca*t作为普通字符串匹配,整个表达式能够匹配的就是确定包含ca*t的行。

用\Q…\E消除元字符的特殊含义


\Q…\E并不是所有语言都支持的,在本书介绍的语言中,明确支持它的有Java、PHP、Objective-C、Golang。其他语言虽然不支持\Q…\E的记法,但其中一些也提供了专门的处理函数消除元字符的特殊含义;比如.NET的Regex.Escape(text),它接收一个字符串,返回的字符串中的所有元字符都做了转义处理,消除了特殊含义,从其结果字符串生成的正则表达式,就只能匹配与text完全一样的字符串,代码见下例。

用专门的函数消除元字符的特殊含义


下表列出了各种语言中消除元字符特殊含义的函数。

PHP中的正则表达式两端必须出现分隔符,所以preg_quote()可以设定第二个参数明确指定分隔符;否则,假如分隔符是/,而text又包含/,就可能发生冲突。请记住,使用正则表达式时,仔细分辨并消除用户输入字符串中元字符的特殊含义,是不可忽略的步骤。

1.4、字符组中的转义

在正则表达式中,如果需要表示作为元字符的普通字符(比如*、?、(等)​,就需要使用转义,这一点不存在疑义。特殊的是,常见的元字符出现在字符组内部基本都不算元字符,也就是说,它们在字符组内部出现时,不需要转义。代码如下例所示。

字符组内部几乎没有元字符

之前讲过,字符组有自己的元字符规定,也有相应的转义规定:在字符组内部,只有三个字符需要转义。

  • 一个是闭方括号]​,如果不是作为字符组结束标志的闭方括号,则必须写成\],比如[0\]9],它可以匹配的字符是0]​、9
  • 一个是横线-,如果不是用于范围表示法(比如[0-9])​,必须写成\-,比如[0\-9],它可以匹配的字符是0-9,当然如果它紧跟在开方括号之后,也可以不用转义,[0\-9][-09]是等价的(我更推荐后一种写法,因为更清晰简洁)​;
  • 还有一个需要转义的字符是^,如果它不是用于排除型字符组([^ab])​,则应当写成\^,比如[\^ab],它可以匹配除^ab之外的任何字符,如果它不是紧跟在开方括号之后,也不用转义,[\^ab][a^b][ab^]是完全等价的(我更推荐后两种写法,因为更清晰简洁)​。从下例可以看到,这两种写法的结果是相同的。

字符组中三个需要转义的元字符

2、正则表达式的处理形式

在之前讲解正则表达式时主要使用的Python语言中,其具体办法是调用re这个package中的函数(方法)​,比如re.search()、re.findall()、re.sub()。选择合适的函数,将正则表达式和字符串传入,这是一种常见的方式,比如PHP也是如此。但是,正则表达式并不只有这一种处理形式,比如Java语言的处理形式就与Python和PHP大不一样,所以下面简单介绍常见的处理形式。

2.1、函数式处理

在函数式处理中,正则表达式的常见操作(查找、替换等)都有对应的函数,执行这些操作时,调用对应的函数,将正则表达式和字符串作为参数传入即可,Python、PHP是函数式处理的典型代表。

2.2、面向对象式处理

Java、.NET、Objective-C、Golang之类的语言中的正则表达式采取了不同的处理形式:进行正则表达式处理之前,必须生成专门的正则表达式对象(在不同的语言里,对象所属的类名不同)​,再调用此对象的成员函数。

2.3、比较

同样是进行查找操作,如果使用函数式处理,只需要调用对应的函数;但使用面向对象式处理,需要分步进行,首先生成各种对象,再调用对象自身的方法。看起来后者要麻烦很多,这种处理方式有什么好处呢?

之前讲过,正则表达式并不等于字符串,即便正则表达式是以字符串形式给出的,进行正则表达式操作之前,必须首先生成专用的“正则表达式对象”​。函数式处理隐去了生成的过程,感觉更加直接;面向对象式处理则暴露了生成的过程,感觉更加细致。

只要执行正则表达式操作,就会产生正则表达式对象;所以,面向对象式处理的代码虽然更烦琐,但是如果需要重复用到某些表达式,它的效率往往远高于函数式处理—在面向对象式处理中,可以将已经生成的对象作为变量保存起来,就不必重复生成了。

面向对象式处理的步骤分明、效率更高,而函数式处理的好处是方便顺手、代码简洁。所以最好的办法是,根据应用场合的不同,采取不同的处理方式:如果正则表达式只是单次使用,则选择函数式处理;如果正则表达式需要重复使用,则选择面向对象式处理。实际上,许多编程语言也提供了对应的设计,比如Python提供了一些面向对象式处理的方法,Java和.NET也提供了一些函数式处理的方法。

在Python中进行面向对象式处理,可以先调用re.compile()生成专门的RegexObject对象(也就是正则表达式对象)​,在进行正则表达式操作时,可以把这个对象作为参数,传给对应的函数;另一方面,re中各个函数也可作为对象自身的成员方法,所以调用生成好的RegexObject对象的成员方法,也是可以的,如下例所示。

Python中可以使用两种处理方式

Java语言虽然采用面向对象式处理,但也提供了一些便捷函数,比如Pattern.matches(regex,input),它用来验证input能否由regex匹配,如下例所示。

Java中也可以使用函数式处理


.NET中也提供了类似的方法,比如Regex.IsMatch(regex,input),它也用来验证input能否由regex匹配,代码如下例所示。

.NET(C#)中也可以使用函数式处理

2.4、线程安全性

如果采用面向对象式处理,可以将生成的正则表达式对象存储在变量中反复使用,以提高效率。在单线程编程的环境下,​“反复使用”当然没有问题,如果多个线程共用同一个正则表达式对象,结果会如何呢?

仔细观察面向对象式处理可以发现,在面向对象式处理中,一般会出现两个对象:一个是单独对应正则表达式的,可以叫作“正则表达式对象”​,比如Java中的Pattern、.NET中的Regex、Python中的RegexObject、Objective-C中的NSRegularExpression、Golang中的Regexp;另一个是在正则表达式与希望处理的字符串联系起来时生成的对象,可以叫作“匹配结果对象”​,比如Java中的Matcher、.NET中的Match、Python中的MatchObject。​这几个对象之间的关系如下图所示。

正则表达式对象本身基本没有什么状态可言,所以这个对象总是线程安全的,可以由多个线程共享。匹配结果对象则需要维护自身状态,比如当前匹配是否成功,当前匹配的开始或结束偏移值等。如果多个线程共享同一个匹配结果对象,有可能遇到这样的情况:某个线程正在查询当前匹配的开始位置,另一个线程调用了matcher.find()(Java中)或者Match.nextMatch()(.NET中)之类的方法,尝试进行下一次匹配,就可能产生混乱。所以匹配结果对象一般不是线程安全的,也不应由多个线程共享。

因此,最理想的办法是:多个线程可以共享同一个正则表达式对象,节省时间;但操作不同文本时,应当针对各个线程生成专属的匹配结果对象,如下图所示。


多线程环境下错误使用正则表达式(以Java为例)

多线程环境下正确使用正则表达式(以Java为例)

3、表达式中的优先级

正则表达式千变万化,都是由之前介绍的字符组、括号、量词等基本结构组合而成的,只要掌握了组合的规则,面对再复杂的表达式,都能把结构梳理清楚。

仔细观察会发现,正则表达式的元素之间的组合关系只有4种。

除列出的4种组合关系外,正则表达式中的其他结构,比如环视(?=…),都可以视为单独的元素,其中的正则表达式最终可以归类到上面的4种组合关系中。

所以,真正需要关心的就是这4种组合的优先级,详见下表。

有了上表,就可以拆解各种表达式了,下表给出了几个简单的例子。

如果你觉得上面这几个例子都不难理解,来看一个更复杂的表达式ab*(cd|e+)?|fg,它的优先级划分关系如下图所示。


判断优先级并不困难,一般来说,根据上面介绍的知识逐步拆解即可,但是多选结构值得单独强调。

常见的多选结构是以竖线配合括号(option1|option2)的形式给出来的;但是,多选结构并非必须与括号连用,如果没有括号,单独出现竖线|,也可以实现多选结构的功能,比如正则表达式ab|cd,它能匹配的字符串和(ab|cd)是完全一样的。

比较起来,后一种写法更加清楚,因为两端的括号明确限定了竖线所标识的多选结构的范围,所以很难混淆^(ab|cd)$和^ab|cd$,后者等于(^ab|cd$);而且在匹配之后,还可以使用分组及反向引用等功能(\1、\2之类)​,明确得到匹配的文本(不用括号则做不到这点)​,再进行下一步操作。但是,分组又会降低效率,尤其是在正则表达式非常复杂,或者要处理的文本非常多时,使用分组可能会严重影响效率。

考虑到这些因素,我推荐的做法是:应用多选结构一定不能省略括号,但同时,为了避免分组影响效率,如果不需要提取捕获文本,应当把普通的括号(…)改为非捕获型括号(?:…),这样既明确标识了多选结构,又免去了分组捕获的成本。不过在本书中,为了格式简单清晰考虑,除非特别说明,一般不使用非捕获型括号。

4、回车和换行

许多程序员经常搞不清楚回车\r和换行\n到底有什么区别,只知道两个字符写法不同。然而键盘上只有回车键,它起到的作用却是“换行”​。在大多数时候,我们只是在编辑文本时敲一下回车键,然后本行结束,光标自动转到下一行的开始位置。因为平台和软件的不同,有时候输入的是换行\n,有时候输入的是回车换行\r\n,历史久远的某些平台可能会输入\r。为什么会这样?其中的区别到底在哪里?

其实,这是一个历史问题。

在文字处理软件还没有发明的年代,大家只能用机械打字机来打字,如今还可以在一些电影里看到它的样子。不同于今天计算机中的文字处理软件,可以由计算机来排版,机械打字机的每个键“打”在纸上的位置都是相同的,为了避免字符输入重叠,每打印一个字符,打印纸就向左移动一格,这样下一个字符才会打在当前字符的右侧。实现这个功能的装置的中文名是“打印头”​,英文名是carriage—大概因为它比较像马车吧。

在使用机械打字机时,每打印完一行,就必须敲“回车”​,让打印头回到初始位置,再次打字仍然从最左侧开始。同时还不要忘记敲“换行”​,让打印纸向上移动一行,否则两行就会重叠在一起,后一行的字符直接打在前一行已有的字符上。

今天在计算机中,无论回车还是换行,都不会让两行文字重叠在一起,所以把回车和换行分开显得有点怪异。不过讲清楚它们背后的历史,确实能解开许多人的疑惑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/10 7:52:54

基于约束差分进化算法的微电网拓扑优化与Matlab实现

1. 项目背景与核心挑战微电网作为分布式能源系统的重要实现形式,正在经历从单一个体向多系统协同的演进。这个进化过程带来了一个关键的技术瓶颈:当数十个甚至上百个微电网需要互联时,传统的拓扑设计方法在计算效率和方案质量上都会遇到天花板…

作者头像 李华
网站建设 2026/8/10 7:52:30

重新定义Zotero插件管理:从繁琐手动到智能集成的转变

重新定义Zotero插件管理:从繁琐手动到智能集成的转变 【免费下载链接】zotero-addons Zotero Add-on Market | Zotero插件市场 | Browsing and installing plugins within Zotero 项目地址: https://gitcode.com/gh_mirrors/zo/zotero-addons 在学术研究日益…

作者头像 李华
网站建设 2026/8/10 7:52:23

按键提示音设计:从PCM原理到跨平台优化实践

1. 按键提示音原始数据解析与应用场景 在交互设计领域,按键提示音是提升用户体验的重要细节元素。作为从业十余年的UI设计师,我发现很多开发者容易忽视这个看似简单的功能背后隐藏的技术细节。今天我们就来深入探讨按键提示音的原始数据构成及其应用场景…

作者头像 李华
网站建设 2026/8/10 7:51:42

React Native与鸿蒙跨平台开发中的状态管理优化

1. React Native与鸿蒙跨平台开发中的状态管理痛点 在React Native与鸿蒙(HarmonyOS)的跨平台开发实践中,闭包(closure)中的groups/members状态管理一直是性能优化的重点难点。特别是在社交类、即时通讯等高并发交互场景下,不当的状态更新方式会导致界面…

作者头像 李华
网站建设 2026/8/10 7:50:29

前端开发者零基础速通Java:环境配置、Spring Boot与AI集成实战

如果你是一名前端开发者,或者对AI应用开发感兴趣,但一直被Java这门“后端老大哥”挡在门外,觉得环境配置复杂、概念抽象、不知从何学起,那么这篇文章就是为你准备的。我们直接切入核心:如何让零基础,尤其是…

作者头像 李华
网站建设 2026/8/10 7:49:27

GPT Codex与Vibe Coding:AI如何重塑Java开发者的编码心流体验

最近在技术社区里,一个词出现的频率越来越高:Vibe Coding。它不像传统的“敏捷开发”或“DevOps”那样有明确的定义,更像是一种感觉——一种在流畅、直觉式的编码状态下,想法能快速转化为代码的体验。很多开发者都渴望这种状态&am…

作者头像 李华