• 正则表达式使用文档


    通过网站 https://regex101.com/ 可以测试正则表达式的匹配结果及匹配过程.

    本文章抛开各个编程语言实现差异, 仅做正则本身的介绍, 会尽量将正则这玩意说明白, 使得你看完这边文章后对正则基本可以运用自如.

    温馨提示, 这篇文章会比较长, 大致浏览即可. 正确的方式是收藏起来, 等到使用正则的时候翻看

    语法介绍

    在平常进行字符串匹配的时候如何做呢? 比如希望从字符串Hello Word中匹配到第一个单词, 我们就会拿着Hello子串进行匹配.

    正则表达式的表示与其相同, 区别是在子串匹配时每个字符都会进行原样匹配, 而正则表达式中会存在一些特殊符号, 这些符号会代表一些特殊的含义, 如a+的意思是匹配任意多个连续的a字符, 是不是十分简单?

    如此说来, 要使用正则表达式, 关键点就在于了解其中的特殊字符上.

    分类字符含义
    单字符.任意字符(换行符除外)
    \d任意数字
    \D非数字
    \w字母数字下划线
    \W非字母数字下划线
    $字符串结束位置
    ^字符串开始位置
    空白符\s任意空白字符
    \S非空白字符(包括空格)
    \r回车
    \n换行
    \f换页
    \t制表符
    \v垂直制表符
    量词{m}前面内容出现 m 次
    {m,}>=m次
    {m,n}m-n 次
    *同 {0,}
    +同 {1,}
    ?同 {0, 1}
    范围选择|或. eg: `ab
    […]单字符多选一. eg: [abcd] msg: a或b或c或d
    [a-c]ASCII 表范围. eg: [a-z] msg: 所有小写字母
    若其中的-是需要匹配的单字符, 需使用\-进行转义
    [^…]取反. []中标识的字符外的任意字符

    以上所有均可以任意嵌套使用, 如:

    • https?|ftp: 可匹配 http https ftp

    高级用法

    分组

    有这样一个正则表达式 ab{3} , 它会匹配字符串 abbb. 如果我们想要匹配字符串 ababab , 如何在正则表达式中指定令ab 重复3次呢?

    用程序员的通俗思维想一下, 没错, 加括号. (ab){3} 的意思就是匹配字符串 ababab. 而这, 就是分组.

    有的小朋友会问题了, 这不就是指定下优先级嘛, 和分组有什么关系? 为什么叫分组呢? 别急, 往下看

    在正则表达式中, 分组有如下作用:

    1. 在表达式后面可以进行引用
    2. 在匹配结果中, 会将匹配的分组同时提取出来

    正则引用分组

    在正则表达式中, 可以通过 \1 来引用分组. 其中的数字是分组的编号, 从1开始. 从左往右依次递增.

    比如正则表达式 (ab)(cd)\2\1 会匹配字符串 abcdcdab 同时会在结果中将2个分组提取出来.

    image-20230910204040697

    这里注意, 在有些编程语言的实现上, 通过$符号引用分组(比如 js), 用的时候再搜就行.

    不引用分组

    有的时候我们只是希望将多个字符合并, 并不需要引用分组. 这时可以通过 (?:...) 来指定不需要引用的分组.

    嵌套分组

    对于比较复杂的场景, 会存在括号嵌套括号的情况, 此时分组编号是全局的. 简单说, 左括号是第几个, 分组编号就是几.

    比如正则表达式 (a(bc)d)\2\1 会匹配字符串 abcdbcabcd

    分组应用场景

    简单介绍几种可能预见的应用场景:

    匹配重复单词

    比如正则表达式: (\w+) \1

    文本替换

    比如这段python代码:

    import re
    
    test_str = 'hello, hujingnb is good, haha'
    pattern = r'(\w+) is good'
    repl = r'\1 is bad'
    # hello, hujingnb is bad, haha
    print(re.sub(pattern, repl, test_str))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    比如常用的sublime工具中, 也可通过类似操作进行文本替换.

    匹配模式

    在匹配的时候, 可以通过设置(?i)来修改匹配模式为忽略大小写, 使用方式如下:

    • (?i)hello: 放在正则表达式最前面, 整个正则表达式均为忽略大小写模式
    • h(?i)hello: 放在正则表达式中间, 即从某处开始, 改为忽略大小写模式. 注意, 不是所有语言均可用
    • ((?i)hello) \1: 放在分组开头, 标识分某个分组改为忽略大小写模式. 注意, 不是所有语言均可用

    可以调整的匹配模式如下, 匹配模式格式均为(?), 使用方法相同, 多个模式可以放在一起使用, 如 (?is):

    • a: 测试 仅匹配 ASCII 字符, unicode 编码字符不进行匹配
    • i: 测试 忽略大小写
    • m: 测试 多行模式. 修改^$的行为, 改为匹配每一行的开头结尾
    • n: 测试 开启后, (...) 这种普通分组不会做为分组存在, 仅(?...) 这种命名分组会进行捕获
    • s: 测试 .可以匹配任意符号, 包括换行符
    • u: 测试 匹配完整的 unicode 编码, 默认行为, 基本不需要设置
    • U: 测试 懒惰模式. 开启懒惰模式, 在此模式下, 量词后面加?为恢复贪婪模式
    • x: 测试 详细模式. 将正则表达式中的所有空格及换行均忽略, 且每行#后为注释内容. 匹配规则中的空格可使用\转义 (或者放到分组中使用, 也可以通过[ ]使用)

    注意: 大部分语言都可以直接在正则表达式中修改匹配模式, 但部分语言不行, 比如:

    • js: /<正则>/

    边界匹配

    在正则使用中, 可能会想要进行位置匹配, 但并不希望匹配内容出现在结果中. 于是就出现了这样一组符号, 仅用于匹配位置, 比如前面出现过的 ^$

    用于匹配边界的符号有如下几种:

    符号demo含义
    ^匹配 不匹配匹配字符串的开始位置
    $匹配 不匹配匹配字符串的结束位置
    \b匹配匹配单词边界, 边界包括 空格.- 等等符号, 注意_不是单词边界
    \B匹配 不匹配匹配非单词边界, 与\b相反
    \A匹配 不匹配 ^差异匹配字符串的开始位置, 与^相似. 但多行模式下, ^的行为会改为匹配行开始位置, \A行为不会改变
    \Z匹配 $差异匹配字符串的结束位置, 与$相似. 同样, 在多行模式下, 行为不会改变
    (?=...)匹配 不匹配前向肯定断言. 简单说, 右边匹配 ...
    (?!...)匹配 不匹配前向否定断言. 简单说, 右边不匹配 ...
    (?<=...)匹配 不匹配后向肯定断言. 简单说, 左边匹配 ...
    (?匹配 不匹配后向否定断言. 简单说, 左边不匹配 ...

    扩展语法

    所有扩展语法格式均为为(?...). (反过来不成立)

    注意, 扩展语法并不是所有编程语言都支持的, 在使用前可前往网站测试是否支持.

    命名分组

    使用编号引用分组的方式并不友好, 甚至有时候改了下正则表达式, 后面编号都要改一遍. 因此, 我们可以给分组起个名.

    • (?P...): 给分组起名为 xxx
    • (?P=xxx): 引用 xxx分组

    比如正则表达式 (?Pab)(?P=reg_name) 会匹配字符串 abab

    注意 命名分组也会占用分组的编号哦, 也就是说 (?Pab)(?P=reg_name)(?Pab)\1 效果是一样的.

    测试

    分支判断

    根据前面是否匹配到分组信息, 来使得后面能够有不同的匹配行为.

    语法为: (?(/)|) , 前面指定分组编号或者名字, 如果分组存在, 则使用 进行匹配, 否则使用 进行匹配.

    比如这个例子, ^(<)?(\w+)(?(1)>|$)$, 可以匹配到字符串 aaa, 也就是< 开头的必须由 > 结尾.

    注释

    语法 (?#这部分是注释)

    匹配规则

    下面介绍下几种匹配规则:

    • 贪婪模式: 尽可能多的匹配. 是正则匹配时的默认模式
    • 懒惰模式: 尽可能少的匹配. 通过在量词后添加?指定. 如a*? 就是a*的懒惰版本
    • 独占模式: 在匹配过程中不进行回溯. 通过在量词后添加+指定. 如a++就是a+的独占版本

    简单对着几种规则进行说明

    贪婪模式

    贪婪模式其实就是我们平常最进场使用的模式. 其原则是向后查找尽量长的字符串进行匹配.

    比如使用正则b*来匹配字符串abbbc, 能够匹配到如下内容:

    位置(前闭后开)匹配内容
    0-0
    1-4bbb
    4-4
    5-5

    匹配过程大致如下:

    1. 0-0: 匹配第一个字符, 发现不是 a, 输出空
    2. 1-4: 一直匹配到字母c发现匹配不上了, 输出bbb
    3. 剩下的同理

    懒惰模式

    匹配过程与贪婪模式相似, 区别是只要有能够匹配到的, 就输出, 会输出符合规则的最短子串.

    还用上面相同的例子举例, 使用正则b*?匹配字符串abbbc, 能够匹配到如下内容:

    位置(前闭后开)匹配内容
    0-0
    1-1
    1-2b
    2-2
    2-3b
    3-3
    3-4b
    4-4
    5-5

    与上面的一对比, 区别是不是就很明显了? 这次匹配到的是没单个b字符, 就连每个b字符左面的空字符都匹配上了.

    匹配过程就不再赘述了. 这里用一个更加明显且常用的场景来说明贪婪模式懒惰模式的区别, 当我们匹配字符串"hi mom" and "hi son"时:

    • ".*" : 贪婪模式下, 匹配到的是字符串"hi mom" and "hi son"
    • ".*?": 懒惰模式下, 则会分别匹配到字符串"hi mom""hi son"

    独占模式

    要说明独占模式, 得先简单说一下正则匹配的回溯现象.

    比如, 使用正则 ab+bc 匹配字符串 abbbc的时候, 在贪婪模式下(下方的括号为了标明当前匹配的位置):

    正则匹配位置字符串匹配位置说明
    (a)b+bc(a)bbbc字符 a 完成匹配, 继续下一个匹配
    a(b+)bca(b)bbc此时, 要匹配字符 b 的数量为 >= 1, 因此会继续向后匹配
    正则位置不变, 字符串匹配后移(后续相同操作忽略)
    a(b+)bcabbb©当匹配到这个位置的时候, 发现已经匹配不上了. 则字符串会向前移动, 以继续完成匹配.
    ab+(b)cabb(b)c此时, 字符串将已经匹配过的字符又吐出来了. 这个过程就被称为回溯
    后续完成匹配, 不再赘述

    如果正则是ab+bbc的话, 匹配相同的字符串甚至会发生多次回溯. (懒惰模式也存在回溯现象, 不再赘述)

    而回溯现象是及其影响性能的. 而独占模式则是将回溯直接关掉, 匹配性能更好, 但是对正则书写的要求也更高些.

    比如匹配abbbc字符串时, 开启独占模式的匹配规则ab++bc会匹配不到任何数据.

    ab++c则是能够匹配到字符串abbbc的, 因为匹配过程无需回溯,

    这里说句题外话, 在有些编程语言中不支持回溯模式, 比如Go, Python中使用也需要通过regex库.

    回溯现象真实场景

    你以为回溯现象造成的性能微乎其微么? 不, 有这样一篇文章一个由正则表达式引发的血案, 讲述了因为正则回溯而导致的 CPU 爆满, 感兴趣的去原文看一下.

    简单来说, 就是正则表达式 ^([a-z]|[a-z])+$ 在进行匹配的时候,因为a-z在前后组合中重复出现了, 导致大量回溯后的重复判断 . 如果所有的字符都能够成功匹配到前一个组合, 问题还不大, 但一旦有一个字符不匹配就会发生回溯, 且不匹配的字符越靠后, 回溯的层级越深. 查看回溯匹配

    对于这种情况如何修改呢?

    1. 将重复匹配去掉, 改为 ^[a-z]+$
    2. 使用独占模式防止回溯. ^([a-z]|[a-z])++$

    回溯的介绍, 也可参考此文章

  • 相关阅读:
    如何在 Vim 中剪切、复制和粘贴
    强化学习 DQN 速成
    【web框架】——Django——如桃花来
    8.10 - 软件运维
    线程的创建和两种线程实现方式的区别
    「AI人工智能」关于AI的灵魂发问
    Xposed项目配置
    基于dlib进行人脸识别demo
    Java:为什么使用Java开发Web和移动应用
    数据库(一):MySQL
  • 原文地址:https://blog.csdn.net/qq_31725391/article/details/132825457