【问题标题】:Why UTF-8 BOM bytes efbbbf can be replaced by \ufeff?为什么 UTF-8 BOM 字节 efbbbf 可以替换为 \ufeff?
【发布时间】:2019-01-18 03:32:31
【问题描述】:

UTF-8byte order mark (BOM)EF BB BF,如 section 23.8 of the Unicode 9 规范中所述(搜索“签名”)。

在 Java 中删除这个的许多解决方案只是一个简单的一行代码:

 replace("\uFEFF", "")

我不明白为什么会这样。

这是我的测试代码。我在调用 String#replace 后检查了二进制文件,我发现 EF BB BF 确实被删除了。看到这个code run live at IdeOne.com

太神奇了。为什么会这样?

@Test
public void shit() throws Exception{
    byte[] b = new byte[]{-17,-69,-65, 97,97,97};//EF BB BF 61 61 61
    char[] c = new char[10];
    new InputStreamReader(new ByteArrayInputStream(b),"UTF-8").read(c);
    byte[] bytes = new StringBuilder().append(c).toString().replace("\uFEFF", "").getBytes();//
    for(byte bt: bytes){//61 61 61, we can see EF BB BF is indeed removed
        System.out.println(bt);
    }
}

【问题讨论】:

  • 您将编码与字符代码点混淆了。此外,在正常使用中,UTF-8 编码的内容不应使用 BOM。

标签: java byte-order-mark


【解决方案1】:

原因是 unicode 文本应该以字节顺序标记开头(UTF-8 除外,它不是推荐强制[1])。

来自维基百科

字节顺序标记 (BOM) 是一个 Unicode 字符,U+FEFF BYTE ORDER MARK (BOM),它在文本流的开头显示为幻数 ...
...
BOM 采用与文档其余部分相同的方案编码 ...

这意味着这个特殊字符 (\uFEFF) 也必须以 UTF-8 编码。

UTF-8 可以将 Unicode 代码点编码为一到四个字节。

  • 可以用7位表示的代码点被编码在一个字节中,最高位始终为零0xxx xxxx
  • 根据位数以多个字节编码的所有其他代码点,第一个字节的左侧设置位表示用于编码的字节数,例如110x xxxx 表示编码由两个字节表示,连续字节总是以10xx xxxx 开头(x 位可用于代码点)

U+0000 - U+007F 范围内的代码点可以用一个字节编码。
U+0080 - U+07FF 范围内的代码点可以用两个字节编码。 U+0800 - U+FFFF 范围内的代码点可以用三个字节编码。

详细解释在Wikipedia

对于 BOM,我们需要三个字节。

hex    FE       FF
binary 11111110 11111111

以 UTF-8 编码位

pattern for three byte encoding 1110 xxxx  10xx xxxx  10xx xxxx
the bits of the code point           1111    11 1011    11 1111
result                          1110 1111  1011 1011  1011 1111
in hex                          EF         BB         BF

EF BB BF 听起来已经很熟悉了。 ;-)

字节序列EF BB BF 就是以UTF-8 编码的BOM。

由于字节顺序标记对 UTF-8 没有意义,因此在 Java 中不使用。

将 BOM 字符编码为 UTF-8

jshell> "\uFEFF".getBytes("UTF-8")
$1 ==> byte[3] { -17, -69, -65 }  // EF BB BF

因此,当读取文件时,字节序列被解码为\uFEFF

用于编码,例如UTF-16 BOM 被添加

jshell> " ".getBytes("UTF-16")
$2 ==> byte[4] { -2, -1, 0, 32 }  // FE FF + the encoded SPACE

[1] 引用自:http://www.unicode.org/versions/Unicode9.0.0/ch23.pdf

虽然有 UTF-8 文本从不存在任何字节顺序问题,这个序列可以作为签名 对于未标记字符集的 UTF-8 编码文本。与 UTF-16 中的 BOM 一样, 此字节序列在其他字符的文本文件开头将极为罕见 编码。

【讨论】:

  • @BasilBourque 不知道这样会误读句子。我现在更清楚了我想说什么。
【解决方案2】:

InputStreamReader 正在将 UTF-8 编码的字节序列 (b) 解码为 UTF-16BE,并在此过程中将 UTF-8 BOM 转换为 UTF-16BE BOM (\uFEFF)。选择 UTF-16BE 作为目标编码是因为 Charset 默认为这种行为:

https://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html

UTF-16 字符集由 RFC 2781 指定;转型 它们所依据的格式在 ISO 修正案 1 中指定 10646-1 并且也在 Unicode 标准中进行了描述。

UTF-16 字符集使用 16 位数量,因此 对字节顺序敏感。在这些编码中,流的字节顺序 可以由表示的初始字节顺序标记表示 Unicode 字符“\uFEFF”。字节序标记处理如下:

解码时,UTF-16BE 和 UTF-16LE 字符集解释 初始字节顺序标记为零宽度非中断空间;什么时候 编码,它们不写字节顺序标记。

解码时,UTF-16 字符集将字节顺序标记解释为 输入流的开头以指示字节顺序 流,但如果没有字节顺序标记,则默认为大端;什么时候 编码,它使用大端字节序并写入大端 字节序标记。

查看JLS 3.1了解为什么String的内部编码是UTF-16:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.1

Java 编程语言以 16 位序列表示文本 代码单元,使用 UTF-16 编码。

String#getBytes() 以平台的默认编码返回一个字节序列,对于您的系统而言,该编码似乎是 UTF-8。

总结

当使用InputStreamReader将字节序列解码为String时,序列EF BB BF (UTF-8 BOM) 被转换为FE FF (UTF-16BE BOM),因为具有默认 Charsetjava.lang.String 的编码是 UTF-16 BE 在存在 BOM 的情况下。替换 UTF-16BE BOM 并调用 String#getBytes() 后,字符串被解码为 UTF-8(您的平台的默认字符集),您会看到没有 BOM 的原始字节序列。

【讨论】:

  • 该语言在哪里证明它是 UTF-16BE,而不是 UTF-16-Host?
  • @Deduplicator 调整答案解释为什么选择 UTF-16BE
猜你喜欢
  • 2012-02-12
  • 2012-02-08
  • 2015-11-30
  • 1970-01-01
  • 1970-01-01
  • 2012-07-19
  • 2021-11-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多