正则表达式快速入门

供新手入门和老手参考的教程和相关资料,包括中文帮助
User avatar
amnesiac
Posts: 186
Joined: 22 Nov 2013, 03:08
Location: Egret Island, China
Contact:

正则表达式快速入门

18 Aug 2014, 21:54

导言:这个简单的入门向导主要是让您明白正则表达式是什么、能做什么、以及如何在 AutoHotkey 中使用它。注意这里介绍的正则表达式基于 AutoHotkey 所使用的 PCRE 引擎,在其他语言中特性和语法可能有差异。例如 SciTE 编辑器也支持正则表达式,不过风格不一样。

帮助中的相关部分:
RegEx: Quick Reference
RegExMatch()
RegExReplace()
RegEx: Callouts
RegEx: SetTitleMatchMode

简明规则介绍

也许您以前曾听说过正则表达式,或曾看过别人写的式子,感觉它像天书一样复杂。不过,只要您跟我一步步操作,您会发现其实没有想象中的那么难。很可能您使用过 Dos/Windows 下用于文件查找的通配符即 *?。如果想查找某个目录下的所有的 AutoHotkey 文档的话,您会搜索 *.ahk。在这里,* 会被解释成任意的字符串。而正则表达式也是用来进行文本匹配的工具,不过它比通配符更强大,可以进行更精确的匹配,当然,相应会复杂一些。学习正则表达式比较好的方法是从例子开始,理解例子后对例子进行修改、实验,所以我这里介绍几个简单的例子,并加以详细说明,现在就开始吧!

普通匹配
我想找出 Haystack 中 is 首次出现的位置:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "is", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

这几乎是最简单的匹配模式了,很简单吧?
需要了解的是 RegExMatch() 函数从字符串的左边开始寻找匹配的首个字符串然后返回这个字符的位置,源字符串中首个字符的位置为 1。
提一下,中文字符是普通字符,可以直接使用在匹配模式中进行匹配。

句点
我想找出 Haystack 中的首个字符:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, ".", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

从结果可以看出,这里得到的不是句点的位置,而是位置在第一个的字母 T。. 是最常用的元字符,它可以匹配除换行符外的任意字符(包括句点),可以把它当成 Windows 通配符中的星号。

次数匹配
我想找到从“is”开始往后的整个字符串:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "is.*", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

这个模式中,* 不匹配字符,而是修饰前面一个单元,表示前一个单元的字符可以出现任意次。那么,.* 则表示可以匹配除换行符外的任意字符出现任意次数,所以这里匹配了从 is 出现往后的所有内容。
相关:
* 表示前一单元可以出现零次或多次;
+ 表示前一单元可以出现一次或多次;
? 表示前一单元可以出现零次或一次;
{n,m} 表示前一单元可以出现 n 次到 m 次(n<m),这种形式有几种变形:{n}、{n,} 等。


位置匹配
示例 1 中虽然找出了首个 is 的位置,但它是在 This 单词中,而我需要找到单词 is 的位置。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "\bis\b", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

现在找到啦,这里匹配模式中 \b 不匹配字符,它匹配这样一个位置:它的前一个字符和后一个字符不全是字母、数字和下划线(一个是,同时另一个不是或不存在)。首个 \b 后是字母 i,那么它前一个字符不能是字母、数字和下划线中的其中一种,所以排除了 This,因为这里前一个字符是字母 h。
相关:与 \b 这样匹配位置的还有,^ 匹配字符串的开始,$ 匹配字符串的结束。
思考:结合前面的内容,这里如果要匹配 g 开始的单词呢?

分支条件
如果我想匹配 Haystack 中 good 或 book 呢?

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos1 := RegExMatch(Haystack, "good|book", Match1, 1)
MsgBox, % "FoundPos: " FoundPos1 "`n" "Match: " Match1
FoundPos2 := RegExMatch(Haystack, "good|book", Match2, 12)
MsgBox, % "FoundPos: " FoundPos2 "`n" "Match2: " Match2

现在两个都匹配到了,第二次 RegExMatch() 函数参数中的 12 表示从第 12 个字符开始匹配。这里模式中 | 表示可选的,即只要符合其中一个分支就会形成匹配。
这里简单提到一点:假如匹配时,同时符合多个分支,那么实际产生的匹配是最前面那个。a|ab 模式实际不会得到 ab 的匹配,如果有兴趣,可以执行下面的代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "ababa"
FoundPos := 0
Loop
{
StartingPos := FoundPos + 1
FoundPos := RegExMatch(Haystack, "a|ab", Match, StartingPos)
If (FoundPos = 0)
Exit
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
}

