PHP、XML、以及字元編碼:一則關於悲情、憤怒以及傷逝(資料)的故事

譯者按:

我不是很喜歡在翻譯文章的時候插嘴,不過在讀這篇文章前,交代一下這篇文章的大致脈絡,或許可以幫助您做比較充分的了解。

這是一篇血淚交織的技術文章,原作者是我們的朋友,台灣女婿Steve Minutillo。Steve是Feed on Feeds(簡稱FoF)的作者,FoF是一套架設在伺服器端,透過網頁讀取各站台RSS以及Atom格式新聞的匯集閱讀程式(aggregator),或簡單說,如果你有支援 PHP 以及 MySQL 的主機空間,你可以使用 FoF,自己架設一套類似台灣部落格Meerkat的網頁新聞閱讀平台,不過卻是專屬你自己使用。

Steve最近在做FoF的改版工作,在這個星期內,已經先後釋出了0.1.4、0.1.5及0.1.6版,在最近的改版中,主要針對的是介面的調整,符合XHTML規格,以及對台灣或遠東地區使用者來說最重要的,預設採取UTF-8編碼以及多國字元的處理。在釋出0.1.6版時,我問了個問題:「可以將BIG5或GB2312等編碼的文字,轉換成UTF-8嗎?」Steve於是便很悲憤的繼續製作FoF 0.1.7版,並且寫下了這篇悲憤的文章。

簡單來說,目前的 PHP 環境,在多國語文處理方面,可說非常糟糕,而Steve為了可以讓各國使用者都可以順利使用 FoF,則不斷在目前的困境中,尋找可能的解決之道,而我覺得,這篇文章對於遠東地區的使用者來說,要解決在 PHP 處理多國語文的需求,也是相當有參考價值。

PHP、XML、以及字元編碼:一則關於悲情、憤怒以及傷逝(資料)的故事

原文:PHP, XML, and Character Encodings: a tale of sadness, rage, and (data-)loss

Steve Minutillo

我寫了一套叫做Feed on Feeds的小程式,這是一套供RSS以及Atom規格使用的新聞匯集軟體。長久以來,我就知道這個程式無法正確處理各國字元,所以我嘗試修復這個問題。我知道經常在匯入新聞來源以及輸出成網頁的時候,會有字元混亂的狀況。我遵從「處處均使用UTF-8」的原則:即使FoF中,匯入的新聞來源使用的是不同的編碼方式,但是統一使用同一種編碼輸出,我會將各種編碼,都轉換成UTF-8。我先是將所有用於做顯示使用的程式,都做了「UTF化」,並且確保資料庫本身不會把字元搞亂,最後我注意到實際進行字元解析(munge)的部份,那就是XML解析器(parser)本身,而FoF所使用的RSS以及Atom解析器,叫做MagpieRSS

以下是 Magpie 產生XML解析器的方法:

$parser = xml_parser_create();

真好!真簡單!但是這樣就可以字元了,特別是數值實體(numeric entities,zonble:我不曉得這樣翻譯對不對,我一時之間沒有找到對應的譯詞)。在閱讀了一些PHP文件後,我發現你可以在PHP的XML解析器中,做兩項設定:一是資料來源的編碼,二是目標的編碼。你可以用下述方法,設定編碼:

$parser = xml_parser_create();
xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8");

這麼做的意思是:「不管輸入的XML用的是什麼編碼,我都要你轉換成 UTF-8。而如果你從中找到了任何的數值實體,也轉換成UTF-8字元。」

我做了這樣的嘗試,但是,完全沒有用。有些資料來源會正確轉換成 UTF-8,有些則否。有些原本就是 UTF-8 的資料來源,又被重新編碼了一次,而變成亂碼。在繼續閱讀了更多文件以及臭蟲回報後,我發現,如果你沒有指定資料來源的編碼方式的話,PHP對你的你的XML檔案,預設是以ISO-8859-1編碼判讀!我對PHP的XML解析器,並不會檢驗XML宣告以決定問字編碼方式而大感意外,更進一步,我對他們選擇了這樣瘋狂的預設值,而大感震驚。但是,不管怎樣,你可以用以下方法,同時設定來源以及目標的編碼方式:

$parser = xml_parser_create("EBCIDIC");
xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8");

這麼做的意思是:「我打算給你一些XML,用的是EBCIDIC編碼
(Extended Binary Coded Decimal Interchange Code,擴充二-十進位交換碼)。我要你在進行解析的同時,將所有的字元全部轉換成UTF-8。也別忘了,在你找到了數值實體的時候,同樣也要轉換成UTF-8。」

這樣就有用了,但是會出現一個問題。你怎麼會知道XML中使用的是什麼編碼呢?我所能構想到的唯一解答就是:自行掃描XML檔案,然後自己找出編碼方式!

$rx ='//m';
if (preg_match($rx, $xml, $m)) {
  $encoding = strtoupper($m[1]);
} else {
  $encoding = "UTF-8";
}

以上的正規表示式會在XML宣告中,尋找關於文字編碼的宣告,如果找到的話,就存在$encoding變數當中。如果沒找到,就假設XML檔案已經使用了UTF-8編碼,UTF-8編碼也是XML規格的預設編碼方式。

所以,完整的程式碼如下:

$rx ='//m';
if (preg_match($rx, $xml, $m)) {
  $encoding = strtoupper($m[1]);
} else {
  $encoding = "UTF-8";
}

$parser = xml_parser_create($encoding);
xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8" );

