Java字节流非英文字符

Java byte stream non english characters

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

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

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

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

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

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

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

是这样的:

如果一个完全陌生的人走到你面前说“嘿!”,他们说的是什么语言?应该是英文吧但也许是荷兰语,它也有 'Hey!'。也可能是日语,他们甚至没有问候你,他们说 'Yes'(或多或少)。你怎么知道?

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

文本文件同理.

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

这让我们得到以下解释:

  1. 您在编辑器中输入 τέστ,然后点击 'save'。你的编辑在做什么?它保存在 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(因为你最终得到的是半个字符),所以它猜错了,并向你展示了 gobbledygook。

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

  1. 当你要处理字符时,使用charReaderWriterString等面向字符的东西。

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

  3. 切勿 误以为这两者很容易互换,因为它们不是。每当你从一个 'world' 转到另一个时,你必须指定字符集编码,因为如果没有,java 会选择你不想要的 'platform default'(因为现在你有依赖于关于外部因素,无法测试。哎呀)。

  4. 尽可能默认为 UTF-8。

tl;博士

阅读:The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

不使用octets (bytes). Use classes purpose-built for handling text. For example, use Files and its readAllLines方法解析文本文件。

详情

请注意 that tutorial page 底部的警告,这不是处理文本文件的正确方法:

CopyBytes seems like a normal program, but it actually represents a kind of low-level I/O that you should avoid. Since xanadu.txt contains character data, the best approach is to use character streams, as discussed in the next section.

文本文件可以使用也可以不使用单个八位字节来表示单个字符,例如 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

你说:

I think because UTF we have 2 bytes per character)

没有“UTF”这样的东西。

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

τέστ 等文本内容可以使用 UTF-16 或 UTF-8 编码写入文件。请注意 UTF-16 is “considered harmful”,现在通常首选 UTF-8。请注意,UTF-8 是 US-ASCII 的超集,因此任何 US-ASCII 文件也是 UTF-8 文件。

字符作为代码点

如果您想对文本中的每个字符进行示例,请将它们视为 code point 个数字。

切勿在 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

如果您还不熟悉流,convert IntStream to a collection,例如 ListInteger 个对象。

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 ) ; // έ 字符.

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