唠唠闲话

正则表达式在爬虫,脚本编写等许多任务上都有重要应用,本篇系统地整理一遍正则表达式的语法,以及 Python, Julia 和 Shell 与正则表达式相关的工具。学习链接:编程胶囊


基础语法

公用语法

在大多数支持正则表达式的语言中,这部分规则一致,语言包括 Shell, Python 和 Julia。

  1. 匹配位置:^ 匹配开头,$ 匹配结尾,比如

    • ^ab 匹配 abc, abcd, abb, ab
    • ^ab 不匹配 aab, aac, bab
    • ab$ 匹配 aab, ab, abab
    • ab$ 不匹配 aabb, b, abb
    • ^ab$ 精确匹配 ab
  2. 匹配字符

    匹配符 说明
    . 匹配除回⻋以外的任意⼀个字符
    ( ) 字符串分组
    [ ] 定义字符类,匹配括号中的⼀个字符
    \ 转义字符
    * 匹配(前边)字符的若干次(包括 0 次)
    ? 字符出现 0 次或 1 次
    + 字符至少出现依次
    {n,} 字符至少出现 n 次
    {n, m} 字符至少出现 n 次,最多 m 次
    {m} 字符正好出现 m 次
  3. 一些例子

    • ^a.c$ 匹配 abc, a.c, aec
    • ^a.c$ 不匹配 abbc, ac, .c
    • ^a\.c 精确匹配 a.c
    • ^a*c 匹配 c, ac, aac, ...
    • ^a?c$ 仅匹配 c, ac
    • ^a+c$ 匹配 ac, aac, ...
  4. 中括号表示匹配一个字符,中括号内部:

    • - 代表区间,注意右区间不能比左区间小,否则匹配无效
    • [2-8] 表示匹配数字 2, 3,..., 8
    • [ac-k] 表示匹配字母 a, c, d,..., k
    • ^ 表示取否,比如 [^13-9] 匹配除了 1, 3,...,9 以外的字符
    • \ 匹配特殊字符,比如 [\-] 匹配 -[\[] 匹配 [

贪婪匹配

  1. ? 在正则表达式中有多种含义:
  • 放在字符后,代表该字符匹配 0/1 次
  • 放在分组开头,?: 代表不捕获分组内容
  • 放在分组开头,?=, ?!, ?<=, ?<! 代表断言
  • 放在变长匹配后边,代表非贪婪匹配,也即匹配成功时即刻停止
  1. 例子

    1
    2
    3
    4
    5
    6
    reg1 = r"a{2,}"
    reg2 = r"a{2,}?"
    collect(eachmatch(reg1, "aaaaa"))
    # 贪婪匹配,只匹配到一个元素 aaaaaa
    collect(eachmatch(reg1, "aaaaa"))
    # 非贪婪匹配,匹配到 2 个,"aa", "aa"
  2. 比起“非贪婪匹配”,更准确的说法是“匹配即停”,也即对于当可变长的规则,比如

    1
    2
    3
    4
    5
    6
    reg1 = r"a.*b"
    reg2 = r"a.*?b"
    collect(eachmatch(reg1, "aaaaabaab"))
    # 贪婪匹配,只匹配到一个元素 aaaaabaab
    collect(eachmatch(reg2, "aaaaabaab"))
    # 即停匹配,第一处匹配到 aaaaab,第二处匹配到 aab

    按“贪婪匹配”的说法,使用 reg2 匹配时,应该得到两处 ab 而不是 aaaaabaab,我们从原理理解匹配的结果更自然:正则匹配从左到右,? 在成功匹配后即停止,并返回结果

特殊匹配

  1. 这部分规则在 Python 和 Julia 中的表述一致,与 Linux 的 Perl 也一致,但 shell 中的 grep 用另一套“POSIX 特殊字符”,所以也分开讨论。

  2. 这些字符可以在中括号内,也可以在外部

    • \w 匹配单词字符,包括 [_a-zA-Z0-9] 以及汉字等,不包括符号
    • \d 匹配数字,相当于 [0-9]
    • \s 匹配空白字符,包括空格,tab,换行等??
    • \b 匹配单词的边界
      • \babc 匹配带左边界的单词 abcabcd.abcdef
      • \babc 不匹配 aabc_abcababc
      • \b\w{4}\b 匹配带左右边界的四字单词
    • \W, \D, \S 匹配相应的取反情形

分组

正则表达式提供了一种将表达式分组的机制,当使用分组时,除了获得整个匹配,还能够在匹配中选择每一个分组。

  1. 分组用括号抓取,比如

    • (\d{4})-(\d{2})-(\d{2}) 匹配并提取数据 2022-02-28 中的日期,一共三个分组
    1
    2
    3
    ## Python 正则
    re.findall(r'(\d{4})-(\d{2})-(\d{2})', '2022-02-282321-02-21')
    # [('2022', '02', '28'), ('2321', '02', '21')]
    1
    2
    3
    4
    ## Julia 正则
    collect(eachmatch(r"(\d{4})-(\d{2})-(\d{2})", "2022-02-282321-02-21"))
    # RegexMatch("2022-02-28", 1="2022", 2="02", 3="28")
    # RegexMatch("2321-02-21", 1="2321", 2="02", 3="21")
  2. 分组内,使用“或”运算 |,比如

    • (\.jpg|\.png) 匹配 .jpg.png
    • (\.(jpg|png)) 嵌套匹配,两个分组,组1为 .jpg.png,组2为 jpgpng
    1
    2
    3
    4
    5
    6
    7
    8
    ### 匹配到两段信息,元组长度分别为 1
    collect(eachmatch(r"\d(\.jpg|\.png)", "1.jpg, 2.png"))
    # RegexMatch("1.jpg", 1=".jpg")
    # RegexMatch("2.png", 1=".png")
    ### 匹配到两端信息,元组长度分别为 2
    collect(eachmatch(r"\d(\.(jpg|png))", "1.jpg, 2.png"))
    # RegexMatch("1.jpg", 1=".jpg", 2="jpg")
    # RegexMatch("2.png", 1=".png", 2="png")
  3. 有时候,我们并不需要捕获某个分组的内容,但是又想使用分组的特性(比如或运算)。这个时候就可以使用非捕获组 (?:表达式),从而不捕获数据,还能使用分组的功能,比如匹配前缀和后缀,但只捕获前缀的分组

    1
    2
    3
    4
    ## Julia
    collect(eachmatch(r"(\w)\.(?:jpg|png)", "1.jpg, 2.png"))
    # RegexMatch("1.jpg", 1="1")
    # RegexMatch("2.png", 1="2")
    1
    2
    3
    # Python
    re.findall(r"(\w)\.(?:jpg|png)", "1.jpg, 2.png")
    # ['1', '2']
  4. 分组的回溯引用,\N 表示表达式的第 N 个分组,比如

    1
    2
    3
    collect(eachmatch(r"(\w)(\w)\2\1", "abbaddadda"))
    # RegexMatch("abba", 1="a", 2="b")
    # RegexMatch("adda", 1="a", 2="d")
    1
    2
    re.findall(r"(\w)(\w)\2\1", "abbaddadda")
    # [('a', 'b'), ('a', 'd')]

断言

参考博客园:如同 ^ 代表开头,$ 代表结尾,\b 代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为“零宽”。

  1. 先行断言和后行断言总共有四种:

    • 正向先行断言
    • 反向先行断言
    • 正向后行断言
    • 反向后行断言
  2. 正向先行断言:(?=pattern),代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配 pattern。 例如对 a regular expression 这个字符串,要想匹配 regular 中的 re,但不匹配 expression 中的 re,可以用 re(?=gular)。该表达式限定了 re 右边的位置,这个位置之后是 gular,但并不消耗 gular 这些字符,将表达式改为 re(?=gular)g,将会匹配 reg,括号这一砣匹配了 eg 之间的位置。

  3. 负向先行断言:(?!pattern),代表字符串中的一个位置,紧接该位置之后的字符序列不能匹配 pattern。例如对 regex represents regular expression 这个字符串,要想匹配除 regexregular 之外的 re,可以用 re(?!g)。该表达式限定了 re 右边的位置,这个位置后面不是字符 g。负向和正向的区别在于该位置之后的字符能否匹配括号中的表达式。

  4. 正向后行断言:(?<=pattern),代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配 pattern。例如对 regex represents regular expression 这个字符串,有 4 个单词,要想匹配单词内部的 re,但不匹配单词开头的re,可以用 (?<=\w)re。之所以叫后行断言,是因为正则表达式引擎在匹配字符串和表达式时,是从前向后逐个扫描字符串中的字符,并判断是否与表达式符合,当在表达式中遇到该断言时,正则表达式引擎需要往字符串前端检测已扫描过的字符,相对于扫描方向是向后的。

  5. 负向后行断言:(?<!pattern),代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配 pattern 。例如对 regex represents regular expression 这个字符串,要想匹配单词开头的 re,可以用 (?<!\w)re,当然也可以用 \bre 来匹配。

  6. 总之,正向 = 代表正匹配,负向 ! 代表反匹配;先行是顺着扫描,也限定该位置后的字符,后行 < 是逆着扫描,限定该位置前的字符性质。

  7. 为了匹配 regex represents regular expression 的片段,有这几种断言方式:

    • 正向先行 re(?=g) 限定该位置后边必须是什么,匹配了 regex, regularre,排除了 represents, expressionre
    • 负向先行 re(?!s|p) 限定该位置后边不能是什么,匹配了 regex, regularre,排除 represents, expression 中的 re
    • 正向后行 (?<=r)e 限定该位置前边必须是什么,匹配所有带 re 的单词内的 e
    • 负向后行 (?<!p)re 限定该位置前边不能是什么,排除 represents, expression 中后一处 re
    • 经典例子:先行断言 (?=.*?[a-z])(?=.*?[A-Z]).+ 匹配同时包含大小写字母的非空字符串
      • 第一个括号 (?=.*?[a-z]) 匹配包含一个小写字母之后的所有位置
      • 第二个括号 (?=.*?[A-Z]) 匹配包含一个大写字母之后的所有位置
      • 两个括号配合,表示至少包含一个大写和小写字符的位置
      • 最后的 .+ 表示字符串本身长度不小于 1
      • 实际上由于前两条先行断言的限定,最后的匹配用 .*.{2,} 效果等价
    • 一般地,这可以作为匹配用到模板,(?=.*?[a-z]{m,}) 表示至少包含 m 个括号中的字符

shell

  1. grep -Eegrep 命令对文本进行正则匹配,注意 grep 默认参数为 grep -e,此时部分正则表达式无效,比如 grep a?c 不匹配 acc

  2. POSIX 特殊字符

    特殊字符 说明
    [:alnum:] 匹配任意字⺟字符0-9 a-z A-Z
    [:alpha:] 匹配任意字⺟,⼤写或⼩写
    [:digit:] 数字 0-9
    [:graph:] ⾮空字符( ⾮空格控制字符)
    [:lower:] ⼩写字符a-z
    [:upper:] ⼤写字符A-Z
    [:cntrl:] 控制字符
    [:print:] ⾮空字符( 包括空格)
    [:punct:] 标点符号
    [:blank:] 空格和TAB字符
    [:xdigit:] 16 进制数字
    [:space:] 所有空⽩字符( 新⾏、空格、制表符)
  3. 注意匹配时,外层还要再套一层 [],比如 [[:alpha:]] 匹配字母

Julia

推荐阅读:String 官方文档

  1. Julia 中,字符串追加 r 代表正则表达式 Regex,举个例子

    1
    2
    3
    re = r"^\s*(?:#|\$)" # 规则:在若干空字符后,匹配 # 或者 $
    typeof(re) # Regex
    isconcretetype(Regex) # true

    例子涉及前边几节的语法:^ 代表从首位开始匹配,\s* 匹配若干空白字符,(?:...) 为不命名元组的写法,#|\$ 在元组内表示或运算,#$

  2. occursin 判断,match 匹配内容

    1
    2
    3
    4
    occursin(r"^\s*(?:#|\$)", "# a comment") # true 判断是否匹配
    match(r"^\s*(?:#|\$)", "# a comment", 2) # 从位置 2 开始匹配,未匹配到,返回 nothing
    m = match(r"^\s*(?:#|\$)", "# a comment") # RegexMatch("#")
    typeof(m) # RegexMatch

    match 匹配成功后,返回具体类型数据 RegexMatch,其包含表达式匹配到的详细信息,比如

    1
    2
    3
    4
    5
    m = match(r"(\d{2})([a-z]{2})", "aa12bb34cc")
    m.match # 匹配到的字符串
    m.captures # 匹配到的分组信息,未分组则返回空列表
    m.offset # 返回匹配到的字符串的起始位置(整数)
    m.offsets # 返回匹配到的分组的起始位置(列表)

    深度截图_选择区域_20220228221528

  3. (?P<tag>expr) 将分组命名为 tag,其中字母 P 可以省略

    1
    2
    3
    4
    5
    6
    7
    m = match(r"(?<tag>\d+):(\d+)","12:45")
    m[:tag] # 使用标签调用分组
    m[2] # 使用索引调用分组
    m = match(r"(?P<hi>\d+):\g<hi>","12:12") # 用 \g + 名称,回溯调用分组内容
    m = match(r"(?<hi>\d+):\g1","12:12") # 用 \g + 数字回溯调用分组内容
    # Python 写法
    # re.findall(r"(?P<hi>\d+):(?P=hi)","12:12")

    Julia 中的回溯调用的语法

    • \g<分组名>
    • \n, \gn, \g<n>,n 为分组序号
  4. replace 的键和值都支持正则表达,其中值字符串的前边追加参数 s

    1
    2
    3
    4
    5
    # 键值使用 s
    replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<1>")
    replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<minute>")
    # \g<0> 代表整个字符串
    replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<0>")

    深度截图_选择区域_20220228224543

  5. 匹配多项使用 eachmatch,返回由 RegMatch 类型数据构成的生成器

    1
    2
    3
    collect(eachmatch(r"\d\d", "1234")) # 匹配到两个
    collect(eachmatch(r"\d\d", "1234"; overlap=true)) # 匹配到三个
    collect(eachmatch(r"\d\n\d", "12\n34")) # 支持换行匹配
  6. . 不能匹配换行号,需要换行匹配时,可以用 [\s\S][\w\W]

    1
    2
    match(r"123.*321", "123\n22\n321") # nothing
    match(r"123[\s\S]321", "123\n22\n321") # RegexMatch("123\n22\n321")
  7. 字符结尾的 ism 类似 Perl,后续再补充

Python

参考菜鸟教程,Python 自 1.5 版本起增加了 re 模块,它提供 Perl 风格的正则表达式模式。

  1. match 从字符起始位置开始匹配

    1
    2
    3
    re.match(r"\d\d", "2a-2b\n20") # 返回 None,未匹配到
    re.match(r"\w\w", "2a-2b\n20") # 返回匹配对象
    # <re.Match object; span=(0, 2), match='2a'>
  2. search 扫描整个字符串,并返回第一个成功匹配的位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    re.search(r"\d\d", "2a-2b\n20")
    # <re.Match object; span=(6, 8), match='20'>
    info = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "ab2022-03-01cd")
    print(info.start()) # 2 | 匹配到的起始位置
    print(info.end()) # 12 | 匹配到的终点位置
    info.span() # (2, 12) | 起始 和 终点
    print(info.groups()) # ('2022', '03', '01') | 分组内容
    print(info.group(0)) # 2022-03-01 | 位置 0 代表匹配的字符串
    info.group() # 等同于 info.group(0)
    info.group(1) # 2022 | 位置 1 代表分组第一个元素
    info.regs # ((2, 12), (2, 6), (7, 9), (10, 12)) | 分组内容的索引
    info.re # 返回使用的正则匹配规则
    info.string # 返回被匹配的字符串
    info.groupdict() # {'year': '2022', 'month': '03', 'day': '01'} | 字典形式返回匹配内容

    匹配对象的常用属性和方法
    深度截图_选择区域_20220305155144

  3. sub 检索和替换

    1
    2
    3
    4
    5
    # 提取日期,并将格式的 - 改为 /
    txt = "ab2022-03-01cd"
    reg = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
    repl = r"\1/\2/\3"
    re.sub(reg, repl, txt) # 'ab2022/03/01cd'
  4. compile 编译正则表达式,搭配其他函数使用

    1
    2
    reg = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})") # 编译正则表达式
    reg.search("ab2022-03-01cd") # 返回查找对象
  5. findall 查找所有匹配的字符串

    1
    2
    3
    4
    re.findall("\w{2}", "2022-03-01") # 返回字符串构成的列表
    # ['20', '22', '03', '01']
    re.findall("(\w{2})-(\w{2})", "20-22-03-01") # 返回字符串元组构成的列表
    # [('20', '22'), ('03', '01')]
  6. finditer 返回迭代器形式的匹配信息

    1
    2
    3
    4
    5
    list(re.finditer("\w{2}", "2022-03-01"))
    # [<re.Match object; span=(0, 2), match='20'>,
    # <re.Match object; span=(2, 4), match='22'>,
    # <re.Match object; span=(5, 7), match='03'>,
    # <re.Match object; span=(8, 10), match='01'>]
  7. split 按匹配规则进行字符串拆分

    1
    2
    re.split("\d", "1bb3cc1dd")
    # ['', 'bb', 'cc', 'dd']
  8. 在使用以上函数时,支持修饰符,且多个修饰符用 | 指定,比如

    1
    2
    re.findall(r"ab.*cd", "ab\ndd\nCD") # [] | 换行匹配不到
    re.findall(r"ab.*cd", "ab\ndd\nCD", re.I | re.S) # ['ab\ndd\nCD'] | `.` 可以匹配换行,字母不分大小写
  9. 常见修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别匹配
re.M 多行匹配,影响 ^$
re.S 使 . 匹配包括换行在内的所有字符
re.U 根据 Unicode 字符集解析字符。这个标志影响 \w, \W, \b, \B
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

Perl

shell 中使用 grep -P 使用 Perl 的正则表达式语法