在 AutoHotkey 中谈到编码时可能在三种情况中:
脚本文件编码
首先请阅读帮助中相关内容:脚本文件代码页。这里说明了解释器(AutoHotkey.exe)加载脚本时选择编码的优先级顺序:
- 若脚本文件开头为字节顺序标记(BOM),则据其选择相应的编码(UTF-8 BOM 或 UTF-16 BOM);
- 若解释器命令行中包含了 /CPn 选项,则使用 n 指定的编码;
- 其他情况下,则使用系统默认代码页(一般简单称为 ANSI 编码)。
- 脚本文件中包含多种非 ANSI 编码字符集的字符,如同时包含简体中文和俄文;
- 脚本可能在不同默认代码页的系统中执行,例如在简体中文系统和日文系统;
注:AutoHotkey Basic 默认脚本编码为 ANSI;对于 AutoHotkey_L,在 1.1.08.00 版本之前,ANSI 构建(build)默认脚本编码为 ANSI,但 Unicode 构建默认编码为 UTF-8,为了减少混乱,该版本之后脚本默认编码都使用 ANSI(所以 UTF-8 编码的脚本必须包含 BOM 头部才能被正确识别)。
SciTE4AutoHotkey 中在工具中设置默认代码页为 UTF-8 时创建的 UTF-8 脚本不含 BOM: 此时【File】【Encoding】中显示的编码并不会改变,因为上面的默认代码设置是通过脚本实现的,因此若要使用 UTF-8 BOM 编码,则每次创建脚本时都需要手动在该菜单下选择【UTF-8 with BOM】,幸运的是中文论坛已经有人解决了该问题(SciTE4AutoHotkey 新建编码为 UTF-8、SciTE4AutoHotkey 新建文件默认编码 UTF-8 with BOM)。对于其他编辑器(如 Notepad++),必须注意选择 UTF-8 编码时是否包含了 BOM(不同的编辑器有所区别),例如记事本,另存时编码中的 UTF-8 是包含了 BOM 的。
脚本编码错误一般具体表现为:脚本加载时出现错误或执行时字符串出现问题(如 MsgBox 显示乱码),因为无效的或不存在于原生代码页中的字符会被替换为占位符:ANSI’?‘或 Unicode‘�’。
基于上述的说明,推荐脚本文件编码统一使用 UTF-8 BOM。
文件编码
文件编码是指在读取和写入文件(文件 I/O)时使用的编码。FileEncoding 可以设置默认编码(A_FileEncoding 包含了脚本当前的默认编码设置),它会被用于 FileRead、FileReadLine、FileAppend、FileOpen 和文件读取循环,不过其中一些命令(函数)中可以使用编码参数覆盖该默认设置。
注意下面两点:
- 在读取文件时,若文件头部包含 BOM,则优先使用该标记指示的编码;
- 如果当前命令中未指定编码参数,之前也未设置默认编码,则使用系统默认编码。
IniRead 和 IniWrite 总是使用 UTF-16 或系统默认代码页,即除了 UTF-16 编码(通过 BOM 判断)外,其他所有情况都被视为系统默认编码。
原理: IniRead 和 IniWrite 依靠外部函数 GetPrivateProfileString 和 WritePrivateProfileString 来读取和写入值,这些函数仅支持 UTF-16 编码的 Unicode 文件,其他所有文件都被认为使用系统默认代码页。
对于 IniRead 支持的 UTF-16 编码,需注意下面几点:
- 实际仅支持 UTF-16 LE BOM 一种形式,其他可能出问题;
- 在 ANSI 构建中也支持 UTF-16 LE BOM 编码的 INI 文件;
- 当 IniWrite 的目标文件不存在时,ANSI 构建使用系统默认编码创建文件,而 Unicode 构建使用 UTF-16 LE BOM。 所以,如果希望使用指定编码的 INI 文件,则需先使用 FileAppend 并指定编码创建文件。
字符串编码
这部分属于进阶内容,相对于前面的内容有较高难度,多实践是理解的关键。
字符串编码是指内存中存储字符时使用的编码,这种编码被称为可执行程序的原生编码,相关帮助内容请参阅 Unicode 与 ANSI 两种构建的比较。简而言之,字符串编码与构建有关,Unicode 构建使用 UTF-16 LE 编码,而 ANSI 构建使用系统默认编码,AutoHotkey Basic 与 ANSI 构建相同。各分支比较的相关说明请参阅选择哪个分支?。
编码转换
通常我们无需关心字符串编码,例如赋值或显示时如果需要转换都会自动进行, 但通过一些高级方法操作字符串则必须考虑,例如在 DllCall、PostMessage/SendMessage、NumPut/NumGet、Capacity 和 StrPut/StrGet 中处理字符串时。
此时一般操作过程为:首先确定原生编码(通过 A_IsUnicode),然后计算目标字符串的大小。例如:
Code: Select all
; 获取汉字的 GBK 编码,适用于 AutoHotkey_L 中两种构建。
SetFormat, integer, H ; 让最后获取的编码为十六进制格式。
Char := "中"
; 因不同构建原生编码不同,所以需分别处理:
If A_IsUnicode
{
VarSetCapacity(GBKChar, 3) ; 一个汉字的 GBK 编码占用两个字节,加上字符串截止符。
StrPut(Char, &GBKChar, "CP936") ; 对于简体中文系统,编码参数中使用 CP0 亦可;这里的 GBKChar 为二进制变量,无法通过常规赋值。
}
else
{
GBKChar := Char
}
GBKCode := (NumGet(GBKChar, 0, "UChar") << 8) + NumGet(GBKChar, 1, "UChar")
MsgBox, % GBKCode ; 显示“中”的 GBK 编码为“D6D0”(这里显示为“0xD6D0”)
Code: Select all
GBKCode := NumGet(GBKChar, 0, "UInt") ; 这里获取到的编码将为“0xD0D6”。
Code: Select all
GBKCode := NumGet(GBKChar, 0, "UChar") . NumGet(GBKChar, 1, "UChar")
Code: Select all
GBKCode := Asc(GBKChar) ; 与上面的示例不同,此处的 GBKChar 仅指中文字符。
刚才的例子看了可能困惑多于收获,为了让大家掌握字符串编码转换的要领,接着再看另一个例子(这次是解疑):
Code: Select all
SetFormat, integer, H
NativeString := "中"
StrCap := StrPut(NativeString, "CP65001")
VarSetCapacity(UTF8String, StrCap)
StrPut(NativeString, &UTF8String, "CP65001")
Loop, % StrCap - 1 ; StrPut 返回的长度中包含末尾的字符串截止符,因此必须减 1。
{
UTF8Codes .= SubStr(NumGet(UTF8String, A_Index - 1, "UChar"), 3) ; 逐字节获取,去除开头的“0x”后连接起来。
}
MsgBox, % UTF8Codes ; 显示“E4B8AD”,前面附加“0x”就变成十六进制了。
其中,连接字符串的方式之前使用数值计算(左移),这里则采用字符串连接,个人感觉这种方式更好:
Code: Select all
UTF8Codes .= SubStr(NumGet(UTF8String, A_Index - 1, "UChar"), 3)
字符编码常用在与网络交互时,如提交内容到网页或获取网页返回的内容。为了方便,这里把刚才的操作写成函数:
Code: Select all
SetFormat, integer, H
String := "汉字"
MsgBox, % Encode(String, "CP65001")
return
Encode(Str, Encoding, Separator = "")
{
StrCap := StrPut(Str, Encoding)
VarSetCapacity(ObjStr, StrCap)
StrPut(Str, &ObjStr, Encoding)
Loop, % StrCap - 1
{
ObjCodes .= Separator . SubStr(NumGet(ObjStr, A_Index - 1, "UChar"), 3)
}
Return, ObjCodes
}
%E6%B1%89%E5%AD%97
而在百度中,则被编码为(这里为 GBK 编码,调用时编码参数为“CP936”):
%BA%BA%D7%D6
所以,此时分隔符中使用百分号就行了。最后,转换字符串编码时建议在 StrPut/StrGet 的编码参数中尽量不使用“CP0”,而指明具体的编码。因为“CP0”表示系统默认编码,与系统有关。不使用 Asc 也是因为它依赖于 AutoHotkey_L 构建,尽管在特定构建中它可能比较简单。
小结
多实践、多小结以加深理解,使用推荐的编码方式可以减少实际中可能遇到的编码问题。此外,需注意下面几点:
- 一个汉字占用两个字节的说法不严谨,应具体指明字符集(charset)和编码(encoding);
- 在 Basic 版本中,Asc(char) 总是将一个字节视为一个字符,即完全等同于:
- NumGet(char, 0, "UChar")