編碼是每個腳本人惨撇、程序員最常見的困惑之一(中文用戶尤其常遇)靴姿,在 AutoHotkey 也不例外。這里一起說說在 AutoHotkey 中可能遇到的編碼問題贯要,以及可以避免這些問題的方法霹菊。
在 AutoHotkey 中談到編碼時可能在三種情況中:
腳本文件編碼
首先請閱讀幫助中相關(guān)內(nèi)容:腳本文件代碼頁。這里說明了解釋器(AutoHotkey.exe)加載腳本時選擇編碼的優(yōu)先級順序:
- 若腳本文件開頭為字節(jié)順序標(biāo)記(BOM)提前,則據(jù)其選擇相應(yīng)的編碼(UTF-8 BOM 或 UTF-16 BOM)吗货;
- 若解釋器命令行中包含了 /CPn 選項,則使用 n 指定的編碼狈网;
- 其他情況下宙搬,則使用系統(tǒng)默認(rèn)代碼頁(一般簡單稱為 ANSI 編碼)。
一般情況下建議腳本文件使用 ANSI 或 UTF-8 BOM(注意必須加上 BOM)編碼拓哺,這樣一般情況下腳本文件都能正常加載勇垛,但下列兩種情況中必須使用 UTF-8 BOM 編碼,否則腳本加載或執(zhí)行時會出問題:
- 腳本文件中包含多種非 ANSI 編碼字符集的字符士鸥,如同時包含簡體中文和俄文闲孤;
- 腳本可能在不同默認(rèn)代碼頁的系統(tǒng)中執(zhí)行,例如在簡體中文系統(tǒng)和日文系統(tǒng)烤礁;
對于簡體中文 Windows 系統(tǒng)讼积,默認(rèn)代碼頁為 CP936(也可以認(rèn)為是 GB2312、GBK 或 GB18030)鸽凶,這是種雙字節(jié)字符集(縮寫 DBCS币砂,是多字節(jié)字符集即 MBCS 的一種,與單字節(jié)字符集 SBCS 相對玻侥;典型的多字節(jié)字符集如漢字决摧,而單字節(jié)字符集常見于歐洲語言)。對于上面的第二種情況凑兰,如果腳本只包含 ANSI 字符掌桩,那么應(yīng)該也能正常執(zhí)行,不過中文用戶腳本中不包含漢字的可能性有多大呢姑食?
注:AutoHotkey Basic 默認(rèn)腳本編碼為 ANSI波岛;對于 AutoHotkey_L,在 1.1.08.00 版本之前音半,ANSI 構(gòu)建(build)默認(rèn)腳本編碼為 ANSI则拷,但 Unicode 構(gòu)建默認(rèn)編碼為 UTF-8,為了減少混亂曹鸠,該版本之后腳本默認(rèn)編碼都使用 ANSI(所以 UTF-8 編碼的腳本必須包含 BOM 頭部才能被正確識別)煌茬。
SciTE4AutoHotkey 中在工具中設(shè)置默認(rèn)代碼頁為 UTF-8 時創(chuàng)建的 UTF-8 腳本不含 BOM:

此時【File】【Encoding】中顯示的編碼并不會改變,因為上面的默認(rèn)代碼設(shè)置是通過腳本實現(xiàn)的彻桃,因此若要使用 UTF-8 BOM 編碼坛善,則每次創(chuàng)建腳本時都需要手動在該菜單下選擇【UTF-8 with BOM】,幸運(yùn)的是中文論壇已經(jīng)有人解決了該問題(SciTE4AutoHotkey 新建編碼為 UTF-8、SciTE4AutoHotkey 新建文件默認(rèn)編碼 UTF-8 with BOM:補(bǔ)充后一方法眠屎,待驗證)剔交。對于其他編輯器(如 Notepad++),必須注意選擇 UTF-8 編碼時是否包含了 BOM(不同的編輯器有所區(qū)別)改衩,例如記事本岖常,另存時編碼中的 UTF-8 是包含了 BOM 的。
腳本編碼錯誤一般具體表現(xiàn)為:腳本加載時出現(xiàn)錯誤或執(zhí)行時字符串出現(xiàn)問題(如 MsgBox 顯示亂碼)燎字,因為無效的或不存在于原生代碼頁中的字符會被替換為占位符:ANSI’?‘或 Unicode‘?’腥椒。
基于上述的說明,推薦腳本文件編碼統(tǒng)一使用 UTF-8 BOM候衍。
文件編碼
文件編碼是指在讀取和寫入文件(文件 I/O)時使用的編碼笼蛛。FileEncoding 可以設(shè)置默認(rèn)編碼(A_FileEncoding 包含了腳本當(dāng)前的默認(rèn)編碼設(shè)置),它會被用于 FileRead蛉鹿、FileReadLine滨砍、FileAppend、FileOpen 和文件讀取循環(huán)妖异,不過其中一些命令(函數(shù))中可以使用編碼參數(shù)覆蓋該默認(rèn)設(shè)置惋戏。
注意下面兩點(diǎn):
- 在讀取文件時,若文件頭部包含 BOM他膳,則優(yōu)先使用該標(biāo)記指示的編碼响逢;
- 如果當(dāng)前命令中未指定編碼參數(shù),之前也未設(shè)置默認(rèn)編碼棕孙,則使用系統(tǒng)默認(rèn)編碼舔亭。
INI 文件編碼
IniRead 和 IniWrite 總是使用 UTF-16 或系統(tǒng)默認(rèn)代碼頁,即除了 UTF-16 編碼(通過 BOM 判斷)外蟀俊,其他所有情況都被視為系統(tǒng)默認(rèn)編碼钦铺。
原理: IniRead 和 IniWrite 依靠外部函數(shù) GetPrivateProfileString 和 WritePrivateProfileString 來讀取和寫入值,這些函數(shù)僅支持 UTF-16 編碼的 Unicode 文件肢预,其他所有文件都被認(rèn)為使用系統(tǒng)默認(rèn)代碼頁矛洞。
對于 IniRead 支持的 UTF-16 編碼,需注意下面幾點(diǎn):
- 實際僅支持 UTF-16 LE BOM 一種形式烫映,其他可能出問題沼本;
- 在 ANSI 構(gòu)建中也支持 UTF-16 LE BOM 編碼的 INI 文件;
- 當(dāng) IniWrite 的目標(biāo)文件不存在時锭沟,ANSI 構(gòu)建使用系統(tǒng)默認(rèn)編碼創(chuàng)建文件擅威,而 Unicode 構(gòu)建使用 UTF-16 LE BOM。 所以冈钦,如果希望使用指定編碼的 INI 文件,則需先使用 FileAppend 并指定編碼創(chuàng)建文件李请。
下圖是 Notepad2 中編碼設(shè)置界面瞧筛,INI 文件編碼必須選擇 ANSI(936) 或 Unicode (UTF-16 LE BOM) 腳本才能正常讀取和寫入:

