繁体   English   中英

Java 字节 stream 非英文字符

[英]Java byte stream non english characters

我读了这段代码 作为 xanadu.txt 内容使用“测试”。 该文件有 4 个字节大小。 如果我使用调试每次运行一个字节的out.write(c)并且在每次打开文件 outagain.txt(使用记事本)后我依次看到:t-->te-->tes-->test。 好的,但是如果我们将源文件 (xanadu.txt) 的内容更改为等同于测试 (τέστ) 的希腊语(或其他语言),那么该文件现在有 8 个字节大小(我认为因为 UTF 我们每个字符有 2 个字节)。 再次调试时,每次out.write(c)运行时,它都会出现没有意义的象形文字。 当最后一个字节(第 8 个)打印出来时,原始的希腊词 (τέστ) 突然出现。 为什么? 如果我们选择控制台 stream (in.netbeans) 作为目标,则相同,但在这种情况下,如果调试,奇怪的字符将保留在末尾,但如果我们正常运行它则不会(.!!)。

如您所见,单个字符(Java 内部表示中的 16 位)变成字节流表示中的可变字节数,特别是 UTF-8。

(有些字符占用两个char值;我将忽略那些,但答案仍然适用,只是更多)

如果您像在实验中那样输出“按字节”,在某些情况下,您将得到 output 一个小数字符。 这是一个没有意义的非法序列; 尽管如此,某些软件(例如记事本)仍会尝试理解它。 这甚至可能包括猜测编码。 例如,我不知道是这种情况,但如果文件的前几个字节无效 UTF-8——我们知道你的半个字符 output 无效 UTF-8——记事本可能会猜测一种完全不同的编码,它将字节序列视为完全不同字符的有效表示。

tl; dr - 垃圾输出,垃圾显示。

现代计算机有一张巨大的表,里面有 40 亿个字符。 每个字符都由一个 32 位数字标识。 你能想到的角色都在这里; 从基本的“测试”到“τέστ”再到雪人 (☃),再到表示即将出现从右到左拼写的单词的特殊不可见连字,再到一堆连字(例如 ff - 这是一个单一的表示 ff 连字的字符)到表情符号、彩色和所有:。

整个答案本质上是这些 32 位数字的序列。 但是您想如何将这些存储在文件中? 这就是“编码”的用武之地。有很多很多编码,一个关键问题是(几乎)没有任何编码是“可检测的”。

就像这样:

如果一个完全陌生的人走到你面前说“嘿”? 他们说什么语言。 可能是英语,但也许是荷兰语。 其中还有“嘿”。 也可能是日本人,他们甚至都不问候你? 他们说“是”(或多或少)。 你怎么知道的?

答案是,要么来自外部环境(如果您在英国纽卡斯尔中部,可能是英语),要么因为他们明确告诉您,但一个是外部环境,另一个不常见。

文本文件也是如此

它们只包含编码的文本,它们不指示它是什么编码。 这意味着您需要在保存该 txt 内容时告诉编辑器或您的newBufferedReader java,或您的浏览器,您想要什么编码。 然而,因为每次都必须这样做很烦人,所以大多数系统都有一个默认选择。 一些文本编辑器甚至试图弄清楚它是什么编码,但就像那个人说“嘿”,对你来说可能是英语或日语,有着截然不同的解释。 这种对字符集编码的半智能猜测也会发生同样的情况。

这让我们得到以下解释:

  1. 您在编辑器中τέστ并点击“保存”。 你的编辑在做什么? 它保存在 UTF-16 中吗? UTF-8? UCS-4? ISO-8859-7? 对于所有这些编码,都会生成完全不同的文件,因为它有 8 个字节。 这意味着它是 UTF-16 或 UTF-8。可能是 UTF-8。

  2. 然后,您将这些字节一个一个地复制过来,这是有问题的:在 UTF-8 中,一个字节可以是一个字符的一半。 (你说:UTF-8 将字符存储为 2 个字节;那不是真的,UTF-8 存储字符时每个字符都是 1、2、3 或 4 个字节;它的每个字节长度可变,- τέστ 中的每个字符存储为 2 个字节,虽然) - 这意味着如果你已经复制了 3 个字节:你的文本编辑器猜测它可能是什么的能力受到严重阻碍,它可能会猜测 UTF-8 但随后意识到它根本无效 UTF-8 (因为你最后得到的半个字符),所以它猜错了。 并向您展示官方文章。

