49 正则表达式

49.1 正则表达式详解

在对字符串进行查找或替换时, 有时要查找替换的不是固定的子串而是某种模式。 比如,要查找或替换连续的三个数字,正文中的电子邮件地址, 网址,电话号码,等等。 正则表达式(regular expressions)用于表示各种复杂模式。 基本R中的正则表达式规则可以用POSIX 1003.2标准或者Perl规则。 建议使用perl语言的正则表达式, 在基本R的有关函数中规定参数perl=TRUE

stringr包提供了更方便的正则表达式功能, 其正则表达式规则是ICU正则表达式规则, 针对UTF-8编码的文本数据, 基本与perl规则兼容。

在正则表达式的模式(pattern)中,

.*+?{}\[]^$()

等字符是特殊字符,有特殊的解释。 除了\之外的其它12个都称为“元字符”(meta characters)。

在R语言中使用正则表达式时, 需要注意R字符型常量中一个\要写成两个。 为了避免\的转义, 可以使用R的原始字符串功能, 将模式写成r"{...}"这样的形式, 其中...是实际的正则表达式内容, 不需要将\写成两个。

49.1.1 字面匹配与匹配显示

如果模式中不含特殊字符,匹配为原样的子串。 也叫做字面(literal)匹配。 stringr包提供了定义正则表达式、匹配正则表达式、按正则表达式替换、抽取匹配结果、用富文本显示匹配结果等强大功能, 其中str_view()函数可以在RStudio软件中高亮显示匹配部分, 并在匹配部分两边用<>界定。 在所有输出中都用<>界定匹配部分。

下面的程序在字符型向量x的三个字符串元素中查找子字符串"the"并加亮显示(一般输出中用<>界定匹配部分):

x <- c("New theme", "Old times", "In the present theme")
str_view(x, "the")
## [1] │ New <the>me
## [3] │ In <the> present <the>me

因为某些希望原样匹配的字符串中可能有元字符(对正则表达式需要特殊解释的字符), 所以字面匹配的模式应该用fixed()函数保护。 例如, 在文件名中找到以.txt结尾的:

x <- c("myprog.Rmd", "data.txt", "source.R")
str_view(x, fixed(".txt"))
## [2] │ data<.txt>

49.1.2 不区分大小写匹配和regex函数

str_view(string, pattern)中的pattern应该为正则表达式类型, 如果输入了字符串, 会自动被函数regex()转换成正则表达式类型。 正则表达式的模式一般是区分大小写的, 如:

str_view(c("Dr. Wang", "DR. WANG", "dR. W.R."), "Dr")
## [1] │ <Dr>. Wang

通过在regex()函数中加选项ignore_case=TRUE可以进行不区分大小写的匹配:

str_view(c("Dr. Wang", "DR. WANG", "dR. W.R."), 
             regex("Dr", ignore_case=TRUE))
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.

fixed()函数也允许使用ignore_case=TRUE,如:

str_view(c("Dr. Wang", "DR. WANG", "dR. W.R."), 
             fixed("Dr", ignore_case=TRUE))
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.

在模式前面附加(?i)前缀式选项也可以实现不区分大小写匹配:

str_view(c("Dr. Wang", "DR. WANG", "dR. W.R."), 
  "(?i)Dr")
## [1] │ <Dr>. Wang
## [2] │ <DR>. WANG
## [3] │ <dR>. W.R.

也可以提前将输入的源字符串统一转换为小写, 这时模式中也仅使用小写, 也可以达到不区分大小写匹配的目的。 还可以将模式中大写字母和小写字母用字符类表示(见§49.1.4)。

49.1.3 用句点匹配单个字符

在模式中用“.”匹配任意一个字符(除了换行符"\n",能否匹配此字符与选项有关)。 如

s <- c("abc", "cabs", "lab")
str_view(s, "ab.")
## [1] │ <abc>
## [2] │ c<abs>

像句点这样的字符称为元字符(meta characters), 在正则表达式中有特殊作用。 如果需要匹配句点本身,用“[.]”或者“\.”表示。 比如,要匹配a.txt这个文件名,如下做法有错误:

str_view(c("a.txt", "a0txt"), "a.txt")
## [1] │ <a.txt>
## [2] │ <a0txt>

结果连a0txt也匹配了。用“[.]”表示句点则将句点不做特殊解释:

str_view(c("a.txt", "a0txt"), "a[.]txt")
## [1] │ <a.txt>
str_view(c("a.txt", "a0txt"), "a\\.txt")
## [1] │ <a.txt>

注意在R语言字符型常量中一个\需要写成两个。

如果仅需按照原样进行查找, 也可以将pattern的字符串用fixed()函数保护,如: 如

str_view(c("a.txt", "a0txt"), fixed("a.txt"))
## [1] │ <a.txt>

49.1.4 匹配一组字符中的某一个

模式中使用方括号给定一个字符类, 单个字符与字符类中任何一个字符相同都算是匹配成功。 比如,模式“[ns]a.[.]xls” 表示匹配的第一个字符是ns, 第二个字符是a,第三个字符任意,第四个字符是句点, 然后是xls。 测试:

str_view(c("sa1.xls", "dna2.xlss", "nat.xls"), "[ns]a.[.]xls")
## [1] │ <sa1.xls>
## [2] │ d<na2.xls>s
## [3] │ <nat.xls>

注意匹配并不需要从开头匹配到结尾, 中间匹配是允许的,类似于搜索符合某种规律的子串。 在上例中第二个元素是从第二个字符开始匹配的,也没有匹配到末尾。

例:模式[Rr]eg[Ee]x可以匹配RegExRegexregexregEx

在“[]”中有一些特殊字符:

  • 允许用-表示一个范围, 如[a-z]匹配小写英文字母, [A-Z]匹配大写英文字母, [a-zA-Z]匹配大小写的英文字母, [a-zA-Z0-9]匹配大小写的英文字母和数字。 为了匹配一个16进制数字, 可以用[0-9A-Fa-f]
  • 在开头用“^”表示余集,比如"[^0-9]"表示匹配非数字。
  • 如果要在[]包含原样的^-,可以用前导转义符\

例:下面的模式“[ns]a[0-9][.]xls”要求匹配的第三个字符为数字。

str_view(c("sa1.xls", "dna2.xlss", "nat.xls"), 
  "[ns]a[0-9][.]xls")
## [1] │ <sa1.xls>
## [2] │ d<na2.xls>s

例:模式[ns]a[^0-9][.]xls要求匹配的第三个字符不能为数字:

str_view(c("sa1.xls", "dna2.xlss", "nat.xls"), 
  "[ns]a[^0-9][.]xls")
## [3] │ <nat.xls>

要求第三个字符为^-的例子:

str_view(c("5 + 3", "5 - 3", "5 ^ 3"), 
  r"{5[ ][\^\-][ ]3}")
## [2] │ <5 - 3>
## [3] │ <5 ^ 3>

49.1.5 原样匹配元字符

元字符(meta characters)是在正则表达式中有特殊含义的字符。 比如句点可以匹配任意一个字符, 左方括号代表字符集合的开始。 所以元字符不能直接匹配自身, 将元字符写在方括号内可以原样匹配这些字符, 比如可以用“[.]”匹配一个句点, 用[$]匹配“$”符号,用[|]匹配“|”符号, 等等。。

