Wenhu Next Generation Bioinformatician

01-正则表达式

2018-01-22
Wenhu

本讲主要介绍正则表达式(regular expression, regex)的概念、主要语法风格以及在R中的使用方法。欢迎转载,但请注明出处!

为何讲它

一周前,在学习The Art of R Programming第35页的时候遇到一个练习,我想用模式匹配去解决,也就是regex,但遇到了未曾料到的问题,于是我在stackoverflow上提问了,大牛的解答让我想要集中几天精力搞清楚regex到底怎么用,问题见此:my question不巧被标记了重复

regex是一个相对独立的内容,几乎所有编程语言都实现了这种功能,这也证明了它的重要性,另外,本讲标题序号虽然是01,但其实大家可以放在遇到实际问题时再来学,那样有了共鸣,印象更深刻!

正则表达式

我们在面对生物数据,尤其是序列信息(比如碱基序列、氨基酸序列等)的时候, 会时常想要问自己,这其中是否包含着且含有多少某种已知的模式,一段DNA中是否包含转录起始特征TATA box、一段RNA中是否包含某种lncRNA、一段肽链中是否包含锌指结构等等;另一方面,我们在操作数据时,会时常遇到诸如把某个字符(对象)换成另一种字符(对象)的替换操作,而其本质还是如何搜索符合某种(替换)模式的对象。

在这些几乎天天都可以碰到的模式匹配/搜索问题中,正则表达式就是一把解决问题的利剑!

正则表达式:它是一连串用来描述模式的字符,它的书写遵循一定的语法规则,当用户进行文本匹配的时候,程序语言会调用预设的引擎(其实就是内置的或第三方库里的软件包),利用正则表达式去搜索符合模式的文本信息!英文名是regular expression,常缩写为regex,或regexp。

知道英文名后,大家就应该不用去纠结“正则”这个反人类名词了,你可以理解为规则的、规范的,或者直接叫它“模式表达式”。多说一句,劝大家学习时尽量阅读英文材料,中文材料中很多硬生生的翻译,不仅不能达意,还会让很多人产生误解!

语法风格

由以上概念,我们大概会以为正则表达式就像一种模式书写规范,它应该是唯一的,且被大多数编程语言所共用。然而,事情却不那么优雅,从regex诞生之日起,先后产生了数十种适应于不同程序语言的语法规则和风格,而且有些语言还实现了不只一种语法规则,比如R。其中流传最广的一种是:POSIX (Portable Operating System Interface for uniX)标准下的BRE (Basic Regular Expression)、ERE (Extended Regular Expression);另一种则是PCRE (Perl Compatible Regular Expressions),详情请参考regular expression in wiki

在R语言(版本>R 2.10.0)中,它使用了两种语法规则:默认的是Ville Laurikari’s TRE,还有一种就是PCRE

在本讲中,我使用的是高效且被广泛使用的PCRE语法,同样也推荐大家使用,操作很简单,只需在相应的R函数里加上perl = TRUE 参数即可调用此语法,在后面的内容中大家会看到!

常用语法