这里要吸取的教训是双重的:

  1. 当你想处理字符时,使用charReaderWriterString和其他面向字符的东西。

  2. 当你想处理字节时,使用bytebyte[]InputStreamOutputStream和其他面向字节的东西。

  3. 永远不要犯这两者很容易互换的错误,因为它们不是。 每当您从一个“世界”到另一个“世界”的 go 时,您必须指定字符集编码,因为如果没有,java 会选择您不想要的“平台默认值”(因为现在您的软件依赖于外部因素并且不能被测试。哎呀)。

  4. 尽你所能默认为 UTF-8。

tl;博士

阅读: 每个软件开发人员绝对、绝对必须了解 Unicode 和字符集的绝对最低限度(没有借口!)

不要按八位字节(字节)解析文本文件。 使用专门用于处理文本的类。 例如,使用Files及其readAllLines方法。

细节

请注意该教程页面底部的警告,这不是处理文本文件的正确方法:

CopyBytes 看起来像一个普通的程序,但它实际上代表了一种您应该避免的低级 I/O。 由于 xanadu.txt 包含字符数据,因此最好的方法是使用字符流,这将在下一节中讨论。

文本文件可能会也可能不会使用单个八位字节来表示单个字符,例如US-ASCII文件。 您的示例代码假定每个字符一个八位字节,它适用于test作为内容,但不适用于τέστ作为内容。

作为程序员,您必须从数据文件的发布者那里知道在编写代表原始文本的数据时使用了何种编码。 一般写文字最好使用UTF-8编码。

用两行写一个文本文件:

测试 τέστ

...并使用编码为UTF-8文本编辑器保存。

将该文件作为String对象的集合读取。

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

运行时:

line = test
line = τέστ

UTF-16 与 UTF-8

你说:

我认为因为 UTF 我们每个字符有 2 个字节)

没有“UTF”之类的东西。

  • UTF-16编码对每个字符使用一对或多对八位字节。
  • UTF-8编码每个字符使用 1、2、3 或 4 个八位字节。

可以使用 UTF-16 或 UTF-8 编码将诸如τέστ之类的文本内容写入文件。请注意, UTF-16“被认为是有害的” ,如今通常首选 UTF-8。 请注意,UTF-8 是 US-ASCII 的超集,因此任何 US-ASCII 文件也是 UTF-8 文件。

字符作为代码点

如果您想对文本中的每个字符进行示例,请将它们视为代码点编号。

切勿在 Java 中使用char类型。该类型甚至无法表示Unicode中定义的一半字符,现在已过时。

我们可以通过添加这两行代码来查询上面示例文件中的每个字符。

IntStream codePoints = line.codePoints();
codePoints.forEach( System.out :: println );

像这样:

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
        IntStream codePoints = line.codePoints();
        codePoints.forEach( System.out :: println );
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

运行时:

line = test
116
101
115
116
line = τέστ
964
941
963
964

如果您还不熟悉流,请将IntStream转换为集合,例如Integer对象的List

Path path = Paths.get( "/Users/basilbourque/some_text.txt" );
try
{
    List < String > lines = Files.readAllLines( path , StandardCharsets.UTF_8 );
    for ( String line : lines )
    {
        System.out.println( "line = " + line );
        List < Integer > codePoints = line.codePoints().boxed().collect( Collectors.toList() );
        for ( Integer codePoint : codePoints )
        {
            System.out.println( "codePoint = " + codePoint );
        }
    }
}
catch ( IOException e )
{
    e.printStackTrace();
}

运行时:

line = test
codePoint = 116
codePoint = 101
codePoint = 115
codePoint = 116
line = τέστ
codePoint = 964
codePoint = 941
codePoint = 963
codePoint = 964

给定代码点编号,我们可以确定预期的字符

字符串 s = Character.toString( 941 ); // 字符。

请注意,某些文本字符可能表示为多个代码点,例如带有变音符号的字母。 (文本处理不是一件简单的事情。)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM