0%

网页中查看文件乱码处理方案

对于文件乱码的问题一直有种一头雾水的感觉,最近又碰到了这个问题,正好借着这个机会学习一下文件编码相关的知识。

问题描述

网站中包含一个查看文件的功能,目前文件是通过 layer 弹窗的形式来展示,但测试下来部分文件会出现乱码的情况,如下:

picture 3

这个问题根本原因是:打开文件时使用的编码,与文件保存时使用的编码,两个编码不一致导致的。

上面测试的几个出问题的文件,编码方式都是 GBK,而浏览器默认使用 UTF-8 编码打开文件,编码和解码规则不同,因此出现了乱码。

这里放上 IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质 这篇文章中关于乱码的描述:

picture 4

要解释乱码的问题,首先要理解两个关键概念:字符集字符编码。拿上面的 UTF-8GBK 编码为例,它们使用的字符集和字符编码分别是:

  • **GBK**。字符集:GBK 字符集;字符编码:GBK 编码。
  • **UTF-8**。字符集:Unicode 字符集;字符编码:UTF-8 编码。

字符集 中定义了所有可使用的字符,它包含了 字符字符对应的代码点不同字符集对字符的代码点计算方式是不同的,所以同一个字符在不同 [字符集] 中对应的代码点是不同的。还是拿 UTF-8GBK 为例:

  • GBK 字符集 主要覆盖中文。字符 [你]GBK 字符集中的代码点是 0xC4E3
  • Unicode 字符集 是一个全球字符集,它覆盖所有已知的书写系统,其中 GBK 字符集中的所有字符它也全部包含。字符 [你]Unicode 字符集中的代码点是 U+4F60

字符编码 是将字符集中的代码点转换为字节序列的一种方式,计算机能够存储和处理这些字符。不同的字符编码可以对同一个字符集进行编码,比如 UTF-8UTF-16UTF-32 三种字符编码使用的都是 Unicode 字符集。但三种字符编码不能兼容,如果使用 UTF-8 编码,使用 UTF-16 进行解码,会出现乱码的情况。

关于字符编码更多的知识,这里就不深入了,比如上面的案例中,还可以继续深究:为何只有中文部分乱码了,英文、数字、符号都是能够正常展示的?如果想深入了解建议阅读一下下面几篇文章:

  1. 字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8
  2. 史诗级计算机字符编码知识分享,万字长文,一文即懂!
  3. IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质
  4. Unicode与JavaScript详解

附:在 Usage statistics of character encodings for websites 这个网站统计了目前各种字符编码被使用的比例,如下:
picture 5

处理思路

经过上面对乱码原因的分析,针对页面文件渲染时的乱码问题,有如下两种处理思路:

  1. 在渲染文件内容前,想办法确认文件的字符编码。
  2. 保存文件时,使用统一的编码进行保存,比如统一按照 UTF-8 格式保存。

经过调查,服务端和网页端都可以处理,下面将可行的方案列出来。

