正则表达式(Regular Expression)

精通正则表达式

正则表达式可以说是一门艺术,经验的积累才能达到更好的境界!

目前主要存在三种正则表达式的版本:Perl (PCRE), POSIX Basic Regular Expressions(BRE), POSIX Extended Regular Expressions(ERE).

第一章:正则表达式入门

  • ^脱字符代表一行的开始,$美元符号代表一行的结尾。这里注意Singleline和Multiline模式的区别。

  • 在字符组内部,字符组元字符 ‘-’(连字符)表示一个范围。只有在字符组内部,连字符才是元字符–否则它就只能匹配普通的连字符号。但是如果‘-’出现在字符组的开始,它表示的也是普通的字符。同样的道理,问号和点号通常被当作元字符处理,但在字符组中则代表普通的字符。(这点其实可以被利用)

  • 下表是元字符及其在正则表达式上下文中的行为的一个完整列表:(自己来不断完善…)

    字符 含意
    \ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个后向引用、或一个八进制转义符。 例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘' 匹配 “” 而 “\(” 则匹配 “(“。
    ^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
    $ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
    * 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。 * 等价于{0,}。
    + 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
    ? 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 中的”do” 。? 等价于 {0,1}。
    {n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
    {n,} n 是一个非负整数。至少匹配n 次。 例如: ‘o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。 ‘o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
    {n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如: “o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
    ? 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。 非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。 例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。
    . 匹配除 “\n” 之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用象 ‘[.\n]’ 的模式。
    (pattern) 匹配pattern 并获取这一匹配。 所获取的匹配可以从产生的 Matches 集合得到,要匹配圆括号字符,请使用 ‘\(‘ 或 ‘\)’。
    (?:pattern) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。 这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。 例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。
    (?=pattern)
    肯定 顺序环视 :子表达式能够匹配右侧的文本
    正向预查,在任何匹配 pattern 的字符串开始处匹配查找字符串。
    这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。
    例如, ‘Windows (?=95|98|NT|2000)’ 能匹配 “Windows 2000” 中的 “Windows” , 但不能匹配 “Windows 3.1” 中的 “Windows”。
    预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索, 而不是从包含预查的字符之后开始。
    (?!pattern)
    否定顺序环视:子表达式不能匹配右侧的文本
    负向预查,在任何不匹配的字符串开始处匹配查找字符串。
    这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。
    例如’Windows (?!95|98|NT|2000)’ 能匹配 “Windows 3.1” 中的 “Windows”, 但不能匹配 “Windows 2000” 中的 “Windows”。
    预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索, 而不是从包含预查的字符之后开始
    (?<=pattern))))) 肯定逆序环视:子表达式能够匹配左侧的文本
    (?<!pattern))))) 否定逆序环视:子表达式不能匹配左侧的文本
    x|y 匹配 x 或 y。例如,’z|food’ 能匹配 “z” 或 “food”。’(z|f)ood’ 则匹配 “zood” 或 “food”。
    [xyz] 字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
    [^xyz] 负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’。
    [a-z] 字符范围。匹配指定范围内的任意字符。例如,’[a-z]’ 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。
    [^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。 例如,’[^a-z]’ 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。
    \b 匹配一个单词边界,也就是指单词和空格间的位置。 例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
    \B 匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
    \cx 匹配由x指明的控制字符。 例如, \cM 匹配一个 Control-M 或回车符。 x 的值必须为 A-Z 或 a-z 之一。否则将c视为一个原义的’c’字符。
    \d 匹配一个数字字符。等价于 [0-9]。
    \D 匹配一个非数字字符。等价于 [^0-9]。
    \f 匹配一个换页符。等价于 \x0c 和 \cL。
    \n 匹配一个换行符。等价于 \x0a 和 \cJ。
    \r 匹配一个回车符。等价于 \x0d 和 \cM。
    \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
    \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
    \t 匹配一个制表符。等价于 \x09 和 \cI。
    \v 匹配一个垂直制表符。等价于 \x0b 和 \cK。
    \w 匹配包括下划线的任何单词字符。等价于’[A-Za-z0-9_]’。
    \W 匹配任何非单词字符。等价于 ‘[^A-Za-z0-9_]’。
    \xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。 例如, ‘\x41’ 匹配 “A”。’\x041’ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。
    \num 匹配 nuuuuum,其中 num 是一个正整数。对所获取的匹配的 反向引用 。例如’(.)’ 匹配两个连续的相同字符。
    \n 标识一个八进制转义值或一个 反向引用 。 如果 \n 之前至少 n 个获取的子表达式,则 n 为后向引用。 否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
    \nm 标识一个八进制转义值或一个 反向引用 。 如果 \nm 之前至少有is preceded by at least nm 个获取得子表达式,则 nm 为后向引用。 如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的后向引用。 如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
    \nml 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
    \un 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如,\u00A9 匹配版权符号 (?)。
    \G 匹配的起始位置、或者上一次匹配的结束位置
    (?>pattern) 固化分组 :固化分组中的匹配一旦匹配,就是固定下来,不会保存回溯状态。 固化分组的实质就是抛弃备选状态!
    (?(if)then|else)
    条件判断 :(?(expression)yes|no) 可以视为 (?(?=expression)yes|no)。
    注意:::正则表达式中作决策最重要的一点就是: 决策的模式expression的长度为0,代表了yes或no的正则表达式将从与expression相同的位置开始匹配.
  • 字符组的特殊性在于: 关于元字符的规定是完全独立于正则表达式语言”主体” 的.

  • 多选结构和字符组是截然不同的,它们的功能完全不同,只是在有限的情况下,它们的表现相同。
    很明显,它们的作用域是不相同的,字符组只限定于一个字符,而多选结构则是一个完整的子表达式。

第二章:入门示例拓展

  • 捕获型括号的编号顺序是以开括号出现的顺序来决定的。
  • 环视最重要的特性就是不占用匹配字符,相当于只是站在那个地方看一看,但是并不走(传动)。
  • 环视结构不匹配任何字符,只匹配文本中的特定位置,这一点与单词分界符\b、锚点^和$类似。

第三章:正则表达式的特性和流派概览

  • 区分Unicode中的”代码点”和”字符”的概念,一个字符可能由多个代码点来构
    例如:à是由两个代码点构成(a)和钝重音的结合。许多程序似乎把”字符”和”代码点”视为等价,也就是说,点号可以匹配单个代码点,无论是基本字符还是组合字符。所以,à能够由[^..$]来匹配,面不是[^.$]。
    在支持Unicode的编辑器中输入正则表达式时,一定要记住组合字符的概念。
    比如[à]会被当作[a‘]。同样,如果两个代码点的字符如à后有一个量词,量词也只会作用于第二个代码点。
  • “单行模式”所谓的意义就是”点号通配模式”

    在此模式下,点号可以匹配任何字符,包括换行符;锚点^不能匹配字符串内部的换行符,而只能匹配目标字符串的起始位置。

    “多行模式”所谓的意义就是”增强的行锚点模式”

    在此模式下,点号不能匹配换行符。锚点^能够匹配字符串中内嵌的文件行的开头位置。 支持这些模式的流派通常会提供\A、\Z来唯一的匹配整个字符串的开始和结束。

  • Unicode标准还定义了每个字符的性质,许多支持Unicode的程序能够通过\p{quality}来支持其中的一部分。

    分类 等价表示法及描述
    \p{L} 字母
    \p{M} 不能单独出现,而必须与其它基本字符一起出现(重音符号、包围框、等等)的字符
    \p{Z} 用于表示分隔,但本身不可见的字符(各种空白字符)
    \p{S} 各种图形符号和字母符号
    \p{N} 任何数字字符
    \p{P} 标点字符
    \p{C} 匹配其它任何字符——很少用于正常字符
  • .NET提供:简单的字符组减法:[[a-z]-[aeiou]]
    Java提供:完整的字符组集合运算:[[a-z]&&[^aeiou]]

第四章:表达式的匹配原理

  • 优先选择最左端(最靠开头)的匹配结果。 标准的匹配量词(*、+、?和{m,n})是匹配优先的

  • 正则匹配的基本原理:匹配先从需要查找的字符串的起始位置尝试匹配。 在这里,尝试匹配的意思是:在当前位置测试整个正则表达式(可能很复杂)能匹配的每样文本。 如果在当前位置测试了所有的可能之后不能找到匹配结果,就需要从字符串中的第二个字符之前的位置开始重新尝试。 在找到匹配结果以前必须在所有的位置重复此过程。 只有在尝试过所有的起始位置(直到字符串的最后一个字符)都不能找到匹配结果的情况下,才会报告”匹配失败”。

  • NFA(表达式主导):是以表达式为中心,尝试找到正则表达式能匹配的文本的一部分。
    DFA(文本主导):以文本为中心,尝试能不能找出与文本相匹配的正则表达式的分支。
  • DFA不支持捕获型括号和回溯。

  • NFA引擎最重要的性质是:回溯!

    它会依次处理各个子表达式或组成元素,遇到需要在两个可能成功的可能中进行选择的时候,它会选择其一,同时记住另一个,以备稍后可能的需要。回溯使用的原则是LIFO(后进先出、极有可能使用的是栈来保存状态的)。

  • 固化分组

    具体来说 ,使用(?>…)的匹配与正常的匹配并无差别,但是如果 匹配进行到此结构之后(也就是,进行到固化分组的闭括号之后) ,那么此结构体中的所有备用状态都会被抛弃。(在固化分组的内部,该回溯的地方还是照样的回溯,只是在出固化分组后会抛弃备用状态)

  • 环视结构与固化分组类似,在环视结束之后,所有的备用状态都会被抛弃,所以可以用环视来模拟固化分组。(?>regex)可以用(?=(regex))1来模拟。

  • 匹配优先和忽略优先都不会影响需要检测路径的本身,而只会影响检测的顺序。如果不能匹配,无论是按照匹配优先还是忽略优先的顺序,最终每条路径都会被测试。

  • 固化分组对提高效率很有用,因为可以利用它来抛弃不必要的备选分支。

  • 多选结构的优先顺序:(所以,小心安排多选分支的顺序)

  • 传统NFA:多选结构既不是匹配优先的,也不是忽略优先的,而是按照从左到右的顺序排列的。

  • DFA和POSIX NFA:它们总是匹配所有多选分支中能匹配最多文本的那个。

  • POSIX标准规定:如果在字符串中的某个位置存在多个可能的匹配,应当返回的是最长的匹配。
    DFA会选择所有可能匹配中最长的匹配,称为”最左最长规则”。
  • NFA与DFA的比较:

    • DFA的速度与正则表达式无关,而NFA中两者直接相关。
    • DFA不需要太多的优化,因为它的速度很快。DFA在预编译时所做的优化效果,也要好于大多数NFA引擎复杂的优化措施。
    • DFA(或者POSIX NFA)返回最左边的最长的匹配的文本。传统型NFA可能返回同样的结果,也可能是别的结果,取决于文本和正则表达式。
    • NFA引擎能够提供许多DFA不支持的功能。如捕获括号、环视、忽略(占有)优先量词、固化分组等等。
  • POSIX NFA最大的特点是它最终必须尝试正则表达式的每一种可能,多选分支的顺序其实不重要。

第五章:正则表达式实用技巧

  • 集中关注在特定时刻真正容许匹配的字符。
  • .*经常被滥用,还是需要清楚的明白真正容许匹配和不容许匹配的字符。
  • 标准的正则表达式无法匹配任意深度的嵌套结构,但是现在许多主流的语言都提供了解决方案,如.NET、Perl、PHP。
  • 如果所有的正则部分都是可选结构,则往往是错误的,因为它会出现不期望的匹配。
  • 可以用\G来屏蔽引擎自带的驱动装置。

第六章:打造高效的正则表达式

  • 对于POSIX NFA,多个量词的嵌套会造成指数级的复杂度。
    而传统NFA在无法匹配时进行的尝试次数与POSIX NFA一样多,所以绝不要用多个量词的嵌套!
  • 常见的优化措施:

    • 加速某些操作:由于引擎可能经过特别的优化,所以\d往往会比[0-9]的效率更好。其它的类似。
    • 避免冗余操作:如果以\A、^、\b、$等开头或结尾可以避免进行无效的传动,往往能提高效率。
  • 正则表达式的应用原理:

    • 正则表达式编译:检查语法的正确性,如果正确,就编译为内部的形式。
    • 传动开始:将正则引擎”定位”到目标字符串的起始位置。
    • 元素检测:引擎开始测试正则表达式和文本,依次测试正则表达式的各个元素。
    • 寻找匹配结果:报告匹配成功或者继续寻找最长的匹配。
    • 传动装置的驱动:如果没有找到匹配,传动装置会驱动引擎,从文本的下一个字符开始新的一轮尝试。
    • 匹配彻底失败:如果从每一个字符开始都失败了,则报告失败。
  • 许多情况下,占有优先量词、固化分组会大大的提高效率,而且它们不会改变匹配的结果。因为它会在退出分组时抛弃备用状态,从而大大的减少回溯的次数。而且聪明的引擎实现方式应该会自己做到这一点。

  • 对于不太长的确定大小的量词,分开写也许更好。 ====比={4}快上100倍。同理:XX*代替X+、====={0,2}代替={5,7}。

  • 不要滥用括号、不要滥用字符组,使用非捕获型括号。

Python 中的正则表达式 [1]

  • Python中的正则表达式由re模块提供(原始的regex模块已被弃用),它会使用由C语言编写的正则引擎,所以速度很快。

  • 正则表达式对象是RegexObject类型,eg: pattern = re.compile('ab*', re.IGNORECASE)

  • 反斜杠的麻烦 : 如果要匹配字符串 "\section" ,那么我们需要如下的步骤:

    • 传递给正则引擎 "\\section" 而不是 "\section" ,因为这样引擎才知道pattern中是匹配一个真实存在的 \ 字符,而不是转义字符s;
    • 由于Python中字符串字面值的转义规则,所以写下 "\\\\section" 才能给正则引擎传递字符串 "\\section"

    大量重复的反斜杠所生成的字符串也很难懂,因此使用python raw字面值会使得pattern看起来更清晰。

    r"\\section" raw string取消字符串的转义,直接把 "\\section" 传递给了正则引擎,这样就清晰多了。

  • 执行匹配操作的函数:

    方法/属性 作用
    match() 决定 RE 是否在字符串 刚开始的位置 匹配
    search() 扫描字符串,找到这个 RE 匹配的位置
    findall() 找到 RE 匹配的所有子串,并把它们作为一个列表返回
    finditer() 找到 RE 匹配的所有子串,并把它们作为一个迭代器返回

    如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 MatchObject 实例,其中有这次匹配的信息。

    方法/属性 作用
    group() 返回被 RE 匹配的字符串 m.group() == 'tempo'
    start() 返回匹配开始的位置 m.start() == 0
    end() 返回匹配结束的位置 m.end() == 5
    span() 返回一个元组包含匹配 (开始,结束) 的位置 m.span() == (0, 5)

    注解

    必须注意match()的特殊性,它只会在从 刚开始的位置 就匹配时才匹配成功,也就是说match()成功时的start()一定等于0。

  • 对于常用的匹配和替换操作,re模块都提供了模块级的函数来方便使用, re.search(), re.match(), re.sub()
    对于只使用一次正则表达式,使用模块级函数也许更方便;但对于重复使用的正则表达式,使用编译对象则更有效率。
  • 常用的编译标志:

    标志 含义
    DOTALL, S 使 . 匹配包括换行在内的所有字符
    IGNORECASE, I 使匹配对大小写不敏感
    LOCALE, L 做本地化识别(locale-aware)匹配
    MULTILINE, M 多行匹配,影响 ^ 和 $
    VERBOSE, X 能够使用 REs 的 verbose 状态,使之被组织得更清晰易懂
  • 分组 :

    • 组用 “(” 和 “)” 来指定,并通过 group()、start()、end() 、span() 得到它们匹配文本的开始和结尾索引;

    • 组是从 0 开始计数的(左括号对应的编号)。组 0 总是存在;它就是整个 RE 所匹配的文本。

    • group() 可以一次输入多个组号,在这种情况下它将返回一个包含那些组所对应值的元组。

    • groups() 方法返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。

    • 模式中的逆向引用允许你指定先前捕获组的内容:

      >>> re.search(r'(\b\w+)\s+\1', 'Paris in the the spring').group() == 'the the'
      
  • 无捕获组命令组

    • 无捕获组: (?:...) ,它与普通分组几乎没有区别,但在修改已有组时尤其有用,你可以不用改变所有其他组号的情况下添加一个新组。

    • 命名组:命名 (?P<name>...) 、引用 (?P=name)

      >>> re.search(r'(?P<word>\b\w+)\s+(?P=word)', 'Paris in the the spring').group() == 'the the'
      
  • 前向界定符 ,又叫 零宽界定符 ,分为前向肯定界定符 (?=...) 和前向否定界定符 (?!...)

    注解

    前身界定符就是前面提到过的 顺序环视零宽是它们最大的特点 ,意味着他们不会消耗字符。

  • 修改字符串

    方法/属性 作用
    split() 将字符串在 RE 匹配的地方分片并生成一个列表,
    sub() 找到 RE 匹配的所有子串,并将其用一个不同的字符串替换
    subn() 与 sub() 相同,但返回新的字符串和替换次数

    有时,你不仅对定界符之间的文本感兴趣,也需要知道定界符是什么。如果捕获括号在 RE 中使用,那么它们的值也会当作列表的一部分返回:

    >>> p = re.compile(r'\W+')
    >>> p2 = re.compile(r'(\W+)')
    >>> p.split('This... is a test.')
    ['This', 'is', 'a', 'test', '']
    >>> p2.split('This... is a test.')
    ['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
    
  • 常见问题

    • 优先使用字符串的方法;

    • 注意match()只匹配开始的位置;

    • 贪婪 vs 不贪婪:不贪婪的限定符 *?、+?、?? 或 {m,n}?,尽可能匹配小的文本。

      >>> print re.match('<.*?>', s).group()   # 结果为<html>
      
    • 用 re.VERBOSE 忽略不在字符类中的空白符,可以使 REs 格式更加干净。

C++ 中的正则表达式

C++中常用的2个正则表达式库

  • boost 的 regex (C++11引入标准库)
  • Google 的 re2( 速度极其快,纯自动机实现

<regex> [2]

  • regex 最重要的就是一个类 regex ;两个迭代器 regex_iterator, regex_token_iterator ;三个核心函数 regex_match(), regex_search(), regex_replace()

  • regex 类 ,它实际上是basic_regex<char>的typedef,从字符串生成正则表达式对象,同时可以设置正则匹配的属性:

    regex reg("Color\\d+",boost::regex::icase|boost::regex::perl);
    
  • regex_match() 函数 ,测试 完全匹配 ,注意这里要求必须完全匹配。得到的匹配结果boost::cmatch包含了所有捕获结果(即正则中括号捕获的结果):

    std::string str;
    boost::cmatch mat;
    boost::regex reg( szReg );
    
    assert(regex_match(str,reg) == true);
    if(boost::regex_match( szStr, mat, reg)) {  //如果匹配成功
        //显示所有子串
        for(boost::cmatch::iterator itr=mat.begin(); itr!=mat.end(); ++itr) {
            //       指向子串对应首位置        指向子串对应尾位置          子串内容
            LOG(INFO) << itr->first-szStr << ' ' << itr->second-szStr << ' ' << *itr << endl;
        }
    }
    
  • regex_search() 函数 ,区别在于并不要求完全匹配,可以匹配子字符串,所以从文本中提取特定字符就很好用:

    string ss(szStr);
    boost::smatch mat;
    boost::regex reg( "\\d+" );    //查找字符串里的数字
    std::string::const_iterator start = ss.begin();
    std::string::const_iterator end = ss.end();
    while(boost::regex_search(start, end, mat, reg)) {   // regex_search一次只提取出一个匹配结果
        std::cout<< "SEARCHED: " << mat[0] << std::endl;
        start = mat[0].second;                           // 需要设置start以跳过当前已经匹配过的部分
    }
    

    regex_search()一次只匹配一个匹配项,如果要匹配所有的匹配项,当然可以用上面的循环,但是更好的选择是用regex_token_iterator

  • regex_replace() 函数 ,文本替换。它在输入数据中进行搜索,查找正则表达式的所有匹配,对于每个匹配,该算法调用match_results::format。 在替换的正则表达式中,$1~$9(或1~9)表示第几个子串,$&表示整个串,$`表示第一个串,$’表示最后未处理的串;(?1~?9新字串)表示把第几个子串替换成新字串:

    boost::regex reg( szReg );
    string s = boost::regex_replace( string(szStr), reg, "ftp://$2$5");
    cout << "ftp site:"<< s << endl;
    
    string s1 = "(<)|(>)|(&)";
    string s2 = "(?1&lt;)(?2&gt;)(?3&amp;)";
    boost::regex reg( s1 );
    string s = boost::regex_replace( string("cout << a&b << endl;"), reg, s2, boost::match_default | boost::format_all);
    cout << "HTML:"<< s << endl;   // 输出:HTML: cout &lt;&lt; a&amp;b &lt;&lt; endl;
    
  • regex_iterator 迭代器 ,这个迭代器是访问所有与模式匹配的子串的最佳选择,比循环用regex_search()更好用:

    //使用迭代器找出所有数字
    boost::regex reg( "\\d+" );    //查找字符串里的数字
    boost::cregex_iterator itrBegin = make_regex_iterator(szStr,reg); //(szStr, szStr+strlen(szStr), reg);
    boost::cregex_iterator itrEnd;
    for(boost::cregex_iterator itr=itrBegin; itr!=itrEnd; ++itr)
    {
        //       指向子串对应首位置        指向子串对应尾位置          子串内容
        LOG(INFO) << (*itr)[0].first-szStr << ' ' << (*itr)[0].second-szStr << ' ' << *itr << endl;
    }
    
  • regex_token_iterator 迭代器 ,它和regex_iterator相似,但是它列举的是与正则表达式不匹配的字符序列,该特性对于字符串分割非常有用。 make_regex_token_iterator函数简化regex_token_iterator的构造。 最后的参数用-1表示以reg为分隔标志拆分字符串,如果不是-1则表示取第几个子串,并且可以使用数组来表示同时要取几个子串:

    const char *szStr = "http://www.cppprog.com/48.html";
    //使用迭代器拆分字符串
    boost::regex reg("/");  //按/符拆分字符串
    boost::cregex_token_iterator itrBegin = make_regex_token_iterator(szStr,reg,-1); //使用-1参数时拆分
    boost::cregex_token_iterator itrEnd;
    for(boost::cregex_token_iterator itr=itrBegin; itr!=itrEnd; ++itr) {
        cout  << *itr << ",";
    }
    
    cout << endl;
    boost::regex reg("(.)/(.)");    //取/的前一字符和后一字符
    int subs[] = {1,2};             // 第一子串和第二子串, 即模式中的2个(.)
    boost::cregex_token_iterator itrBegin = make_regex_token_iterator(szStr,reg,subs); //使用其它数字时表示取第几个子串,可使用数组取多个串
    boost::cregex_token_iterator itrEnd;
    for(boost::cregex_token_iterator itr=itrBegin; itr!=itrEnd; ++itr) {
        cout << *itr << ",";
    }
    

    输出:

    http:,,www.cppprog.com,48.html,
    :,/,m,2,9,1,2,4,
    
  • regex_error 异常 ,所有正则表达式的异常都是继承自regex_error的,所以可以通过catch它来捕获所有抛出的异常。

    >>> class regex_error : public std::runtime_error {...}
    

re2 [3]

Google的re2号称最快的正则表达式引擎,纯自动机实现,性能超乎想象的高,而且只使用了0.5MB的内存作为空间换时间的开销。 于是在关心性能的时候,re2就成了最佳选择,而且它的功能也一般都能够满足需求。

  • re2的历史:由Go语言作者之一的Russ Cox开发,这是他发现其它的引擎都是非纯自动机实现,于是就开发了这个纯自动机实现的正则引擎。

  • RE2主要有 两个主要匹配接口FullMatchPartialMatch ,它们的区别在于一个要求 完全匹配 另一个不要求:

    EXPECT_EQ(RE2::FullMatch("123 hello:1234", "(\\w+):(\\d+)"), false)
    EXPECT_EQ(RE2::PartialMatch("123 hello:1234", "(\\w+):(\\d+)"), true)
    
  • 预编译 正则表达式,对于经常使用的Pattern必须预编译好:

    RE2 pattern("(\\w+):(\\d+)", RE2::Quiet);
    assert(RE2::PartialMatch("hello:1234", pattern, &s, &i));  // 成功匹配
    
  • re2::RE2::Options 设置正则匹配时的选项,最常用的莫过于大小写敏感选项:

    string str = "aaaaa;bbbbb;cccccc;ddddd";
    string out;
    re2::RE2::Options o;
    o.set_case_sensitive(false);
    re2::RE2 p0("B+", o);  // 由于设置了大小写不敏感,所以这里大写的B也能匹配小写的b
    re2::RE2::Extract(str, p0, "\\0-\\0", &out);
    LOG(INFO) << "OUT: " << out;
    
    [9637:0523/162211:INFO:image/category_browsing/category/tests/tests.cpp(32)] OUT: bbbbb-bbbbb
    
  • re2非常便捷的一点在于,可以在匹配时使用 非常直观的语法进行组的捕获和赋值

    int32_t i;
    string s;
    
    // 成功匹配
    assert(RE2::PartialMatch("hello:1234", "(\\w+):(\\d+)", &s, &i));
    assert(s == "hello");
    assert(i == 1234);
    
    // 失败:"hello"不能被解析成一个整形
    assert(!RE2::PartialMatch("hello:1234", "(\\w+):(\\d+)", &i));
    
    // 成功:跳过NULL参数
    assert(RE2::PartialMatch("hello:1234", "(\\w+):(\\d+)", (void*)NULL, &i));
    
    // 失败:整形越界了
    assert(!RE2::PartialMatch("hello:123456789123", "(\\w+):(\\d+)", &s, &i));
    
  • 循环匹配 ,使用 StringPiece + Consume系列方法 来进行多次匹配:

    string str = "aaaaa;bbbbb;cccccc;ddddd";
    string word;
    
    re2::RE2 p1("(\\w+)");
    re2::StringPiece input1(str);
    while (RE2::FindAndConsume(&input1, p1, &word)) {
      LOG(INFO) << word << " | " << input1;
      word.clear();
    }
    
    re2::RE2 p2("(\\w+);");  // 注意这里p2与p1的区别,p2必须把间隔符也带上
    re2::StringPiece input2(str);
    while (RE2::Consume(&input2, p2, &word)) {
      LOG(INFO) << word << " - " << input2;
      word.clear();
    }
    
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(32)] aaaaa | ;bbbbb;cccccc;ddddd
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(32)] bbbbb | ;cccccc;ddddd
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(32)] cccccc | ;ddddd
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(32)] ddddd |
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(39)] aaaaa - bbbbb;cccccc;ddddd
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(39)] bbbbb - cccccc;ddddd
    [7860:0523/152154:INFO:image/category_browsing/category/tests/tests.cpp(39)] cccccc - ddddd
    

    ConsumeFindAndConsume 的区别在于, Consume 必须要求从头匹配。 所以p2正则的后面必须加上 ; 以使得input2每次后移之后能够从头匹配。

    因此,对于一般的任务来说, FindAndConsume 应该更实用一些,它不要求从头匹配。

  • 使用 动态参数 ,用于在运行时生成的正则表达式等场景:

    const RE2::Arg* args[10];
    int n;
    // running... to generate: pattern, args, n
    bool match = RE2::FullMatchN(input, pattern, args, n);
    bool match = RE2::FullMatch(input, pattern, *args[0], *args[1], ..., *args[n - 1]);  // equivalent to this
    
  • 在匹配时还可以 解析不同格式的数字 :8进制、16进制、C风格的数字:

    int a, b, c, d;
    RE2::FullMatch("100 40 0100 0x40", "(.*) (.*) (.*) (.*)", RE2::Octal(&a), RE2::Hex(&b), RE2::CRadix(&c), RE2::CRadix(&d))
    

    这段代码的结果是 a=b=c=d=64 ,注意 RE2::CRadix C风格的数字表示法,即以0开头是8进制,0x开头是16进制,一般为10进制,这在计算机中很常用。

  • 替换操作 : new_str中可以使用 \0, \1, ..., \n 来捕获p中出现的组。

    • Relpace(in, p, new_str) 只替换第一次出现的子串
    • GlobalReplace(in, p, new_str) 全局替换所有出现的子串
    • Extract(in, p, new_str, &out) 替换第一次出现的子串,并用新模式进行替换后复制到out,这个方法的有用之处在于在new_str中使用 \0, \1, ..., \n 来捕获p中出现的组。
  • QuoteMeta(str) 对str中的 正则元字符进行转义

    太棒了!这样的话就可以不用写一个难懂的、充满了转义符的正则表达式字符串,用这个函数优美的解决!

脚注

[1]Python 中的正则表达式参考 此文
[2]boost regex reference
[3]re2的 项目主页 ,以及它所支持的 语法