背景:
最近在做一個關於上傳文件,需要識別文件編碼的場景需求,項目中使用org.springframework.web.multipart.commons.CommonsMultipartFile來接收上傳上件對象,此對象並沒有類似getFileCharset()等相關的獲取文件編碼的API。因此,在網上補了一下功課,了解一下,識別文件編碼的常用方案,總結於此,以備后查,僅供參考。
一、BOM是什么
BOM,即字節順序標記(ByteOrderMark)是用來判斷文本文件是哪一種Unicode編碼的標記,其本身是一個Unicode字符("\uFEFF"),位於文本文件頭部。
BOM頭是微軟公司為了解決不同的編碼沖撞問題引入的。比如,當你新建一個 文本文檔 之后,在里面輸入 “聯通” 兩個字,然后保存。當你再次打開的時候,原來輸入的 “聯通” 會變成兩個亂碼。這就是因為 GB2312 編碼與 UTF8 編碼產生了編碼沖撞造成的。
在UTF-8編碼文件中BOM在文件頭部,占用三個字節,用來標示該文件屬於UTF-8編碼。UTF-8 的BOM對UFT-8沒有作用,是為了支持UTF-16,UTF-32才加上的BOM,BOM簽名的目的是告訴編輯器當前文件采用何種編碼,方便編輯器識別,但是BOM雖然在編輯器中不顯示,但是會產生輸出,有些軟件不識別BOM的頭,打開保存后會識別出錯。
在不同的Unicode編碼中,對應的BOM的二進制字節Bytes Encoding如下:
FE FF UTF16BE
FF FE UTF16LE
EF BB BF UTF8
因此,我們可以根據文件頭部的幾個字節和上面的表格對應來判斷該文件是哪種編碼形式。
二.、如何獲取文件的BOM字符
BOM頭在記事本中是看不到的,通過程序讀取文件是可以識別的,也可以打印出來
// <Buffer ef bb bf 68 65 6c 6c 6f 20 77 6f 72 6c 64> // 前三個字節就是對應的我們UTF8編碼的文本的BOM頭字符
參考JAVA識別代碼:
/**
* 判斷文件編碼
*
* @param inputStream
* @return
*/
private String getFilecharset(InputStream inputStream) {
String charset = "GBK";
byte[] first3Bytes = new byte[3];
try {
boolean checked = false;
BufferedInputStream bis = new BufferedInputStream(inputStream);
bis.mark(0);
int read = bis.read(first3Bytes, 0, 3);
if (read == -1) {
return charset; // 文件編碼為 ANSI
} else if (first3Bytes[0] == (byte) 0xFF
&& first3Bytes[1] == (byte) 0xFE) {
charset = "UTF-16LE"; // 文件編碼為 Unicode
checked = true;
} else if (first3Bytes[0] == (byte) 0xFE
&& first3Bytes[1] == (byte) 0xFF) {
charset = "UTF-16BE"; // 文件編碼為 Unicode big endian
checked = true;
} else if (first3Bytes[0] == (byte) 0xEF
&& first3Bytes[1] == (byte) 0xBB
&& first3Bytes[2] == (byte) 0xBF) {
charset = "UTF-8"; // 文件編碼為 UTF-8
checked = true;
}
bis.reset();
if (!checked) {
int loc = 0;
while ((read = bis.read()) != -1) {
loc++;
if (read >= 0xF0)
break;
if (0x80 <= read && read <= 0xBF) // 單獨出現BF以下的,也算是GBK
break;
if (0xC0 <= read && read <= 0xDF) {
read = bis.read();
if (0x80 <= read && read <= 0xBF) // 雙字節 (0xC0 - 0xDF)
// (0x80
// - 0xBF),也可能在GB編碼內
continue;
else
break;
} else if (0xE0 <= read && read <= 0xEF) {// 也有可能出錯,但是幾率較小
read = bis.read();
if (0x80 <= read && read <= 0xBF) {
read = bis.read();
if (0x80 <= read && read <= 0xBF) {
charset = "UTF-8";
break;
} else
break;
} else
break;
}
}
}
bis.close();
} catch (Exception e) {
e.printStackTrace();
log.error("getFilecharset exception ",e);
}
return charset;
}
三、BOM頭存在哪些問題
雖然BOM頭起到了標記文件編碼的作用,但是他並不屬於文件的內容部分,類似WINDOWS自帶的記事本等軟件,在保存一個以UTF-8編碼的文件時,會在文件開始的地方插入三個不可見的字符(0xEF 0xBB 0xBF,即BOM)。它是一串隱藏的字符,用於讓記事本等編輯器識別這個文件是否以UTF-8編碼。對於一般的文件,這樣並不會有什么問題。
但在某些場景下,會產生一些額外的問題:
1、比如我們把幾個JS文件合並成一個文件后,如果文件中間含有BOM字符,就會導致瀏覽器JS語法錯誤。
2、對於 PHP來說,BOM是個大麻煩。PHP並不會忽略BOM,所以在讀取、包含或者引用這些文件時,會把BOM作為該文件開頭正文的一部分。根據嵌入式語言的特點,這串字符將被直接執行(顯示)出來。由此造成即使頁面的 top padding 設置為0,也無法讓整個網頁緊貼瀏覽器頂部,因為在html一開頭有這3個字符呢!最大的麻煩還不是這個。受COOKIE送出機制的限制,在這些文件開頭已經有BOM的文件中,COOKIE無法送出(因為在COOKIE送出前PHP已經送出了文件頭),所以登入和登出功能失效。一切依賴COOKIE、SESSION實現的功能全部無效。
四、如何去掉文件中的BOM頭
1、在文件另存為的時候選擇無BOM頭的UTF8編碼
2、使用node中的文件模塊獲取文件的buffer數據並去掉前三個字節,代碼如下:
function deleteUtf8BomHead(path) { let buf = fs.readFileSync(path); if (buf[0] == 0xef && buf[1] == 0xbb && buf[2] == 0xbf) { buf = buf.slice(3) } return buf }
3、EDITPLUS去BOM頭的方法
編輯器調整為UTF8編碼格式后,保存的文件前面會多出一串隱藏的字符(也即是BOM),用於編輯器識別這個文件是否是以UTF8編碼。
運行Editplus,點擊工具,選擇首選項,選中文件,UTF-8標識選擇 總是刪除簽名,然后對PHP等文件編輯和保存后的PHP文件就是不帶BOM的了。
4、UltraEdit去除BOM頭辦法
打開文件后,另存為選項的編碼格式里選擇(UTF-8 無BOM頭)保存。
備注 :
1、在編輯、更改任何文本文件時,請使用不會亂加BOM的編輯器。Linux下的編輯器目前沒發現這個問題。Windows下,請勿使用記事本等編輯器。推薦的編輯器是:notepad ++, Editplus 2.12版本以上, EmEditor, UltraEdit(需要取消‘添加BOM’的相關選項), Dreamweaver(需要取消‘添加BOM’的相關選項)等。對於已經添加了BOM的文件,要取消的話,可以用以上編輯器另存一次。(Editplus需要先另存為GB,再另存為UTF-8。)