网页端处理html 代码中可以通过指定 <meta charset="xxx"> 的形式来指定网页的编码,如果要加载 GBK 编码的文件,那么就指定 charset="GBK" 就可以了。但是在指定 charset 前,需要先确定文件的编码。有如下几种方式可以尝试:

  1. 通过第三方库检测文件编码,比如 jschardet
  2. 请求文件 url 返回内容为字节流信息,然后先将文件通过 utf-8 编码读取出来,判断内容中是否包含乱码字符(�),如果包含再将文件转换为 GBK 编码。(注:其他编码格式的文件还是会出现乱码的问题
  3. 文件编码由用户自己判断。在页面中增加一个选择框,供用户选择加载文件时使用的字符编码。默认使用 utf-8 加载文件,如果用户发现有乱码的情况,那么自行切换编码格式。
  4. 请求文件 url 返回内容为文本信息(使用默认的编码,也就是 UTF-8),之后在 JS 端将乱码的内容恢复。(尝试过后发现无法这么处理,这个步骤相当于文件先用 GBK 编码,再用 UTF-8 解码,之后再用 GBK 编码,这么一套操作下来整个编码全乱了)

    乱码恢复指北 这篇文章中看到,�这类字符是通过 UTF-8 读取 GBK 编码中文内容时,UTF-8 编码将无法显示的字符替换为该符号,对应的码位为 0xFFFD,类似的还是 [锟斤拷] 这类字符(用 UTF-8 读取 GBK 编码的文件,再用 GBK 读取),这类字符是无法恢复了。
    picture 6
    picture 7

服务端处理

  1. 存储文件时(txtdoc 等文本文件),文件编码统一使用 utf-8 格式进行存储。(尝试过后发现,在 Java 中将 MultipartFile 使用统一字符编码存储时,还是需要知道原文件的编码,否则强行存储会出现乱码情况)
  2. 存储文件时,获取文件的编码格式,将编码格式拼接到 url 尾部,之后读取时根据这个编码格式进行加载。

    注:服务端获取文件编码,可以通过 Apache Tika 库来处理

  3. 访问文件 url 目前是请求服务端的 rsdun-articles 服务,在该服务中增加处理,访问文本类文件时,获取文件的编码格式,并直接指定返回响应中的 contentType 为对应的编码,如下:
    picture 8
    picture 9

在尝试上面几个方案时,碰到了很多坑。比如文件目前是通过 layer.open() 打开的,这里开启的是一个子 iframe 页面,如果想将在页面头部添加 <meta charset='GBK'> 这个标识,那么只能在页面加载完成后获取子 iframedocument 属性,然后将这个 charset 标识插入到页面中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
layer.open({
type: 2,
title: false,
area: ['1000px', '700px'],
shade: 0.5,
closeBtn: 0,
shadeClose: true,
content: link,
success: function(layero, index) {
var iframe = layero.find('iframe')[0];
// 管理页面的地址是 {ip:8301},文件地址是 {ip},这会出现跨域问题
var doc = iframe.contentDocument || iframe.contentWindow.document;
var meta = doc.createElement('meta');
meta.setAttribute('charset', 'GBK');
doc.getElementsByTagName('head')[0].appendChild(meta);
}
});

但是这样插入会出现跨域问题,导致 charset 标识无法被插入,如下:

picture 10

解决了跨域问题,charset 成功插入到页面中了,但是页面中的文件内容仍然是乱码。搜索了一下,原因如下:浏览器在解析 HTML 时会遵循字符集声明,如果未指定,那么默认是 UTF-8。一旦解析开始,字符集设置是固定的。后续尝试通过 JS 修改字符集不会对已经解析的内容产生影响。

解决办法

结合上面的几个处理思路,最终选用的方案如下:**服务端拦截携带 charset=xxx 后缀的 url(目前进拦截 txt 文件),这些 URL 请求的返回的 ContentType 会被修改为对应的 charset**,如下:

picture 12

网页端仍然通过 layer 弹窗的形式展示文件内容,文件字符编码的检测由用户来判断,在弹窗中增加选择框,选择框中提供常用的字符编码,用户可以自行切换,如下:

picture 13

文件编码选择 [由用户自行判断,而不是由程序自动检测] 的原因,是因为自动检测文件编码的方案并不能100%保证检测结果准确,并且这类方案涉及文件 IO 操作,会影响性能。

注:目前 [即时通讯信息检索] 页面的文件中,只有 txtpdf 类文件可以直接预览(图片、语音、视频除外),其他文件都需要下载后才能查看,出现乱码问题的文件是 txt 类文件,因此上面的处理方案目前仅针对 txt 类文件。

参考文章

从浏览器到服务端的中文乱码深入分析

关于编码的那些事儿

如何在前端识别文件的编码

字符编码那点事:快速理解ASCII、Unicode、GBK和UTF-8

字符编码笔记:ASCII,Unicode 和 UTF-8

Unicode与JavaScript详解

Detect-File-Encoding-And-Language

IM开发基础知识补课(八):史上最通俗,彻底搞懂字符乱码问题的本质

乱码恢复指北