36

在 OS X 和 Linux 上的 Java 6 中列出目录内容时,我遇到了一个奇怪的文件名编码问题:File.listFiles()和相关的方法似乎返回的文件名与系统其余部分的编码不同。

请注意,导致我出现问题的不仅仅是这些文件名的显示。我主要对文件名与远程文件存储系统的比较感兴趣,所以我更关心名称字符串的内容,而不是用于打印输出的字符编码。

这是一个演示程序。它创建一个具有 Unicode 名称的文件,然后打印出从直接创建的文件中获得的文件名的URL 编码版本,以及列在父目录下的相同文件(您应该在空目录中运行此代码)。结果显示该File.listFiles()方法返回的不同编码。

String fileName = "Trîcky Nåme";
File file = new File(fileName);
file.createNewFile();
System.out.println("File name: " + URLEncoder.encode(file.getName(), "UTF-8"));

// Get parent (current) dir and list file contents
File parentDir = file.getAbsoluteFile().getParentFile();
File[] children = parentDir.listFiles();
for (File child: children) {
    System.out.println("Listed name: " + URLEncoder.encode(child.getName(), "UTF-8"));
}

这是我在系统上运行此测试代码时得到的结果。注意%CC%C3字符表示。

OS X 雪豹:

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)

KUbuntu Linux(在同一 OS X 系统上的 VM 中运行):

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1)
OpenJDK Client VM (build 16.0-b13, mixed mode, sharing)

我尝试了各种技巧来让字符串达成一致,包括设置file.encoding系统属性和各种LC_CTYPE环境LANG变量。没有什么帮助,我也不想诉诸这样的黑客。

这个(有点相关?)问题不同,尽管名称很奇怪,我仍然能够从列出的文件中读取数据

4

6 回答 6

16

使用 Unicode,表示同一个字母的有效方式不止一种。您在 Tricky Name 中使用的字符是“带有抑扬符的拉丁小写字母 i”和“带有上方圆环的拉丁小写字母 a”。

你说“注意%CC%C3字符的表示”,但仔细观察你看到的是序列

i 0xCC 0x82 vs. 0xC3 0xAE
a 0xCC 0x8A vs. 0xC3 0xA5

也就是说,第一个是字母i后跟 0xCC82,它是Unicode\u0302 “组合抑扬符”字符的 UTF-8 编码,而第二个是 UTF-8 表示\u00EE“带有抑扬符的拉丁小写字母 i”。对于另一对类似,第一个是字母a后面跟着 0xCC8A 的“组合上面的环”字符,第二个是“拉丁小写字母 a 上面的环”。这两种都是有效 Unicode 字符串的有效 UTF-8 编码,但一种是“组合”格式,另一种是“分解”格式。

OS X HFS Plus 卷将字符串(例如文件名)存储为“完全分解”。Unix 文件系统实际上是根据文件系统驱动程序选择存储它的方式来存储的。您不能在不同类型的文件系统中做出任何笼统的陈述。

有关组合形式与分解形式的一般讨论,请参阅有关Unicode Equivalence的 Wikipedia 文章,其中特别提到了 OS X。

有关转换表单的信息,请参阅 Apple 的技术问答QA1235 (不幸的是在 Objective-C 中)。

Apple 的 java-dev 邮件列表中最近的一个电子邮件线程可能对您有所帮助。

基本上,在比较字符串之前,您需要将分解的形式规范化为组合形式。

于 2010-09-01T00:33:00.037 回答
2

从问题中提取的解决方案:

感谢 Stephen P 让我走上正轨。

首先修复,对于不耐烦的人。如果您使用 Java 6 进行编译,您可以使用java.text.Normalizer类将字符串规范化为您选择的常用形式,例如

// Normalize to "Normalization Form Canonical Decomposition" (NFD)
protected String normalizeUnicode(String str) {
    Normalizer.Form form = Normalizer.Form.NFD;
    if (!Normalizer.isNormalized(str, form)) {
        return Normalizer.normalize(str, form);
    }
    return str;
}

由于java.text.Normalizer仅在 Java 6 及更高版本中可用,因此如果您需要使用 Java 5 进行编译,您可能不得不求助于sun.text.Normalizer实现以及类似这种基于反射的 hack参见此规范化功能如何工作?

仅此一项就足以让我决定不支持使用 Java 5 编译我的项目:|