建議腳本的 INI 文件統(tǒng)一使用 UTF-16 LE BOM 編碼厉熟,以避免腳本在不同系統(tǒng)中運(yùn)行可能遇到的潛在問題。小頭(Little endian)较幌、大頭(Big endian)的更多相關(guān)知識請參閱:字符編解碼的故事(ASCII揍瑟,ANSI,Unicode乍炉,Utf-8區(qū)別)绢片、字符編碼筆記:ASCII,Unicode和UTF-8
字符串編碼
這部分屬于進(jìn)階內(nèi)容岛琼,相對于前面的內(nèi)容有較高難度底循,多實踐是理解的關(guān)鍵。
字符串編碼是指內(nèi)存中存儲字符時使用的編碼槐瑞,這種編碼被稱為可執(zhí)行程序的原生編碼熙涤,相關(guān)幫助內(nèi)容請參閱 Unicode 與 ANSI 兩種構(gòu)建的比較。簡而言之困檩,字符串編碼與構(gòu)建有關(guān)祠挫,Unicode 構(gòu)建使用 UTF-16 LE 編碼,而 ANSI 構(gòu)建使用系統(tǒng)默認(rèn)編碼悼沿,AutoHotkey Basic 與 ANSI 構(gòu)建相同等舔。各分支比較的相關(guān)說明請參閱[選擇哪個分支?]({{ site.url }}/2014/08/02/choose-versions.html)糟趾。
編碼轉(zhuǎn)換
通常我們無需關(guān)心字符串編碼慌植,例如賦值或顯示時如果需要轉(zhuǎn)換都會自動進(jìn)行, 但通過一些高級方法操作字符串則必須考慮拉讯,例如在 DllCall涤浇、PostMessage/SendMessage、NumPut/NumGet魔慷、Capacity 和 StrPut/StrGet 中處理字符串時只锭。
此時一般操作過程為:首先確定原生編碼(通過 A_IsUnicode),然后計算目標(biāo)字符串的大小院尔。例如:
; 獲取漢字的 GBK 編碼蜻展,適用于 AutoHotkey_L 中兩種構(gòu)建。
SetFormat, integer, H ; 讓最后獲取的編碼為十六進(jìn)制格式邀摆。
Char := "中"
; 因不同構(gòu)建原生編碼不同纵顾,所以需分別處理:
If A_IsUnicode
{
VarSetCapacity(GBKChar, 3) ; 一個漢字的 GBK 編碼占用兩個字節(jié),加上字符串截止符栋盹。
StrPut(Char, &GBKChar, "CP936") ; 對于簡體中文系統(tǒng)施逾,編碼參數(shù)中使用 CP0 亦可;這里的 GBKChar 為二進(jìn)制變量,無法通過常規(guī)賦值汉额。
}
else
{
GBKChar := Char
}
GBKCode := (NumGet(GBKChar, 0, "UChar") << 8) + NumGet(GBKChar, 1, "UChar")
MsgBox, % GBKCode ; 顯示“中”的 GBK 編碼為“D6D0”(這里顯示為“0xD6D0”)
上面獲取編碼時為什么需要那么復(fù)雜曹仗?下面這樣不行嗎?
GBKCode := NumGet(GBKChar, 0, "UInt") ; 這里獲取到的編碼將為“0xD0D6”蠕搜。
可以發(fā)現(xiàn) GBK 編碼在內(nèi)存中存放時使用大頭方式(即第一個字節(jié)在前怎茫,先高位、后低位)妓灌,在低字節(jié)(LChar)與高字節(jié)(WChar)(還有低字與高字)的編碼問題時經(jīng)常需要需要注意這種情況轨蛤。下面這樣是可以的(將顯示“0xD60xD0”):
GBKCode := NumGet(GBKChar, 0, "UChar") . NumGet(GBKChar, 1, "UChar")
那么使用下面這樣呢?
GBKCode := Asc(GBKChar) ; 與上面的示例不同虫埂,此處的 GBKChar 僅指中文字符祥山。
Asc() 可以獲取首個字符的編碼,但在 ANSI 構(gòu)建中的原生編碼為系統(tǒng)默認(rèn)編碼告丢,所以一個字符占用一個字節(jié)(雙字節(jié)字符集編碼中兩個編碼的字符實際上被視為兩個獨(dú)立單元)枪蘑,所以顯示“0xD6”;而 Unicode 構(gòu)建中原生編碼為 UTF-16 LE(由于在內(nèi)存中岖免,所以無需字節(jié)順序標(biāo)記)岳颇,所以這里使用該方式獲取字符編碼即小頭方式,所以顯示“0xD0D6”颅湘。
剛才的例子看了可能困惑多于收獲话侧,為了讓大家掌握字符串編碼轉(zhuǎn)換的要領(lǐng),接著再看另一個例子(這次是解疑):
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) ; 逐字節(jié)獲取,去除開頭的“0x”后連接起來鹿寨。
}
MsgBox, % UTF8Codes ; 顯示“E4B8AD”新博,前面附加“0x”就變成十六進(jìn)制了。
這里盡管還是轉(zhuǎn)換編碼脚草,但許多地方采用適應(yīng)性更強(qiáng)的方式(如設(shè)置變量容量赫悄、獲取編碼和連接字符串的方式),同時還能明白這些具體是怎么來的馏慨。不論轉(zhuǎn)換到什么編碼埂淮,方式還是這一套。所以以后需要轉(zhuǎn)換編碼時写隶,無需去記住哪種編碼是大頭還是小頭存儲倔撞,只需先用個字符測試一次:按順序逐字節(jié)獲取并連接起來,和實際比較慕趴,不符合時調(diào)整順序就行了痪蝇。
其中鄙陡,連接字符串的方式之前使用數(shù)值計算(左移),這里則采用字符串連接霹俺,個人感覺這種方式更好:
UTF8Codes .= SubStr(NumGet(UTF8String, A_Index - 1, "UChar"), 3)
AutoHotkey 中轉(zhuǎn)換編碼可以通過 Transform 的 FromCodePage/ToCodePage 兩個子命令或 Windows API 進(jìn)行柔吼,不過目前建議使用 StrPut/StrGet 代替。
字符編碼常用在與網(wǎng)絡(luò)交互時丙唧,如提交內(nèi)容到網(wǎng)頁或獲取網(wǎng)頁返回的內(nèi)容。為了方便觅玻,這里把剛才的操作寫成函數(shù):
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
}
這里加了個 Separator 參數(shù)想际,在谷歌中搜索“漢字”時,從網(wǎng)址里可以看到被編碼為:
%E6%B1%89%E5%AD%97
而在百度中溪厘,則被編碼為(這里為 GBK 編碼胡本,調(diào)用時編碼參數(shù)為“CP936”):
%BA%BA%D7%D6
所以,此時分隔符中使用百分號就行了畸悬。
最后侧甫,轉(zhuǎn)換字符串編碼時建議在 StrPut/StrGet 的編碼參數(shù)中盡量不使用“CP0”,而指明具體的編碼蹋宦。因為“CP0”表示系統(tǒng)默認(rèn)編碼披粟,與系統(tǒng)有關(guān)。不使用 Asc 也是因為它依賴于 AutoHotkey_L 構(gòu)建冷冗,盡管在特定構(gòu)建中它可能比較簡單守屉。
小結(jié)
多實踐、多小結(jié)以加深理解蒿辙,使用推薦的編碼方式可以減少實際中可能遇到的編碼問題拇泛。此外,需注意下面幾點(diǎn):
- “一個漢字占用兩個字節(jié)”的說法不嚴(yán)謹(jǐn)思灌,應(yīng)具體指明字符集(charset)和編碼(encoding)俺叭;
- 在 Basic 版本中,Asc(char) 總是將一個字節(jié)視為一個字符泰偿,即完全等同于:
- NumGet(char, 0, "UChar")
但 AutoHotkey_L 中則能正確處理字符熄守,Asc(char) 總是能獲取一個字符的編碼(包括單字節(jié)字符集和雙字節(jié)字符集中的字符),因為它是依據(jù)原生編碼的方式獲取的(不過二進(jìn)制變量通常必須手工處理才能獲取正確編碼)甜奄。
- NumGet(char, 0, "UChar")