原创 更廣義的字元集轉換:iconv 收藏

新一篇: 文字列を整数型に変換する | 旧一篇: 字元分類與字串處理

 

更廣義的字元集轉換:iconv

在先前的章節中,我們已見到了兩組 mbs 與 wcs 互轉的函式,第一組對於有「狀態改變」的編碼系統 mbs 無法做到字串的狀態控制,故不適合做該 mbs 的轉換工作;而第二組則可以直接做字串的狀態控制,故使用範圍就更廣了。然而,這兩組字串轉換函式在某些使用場合下都有很大的限制,廣義而言,它們都屬於「字元集轉換函式」,然而它們都直接與 I18N、locale 機制綁在一起,也就是說在使用它們之前,程式都必須設好正確的 locale 才行。故在以下的情況使用它們就會很不方便,甚至根本行不通:

  • 如果程式中需要做 A 字集編碼與 B 字集編碼的轉換時,不幸的是這兩種字集 編碼都不是目前程式所處的 locale 所採用的。若使用前述的 wcs 與 mbs 轉換函式,唯一的辦法只有先呼叫 setlocale(),將程式的 LC_CTYPE locale 先切換到使用字集編碼 A 的 locale,把字串 A 轉成 wcs 字串後,再呼叫一次 setlocale(),將程式的 locale 切換到使用字集編碼 B 的 locale,最後才把 wcs 字串轉成字串 B。萬一 都找不到任何 locale 採用字集 A 或字集 B 時,則這一招就沒有用了。

  • 如果程式中需要同時做多種字集編碼的轉換時,則這些 wcs 與 mbs 的轉換 函式就無法辦到了。原因是一旦 setlocale() 設定好程式的 locale 之後,其影響是遍及整個程式每個部分,我們不可能做到在轉換字集編碼 A 時設好 locale A,又 ``同時'' 設好 locale B 來轉字集編碼 B。
因此,我們需要更廣義的字集轉換系統,一個可以與 locale 完全無關的轉換系統,才能方便地達到上述的要求。故在 XPG2 標準中另外定義了一組全新的函式介面: iconv。事實上,在 glibc 中,表面上看來那些 wcs 與 mbs 轉換函式與 iconv 不太一樣,但它們底層的 wcs 與 mbs 轉換工作卻是由 iconv 來達成的。故 iconv 可以說是字集轉換系統中最基底的函式介面。

iconv 字集轉換系統只有三個函式,在很多系統中是宣告在 iconv.h 裏頭,使用上與一般在做檔案讀寫的概念一樣,先 ``開啟''、之後 ``操作''、完畢後要 ``關閉'',這些函式包括:

  • iconv_t iconv_open(const char *TOENC, const char *FROMENC)

  • size_t iconv (iconv_t CD, const char **INBUF,
    size_t *INBYTESLEFT, char **OUTBUF, size_t *OUTBYTESLEFT)

  • int iconv_close(iconv_t CD)

首先 iconv_open() 函式就是做 ``開啟'' 動作,也就是當我們要將編碼系統 A 轉換到編碼系統 B 時,必須先呼叫此函式,將 FROMENC 設成編碼系統 A 的名字,同時將 TOENC 設成編碼系統 B 的名字,這時此函式就會做類似檔案開啟的動作,傳回一個代表此轉換管道的資料結構 iconv_t 供後續使用。事實上,在系統的實作中真的是將 iconv_open() 當作 ``開啟檔案'' 來處理,故它會受到目前系統或同一行程中可開啟檔案數所限,如果系統或程式的其他部分已開啟了太多的檔案以至於逼近系統上限,則有可能這邊的 iconv_open() 會失敗。

如果 iconv_open() 開啟失敗時,它會傳回 (iconv_t)-1 的值,同時設定 errno 全域變數,用以指出開啟失敗的原因。而開啟失敗的原因,不外乎就是已開啟的檔案數已超過上限、或系統記憶體不足、或系統本身無法做到編碼系統 A 與 B 之間的轉換。有興趣的讀者可以直接去閱讀 info libc* Character Set Handling:: 一節了解各 errno 的值與其所代表的意義。

