字符串处理在多数编程语言中都是重头戏,AHK 自然也不例外。AHK 在字符串处理方面还是很灵活的,如果掌握好了,写起代码来会事半功倍。本文先介绍最常用的操作:拼接和遍历。
一、字符串拼接
拼接恐怕是最常用的操作了,在 AHK 中也非常简单,直接将两个字符串用空格隔开就可以了。
a := "abc"
b := 100
c := "def"
d := 123.456
e := 200
; f 的值是字符串 abc100anddef123.456
f := a b "and" c d
; g 的值是字符串 100200
g := b e
不管是字符串、整数还是浮点数,都可以用空格拼接成一个字符串。更神奇的是,空格可以和其他算术运算符一起使用。
a := 100
b := 200
; c 的值是 100150
c := a b - 50
其实 AHK 中整数 100200 和 字符串 “100200” 是没有区别的,当整数用的时候就是 100200,当字符串用的时候就是
“100200”,当然内部实现上会有一些优化。如果感觉无法接受的话,也可以认为整数和字符串是有区别的,在实际使用中会自动相互转换。
实际上,AHK 中 .(点运算符)是专门用来拼接字符串的,在某些情况还是不能省略的。
a := 300
b := 200
; c 的值是 100
c := a - b
; d 的值是 300-200
d := a . - b
正因为空格可以和其他算术运算符一起使用,使用起来可以很灵活,为了避免歧义,空格只有在必要的情况(即这个表达式没有其他的解释办法时)才起拼接作用。
二、子字符串遍历
还有一个非常常见的操作,是遍历一个字符串的子字符串。Loop, Parse 是专门用来做这个的。
Loop, Parse, InputVar [, Delimiters, OmitChars]
其中 InputVar 是变量名,不需要加 %。Delimiters 是分隔符,OmitChars 是要忽略的字符。
最简单的场景就是遍历字符串中的每一个字符。
text := "abcd"
Loop, Parse, text
{
; 依次弹出
; a
; b
; c
; d
MsgBox, %A_LoopField%
}
Delimiters 和 OmitChars 都省略的情况,就是遍历字符串中的每一个字符。
另外一个典型场景是依次处理一段文字的每一行。
text =
(
line1
line2
line3
)
Loop, Parse, text, `n, `r
{
; 依次弹出
; line1
; line2
; line3
MsgBox, %A_LoopField%
}
这里 Delimiters 是 `n。`r 又是什么呢?在类 UNIX 操作系统中,文本中的换行符就是 `n,但在 Windows
中,换行符是 `r`n。如果处理一段换行符是 `r`n 的文本,直接用 `n 分隔,会保留多余的 `r,所以设置 OmitChars 为 `r
可以将其去掉。可能有不仔细的朋友认为 Loop, Parse, text, `n, `r 是将字符串同时按 `n 和 `r 来分隔,这是错误的,`n 和 `r 的顺序也不能颠倒。
除了按换行符,我们也经常需要按其他符号分隔字符串,比如按空格,按逗号等,就大同小异了。但需要注意空格和逗号有点特殊,因为它们是特殊符号,写法有一些特别。
text := "a b,c d:xxx"
Loop, Parse, text, %A_Space%
{
; 依次弹出
; a
; b,c
; d:xxx
MsgBox, %A_LoopField%
}
Loop, Parse, text, `,
{
; 依次弹出
; a b
; c d:xxx
MsgBox, %A_LoopField%
}
Loop, Parse, text, :
{
; 依次弹出
; a b,c d
; xxx
MsgBox, %A_LoopField%
}
这里空格用的是 %A_Space%,A_Space 是一个 AHK 的内部变量,这样需要用 % 取该变量的值。`,
是用转义的方法取到的逗号字符,因为直接写逗号会被认为是参数的分隔符。类似的还有 `t 或者 %A_Tab% 是 Tab
等等,如果一个字符直接写会报语法错误,那么通常在前边加个 `
就可以了。以后我也会专门写文章介绍转义字符。至于冒号,就没有什么特别,直接写就可以了。
有些情况我们需要按字符串分隔,而不是按字符分隔,比如:
text := "abc()bcd(cde"
需要按 () 分隔 text。需要注意,这种情况这样的写法就是错误的了:
text := "abc()bcd(cde"
Loop, Parse, text, (, )
{
; 依次弹出
; abc
; bcd
; cde
MsgBox, %A_LoopField%
}
一个可行的办法,是先将字符串中的 () 替换成一个字符串里没有的特殊字符,比如 @,然后再用这个特殊字符作为分隔符:
text := "abc()bcd(cde"
; StrReplace 函数是用来替换字符串的,我们以后再详细了解
text := StrReplace(text, "()", "@")
Loop, Parse, text, @
{
; 依次弹出
; abc
; bcd(cde
MsgBox, %A_LoopField%
}
三、字符串解析
; 示例 #1:
Colors = red,green,blue
Loop, parse, Colors, `,
{
MsgBox, Color number %A_Index% is %A_LoopField%.
}
; 示例 #2: 按行读取变量的内容, 一行接一行 (类似于 文件读取 循环).
; 通过 FileRead 可以把文件加载到变量中:
Loop, parse, FileContents, `n, `r ; 在 `r 之前指定 `n, 这样可以同时支持对 Windows 和 Unix 文件的解析.
{
MsgBox, 4, , Line number %A_Index% is %A_LoopField%.`n`nContinue?
IfMsgBox, No, break
}
; 示例 #3: 除了用于剪贴板外, 这个例子和上一个一样.
; 每当剪贴板包含文件时这个例子很有用, 例如从打开的资源管理器窗口
; 复制的那些 (程序自动把这些文件转换为它们的文件名):
Loop, parse, clipboard, `n, `r
{
MsgBox, 4, , File number %A_Index% is %A_LoopField%.`n`nContinue?
IfMsgBox, No, break
}
; 示例 #4: 解析逗号分隔值 (CSV) 文件:
Loop, read, C:\Database Export.csv
{
LineNumber = %A_Index%
Loop, parse, A_LoopReadLine, CSV
{
MsgBox, 4, , Field %LineNumber%-%A_Index% is:`n%A_LoopField%`n`nContinue?
IfMsgBox, No
return
}
}
; 示例 #5: 判断遇到了哪个分隔符.
; 初始化要搜索的字符串.
Colors = red,green|blue;yellow|cyan,magenta
; 初始化计数器来跟踪字符串中我们的位置.
Position := 0
Loop, Parse, Colors, `,|;
{
; 计算在这个字段末尾分隔符的位置.
Position += StrLen(A_LoopField) + 1
; 获取解析循环中找到的分隔符.
Delimiter := SubStr(Colors, Position, 1)
MsgBox Field: %A_LoopField%`nDelimiter: %Delimiter%
}
四、计算长度
StrLen 函数用于计算字符串的长度,这个我们在之前的判断空字符串的文章提及过,所以并不陌生。
OutputVar := StrLen(InputVar)
需要注意的是,StrLen 的结果并非字符串所占用的字节数。如果字符串里包含中文、全角字符以及日文、韩文等非 ASCII
字符,长度也是只算 1 的,虽然实际储存时并非只消耗 1 个字节。同时,StrLen
的结果也不能作为字符串的显示长度,因为通常中文、全角字符等要比英文、半角字符的显示宽度要大(如果是等宽字体,前者的宽度是后者的两倍)。
StrLen 函数对应的命令是 StringLen。
五、查找
查找操作是指判断一个字符串是否包含另一个字符串,如果包含,还需要找到具体的位置,这便是 InStr 函数的工作。
FoundPos := InStr(Haystack, Needle [, CaseSensitive = false, StartingPos = 1, Occurrence = 1])
InStr 函数的参数比较多。Haystack 是被查找的字符串,Needle 是待查找的字符串(即从 Haystack 里查找
Needle),CaseSensitive 用于设置是否区分大小写,StartingPos 是开始查找的位置(从头开始为 1,如果是 0
或者负数,将逆序查找),Occurrence 指查找几次(比如 Occurrence 是 2,那么即使 Haystack 里有一个
Needle,也会因为没有第 2 个 Needle 而返回 0)。
InStr 函数的返回值是 Needle 在 Haystack 的位置,从 1 开始。如果返回 0,说明没找到。
和 InStr 函数有关的命令有 IfInString、IfNotInString、StringGetPos,如果遇到性能问题,可以使用。
另外 if 也可以用来判断一个字符串中是否包含另一个字符串。可以在帮助文档搜索 if var [not] in/contains 找到,里边有详细讲解。我也会在以后专门介绍 if 判断的文章里展开。
六、截取
截取操作是指取一个字符串的子字符串,这正是 SubStr 函数的工作。
NewStr := SubStr(String, StartPos [, Length])
SubStr 函数很好理解,String 即原始字符串。StartPos 为截取的起点,从 1 开始,如果为 0,表示截取最后一个字符;如果为 -1,表示截取最后两个字符,以此类推。Length 为截取的长度,如果省略指截到原始字符串的末尾。
和 SubStr 函数有关的命令有 StringLeft、StringRight、StringMid、StringTrimLeft、StringTrimRight,这些命令用起来都不大方便,如果遇到性能问题,可以使用。
七、分隔
分隔字符串,和我们上一篇文章了解到的遍历字符串很像,但 StrSplit 使用起来更灵活。
Array := StrSplit(String [, Delimiters, OmitChars])
String 是原始字符串,Delimiters 是分割符(和 Loop, Parse 不同,这里支持字符串,而且可以是一个字符串数组,非常强大),OmitChars 是移除和分隔符相邻的特定字符。
返回的结果 Array 是一个数组,可以用 Array[1] 访问第一个元素,用 Array.Length() 获取数组中的元素个数等。以后我们会专门了解数组的用法。
StrSplit 函数对应的命令是 StringSplit,但二者在细节上有很多不同,如果因为性能等原因一定要用 StringSplit,要仔细测试。
八、替换
有时我们需要将字符串的特定内容替换成其他内容,StrReplace 函数就派上用场了。
OutputVar := StrReplace(Haystack, SearchText [, ReplaceText, OutputVarCount, Limit := -1])
StrReplace 函数参数比较多,但也很好理解。Haystack 是原始字符串,SearchText
是被替换的内容,ReplaceText 是替换成的内容(如果省略,代表直接删除),OutputVarCount
用来存放替换的次数(如果省略代表不保存),Limit 指最多替换几次(-1 指全部替换,如果为 1,代表只替换找到的第一个)。
StrReplace 函数对应的命令是 StringReplace。
九、判断类型
判断类型是指判断一个字符串是否是整数、浮点数、字母、大写字母、小写字母、空白、时间等。
帮助文档里讲得很详细,这里就不展开了,搜 if var is [not] type 即可。
这里举个和下一节有关的判断大小写字母的例子:
a := "abc"
b := "ABC"
c := "abc1"
if a is lower
{
; 条件成立,只有字符串里全部是小写字母(a-z)才成立
}
if b is upper
{
; 条件成立,同理
}
if c is lower
{
; 条件不成立,因为包含了一个数字
}
十、大小写转换
有时我们需要转换字符串中字母的大小写,StringLower 命令用于将大写字母转换成小写,StringUpper 命令用于将小写字母转换成大写。
StringLower, OutputVar, InputVar [, T]
StringUpper, OutputVar, InputVar [, T]
参数中的 OutputVar 和 InputVar 都是变量名,即不用加 %。T 参数表示将字符串转换为标题格式,即每个单词的首字母大写,其余部分小写。
十一、移除首尾指定字符
有时我们需要移除一个字符串首尾的某些字符,典型情况就是移除首尾的空格。这就需要使用 Trim 系列函数。
Result := Trim(String, OmitChars = " `t")
Result := LTrim(String, OmitChars = " `t")
Result := RTrim(String, OmitChars = " `t")
这三个函数用法一致,Trim 用于移除字符串首尾(两侧)的指定字符,LTrim 用于移除字符串首部(左侧)的指定字符,RTrim 用于移除字符串尾部(右侧)的指定字符。
另外还有一个和此相关的命令。
AutoTrim, On|Off
AutoTrim 的含义是在用 = 赋值时是否自动移除首尾空白(空格和 Tab),默认是移除。
十二、格式化
格式化操作主要是将整数、浮点数等格式化成特定格式的字符串,用来展示。这主要是 Format 函数的工作,SetFormat 命令也与此有关。Format 函数的参数很复杂,但帮助文档里有详细讲解,暂时就不展开了,以后可能单独讲解。
十三、排序
对字符串排序可以使用 Sort 命令,因为排序比较复杂,我以后再单独讲。
十四、字符编码操作
通常情况,我们不需要了解字符串对应的二进制数据是怎样的。但有些时候我们需要知道字符对应编码(比如 ASCII 编码,或者 UTF-8
编码),在字符与编码间相互转换等,Asc、Chr、Ord 函数用于此类操作。这其中涉及到一些复杂问题,先不展开,我以后会单独讲。
十五、内存操作
通常情况,我们不需要关注字符串在内存是怎么储存的,但某些场景我们需要这么做,比如转编码(此处编码指代码页,比如将字符串从 UTF-8 转成 CP936)。这就需要用到 StrPut 和 StrGet 函数。这属于高级内容,也比较复杂,我以后会单独讲。
十六、正则表达式
正则表达式也是用来查找和替换字符串用的,但自成体系,功能强大,也特别复杂。RegExMatch 函数、RegExReplace 函数和~= 运算符和正则表达式有关。因为 AHK中的正则表达式和其他语言中的大同小异,而且关于正则表达式的内容特别多,暂时就不展开了,以后也会单独讲到。
十七、常规的字符串判空
a := ""
; 方法一:
; = 也可替换成 ==,因为空字符串无大小写问题,不赘述
if (a = "")
{
; 条件成立
}
; 方法二:
if (a)
{
; 做正常的事情
}
else
{
; 条件成立
}
方法一就是其他编程语言中的方法,无需过多解释。方法二是一种简化的写法,在多数情况看起来也是正常的,但存在一些特殊情况。
十八、特殊情况一:数字 0
在一些特殊情况,方法二存在问题。
如果变量为字符串 “0”,或者整数 0,或者浮点数 0.0:
a := "0"
; 或者
; a := 0
; 或者
; a := 0.0
; 方法一:
; = 也可替换成 ==,因为空字符串无大小写问题,不赘述
if (a = "")
{
; 条件不成立
}
; 方法二:
if (a)
{
; 做正常的事情
}
else
{
; 条件成立,判断错误
}
恐怕没有人认为一个长度为一的字符串 “0” 是空字符串。但第二种方法给出了错误的结果。原因是 0 被认为是布尔值 false,而且 AHK
中的字符串和数字并没有那么确切的区别,很多情况可以自动相互转换,直接用 if 判断自然不通过,看来这个简化还是要不得。
十九、特殊情况二:Object()
还有一种特殊情况是空对象 Object()。通常来说,我们不应该认为 Object()
是空变量,但某些库的作者可能不严谨,在本应返回空变量的地方返回了
Object()(我印象中遇到过这样的问题,但今天没有复现出来,如果以后发现了再举例)。如果用方法一判断,就可能出问题。
a := Object()
; 方法一
; = 也可替换成 ==,因为空字符串无大小写问题,不赘述
if (a = "")
{
; 条件不成立
}
; 方法三
if (StrLen(a) = 0)
{
; 条件成立
}
方法一认为 Object() 不是空变量,那么继续将 a 传递下去处理,就可能出问题。方法三是调用 StrLen() 函数来检查字符串的长度,它成功发现了这不是一个有效的字符串。
但用 StrLen() 函数判断也存在问题,一个很严重的问题是方法三的耗时要比方法一长一倍以上。因为调用函数需要一些额外开销,在几乎所有语言都是如此。如果这个判断在一个很大的循环里边,额外的开销是很大的。
另外 Object() 是否算空变量或者空字符串,这其实是有争议的。我个人是认为不应该算,但在某些语言中,所有变量类型都是从 Object 类型继承来的,那么就应该算。在用一些库时,还是需要验证一下是否存在 Object() 的问题,以免以后出了问题没有头绪。
学习了
学习了
SubStr(string, 2, -1)去头去尾~