正则表达式快速入门

55分钟学会正则表达式

Posted by liaozhida on October 22, 2016

转载请注明出处 来源:paraller’s blog

想系统的学习正则表达式,在网上找了很多教程,其中《55分钟学会正则表达式》这个翻译自外网的教程讲的最系统详细,学完整个正则表达式的耗时大概在两个多小时的样子。 为什么写这篇文章,因为我觉得如果不是这篇文章太生硬,废话比较多,排版乱,有些地方存在错误的话,我觉得55分钟就学完了。所以重新整理排版了一次。

学习的工具是 cat log | grep -E 'pattern'

正则表达式的基础语法

核心知识点
  • 正则表达式由只代表自身的字面值和代表特定含义的元字符组成。
  • 字面值(Literals): 意味着它们查找的是字符串自身
  • 元字符:代表一些模式匹配
句点 .
  • 一个.表示匹配任何单个字符。下面这个正则表达式c.t代表: 先找到c,接着找到任何单个字符,再找到t。
  • 将会找到cat,cot ,甚至字面值为c.t的字符串,但是不包括ct或者coot。
反斜杠 \
  • 任何元字符如果用一个反斜杆\进行转义就会变成字面值。所以上述的正则表达式c\.t ,能够匹配文本 c.t
  • 文本a\h,可以通过 a\\h找到
字符类( Character classes )
  • 定义: 字符在方括号中的集合。表示:找到其中任意的字符
  • 正则表达式c[aeiou]t表示“找到c后跟一个元音字母,再找到t”。将会匹配到cat
  • 正则表达式[0123456789]表示找到一个数字
  • 正则表达式[a]a意义相同:“找到a”

一些转义的例子:

  • \[a\] 表示文本 [a]
  • [\[\]ab] 表示匹配一个 [ 或者 ] 或者 a 或者 b
  • [\\[\]] 表示“匹配一个\ 或者 [ 或者 ]

note:

  • 一些字符在字符类内部扮演着元字符的角色,但在字符类外部则充当字面值。比如:连字符 -
  • 一些字符做着相反的事。比如: .表示“匹配任意字符”,但是[.]表示“匹配句点”
  • 一些字符在两种情形都为元字符,但在各自情形里代表不同的含义。
字符类区间(ranges)

你可以在字符类中使用连字符来表示一个字母或数字的区间_:

  • [b-f][bcdef] 一个意思
  • [A-Z][ABCDEFGHIJKLMNOPQRSTUVWXYZ] 都表示“匹配大写字母”。
  • [1-9][123456789] 都表示“匹配一个非零数字”。

区间和单独的字符,可能会共存于同一个字符类:

  • [0-9.,]表示“匹配 一个数字 或者 一个. 或者 一个,”。
  • [0-9a-fA-F]表示“匹配一个十六进制数”。
  • [a-zA-Z0-9\-]表示“匹配 一个字母数字字符 或 -”。

Note:

  • 你可以尝试在区间内以非字母数字字符结束(比如abc[!-/]),但这在其它实现中的语法不一定对。即使语法正确,但在这个区间内很难看出包含了哪个字符。请不要这么干
  • 同样的,区间端点的范围应该一致。即使像[A-z]这种表达式在你选择的实现中合法,但它做的可能会与你想法用出入。
  • 注意。 区间是字符的区间,不是数字的区间。正则表达式[1-31]表示 找到一个1或一个 2或一个3,不是找到一个从1到31的整数
字符类的否定(negation)

你可以通过在最开始的位置使^来排除一个字符类。

  • [^a]表示“匹配除了a的任意字符”。
  • [^a-zA-Z0-9]表示“找到一个非字母数字字符”。
  • [\^abc]表示“找到一个^ 或者 a 或者 b 或者 c”。
  • [^\^]表示“找到除了^的任意字符”
字符类补充
  • \d 含义与[0-9]一致:“匹配一个数字”。
  • \w 的含义与[0-9A-Za-z_]一致:“匹配一个单词字符,( 字母或数字或下划线或汉字)”。
  • \s 表示“匹配任意空白字符(空格,tab)”。
  • \s+ 可以表示回车和换行
  • \D[^0-9]:“匹配任意非数字的字符”。
  • \W[^0-9A-Za-z_]:“匹配任意非单词字符(译者注:匹配任意不是字母,数字,下划线,汉字的字符)”。
  • \S 表示“匹配任意不是空白符的字符”。
  • \t 表示制表符
  • [\u4e00-\u9fa5] 表示中文和英文
  • [^\x00-\xff] 匹配双字节字符,中文也是双字节的字符,不包括英文
