Java无法在文件系统上看到包含非法字符的文件

时间:2012-08-24 12:36:27

标签: java utf-8 character-encoding filenotfoundexception windows-1252

我正在尝试我们在制作中看到的边缘案例。我们有一个业务模型,客户端生成文本文件,然后将它们FTP到我们的服务器。我们在Java后端(在CentOS机器上运行)中提取这些文件并对其进行处理。大多数(95%以上)我们的客户都知道以UTF-8生成这些文件,这正是我们想要的。但是,我们有一些顽固的客户端(但很大的帐户)在Windows机器上使用CP1252字符集生成这些文件。没问题,我们已经配置了我们的第三方库(大多数“处理”工作对我们来说)通过一些神奇的voo doo来处理任何字符集中的输入。

有时,我们会看到一个名称中包含非法UTF-8字符(CP1252)的文件。当我们的软件尝试从FTP服务器读取这些文件时,正常的文件读取方法会引起阻塞并引发FileNotFoundException

File f = getFileFromFTPServer();
FileReader fReader = new FileReader(f);

String line = fReader.readLine();
// ...etc.

例外看起来像这样:

java.io.FileNotFoundException: /path/to/file/some-text-blah?blah.xml (No such file or directory) at java.io.FileInputStream.open(Native Method) at 
java.io.FileInputStream.(FileInputStream.java:120) at java.io.FileReader.(FileReader.java:55) at com.myorg.backend.app.InputFileProcessor.run(InputFileProcessor.java:60) at 
java.lang.Thread.run(Thread.java:662)

所以我认为发生的事情是因为文件 name 本身包含非法字符,所以我们从来没有读过它。如果可以,那么无论文件的内容如何,​​我们的软件都应该能够正确处理它。因此,读取带有非法UTF-8字符的文件名确实存在问题。

作为一个测试案例,我创建了一个非常简单的Java“app”,可以部署在我们的一台服务器上并测试一些东西(源代码如下所示)。然后我登录到Windows机器并创建了一个测试文件,并将其命名为test£.txt。注意文件名中“test”后面的字符。这是Alt-0163。我将其FTP到我们的服务器,当我在其父目录上运行ls -ltr时,我惊讶地发现它被列为test?.txt

在进一步讨论之前,这里是我为测试/复制这个问题而编写的Java“app”:

public Driver {
    public static void main(String[] args) {
        Driver d = new Driver();
        d.run(args[0]);     // I know this is bad, but its fine for our purposes here
    }

    private void run(String fileName) {
        InputStreamReader isr = null;
        BufferedReader buffReader = null;
        FileInputStream fis = null;
        String firstLineOfFile = "default";

        System.out.println("Processing " + fileName);

        try {
            System.out.println("Attempting UTF-8...");

            fis = new FileInputStream(fileName);
            isr = new InputStreamReader(fis, Charset.forName("UTF-8"));
            buffReader = new BufferedReader(isr);

            firstLineOfFile = buffReader.readLine();

            System.out.println("UTF-8 worked and first line of file is : " + firstLineOfFile);
        }
        catch(IOException io1) {
            // UTF-8 failed; try CP1252.
            try {
                System.out.println("UTF-8 failed. Attempting Windows-1252...(" + io1.getMessage() + ")");

                fis = new FileInputStream(fileName);
                // I've also tried variations "WINDOWS-1252", "Windows-1252", "CP1252", "Cp1252", "cp1252"
                isr = new InputStreamReader(fis, Charset.forName("windows-1252"));
                buffReader = new BufferedReader(isr);

                firstLineOfFile = buffReader.readLine();

                System.out.println("Windows-1252 worked and first line of file is : " + firstLineOfFile);
            }
            catch(IOException io2) {
                // Both UTF-8 and CP1252 failed...
                System.out.println("Both UTF-8 and Windows-1252 failed. Could not read file. (" + io2.getMessage() + ")");
            }
        }
    }
}

当我从终端(java -cp . com/Driver t*)运行时,我得到以下输出:

Processing test�.txt
Attempting UTF-8...
UTF-8 failed. Attempting Windows-1252...(test�.txt (No such file or directory))
Both UTF-8 and Windows-1252 failed. Could not read file.(test�.txt (No such file or directory))

test�.txt?!?!我做了一些研究,发现“�”是Unicode替换字符\uFFFD。所以我猜测发生的事情是CentOS FTP服务器不知道如何处理Alt-0163(£),所以用\uFFFD替换它{{1} }})。但我不明白为什么�会显示一个名为ls -ltr ...

的文件

无论如何,似乎解决方案是添加一些逻辑来搜索文件名中是否存在此字符,如果找到,则将文件重命名为其他内容(例如,可以执行String-wise {{ 1}}或类似的东西)系统可以读取和处理。

问题是Java甚至没有在文件系统上看到这个文件。 CentOS知道该文件在那里(test?.txt),但是当该文件被传递到Java时,Java会将其解释为replaceAll("\uFFFD", "_")并且出于某种原因test?.txt ......

如何让Java查看此文件以便我可以对其执行test�.txt?对于这里的背景故事感到抱歉,但我认为这是相关的,因为在这种情况下每个细节都很重要。提前谢谢!

2 个答案:

答案 0 :(得分:5)

欢迎来到精彩的文字编码世界。您有几个级别的问题,您需要单独对它们进行排序。

首先,磁盘上的文件名是什么?它是否包含有效的UTF-8转义序列还是其他的?

这里的问题是您需要正确的文件名,否则Windows文件系统将无法找到该文件。最重要的是,Windows可能会尝试将文件名中的非法字符转换为Unicode \uFFFD,因此无论您尝试什么,都将无法加载该文件(因为没有{{{ 1}}在磁盘上。)

怎么会这样?发生这种情况是因为映射不是双向的。当Windows从磁盘加载文件名时,它会将\uFFFD替换为test�.txt,并为您提供该名称。当您告诉Windows打开test\uFFFD.txt时,它将无法找到该文件,因为没有具有此类名称的文件(只有test\uFFFD.txt)。 您无法找到该文件的真实姓名。

解决方案?您可以打开dos提示符并使用模式test�.txt重命名该文件。由于模式只匹配单个文件,因此可以使用。但是,您无法从Windows资源管理器中执行相同操作,因为它也无法找到该文件。

下一步:FTP。 FTP是一种人类协议 - 它不适合自动数据交换。摆脱FTP。我不知道会花多少钱,但它总是值得的。使用SFTP,scp或FTAPI

问题的一个原因可能是FTP将文件名转换为ASCII。 FTP协议中不允许使用变音符号...或者说,FTP不期望任何变音符号。如果你很幸运,你的FTP客户端将拒绝传输文件,但大多数情况下都是错误的。但是当它们存在时,FTP就会做......某事。无论那是什么。这里的常见效果是名称中包含Unicode的文件被编码两次,因为UTF-8或Unicode被ren test*.txt test.txt?)替换。

或者Java FTP客户端可以使用\u003f从FTP文件名创建一个字符串,这会强制使用系统的默认编码来填充较差的字节 - 不是很好。

解决方案:

  1. 使用FTP服务器拒绝其名称中包含非法字符的文件,或将这些字符替换为不会混淆文件系统/操作系统的文件。
  2. 使用正确处理奇怪名称文件的文件系统。这通常意味着在服务器上摆脱Windows。
  3. 确保用户只能上传到一个目录,并且该目录只能包含一个文件。这样,您可以使用小型shell脚本和模式将其重命名为您可以阅读的内容。

答案 1 :(得分:1)

这是旧的skool java File api中的一个错误,也许只是在Mac上?无论如何,新的java.nio api工作得更好。我有几个文件包含无法使用java.io ...类加载的unicode字符。转换我的所有代码后使用java.nio.Path一切开始工作。我用java.nio.Files ...

替换了apache FileUtils(它有同样的问题)

确保使用适当的字符集读取和写入文件内容,例如:Files.readAllLines(myPath,StandardCharsets.UTF_8)