虽然说regex有很多种语法风格,但毕竟人是比较懒的,既然有前人的轮子在,而且用起来也很不错,那么就不会有人愿意从头再造一遍,所以这些语法大同小异,下面我将介绍一些PCRE中常用的语法规则。(以下内容多数引用自learn regex the easy way

基本匹配

“the” => The fat cat sat on the mat.

“The” => The fat cat sat on the mat. (默认大小写敏感)

元字符(meta characters)

所谓的meta-X(中文翻译成“元”啊,“宏”啊,“后设”啊,先可以不管),在英文中代表概念X背后的概念,而每个meta词汇又有其特有的解释方法,比如data是数据(如工资表),meta-data则是描述这个工资表的数据(什么纸打印的,用的哪种墨盒,存放在哪个档案库等等)。此处的meta characters,指的是这些字符虽然本质是字符,但并非是它们字面(literal)上的意思,而是我们赋予的新的意思,“后设”的意思!

  1. 句号.(period):通配符,可以匹配单个除了换行符(newline)之外的所有字符。

    ".ar" => The car parked in the garage.

  2. 字符集[...](character set):括号中的字符都可以用来匹配,但只能匹配单个位置,如果用短横线-隔开,表示一个范围,例如[a-z],表示从a到z中间所有字符,包括两端。

    "[Tt]he" => The car parked in the garage.

  3. 取反字符集[^...](negated character set):括号中的字符都不可用来匹配。

    "[^c]ar" => The car parked in the garage.

  4. 计数字符+, *, ?(repetition):

  • +:前一个字符重复1次至多次    "c.+t" => The fat cat sat on the mat.
  • *:前一个字符重复0次至多次 "[a-z]*" => The car parked in the garage #21.
  • ?:前一个字符重复0次或1次 "[T]?he" => The car is parked in the garage.
  1. 计数区间{n, m}:和前一条类似,只不过指明了重复次数在n和m之间,包含两端。注意{n}表示只重复n次,{n,}表示重复大于等于n次。

    "[0-9]{2,3}" => The number was 9.9997 but we rounded it off to 10.0.

  2. 亚模式(subpattern):用小括号括起来的字符串属于一个固定的模式,需要不偏不倚的被匹配到。

    "(ar\si)" => The car is parked in the garage. \s代表空格)

  3. 或者|(alternation):或然关系,匹配到其中一个即可。

    "(T|t)he|car" => The car is parked in the garage.

  4. 首尾匹配,^表明后面的字符一定是处于第一的位置,而$表明之前的字符一定是处于最末的位置,见例子:

    "^(T|t)he" => The car is parked in the garage.

    "(at\.)$" => The fat cat. sat. on the mat.

缩写字符集(shorthand character sets)

shorthand definition
\w 匹配所有大小写字母及数字,即[a-zA-Z0-9]
\W 与上面相反,即[^\w]
\d 匹配所有数字,即[0-9]
\D 与上面相反,即[^\d]
\s 匹配所有空格字符,如[\t\n\r]
\S 与上面相反,即[^\s]

前后看(lookaround)

这个语法也就是开头提到的、那个让我解决不了的问题的核心,其实学会了它后,你会感觉很有意思。

假设一下,如果我们想找的模式X周围还有特定的亚模式Y,该怎么办?你可以直接匹配你要的模式X,但如果有些X旁边没有Y,我们该怎么排除出去呢?这时候“前后看”就派上大用场了,我们以Y为眼睛,让它看看四周有没有X,有的话就直接匹配上!

  • 向前看X(?=Y)(positive lookahead):顾名思义,是匹配Y前面的X。

    "(T|t)he(?=\sfat)" => The fat cat sat on the mat.

  • 向前删X(?!Y)(negative lookahead):和上面相反,是匹配Y前面的X。

    "(T|t)he(?!\sfat)" => The fat cat sat on the mat.

  • 向后看(?<=Y)X(positive lookbehind):匹配Y后面的X。

    "(?<=(T|t)he\s)(fat|mat)" => The fat cat sat on the mat.

  • 向后删(?<!Y)X(negative lookbehind):匹配Y后面的X。

    "(?<!(T|t)he\s)(cat)" => The cat sat on cat.

有一点要注意的是,lookaround中的模式,即上面的模式Y,是不会被引擎捕获到的,这一点在某些特定情况下还蛮有用的,请参看my question

PCRE本身的语法还有不少,但常用的基本都在这里了,如果想深入了解,请参考PCRE syntax

R中怎么使用