乘法器(Multipliers)

可以在字面值或者字符类后跟着一个大括号来使用乘法器。

  • 正则表达式a{1}同a,表示“匹配一个a”。
  • \d{3} 表示3个相连的数字。
  • a{0} 表示“匹配空字符”。
  • a\{2\} 代表文本 a{2}
  • 在字符类中大括号没有特别的含义。[{}]代表“匹配一个{ 或者 }
  • 乘法器没有记忆。[abc]{2}表示“匹配a或者b或者c,接着匹配a或者b或者c。这跟匹配aabbcc含义不同
乘法器区间
  • x{4,4} 跟x{4}一样。
  • colou{0,1}r 表示“匹配colour或color。
  • a{3,5} 表示“匹配aaaaa或aaaa或aaa”。
  • a{1,} 表示“在一列中找到一个或多个a”。然而你的乘法器将会是贪婪的。在找到第一个a后,它将会尽可能匹配到更多的a。
  • .{0,} 表示“匹配任何情形”。不管你的输入文本是什么——甚至为空

注意:

  • 优先选择更长的匹配,因为乘法器是贪婪的。如果你输入的文本是I had an aaaaawful day,该正则表达式就会在aaaaawful中匹配到aaaaa。不会在第三个a后就停止匹配。
  • 乘法器在找到第一个文本的时候就会停止,如果你的输入文本为I had an aaawful daaaaay,之后这个正则表达式会在第一次的匹配中于aaawful找到aaa。只有在你说“给我找到另一个匹配”的时候,它才会继续搜索然后在daaaaay中找到aaaaa
乘法器补充
  • ?代表的含义与{0,1}相同。比如说,colou?r表示“匹配colour或color”。
  • *等于{0,}。比如说,*表示“匹配一切”,跟上面提到的一样。
  • +等于{1,}。比如说,\w+表示至少匹配一个或以上的单词。 [0-9]+代表至少匹配一个或以上数字
  • \?\*\+表示?*+
  • [?*+]表示找到一个?或者一个*或者一个+
惰性(Non-greed)

前面说到乘法器是贪婪的,可以通过添加?来消除贪婪特性

  • \d{4,5}? 就会等于 \d{4} ,在找到合适的文本之后就停下来
  • ".*?"表示“匹配一个双引号,跟着一个尽可能少的字符,再跟着一个双引号”。这实际上很有用
分支(Alternation)

你可以使用管道符号来实现匹配多种选择:

  • cat|dog表示“匹配cat或dog”。
  • red|blue|red||blue以及|red|blue 都是同样的意思,“匹配red或blue或空字符串”。
  • a|b|c[abc]一样。
  • cat|dog|\|表示“匹配cat或dog或管道符号”。
  • [cat|dog]表示“找到a或c或d或d或g或o或t或一个管道符号”。
