有没有跨平台的Java方法可以删除文件名中的特殊字符?

64
我正在制作一个跨平台应用程序,它会根据从网上获取的数据重命名文件。我想对来自Web API的字符串进行平台本地化处理。
我知道不同的平台有不同的文件名要求,所以我想知道是否有一种跨平台的方法可以做到这一点?
编辑:在Windows平台上,文件名中不能包含问号“?” ,而在Linux上可以。文件名可能包含这些字符,我希望支持这些字符的平台能够保留它们,但其他平台则会将它们剥离掉。
此外,我更喜欢使用标准的Java解决方案,而不需要第三方库。

本,你能提供一些例子吗? - OscarRyz
在我的问题中添加了问号注释。 - Ben S
8个回答

33

如其他地方建议的那样,这通常不是您想要做的。最好使用安全方法(例如File.createTempFile())创建临时文件。

您不应该使用白名单仅保留“良好”的字符。如果文件只由汉字组成,则会将其全部剥离。出于这个原因,我们不能使用包含列表,而必须使用排除列表。

Linux几乎允许任何东西,这可能真的很麻烦。我只会将Linux限制为与Windows相同的列表,以便将来自己省心。

在Windows上使用此C#代码片段,我生成了一个无效的Windows字符列表。这个列表中的字符比您想象的要多得多(41个),因此我不建议尝试创建自己的列表。

        foreach (char c in new string(Path.GetInvalidFileNameChars()))
        {
            Console.Write((int)c);
            Console.Write(",");
        }

这是一个简单的Java类,用于“清洁”文件名。

public class FileNameCleaner {
final static int[] illegalChars = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47};
static {
    Arrays.sort(illegalChars);
}
public static String cleanFileName(String badFileName) {
    StringBuilder cleanName = new StringBuilder();
    for (int i = 0; i < badFileName.length(); i++) {
        int c = (int)badFileName.charAt(i);
        if (Arrays.binarySearch(illegalChars, c) < 0) {
            cleanName.append((char)c);
        }
    }
    return cleanName.toString();
}
}

编辑:

正如Stephen建议的那样,您可能还应该验证这些文件访问仅在您允许的目录中发生。

以下答案提供了Java中建立自定义安全上下文并在该“沙盒”中执行代码的示例代码。

如何创建安全的JEXL(脚本)沙盒?


1
好的Java示例,但是为什么你没有包括正斜杠(47)呢? - THelper
1
不知道为什么它不在列表中。我们实际上在生产代码中遇到了这个问题。我已经修复了答案,包括47。 - Sarel Botha
3
为了让 binarySearch 正常工作,必须对 illegalChars 数组进行排序。请添加 Arrays.sort(illegalChars) 或将该数组更改为 "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 42, 47, 58, 60, 62, 63, 92, 124}"。 - Franz Kafka
你的解决方案使用了 charAt()... 基本上你不应该使用 charAt。原因是 charAt 无法处理位于 基本多文种平面 之外的 Unicode 代码点,因为它是一个 16 位值。相反,使用 codePointAt() 返回一个整数。此外,这消除了您当前正在执行的强制转换为 int 的需要。 - Stijn de Witt
请记住,length()返回字符数,因此如果您使用codePointAt,则需要使用codePointCount()badFileName.codePointCount(0, badFileName.length()); - Stijn de Witt
嗯,你也在错误地添加...我会在另一个答案中发布更新的代码,以正确处理Unicode。 - Stijn de Witt

30
或者只需执行以下操作:
String filename = "A20/B22b#öA\\BC#Ä$%ld_ma.la.xps";
String sane = filename.replaceAll("[^a-zA-Z0-9\\._]+", "_");

结果:A20_B22b_A_BC_ld_ma.la.xps

解释:

[a-zA-Z0-9\\._] 匹配a-z大小写字母、数字、点和下划线

[^a-zA-Z0-9\\._] 是反向匹配,即所有不符合第一个表达式的字符

[^a-zA-Z0-9\\._]+ 是一串不符合第一个表达式的字符序列

所以,每一串不包含a-z、0-9或. _字符的字符序列都将被替换。


15
这适用于仅包含英文字母的文件名。如果文件名仅由中文字符组成,则将删除其中的所有内容。出于这个原因,我们不能使用白名单来剔除不良字符,必须使用黑名单。 - Sarel Botha
看这里:https://dev59.com/j2kw5IYBdhLWcg3w4emS 如果你使用Java 7,它应该可以工作。 - D-rk
@Dirk 被踩是因为正则表达式在这里不是解决方案。如果文件名是用多种语言编写的呢? - Franz Kafka
1
这取决于实际需求。如果白名单字符足够,那么这个解决方案会更易读。 - D-rk
4
为了保留文件名中的非拉丁字符,您可以使用Unicode标志(自Java 1.7起)进行如下操作: String sane = filename.replaceAll("(?U)[^\\w\\._]+", "_") ;。该代码将替换文件名中非单词字符、下划线和点之外的所有字符为下划线。 - Arie

