【指南】常見的編碼問題

作者:amnesiac 首發(fā):官方論壇中文版

編碼是每個腳本人惨撇、程序員最常見的困惑之一(中文用戶尤其常遇)靴姿,在 AutoHotkey 也不例外。這里一起說說在 AutoHotkey 中可能遇到的編碼問題贯要,以及可以避免這些問題的方法霹菊。

在 AutoHotkey 中談到編碼時可能在三種情況中:

腳本文件編碼

首先請閱讀幫助中相關(guān)內(nèi)容:腳本文件代碼頁。這里說明了解釋器(AutoHotkey.exe)加載腳本時選擇編碼的優(yōu)先級順序:

  1. 若腳本文件開頭為字節(jié)順序標(biāo)記(BOM)提前,則據(jù)其選擇相應(yīng)的編碼(UTF-8 BOM 或 UTF-16 BOM)吗货;
  2. 若解釋器命令行中包含了 /CPn 選項,則使用 n 指定的編碼狈网;
  3. 其他情況下宙搬,則使用系統(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:
![SciTE 設(shè)置對話框]({{ site.url }}/assets/images/20140811000.png)

此時【File】【Encoding】中顯示的編碼并不會改變,因為上面的默認(rèn)代碼設(shè)置是通過腳本實現(xiàn)的彻桃,因此若要使用 UTF-8 BOM 編碼坛善,則每次創(chuàng)建腳本時都需要手動在該菜單下選擇【UTF-8 with BOM】,幸運(yùn)的是中文論壇已經(jīng)有人解決了該問題(SciTE4AutoHotkey 新建編碼為 UTF-8SciTE4AutoHotkey 新建文件默認(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):

  1. 在讀取文件時,若文件頭部包含 BOM他膳,則優(yōu)先使用該標(biāo)記指示的編碼响逢;
  2. 如果當(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)編碼钦铺。

原理: IniReadIniWrite 依靠外部函數(shù) GetPrivateProfileString 和 WritePrivateProfileString 來讀取和寫入值,這些函數(shù)僅支持 UTF-16 編碼的 Unicode 文件肢预,其他所有文件都被認(rèn)為使用系統(tǒng)默認(rèn)代碼頁矛洞。

對于 IniRead 支持的 UTF-16 編碼,需注意下面幾點(diǎn):

  1. 實際僅支持 UTF-16 LE BOM 一種形式烫映,其他可能出問題沼本;
  2. 在 ANSI 構(gòu)建中也支持 UTF-16 LE BOM 編碼的 INI 文件;
  3. 當(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) 腳本才能正常讀取和寫入:
![Notepad2 編碼設(shè)置對話框]({{ site.url }}/assets/images/20140811001.png)

建議腳本的 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)制變量通常必須手工處理才能獲取正確編碼)甜奄。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柠横,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子课兄,更是在濱河造成了極大的恐慌牍氛,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烟阐,死亡現(xiàn)場離奇詭異搬俊,居然都是意外死亡紊扬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門唉擂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來餐屎,“玉大人,你說我怎么就攤上這事玩祟「顾酰” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵空扎,是天一觀的道長藏鹊。 經(jīng)常有香客問我,道長转锈,這世上最難降的妖魔是什么盘寡? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮撮慨,結(jié)果婚禮上竿痰,老公的妹妹穿的比我還像新娘。我一直安慰自己砌溺,他們只是感情好影涉,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抚吠,像睡著了一般常潮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上楷力,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天喊式,我揣著相機(jī)與錄音,去河邊找鬼萧朝。 笑死岔留,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的检柬。 我是一名探鬼主播献联,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼何址!你這毒婦竟也來了里逆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤用爪,失蹤者是張志新(化名)和其女友劉穎原押,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偎血,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诸衔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年盯漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笨农。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡就缆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谒亦,到底是詐尸還是另有隱情竭宰,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布诊霹,位于F島的核電站羞延,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏脾还。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一入愧、第九天 我趴在偏房一處隱蔽的房頂上張望鄙漏。 院中可真熱鬧,春花似錦棺蛛、人聲如沸怔蚌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桦踊。三九已至,卻和暖如春终畅,著一層夾襖步出監(jiān)牢的瞬間籍胯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工离福, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杖狼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓妖爷,卻偏偏與公主長得像蝶涩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子絮识,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容