作者Wush978 (拒看低质媒体)
看板R_Language
标题[心得] Regular Expression
时间Sun Apr 23 17:11:36 2017
[关键字]: 字串处理、正则表示式
[重点摘要]:
以下是我发在R语言翻转教室中介绍「正则表示式」的原始Rmd档案。
好读板欢迎到网站上观看:
http://datascienceandr.org/articles/RegularExpression.html
另外我想强调:里面只简单介绍我常用、熟悉的功能,不是全部的正则表示式的功能。
---
# 前言
正则表示式(Regular Expression)是我们在处理纯文字资料时,几乎可以解决所有问题
的技术。
R 语言有内建许多与正则表示式相关的函数,不需要安装套件即可使用。
这篇文章想要跟各位同学介绍我自己很常使用的一些正则表示式的函数。
## 什麽是正则表示式?
正则表示式是一种描述文字模式的语言。
它不是单纯依照应用归纳出来的工具,背後具有相当的数学基础。
正则表示式的诞生,来自於美国数学家Stephen Cole Kleene在超过半个世纪之前的研究
成果:@Kleene56。
目前各种程式语言中,几乎都内建正则表示式,但是他们的语法主要分成两个派系:
- 一种语法出自於电机电子工程师学会(IEEE)制定的标准
- 一种语法,则来自於另一个程式语言:Perl
正则表示是可以让我们撰写程式来自文字中比对、取代甚至是抽取各种资讯。以下我们将
从简单的应用开始介绍。
# R 语言中的正则表示式
这篇文章中,我不会介绍全部与正则表示式相关的函数与功能。
以下我介绍的这些函数,目前已经足够我解决了许多工作上的问题了。
学习技术的目的应该还是聚焦在解决问题上,所以只要能学会一个泛用的功能,比学会每
个功能都介绍。
因此我没介绍的功能,就再请同学们有兴趣再去自行补足。
## 比对
我们回到翻转教室中02-RDataEngineer-05-Data-Manipulation的一个问题:我们想要检
查飞机的尾翼号码上有AA的资料,是不是刚好都是美国航空(carrier资料也会是AA)的
问题。
那时候我们请同学使用的函数:`grepl`就是R 之中使用正则表示式做比对的函数。
```{r, echo = FALSE}
args(grepl)
```
在大部分的状况,我们把要搜寻的文字规则放到`grepl`的第一个参数,也就是`pattern`
参数之中。
在这里的例子,就是`"AA"`。
接着把被搜寻的文字放到`grepl`的第二个参数,也就是`x`参数。
在这里的例子,是`flights$tailnum`
接着,`grepl`就会告诉我们,每一个`flights$tailnum`的字串是不是有包含`"AA"`。
举例来说,`flights$tailnum`的前六个元素的结果依序是:`r head(grepl("AA",
flights$tailnum))`
我们可以看一下资料中的`tailnum`:
```{r}
head(flights$tailnum)
```
确实,只有第三个元素`r inline.print.string(flights$tailnum[3])`有包含`"AA"`。
更广泛的来说,这里的`pattern`参数,就是一个正则表示式的语言。
R 会先把`"AA"`解析乘要比对的内容,并且一一比对参数`x`的元素。
如果比对成功,对应的答案就会是`TRUE`。
反之,则会是`FALSE`。
刚好在这个例子,`"AA"`对正则表示式来说,就是简单的比对这个字串中有没有连续两个
`"A"`。
这样子的功能,同学在Word中也能透过CTRL+F等搜寻功能达成。
但是正则表示式只有这麽简单嘛?
## 开头或结尾
在正则表示式中,我们可以指定模式出现的位置是不是在文字的开头或结尾。
这里我们拿一个与`grepl`的行为非常接近的函数:`grep`来做范例。
`grep`与`grepl`的参数有八成像,具体来说,多了`value`与`invert`这两个参数。
这篇文章中限於篇幅,我们不会仔细的讲解所有的参数。
所以有兴趣的同学麻烦再自行研究。
`grep`与`grepl`最大的差异,在於它回传的是「符合`pattern`」的「位置」或「文字」
,而不是一个布林向量。
因此结果的向量长度,会短很多。
在正则表示式中,我们可以在`pattern`的第一个字元放上`"^"`,代表接下来的模式一定
要从字串的开头开始。
举例来说:
```{r}
grep("^AA", flights$tailnum)
```
以上的程式码告诉我们,有哪些位置的tailnum资料是由`"AA"`开头的,结果是R没有找到
。
同学可以用`substring`做验证:
```{r}
sum(substring(flights$tailnum, 1, 2) == "AA", na.rm = TRUE)
```
结果会与刚刚的`grep`的结果符合。
又如果我们在`pattern`的最後一个字元放上`$`,则代表接下来之前的模式一定要是字串
的最後。
举例来说:
```{r}
head(grep("AA$", flights$tailnum))
```
我们可以加上参数`value = TRUE`来看比对成功的文字:
```{r}
head(grep("AA$", flights$tailnum, value = TRUE))
```
这里,我们还是可以用`substring`来验证我们的结果:
```{r}
ans.substring <- substring(flights$tailnum, nchar(flights$tailnum) - 1,
nchar(flights$tailnum)) == "AA"
ans.grepl <- grepl("AA$", flights$tailnum)
all(ans.substring == ans.grepl, na.rm = TRUE)
```
以上的指令中,`nchar`代表这个字串中有多少个字元。
组合`substring`与`nchar`,我们就可以切割出字串最後两个字元。
而`all`这个函数,必须要参数中的布林向量全部都是`TRUE`的时候,才会回传`TRUE`。
## 不定长度:同时比对`"AA"`、`"AAA"`或`"AAAA"`等等
在正则表示式中,我们能写出一种模式,告诉R 我们的目标的长度是不确定的。
举例来说:`"AA"`在正则表示式中等价於`"A{2}"`,也就是把字元`"A"`重复两次的模式
。
大括号里的数字,就代表重复的次数。
而重复的字元,限定是大括号前面的一个字元。
R 要刚好找到,才判断字串符合我们提供的模式。
```{r}
all(grep("AA", flights$tailnum) == grep("A{2}", flights$tailnum), na.rm =
TRUE)
```
同理可证,我们可以找连续三个A:
```{r}
head(grep("A{3}", flights$tailnum, value = TRUE))
```
因此我们可以告诉R,我们需要的A可以有连续2、3或4个。
```{r}
grep("A{2,4}", c("A", "AA", "AAA", "AAAA", "AAAAA"), value = TRUE)
```
同学有没有注意到,连续五个A也被算是比对成功呢?因为五个A也算是符合连续2~4个A的
模式。
甚至是我们可以告诉R,只要有连续的A(但是至少要一个)就行了。
这里的`"A+"`的模式代表,至少要有一个A。
```{r}
grep("A+", c("", "A", "AA", "AAA", "AAAA", "AAAAA"), value = TRUE)
```
同学请注意到,空字串`""`比对失败。
与`+`类似的,还有`*`与`?`的符号。
`?`代表0个或1个。
`*`代表连续0个、1个、2个...
总而言之,透过大括号,以及`+`、`*`与`?`,我们可以透过正则表示式表达各种可能的
长度的模式。
最後提醒同学,大括号、`+`、`*`与`?`都是针对他们之前的一个字元而已。
## 不定字元
那除了长度可以有不确定之外,模式中的字元也可以是不确定的。
举例来说,如果我们要找的不再是如`"A"`、`"AA"`与`"AAA"`等连续的相同字元,而是两
个A中间夹杂任意字元的模式。
我们可以利用:`"A.A"`来表示这样的模式。
这里的`.`代表一个任意字元:
```{r}
head(grep("A.A", flights$tailnum, value = TRUE))
```
同学可以看到,R 找到符合`"A.A"`模式的字元的前六个之中,你们是不是能找到两个A中
间夹着任意字元的模式呢?
## 否定字元
上一个模式中,包含`"AAA"`这样的模式。
如果我们希望两个A中间夹着的字元是数字呢?
我们可以利用`"A[0-9]A"`来表示:
这里的中括号,代表一个字元。而这个字元必须要符合中括号之间的字元,才算有效。
而`0-9`在这里就代表0, 1, 2, ..., 9 等十个字元的集合。
```{r}
grep("A[0-9]A", flights$tailnum, value = TRUE)
```
结果在tailnum中,是找不到符合这种模式的航班。
理由是因为,航班的tailnum资料需要注册,并且符合一定的规则的。
通常第一个字元一定是`N`,後接若干个数字,最後会是英文字母。
因此,`A[0-9]A`的模式就不存在了。
有兴趣的同学可以参考[Aircraft
registration](
https://en.wikipedia.org/wiki/Aircraft_registration)上的资料。
中括号除了代表「符合」的模式,之外,也可以代表「不符合」。
两者的差别在於「符合」的模式,是`[]`,而「不符合」的模式,是`[^]`。
举例来说:
```{r}
head(grep("N[13]", flights$tailnum, value = TRUE))
```
找出来的模式,N的後面必须是1或3。
```{r}
head(grep("N[^13]", flights$tailnum, value = TRUE))
```
找出来的模式,N的後面**不能**是1或3。
这类不定字元的模式,也可以与前面介绍的不定长度的技巧一起使用。
举例来说:
```{r}
head(grep("N[13]{3}", flights$tailnum, value = TRUE))
```
这里找出来的模式,N後面一定有连续三个1或3。
大家有没有开始慢慢感受到正则表示式的威力了呢?
## 子模式
以上的内容我很早就自己学会了。
但是在实务上,还是常常遇到没办法解决的问题。
直到我学会子模式之後,是我才开始觉得正则表示式能够解决大部份我的问题。
这里先跟同学复习一下,刚刚我们介绍的正则表示式语法中,如大括号、`+`、`*`与`?`
都是针对他们之前的一个字元而已。
那如果我们不只是针对一个字元呢?
举例来说,如果我想要找的是像1212这样,12重复两次的模式呢?
答案就是用小括号建立子模式。
举例来说,`"(12){2}"就是连续12这两个数字的模式,连续出现两次的模式:
```{r}
head(grep("(12){2}", flights$tailnum, value = TRUE))
```
这样找出来的模式,都一定会有1212。
这样的手法,可以跟上面教过的语法整合。
举例来说,我们可能想要的是:
```{r}
head(grep("(1[23]){2}", flights$tailnum, value = TRUE))
```
这里的小括号中的子模式,就是前面教的:`"1[23]"`,也就是1後面可以接2或3。
而这样的模式,再重复两次。
因此同学会看到上面的指令找出来的结果,都会有1312, 1212, 1213... 等模式出现。
## 抽取资讯
在R之中,我们不只可以寻找子模式,我们还可以请R 把子模式的资讯抽取出来。
我们回到RDataEngineer-01-Parsing的最後一个测验。
这个问题中,我们需要从海盗的资料抽取资讯。我们先从github中下载这份课程中的资料
:
```{r pirate_info, cache = TRUE}
pirate_path <- tempfile(fileext = ".txt")
download.file("
https://raw.githubusercontent.com/wush978/DataScienceAndR/course/02-RDataEngineer-01-Parsing/pirate-info-2015-09.txt",
destfile = pirate_path)
pirate_info <- readLines(file(pirate_path, encoding = "BIG5"))
head(pirate_info)
```
透过翻转教室中的教学,我们知道可以使用`strsplit`或`substring`来从文字中萃取资
料。
但是如果利用正则表示式的子模式来抽取时间的资讯,我们可以这样写:
```{r, dependson="pirate_info"}
head(
regmatches(
pirate_info,
regexec("日期:([0-9]{4})年([0-9]{1,2})月([0-9]{1,2})日", pirate_info)
))
```
这里我们使用的是`regexec`的函数,并且与`regmatches`函数搭配。
`regexec`也是R里面的正则表示式的函数之一。
`regmatches`可以从比对出来的结果,抽取资讯。
这样的组合在抽取子模式的内容时,是很方便的。
举例来说,同学可以看到上面的输出,是一个list物件。
那些pirate_info的字串元素,如果不符合我们写的模式,为应的list物件中的元素,就
会是`character(0)`。
如果有比对成功,R不只会回传字串的内容,还会帮我们把子模式的内容也抽出来放在後
面。
所以同学会看到第六笔资料:`"1. 日期:2015年9月6日"`在抽出子模式之後,我们知道
:
- 第一个子模式(年)的内容是`"2015"`
- 第二个子模式(月)的内容是`"9"`
- 第三个子模式(日)的内容是`"6"`
这样的功能,在实务上非常的好用。
## 跳脱字元
运用上述的技巧,我们就可以去抽取,例如文字中的成对括号之间的文字。
举例来说,如果我们有一个文字资料:
```{r}
x <- "123(45)657"
```
我们希望能够抓取两个小括号之间的数字。
我第一次遇到类似问题时,就根据上面我学到的技巧,写出这样的正则表示式:
```{r}
pattern <- "((.*))"
```
那时我心里这样想:外圈的括号代表文字中的括号。内圈的括号代表子模式。
R 应该可以懂我吧?
当然不懂!
**小括号就是代表子模式,这是绝对的!**
```{r}
regmatches(x, regexec(pattern, x))
```
所以R 并没有找到两个括号之间的数字:`"45"`。
那我们怎麽半呢?答案是要透过「跳脱字元」。
正则表示式的跳脱字元,是`"\"`,刚好与R 的字串的跳脱字元一样。
因此虽然我心里想要输入的是:
`\((.*)\)`
但是跑到R 的双引号中间之後,每一个`"\"`都要重复两遍:
```{r}
pattern <- "\\((.*)\\)"
```
我们可以透过`cat`函数来看R 看到的双引号之间的文字,是什麽:
```{r}
cat(pattern)
```
虽然我们输入的时候,`"\"`需要两个,可是透过`cat`,我们可以确定R 并没有搞混。
```{r}
regmatches(x, regexec(pattern, x))
```
结果R 也真的能找到45。
因此这里要提醒同学,所有之前讲过得特殊符号,例如大括号、中括号、小括号、
`+`... 在比对的时候,如果这些刚好是你要比对的文字,那就要加上跳脱字元`"\"`,程
式才能正确的诠释我们想表达的意思。
又刚好跳脱字元`"\"`也是R 的字串的跳脱字元,所以我们在输入时,一个`"\"`就要输入
两次。
因此,如果刚好要比对的符号是`\`,那在R里面的输入就要是`"\\\\"`了...
# 总结
以上介绍了我个人常用的正则表示式的功能,希望对同学有帮助。
之後我会释出一个小关卡,让读过这篇文章的同学可以练习正则表示式。
看不懂的同学,也都很欢迎到聊天室,或是下面的留言板讨论。
如果对内容有建议的网友,也欢迎在或是到
<
https://github.com/wush978/DataScienceAndR/issues>发issue给我。
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 1.161.235.120
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/R_Language/M.1492938702.A.203.html
1F:→ celestialgod: 字串处理我比较喜欢stringr XDD 04/23 17:14
2F:→ celestialgod: maybe 我有空来写一篇stringr教学 04/23 17:14
3F:→ Wush978: 我记得他们的语法比较简洁。但是能不用套件就不用套件 04/23 20:21
4F:→ Wush978: 毕竟安装套件有一定的overhead 04/23 20:21
5F:→ Wush978: Btw, 反正会正规表示式之後,用不用stringr也不会是主要 04/23 20:22
6F:→ Wush978: 的问题... 04/23 20:22
7F:推 obarisk: 5个A那里有点问题,实际上是.*a{2,4}.*这个pattern,所以 04/23 20:57
8F:→ obarisk: 5A才会抓到,如果是BA{2,4}B,那就不会抓到5个A 04/23 20:57
9F:→ Wush978: 请问楼上指的问题,是什麽呢? 04/23 21:18
10F:→ Wush978: 原文只是说明AAAAA会符合A{2,4} 04/23 21:19
11F:→ Wush978: 我不太懂为什麽会牵涉到.*a{2,4}.* 04/23 21:19
12F:推 obarisk: match 和 contain 的差别 04/24 00:21
13F:→ Wush978: 那为什麽会牵涉到那个看起来和我写的pattern毫无关系的 04/24 00:43
14F:→ Wush978: .*a{2,4}.*呢? (大小写不同,还加上可能多余的前後缀) 04/24 00:44
15F:推 cywhale: 推 正则配reg*..,gsub等还蛮好用,只是有时规则想破脑XD 04/24 08:56
16F:推 obarisk: 抱歉,是 A{2,4}.* 才对,AAAAA是符合contain 04/24 21:32
17F:→ obarisk: 但是A{2,4} match 的是 AA AAA AAAA 04/24 21:33
18F:→ obarisk: 应该是我表达不好QQ 04/24 21:34
19F:推 obarisk: 其它大概只剩 \1 的引用比对可以补充了 04/24 21:36
20F:→ celestialgod: 楼上,Wush的意思只是要表示AAAAA 会被A{2,4}抓到 04/24 21:44
21F:→ celestialgod: 除非你用 ^A{2,4}$才不会抓到AAAAA 04/24 21:44
22F:→ celestialgod: grpel("A{2,4}", "AAAAA") => TRUE 04/24 21:47