只得到 a 的匹配,这是由于进行匹配时如果满足前面的分支后不再测试后面的分支,直接到下一个字符进行测试。假如这里模式换成 ab|b,那么也会产生b的匹配。

小括号
我想匹配某个字符串中所有邮政编码,你也许马上想到了可以使用上面的分支条件,0|1|2|3|4|5|6|7|8|9{6},实验后发现不对了,这里 {6} 只修饰 9,而前面的数字只进行单独的匹配。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "654321-123456"
FoundPos := 0
Loop
{
StartingPos := FoundPos + 1
FoundPos := RegExMatch(Haystack, "(0|1|2|3|4|5|6|7|8|9){6}", Match, StartingPos)
If (FoundPos = 0)
Exit
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
}

这里模式中,() 使得其中包含的内容成为一个单元,而 {6} 修饰这个单元,所以可以匹配任意六位数。
说明:让包含的内容成为一个单元是小括号的一个作用,后面再介绍其他用法。

方括号
上面的例子中这么写也太繁琐了吧,如果要匹配所有字母,小写加大写有 52 个,不是会写的很长吗?别急,这里所有数字的组合可以简写成 [0-9],那么 (0|1|2|3|4|5|6|7|8|9){6} 就变成了 [0-9]{6},简单了吧?试验下,看看效果是不是相同。
这里的模式中,[0-9] 表示匹配从 0 到 9 的所有数字,这是一个字符组(也是一个单元),{6} 是修饰这个字符组的。这样我们可以根据实际把需要的字符放在方括号中进行匹配,如需要匹配 13 或 15 开始的手机号码,可以用这样的模式 1[35][0-9]{9},共11位数字。
类似的,[a-z] 可以匹配任意一个小写字母,而 [a-zA-Z0-9_] 可以匹配字母、数字和下划线中任意一个。
那么如果我需要匹配非数字的任意两个字符呢?

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

; 请使用这两行替换上一个例子中相应行进行试验
Haystack := "654321-123456: This is a good book."
FoundPos := RegExMatch(Haystack, "[^0-9]{2}", Match, StartingPos)

这里,方括号中的 ^ 表示排除,注意必须紧跟着 [ 才有排除的含义,如果在其他位置则变成普通字符(还记得吗,在方括号外它表示匹配字符串的开始处)。有趣的是,这里 - 表示从前一个字符到后一个字符的范围,如果需要让方括号单元可以匹配连字符,必须把它放在紧接着"]"的位置,即在单元中它后面不能含有其他字符。当 - 在方括号外时为普通字符。
另外还需要注意:在方括号中,如果需要匹配 ],必须进行转义,如 [0-9\]] 可以匹配任一数字或 ]。
思考,如果我需要匹配任意一个汉字呢?

常用字符组
忘了告诉你一个秘密,[0-9] 还有更简单的写法 \d。特殊字符组的简化形式主要有这些:
\d 相当于 [0-9],匹配任意一个数字;
\w 相当于 [0-9a-zA-Z_],匹配单词中任一的字符;
\s 通常情况下相当于 [ \f\n\r\t\v],注意方括号中首个是空格,匹配任一空白字符;

如果我想排除这些字符呢,即匹配这些字符外的其他字符?
\D 相当于 [^0-9];
\W 相当于 [^0-9a-zA-Z_];
\S 匹配非空白字符。

其他还有一些特殊字符组,参见帮助中 POSIX 形式表示的特殊字符组。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

; 请使用这两行替换上一个例子中相应行进行试验
Haystack := "654321-123456: This is a good book."
FoundPos := RegExMatch(Haystack, "\D{2}", Match, StartingPos)

效果和上一个例子一样吗?

匹配元字符
说了那么多,那我该如何匹配句点呢?

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "\.", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

