天天看點

【轉】UTF-8檔案的Unicode簽名BOM(Byte Order Mark)問題

最近在接招聘的PHP的網站,用utf8編碼,經常調用header方法遇到http頭已經發送的問題,頭疼不解。

每次都是需要通過UltraEdit把檔案另存為utf-8無BOM才行,在網上搜尋了一下,找到原因所在:

經常遇到的問題是,使用了BOM編碼後,腳本執行錯誤,或使用fileStream讀取并轉換為XML會報錯"The 

markup in the document following the root element must be well-formed."。

一、介紹

UTF-8 是一種在web應用中經常使用的一種 Unicode 字元的編碼方式,使用 UTF-8 的好處在于它是一種變長的編碼方式,對于 ANSII 碼編碼長度為1個位元組,這樣的話在傳輸大量 ASCII 字元集的網頁時,可以大量節約網絡帶寬。

UTF-8簽名(UTF-8 signature)也叫做BOM(Byte Order Mark),是UTF編碼方案裡用于辨別編碼的标準标記。BOM,是UTF編碼方案裡用于辨別編碼的标準标記,在UTF-16裡本來是FF FE,變成UTF-8就成了EF BB 

BF。這個标記是可選的,因為UTF8位元組沒有順序,是以它可以被用來檢測一個位元組流是否是UTF-8編碼的。微軟做這種檢測,但有些軟體不做這種檢測,而把它當作正常字元處理。微軟在自己的UTF-8格式的文本檔案之前加上了EF BB BF三個位元組, windows上面的notepad等程式就是根據這三個位元組來确定一個文本檔案是ASCII的還是UTF-8的, 然而這個隻是微軟暗自作的标記, 其它平台上并沒有對UTF-8文本檔案做個這樣的标記。也就是說一個UTF-8檔案可能有BOM,也可能沒有BOM。隻有一個BOM,是不會有問題的。如果多個檔案設定了簽名,在二進制流中就會包含多個UTF-8簽名,也就是導緻XML轉換失敗的"root 

element must be well-formed"原因。

二、檢視和轉換

既然一個UTF-8檔案可能有BOM,也可能沒有,那該如何區分呢?隻要用帶十六進制編輯方式的軟體,例如,用UltraEdit-32打開檔案,切換到十六進制編輯模式,察看檔案頭部是否有EF BB BF。有,則為帶BOM方式。

Windows自帶的notepad記事本,儲存為UTF-8時,預設就帶BOM。轉換的方法有很多,常見的UltraEdit-32或NotePad++都可以,以UltraEdit-32為例。打開檔案後,選擇"另存為",在"格式"一欄中有如下選擇:

另外,DreamWeaver CS3也有類似的選項,在"首選項"中,如果選擇 Unicode (UTF-8) 作為預設編碼,則可以選擇"包括 Unicode 簽名 (BOM)"選項,以在文檔中包括位元組順序标記 (BOM)。否則,不帶BOM:

三、其他知識

所謂的unicode儲存的檔案實際上是utf-16,隻不過恰好跟unicode的碼相同而已,但在概念上unicode與utf是兩回事,unicode是記憶體編碼表示方案,而utf是如何儲存和傳輸unicode的方案。utf-16還分高位在前(LE)和高位在後(BE)兩種。官方的utf編碼還有utf-32,也分LE和BE。非unicode官方的utf編碼還有utf-7,主要用于郵件傳輸。utf-8的單位元組部分是和iso-8859-1相容的,這主要是一些舊的系統和庫函數不能正确處理utf-16而被迫出來的,而且對英語字元來說,也節省儲存的檔案空間(以非英語字元浪費空間為代價)。在iso-8859-1的時候,utf8和iso-8859-1都是用一個位元組表示的,當表示其它字元的時候,utf-8會使用兩個或三個位元組。

