从Windows和Linux读取文件会得到不同的结果(字符编码?)

目前我正在尝试读取一个MIME格式的文件,其中包含一些PNG的二进制string数据。

Windows中,读取文件给了我适当的二进制string,这意味着我只是复制string并将扩展名更改为PNG,我看到图片。


在Windows中读取文件后的示例如下:

--fh-mms-multipart-next-part-1308191573195-0-53229 Content-Type: image/png;name=app_icon.png Content-ID: "<app_icon>" content-location: app_icon.png ‰PNG 

等…等…

在Linux下读取文件后的一个例子如下:

  --fh-mms-multipart-next-part-1308191573195-0-53229 Content-Type: image/png;name=app_icon.png Content-ID: "<app_icon>" content-location: app_icon.png �PNG 

等…等…


我无法将Linux版本转换为图片,因为它们都变成了一些时髦的符号,有很多颠倒的“?” 和“1/2”符号。

任何人都可以启发我正在发生的事情,也许提供一个解决scheme? 现在玩了一个多星期的代码。

是三个字符的序列 – 0xEF 0xFFFD ,并且是Unicode代码点0xFFFD UTF-8表示。 代码本身就是非法UTF-8序列的替代字符 。

显然,出于某种原因,你的源代码(在Linux上)所涉及的一系列例程不正确地处理PNG头文件。 PNG头文件以字节0x89开头(后面跟着0x4E0x47 ),在Windows中正确处理(可能将文件视为CP1252字节序列)。 在CP1252中 , 0x89字符显示为

然而,在Linux上,这个字节被一个UTF-8例程(或者一个认为把文件作为一个UTF-8序列处理的好的库)解码。 由于它自己的0x89不是ASCII-7范围内的有效代码点(ref: UTF-8编码方案 ),因此无法将其映射到0x00-0x7F范围内的有效UTF-8代码点。 此外,它不能映射到一个有效的代码点,表示为一个多字节的UTF-8序列,对于所有的多字节序列,最小2位设置为1( 11.... ),因为这是文件的开始,它也不能作为连续字节。 由此产生的行为是,UTF-8解码器现在用UTF-8替换字符0xEF 0xBF 0xBD (考虑到该文件不是以UTF-8开头)替换0x89 ,这将在ISO-8859中显示-1如同。

如果您需要解决此问题,则需要在Linux中确保以下内容:

  • 读取PNG文件中的字节,使用合适的文件编码(即不是UTF-8); 如果您正在将文件作为一系列字符*读取,则显然这是必要的,而如果您单独读取字节,则不需要。 你可能正在做这个,所以这也是值得验证后续的步骤。
  • 当您查看文件的内容时,请使用合适的编辑器/视图,该编辑器/视图不会对文件执行任何内部解码以获得一系列UTF-8字节。 使用合适的字体也将有所帮助,因为您可能想要防止前所未有的字形(对于0xFFFD实际上是钻石字符 )无法表示,并且可能会导致进一步的变化(不太可能,但您永远不知道编辑器/查看器已被写入)。
  • 也许是用合适的编码(ISO-8859-1)而不是UTF-8编写文件(如果你这样做的话)。 如果您将文件内容作为字节而不是字符处理和存储在内存中,那么将它们写入输出流(不包含任何字符串或字符引用)就足够了。

*显然,如果将字节序列转换为字符或字符串对象,则Java运行时将执行将字节序列解码为UTF-16码点。

在Java中, Stringbyte[]

  • byte[]表示原始二进制数据。
  • String表示文本,它具有关联的字符集/编码,以便能够分辨它所表示的字符。

二进制数据≠文本

String文本数据具有Unicode / UTF-16作为字符集/编码(或序列化时为Unicode / mUTF-8)。 每当你从一个不是String转换成String或反之亦然时,你需要为非String文本数据指定一个字符集/编码(即使你使用平台的默认字符集隐式地进行编码)。

PNG文件包含表示图像(和相关元数据)的原始二进制数据,而不是文本。 因此,你不应该把它当作文本。

\x89PNG不是文字,只是识别PNG文件的“魔术”标题。 0x89甚至不是一个字符,它只是一个任意的字节值,并且它唯一的显示表达式是\x89 ,…类似的, PNG实际上存在二进制数据,也可能是0xdeadbeef而且它什么都不会改变。 PNG碰巧是人类可读的事实只是一个方便。

你的问题来自于你的协议混合了文本和二进制数据,而Java(不同于其他一些语言,比如C)不同于文本处理二进制数据。

Java提供*InputStream读取二进制数据, *Reader读取文本。 我看到两种处理输入的方法:

  • 把一切都当成二进制数据。 当你阅读一个完整的文本行时,使用适当的字符集/编码将其转换为String
  • InputStreamReader之上InputStream ,在需要二进制数据时直接访问InputStreamReader当需要文本时访问InputStreamReader

你可能想缓冲,把它放在第二种情况下的正确位置是*Reader下面。 如果你使用了BufferedReaderBufferedReader可能会比InputStream消耗更多的输入。 所以,你会有这样的事情:

  ┌───────────────────┐ │ InputStreamReader │ └───────────────────┘ ↓ ┌─────────────────────┐ │ BufferedInputStream │ └─────────────────────┘ ↓ ┌─────────────┐ │ InputStream │ └─────────────┘ 

您将使用InputStreamReader读取文本,然后使用BufferedInputStream从同一个流中读取适当数量的二进制数据。

一个有问题的情况是将"\r" (旧的MacOS)和"\r\n" (DOS / Windows)识别为行结束符。 在这种情况下,你最终可能会读一个字符太多。 您可以采取不赞成的DataInputStream.readline()方法所采用的方法:将内部InputStream透明地包装到PushbackInputStream并且不读取该字符。

但是,由于您看起来没有内容长度 ,因此我会推荐第一种方法,将所有内容视为二进制文件,并在读取完整行后才转换为String 。 在这种情况下,我会将MIME分隔符视为二进制数据。

输出:

既然你正在处理二进制数据,你不能println()它。 PrintStreamwrite()方法可以处理二进制数据(例如:输出到二进制文件)。

或者,也许您的数据必须在将其视为文本的频道上传输。 Base64被设计用于确切的情况(将二进制数据作为ASCII文本传输)。 Base64编码形式仅使用US_ASCII字符,所以您应该可以将它用于US_ASCII(ISO-8859- *,UTF-8,CP-1252,…)的超集中的任何字符集/编码。 由于您正在将二进制数据转换为文本/来自文本,所以Base64唯一合理的API应该是这样的:

 String Base64Encode(byte[] data); byte[] Base64Decode(String encodedData); 

这基本上是内部java.util.prefs.Base64使用的。

结论:

在Java中, Stringbyte[]

二进制数据≠文本