第二個 iconv() 函式就是用來做實際編碼系統轉換工作的,它必須先呼叫過 iconv_open() 並取得 iconv_t 結構後才能工作。只要 iconv_open() 可以開啟成功,則理論上它就可以進行轉換工作,而且不論其來源編碼與目標編碼是 mbs 字串或 wcs 字串、或二者的混合互轉,都沒有關係。但要注意的是,如果其中有 wcs 字串時,就算用來存放 wcs 字串是使用 wchar_t * 形別,在傳入此函式時仍然統一用 char * 來處理。

若要做字串 A 轉換成字串 B 時,字串 A 是經由 *INBUF 傳入,而 *INBYTESLEFT 則傳入陣列 A 的長度,一律以位元組數來計算。而轉換的結果則由 *OUTBUF 傳回,同樣的 *OUTBYTESLEFT 則為 *OUTBUF 的長度。如果轉換成功了,則 *INBUF 最後會設在存放字串 A 的陣列末尾,而 *INBYTESLEFT 會設為目前此陣列還剩多少位元組可以用。而 *OUTBUF 與 *OUTBYTESLEFT 也是一樣。因此我們可以在相同的 A、B 陣列中重複呼叫此函式。舉個例子來說,如果轉換過程中在 A 陣列裏頭遇到了不合法的字元而無法進行轉換時,則 *INBUF 與 *OUTBUF 就會停在無法轉換的位置上,並將已轉換成功的結果傳回,則我們可以自行決定看是要跳過那個不合法的字元,繼續轉換剩餘的部分,或做其他的特殊處理 ...等等。

在很多情況下有可能造成 iconv() 的轉換失敗,例如前面提到的在 A 陣列中遇到不合法的字元,或者 A 中某個字元在 B 中找不到可對應的字。其中第二個情況是最有可能發生的,而目前 glibc-2.1.x 系統中遇到這種情況時就當做是轉換失敗,而不做進一步的處理。當然這並不是最好的處理方式,故未來可能會有所改變。而根據 info libc 的說明,如果轉換失敗了, iconv() 最後的傳回值是 (size_t)-1,並設定 errno 的值說明失敗的原因;如果轉換成功,則傳回值會是已成功轉換的字元個數。但我們實際的測試結果,在 glibc-2.1.3 的系統下,如果轉換成功後其傳回值卻永遠是 0,與 info libc 上的描述不一樣,這點有點奇怪,不曉得是不是目前 glibc 的 bug?

若遇到含「狀態改變」的編碼系統時,iconv() 也能正確工作,它能在轉換的過程中隨時記錄、更新字串目前的狀態 (應該就是記錄在 iconv_t CD 結構中),故就算是採「分期付款」方式將同一字串切成數分一段段來轉換,也不會出問題。但要注意的是,在第一次使用 iconv() 之前,必須先初始化一下 A、B 兩字串的狀態,就好像我們在先前所提的 mbsrtowcs() 等函式一樣,在使用前也必須先做好狀能初始化,如此才能讓後續可以正常工作。而對於 iconv() 而言,初始化的方式就是呼叫它時,*INBUF 與 *OUTBUF 都設為 NULL 即可。

最後一個 iconv_close() 函式,就是當整個轉換結束後,用來做「關閉檔案」用的。

底下我們就寫了一個範例程式,用來說明 iconv 函式介面的使用方式:

#include <stdio.h>
#include <string.h>
#include <iconv.h>