组合(Grouping)
  • 在一周中找到一天,使用(Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day
  • (\w*)ility等同于\w*ility。都表示“找到以ility结尾的单词”。为什么第一种形式更有用,后面会看到…
  • \(\)表示“匹配一个(后,再匹配一个)”。
  • [()]表示“匹配一个( 或 一个)”。
单词边界(Word boundaries)

单词边界是一个单词字符和非单词字符之间的位置。记住,一个单词字符是\w,它是[0-9A-Za-z_],一个非单词字符是\W,也就是[^0-9A-Za-z_]。

文本的开头和结尾总是当作单词边界。输入的文本it's a cat有八个单词边界。如果我们在cat后追加一个空格,这里就会有九个单词边界。

  • 正则表达式\b表示“匹配一个单词边界”。
  • \b\w\w\w\b表示“匹配一个三个字母的单词”。
  • a\ba表示“找到a,跟着一个单词边界,接着找到a”。不管输入文本是什么,这个正则表达式永远都不会成功找到一个匹配。
  • 单词边界不是字符。它们宽度为零.下面的正则表达式表示相同的含义:
行边界(Line boundaries)

每一块文本会分解成一个或多个行,用换行符分隔,像这样:

  • 正则表达式^表示“匹配开始行”。
  • 正则表达式$表示“匹配结束行”。
  • ^$表示“匹配空行”。
  • ^.*$将会匹配整个文本,因为换行符是一个字符,所以.会匹配它。为了匹配单行,要使用惰性乘法器,^.?$ , ^.*?$
  • \^\$表示“匹配尖符号后跟着一个美元符号”。
  • [$]表示“匹配一个美元符”。然而,[^]是非法单正则表达式。要记住的是尖符号在方括号中时有不同的特殊含义。把尖符号放在字符类中,这么用[\^]
  • 像单词边界一样,行边界也不是字符。它们宽度为零。

捕获和替换

这里就是正则表达式开始变得异常强大的地方。

捕获组

你已经知道,括号是用来表示组。它们也可以用来捕获子串。如果正则表达式是一个很小的电脑程序,这个捕获组就是它的输出(的一部分)。

正则表达式(\w*)ility表示“找到一个以ility结束的单词”。捕获组1就是匹配了部分内容的\w*

  • 文本包含单词accessibility,捕获组1就是accessib
  • 文本只包含ility,捕获组1就是空字符串。

你可以拥有多个捕获组,它们甚至可以嵌套使用。捕获组从左到右进行编号。只要计算左圆括号。

假设我们到正则表达式是(\w+) had a ((\w+) \w+)。如果我们的输入文本是I had a nice day,那么: 捕获组1是I。 捕获组2是nice day。 捕获组3是nice

在一些实现中,你可能可以访问捕获组0,即完整匹配:I had a nice day

是的,这确实意味着圆括号有些重复。从一个成功返回的匹配中捕获组数量总是等于原来正则表达式中捕获组的数量。记住这一点,因为它可以帮助你理解一些令人困惑的情形。 正则表达式((cat)|dog)表示“匹配cat或dog”。这里总是存在两组捕获组。如果我们的输入文本是dog,那么捕获组1是dog,捕获组2是空字符串,因为另一个选择未被使用

正则表达式a(\w)*表示“ 匹配一个以a开头的单词”。这里总是只有一个捕获组(译者注:除去捕获组0):

  • 如果输入文本是a,捕获组1是空字符串。
  • 如果输入文本是ad,捕获组1是d
  • 如果输入文本是avocado,捕获组1是vocado
替换

一旦你用了正则表达式来查找字符串,你可以指定另一个字符串来替换它。第二个字符串时替换表达式

你可以在你的替换表达式中引用捕获组。这是你可以在替换表达式唯一能的特殊的事,它意味着你不必完全销毁你刚刚发现的东西。

比方说,你尝试去用ISO 8691格式的日期(YYYY-MM-DD)去替换美式日期(MM/DD/YY)。

  • 通过正则表达式(\d\d)/(\d\d)/(\d\d)开始。注意这里有三个捕获组:月,日和两个数字表示的年。
  • 通过使用一个\和一个捕获组号来引用一个捕获组。所以,你的替换表达式为20\3-\1-\2
  • 如果我们的输入文本是03/04/05(表示 3月4号,2005年),那么:
    • 捕获组1是03;
    • 捕获组2是04;
    • 捕获组3是05;
  • 你可以在替换表达式中多次引用捕获组。
  • 在替换表达式中的反斜杆必须进行转义。举个例子,你有一些在计算机程序的字面值中使用的文本。那就意味着你需要在普通文本中的每个双引号或者反斜杆前放置一个反斜杆。
  • 正则表达式([\"])中,捕获组1是"或者\
  • 替换表达式\\\1中,一个字面值反斜杆后跟着一个匹配的双引号或者反斜杆。
后向引用(Back-references)
  • 你可以在同样的表达式中引用同一个捕获组。这称为后向引用
  • 表达式([abc])\1表示匹配aabbcc
结合正则表达式编程

过度反斜线综合征(Excessive backslash syndrome)

在一些编程语言中,如Java,对于含有正则表达式的字符串没有提供特别的支持。字符串有自己的转义规则,这些规则与正则表达式的转义规则叠加,通常会导致反斜杆过多(overload)。比如(还是Java):

  • 为了匹配一个数字,正则表达式\d在源代码中变成String re = "\d;"
  • 为了匹配一个双引号字符串,"[^"]*"变成String re = "\"[^\"]*\"";。
  • 为了匹配一个反斜杆或者一个左方括号或者一个又方括号,正则表达式[\\\\[\\]]变成String re = "[\\\\\\[\\]]";。
  • String re = "\s";和String re = "[ ]";是一样的。注意不同的转义“优先级”。

在其它编程语言里,通过一个特殊标记来标识正则表达式,通常是正斜杆/。这里有一些JavaScript例子:

  • 为了匹配一个数字,\d变成var regExp = /\d/;。
  • 匹配一个反斜杆或者一个左方括号或者一个右方括号,var regExp = /[\\\\[\\]]/;。
  • var regExp = /\s/;和var regExp = /[ ]/;一样。
  • 当然,这意味着必须对正斜杠而不是双引号进行转义。匹配URL的前面部分:var regExp = /https?:\/\//;。

基于这一点,我希望你明白为什么我对你反复提及反斜杆。

练习

题目如下:

  • 将文本中的所有 中文 替换成 中文
  • 编写一个正则表达式匹配1到31(含)之间的整数。
  • 双引号内所有不包含 " 的文本
  • 双引号内所有文本

答案:

## Find:
(_)([^\x00-\xff]+)+(_)
## Replace
`\*\*\2\*\*`
[1-9]|[12][0-9]|3[01]
cat log | grep -E '"[^"]{0,}"'
cat log | grep -E '".{0,}"'

练习草稿

I had a nice day

a,a
a!b
90
"i love you , mary!"
"i love you","hey"
food
z...z
a b     c
d       g
e
iec ieac
c[abc]at
2016-10-22 12:12:12
cat
c.t
c\.t  [a]

其他

偏移量(Offsets)

在文本编辑器中,会在你光标所在处开始搜索。这个编辑器会向前开始搜索文字,然后停在第一个匹配的地方。下一次搜索会在第一次完成搜索的地方的右侧开始。

当编程的时候,文本的_偏移量_必须的。这个偏移量会在代码中有明确的支持,或保存在包含文本的对象中(如Perl),或包含正则表达式的对象中(如JavaScirpt)。(在Java里,这是一个由正则表达式和复合对象的字符串。)在任何情况下,默认值> 0,表示文本的开始。搜索后,偏移量会自动更新,或者作为输出的一部分返回。

无论什么情况,通常很容易去使用循环来解决这个问题。

注意正则表达式匹配空字符串是完全可能的。 你可以立马实现的一个简单的例子是a{0}在这种情况下,新的偏移量等于旧偏移量,从而导致死循环。

一些实现可能保护你避免发生这些情况,但要查下对应的文档。

动态正则表达式

动态地构造一个正则表达式字符串时一定要小心。如果你使用的字符串不是固定的,那么它可能包含意想不到的元字符。这会导致语法错误。更糟糕的是,它可能产生一个语法正确,但行为不可预期的正则表达式。

有bug的Java代码:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(sep);

这个bug就是:String.split()认为sep是一个正则表达式。但是在Windows下,sep是由犯斜杆组成的字符串\.这不是一个语法正确的正则表达式。结果是:一个异常PatternSyntaxException。

任何一个优秀的编程语言都提供了一种机制,用以转义在一个字符串中出现的所有元字符。在Java中,你可以这么做:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(Pattern.quote(sep));
邮件地址

不要使用正则表达式来验证邮件地址。

首先,这很难保证正确无误。电子邮件地址确实符合一个正则表达式,但是这个表达式长又复杂地让人联想到世界末日。任何缩略都会可能产生遗漏(false negatives)。(你知道吗?电子邮件地址可以包含注释!)

其次,即使所提供的电子邮件地址符合正则表达式,但也并不能证明它的存在。验证电子邮件地址的唯一方法是发送电子邮件给它。

标记

在正式的应用中,不要使用正则表达式来解析HTML或XML。解析HTML/XML是

不可能使用简单的正则 一般来说很难 一个已解决了的问题。 不妨找一个已有的解析库来为你搞定这些工作。

一些场景

替换
Host    segmentfault.com
Connection  keep-alive
Content-Length  55
Accept  */*
Origin  https://segmentfault.com
X-Requested-With    XMLHttpRequest
User-Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
^(\S+)(\s+)(.{0,})
"\1":"\3",
"Host":"segmentfault.com",
"Connection":"keep-alive",
"Content-Length":"55",
"Accept":"*/*",
"Origin":"https://segmentfault.com",
"X-Requested-With":"XMLHttpRequest",
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
文本块 换行匹配
---
layout:     post
tags:
    - linux
---


---(\n(.{0,}))*---
乘法器惰性匹配

实际场景: 匹配<>内的数据 ; 如果不加? 会把整个文本匹配上

<span class="RichText CopyrightRichText-richText" itemprop="text" data-reactid="321">什么是用户画像

正则表达式:<(.{0,}?)>
删除 doc 注释
/**
 * Returns the number of elements in this list.  If this list contains
 * more than <tt>Integer.MAX_VALUE</tt> elements, returns
 * <tt>Integer.MAX_VALUE</tt>.
 *
 * @return the number of elements in this list
 */
int size();

正则表达式: (/\*\*)(\n(\s+)(\*)(.{0,}))*(\/)

参考网站

55分钟学会正则表达式(译)