終於成功了。我所訂閱的新聞來源,都正確轉換成了UTF-8編碼,不過這只是巧合而已,因為我所訂閱的新聞,使用的都是UTF-8或ISO-8859-1編碼。在釋出這版程式之前,就有人抱怨無法使用ISO-8859-15以及BIG-5編碼。我又查閱了PHP文件,並且再三檢查程式碼,原因是我又意外發現,PHP 4.x版只支援UTF-8、ISO-8859-1以及US-ASCII。所以如果有人想要訂閱使用了除ISO-8859-1之外的其他ISO-8859系列的編碼方式、BIG5或SHIFT-JIS編碼的新聞,仍然無解。

雖然PHP 5已經釋出了,不過也沒有什麼幫助:雖然某種意義上,PHP 5支援的編碼方式的清單長了許多,但是仍然不包括BIG5以及GB2312,這兩者是最主要的中文編碼方式。

所以,我又繼續翻閱PHP文件,然後找到了一項可能的解答:mbstring!mbstring系列的漢數可以支援多種編碼,並且可以做各種編碼方式之間的轉換。所以解決方式如下:先用一段正規表示式找出資料來源的編碼,如果PHP本身便可以處理的話,那好。如果不行的話,那就剪下一段XML宣告,然後將內容改成encoding=”utf-8″,並且將整個XML檔案透過mb_convert_encoding,在解析器處理內容之前,先轉換成UTF-8。如果mb_convert_encoding 爆炸了話(比方說無法判斷來源的編碼,或是系統中根本沒有這個函數,這點很有可能,因為mbstring本來就是額外的延伸功能),就會自動放棄轉換,就直接送往解析器處理,並且在字元變成一團亂的時候,想辦法避開你的視線。至少我做了嘗試了。

$rx ='//m';
if (preg_match($rx, $source, $m)) {
  $encoding = strtoupper($m[1]);
} else {
  $encoding = "UTF-8";
}

if($encoding == "UTF-8" || $encoding == "US-ASCII" || $encoding == "ISO-8859-1") {
  $parser = xml_parser_create($encoding);
} else {
  $encoded_source = @mb_convert_encoding($source, "UTF-8", $encoding);
  if($encoded_source != NULL) {
    $source = str_replace ( $m[0], '', $encoded_source);
  }
  $parser = xml_parser_create("UTF-8");
}

xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8");

相當讓人驚訝,這樣的修改之後的修改之後的修改之後的修改⋯成功了!這段程式可以正確解析 ISO-8859-15 、BIG-5 甚至 GB2312 的資料來源,並且轉換成UTF-8編碼,在同一頁面上正確顯示。我已經在我自己本機使用的 FoF 上,做好了上述的修改,然後我打算在釋出之前,繼續測試幾天。說不定在釋出後,世界各地的使用者可能又會發現,這套又繁複又充滿悲劇色彩的解決方案,不到幾分鐘時間,又破功了。不過,直到現在,我可以宣稱這是一套最先進的,在PHP上的XML具字元偵測功能的解析方式。我相信這該是在 PHP 4.x 版本上能夠做到最好的方式了。

附註:當我說PHP4可以支援更多編碼方式的時候,我的意思是,PHP5(我使用的是RC3版,在最後釋出的版本中,可能會修好這些問題)根本是徹頭徹尾的白痴。雖然XML解析器支援了一系列更多的編碼,但是實在難以使用。如果你想要確實設定輸入編碼,PHP還是只限制你使用UTF-8、ISO-8859-1或US-ASCII,甚至就算用了libxml2這套底層的解析器,支援會更多,但是同樣如此。不過,如果你知道一些超級神秘的程式碼的話,你可以這樣建構解析器:

$parser = xml_parser_create("");

有注意到甚麼差別嗎?在PHP5的奇怪世界中,傳遞一個空白的字串的意思是:「你就一直做你該做的事情,自動偵測那愚蠢的編碼吧!」不過,這樣又會有另外一個問題:如果你這樣偵測那愚蠢的編碼,那麼愚蠢的目標編碼方式還是會很愚蠢的設定成ISO-8859-1。我實在想不透,誰會想要轉換成ISO-8859-1?而且這跟文件上的說法根本不一樣,文件上說,目標編碼預設是跟來源編碼相同。然後,你還是被限制在輸出時,只能夠使用UTF-8、ISO-8859-1或US-ASCII。所以你能做的—如果你想這麼做的話—你可以用一段正規表示式(真是糟透了!)找出資料來源的編碼方式,但是你卻不能夠將輸出時的目標編碼方式,設定成與來源的編碼方式相同。不過,至少,你可以這麼做:

$parser = xml_parser_create("");
xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, "UTF-8");

意思是,「自動偵測來源的編碼方式,然後把什麼東西,包括數值實體,全部轉換成UTF-8。」

至少⋯在PHP5釋出之後,並且安裝在您的伺服器上後,你便可以這麼做了,這點對我而言,可能不會花上幾年的時間(我已經接獲FoF無法在PHP 3上執行的相關抱怨了)。

15 thoughts on “PHP、XML、以及字元編碼:一則關於悲情、憤怒以及傷逝(資料)的故事

  1. Pingback: hkdom.com - 亞當閒話

  2. Pingback: hkdom.com - 亞當閒話

  3. Pingback: 終極邊疆 Final Frontier BLOG

  4. Pingback: CLK Observatory

  5. Pingback: Justins WebLog » History of Justins WebLogs

  6. Pingback: 今日連結 (2006-03-07) [JeffHung.Blog]

Comments are closed.