第 7 章 正则表达式

维基百科关于 正则表达式的描述学习正则表达式

R 内置的三种匹配模式

  1. fixed = TRUE: 字面意思匹配 exact matching.
  2. perl = TRUE: 使用 Perl 正则表达式.
  3. fixed = FALSE, perl = FALSE: 使用 POSIX 1003.2 extended 正则表达式 (默认设置).

不要拘泥于一种解决方案,比如清理数据中正则表达式有 Base R 提供的一套,stringr 又一套,提高效率的工具 RStudio 插件 regexplain 和辅助创建正则表达式 RVerbalExpressions

正则表达式 (regular expression,简称 regexp), 函数 regexprgregexpr 的名称就好理解了,在控制台输入 ?regex 查看 R 支持的正则表达式,这个文档看上百八十回也不过分。R 内支持正则表达式的函数有 grepgreplsubgsubregexprgregexprregexecstrsplit。函数 aproposbrowseEnvhelp.searchlist.filesls 是通过函数 grep 来使用正则表达式的,它们全都使用 extended regular expressions

grep(pattern, x, ignore.case = FALSE, perl = FALSE, value = FALSE,
     fixed = FALSE, useBytes = FALSE, invert = FALSE)

匹配模式 pattern 的内容 可以用函数 cat 打印出来,注意反斜杠进入 R 字符串中时,需要用两个,反斜杠 \ 本身是转义符,否则会报错。

cat("\\") # \ 反斜杠是转义字符
#> \
cat("\\.")
#> \.
cat("\\\n") # 注意 \n 表示换行
#> \

7.1 字符常量

单引号 ' 双引号 " 和反引号 ` 三种类型的引用 (quotes) 是 R 语法的一部分25,此外反斜杠 \ 用来转义下面的字符

表 7.1: 字符常量表
字符常量 含义
\n 换行 newline
\r 回车 carriage return
\t 制表符 tab
\b 退格 backspace
\a 警报(铃)alert (bell)
\f 换页 form feed
\v 垂直制表符 vertical tab
\\ 反斜杠 backslash \
\' 单引号 ASCII apostrophe '
\" 双引号 ASCII quotation mark "
\` 反引号或沉音符 ASCII grave accent (backtick) `
\nnn 八进制 character with given octal code (1, 2 or 3 digits)
\xnn 十六进制 character with given hex code (1 or 2 hex digits)
\unnnn Unicode character with given code (1–4 hex digits)
\Unnnnnnnn Unicode character with given code (1–8 hex digits)

7.2 软件环境

R 内置的正则表达式实现是基于 PCRE ICU TRE iconv 等第三方库,搞清楚自己使用的版本信息是重要的,一些字符集的解释与区域环境有关,如 [:alnum:][:alpha:]等,所以获取当前的区域设置也很重要

# find a suitable coding for the current locale
localeToCharset(locale = Sys.getlocale("LC_CTYPE"))
#> [1] "UTF-8"     "ISO8859-1"
# 软件版本信息
extSoftVersion()
#>                                              zlib 
#>                                          "1.2.11" 
#>                                             bzlib 
#>                              "1.0.6, 6-Sept-2010" 
#>                                                xz 
#>                                           "5.2.4" 
#>                                              PCRE 
#>                                 "8.39 2016-06-14" 
#>                                               ICU 
#>                                            "63.1" 
#>                                               TRE 
#>                         "TRE 0.8.0 R_fixes (BSD)" 
#>                                             iconv 
#>                                      "glibc 2.28" 
#>                                          readline 
#>                                             "7.0" 
#>                                              BLAS 
#> "/usr/lib/x86_64-linux-gnu/blas/libblas.so.3.8.0"
# 区域及其编码信息
l10n_info()
#> $MBCS
#> [1] TRUE
#> 
#> $`UTF-8`
#> [1] TRUE
#> 
#> $`Latin-1`
#> [1] FALSE
# 表示数字、货币的细节
Sys.localeconv()
#>     decimal_point     thousands_sep          grouping   int_curr_symbol 
#>               "."                ""                ""            "USD " 
#>   currency_symbol mon_decimal_point mon_thousands_sep      mon_grouping 
#>               "$"               "."               ","        "\003\003" 
#>     positive_sign     negative_sign   int_frac_digits       frac_digits 
#>                ""               "-"               "2"               "2" 
#>     p_cs_precedes    p_sep_by_space     n_cs_precedes    n_sep_by_space 
#>               "1"               "0"               "1"               "0" 
#>       p_sign_posn       n_sign_posn 
#>               "1"               "1"
# PCRE 启用的配置选项
pcre_config()
#>              UTF-8 Unicode properties                JIT 
#>               TRUE               TRUE               TRUE 
#>              stack 
#>               TRUE
# 比较全的字符信息
stringi::stri_info()
#> $Unicode.version
#> [1] "11.0"
#> 
#> $ICU.version
#> [1] "63.1"
#> 
#> $Locale
#> $Locale$Language
#> [1] "en"
#> 
#> $Locale$Country
#> [1] "US"
#> 
#> $Locale$Variant
#> [1] ""
#> 
#> $Locale$Name
#> [1] "en_US"
#> 
#> 
#> $Charset.internal
#> [1] "UTF-8"  "UTF-16"
#> 
#> $Charset.native
#> $Charset.native$Name.friendly
#> [1] "UTF-8"
#> 
#> $Charset.native$Name.ICU
#> [1] "UTF-8"
#> 
#> $Charset.native$Name.UTR22
#> [1] NA
#> 
#> $Charset.native$Name.IBM
#> [1] "ibm-1208"
#> 
#> $Charset.native$Name.WINDOWS
#> [1] "windows-65001"
#> 
#> $Charset.native$Name.JAVA
#> [1] "UTF-8"
#> 
#> $Charset.native$Name.IANA
#> [1] "UTF-8"
#> 
#> $Charset.native$Name.MIME
#> [1] "UTF-8"
#> 
#> $Charset.native$ASCII.subset
#> [1] TRUE
#> 
#> $Charset.native$Unicode.1to1
#> [1] NA
#> 
#> $Charset.native$CharSize.8bit
#> [1] FALSE
#> 
#> $Charset.native$CharSize.min
#> [1] 1
#> 
#> $Charset.native$CharSize.max
#> [1] 3
#> 
#> 
#> $ICU.system
#> [1] TRUE
#> 
#> $ICU.UTF8
#> [1] TRUE