前面提到,R实现了两种regex语法风格,个人推荐使用PCRE风格,不然前面的也白看了不是?R中常用的模式匹配操作,一是运用base包中的几个函数,二是利用stringr包中的系列函数,本文只介绍前者,在熟练之后,大家应该去学习下后者,大神出品,方便易用,参见R for data science - Strings。(以下内容多数引用自Regular Expressions with The R Language

匹配

  • grep函数:”grep”的意思是”global search for regular expression and print matching lines”,这是最核心的函数,其他的几个函数与之大同小异。第一个参数是pattern,即模式;第二个则是input,即需要被匹配的文本。当value=FALSE时,给出匹配上的元素的位置,而value=TRUE则会直接给出匹配上的元素。
> grep("a+", c("abc", "def", "cba a", "aa"), perl=TRUE, value=FALSE)
[1] 1     3       4
> grep("a+", c("abc", "def", "cba a", "aa"), perl=TRUE, value=TRUE)
[1] "abc" "cba a" "aa"
  • grepl函数:”l”在这里是”logical”的缩写,所以,你可以猜到,这个函数给出的结果是一串逻辑值。
> grepl("a+", c("abc", "def", "cba a", "aa"), perl=TRUE)
[1] TRUE  FALSE TRUE  TRUE
  • regexpr函数:上面的函数只能告诉你哪些文本被匹配上了,但如果我们需要知道具体匹配的位置,就需要用到regexprgregexpr函数了。除了没有value参数外,regexpr函数的写法与上面完全一样,但得到的结果则是对应于每个input的元素、模式从左至右第一次被匹配到的位置!如果没有匹配上,会显示-1
> regexpr("a+", c("abc", "def", "cba a", "aa"), perl=TRUE)
[1]  1 -1  3  1
attr(,"match.length")
[1]  1 -1  1  2
attr(,"useBytes")
[1] TRUE

可以看到,函数返回的结果是一个和input等长的vector,其中包含的是首次匹配上的位置,这个vector还含有两个属性(attributes),第一个给出的是匹配上的文本的长度,比如这里第四个,匹配上的是"aa",所以长度是2;第二个暂时不用管它。

  • gregexpr函数:它比上面的兄弟多了一个”g”,你可能已经猜到,这是个global匹配函数,与regexpr唯一的区别是,它返回的值是一个list,针对input中的每个元素,都会给出所有匹配上的位置。
> gregexpr("a+", c("abc", "def", "cba a", "aa"), perl=TRUE)
[[1]]
[1] 1
attr(,"match.length")
[1] 1
attr(,"useBytes")
[1] TRUE

[[2]]
[1] -1
attr(,"match.length")
[1] -1
attr(,"useBytes")
[1] TRUE

[[3]]
[1] 3 5
attr(,"match.length")
[1] 1 1
attr(,"useBytes")
[1] TRUE

[[4]]
[1] 1
attr(,"match.length")
[1] 2
attr(,"useBytes")
[1] TRUE
  • regmatches函数:前面的grep函数中,如果设置value=TRUE,那么会给出每个匹配上的元素的整体(比如用”a”去匹配”abc”,grep会得到”abc”),而如果我们需要的仅仅是匹配上的部分(即”a”),regmatches就能派上用场了。这个函数需要的参数一个自然是模式,另一个则是regexprgregexpr返回的值,因为其中包含了匹配上的位置。
> x <- c("abc", "def", "cba a", "aa")
> m <- regexpr("a+", x, perl=TRUE)
> regmatches(x, m)
[1] "a"  "a"  "aa"

> m <- gregexpr("a+", x, perl=TRUE)
> regmatches(x, m)
[[1]]
[1] "a"

[[2]]
character(0)

[[3]]
[1] "a" "a"

[[4]]
[1] "aa"

替换

  • subgsub函数:顾名思义,需要有三个参数,模式、替换文本、原文本(pattern, replacement, input)。区别也很明显,sub是首位匹配,gsub是全局匹配。
> sub("(a+)", "z", c("abc", "def", "cba a", "aa"), perl=TRUE)
[1] "zbc"   "def"   "cbz a" "z"    
> gsub("(a+)", "z", c("abc", "def", "cba a", "aa"), perl=TRUE)
[1] "zbc"   "def"   "cbz z" "z"
  • 更为直接的方式是,利用前面提到的regmatches函数,因为它得到刚好仅仅是匹配上的值,那么给这个函数赋予一个vector,里面包含替换文本,即可更改原文本
> x <- c("abc", "def", "cba a", "aa")
> m <- gregexpr("a+", x, perl=TRUE)
> regmatches(x, m) <- list(c("one"), character(0), c("two", "three"), c("four"))
> x
[1] "onebc"       "def"         "cbtwo three" "four"

结语

regex用途非常广泛,但当你初次接触时,会有一段时间的羞涩期,不过不用担心,这是正常的,很快你就会发现它的好,欲罢不能。regex本身又是包含很多深层次内容的,本讲的介绍只是蜻蜓点水、管中窥豹,难免挂一漏万,如有错误,还望不吝指正。

要想把regex用得熟、用得好,必须要有一定的训练量!所以最后,给出一些不错的学习资源,望大家继续钻研。

  1. regex-info
  2. regex-online-learning
  3. Regular-Expressions-Cookbook

关注我的最新博文,请订阅my RSS ~~


Similar Posts

上一篇 leaRning--preface

下一篇 01-regex

评论 / Comments