18
这是基于Sarel Botha的被接受答案而来的,只要你不遇到基本多文种平面以外的字符,就可以正常使用。如果你需要完整的 Unicode 支持(谁不需要呢?),请改用这个代码,它是 Unicode 安全的:
public class FileNameCleaner {
  final static int[] illegalChars = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47};

  static {
    Arrays.sort(illegalChars);
  }

  public static String cleanFileName(String badFileName) {
    StringBuilder cleanName = new StringBuilder();
    int len = badFileName.codePointCount(0, badFileName.length());
    for (int i=0; i<len; i++) {
      int c = badFileName.codePointAt(i);
      if (Arrays.binarySearch(illegalChars, c) < 0) {
        cleanName.appendCodePoint(c);
      }
    }
    return cleanName.toString();
  }
}

这里的关键更改:

  • 使用codePointCountlength结合使用,而不仅仅是使用length
  • 使用codePointAt代替charAt
  • 使用appendCodePoint代替append
  • 不需要将char转换为int。实际上,您不应该处理char,因为它们基本上对BMP之外的任何内容都无用。

您可以使用标准函数并处理字符 - 您只需要跳过代理对字符后面的字符即可。此外,字符不需要转换为数字类型 - 它们本质上就是数字。 - weaknespase
2
我已经阅读了最佳答案和这个答案,这个答案似乎更加仔细地考虑了...但是我找不到任何情况下这段代码能够正确执行而另一个不能。有哪些输入可以展示它们之间的差异? - Doddie
这段代码和高评分答案都无法处理超出16位范围的字符。正确的迭代方式在这里描述:https://dev59.com/dnVC5IYBdhLWcg3wvT7g#361345。错误示例:"abcdef"。 - x4rf41

9
这是我使用的代码:
public static String sanitizeName( String name ) {
    if( null == name ) {
        return "";
    }

    if( SystemUtils.IS_OS_LINUX ) {
        return name.replaceAll( "[\u0000/]+", "" ).trim();
    }

    return name.replaceAll( "[\u0000-\u001f<>:\"/\\\\|?*\u007f]+", "" ).trim();
}

SystemUtils is from Apache commons-lang3


1
如果没有SystemUtils:if( File.separatorChar=='/') { return name.replaceAll( "/+", "" ).trim(); } - Tony BenBrahim
文件名中允许使用\u0000吗? - ax.

6

有一个非常好的内置Java解决方案-Character.isXxx()

尝试使用Character.isJavaIdentifierPart(c)

String name = "name.é+!@#$%^&*(){}][/=?+-_\\|;:`~!'\",<>";
StringBuilder filename = new StringBuilder();

for (char c : name.toCharArray()) {
  if (c=='.' || Character.isJavaIdentifierPart(c)) {
    filename.append(c);
  }
}

Result is "name.é$_".


好的,这是一种保守的方式,不能完全满足原始问题(跨平台),但对我来说有效 :) - Mark D
8
它确实会移除连字符,虽然在文件名中是有效的(至少在Windows中),但它能够完成任务。不过,我认为Apache Commons FilenameUtils应该加入一种跨平台的方式来完成这个任务。 - Jaime Hablutzel
它还会删除在Windows中仍然有效的“@”符号。 - azerafati

5

从您的问题中并不清楚,但由于您打算从Web表单接受路径名(?), 您可能应该阻止尝试重命名某些内容;例如"C:\Program Files"。这意味着在进行访问检查之前,您需要将路径名规范化以消除"."和".."。

鉴于此,我不会尝试删除非法字符。相反,我会使用"new File(str).getCanonicalFile()"来产生规范路径,然后检查它们是否符合您的沙箱限制,并最终使用"File.exists()", "File.isFile()"等方法检查源和目标文件是否符合要求,且它们不是同一个文件系统对象。我会通过尝试执行操作并捕获异常来处理非法字符。


1
< p > Paths.get(...) 抛出一个详细的异常,其中包含非法字符的位置。

public static String removeInvalidChars(final String fileName)
{
  try
  {
    Paths.get(fileName);
    return fileName;
  }
  catch (final InvalidPathException e)
  {
    if (e.getInput() != null && e.getInput().length() > 0 && e.getIndex() >= 0)
    {
      final StringBuilder stringBuilder = new StringBuilder(e.getInput());
      stringBuilder.deleteCharAt(e.getIndex());
      return removeInvalidChars(stringBuilder.toString());
    }
    throw e;
  }
}

4
哎呀。聪明,但如果你需要快速解决方案,请不要使用它(尝试/捕获和递归)。此外,如果您从Web接受用户输入,请勿忘记修剪输入;否则,发布一个1Mb长且包含无效字符的文件名肯定会使您的服务器发生堆栈溢出;) - Laurent Grégoire

0

如果您想使用更多的字符,如 [A-Za-z0-9],请查看MS Naming Conventions,并且不要忘记过滤掉“...整数表示在1到31之间的字符...”,就像Aaron Digulla的例子一样。对于这些字符,David Carboni的代码将不足以满足需求。

保留字符列表摘录:

可以使用当前代码页中的任何字符作为名称,包括 Unicode 字符和扩展字符集(128-255)中的字符,但不能使用以下字符: 以下保留字符: - < (小于号) - > (大于号) - : (冒号) - " (双引号) - / (正斜杠) - \ (反斜杠) - | (竖线或管道符) - ? (问号) - * (星号) 整数值零,有时也称为 ASCII NUL 字符。 其整数表示在 1 到 31 范围内的字符,除了备用数据流允许使用这些字符。有关文件流的更多信息,请参阅文件流。 目标文件系统不允许的任何其他字符也不能使用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接