File.listFiles() 使用 JDK 6 破坏 unicode 名称(Unicode 规范化问题)
Posted
技术标签:
【中文标题】File.listFiles() 使用 JDK 6 破坏 unicode 名称(Unicode 规范化问题)【英文标题】:File.listFiles() mangles unicode names with JDK 6 (Unicode Normalization issues) 【发布时间】:2011-04-06 07:37:54 【问题描述】:在 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
环境变量。没有任何帮助,我也不想诉诸此类黑客。
与this (somewhat related?) question 不同,尽管名称很奇怪,我仍可以从列出的文件中读取数据
【问题讨论】:
.java
文件的编码是什么?我认为您可以使用file
命令来确定。
超级优秀的解决方案总结!如果更多人这样做,SO 将是一个更好的目的地。
优秀的帖子!关于 HSF+ 的最后一句话非常好。 Apple 键盘快捷键生成 NFC,但文件系统标准化为 NFD。当您有一个名为"AB"
(拉丁文脚本)的文件、另一个名为"ΑΒ"
(希腊文脚本)和第三个名为"АВ"
(西里尔文脚本)的文件时,它仍然没有帮助。通过 gosh-that's-hard-to-type-ness 谈论安全性。 :) 我曾经有一台机器名为 wraeththu,没有人可以输入登录权限的名称。可能更糟:本可以像原来的那样拼写,在古英语中是 wrǽþþu。 :)
@StephenP 那么你能解释一下为什么 listFiles() 将字符串作为 NFC 返回吗?我很清楚HFS+将它们存储为NFD,那为什么它们不作为NFD返回呢?
我在安装的(远程)NFS 卷上的 Mac OS X(确切地说是 10.9.5)上遇到了同样的问题。我想编写一个实用程序,将 NFD 文件名重命名为 NFC 文件名,并在 NFC 和 NFD 文件名都存在时检测名称冲突(我试图创建这两个文件,在我的系统上是允许的)。但是,Java 总是将 NFC 返还给我。实际上,我必须使用java.nio.file.Paths.get(...)
来返回有效路径,在从java.io.File.listFiles()
返回的元素上调用exists()
可能会返回false
!有什么想法吗?
【参考方案1】:
使用 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编码,第二个是\u00EE
“拉丁小写字母i”的UTF-8带抑扬顿挫”。另一对类似,第一个是字母a
,后跟 0xCC8A“组合上面的环”字符,第二个是“拉丁小写字母 a,上面有环”。这两种都是有效 Unicode 字符串的有效 UTF-8 编码,但一种是“组合”格式,另一种是“分解”格式。
OS X HFS Plus 卷将字符串(例如文件名)存储为“完全分解”。 Unix 文件系统实际上是根据文件系统驱动程序选择存储它的方式来存储的。您不能对不同类型的文件系统做出任何笼统的陈述。
请参阅Unicode Equivalence 上的 Wikipedia 文章,了解组合形式与分解形式的一般讨论,其中特别提到了 OS X。
有关转换表单的信息,请参阅 Apple 的技术问答 QA1235(不幸的是,在 Objective-C 中)。
Apple 的 java-dev 邮件列表中的recent email thread 可能对您有所帮助。
基本上,在比较字符串之前,您需要将分解的形式规范化为组合形式。
【讨论】:
感谢您的出色回答,它让我走上了正轨。请参阅修改后的问题以及我所学内容的摘要和具体解决方案(tl;dr -- 使用 java.text.Normalizer) 我以前遇到过这种问题。很高兴知道它背后的一般理论。谢谢! @James:在一个相关的问题上,我最近认为这就是为什么 Java 的Pattern.CANON_EQ
标志并没有我希望的那么大的帮助。匹配大小写字符加上任何顺序的任何标记似乎要容易得多,匹配字符串NFD("égal")
和模式"e\\pM*gal"
。问题是,如果您从配置文件中读取"e\u0301gal"
或"\u00E9gal"
之类的字符串,它只会在字面上匹配,而不是与CANON_EQ
排列匹配。
您的 Unicode 值错误,例如0xCC82 是 'HANGUL SYLLABLE CYAENH' 不是“组合抑扬符”
@paj28 - 编辑以澄清这些值是 unicode 字符的 utf8 encodings (所以不要使用 \u
前缀,这是错误的) ...0xCC82
是“组合抑扬符”,而\uCC82
是韩文字符。 (前缀 0x 与前缀 \u)【参考方案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 hack 的东西,另请参阅 How does this normalize function work?
仅此一项就足以让我决定不支持使用 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's documentation 的以下引用,文件名以分解 (NFD) 形式存储在 HFS Plus 文件系统上:
在 Mac OS 中工作时,您会发现自己混合使用了预组合和分解的 Unicode。例如,HFS Plus 将所有文件名转换为分解后的 Unicode,而 Macintosh 键盘通常会生成预分解的 Unicode。
因此,File.listFiles()
方法有助于 (?) 将文件名转换为(预)组合的 (NFC) 形式。
【讨论】:
这让我很困惑。虽然我知道字符串应该针对更高级别的应用程序逻辑进行规范化,但文件名的这种强制规范化似乎意味着用两种不同形式编写的文件应该是等效的。但他们不是,不是吗?您可以轻松地拥有两个外观相同但格式不同的文件名,但它们是两个独立的文件,因此应该被视为不同。正确的?那么为什么File
会强迫我们这样做呢? Path
是否有相同的行为?
@DanGravell:好吧,在 OP 处理的文件系统中,文件名被转换为规范化的 unicode。因此,为了进行有用的比较,必须将两个输入标准化为 NFC 或 NFD,它们中的哪一个无关紧要并且不会丢失数据。不过,还有其他文件系统不进行任何此类转换,只需查看标准 Linux 文件系统即可。在那里,进行任何类型的文本规范化都是有损的。
是的,我正在考虑的方案是通过 CIFS 或 NFS 将远程磁盘安装到 Mac 上。我一直在看这个,File
使用 NFC 报告文件名,即使我已将文件重命名为 NFD。随后的exists()
失败。我是在做一些愚蠢的事情还是忽略了一些事情?我不禁认为File
在这方面是坏的。【参考方案3】:
另一种解决方案是使用新的 java.nio.Path api 代替完美运行的 java.io.File api。
【讨论】:
【参考方案4】:我怀疑您只需要指示javac
使用什么编码来编译包含特殊字符的.java
文件,因为您已经在源文件中对其进行了硬编码。否则将使用平台默认编码,可能根本不是UTF-8。
您可以为此使用 VM 参数 -encoding
。
javac -encoding UTF-8 com/example/Foo.java
这样,生成的.class
文件最终将包含正确的字符,您也可以创建和列出正确的文件名。
【讨论】:
虽然 Unicode 文件名被硬编码到示例程序中,但我的真实程序从文件系统或 Web 服务中获取数据。不涉及硬编码字符串。我确实在我的示例代码中尝试了-encoding
选项,但在那里也没有任何区别。
在被烧了太多次之后,我总是在编译行包含-encoding UTF-8
,并确保文件顶部有一个文件是UTF-8的注释,以防万一该文件及其生成文件应该分开。我也总是使用-Dfile.encoding=utf-8
的 jvm arg 运行,因为我厌倦了看到我漂亮的 Java 字符被肢解而没有引发异常。【参考方案5】:
在 Unix 文件系统上,文件名实际上是以空字符结尾的字节 []。因此 java 运行时必须在 createNewFile() 操作期间执行从 java.lang.String 到 byte[] 的转换。字符到字节的转换由语言环境控制。我一直在测试将LC_ALL
设置为en_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 字符(或 UTF-8 中的 C3 AE
)和序列 i+\u0302(UTF-8 中的 69 CC 82
)之间的等价性。 \u0302 是一个组合变音符号(组合抑扬音符号)。在文件创建期间发生了某种规范化。我不确定它是在 Java 运行时还是操作系统中完成的。
注意:我花了一些时间才弄清楚,因为您发布的代码 sn-p 没有组合变音符号,而是等效字符 î
(例如 \u00ee
)。您应该在字符串文字中嵌入了 Unicode 转义序列(但之后很容易说...)。
【讨论】:
您关于两种不同 Unicode 形式等价的观点是完全正确的。有趣的是,如果我在示例中包含了用于组合/组合变音符号的显式 \u 代码,它将掩盖我在真实应用程序中看到的差异。一个做错事的案例效果很好【参考方案6】:我以前见过类似的东西。将文件从 Mac 上传到 web 应用程序的人使用带有 é 的文件名。
a) 在操作系统中 char 是正常的 e + "sign for ´ 应用于前一个 char"
b) 在 Windows 中它是一个特殊的字符:é
两者都是 Unicode。所以...我知道您将 (b) 选项传递给 File create,并且在某些时候 Mac OS 将其转换为 (a) 选项。也许如果您在互联网上发现双重表示问题,您可以找到一种方法来成功处理这两种情况。
希望对你有帮助!
【讨论】:
嗯,事实上,情况正好相反。您使用 Mac 键盘 (a) 选项键入 Java 文件 [原始名称],系统将 [在文件创建时] 将其转换为 (b) 选项。 谢谢,这正是正确的轨道。我在我的问题中添加了一个讨论,该讨论对不同的 Unicode 形式以及当您从File
方法获得每种形式时进行了一些扩展以上是关于File.listFiles() 使用 JDK 6 破坏 unicode 名称(Unicode 规范化问题)的主要内容,如果未能解决你的问题,请参考以下文章
我如何使用 file.listFiles() 来列出子目录和文件
File.list() 与 File.listFiles()