我正在尝试我们在制作中看到的边缘案例。我们有一个业务模型,客户端生成文本文件,然后将它们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
?对于这里的背景故事感到抱歉,但我认为这是相关的,因为在这种情况下每个细节都很重要。提前谢谢!
答案 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 :(得分: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)