File.listFiles() 使用 JDK 6 修改 Unicode 名称(Unicode 规范化问题)

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

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

下面是一个要演示的程序。它创建一个具有 Unicode 名称的文件,然后打印出从直接创建的 File 获取的文件名的 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 Snow Leopard:

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.encodingLC_CTYPELANG

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


答案 1

使用 Unicode,有多种方法可以表示同一个字母。您在棘手名称中使用的字符是“带有 circumflex 的拉丁小写字母 i”和“上面有环的拉丁小写字母 a”。

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

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

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

OS X HFS Plus 宗卷将字符串(例如文件名)存储为“完全分解”。Unix文件系统实际上是根据文件系统驱动程序选择如何存储它来存储的。你不能在不同类型的文件系统上做任何一揽子声明。

请参阅维基百科上关于Unicode等效性的文章,了解组合形式与分解形式的一般讨论,其中特别提到了OS X。

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

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

基本上,您需要先将分解的表单规范化为组合形式,然后才能比较字符串。


答案 2

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

感谢斯蒂芬·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 6及更高版本中可用,因此如果您需要使用Java 5进行编译,则可能必须诉诸于实现和类似这种基于反射的hack的东西 另请参阅此规范化函数如何工作?java.text.Normalizersun.text.Normalizer

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

以下是我在这次肮脏的冒险中学到的其他有趣的事情。

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

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

    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
    
  • 如果使用字符串名称构造对象,则该方法将以您最初为其提供的任何形式返回该名称。但是,如果调用自行发现名称的方法,则它们似乎以 NFC 形式返回名称。这可能是一个令人讨厌的问题。这当然是骗人的。FileFile.getName()File

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

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

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


推荐