害,这恼人的BOM

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 害,这恼人的BOM

0、前言


开发中做了一个导出CSV功能,本地通过wps测试都没有问题,但是测试人员测试的时候发现用excel打开中文表头会出现乱码现象,很奇怪的现象,用nodePad工具打开看也是正常的,但是用excel打开就是中文乱码,通过查找资料了解到是因为csv文件是utf-8编码的,但是没有增加bom头,这样就会导致在window环境下一些软件会用默认编码打开文件从而导致乱码问题,本文详细介绍从前端下载、后端读写如何解决该问题。


1、何为BOM


BOM —— Byte Order Mark,中文名译作“字节顺序标记”。关于 BOM 的说明:在UCS 编码中有一个叫做 "Zero Width No-Break Space" ,中文译名“零宽无间断间隔”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符 "Zero Width No-Break Space"。这样如果接收者收到 FEFF,就表明这个字节流是Big-Endian的;如果收到FFF,就表明这个字节流是 Little- Endian的。因此字符 "Zero Width No-Break Space" (“零宽无间断间

隔”)又被称作 BOM。


image.png

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符 "Zero Width No-Break Space" 的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是 UTF-8编码了。Windows环境就是使用BOM来标记文本文件的编码方式的。

image.png


2、BOM头带来的问题


Windows自带的记事本等软件,在保存一个以UTF-8编码的文件时,会在文件开始的地方插入三个不可见的字符(0xEF 0xBB 0xBF,即BOM)。它是一串隐藏的字符,用于让记事本、office等编辑器识别这个文件是否以UTF-8编码。对于一般的文件,这样并不会产生什么麻烦。但对于解析来说,BOM是个大麻烦。文件读取时并不会忽略BOM,所以在读取、包含或者引用这些文件时,会把BOM作为该文件开头正文的一部分。


通过notePad16进制打开文件可以看出bom头的区别

image.png


带有bom头的文件带来的问题主要有两个:


  • 乱码:如果字段中含有中文、希伯来文、法语、德语等文字,导出的csv文件在Excel中打开后,这些文字呈现出乱码。

image.png


  • 用opencsv等解析文件的api时由于多解析了bom头导致解析内容出错。


3、解决BOM乱码问题


严格来说这并不是csv文件的问题,而是Excel等windows软件处理文件编码方式问题,Excel默认并不是以UTF-8来打开文件,所以在csv开头加入BOM,告诉Excel文件使用utf-8的编码方式。如果文件以UTF-8编码,但又没有增加bom头就会导致excel按照默认编码方式解码,从而导致中文等乱码现象,由于现在读写文件一般都是用UTF-8编码,所以需要在代码中解决此问题。


3.1 Java后端修改


解决方案是在文件写入最开始处增加bom头,这样导出的文件用excel等软件打开就是正常的。


  • 如果是普通的输出流:
    outputStream.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});


  • 如果是包装流:
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(reportFile, true), UTF_8))); writer.write(new String(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, UTF_8));


3.2 前端angular修改


这样修改后,能保证后端传给前端的流中包含bom头,但是使用angular时,通过接受文本的方式还是会把bom去掉,如下所示:

exportData() {
    let url = 'XX/**';
    const fileName = 'test.csv';
    this._http.post(url, this.queryParam).subscribe(res => {
      const data = res.text();
      getCSVFile(data, fileName);
    });
 }
 private  getCSVFile(data: any, fileName: string) {
    const blob = new Blob([data], {type: 'text/csv;charset=utf-8;'});
    const objectUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.setAttribute('style', 'display:none');
    a.setAttribute('href', objectUrl);
    a.setAttribute('download', fileName);
    a.click();
    document.body.removeChild(a);
 }

修改前端代码,使用blob的方式接受后端发过来的流数据:

exportData() {
    let url = 'XX/**';
    const fileName = 'test.csv';
    let requestOptions = new RequestOptions();
    requestOptions.responseType = ResponseContentType.Blob;
    this._http.post(url, this.queryParam, requestOptions).subscribe(res => {
      const data = res.blob();
      getCSVFile(data, fileName);
    });
 }
 private  getCSVFile(data: any, fileName: string) {
    const blob = new Blob([data], {type: 'text/csv;charset=utf-8;'});
    const objectUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.setAttribute('style', 'display:none');
    a.setAttribute('href', objectUrl);
    a.setAttribute('download', fileName);
    a.click();
    document.body.removeChild(a);
 }

由于前端技术有限,简单理解说明下,两处代码的区别,前端相应body常用的方法有res.text()、res.blob()、res.json()等方法,看res.text()源码注释可知,此方法会默认用iso-8859解码成文本,所以如果使用text方法接收,这改后端增加bom是无效的。经过测试还是用blob二进制流的方式接收才能不改变后端传过来的东西。