需要临时改变区域环境设置,配合特殊的画图和文本输出要求。

# 获取当前默认的区域设置
Sys.getlocale()
foo <- Sys.getlocale()
# 恢复默认的区域设置
Sys.setlocale("LC_ALL", locale = foo)

7.3 基本概念

正则表达式的构造方式类似算术表达式,通过各种操作组合子(更小的)表达式,整个表达式匹配一个或多个字符26。大多数字符,包括所有的字母和数字,是匹配自身的正则表达式。元字符 . \ | ( ) [ { ^ $ * + ? 需要转义才能表达其自身的含义,转义的方式是在元字符前面添加反斜杠,如要表达点号 . 需要使用 \.。要注意,它们是否有特殊意义取决于所在的内容。

一个字符集 (character class) 是用一对中括号[]括起来的字符列表,用来匹配列表中的任意单个字符,除非列表中的第一个字符是 ^,它用来匹配不在这个列表中的字符。 [0123456789] 用来匹配任意单个数字,[^abc] 用来匹配除字符 a,b,c 以外的任意字符。字符范围 (character ranges) 可以通过第一个和最后一个字符指定, 中间用连字符 (hyphen) 连接, 由于这种解释依赖于区域和具体实现,所以指定字符范围的使用方式最好避免。唯一可移植(便携,通用)的方式是作为字符集,在列表中列出所有的 ASCII 字母, [ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz].

预定义的一些字符类,它们的解释依赖于当前的语言区域,下面是 POSIX locale 环境下的解释

  • [:alnum:] 表示 [:alpha:][:digit:],含义是 [0-9A-Za-z],但是前者与区域和字符集无关,后者依赖于当前的区域设置和字符编码。要注意在这些字符集名 class names 中,中括号 [] 是符号名的一部分,是必须要包含的。在字符集中,大多数元字符失去它们特殊的意义。

  • [:alpha:] 表示 [:lower:][:upper:]

  • [:blank:] 表示 空格 space 制表符 tab

  • [:cntrl:] 表示控制符,在 ASCII 字符集里里,这些字符有八进制代码,从 000 到 037,和 177(DEL)。

  • [:digit:] 表示数字 0,1,2,3,4,5,6,7,8,9

  • [:graph:] 表示 [:alnum:][:punct:].

  • [:lower:] 表示当前区域下的小写字母

  • [:print:] 表示可打印的字符 [:alnum:], [:punct:] 和空格.

  • [:punct:] 表示标点字符

    ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~`
  • [:space:] 表示空格字符: 水平制表符 tab, 换行符 newline,垂直制表符 vertical tab,换页符 form feed,回车符 carriage return,空格符 space

  • [:xdigit:] 表示 16 进制数字 0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f.

要包含字面的 ] 就把它放在列表的开头,类似地,要包含字面 ^,除了开头可以放在任意位置。要包含字面 - 把它放在开头或者结尾。只有 ^ - \ ] 在字符集内是有特殊的含义

点号 . 匹配任意单个字符,\w 匹配一个词 word 字符(是[[:alnum:]_]的同义词,一个扩展) ,而 \W\w 取反,意味着 [^[:alnum:]_]\d, \s, \D\S 表示数字和空格类和它们的取反

脱字符 caret ^ 和美元符号 $ 是元字符,分别匹配一行的开头和结尾。符号 \<\> 分别匹配一个词的开头和结尾的空字符串。\b 匹配词边缘的空字符串,\B 匹配不在词边缘的空字符串。 词 word 的解释依赖于区域和实现。

7.4 字符串匹配

默认的匹配方式是贪婪的,会使用尽可能多的匹配次数,这个可以变为最小的匹配次数,通过在其之后添加 ?,一个正则表达式可能跟着重复量词,下面的限定符都是限定在它前面的正则表达式

表 7.2: 贪婪匹配限定符
符号 描述
? 匹配至多 1 次
* 匹配 0 次或多次
+ 匹配至少 1 次
{n} 匹配 n 次
{n,} 匹配至少 n 次
{n,m} 匹配至少 n 次,至多 m 次

7.5 级联表达式

Regular expressions may be concatenated; the resulting regular expression matches any string formed by concatenating the substrings that match the concatenated subexpressions.

正则表达式可以是级联 concatenation 的,是不是在讲一个正则表达式里面嵌套一个正则表达式?

两个正则表达式可以通过中缀符号 | 联合,用两个子表达式的任意一个去匹配字符串,例如 abba | cde 要么匹配字符串 abba 要么匹配字符串 cde,要注意在字符集内,即 abba|cde,二选一的匹配不凑效,因为中缀符 | 有它的字面意思。

重复匹配 Repetition 的优先级高于级联,级联高于 | 。 整个子表达式可以括号括起来覆盖这些优先级规则。

7.6 反向引用

反向引用 \N 这里 N 可取 1,2,…,9 匹配被之前第 N 个括起来的子表达式匹配的子字符串,例子见 COS 论坛 https://d.cosx.org/d/420570/5

7.7 命名捕捉

模式 (?:...) 包住的字符就是括号分组,但是不做反向查找。模式 (?<=...)(?<!...) 都是反向查找,它们不允许跟限制符,在 ... 也不允许出现 \C。表 7.3 展示四个反向引用

表 7.3: 环顾四周查找
符号 描述
?= 正向肯定查找
?! 正向否定查找
?<= 反向肯定查找
?<! 反向否定查找

函数 regexprgregexpr 支持命名捕捉 (named capture). 如果一个组被命名了,如 (?<first>[A-Z][a-z]+) 那么,匹配的位置是按名字返回。

下面举个例子说明,从字符串向量 notables 中获得了三组匹配 name.rex 是一段正则表达式,描述的模式是人名

## named capture
notables <- c("  Ben Franklin and Jefferson Davis",
              "\tMillard Fillmore")
# name groups 'first' and 'last'
name.rex <- "(?<first>[[:upper:]][[:lower:]]+) (?<last>[[:upper:]][[:lower:]]+)"
parsed <- regexpr(name.rex, notables, perl = TRUE)
parsed
#> [1] 3 2
#> attr(,"match.length")
#> [1] 12 16
#> attr(,"index.type")
#> [1] "chars"
#> attr(,"useBytes")
#> [1] TRUE
#> attr(,"capture.start")
#>      first last
#> [1,]     3    7
#> [2,]     2   10
#> attr(,"capture.length")
#>      first last
#> [1,]     3    8
#> [2,]     7    8
#> attr(,"capture.names")
#> [1] "first" "last"

notables 是一个长度为2的字符串向量,所以获得两组匹配,捕捉到匹配开始的位置 capture.start 和匹配的长度 capture.length 都是两组,按列来看,字符 B 出现在字符串 Ben Franklin and Jefferson Davis 的第三个位置,匹配的长度 Ben 是三个字符,长度是 3,如图 7.1 所示,需要注意的是一定要设置 perl = TRUE 才能使用命名捕捉功能,函数 sub 不支持命名反向引用 Named backreferences

命名捕捉

图 7.1: 命名捕捉

正则表达式漫画

图 7.2: 正则表达式漫画

正则表达式的直观解释 https://github.com/gadenbuie/regexplain

7.8 表达式注释

The sequence (?# marks the start of a comment which continues up to the next closing parenthesis. Nested parentheses are not permitted. The characters that make up a comment play no part at all in the pattern matching.

If the extended option is set, an unescaped # character outside a character class introduces a comment that continues up to the next newline character in the pattern.