一段關于BOM的更詳細說明,來自這裡:在UCS 編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字元,它的編碼是FEFF。而FFFE在UCS中是不存在的字元,是以不應該出現在實際傳輸中。UCS規範建議我們在傳輸位元組流前,先傳輸字元"ZERO WIDTH NO-BREAK SPACE"。這樣如果接收者收到FEFF,就表明這個位元組流是Big-Endian的;如果收到FFFE,就表明這個位元組流是Little-Endian的。是以字元"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。UTF-8不需要BOM來表明位元組順序,但可以用BOM來表明編碼方式。字元"ZERO WIDTH NO-BREAK SPACE"的UTF-8編碼是EF BB BF。是以如果接收者收到以EF BB BF開頭的位元組流,就知道這是UTF-8編碼了。

Windows就是使用BOM來标記文本檔案的編碼方式的。

PHP也不支援BOM。

PHP在設計時就沒有考慮BOM的問題,也就是說他不會忽略UTF-8編碼的檔案開頭BOM的那三個字元。由于必須在<?或者<?php後面的代碼才會作為PHP代碼執行,是以這三個字元将會直接輸出。如果插件的檔案有這個問題,将會導緻在背景頁面裡激活或者不激活插件後顯示白屏,如果是模版檔案有這個問題,将會導緻這三個字元直接輸出,造成頁面上方有一個小空行。國外的英文插件和模版一般都是用的ASCII碼的編碼方式,不會有BOM,隻有國内的插件和模版會由于作者的不知情造成問題。還有,大家修改模版的時候,由于輸出頁面使用UTF-8編碼,那麼修改模版的時候如果有加入中文字元的話,必須把檔案轉成UTF-8編碼才能正常顯示,這個時候如果所使用的編輯器自動加上了BOM的話,将會造成在頁面上輸出這三個字元,顯示效果就要看浏覽器了,一般是一個空行或是一個亂碼。

又找到一個PHP的腳本,自動尋找包含BOM的UTF-8檔案并删除,感謝作者,我修改了下

set_time_limit(0);
class Jo_Dir {
    public $__dirs = array();

    public function __construct($path) {
        if (!is_readable($path)) {
            throw new Exception("path not found {$path}");
        }

        $this->__dirs[0] = $path;
    }

    protected function _file($file)
    {}
    
    public function run() {
        $i = 0;

        while (true) {
           if (!isset($this->__dirs[$i])) {
                break;
           }

            $this->_fetch($i);
            $i += 1;
        }
    }

    protected function _fetch($i) {
        $dh   = opendir($this->__dirs[$i]);
        $file = null;

        while (false !== ($file = readdir($dh))) {
            if ($file != '.' && $file != "..") {
                $file = $this->__dirs[$i] . '/' . $file;

                if (is_dir($file)) {
                		echo "finding in directory ".$file."<br/>";
                    $this->__dirs[] = $file;
                } else {
                    $this->_file($file);
                }
            }
        }

        closedir($dh);
    }
}
/**
* 取得檔案夾内所有的檔案
*/
class My_Dir extends Jo_Dir {
    protected function _file($file) {
			$ret = checkBOM($file);
			if(strpos($ret,'BOM found') && !strpos($file,'.svn'))
			{
				echo "filename: $file ". $ret ." <br/>";
			} 
    }
}

function checkBOM ($filename) {
       global $auto;
       $contents=file_get_contents($filename);
       $charset[1]=substr($contents, 0, 1);
       $charset[2]=substr($contents, 1, 1);
       $charset[3]=substr($contents, 2, 1);
       if (ord($charset[1])==239 && ord($charset[2])==187 && ord($charset[3])==191) {
            if ($auto==1) {
                  $rest=substr($contents, 3);
                  rewrite ($filename, $rest);

                   checkBOM ($filename);

                  return ("<font color=red>BOM found, automatically removed.</font>");
            } else {
                  return ("<font color=red>BOM found.</font>");
            }
        }
       else return ("<font color=blue>BOM Not Found.</font>");
}

function rewrite ($filename, $data) {
        $filenum=fopen($filename,"w");
        flock($filenum,LOCK_EX);
        fwrite($filenum,$data);
        fclose($filenum);
}

$dir = new My_Dir(".");
$basedir="."; //修改此行為需要檢測的目錄,點表示目前目錄
$auto=1; //是否自動移除發現的BOM資訊。1為是,0為否。
$dir->run();