Go 处理文件BOM头问题
背景
开发中遇到一个上传文件的需求,通过规定上传的文件会将数据按行排列。 Bug复现为,当上传的文件是csv (Win)系产品上传的时,会在文首解析出不可显的字符,导致后端在解析时会校验错误。给用户返回失败。
用vim -b ./data.csv
打开文件发现了文首的不可见字符 \<feff>
。如下图,后来才知道这个是UTF编码自带的编码顺序标记。
所以本文接下来分别介绍下,
- BOM的详细信息,
- 以及我在golang的服务中是怎么处理这种兼容的状况的。
BOM的详细信息
Unicode编码中表示字节排列顺序的那个文件头,叫做BOM(byte-order mark),FFFE和FEFF就是不同的BOM。
在详细说明BOM时,我们先来认识一下 Big Endian
和 Little Endian
字节序
为什么要注意字节序的问题呢?你可能这么问。当然,如果你写的程序只在单机环境下面运行,并且不和别人的程序打交道,那么你完全可以忽略字节序的存在。但是,如果你的程序要跟别人的程序产生交互呢?在这里我想说说两种语言。C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而JAVA编写的程序则唯一采用Big Endian方式来存储数据。试想,如果你用C/C++语言在x86平台下编写的程序跟别人的JAVA程序互通时会产生什么结果?就拿上面的0x12345678来说,你的程序传递给别人的一个数据,将指向0x12345678的指针传给了JAVA程序,由于JAVA采取Big Endian方式存储数据,很自然的它会将你的数据翻译为0x78563412。什么?竟然变成另外一个数字了?是的,就是这种后果。因此,在你的C程序传给JAVA程序之前有必要进行字节序的转换工作。
无独有偶,所有网络协议也都是采用Big Endian的方式来传输数据的。所以有时我们也会把Big Endian方式称之为网络字节序。当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。
Big Endian
Big Endian:最高字节在地址最低位,最低字节在地址最高位,依次排列。
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 12 | 34 | 56 | 78 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Little Endian
Little Endian:最低字节在最低位,最高字节在最高位,反序排列。
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 78 | 56 | 34 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用FEFF表示。这正好是两个字节,而且FF比FE大1。
- 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。
那么,我们要如何处理文件BOM头的问题呢?
Go 处理文件BOM头问题
读文件 BOM头
刚刚我们提到,对于 Big Endian 和 Little Endian 是有不一样的 BOM 头,所以我们也需要不同的处理。基本的处理方式如下:
package main
import (
"fmt"
"io/ioutil"
)
func main(){
filename := "data.csv"
newFile, err := skipBOM(filename)
if err!=nil{
fmt.Print(err.Error())
return
}
fmt.Print(string(newFile))
}
func skipBOM(filename string) (newFile []byte, err error){
file, err := ioutil.ReadFile(filename)
if err!=nil{
return nil, err
}
if len(file) >= 4 && isUTF32BigEndianBOM4(file){
return file[4:],nil
}
if len(file) >= 4 && isUTF32LittleEndianBOM4(file){
return file[4:],nil
}
if len(file) > 2 &&isUTF8BOM3(file){
return file[3:],nil
}
if len(file) == 2 && isUTF16BigEndianBOM2(file){
return file[2:],nil
}
if len(file) == 2 && isUTF16LittleEndianBOM2(file){
return file[2:],nil
}
return file,nil
}
func isUTF32BigEndianBOM4(buf []byte) bool {
if len(buf) < 4{
return false
}
return buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0xFE && buf[3] == 0xFF
}
func isUTF32LittleEndianBOM4(buf []byte) bool {
if len(buf) < 4{
return false
}
return buf[0] == 0xFF && buf[1] == 0xFE && buf[2] == 0x00 && buf[3] == 0x00
}
func isUTF8BOM3(buf []byte) bool {
if len(buf) < 3{
return false
}
return buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF
}
func isUTF16BigEndianBOM2(buf []byte) bool {
if len(buf) < 2{
return false
}
return buf[0] == 0xFE && buf[1] == 0xFF
}
func isUTF16LittleEndianBOM2(buf []byte) bool {
if len(buf) < 2{
return false
}
return buf[0] == 0xFF && buf[1] == 0xFE
}