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

时间:2010-08-31 14:32:37

标签: java unicode normalization unicode-normalization file-encodings

在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 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.encoding系统属性以及各种LC_CTYPELANG环境变量。什么都没有帮助,我也不想诉诸这些黑客。

this (somewhat related?) question不同,我能够从列出的文件中读取数据,尽管有奇怪的名字

6 个答案:

答案 0 :(得分:16)

使用Unicode,有多种有效的方式来表示相同的字母。 您在Tricky名称中使用的字符是“带有抑扬符号的拉丁文小写字母”和“带有上方环的拉丁文小写字母”。

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

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

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

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

有关组合与分解形式的一般性讨论,请参阅维基百科关于\u00EE的文章,其中特别提到了OS X.

有关转换表单的信息,请参阅Apple的Tech Q& A Unicode Equivalence(不幸的是在Objective-C中)。

Apple的java-dev邮件列表中的QA1235可能对您有所帮助。

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

答案 1 :(得分: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实现,例如reflection-based hackHow does this normalize function work?

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

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

  • 混淆的原因是文件名是两种标准化形式之一,无法直接比较:标准化形式规范分解(NFD)或规范化形式规范组合(NFC)。前者倾向于使用ASCII字母后跟"修饰符"添加重音等,而后者只有扩展字符,没有ACSCII前导字符。阅读维基页面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's documentation下面的引文,文件名以分解(NFD)形式存储在HFS Plus文件系统中:

      

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

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

答案 2 :(得分:1)

我之前见过类似的东西。将文件从Mac上传到webapp的人使用的文件名为é。

a)在操作系统中,char是正常的e +“sign for'应用于前一个char”

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

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

希望它有所帮助!

答案 3 :(得分:0)

在Unix文件系统上,文件名实际上是以空字符结尾的byte []。因此,java运行时必须在createNewFile()操作期间执行从java.lang.String到byte []的转换。字符到字节的转换由语言环境控制。我一直在测试将LC_ALL设置为en_US.UTF-8en_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字符(或UTF-8中的C3 AE)与UTF-8中的序列i + \ u0302(69 CC 82之间的等价性)。 \ u0302是一种组合变音符号(结合旋律重音)。在文件创建期间发生了某种规范化。我不确定它是在Java运行时还是操作系统中完成的。

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

答案 4 :(得分:0)

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

您可以使用VM参数-encoding

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

这样生成的.class文件最终会包含正确的字符,您也可以创建并列出正确的文件名。

答案 5 :(得分:-2)

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