帮助中的相关部分:
RegEx: Quick Reference
RegExMatch()
RegExReplace()
RegEx: Callouts
RegEx: SetTitleMatchMode
简明规则介绍
也许您以前曾听说过正则表达式,或曾看过别人写的式子,感觉它像天书一样复杂。不过,只要您跟我一步步操作,您会发现其实没有想象中的那么难。很可能您使用过 Dos/Windows 下用于文件查找的通配符即 * 和 ?。如果想查找某个目录下的所有的 AutoHotkey 文档的话,您会搜索 *.ahk。在这里,* 会被解释成任意的字符串。而正则表达式也是用来进行文本匹配的工具,不过它比通配符更强大,可以进行更精确的匹配,当然,相应会复杂一些。学习正则表达式比较好的方法是从例子开始,理解例子后对例子进行修改、实验,所以我这里介绍几个简单的例子,并加以详细说明,现在就开始吧!
普通匹配
我想找出 Haystack 中 is 首次出现的位置:
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "is", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
需要了解的是 RegExMatch() 函数从字符串的左边开始寻找匹配的首个字符串然后返回这个字符的位置,源字符串中首个字符的位置为 1。
提一下,中文字符是普通字符,可以直接使用在匹配模式中进行匹配。
句点
我想找出 Haystack 中的首个字符:
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, ".", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
次数匹配
我想找到从“is”开始往后的整个字符串:
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "is.*", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
相关:
+ 表示前一单元可以出现一次或多次;
? 表示前一单元可以出现零次或一次;
{n,m} 表示前一单元可以出现 n 次到 m 次(n<m),这种形式有几种变形:{n}、{n,} 等。
示例 1 中虽然找出了首个 is 的位置,但它是在 This 单词中,而我需要找到单词 is 的位置。
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "\bis\b", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
相关:与 \b 这样匹配位置的还有,^ 匹配字符串的开始,$ 匹配字符串的结束。
思考:结合前面的内容,这里如果要匹配 g 开始的单词呢?
分支条件
如果我想匹配 Haystack 中 good 或 book 呢?
Code: Select all
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
这里简单提到一点:假如匹配时,同时符合多个分支,那么实际产生的匹配是最前面那个。a|ab 模式实际不会得到 ab 的匹配,如果有兴趣,可以执行下面的代码:
Code: Select all
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
}
小括号
我想匹配某个字符串中所有邮政编码,你也许马上想到了可以使用上面的分支条件,0|1|2|3|4|5|6|7|8|9{6},实验后发现不对了,这里 {6} 只修饰 9,而前面的数字只进行单独的匹配。
Code: Select all
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
}
说明:让包含的内容成为一个单元是小括号的一个作用,后面再介绍其他用法。
方括号
上面的例子中这么写也太繁琐了吧,如果要匹配所有字母,小写加大写有 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
; 请使用这两行替换上一个例子中相应行进行试验
Haystack := "654321-123456: This is a good book."
FoundPos := RegExMatch(Haystack, "[^0-9]{2}", Match, StartingPos)
另外还需要注意:在方括号中,如果需要匹配 ],必须进行转义,如 [0-9\]] 可以匹配任一数字或 ]。
思考,如果我需要匹配任意一个汉字呢?
常用字符组
忘了告诉你一个秘密,[0-9] 还有更简单的写法 \d。特殊字符组的简化形式主要有这些:
\w 相当于 [0-9a-zA-Z_],匹配单词中任一的字符;
\s 通常情况下相当于 [ \f\n\r\t\v],注意方括号中首个是空格,匹配任一空白字符;
\W 相当于 [^0-9a-zA-Z_];
\S 匹配非空白字符。
Code: Select all
; 请使用这两行替换上一个例子中相应行进行试验
Haystack := "654321-123456: This is a good book."
FoundPos := RegExMatch(Haystack, "\D{2}", Match, StartingPos)
匹配元字符
说了那么多,那我该如何匹配句点呢?
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "\.", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
注意:这里的转义符是正则中的转义符,为反斜线,不是指 AutoHotkey 中的转义符(重音符)。
匹配一些特殊字符
Code: Select all
Haystack := "This is a good book.`r`n"
FoundPos := RegExMatch(Haystack, "`n", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
另外,在这种情况下可以使用 AutoHotkey 的转义符(重音符)替换正则语法的转义符(反斜线),但在其他情况下则不能。
提到一下,空格是普通字符,不需要转义,也不需要特殊表示进行匹配。
贪婪与懒惰
Code: Select all
Haystack := "This is a good book."
FoundPos := RegExMatch(Haystack, "T.*is", Match)
MsgBox, % "FoundPos: " FoundPos "`n" "Match: " Match
这里 ? 修饰匹配次数单元,表示尽可能少重复,同理:
.+? 重复 1 次或更多次,但尽可能少重复
.? 重复 0 次或 1 次,但尽可能少重复
{n,m}? 重复 n 到 m 次,但尽可能少重复
{n,}? 重复 n 次以上,但尽可能少重复
选项
先看这个例子:
Code: Select all
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 外,还可以匹配小写的e。这里的 i 是正则表达式的选项,可以影响模式匹配时的一些行为,i 表示进行模式匹配时不区分大小写。选项与模式之间使用闭括号隔开。这里说明一下,前面的例子都是在不含选项即默认的情况下匹配的行为。更多选项请参见帮助。
常见应用示例
在普通的文本查找/替换等操作时,应该首先考虑 InStr()、IfInString、StringGetPos、StringReplace,这样更简单不易出错(且执行效率较好)。当使用普通匹配很繁琐或容易产生问题时,则应考虑使用正则表达式了。窗口的匹配也是如此。
何时应该转义,如何转义
匹配引号,这个似乎应该写到前面的基础中,还记得表达式中一个原义的引号如何表示的吗?
Code: Select all
OldText := "This is a ""good"" book."
FoundPos := RegExMatch(OldText, """")
还是再啰嗦一点:反斜线或 \Q...\E 这样的转义方法只适用于正则表达式中的特殊符号,而同时适用 AutoHotkey 和正则的转义方法的在帮助中只说明了三个符号(Tab、回车符和换行符)。 匹配模式中的字符是否需要转义需要看字符和字符所在位置(例如有些字符只在方括号中甚至在其中的特殊位置才有特殊含义),如何转义则看是这个字符是属于什么的特殊符号。这点可能是正则中比较容易产生问题的地方。
去除路径中末尾的反斜线
在 FileSelectFolder 命令中,当用户选择了根目录(如 C:\),输出变量会以反斜线结尾,可用下面的方法去掉它:
Code: Select all
Folder := RegExReplace(Folder, "\\$")
Code: Select all
If (SubStr(Folder, 0) = "\")
StringTrimRight, Folder, Folder, 1
有时我们在处理文本时,需要将多个空行合并成一个空行,这时可以参考下面的例子:
Code: Select all
NewText := RegExReplace(OldText, "(*BSR_ANYCRLF)\R+", "`n")
Code: Select all
Loop
{
StringReplace, OldText, OldText, `r`n`r`n, `n, UseErrorLevel
if ErrorLevel = 0 ; 不需要再进行替换
break
}
NewText := OldText
去除多行文本中行首行尾的空格和 Tab
去除单行文本首尾的空格和 Tab,只需在 AutoTrim 设置为 On 的情况下重新赋值,那么多行文本如何处理呢?
Code: Select all
NewText := RegExReplace(OldText, "m)(*ANYCRLF)^[[:blank:]]*(.*?)[[:blank:]]*$", "$1")
- m 选项可以把 OldText 当做多行文本而不是作为一个整体处理(主要影响行首行尾位置的匹配);
- (*ANYCRLF) 的作用是规定换行符的识别,影响位置匹配,默认情况下只识别 CRLF,加上这个后还可以识别单个的 CR 和 LF(如果需要仅识别单个的换行符,使用选项 `n 或选项 `r 进行切换,而使用选项 `a 则可以把更多的字符当成换行符,为了简便我通常将两个选项一起用即 m`a)
- [[:blank:]] 是 POSIX 形式的字符组,包含空格和 Tab,换成 [ \t] 效果也是等同的(我习惯这么写,感觉直观些),这里不能使用 \s 代替;
- 第二个星号使用了懒惰匹配,如果这里贪婪匹配则无法去除行尾的空格和 Tab 了(思考:假如这里用贪婪匹配,且第三个星号换成加号,会有什么问题呢?)。
- 匹配模式中括号的作用是捕获匹配子模式的字符串,我们可以使用像 $1 的方法在后面的替换字符串中进行引用,这种方法叫后向引用。具体细节参见 RegExReplace()。(还记得我们前面介绍的括号的一个作用是什么?)
Code: Select all
AutoTrim, On
NewText := ""
Loop, Parse, OldText, `n, `r ; 这里考虑了换行符为 `r`n 和 `n 的情况
{
TempVar := A_LoopField
NewText .= TempVar "`n"
}
如果只需要去除行首或行尾的空格和 Tab,情况会简单一些:
Code: Select all
NewText := RegExReplace(OldText, "m)^[[:blank:]]*(.*)", "$1") ; 去除行首所有的空格和制表符
NewText := RegExReplace(OldText, "m)^[ \t]*") ; 和前一语句效果等同
NewText := RegExReplace(OldText, "mU)(.*)[[:blank:]]*$", "$1") ; 去除行尾所有的空格和制表符,必须使用非贪婪匹配(这里可以将 U选项去除,但在第一个星号后加上问号或将第二个星号换成加号,效果等同)?
NewText := RegExReplace(OldText, "m)[ \t]*$") ; 和前一语句效果等同
; 还有其他形式,不需要去记住,但最好能理解
小结
上面的例子都很简单,是吧?没错,如果上面的例子你都试验过并理解了,那么恭喜你已经入门了。不过,现在我想和你说的是,正则表达式是比较复杂的工具,我刚才所介绍的只是其中基本的一些规则,这里介绍一些论坛中与正则相关的内容:
- An AHK Introduction to RegEx:在 AHK 中正则主要用于字符串处理和窗口匹配,本教程中将正则的用法融入实际应用,让初学者很容易将其用于自己的脚本。
- AHK 正则终结者:正则辅助工具,以后可能就这方面的工具(包括非针对 AHK 的第三方工具)写一篇介绍文章。
- 正则表达式 模式 手册补充
- 正则匹配模式的所有实例
- 正则反向查找
- A collection/library of regular expressions:正则在 AutoHotkey 中的应用实例集合,写于早期