int main(int argc, char **argv)
{
  FILE *fin, *fout;
  char *encFrom, *encTo;
  char bufin[1024], bufout[1024], *sin, *sout;
  int mode, lenin, lenout, ret, nline;
  iconv_t c_pt;

  if (argc != 5) {
    printf("Usage: a.out <encFrom> <encTo> <fin> <fout>\n");
    return 0;
  }
  encFrom = argv[1];
  encTo   = argv[2];
  if ((fin = fopen(argv[3], "rt")) == NULL) {
    printf("Cannot open file: %s\n", argv[3]);
    return -1;
  }
  if ((fout = fopen(argv[4], "wt")) == NULL) {
    printf("Cannot open file: %s\n", argv[4]);
    return -1;
  }

  if ((c_pt = iconv_open(encTo, encFrom)) == (iconv_t)-1) {
    printf("iconv_open false: %s ==> %s\n", encFrom, encTo);
    return -1;
  }
  iconv(c_pt, NULL, NULL, NULL, NULL);

  nline = 0;
  while (fgets(bufin, 1024, fin) != NULL) {
    nline ++;
    lenin  = strlen(bufin) + 1;
    lenout = 1024;
    sin    = bufin;
    sout   = bufout;
    ret = iconv(c_pt, &sin, &lenin, &sout, &lenout);
    printf("%s -> %s: %d: ret=%d, len_in=%d, len_out=%d\n", 
           encFrom, encTo, nline, ret, lenin, lenout);
    if (ret == -1) {
      printf("stop at: %s\n", sin);
      break;
    }
    fprintf(fout, "%s", bufout);
  }
  iconv_close(c_pt);
  fclose(fin);
  fclose(fout);
  return 0;
}
這個程式可以從命令列輸入來源檔案與目的檔案的編碼系統名稱,將來源檔案的內容轉換成目的檔案。各位讀者可以注意到,在程式中我們完全沒有做 locale 設定等相關動作,原因正是不需要,iconv 函式介面是與 locale 完全無關的。同時,我們的程式只適合拿來做兩個 mbs 編碼系統間的轉換,不適合拿來做其中一個有 wcs 的轉換,原因是程式中我們沒有特別為 wcs 的情況使用 wchar_t * 陣列,同時記得我們以前也提過,wcs 字串是不能拿來做檔案輸出的,而我們的程式目前正是直接做檔案編碼轉換的工作。

這樣的轉碼程式倒底「耐不耐用」呢?有興趣的讀者可以跟著我們一起來測試。由於先前我們已介紹過了如何在您的 GNU/Linux 系統中安裝 zh_TW.Big5 與 zh_CN.GB2312 locale 環境,如果您都已安裝無誤的話,理論上您的系統的 Big5 與 GB2312 的 gconv 系統都有不成問題了 (在 glibc 中,gconv 系統可以說是 iconv 的心臟,我們會在下一小節詳細說明),故我們就直接以 Big5 與 GB2312 兩個編碼系統互轉為例子,測試一下我們的程式。首先,請先準備一個叫 f-big5 的檔案,內容用 Big5 編碼打入這樣一行的內容:

我是研究生
然後,將我們的範例程式編譯好,假設程式執行檔名就是 a.out,接著執行:
a.out BIG5 GB2312 f-big5 output
在 glibc-2.1.x 的系統下,應該可以正確轉換,這時您應該會見到如下的程式輸出:
BIG5 -> GB2312: 1: ret=0, len_in=0, len_out=1012
其中 ret=0 就表示轉換成功,然後,您可以再開一個可以看 GB2312 編碼的 terminal 來看 output 檔的內容,您就會見到原來 f-big5 檔的內容已轉換成 GB2312 碼了。

看起來似乎很完美,我們再進一步測試。現在請在 f-big5 檔中再加一些內容,像這樣:

我是研究生
目前在正在做研究測試
按照上頭的指令再執行一次,我們得到如下的結果:
BIG5 -> GB2312: 1: ret=0, len_in=0, len_out=1012
BIG5 -> GB2312: 2: ret=-1, len_in=6, len_out=1008
stop at: 測試
第一行沒有問題,但第二行失敗了,程式指出第二行最後兩個字 ``測試'' 無法做轉換,為什麼會這樣呢?

要探討其原因,就要更深入 iconv 的編碼系統轉換原理了。我們在之前已多次提過,在做 mbs 與 wcs 之間轉換時,其實就是將 mbs 轉換成系統的基底字集,而 iconv 的轉換原理也是這般。事實上在 glibc 系統中,它在做編碼系統 A 與 B 之間的轉換時,有兩種途徑可選擇:一是如果 iconv 內部已內建了 A 與 B 的對應表格時,就採用此對應表格來轉換,而這也是最可信賴的轉換方式。萬一它找不到適當的對應表格時,它則會先將 A 轉換成基底字集,再由基底字集轉換成 B。也就是系統的基底字集在這裏扮演了中間媒介的角色。