前面说过了,由于句点属于特殊字符,所以要匹配它自身时必须进行转义,即在它前面加一个转义符(反斜线),这样它就成为普通字符了。特殊字符主要包括 \.*?+[{|()^$,对它们进行匹配时需要在前面加上转义符。
注意:这里的转义符是正则中的转义符,为反斜线,不是指 AutoHotkey 中的转义符(重音符)。

匹配一些特殊字符

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book.`r`n"
FoundPos := RegExMatch(Haystack, "`n", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

特殊字符主要有,\t(制表符),\r(回车符),\n(换行符),还支持 \xhh 的转义序列(这里 hh 表示介于 00 到 FF 之间的ANSI 字符的十六进制码)。还有其他一些特殊字符,请参见帮助。
另外,在这种情况下可以使用 AutoHotkey 的转义符(重音符)替换正则语法的转义符(反斜线),但在其他情况下则不能。
提到一下,空格是普通字符,不需要转义,也不需要特殊表示进行匹配。

贪婪与懒惰

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "T.*is", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match

我们发现,这里它不是匹配到单词 This,而是 This is,现在我们把匹配模式修改成 T.*?is,再试试~
这里 ? 修饰匹配次数单元,表示尽可能少重复,同理:
.*? 重复任意次,但尽可能少重复
.+? 重复 1 次或更多次,但尽可能少重复
.? 重复 0 次或 1 次,但尽可能少重复
{n,m}? 重复 n 到 m 次,但尽可能少重复
{n,}? 重复 n 次以上,但尽可能少重复

默认情况下为贪婪匹配,即匹配尽可能多的字符,这里使用问号可以控制它前面一个表示匹配次数的单元进行懒惰匹配,如果表示匹配次数的单元比较多,且都需要进行懒惰匹配,这时使用选项比较方便。

选项
先看这个例子:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Haystack := "Welcome to RegEx world!"
FoundPos := 0
Loop
{
StartingPos := FoundPos + 1
FoundPos := RegExMatch(Haystack, "E", Match, StartingPos)
If (FoundPos = 0)
Exit
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
}

接着把上面的匹配模式 E 换成 i)E,再看看结果。
比较后可以发现,后面这种模式除了匹配大写 E 外,还可以匹配小写的e。这里的 i 是正则表达式的选项,可以影响模式匹配时的一些行为,i 表示进行模式匹配时不区分大小写。选项与模式之间使用闭括号隔开。这里说明一下,前面的例子都是在不含选项即默认的情况下匹配的行为。更多选项请参见帮助。

常见应用示例

在普通的文本查找/替换等操作时,应该首先考虑 InStr()、IfInString、StringGetPos、StringReplace,这样更简单不易出错(且执行效率较好)。当使用普通匹配很繁琐或容易产生问题时,则应考虑使用正则表达式了。窗口的匹配也是如此。

何时应该转义,如何转义
匹配引号,这个似乎应该写到前面的基础中,还记得表达式中一个原义的引号如何表示的吗?

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

OldText := "This is a ""good"" book."
FoundPos := RegExMatch(OldText, """")

引号对于正则来说不应该看成特殊字符,这里的特殊是由于在 RegExMatch() 和 RegExReplace() 中,引号必须确定匹配模式的部分,因此在语句解析时它具有特殊含义,为了传递正确的形式到正则进行处理,应该使用两个连续的引号表示一个原义的引号(这样在 AutoHotkey 解释器解释后传递给正则的就是单个引号了)。 这种表示法是表达式中原义引号的表达方法,如果用反斜线进行转义,这样会在正则处理前解释就出错了。
还是再啰嗦一点:反斜线或 \Q...\E 这样的转义方法只适用于正则表达式中的特殊符号,而同时适用 AutoHotkey 和正则的转义方法的在帮助中只说明了三个符号(Tab、回车符和换行符)。 匹配模式中的字符是否需要转义需要看字符和字符所在位置(例如有些字符只在方括号中甚至在其中的特殊位置才有特殊含义),如何转义则看是这个字符是属于什么的特殊符号。这点可能是正则中比较容易产生问题的地方。

去除路径中末尾的反斜线
在 FileSelectFolder 命令中,当用户选择了根目录(如 C:\),输出变量会以反斜线结尾,可用下面的方法去掉它:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Folder := RegExReplace(Folder, "\\$")

这里还可以用普通方法,即获取最后一个字符,判断它是否为反斜线,是则去除,比起来用正则语句上稍简洁些:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

If (SubStr(Folder, 0) = "\")
StringTrimRight, Folder, Folder, 1

去除文本中的空行
有时我们在处理文本时,需要将多个空行合并成一个空行,这时可以参考下面的例子:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

NewText := RegExReplace(OldText, "(*BSR_ANYCRLF)\R+", "`n")

模式中的 (*BSR_ANYCRLF) 表示后面的 \R 可以匹配 CR/LF/CRLF 三种换行符,这里如果需要将多个空行合并成一个,则需要把替换的字符串换成两个换行符。换成普通方法时,可以用循环替换:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Loop
{
StringReplace, OldText, OldText, `r`n`r`n, `n, UseErrorLevel
if ErrorLevel = 0 ; 不需要再进行替换
break
}
NewText := OldText

比较起来,使用正则可以处理换行符较复杂的情况。普通方法如果需要考虑多种换行符,可以先进行换行符的替换。

去除多行文本中行首行尾的空格和 Tab
去除单行文本首尾的空格和 Tab,只需在 AutoTrim 设置为 On 的情况下重新赋值,那么多行文本如何处理呢?

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

NewText := RegExReplace(OldText, "m)(*ANYCRLF)^[[:blank:]]*(.*?)[[:blank:]]*$", "$1")

这里对其中包含的一些情况进行说明:
  1. m 选项可以把 OldText 当做多行文本而不是作为一个整体处理(主要影响行首行尾位置的匹配);
  2. (*ANYCRLF) 的作用是规定换行符的识别,影响位置匹配,默认情况下只识别 CRLF,加上这个后还可以识别单个的 CR 和 LF(如果需要仅识别单个的换行符,使用选项 `n 或选项 `r 进行切换,而使用选项 `a 则可以把更多的字符当成换行符,为了简便我通常将两个选项一起用即 m`a
  3. [[:blank:]] 是 POSIX 形式的字符组,包含空格和 Tab,换成 [ \t] 效果也是等同的(我习惯这么写,感觉直观些),这里不能使用 \s 代替;
  4. 第二个星号使用了懒惰匹配,如果这里贪婪匹配则无法去除行尾的空格和 Tab 了(思考:假如这里用贪婪匹配,且第三个星号换成加号,会有什么问题呢?)。
  5. 匹配模式中括号的作用是捕获匹配子模式的字符串,我们可以使用像 $1 的方法在后面的替换字符串中进行引用,这种方法叫后向引用。具体细节参见 RegExReplace()。(还记得我们前面介绍的括号的一个作用是什么?)
一种可选的方法是使用循环解析字符串,将多行文本分解成单行处理:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

AutoTrim, On
NewText := ""
Loop, Parse, OldText, `n, `r ; 这里考虑了换行符为 `r`n 和 `n 的情况
{
TempVar := A_LoopField
NewText .= TempVar "`n"
}

我们会发现,这样可能在实现目的的同时,可能也让其他地方发生了一些变化(可能关系不大,也可能产生问题)。
如果只需要去除行首或行尾的空格和 Tab,情况会简单一些:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

NewText := RegExReplace(OldText, "m)^[[:blank:]]*(.*)", "$1")  ; 去除行首所有的空格和制表符
NewText := RegExReplace(OldText, "m)^[ \t]*") ; 和前一语句效果等同
NewText := RegExReplace(OldText, "mU)(.*)[[:blank:]]*$", "$1") ; 去除行尾所有的空格和制表符,必须使用非贪婪匹配(这里可以将 U选项去除,但在第一个星号后加上问号或将第二个星号换成加号,效果等同)?
NewText := RegExReplace(OldText, "m)[ \t]*$") ; 和前一语句效果等同
; 还有其他形式,不需要去记住,但最好能理解

说到这里,都是比较简单的例子,对于更复杂的情况一般根据实际将上面的模式进行组合大部分都可以解决了。原本打算再加上一个在窗口命令中用正则匹配标题的例子,现在觉得不需要了,这里使用正则只在于构造正确匹配模式,并没有什么特殊的地方。

小结
上面的例子都很简单,是吧?没错,如果上面的例子你都试验过并理解了,那么恭喜你已经入门了。不过,现在我想和你说的是,正则表达式是比较复杂的工具,我刚才所介绍的只是其中基本的一些规则,这里介绍一些论坛中与正则相关的内容:
如果希望深入学习它,可以到网上搜索资料或参考我下面列出的教程(它们很可能不是针对 AutoHotkey 所写,所以根据情况需要替换上面的例子进行试验)。
最后,前面简单提过,不过这里我想再次建议:对正则最好的优化是在可以不使用正则时不去使用。正则很强大,但同时也规则复杂以至难以驾驭,因此很容易出错且难以维护就成为了我们在使用中经常遇到的问题。
AutoHotkey 学习指南(Beauty of AutoHotkey)
I do not make codes, and only a porter of AutoHotkey: from official to Chinese, from other languages to AutoHotkey, and show AutoHotkey to ordinary users sometimes.
User avatar
RobertL
Posts: 540
Joined: 18 Jan 2014, 01:14
Location: China

Re: 正则表达式快速入门

17 Sep 2014, 01:35

在群里看到的教程链接。
回头复习下..
我为人人,人人为己?
User avatar
amnesiac
Posts: 186
Joined: 22 Nov 2013, 03:08
Location: Egret Island, China
Contact:

Re: 正则表达式快速入门

17 Sep 2014, 07:07

多谢,已补充。
arcticir
Posts: 471
Joined: 17 Nov 2013, 11:32

Re: 正则表达式快速入门

18 Sep 2014, 04:39

微软正则表达式教程 链接失效了
User avatar
amnesiac
Posts: 186
Joined: 22 Nov 2013, 03:08
Location: Egret Island, China
Contact:

Re: 正则表达式快速入门

18 Sep 2014, 08:17

忘记了原来的内容,现在修正的链接可以供参考。

Return to “教程资料”

Who is online

Users browsing this forum: No registered users and 1 guest