这是我在这次肮脏的冒险中学到的其他有趣的东西。

  • 混淆是由于文件名处于无法直接比较的两种规范化形式之一:规范化形式规范分解 (NFD) 或规范化形式规范组合 (NFC)。前者往往有 ASCII 字母后跟“修饰符”以添加重音等,而后者只有扩展字符而没有 ASCII 前导字符。阅读 wiki 页面 Stephen P 参考资料以获得更好的解释。

  • 示例代码中包含的 Unicode 字符串文字(以及在我的真实应用程序中通过 HTTP 接收的文字)采用 NFD 形式,而该File.listFiles()方法返回的文件名是 NFC。以下迷你示例演示了这些差异:

    String name = "Trîcky Nåme";
    System.out.println("Original name: " + URLEncoder.encode(name, "UTF-8"));
    System.out.println("NFC Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFC), "UTF-8"));
    System.out.println("NFD Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFD), "UTF-8"));
    

    输出:

    Original name: Tri%CC%82cky+Na%CC%8Ame
    NFC Normalized name: Tr%C3%AEcky+N%C3%A5me
    NFD Normalized name: Tri%CC%82cky+Na%CC%8Ame
    
  • 如果您File使用字符串名称构造对象,则该File.getName()方法将以您最初提供的任何形式返回名称。但是,如果您调用File自行发现名称的方法,它们似乎会以 NFC 形式返回名称。这可能是一个令人讨厌的问题。它肯定得到了。

  • 根据Apple 文档中的以下引用,文件名以分解 (NFD) 形式存储在 HFS Plus 文件系统上:

    在 Mac OS 中工作时,您会发现自己混合使用了预组合和分解的 Unicode。例如,HFS Plus 将所有文件名转换为分解的 Unicode,而 Macintosh 键盘通常会生成预分解的 Unicode。

    因此,该File.listFiles()方法有助于(?)将文件名转换为(预)组合(NFC)形式。

于 2015-09-19T03:33:52.420 回答
1

我以前见过类似的东西。将文件从 Mac 上传到 web 应用程序的人使用带有 é 的文件名。

a) 在操作系统中 char 是正常的 e + "sign for ´ 应用于前一个 char"

b) 在 Windows 中,它是一个特殊的字符:é

两者都是Unicode。所以...我知道您将 (b) 选项传递给 File create 并且在某些时候 Mac OS 将其转换为 (a) 选项。也许如果您在互联网上发现双重表示问题,您可以获得一种成功处理这两种情况的方法。

希望能帮助到你!

于 2010-08-31T14:51:17.913 回答
0

在 Unix 文件系统上,文件名实际上是以空字符结尾的字节 []。因此,java 运行时必须在 createNewFile() 操作期间执行从 java.lang.String 到 byte[] 的转换。字符到字节的转换由语言环境控制。我一直在测试设置LC_ALLen_US.UTF-8获得en_US.ISO-8859-1一致的结果。这是 Sun (...Oracle) java 1.6.0_20。但是,对于LC_ALL=en_US.POSIX,结果是:

File name:   Tr%C3%AEcky+N%C3%A5me
Listed name: Tr%3Fcky+N%3Fme

3F是一个问号。它告诉我非 ASCII 字符的转换不成功。话又说回来,一切都如预期的那样。

但是你的两个字符串不同的原因是因为 \u00EE 字符(或C3 AE在 UTF-8 中)和序列 i+\u0302 (69 CC 82在 UTF-8 中)之间的等价性。\u0302 是一个组合变音符号(组合抑扬音符号)。在文件创建期间发生了某种规范化。我不确定它是在 Java 运行时还是操作系统中完成的。

注意:我花了一些时间才弄清楚,因为您发布的代码片段没有组合变音符号,而是等效字符î(例如\u00ee)。您应该在字符串文字中嵌入了 Unicode 转义序列(但之后很容易说......)。

于 2010-08-31T16:06:30.103 回答
0

我怀疑你只需要指示javac使用什么编码来编译.java包含特殊字符的文件,因为你已经在源文件中硬编码了它。否则将使用平台默认编码,它可能根本不是 UTF-8。

您可以为此使用 VM 参数-encoding

javac -encoding UTF-8 com/example/Foo.java

这样,生成的.class文件最终将包含正确的字符,您也将能够创建和列出正确的文件名。

于 2010-08-31T19:34:13.957 回答
-2

另一种解决方案是使用新的 java.nio.Path api 代替完美运行的 java.io.File api。

于 2014-03-05T10:04:41.570 回答