export declare abstract class Body {
/**
 * Attempts to return body as parsed `JSON` object, or raises an exception.
 */
 json(): any;
    /**
 * Returns the body as a string, presuming `toString()` can be called on the response body.
 *
 * When decoding an `ArrayBuffer`, the optional `encodingHint` parameter determines how the
 * bytes in the buffer will be interpreted. Valid values are:
 *
 * - `legacy` - incorrectly interpret the bytes as UTF-16 (technically, UCS-2). Only characters
 * in the Basic Multilingual Plane are supported, surrogate pairs are not handled correctly.
 * In addition, the endianness of the 16-bit octet pairs in the `ArrayBuffer` is not taken
 * into consideration. This is the default behavior to avoid breaking apps, but should be
 * considered deprecated.
 *
 * - `iso-8859` - interpret the bytes as ISO-8859 (which can be used for ASCII encoded text).
 */
 text(encodingHint?: 'legacy' | 'iso-8859'): string;
    /**
 * Return the body as an ArrayBuffer
 */
 arrayBuffer(): ArrayBuffer;
    /**
 * Returns the request's body as a Blob, assuming that body exists.
 */
 blob(): Blob;


4、Java处理BOM头文件


java普通的文件读取方式对于bom是无法正常识别的。使用普通的InputStreamReader,如果采用的编码正确,那么可以获得正确的字符,但bom仍然附带在结果中,很容易导致数据处理出错,尤其是在通过字符长度读取文件内容时。另外,对于存在BOM头的文件,无法猜测它使用的编码。


4、1 实现原理


整体解决思路就是对BOM头进行捕捉和过滤。


4.2 使用apache的工具类


使用BOMStream,该类的构造方式:


BOMInputStream bomIn = new BOMInputStream(in) //仅能检测到UTF8的bom,且在流中exclude掉bom
BOMInputStream bomIn = new BOMInputStream(in, include); //同上,且指定是否包含

也可以指定检测多种编码的bom,但目前仅支持UTF-8/UTF-16LE/UTF-16BE三种,对于UTF32之类不支持。


BOMInputStream bomIn = new BOMInputStream(in, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE);

有用的方法:


bomIn.hasBOM()、hasBOM(ByteOrderMask.**)可用于判断当前流中是否检测到了bom。
FileInputStream fis = new FileInputStream(file);
//可检测多种类型,并剔除bom
BOMInputStream bomIn = new BOMInputStream(in, false,ByteOrderMark.UTF-8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE);
String charset = "utf-8";
//若检测到bom,则使用bom对应的编码
if(bomIn.hasBOM()){
   charset = bomIn.bs.getBOMCharsetName();
}
InputStreamReader reader = new InputStreamReader(bomIn, charset);
...


4.3、使用一个更强大点的工具类UnicodeStream


UnicodeStream(可以支持UTF-8/UTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE)


FileInputStream fis = new FileInputStream(file);
UnicodeReader ur = new UnicodeReader(fis, "utf-8");
BufferedReader br = new BufferedReader(ur);
...

相较于Apache的工具类,这里的UnicodeReader支持更多的BOM编码。


原理:UnicodeReader通过PushbackInputStream+InputStreamReader实现BOM的自动检测和过滤读取; 当没有检测到BOM时,pushback流将回退,并采用构造函数传入的编码进行读取。 否则使用BOM对应的编码进行读取。


相对来说,第二种方式更加轻量和强大;另外也更加透明,可以随便修改源码来实现自己的需求。


5、总结


  • 如果再生成的文件只是为了程序之间传输数据,应该是写无bom头文件,这样会避免解析时的问题。


  • 如果是要给人员阅读就考虑是否会在window下用微软的软件打开问题,也就是要增加bom来解决乱码问题。





目录
相关文章
|
3月前
|
Web App开发 自然语言处理
一盏茶的功夫带你掌握烦人的 this 指向问题( 一 )
一盏茶的功夫带你掌握烦人的 this 指向问题( 一 )
|
3月前
|
Web App开发 自然语言处理
一盏茶的功夫带你掌握烦人的 this 指向问题( 二 )
一盏茶的功夫带你掌握烦人的 this 指向问题( 二 )
|
4月前
|
Python
惊呆了!学会这一招,你的Python上下文管理器也能玩出花样来文管理器也能玩出花样来
【7月更文挑战第6天】Python的上下文管理器是资源优雅管理的关键,与with语句结合,确保资源获取和释放。通过实现`__enter__`和`__exit__`,不仅能做资源分配和释放,还能扩展实现如计时、自动重试、事务处理等功能。例如,TimerContextManager类记录代码执行时间,展示了上下文管理器的灵活性。学习和利用这一机制能提升代码质量,增强功能,是Python编程的必备技巧。
33 0
|
Go
腥风血雨中,这招救了我的代码!
腥风血雨中,这招救了我的代码!
57 0
|
Java 程序员 开发者
只用一行代码,你能玩出什么花样?
只用一行代码,你能玩出什么花样?
95 1
|
Python
一日一技:你的代码是如何被炫技毁掉的
一日一技:你的代码是如何被炫技毁掉的
104 0
|
数据安全/隐私保护
推荐5个神仙软件,个个让你爱不释手
最近陆陆续续收到好多小伙伴的咨询,这边也是抓紧时间整理出几个好用的软件,希望可以帮到大家。
198 0
|
JSON Java 测试技术
如何写出让人抓狂的代码?
如何写出让人抓狂的代码?
如何写出让人抓狂的代码?
|
开发工具 git Python
有了它,Python编码再也不为字符集问题而发愁了!
不论是什么编程语言,都免不了涉及到字符集的问题,我们经常在读写本文、获取网页数据等等各类情景下,需要和字符集编码打交道。这几天在公司就遇到了这么一个问题,由于软件需要初始化许多参数信息,所以使用ConfigParser模块进行配置文件的读写操作。本来一切OK,但当把这些.ini配置文件提交到git仓库后,再次下载使用时,默认的utf-8字符集编码,被git默认修改成了gbk编码。导致读取配置文件时默认使用的utf-8编码,最终导致异常报错。那么该如何解决读取文件时的字符集问题呢?Python有专门的字符集检测模块chardet,今天就带大家一起学习下它。
193 0
|
存储 缓存 安全
哦!这该死的 C 语言!(二)
C 语言是一门抽象的、面向过程的语言,C 语言广泛应用于底层开发,C 语言在计算机体系中占据着不可替代的作用,可以说 C 语言是编程的基础,也就是说,不管你学习任何语言,都应该把 C 语言放在首先要学的位置上。
哦!这该死的 C 语言!(二)