第一篇裡我提到了一個問題叫衝碼問題,第二篇裡也帶到過這個問題會造成亂碼。狹義上來說這個問題專指被命名為「許功蓋問題」的大五碼衝碼問題,不過同樣概念的衝碼問題其實並不只有這一個,而它造成的問題當中亂碼還是比較小的一個。這篇文章就來詳細談談它吧。
「是否看過坊間常見的許茹芸淚海慶功宴吃蓋飯第四集」
跟我同年代用過大五碼的人多半看過這個小標題吧。這句話出自 pietty 的作者 piaip,他說「這個句子包括了大部份容易出問題的 Big5 字代表」,因此可以拿來測試軟體系統有沒有衝碼問題。
究竟這是個什麼樣的問題?
第一篇提到衝碼問題時我說這是由於大五碼延伸了第二位元組到了英文字的範圍。如果這範圍內只有英文的話還沒事,但這一塊 0x40~0x7E 裡除了大小寫英文字還有幾個特殊符號;而就這麼碰巧,有一個特殊符號在系統與程式設計當中有著除了文字以外的特別意義:0x5C 反斜線「\」。反斜線最為人所知的兩個文字以外的特殊意義分別是:
- 許多程式語言裡將它做為「跳脫字元」(Escape character),作用是使它和跟在它後面的一個字一起作特別的解釋。懂一點 C/C++ 語言的人一定知道
'\n'
是換行,'\0'
是字串結尾,雙引號字串裡要放雙引號要寫成\"
,等等,這些都是這種跳脫字元的例子。 - MS-DOS 系統 (以及後來的 Windows) 當中,作為路徑的分隔符號。根據維基百科,儘管階層式檔案系統在 Unix 系統上是正斜線分隔,但可能是因為引入時的 DOS 2.0 裡正斜線已經有了命令列選項的意義,並且 DOS 允許選項黏在指令後面不加空白,直接引入會造成指令歧義1所以才改成反斜線的。
由於反斜線的 ASCII 編碼是 0x5C,包括在延伸到的這段英文字碼範圍當中,因此就會發生 DBCS 編碼的位元組中包含了 0x5C,被不了解 DBCS 的系統解釋成特殊意義造成問題。
之所以在大五碼環境裡會被叫做「許功蓋問題」,就是因為大五碼裡第二個位元組是 0x5C 的字當中,「許」(0xB35C)、「功」(0xA55C)、「蓋」(0xBB5C) 這三個字是最常用的幾個字之一,然後連起來又很像是哪個人的名字的關係。在大五碼最原始的 0xA140~0xF9FE 的字碼範圍裡,由於尾碼是 0x5C 而有衝碼問題的字一共有以下這些:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
A | ﹏ | 兝 | α | 么 | 功 | 吒 | 吭 | 沔 | 坼 | 歿 | 俞 | 枯 | 苒 | 娉 | 珮 | |
B | 豹 | 崤 | 淚 | 許 | 廄 | 琵 | 跚 | 愧 | 稞 | 鈾 | 暝 | 蓋 | 墦 | 穀 | 閱 | 璞 |
C | 餐 | 縷 | 擺 | 黠 | 孀 | 髏 | 躡 | ふ | ж | 尐 | 佢 | 汻 | 岤 | 狖 | 垥 | 柦 |
D | 胐 | 娖 | 涂 | 罡 | 偅 | 惝 | 牾 | 莍 | 傜 | 揊 | 焮 | 茻 | 鄃 | 幋 | 滜 | 綅 |
E | 赨 | 塿 | 槙 | 箤 | 踊 | 嫹 | 潿 | 蔌 | 醆 | 嬞 | 獦 | 螏 | 餤 | 燡 | 螰 | 駹 |
F | 礒 | 鎪 | 瀙 | 酀 | 瀵 | 騱 | 酅 | 贕 | 鱋 | 鱭 |
這類型的問題最常出現在程式原始碼當中,當後半字的反斜線被解釋成跳脫字元時這個字就被破壞了,因此就會發生第二篇中談到的字碼組合歪掉造成的亂碼。例子像是:簡單寫一個 "執行成功"
看似沒問題,但因為「功」字後半是反斜線,它和其後方的雙引號合起來變成一個引號的跳脫字元,所以這個字串對編譯器看起來是沒有結束引號的,因此出現編譯錯誤;而如果寫 "執行成功!"
,則反斜線在這裡會解釋為它後面的位元組 ("!"
的第一位元組) 照實保留,於是只有反斜線因為跳脫字元的解釋而消失了,造成半個字被吃掉出現亂碼。
其他的衝碼
衝碼問題不只有 0x5C 會出現;只要因為各種原因,有些位元組具有除了文字以外的意義,而它又落在 0x40~0x7E 的範圍之內,就可能會因為類似的原因造成衝碼。例如:
- 部份檔案系統 (如 FAT) 會拒絕 0x7C 管線符號「|」做為檔名,因此含有這個位元組的字也無法做為檔名。這種字中常見的包括「四」0xA57C、「院」0xB07C、「會」0xB77C 等字。
- 這段範圍內包含了兩種括號:中括號「[」0x5B、「]」0x5D、大括號「{」0x7B、「}」0x7D,因此如果對括號有特殊處理的話也會有衝碼問題,例如語法突顯功能若碰到這些字就會影響括號的配對。上面那串測試句子當中,「坊」0xA77B 的後半字就是左大括號。
- 一個實際發生過的例子:當年《洞窟物語》還在小遊戲版由一群網友中文化時,我本人有幸曾在其中幫忙統整資訊,這其中就碰到了一個問題:在結局畫面顯示製作者名單時,在要顯示遊戲名字的地方會當機。在我下去查看原始碼時才發現問題所在:那一段的遊戲指令碼不知道是什麼原因使用了中括號 [] 夾住要顯示的字串 (其他地方都是引號,所以還真不知道是為什麼),但「窟」這個字的大五碼是 0xB85D,第二位元組是右中括號出現衝碼,因此處理這段指令碼的程式誤判這是括號結束,造成後續文字被當成不存在的指令而當機。印象沒錯的話這個問題當時就有人有回報給了原作者修掉了的樣子。
- 在正規表示式裡,反斜線、中括號、大括號、管線符號都有特殊意義,所以如果要在大五碼字串使用正規表示式找字的話,對於這些會衝碼的字要特別處理。我正好有在 PTT 回過一篇問這個問題的文章。
- 這段範圍裡其他的符號其實都有可能因為類似原因造成衝碼,雖然我目前是還沒看過實際例子就是了。還沒提到的符號包含小老鼠「@」0x40、上標記號「^」0x5E、底線「_」0x5F、反引號「`」0x60、波浪號「~」0x7E。上面的測試句子裡就包含了:「否」0xA75F 後半是底線、「常」0xB160 後半是反引號,這兩個字落在這個範圍。
怎麼辦?
談了兩篇半的理論總算要來提要怎麼解決了。前一篇談了一些亂碼的產生原因,但沒有談到解法是因為,大多數的亂碼問題 (編碼解釋錯誤) 多半是在正確的使用之下可以避免的,但衝碼問題卻是我就是要用這個字卻沒有辦法直接用,兩種狀況是不一樣的。
不過,其實我在上面提到的那篇正規表示式的文章裡已經有提到解決的方向了:就是讓我們想要用的位元組真的被辨認為那個位元組,而不是特殊字元。
最簡單的狀況是反斜線。一般來說,原本有跳脫字元意義的反斜線如果要被辨認為一個「反斜線字元」的話,會需要寫兩個反斜線字元;這樣第一個反斜線做為跳脫字元,和其後面一個字--也是反斜線--組合成代表「反斜線字元」這個字的意義。也就是說,我們需要兩個反斜線來表示原本屬於後半個字的反斜線。實際的表現出來的話--就以"功"
為例--後半字的反斜線要多寫一次,所以會變成"功\"
這個樣子。這是一個很有趣的表現:因為雙位元組編碼的關係,重覆的反斜線只顯示出一個來,但它實際上卻是兩個反斜線;因此原始碼出現這種東西時,多半會有人寫上註解表示因為許功蓋問題這裡多了一個反斜線,防止之後看程式碼的人誤刪造成錯誤。這就是很多地方都查得到的「在許功蓋問題字後面加一個 \ 就行了」的快速解法的理由所在。
其他非跳脫字元的衝碼字就必須要多動一點手腳了。最好的例子還是我上面那篇正規表示式的例子:我們要在大五碼字串裡找尋「四」這個用到管線符號而衝碼的字。正規表示式裡要表示我就是要管線符號這個字要在它前面加上跳脫字元反斜線,因此我們若需要表示「四」的兩個位元組 0xA5 0x7C, 必須在 0x7C 前面加上反斜線 0x5C,全部就變成 0xA5 0x5C 0x7C;但這麼一來, 0xA5 0x5C 就會被解釋成 0xA55C 這個字--就是「功」--所以寫起來就會變成 /功|/
這個樣子。這種狀況其實相對罕見,畢竟不是跳脫字元的衝碼問題並不是那麼常遇到--我玩了十幾年的程式語言,到現在碰到的非跳脫字元衝碼問題就上面舉的兩個而已,一個還不是我自己遇到的--所以基本上可以不用特別去記說什麼字加反斜線會變成什麼字,用到的時候寫隻小程式印出來就知道了。
有的時候如果衝碼字是在不太重要的地方 (例如註解) 時,問題只會在特定的情況出現:例如 C/C++ 當中,反斜線在行尾會跳脫掉這行的換行字元,使下一行接到這一行來,因此 //執行成功
這種註解就會因為「功」的後半字元使得下一行原本不是註解的程式碼接了上來,造成程式邏輯錯誤。這種問題只要問題字不在行尾就不會出現,所以「功」字後面只要多一個其他不是空白的字就正常了--但這和英文環境裡裸的反斜線比起來確實不那麼直覺就是。
小結
衝碼問題的原因要嚴格說起來的話其實仍然是「用了錯誤的方式解讀位元組」而已,只是錯誤的方式和其他亂碼不一樣罷了;但也是因為這個不一樣的原因,使得發生亂碼已經是比較好的結果了,不少狀況都會因為錯誤解釋造成執行錯誤,甚至當機。
但相對的,也因為問題的原因很明確,有避開條件的編碼就不會有衝碼問題;而就算發生了,找到問題點和解決的方法也相對容易。DBCS 編碼裡就我所知有這樣的衝碼問題的應該只有大五碼和 Shift-JIS,其他常見的 DBCS 編碼由於是 EUC 編碼或其延伸,並不會發生這種問題;Unicode 的編碼則因為已經考慮到了單位元組字區的相容性所以也不會有衝碼問題。因此這在現在的環境裡已經是只有在舊環境或處理舊資料時才會發生的問題了。
不過,就算到了 2021 年的現在,有些環境還是會預設一些工作的編碼是地區編碼--特別是 Windows 最多。這部份的問題已經是屬於環境設定的部份,就留給之後再來詳談了。
註腳
- 例如:
dir/w
原本是解釋成「執行dir
指令,帶入/w
參數」,但若直接引入階層式檔案系統的表示法,則也能解釋成「執行目前工作目錄下的dir
資料夾下的w
程式」。 ↩︎