还可以用前导转义字符“\”来表示某个元字符需要原样匹配, 比如, 为匹配左方括号,在前面加上转义字符\变成\[, 但是在R字符串中一个\必须用\\表示, 所以模式“\[”在R中写成字符串常量, 必须写成"\\["。 其它的元字符如果要原样匹配也可以在前面加上转义字符\, 比如匹配\本身可以用\\,但是在R字符型常量中需要写成"\\\\"。 用R的原始字符串格式可以不用将\写成\\, 原始字符串是指字符串前面加上r字母作为前缀, 用原始字符串作为模式字符串时, 模式需要用括号包裹起来。

例,匹配x[5],因为[是元字符,需要写成:

str_view(c("int x;", "int x[5]"), "int x\\[5\\]")
## [2] │ <int x[5]>

可以用原始字符串格式写成:

str_view(c("int x;", "int x[5]"), r"{int x\[5\]}")
## [2] │ <int x[5]>

当然,因为模式中所有字符都不想作为特殊字符, 也可以用fixed()说明:

str_view(c("int x;", "int x[5]"), fixed("int x[5]"))
## [2] │ <int x[5]>

注意其中的方括号不需要用反斜杠保护。

49.1.6 匹配某类字符

49.1.6.1 匹配空白

表示空白的元字符有:

\f 换页符
\n 换行符
\r 回车符
\t 制表符
\v 垂直制表符

不同操作系统的文本文件的行分隔符不同, 为了匹配Windows格式的文本文件中的空行, 用“\r\n\r\n”; 为了匹配Unix格式的文本文件中的空行则用“\r\r”。 写成R的字符型常量时, 这些表示本身也是R的相应字符的表示, 所以在R字符型常量中这些字符不需要用两个\表示一个\

匹配任意一个空白字符用“\s”, 这等价于“[ \f\n\r\t\v]”, 但是其中的\在R字符串中要写成\\, 所以\s写成\\s。 大写的“\S”则匹配任意一个非空白的字符。

49.1.6.2 匹配数字

\d匹配一个数字,相当于[0-9]。 用\D匹配一个非数字。 如

str_view(c("n1.xls", "na.xls"), r"{n\d[.]xls}")
## [1] │ <n1.xls>

49.1.6.3 匹配字母、数字、下划线

匹配字母、数字、下划线字符用\w(小写), 等价于[a-zA-Z0-9_]\W(大写)匹配这些字符以外的字符。 如

str_view(c("file-s1.xls", "s#.xls"), 
  r"{s\w[.]}")
## [1] │ file-<s1.>xls

可以看出,模式匹配了s1.而没有匹配s#.

49.1.6.4 十六进制和八进制数

在模式中可以用十六进制数和八进制数表示特殊的字符。 十六进制数用\X引入, 比如\X0A对应\n字符。 八进制数用\0引入, 比如\011表示\t字符。

例如,str_view("abc\nefg\n", r"{\x0A}")可以匹配两个换行符。

49.1.6.5 POSIX字符类

\d, \w这样的字符类不方便用在方括号中组成字符集合, 而且也不容易记忆和认读, 按照R的双反斜杠规则写在字符串中就更难认读。

在模式中方括号内可以用[:alpha:] 表示任意一个字母。 比如,[[:alpha:]]匹配任意一个字母(外层的方括号表示字符集合, 内层的方括号是POSIX字符类的固有界定符)。

这样的POSIX字符类有:

  • [:alpha:]表示任意一个字母;
  • [:lower:]为小写字母;
  • [:upper:]为大写字母;
  • [:digit:]为数字;
  • [:xdigit:]为十六进制数字。
  • [:alnum:]为字母数字(不包括下划线);
  • [:blank:]为空格或制表符;
  • [:space:]为任何一种空白字符,包括空格、制表符、换页符、换行符、回车符;
  • [:print:]为可打印字符;
  • [:graph:][:print:]一样但不包括空格;
  • [:punct:][:print:]中除[:alnum:]和空白以外的所有字符;

例如:

str_view(c("x1", "_x", ".x", ".1"), 
  "[[:alpha:]_.][[:alnum:]_.]")
## [1] │ <x1>
## [2] │ <_x>
## [3] │ <.x>
## [4] │ <.1>

模式匹配长度为2的字符串, 第一个字符是字母、下划线或者小数点, 第二个字符是字母、数字、下划线或者小数点。 这个模式试图匹配由两个字符组成的合法R变量名, 但是最后一个非变量名.1也被匹配了。 解决这样的问题可以采用后面讲到的|备择模式。

49.1.7 备择模式

如果有两种模式都算正确匹配,则用|连接这两个模式表示两者都可以。 例如,某个人的名字用James和Jim都可以, 表示为James|Jim, 如

str_view(c("James, Bond", "Jim boy"), "James|Jim")
## [1] │ <James>, Bond
## [2] │ <Jim> boy

两个字符的合法R变量名的匹配:

str_view(c("x1", "_x", ".x", ".1"), 
  "[[:alpha:]_][[:alnum:]_.]|[.][[:alpha:]_]")
## [1] │ <x1>
## [2] │ <_x>
## [3] │ <.x>

49.1.8 匹配开头和末尾

模式匹配相当于在字符串内部搜索某种模式, 如果必须从字符串开头匹配, 在模式中取第一个模式规定为^\A。 如果模式中最后一个字符是$\Z, 则需要匹配到字符串末尾。 用\Z匹配字符串末尾时如果末尾有一个换行符则匹配到换行符之前。

str_view(c(
  "n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), 
  r"{^n\d[.]xls$}")
## [1] │ <n1.xls>

只匹配了第一个输入字符串。

用“\A”和“\Z”的写法:

str_view(c(
  "n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), 
  r"{\An\\d[.]xls\Z}")

有时候源文本的每个字符串保存了一个文本文件内容, 各行用\n分隔, 后面将给出匹配每行的行首与行尾的方法。

49.1.9 单词边界

\b匹配单词边界, 这样可以查找作为单词而不是单词的一部分存在的内容。 \B匹配非单词边界。 如

str_view(c("a cat meaos", "the category"), 
  r"{\bcat\b}")
## [1] │ a <cat> meaos

又如, 找到单独的函数sum而不是子串sum

x <- c("summary(x)", "summarize(df)", 
  "rowsum(x)", "sum(x)")
str_view(x, r"{\bsum\b}")
## [4] │ <sum>(x)

49.1.10 重复匹配

49.1.10.1 加号重复匹配

模式中在一个字符或字符集合后加后缀+表示一个或多个前一字符。 比如

str_view(c("sa1", "dsa123"), "sa[[:digit:]]+")
## [1] │ <sa1>
## [2] │ d<sa123>

例如,匹配电子邮件地址:

str_view("abc123@efg.com", 
             "^[[:alnum:]_]+@[[:alnum:]_]+[.][[:alnum:]_]+$")
## [1] │ <abc123@efg.com>

匹配的电子邮件地址在@前面可以使用任意多个字母、数字、下划线, 在@后面由小数点分成两段, 每段可以使用任意多个字母、数字、下划线。 这里用了^$表示全字符串匹配。

49.1.10.2 星号和问号重复匹配

在一个字符或字符集合后加后缀*表示零个或多个前一字符, 后缀?表示零个或一个前一字符。

比如, ^https?://[[:alnum:]./]+$可以匹配http或https开始的网址。 如

str_view(c("http://www.163.net", "https://123.456."),
             "^https?://[[:alnum:]_./]+$")
## [1] │ <http://www.163.net>
## [2] │ <https://123.456.>

(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)

x[[:digit:]]*能匹配“x”, “x1”, “x123”这样的变量名,如:

str_view(c("x", "x1", "x123"), "x[[:digit:]]*")
## [1] │ <x>
## [2] │ <x1>
## [3] │ <x123>
str_view(c("x", "x1", "x123"), "x\\d*")
## [1] │ <x>
## [2] │ <x1>
## [3] │ <x123>

从上面的例子可以看出, 在指定某一字符类(或集合)重复时, 不需要严格重复前一字符, 而是只要保持同一类即可。

49.1.10.3 计数重复

问号可以表示零个或一个, 而加号、星号重复不能控制重复次数。 在后缀大括号中写一个整数表示精确的重复次数。 如

str_view(c("1", "12", "123", "1234"), 
  "[[:digit:]]{3}")
## [3] │ <123>
## [4] │ <123>4

模式匹配的是三位的数字。 因为没有要求从开头一直匹配到末尾, 所以三位以上数字也能匹配其中开始的三位。

可以在后缀大括号中指定重复的最小和最大次数, 中间用逗号分隔。 最小重复数允许指定为0。 重复数的逗号后面空置表示重复数没有上限。 例如,后缀{3,}表示前一模式必须至少重复3次。

例:月日年的日期格式可以用

[[:digit:]]{1,2}[-/][[:digit:]]{1,2}[-/][[:digit:]]{2,4}

来匹配。

比较长的正则表达式会很难认读, 我们可以用paste0()函数将其拆分为几个部分, 并对每一部分添加适当的注释。 如 (注意这个模式还会匹配非日期)

pat <- paste0(
  "[[:digit:]]{1,2}[-/]", # month
  "[[:digit:]]{1,2}[-/]", # day
  "[[:digit:]]{2,4}")     # year
str_view(c("2/4/1998", "13/15/198"), pat)
## [1] │ <2/4/1998>
## [2] │ <13/15/198>

也可以用regex()函数中加选项comment=TRUE加注释, 这时一个正则表达式可以写成多行, 在行尾添加注释, 所有原样空格必须转义或写在字符集中,如:

pat <- regex(
  r"{[[:digit:]]{1,2}[-/]   # month
  [[:digit:]]{1,2}[-/]      # day
  [[:digit:]]{2,4}}",       # year
  comment = TRUE)
str_view(c("2/4/1998", "13/15/198"), pat)
## [1] │ <2/4/1998>
## [2] │ <13/15/198>

49.1.11 贪婪匹配和懒惰匹配

无上限的重复匹配如*, +, {3,}等缺省是贪婪型的, 重复直到文本中能匹配的最长范围。 比如我们希望找出圆括号这样的结构, 很容易想到用\(.+\)这样的模式(注意圆括号是元字符,需要用反斜杠保护), 但是这不会恰好匹配一次, 模式会一直搜索到最后一个)为止。

例如:

str_view("(1st) other (2nd)", r"{\(.+\)}")
## [1] │ <(1st) other (2nd)>

我们本来期望的是提取两个“(1st)”和“(2nd)”组合, 不料整个地提取了“(1st) other (2nd)”。 这就是因为.+的贪婪匹配。

如果要求尽可能短的匹配, 使用*?, +?, {3,}?等“懒惰型”重复模式。 在无上限重复标志后面加问号表示懒惰性重复。

比如,上例中模式修改后得到了期望的结果:

str_view("(1st) other (2nd)", r"{\(.+?\)}")
## [1] │ <(1st)> other <(2nd)>

懒惰匹配会造成搜索效率降低, 应仅在需要的时候使用。

49.1.12 允许句点匹配换行

句点通配符一般不能匹配换行,如

str_view("(1,\n2)", r"{\(.+?\)}")

跨行匹配失败。

一种办法是预先用str_replace_all()gsub()把所有换行符替换为空格。 但是这只能解决部分问题。

解决方法是在将模式用regex()保护并加选项dotall=TRUE, 使得句点通配符可以匹配换行符, 称为句点全匹配模式。 如

str_view("(1,\n2)", 
  regex(r"{\(.+?\)}", dotall=TRUE))
## [1] │ <(1,
##     │ 2)>

也可以在在Perl格式的正则表达式开头添加(?s)选项, 如:

str_view("(1,\n2)", r"{(?s)\(.+?\)}")
## [1] │ <(1,
##     │ 2)>

49.1.13 多行模式

regex()函数中加选项multiline=TRUE, 或者在正则表达式开头用(?m)表示把整个输入字符串看成用换行符分开的多行。 这时^$匹配每行的开头和结尾, “每行”是指字符串中用换行符分开的各个字符子串。 (?s)(?m)可以同时使用,写成(?sm)

例:

str_view("(1,2)\n(3,4)\n", r"{^\(.+?\)$}")

元数据中包含两行内容, 结果没有能够匹配, 这是因为模式要求从整个字符串开头一直匹配到末尾。

增加multiline=TRUE则可以对每行分别匹配, 结果找到两处匹配:

str_view("(1,2)\n(3,4)\n", 
  regex(r"{^\(.+?\)$}", multiline=TRUE))
## [1] │ <(1,2)>
##     │ <(3,4)>
##     │

使用(?m)选项:

str_view("(1,2)\n(3,4)\n", 
  r"{(?m)^\(.+?\)$}")
## [1] │ <(1,2)>
##     │ <(3,4)>
##     │

49.1.14 逐行处理

虽然正则表达式有多行和跨行选项, 但是当源数据很长时, 匹配效率会很低。

R的readLines()函数可以把一整个文本文件读成一个字符型向量, 每个元素为一行, 元素中不包含换行符。 R的字符型函数可以对这样的字符型向量每个元素同时处理, 也就实现了逐行处理。

如果字符串x中包含了一整个文本文件内容, 其中以\n分隔各行, 为了实现逐行处理, 可以先用str_split()函数拆分成不同行:

cl <- strs_plit(x, "\r?\n")[[1]]

结果将是一个字符型向量, 每个元素是原来的一行,最后一个元素是空字符串。 如

x <- c("This is first line.\nThis is second line.\n")
cl <- str_split(x, "\r?\n")[[1]]
cl
## [1] "This is first line."  "This is second line." ""

49.1.15 替换

stringr包的str_replace_all(string, pattern, replacement)在字符型向量string的每个元素中查找模式pattern, 并将所有匹配按照replacement进行替换。 在replacement可以用\1, \2中表示模式中的捕获, 除此之外元字符没有特殊作用。

基本R中gsub()有类似功能。

如:

str_replace_all(c("123,456", "011"), ",", "")
## [1] "123456" "011"

又如:

str_replace_all(c("123,456", "011"), 
                "([[:digit:]]+),([[:digit:]]+)", "\\2,\\1")
## [1] "456,123" "011"

注意源数据中第二个元素因为不能匹配所以就原样返回了, 没有进行替换。

str_replace()则仅对输入字符型向量的每个元素中模式的第一次出现进行替换, 不如str_replace_all()常用。

函数str_remove_all()相当于在str_replace_all()中指定替换内容为空字符串。 如:

str_remove_all(c("123,456", "011"), ",")
## [1] "123456" "011"

49.1.16 分组与捕获

在正则表达式中用圆括号来分出组, 作用是

  • 确定优先规则;
  • 组成一个整体;
  • 拆分出模式中的部分内容(称为捕获);
  • 定义一段供后续引用或者替换。

圆括号中的模式称为子模式,或者捕获

49.1.16.1 确定优先级

在使用备择模式时,James|Jim是在单词James和Jim之间选择。 如果希望选择的是中间的mesJi怎么办? 可以将备择模式保护起来, 如Ja(mes|Ji)m, 就可以确定备择模式的作用范围。

正则表达式有比较复杂的优先级规则, 所以在不确定那些模式组合优先采纳时, 应该使用圆括号分组, 确定运算优先级。

49.1.16.2 组成整体

元字符问号、加号、星号、大括号等表示重复, 前面的例子中都是重复一个字符或者字符类。 如果需要重复由多个字符组成的模式, 如x[[:digit:]]{2}怎么办? 只要将该模式写在括号中,如:

str_view(c("x01x02", "_x11x9"), 
  "(x[[:digit:]]{2})+")
## [1] │ <x01x02>
## [2] │ _<x11>x9

上例的元数据中, 第一个元素重复了两次括号中的模式, 第二个元素仅有一次括号中的模式。

注意: 用表示重复的元字符(+, *)重复某个模式时, 从第二次开始, 并不是要重复前面的字符或者子串, 而是重复前面的模式。 比如上例中x01x02

49.1.16.3 捕获与向后引用

有时一个模式中部分内容仅用于定位, 而实际有用的内容是其中的一部分, 就可以将这部分有用的内容包在圆括号中作为一个捕获。

分组是自动编号的, 以左开括号的序号为准(除了作为选项、有名捕获等开扩号以外)。 在替换或者向后引用时, 可以用\1\2等表示匹配中第一个左括号对应的分组, 第二个左扩号对应的分组,……。

比如,如果想严格重复前面的某个子字符串怎么办? 在模式中可以用\1, \2等表示严格重复前面捕获的子字符串, 这称为“向后引用”(back reference)。

例如,([a-z]{3})\1这样的模式可以匹配如abcabc, uxzuxz这样的三字母重复:

str_view(c("abcabc", "aabbcc"), 
  r"{([a-z]{3})\1}")
## [1] │ <abcabc>

又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:

str_view(c("2008-08-08", "2017-01-18"), 
  r"{\d{2}(\d{2})-\1-\1}")
## [1] │ <2008-08-08>

49.1.16.4 捕获与替换

利用分组捕获可以对字符串进行修改。

例:希望把带有前导零的数字的前导零删除,可以用如

x <- c("1204", "01204", "001204B")
pat <- r"{\b0+([1-9][0-9]*)\b}"
repl <- r"{\1}"
str_view(x, pat)
## [2] │ <01204>
str_replace_all(x, pat, repl)
## [1] "1204"    "1204"    "001204B"

上例的模式中的\b表示单词边界, 所以中间的0不会被当作前导零, 不是整个数字的也不会被修改。

上例中的str_view()仅用于调试目的, 在进行替换时不是必要步骤。

例:为了交换横纵坐标,可以用如下替换

x <- "1st: (5,3.6), 2nd: (2.5, 1.1)"
pat <- paste0(
  "[(]([[:digit:].]+),",
  "[[:space:]]*([[:digit:].]+)[)]", sep="")
repl <- "(\\2, \\1)"
str_view(x, pat)
## [1] │ 1st: <(5,3.6)>, 2nd: <(2.5, 1.1)>
str_replace_all(x, pat, repl)
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"

例: 要匹配yyyy-mm-dd这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:

x <- c("1998-05-31", "2017-01-14")
pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})"
repl <- r"{\2/\3/\1}"
str_view(x, pat)
## [1] │ <1998-05-31>
## [2] │ <2017-01-14>
str_replace_all(x, pat, repl)
## [1] "05/31/1998" "01/14/2017"

49.1.16.5 命名捕获

可以用(?<name>...)这样的格式对捕获命名, 而不是仅根据开括号的序号区分。 例如:

x <- c("1998-05-31", "2017-01-14")
pat <- r"{(?<year>[0-9]{4})-(?<mon>[0-9]{1,2})-(?<day>[0-9]{1,2})}"
str_view(x, pat)
## [1] │ <1998-05-31>
## [2] │ <2017-01-14>

str_replace_all()还不支持使用命名捕获作为替换。

49.1.16.6 非捕获分组

如果某个分组仅想起到分组作用但是不会提取具体的匹配内容也不会用该组内容做替换, 可以将该组变成“非捕获分组”, 办法是把表示分组开始左圆括号变成(?:三个字符。 这在用分组表示优先级时比较有用, 如"Jam(es|Ji)m"可以写成"Jam(?:es|Ji)m"。 非捕获分组在向后引用和替换时不计入\1\2这样的排列中。

比如,把1921-2020之间的世纪号删去,可以用

x <- c("1978", "2017", "2035")
pat <- r"{\A(?:19|20)([0-9]{2})\Z}"
repl <- r"{\1}"
str_view(x, pat)
## [1] │ <1978>
## [2] │ <2017>
## [3] │ <2035>
str_replace_all(x, pat, repl)
## [1] "78" "17" "35"

其中用了非捕获分组使得备择模式19|20优先匹配。 注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:

x <- c("1978", "2017", "2035")
pat1 <- r"{\A19(2[1-9]|[3-9][0-9])\Z}"
pat2 <- r"{\A20([01][0-9]|20)\Z}"
repl <- r"{\1}"
str_view(x, pat1)
## [1] │ <1978>
str_view(x, pat2)
## [2] │ <2017>
x |>
  str_replace_all(pat1, repl) |>
  str_replace_all(pat2, repl)
## [1] "78"   "17"   "2035"

49.1.16.7 提取捕获内容

为了找到字符型向量中哪些元素能匹配模式, 可以用str_subset(), str_which(), str_detect()

为了提取字符型向量中匹配模式的部分子串, 可以用str_extract()str_extract_all()

为了提取字符型向量中匹配模式的部分以及捕获的部分, 可以用str_match()str_match_all()

参见49.2

49.1.16.8 非匹配向前向后偷窥

如果要提取的一个模式以某个后缀作为标志, 可以将这个后缀作为模式的一部分, 然后仅捕获该后缀之前的部分。 比如, 对http://baidu.com, https://baidu.com, ftp://baidu.com等网址, 希望提取http, https, ftp等协议, 可以匹配到冒号为止:

addrs <- c(
  "http://baidu.com", 
  "https://baidu.com", 
  "ftp://baidu.com")
pat1 <- r"{^(.+):}"
str_view(addrs, pat1)
## [1] │ <http:>//baidu.com
## [2] │ <https:>//baidu.com
## [3] │ <ftp:>//baidu.com

这样的模式多匹配了一个“:”号。 可以用str_match()函数提取其中的捕获:

str_match(addrs, pat1)
##      [,1]     [,2]   
## [1,] "http:"  "http" 
## [2,] "https:" "https"
## [3,] "ftp:"   "ftp"

结果是字符型矩阵,第二列是所需的内容。 但这样还是在整个匹配中包含了不需要的内容。

可以用(?=)包含要提取的模式的标志性后缀, 但匹配结果不包含这个后缀,如:

pat2 <- r"{^(.+)(?=:)}"
str_view(addrs, pat2)
## [1] │ <http>://baidu.com
## [2] │ <https>://baidu.com
## [3] │ <ftp>://baidu.com

可见这次没有匹配“:”部分。 提取只要用str_extract()

str_extract(addrs, pat2)
## [1] "http"  "https" "ftp"

类似地, 如果需要提取的部分以某个前缀为标志, 可以用(?<=)包含这个前缀, 最后匹配的模式不不含这个前缀。 例如, 设输入的字符串向量addrs中都是一些网址, 希望仅匹配//后面的主要部分, 可以用:

pat3 <- r"{(?<=//).+$}"
str_view(addrs, pat3)
## [1] │ http://<baidu.com>
## [2] │ https://<baidu.com>
## [3] │ ftp://<baidu.com>

可见仅匹配了//后面的内容。 提取:

str_extract(addrs, pat3)
## [1] "baidu.com" "baidu.com" "baidu.com"

还可以用(?!)包含不允许的后缀, 用(?<!)包含不允许的前缀。

49.1.17 在数据框中拆分列

tidyr::separate_wider_regex()可以对一个字符型列用指定的模式拆分出其中的内容。 比如, 有如下的数据:

smix
Alice-F:13
Duke-M:14
Thomas-M:11

可以在tidyr::separate_wider_regex()用有名向量形式输入一个分组的模式patterns, 将有名部分拆分为新的数据框列,如:

d.swd |>
  tidyr::separate_wider_regex(smix,
    patterns = c(
      name = "[[:alpha:]]+", "-",
      sex = "F|M", ":",
      age = "[[:digit:]]+"
    ))
## # A tibble: 3 × 3
##   name   sex   age  
##   <chr>  <chr> <chr>
## 1 Alice  F     13   
## 2 Duke   M     14   
## 3 Thomas M     11

如果字符串中有固定的分隔符, 可以用tidyr::separate_wider_delim(); 如果字符串有固定的列位置分配, 可以用tidyr::separate_wider_position()

字符串内容中捕获的更一般的提取, 可以用stringr::str_match_all()

49.2 stringr包的正则表达式函数

前面主要使用了str_view(), str_replace_all()函数。 这里对stringr包的正则表达式函数进行介绍。

49.2.1 str_view()函数

str_view(string, pattern)在RStudio中加亮显示pattern给出的正则表达式模式在string中的所有匹配,并将每个匹配部分用<>界定。 在其它输出中不加亮显示, 仅使用<>界定。 string是输入的源字符型向量。

如果要匹配的是固定字符串, 写成str_view(string, fixed(pattern))

如果要匹配的是单词等的边界, 模式用boundary()函数表示,如 str_view("a brown fox", boundary("word"))将匹配首个单词。

49.2.2 regex()函数

stringr包用到正则表达式模式的地方, 实际上应该写成regex(pattern), 只写模式本身是一种简写。 regex()函数可以指定ignore_case=TRUE要求不区分大小写, 指定multi_line=TRUE使得^$匹配用换行符分开的每行的开头和结尾, dotall=TRUE使得.能够匹配换行符。 comment=TRUE使得模式可以写成多行, 行尾的井号后面表示注释, 这时空格不再原样匹配, 为了匹配空格需要写在方括号内或者用反斜杠开头。

regex()类似的表示模式的函数有fixed()boundary()coll()

49.2.3 构造复杂的模式

可以用paste0()将多个字符串连接在一起构成一个模式。 可以在regex()中用comment = TRUE将模式写成多行带有行尾注释的形式。 例如,年月日格式的日期:

pat_ymd <- regex(r"{
  (19|20)[[:digit:]]{2} # 年份
  ([-/])
  (0?[1-9]|1[012])      # 月
  \2
  (1[0-9]|2[0-9]|3[01]|0?[1-9]) # 日
  }", comments=TRUE)
str_view(c(
  "1978-1-03", "1911-07-31",
  "2023-10-18", "3000-1-1",
  "2023-14-01"), pat_ymd)
## [1] │ <1978-1-03>
## [2] │ <1911-07-31>
## [3] │ <2023-10-18>

可以用str_flatten(subpats, sep)将保存在向量subpats中的元素用sep分隔符连接起来, 组成一个长模式。 为防止输入的内容有元字符, 可以用str_escape()进行转义。 str_escape()将输入内容中的正则表达式元字符转义为原样匹配, 如:

str_escape("x[1] = myfile.txt")
## [1] "x\\[1\\] = myfile\\.txt"

例:要匹配保存在字符型向量中的任何一个颜色名, 将这些颜色名用|连接成一个长的正则表达式后匹配。

subpats <- c("red", "blue", "green")
x <- c("A red fox", "The white house", "Green sleeves")
pat <- regex(str_flatten(str_escape(subpats), "|"),
  ignore_case=TRUE)
str_view(x, pat)
## [1] │ A <red> fox
## [3] │ <Green> sleeves

49.2.4 检查哪些元素能够匹配

str_detect(string, pattern)返回字符型向量string的每个元素是否匹配pattern中的模式的逻辑型结果。 与基本R的grepl()作用类似。 如

x <- c("New theme", "Old times", "In the present theme")
str_view(x, "the")
## [1] │ New <the>me
## [3] │ In <the> present <the>me
str_detect(x, "the")
## [1]  TRUE FALSE  TRUE

上例中的str_view()仅用作调试目的。

当要查找的内容是tibble的一列时, 用filter()str_detct()配合, 可以进行行子集选择。 比如,在数据框的人名中查找中间有空格的名字:

tibble(name=c("马思聪", "李  明")) |>
  filter(str_detect(name, 
    "[[:alpha:]]+[[:space:]]+[[:alpha:]]+"))
## # A tibble: 1 × 1
##   name  
##   <chr> 
## 1 李  明

可以在dplyr::summarize()中用sum(str_detect(x, pat))计算符合模式的观测个数, 用mean计算比例。

因为str_detect()返回与输入源字符串向量等长的逻辑型向量, 所以特别适用于用逻辑运算来满足复杂的条件。 例如, 在姓名中找到同时有ay的:

x <- c("Alice", "Becka", "Gail", 
  "Karen", "Kathy", "Mary", "Sandy")
x[str_detect(x, fixed("a")) & 
    str_detect(x, fixed("y"))]
## [1] "Kathy" "Mary"  "Sandy"

a或有y的:

x <- c("Alice", "Becka", "Gail", 
  "Karen", "Kathy", "Mary", "Sandy")
x[str_detect(x, fixed("a")) | 
    str_detect(x, fixed("y"))]
## [1] "Becka" "Gail"  "Karen" "Kathy" "Mary"  "Sandy"

用这种方法可以将复杂的匹配条件拆分为多个条件, 并用与、或、非组合起来。

49.2.5 计数模式匹配次数

str_count()则返回模式在每个元素中匹配的次数。 如

str_count(c("123,456", "011", "I,II,III"), "[[:digit:]]")
## [1] 6 3 0

正则表达式在匹配时, 是否允许同一源字符串中的两次匹配有重叠部分? 这是不允许的。 例如:

str_view("1212121", "121")
## [1] │ <121>2<121>
str_count("1212121", "121")
## [1] 2

如果允许重叠, 就可以有3次121模式。

49.2.6 返回匹配的元素或元素序号

str_subset(string, pattern)返回字符型向量中能匹配pattern的那些元素组成的子集, 与基本R函数grep(pattern, string, value=TRUE)效果相同。 注意,返回的是整个元素而不是匹配的子串。

比如,查找人名中间有空格的:

str_view(c("[马思聪]", "[李  明]"), 
             "[[:alpha:]]+[[:space:]]+[[:alpha:]]+")
## [2] │ [<李  明>]
str_subset(c("[马思聪]", "[李  明]"), 
           "[[:alpha:]]+[[:space:]]+[[:alpha:]]+")
## [1] "[李  明]"

注意上例中仅返回了有匹配的元素, 而且是匹配元素的整个字符串而不是匹配的部分。

str_which(string, pattern)返回字符型向量string的元素当中能匹配pattern中的模式的元素序号。 与基本R的grep()作用类似。 如

x <- c("New theme", "Old times", "In the present theme")
str_which(x, "the")
## [1] 1 3

49.2.7 提取匹配内容

str_subset()返回的是有匹配的源字符串, 而不是匹配的部分子字符串。 用str_extract(string, pattern)从源字符串中取出首次匹配的子串, 没有匹配则不返回结果。 结果是一个与str_subset()长度相同的字符串向量, 仅匹配元素的首个匹配子串被返回。 因为输出长度不一定等于输入长度, 所以这个函数应该慎用。

str_view(c("A falling ball", "case"), "all")
## [1] │ A f<all>ing b<all>
str_extract("A falling ball", "all")
## [1] "all"

注意第二个输入字符串不匹配所有返回结果中不包含该元素的对应输出。 这会造成结果的对应困难, 所以应仅在确保所有输入都能匹配时才使用str_extract()这个函数, 一般应使用str_extract_all()

str_extract_all(string, pattern)取出所有匹配子串, 结果是一个列表, 长度与输入的字符型向量string的元素个数相等, 列表的每个元素对应于string的每个元素。 结果列表的每个元素是一个字符型数组, 存放所有匹配的子字符串, 没有匹配的输入字符串对应的输出元素是空向量(长度为0的向量)。 如:

x <- c("A falling ball", "Phone call.", "case")
str_view(x, "all")
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
str_extract_all(x, "all")
## [[1]]
## [1] "all" "all"
## 
## [[2]]
## [1] "all"
## 
## [[3]]
## character(0)

str_extract_all()可以加选项simplyfy=TRUE, 使得返回结果变成一个字符型矩阵, 每行是原来一个元素中取出的各个子串, 列数等于最大匹配次数, 没有那么多匹配次数的填以空字符串。 如果正常匹配结果不会出现空字符就可以用这种方法简化结果的保存和访问。 如

str_extract_all(x, "all", simplify=TRUE)
##      [,1]  [,2] 
## [1,] "all" "all"
## [2,] "all" ""   
## [3,] ""    ""

49.2.8 提取分组捕获内容

str_subset()提取的是能匹配模式的元素子集, 而不是匹配的模式或者捕获; str_extract()str_extract_all()提取的是每个元素的首次或者所有匹配的子字符串, 而不是其中的捕获。

str_match(string, pattern)提取每个元素的首次匹配内容以及其中各个捕获分组内容, 结果是一个矩阵, 行数等于输入的字符型向量string的元素个数, 矩阵每行对应于向量string中的一个元素, 每行第一个元素是匹配内容,其它元素是各个捕获, 没有则为字符型缺失值(不是空字符串)。

比如,希望匹配中间有空格的人名并捕获空格前后部分:

str_match(c("马思聪", "李  明"), 
  r"{([[:alpha:]]+)[[:space:]]+([[:alpha:]]+)}")
##      [,1]     [,2] [,3]
## [1,] NA       NA   NA  
## [2,] "李  明" "李" "明"

上例中源数据第一个元素没有匹配, 所以结果都是缺失值NA, 第二个元素的结果在第二行, 首先是整个匹配的子字符串, 然后是捕获的两个部分。

stringr::str_match_all(string, pattern)匹配每个字符串中所有出现位置, 结果是一个列表, 每个列表元素对应于输入的字符型向量string的每个元素, 结果中每个列表元素是一个字符型矩阵, 用来保存所有各个匹配以及匹配中的捕获, 每行是一个匹配的结果,首先是匹配结果,其次是各个捕获。 结果列表中每个作为列表元素的矩阵大小不一定相同。 当某个元素完全没有匹配时, 结果列表中对应元素是行数为0的矩阵。

比如,模式为19xx或者20xx的年份, 并将其分为前两位和后两位:

x <- c("1978-2000", "2011-2020-2099", "2100-2199")
pat <- r"{\b(19|20)([0-9]{2})\b}"
str_view(x, pat)
## [1] │ <1978>-<2000>
## [2] │ <2011>-<2020>-<2099>
mlist <- str_match_all(x,  pat); mlist
## [[1]]
##      [,1]   [,2] [,3]
## [1,] "1978" "19" "78"
## [2,] "2000" "20" "00"
## 
## [[2]]
##      [,1]   [,2] [,3]
## [1,] "2011" "20" "11"
## [2,] "2020" "20" "20"
## [3,] "2099" "20" "99"
## 
## [[3]]
##      [,1] [,2] [,3]

下面的程序合并上面提取的年份后两位为一个字符型向量:

library(purrr)
mlist |>
  keep(\(ele) nrow(ele) > 0) |>
  map(\(ele) ele[,3]) |>
  reduce(c)
## [1] "78" "00" "11" "20" "99"

用基本R功能:

ml <- Filter(function(m) nrow(m)>0, mlist)
ml <- Map(function(m) m[,3], ml)
ml <- Reduce(c, ml); ml
## [1] "78" "00" "11" "20" "99"

49.2.9 定位匹配位置

str_locate(string, pattern)对输入字符型向量string的每个元素返回首次匹配pattern的开始和结束位置。 输出结果是一个两列的矩阵,每行对应于输入的一个元素, 每行的两个元素分别是首次匹配的开始和结束字符序号(按字符计算)。如

x <- c("A falling ball", "Phone call.")
str_view(x, "all")
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
str_locate(x, "all")
##      start end
## [1,]     4   6
## [2,]     8  10

str_locate_all(string, pattern)则可以返回每个元素中所有匹配的开始和结束位置, 结果是一个列表, 每个列表元素对应于输入字符型向量的每个元素, 结果中每个列表元素是一个两列的数值型矩阵, 每行为一个匹配的开始和结束字符序号。如

x <- c("A falling ball", "Phone call.")
str_view(x, "all")
## [1] │ A f<all>ing b<all>
## [2] │ Phone c<all>.
str_locate_all(x, "all")
## [[1]]
##      start end
## [1,]     4   6
## [2,]    12  14
## 
## [[2]]
##      start end
## [1,]     8  10

注意如果需要取出匹配的元素可以用str_subset(), 要取出匹配的子串可以用str_extract()str_extract_all(), 取出匹配的子串以及分组捕获可以用str_match()str_match_all()

49.3 附录:利用基本R函数进行正则表达式处理

基本R函数grep, sub, gsub, regexpr, gregexpr, regexec中的 pattern参数可以是正则表达式, 这时应设参数 fixed=FALSEstrsplit函数中的参数split也可以是正则表达式。 regmatches函数从regexpr, gregexpr, regexec的结果中提取匹配的字符串。

以原样匹配为例。

x <- c("New theme", "Old times", "In the present theme")
regexpr("the", x, perl=TRUE)
## [1]  5 -1  4
## attr(,"match.length")
## [1]  3 -1  3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

这里使用了regexpr函数。 regexpr函数的一般用法为:

x <- c("New theme", "Old times", "In the present theme")
regexpr(pattern, text, ignore.case = FALSE, perl = FALSE,
        fixed = FALSE, useBytes = FALSE)

自变量为:

  • pattern 是一个正则表达式,如果用了fixed=TRUE选项,则当作普通原样文本来匹配;
  • text 是源字符串向量,要从其每个元素中查找pattern模式出现的位置;
  • ignore.case:是否要忽略大小写匹配;
  • perl 选择是否采用perl格式,如果不把pattern当作普通原样文本,应该选perl=TRUE,perl语言的正则表达式是事实上的标准,所以这样兼容性更好;
  • fixed 当fixed=TRUEpattern作为普通原样文本解释;
  • useBytes 为TRUE时逐字节进行匹配,否则逐字符进行匹配。之所以有这样的区别,是因为有些编码中一个字符由多个字节构成,BGK编码的汉字由两个字节组成,UTF-8编码的汉字也是由两个字节构成。

regexpr()函数返回一个整数值的向量, 长度与text向量长度相同, 结果的每个元素是在text的对应元素中pattern的首次匹配位置; 没有匹配时结果元素取-1。 结果会有一个match.length属性,表示每个匹配的长度, 无匹配时取-1。

如果仅关心源字符串向量text中哪些元素能匹配pattern, 可以用grep函数,如

x <- c("New theme", "Old times", "In the present theme")
grep("the", x, perl=TRUE)
## [1] 1 3

结果说明源字符串向量的三个元素中仅有第1、第3号元素能匹配。 如果都不匹配,返回integer(0)

grep可以使用与regexpr相同的自变量, 另外还可以加选项invert=TRUE,这时返回的是不匹配的元素的下标。

grep()如果添加选项value=TRUE, 则结果不是返回有匹配的元素的下标而是返回有匹配的元素本身(不是匹配的子串), 如

x <- c("New theme", "Old times", "In the present theme")
grep("the", x, perl=TRUE, value=TRUE)
## [1] "New theme"            "In the present theme"

grepl的作用与grep类似, 但是其返回值是一个长度与源字符串向量text等长的逻辑型向量, 每个元素的真假对应于源字符串向量中对应元素的匹配与否。如

x <- c("New theme", "Old times", "In the present theme")
grepl("the", x, perl=TRUE)
## [1]  TRUE FALSE  TRUE

就像grep()grepl()本质上给出相同的结果,只是结果的表示方式不同, regexec()regexpr()也给出仅在表示方式上有区别的结果。 regexpr()主要的结果是每个元素的匹配位置, 用一个统一的属性返回各个匹配长度; regexec()则返回一个与源字符串向量等长的列表, 列表的每个元素为匹配的位置,并且列表的每个元素有匹配长度作为属性。 所以,这两个函数只需要用其中一个就可以,下面仅使用regexpr()regexec()的使用效果如

x <- c("New theme", "Old times", "In the present theme")
regexec("the", x, perl=TRUE)
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## 
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"
## 
## [[3]]
## [1] 4
## attr(,"match.length")
## [1] 3
## attr(,"useBytes")
## [1] TRUE
## attr(,"index.type")
## [1] "chars"

grep(), grepl(), regexpr(), regexec()都只能找到源字符串向量的每个元素中模式的首次匹配, 不能找到所有匹配。 gregexpr()函数可以找到所有匹配。 如

x <- c("New theme", "Old times", "In the present theme")
gregexpr("the", x, perl=TRUE)
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## 
## [[2]]
## [1] -1
## attr(,"match.length")
## [1] -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE
## 
## [[3]]
## [1]  4 16
## attr(,"match.length")
## [1] 3 3
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

其结果是一个与源字符串向量等长的列表, 格式与regexec()的结果格式类似, 列表的每个元素对应于源字符串向量的相应元素, 列表元素值为匹配的位置, 并有属性match.length保存了匹配长度。 匹配位置和匹配长度包含了所有的匹配, 见上面例子中第三个元素的匹配结果。

函数grep, grepl结果仅给出每个元素能否匹配。 regexpr(), regexec(), gregexpr()则包含了匹配位置与匹配长度, 这时,可以用regmatches()函数取出具体的匹配字符串。 regmatches()一般格式为

regmatches(x, m, invert = FALSE)

其中x是源字符串向量, mregexpr()regexec()gregexpr()的匹配结果。 如

x <- c("New theme", "Old times", "In the present theme")
m <- regexpr("the", x, perl=TRUE)
regmatches(x, m)
## [1] "the" "the"

可以看出,regmatches()仅取出有匹配时的匹配内容, 无匹配的内容被忽略。

取出多处匹配的例子如:

x <- c("New theme", "Old times", "In the present theme")
m <- gregexpr("the", x, perl=TRUE)
regmatches(x, m)
## [[1]]
## [1] "the"
## 
## [[2]]
## character(0)
## 
## [[3]]
## [1] "the" "the"

regmatches()第二个自变量是gregexpr()的结果时, 其输出结果变成一个列表, 并且不再忽略无匹配的元素, 无匹配元素对应的列表元素为character(0), 即长度为零的字符型向量。 对有匹配的元素, 对应的列表元素为所有的匹配字符串组成的字符型向量。

实际上, 如果pattern中没有正则表达式, grep(), grepl(), regexpr(), gregexpr() 中都可以用fixed=TRUE参数取代perl=TRUE参数, 这时匹配总是解释为原样匹配, 即使pattern中包含特殊字符也是进行原样匹配。

49.3.1 不区分大小写匹配

在基本R中, 为了不区分大小写匹配, 可以在grep等函数调用时加选项ignore.case=TRUE; 如

grep("Dr", c("Dr. Wang", "DR. WANG", "dR. W.R."))
## [1] 1
grep("dr", c("Dr. Wang", "DR. WANG", "dR. W.R."), ignore.case=TRUE)
## [1] 1 2 3
grep("(?i)dr", c("Dr. Wang", "DR. WANG", "dR. W.R."))
## [1] 1 2 3

49.3.2 匹配单个字符

在模式中用“.”匹配任意一个字符(除了换行符"\n",能否匹配此字符与选项有关)。如

s <- c("abc", "cabs", "lab")
mres <- regexpr("ab.", s, perl=TRUE); mres
## [1]  1  2 -1
## attr(,"match.length")
## [1]  3  3 -1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

regexpr仅给出每个元素中模式的首次匹配位置而不是给出匹配的内容。 regmatches函数以原始字符型向量和匹配结果为输入, 结果返回每个元素中匹配的各个子字符串(不是整个元素),如:

regmatches(s, mres)
## [1] "abc" "abs"

注意返回结果和输入字符型向量元素不是一一对应的,仅返回有匹配的结果。

像句点这样的字符称为元字符(meta characters), 在正则表达式中有特殊函数。 如果需要匹配句点本身,用“[.]”或者“\.”表示。 比如,要匹配a.txt这个文件名,如下做法有错误:

grep("a.txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1 2

结果连a0txt也匹配了。用“[.]”表示句点则将句点不做特殊解释:

grep("a[.]txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1
grep("a\\.txt", c("a.txt", "a0txt"), perl=TRUE)
## [1] 1

注意在R语言字符型常量中一个\需要写成两个。

如果仅需按照原样进行查找, 也可以在grep(), grepl()regexpr()gregexpr()等函数中加选项fixed=TRUE, 这时不要再用perl=TRUE选项。 如

grep("a.txt", c("a.txt", "a0txt"), fixed=TRUE)
## [1] 1

49.3.3 匹配一组字符中的某一个

模式“[ns]a.[.]xls” 表示匹配的第一个字符是ns, 第二个字符是a,第三个字符任意,第四个字符是句点, 然后是xls。 例:

regexpr("[ns]a.[.]xls", c("sa1.xls", "dna2.xlss", "na3.xls"), perl=T)
## [1] 1 2 1
## attr(,"match.length")
## [1] 7 7 7
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

49.3.4 原样匹配元字符

例:

grep("int x\\[5\\]", c("int x;", "int x[5]"), perl=TRUE)
## [1] 2

也可以用“[[]”表示“[”, 用“[]]”表示“]”,如

grep("int x[[]5[]]", c("int x;", "int x[5]"), perl=TRUE)
## [1] 2

49.3.5 匹配数字

例:

grep("n\\d[.]xls", c("n1.xls", "na.xls"), perl=TRUE)
## [1] 1

49.3.6 匹配开头和末尾

例:

grep("^n\\d[.]xls$", c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), perl=TRUE)
## [1] 1
grep("\\An\\d[.]xls\\Z", c("n1.xls", "na.xls", "cn1.xls", "n1.xlsx"), perl=TRUE)
## [1] 1

只匹配了第一个输入字符串。

49.3.7 匹配字母、数字、下划线

例:

m <- regexpr("s\\w[.]", c("file-s1.xls", "s#.xls"), perl=TRUE)
regmatches(c("file-s1.xls", "s#.xls"), m)
## [1] "s1."

可以看出,模式匹配了s1.而没有匹配s#.

49.3.8 十六进制和八进制数

例如

gregexpr("\\x0A", "abc\nefg\n")[[1]]
## [1] 4 8
## attr(,"match.length")
## [1] 1 1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

匹配了两个换行符。

49.3.9 POSIX字符类

例如:

grep("[[:alpha:]_.][[:alnum:]_.]", c("x1", "_x", ".x", ".1"))
## [1] 1 2 3 4

49.3.10 加号重复匹配

s <- c("sa1", "dsa123")
mres <- regexpr("sa[[:digit:]]+", s, perl=TRUE)
regmatches(s, mres)
## [1] "sa1"   "sa123"

例如:

p <- "^[[:alnum:]_]+@[[:alnum:]_]+[.][[:alnum:]_]+$"
x <- "abc123@efg.com"
m <- regexpr(p, x, perl=TRUE)
regmatches(x, m)
## [1] "abc123@efg.com"

匹配的电子邮件地址在@前面可以使用任意多个字母、数字、下划线, 在@后面由小数点分成两段, 每段可以使用任意多个字母、数字、下划线。 这里用了^$表示全字符串匹配。

49.3.11 星号和问号重复匹配

^https?://[[:alnum:]./]+$可以匹配http或https开始的网址。 如

s <- c("http://www.163.net", "https://123.456.")
grep("^https?://[[:alnum:]_./]+$", s, perl=TRUE)
## [1] 1 2

(注意第二个字符串不是合法网址但是按这个正则表达式也能匹配)

49.3.12 计数重复

例:

grep("[[:digit:]]{3}", c("1", "12", "123", "1234"))
## [1] 3 4

模式匹配的是三位的数字。

日期匹配例:

pat <- paste(
  c("[[:digit:]]{1,2}[-/]",
    "[[:digit:]]{1,2}[-/]",
    "[[:digit:]]{2,4}"), collapse="")
grep(pat, c("2/4/1998", "13/15/198"))
## [1] 1 2

49.3.13 贪婪匹配和懒惰匹配

例如:

s <- "<B>1st</B> other <B>2nd</B>"
p1 <- "<[Bb]>.*</[Bb]>"
m1 <- regexpr(p1, s, perl=TRUE)
regmatches(s, m1)[[1]]
## [1] "<B>1st</B> other <B>2nd</B>"

我们本来期望的是提取第一个“<B>……</B>”组合, 不料提取了两个“<B>……</B>”组合以及中间的部分。

比如,上例中模式修改后得到了期望的结果:

s <- "<B>1st</B> other <B>2nd</B>"
p2 <- "<[Bb]>.*?</[Bb]>"
m2 <- regexpr(p2, s, perl=TRUE)
regmatches(s, m2)[[1]]
## [1] "<B>1st</B>"

49.3.14 单词边界

例:

grep("\\bcat\\b", c("a cat meaos", "the category"))
## [1] 1

49.3.15 句点全匹配与多行模式

句点通配符一般不能匹配换行,如

s <- "<B>1st\n</B>\n"
grep("<[Bb]>.*?</[Bb]>", s, perl=TRUE)
## integer(0)

跨行匹配失败。而在HTML的规范中换行是正常的。 一种办法是预先用gsub把所有换行符替换为空格。 但是这只能解决部分问题。

另一方法是在Perl正则表达式开头添加(?s)选项, 这个选项使得句点通配符可以匹配换行符。 如

s <- "<B>1st\n</B>\n"
mres <- regexpr("(?s)<[Bb]>.*?</[Bb]>", s, perl=TRUE)
regmatches(s, mres)
## [1] "<B>1st\n</B>"

多行模式例:

s <- "<B>1st\n</B>\n"
mres1 <- gregexpr("^<.+?>", s, perl=TRUE)
mres2 <- gregexpr("(?m)^<.+?>", s, perl=TRUE)
regmatches(s, mres1)[[1]]
## [1] "<B>"
regmatches(s, mres2)[[1]]
## [1] "<B>"  "</B>"

字符串s包含两行内容,中间用\n分隔。 mres1的匹配模式没有打开多行选项, 所以模式中的^只能匹配s中整个字符串开头。 mres2的匹配模式打开了多行选项, 所以模式中的^可以匹配s中每行的开头。

49.3.16 备择模式

例如,某个人的名字用James和Jim都可以, 表示为James|Jim, 如

s <- c("James, Bond", "Jim boy")
pat <- "James|Jim"
mres <- gregexpr(pat, s, perl=TRUE)
regmatches(s, mres)
## [[1]]
## [1] "James"
## 
## [[2]]
## [1] "Jim"

49.3.17 分组与捕获

例: 希望把“<B>……</B”两边的“<B>”和“</B>”删除, 可以用如下的替换方法:

x <- "<B>1st</B> other <B>2nd</B>"
pat <- "(?s)<[Bb]>(.+?)</[Bb]>"
repl <- "\\1"
gsub(pat, repl, x, perl=TRUE)
## [1] "1st other 2nd"

替换模式中的\1(写成R字符型常量时\要写成\\)表示第一个圆括号匹配的内容, 但是表示选项的圆括号((?s))不算在内。

例:希望把带有前导零的数字的前导零删除,可以用如

x <- c("123", "0123", "00123")
pat <- "\\b0+([1-9][0-9]*)\\b"
repl <- "\\1"
gsub(pat, repl, x, perl=TRUE)
## [1] "123" "123" "123"

其中的\b模式表示单词边界, 这可以排除在一个没有用空格或标点分隔的字符串内部拆分出数字的情况。

例:为了交换横纵坐标,可以用如下替换

s <- "1st: (5,3.6), 2nd: (2.5, 1.1)"
pat <- paste0(
  "[(]([[:digit:].]+),",
  "[[:space:]]*([[:digit:].]+)[)]")
repl <- "(\\2, \\1)"
gsub(pat, repl, s, perl=TRUE)
## [1] "1st: (3.6, 5), 2nd: (1.1, 2.5)"

例如,要匹配yyyy-mm-dd这样的日期, 并将其改写为mm/dd/yyyy, 就可以用这样的替换模式:

pat <- "([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})"
repl <- "\\2/\\3/\\1"
gsub(pat, repl, c("1998-05-31", "2017-01-14"))
## [1] "05/31/1998" "01/14/2017"

分组除了可以做替换外, 还可以用来表示模式中的重复出现内容。 例如,([a-z]{3})\1这样的模式可以匹配如abcabc, uxzuxz这样的三字母重复。如

grep("([a-z]{3})\\1", c("abcabc", "aabbcc"))
## [1] 1

又例如,下面的程序找出了年(后两位)、月、日数字相同的日期:

x <- c("2008-08-08", "2017-01-18")
m <- regexpr("\\d\\d(\\d\\d)-\\1-\\1", x)
regmatches(x, m)
## [1] "2008-08-08"

下面是一个非捕获分组示例。 设需要把1921-2020之间的世纪号删去,可以用

pat <- "\\A(?:19|20)([0-9]{2})\\Z"
repl <- "\\1"
x <- c("1978", "2017", "2035")
gsub(pat, repl, x, perl=TRUE)
## [1] "78" "17" "35"

其中用了非捕获分组使得备择模式19|20优先匹配。 注意模式并没有能保证日期在1921-2020之间。更周密的程序可以写成:

x <- c("1978", "2017", "2035")
p1 <- "\\A19(2[1-9]|[3-9][0-9])\\Z"
r1 <- "\\1"
p2 <- "\\A20([01][0-9]|20)\\Z"
x <- gsub(p1, r1, x, perl=TRUE)
x <- gsub(p2, r1, x, perl=TRUE)
x
## [1] "78"   "17"   "2035"