使用 CNS 11643 全字庫字形屬性資料輔助修改倉頡字根定義

CNS 11643 中文標準交換碼又稱全字庫, 是台灣政府訂定的中文字集,其有自己的字型與編碼, 編碼也與後來發展的 unicode 不同。 全字庫有釋出字形屬性資料,也就是一個漢字由哪些部件組成的資料。 在修改倉頡字根時,可以據此找出哪些字含有哪些部件, 如找出所有帶有 羽 的中文字,並修改其編碼。

重定義倉頡字根

倉頡中的 羽 是拆成 尸一尸戈一 。 如果是完整的拆解,應該是 尸戈一尸戈一 , 但因為字根省略規則,字首只取首尾二碼,字身則全取, 所以變成 尸一尸戈一 。

在倉頡中某些二點的字形可以拆成 卜 , 例如 斗 可以拆成 卜十 , 但看來倉頡三代中 羽 的二點不在這個範圍內。

由於省略規則,羽在擔任字首、次字首、第三個字身等時候, 會被拆成 尸一 尸一一 等我覺得很奇怪的拆法, 對此我很不滿;我認為應該要拆成 尸卜 才對。

自定義輸入法對照表

某些輸入法允許使用者自行定義編碼, 可以讓使用者自己客制化輸入哪些字根會輸出哪些字。 如果用倉頡這種拆字輸入法,就可以自己修改哪些字根可以出哪些字。 例如我可以把 栩 從 木尸一一 改成 木尸卜卜 ; 或是直接依據其它東西,像把 貓 定義成 金日廿 , 因為這三個字剛好是 cat 的對應按鍵。

相對於注音,不知道為什麼很少這樣做。 可能因為注音的重碼率太高,多半得搭配智慧選字, 所以輸入法引擎會對注音輸入做很多黑魔法, 不是簡單一張字根對照表能解決的。 拆字的輸入法都是直接出字,少數情況才要選字。

關於表格的格式,很久以前台灣的輸入法界還很繁榮的時候, 弄出了一個副檔名 cin 的格式,讓各輸入法引擎能匯入各種輸入法。 我知道至少 open vanilla、奇摩輸入法、gcin 都是這個格式。 雖然各引撆可能會有自定義的選項。 這種格式的來源,可能是中文 linux 發行版 CLE 中的 xcin 輸入法。 後來的 open vanilla 、 xim 、 jscin, 都有 cin 格式的說明文件。

網路上可以找到一些輸入法的 cin 表格, 我自己也有做一份 supcj.cin 供個人使用。 格式就是用 % 定義特殊指令, 字根與字的對應則是用空格分隔,一行一組。

以 gcin 來說,就是用 gcin2tab, 把 cin 檔案轉成 gcin 使用的 gtab 檔, 然後丟到 /usr/share/gcin/table/ , 再在 gtab.list 裡加入檔名就可以了。 所以如果想加字,例如讓 cat 對應成貓, 就在 supcj.cin 中加入這行: cat 貓 ,再重新產生表格即可。

在 windows xp 也有一個工具叫通用輸入法編輯工具, 可以讓使用者以文字格式匯入表格,然後產生輸入法。 不過在 windows 8 後架構改變, 這個工具產生的輸入法在一些新版程式不能動,也不再內建這個工具。 ( 所以很多人從 xp 把輸入法編輯工具的 exe 檔複製過去。 )

所以,當時較通用格式有二種, windows 的通用輸入法編輯工具的格式,和 cin 的格式。 不過後來不在這個時空背景下的輸入法, 也就是 2000 年左右的台灣,就會定義自己的格式, 例如中國開發的 scim,非中文國家的 ibus, 或是完全生在新時代的 pime。

不過說是重新定義,反正也是大同小異, 像 scim 就是改用 tab 來分隔字根和字, 用等號定義參數,註解要用三個井號。 以會寫程式的來說,要修改都不是什麼難事, pime 雖然改用 json 來當表格, 但也提供了一個轉換程式把 cin 轉換過去。

人力找出含有特定字根的字

要重定義羽和弱的編碼,首先就要找出所有帶該字形的字, 但單純從倉頡的對應來看其實不太準。 如第一節所述,因為倉頡的省略規則, 同一個部件羽,可能是尸一、尸一一、尸一尸戈一, 甚至如果羽是最後一個字身,又有超過三個字首,那羽就會被編為 一 。 同樣的,如果編碼中含有尸一,也不一定代表該字含有羽, 例如 刁 這個字也可以拆成 尸一 。