要注意的是,在其他的 UNIX 系統中,它們可能只提供第一種途徑供轉換,而 glibc 提供第二種途徑的理由是,這樣可以確保任意兩個字集編碼都有機會進行轉換,只要這兩個字集編碼都同時被系統所支援。而且,由於 glibc 所選擇的「基底字集」理論上已包含了目前世界上所有正在使用的電腦字集,故使用基底字集做為中間媒介是合理而且可行的。

但各種字集編碼的問題是很複雜的玩意兒,有時候這一套也有出槌的時候。就像前面見到的 Big5 的 ``測試'' 無法轉成 GB2312 一樣,我們再做以下的實驗,就會知道原因了。因為 glibc 是以 UCS4 做為其系統的基底字集,而 UCS4 的 mbs 形式為 UTF8 編碼系統,故我們現在準備兩個檔案,一個是 f-big5,內含 Big5 碼的 ``測試'' 兩字,另一個是 f-gb,內含 GB2312 碼的 ``測試'' 兩字。現在我們用上頭那個範例程式將這兩個檔案都轉成 UTF8 編碼:

a.out BIG5 UTF8 f-big5 output1
a.out GB2312 UTF8 f-gb output2
這兩個都可以轉換成功。然後,我們用 diff 程式比較一下 output1 與 output2 的內容,發現它們的內容不一樣!

這就是無法轉換成功的原因!因為在中文裏頭意義一樣的兩個字,在 Big5 與 GB2312 裏頭分別被對應到不同的 UCS4 編碼裏頭去了。為什麼會如此?這牽涉到更深刻的 UCS4 編碼規則與其他編碼系統的對應問題,在這裏我們就先略過了,以後若有機會的話再回來討論。現在的問題是,在 glibc 中,如果 A 與 B 的某些字分別被對應到基底字集中不同的編碼中,就有可能會無法轉換成功。而這往往是很有可能發生的,特別是 glibc 直接採用 UCS4 做為其基底字集時。

而且,在回顧一下我們前面談的,在不同的 UNIX 系統下,其 iconv 底層的運作模式可能都不太一樣,這就造成了更嚴重的問題:就算系統同時支援 A 與 B 兩種編碼,我們仍不能保證可以將 A 轉成 B。或者,該系統中可以將 A 轉成 C,也可以將 C 轉成 B,但也不能保證可以將 A 直接 (或間接) 轉成 B。

這的確是另人氣餒的問題,現在也許讀者就會問了,那 iconv 有什麼用?我們的認為是,它在小字集與大字集編碼間的轉換是有用的。例如 Big5 與 GB2312 分別是 UTF8 的子字集,則我們可以很放心地使用 iconv 將 Big5 或 GB2312 轉成 UTF8,或反方向地轉換回來 (當然,前提是原來的 UTF8 字串中沒有包含 Big5 或 GB2312 所沒有的字)。又或者 Big5 是 GBK 的一個子字集,我們一樣也可以利用它來做轉換。

但如果要直接用 iconv 來做 Big5 與 GB2312 的轉換的話,唯一個可靠方法就是在 iconv 系統中加入一個 Big5 與 GB2312 直接互轉的表格,並且希望此表格可以成系統標準的一部分 (至少要是 glibc 標準的一部分),如此我們的程式這樣寫才會有用 (至少在 glibc 的系統下)。因此,現在我們就進入下一小節,來看看 glibc 的 iconv 的核心部分,做為本章的一個結束。

发表于 @ 2004年12月04日 13:19:00|评论(loading...)|编辑

新一篇: 文字列を整数型に変換する | 旧一篇: 字元分類與字串處理

评论:没有评论。

发表评论  


当前用户设置只有注册用户才能发表评论。如果你没有登录,请点击登录
Csdn Blog version 3.1a
Copyright © Lafaer