一開始我想那就找出所有帶有尸一的字再人力過濾, 反正應該不會太多。 結果蠻多的,如果把罕用字都算進來的話大約有 1000 個字。 後來去掉 unicode cjk 擴展平面 A B C D, 就大概少了一半,但還是有大約一半是誤判不含羽的字。 自己肉眼判斷,但後來發現誤判率超高, 我挑剩字裡大概還有一成是漏挑的。

後來想想這樣不行,何況還有漏挑的; 剛好看到 gcin 論壇 有人分享了 cns 中文標準交換碼的開放政府資料 , 含有每個字由哪些部件組成的資料, 於是就拿來用了。

使用全字庫漢字部件屬性資料篩選字根

全字庫的開放資料我是在上面 論壇裡提供的 data.gov 連結 下載的。 下載 zip 解壓縮後,會用到的有 MapingTable/Unicode Properties 二個資料夾。

例如我們要找羽這個部件,首先因為全字庫有自己的編碼, 要找出羽的全字庫編碼。 想偷懶可以去 全字庫網站查 ; 不然就是 echo -n 羽 | iconv -t UTF-32BE | xxd -p , 然後用 unicode 碼在 MapingTable/CNS2UNICODE_Unicode BMP.txt 裡查。 例如羽 unicode 是 7FBD, 就 grep 7FBD MapingTables/Unicode/CNS2UNICODE_Unicode* , 可以查到羽的全字庫編碼是 1-4851。

MapingTables/Unicode/CNS2UNICODE_Unicode* 的格式即是用 tab 分隔,第一欄是全字庫編碼, 第二欄是 unicode 編碼。 其中 BMP 是對應到 unicode 的基本平面,2 15 則是第 2 和 15。

然後用 1-4851 去 Properties/CNS_component.txt 裡查: grep 1-4851 Properties/CNS_component.txt , 可以找到羽是由 415 這個單獨部件組成的; 所以可以判斷羽在全字庫中是視為一個部件存在。 接著就找出所有帶用 415 這個部件的字: awk '($2 ~ 415) {print "^" $1}' Properties/CNS_component.txt > 415.txt , 然後把查出來的全字庫編碼對應回 unicode: grep -f 415.txt 'MappingTables/Unicode/CNS2UNICODE_Unicode BMP.txt' > u.txt , 最後再把 unicode 編碼轉回 utf8: awk '{print $2, "000a"}' u.txt | xxd -p -r | iconv -f UTF-16BE 。

Properties/CNS_component.txt 的格式一樣是 tab 分隔, 第一欄全字庫編碼,第二欄是用逗號分隔的部件列表, 部件是用十進位數字表示。 想知道哪個數字是哪個部件, 可以在 Properties/CNS_component_word.zip 裡的圖片看; 在 zip 裡放 zip,政府單位不意外。 或是也可以直接看那個字長什麼樣子去判斷, 或找只含有那個部件的字。

如果要找 2 15 平面的話,因為前面的指令, 是利用 utf 16 如果一個字可以用 2 byte 表示,就可以直接用 2 byte 表示。 所以才能直接轉成二進位再轉到 utf 16。 另外要指定 be 是要讓位元組順序不要反過來,不然 7FBD 會變成 BD7F。 如果 2 15 平面則會超過 2 byte,可以改成用 utf 32, 但要在前面補 0 補到八個 16 進位數字,也就是 4 byte。 2 15 平面的字幾乎用不到,而且量還不少,所以我就沒有處理。 如果要處理的話指令如下:

awk '($2 ~ 415) {print "^" $1}' Properties/CNS_component.txt  |
    grep -h -f - MapingTables/Unicode/CNS2UNICODE_Unicode\ [12]* |
    awk '{printf "000%s0000000a", $2}' |
    xxd -p -r | iconv -f UTF-32BE

這樣找出來的字都可以確定有羽這個部件,也可以確定沒有漏。 關於弱就少很多,就算把 2 15 都算進去,也不到 200 個。

修改倉頡編碼

再來就是要把編碼修正。 由於字首字身的省略規則不一樣,所以只能一個個看。 我認為比較快的做法是,挑出所有的 一 字根, 然後標記其中不需要被取代為 卜 的,其它就全部取代。 我的做法是用 vi 搜尋 一 字根對應的 m 字母, 如果 m 不是屬於該字中的羽的一部份, 就用 vi 的 r. 指令把 m 取代為 z (倉頡中幾乎用不到 z), 然後按 n 搜尋下一個 m, 下一個開始就可以把 r. 簡化成 . ,直接重複上一個動作。 最後再用 sed 's/m/y/g; s/z/m/g' 把所有 一 改成 卜 把 z 改回 一 。