如何从路径和文件名中删除非法字符?

591

我需要一种强大且简单的方法来从简单字符串中删除非法的路径和文件字符。 我使用了以下代码,但似乎没有任何作用,我漏掉了什么?

using System;
using System.IO;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string illegal = "\"M<>\"\\a/ry/ h**ad:>> a\\/:*?\"<>| li*tt|le|| la\"mb.?";

            illegal = illegal.Trim(Path.GetInvalidFileNameChars());
            illegal = illegal.Trim(Path.GetInvalidPathChars());

            Console.WriteLine(illegal);
            Console.ReadLine();
        }
    }
}

2
Trim方法从字符串的开头和结尾删除字符。然而,您可能应该问为什么数据无效,而不是尝试清洗/修复数据,拒绝该数据。 - user7116
9
Unix风格的文件名在Windows上无效,而且我不想使用8.3短文件名。 - Gary Willoughby
1
GetInvalidFileNameChars() 将从文件夹路径中剥离像:\等字符。 - CAD bloke
2
Path.GetInvalidPathChars() doesn't seem to strip * or ? - CAD bloke
25
我测试了这个问题的五个答案(循环1万次),以下方法是最快的。正则表达式排名第二,慢25%: public string GetSafeFilename(string filename) { return string.Join("_", filename.Split(Path.GetInvalidFileNameChars())); } - Brain2000
我在这个答案中添加了一个新的快速替代方案,并进行了一些基准测试。 - c-chavez
30个回答

12

这是一个代码片段,应该可以帮助你在 .NET 3 及以上版本中使用。

using System.IO;
using System.Text.RegularExpressions;

public static class PathValidation
{
    private static string pathValidatorExpression = "^[^" + string.Join("", Array.ConvertAll(Path.GetInvalidPathChars(), x => Regex.Escape(x.ToString()))) + "]+$";
    private static Regex pathValidator = new Regex(pathValidatorExpression, RegexOptions.Compiled);

    private static string fileNameValidatorExpression = "^[^" + string.Join("", Array.ConvertAll(Path.GetInvalidFileNameChars(), x => Regex.Escape(x.ToString()))) + "]+$";
    private static Regex fileNameValidator = new Regex(fileNameValidatorExpression, RegexOptions.Compiled);

    private static string pathCleanerExpression = "[" + string.Join("", Array.ConvertAll(Path.GetInvalidPathChars(), x => Regex.Escape(x.ToString()))) + "]";
    private static Regex pathCleaner = new Regex(pathCleanerExpression, RegexOptions.Compiled);

    private static string fileNameCleanerExpression = "[" + string.Join("", Array.ConvertAll(Path.GetInvalidFileNameChars(), x => Regex.Escape(x.ToString()))) + "]";
    private static Regex fileNameCleaner = new Regex(fileNameCleanerExpression, RegexOptions.Compiled);

    public static bool ValidatePath(string path)
    {
        return pathValidator.IsMatch(path);
    }

    public static bool ValidateFileName(string fileName)
    {
        return fileNameValidator.IsMatch(fileName);
    }

    public static string CleanPath(string path)
    {
        return pathCleaner.Replace(path, "");
    }

    public static string CleanFileName(string fileName)
    {
        return fileNameCleaner.Replace(fileName, "");
    }
}

8

大多数上述解决方案都会将路径和文件名中的非法字符组合在一起,这是错误的(即使两者当前返回相同的字符集)。我会首先将路径+文件名拆分为路径和文件名,然后对它们中的一个应用适当的字符集,然后再将两个内容合并。

wvd_vegt


+1:非常正确。今天在使用 .NET 4.0 工作时,顶部答案中的正则表达式解决方案会删除完整路径中的所有反斜杠。因此,我为目录路径制作了一个正则表达式,为文件名制作了另一个正则表达式,分别进行清理,然后重新组合。 - dario_ramos
这可能是正确的,但这并没有回答问题。相比于已经在这里提供的一些完整解决方案(例如下面Lilly的答案),我不确定一个模糊的“我会这样做”的回答是否有太大帮助。 - Ian Grainger

6
这似乎是O(n)的,不会在字符串上花费太多内存:
    private static readonly HashSet<char> invalidFileNameChars = new HashSet<char>(Path.GetInvalidFileNameChars());

    public static string RemoveInvalidFileNameChars(string name)
    {
        if (!name.Any(c => invalidFileNameChars.Contains(c))) {
            return name;
        }

        return new string(name.Where(c => !invalidFileNameChars.Contains(c)).ToArray());
    }

1
当使用“Any”函数时,我不认为它是O(n)。 - II ARROWS
@IIARROWS,你认为它是什么? - Alexey F
我不知道,当我写评论时并没有感觉到这样...现在我尝试计算一下,看起来你是对的。 - II ARROWS
我选择了这个,因为考虑到你的性能。谢谢。 - Berend Engelbrecht

6

如果您删除或替换无效字符为单个字符,则可能会发生冲突:

<abc -> abc
>abc -> abc

这里有一个简单的方法可以避免这个问题:
public static string ReplaceInvalidFileNameChars(string s)
{
    char[] invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars();
    foreach (char c in invalidFileNameChars)
        s = s.Replace(c.ToString(), "[" + Array.IndexOf(invalidFileNameChars, c) + "]");
    return s;
}

结果如下:
 <abc -> [1]abc
 >abc -> [2]abc

5

文件名不能包含来自Path.GetInvalidPathChars()+#符号以及其他特殊名称的字符。我们将所有检查组合到一个类中:

public static class FileNameExtensions
{
    private static readonly Lazy<string[]> InvalidFileNameChars =
        new Lazy<string[]>(() => Path.GetInvalidPathChars()
            .Union(Path.GetInvalidFileNameChars()
            .Union(new[] { '+', '#' })).Select(c => c.ToString(CultureInfo.InvariantCulture)).ToArray());


    private static readonly HashSet<string> ProhibitedNames = new HashSet<string>
    {
        @"aux",
        @"con",
        @"clock$",
        @"nul",
        @"prn",

        @"com1",
        @"com2",
        @"com3",
        @"com4",
        @"com5",
        @"com6",
        @"com7",
        @"com8",
        @"com9",

        @"lpt1",
        @"lpt2",
        @"lpt3",
        @"lpt4",
        @"lpt5",
        @"lpt6",
        @"lpt7",
        @"lpt8",
        @"lpt9"
    };

    public static bool IsValidFileName(string fileName)
    {
        return !string.IsNullOrWhiteSpace(fileName)
            && fileName.All(o => !IsInvalidFileNameChar(o))
            && !IsProhibitedName(fileName);
    }

    public static bool IsProhibitedName(string fileName)
    {
        return ProhibitedNames.Contains(fileName.ToLower(CultureInfo.InvariantCulture));
    }

    private static string ReplaceInvalidFileNameSymbols([CanBeNull] this string value, string replacementValue)
    {
        if (value == null)
        {
            return null;
        }

        return InvalidFileNameChars.Value.Aggregate(new StringBuilder(value),
            (sb, currentChar) => sb.Replace(currentChar, replacementValue)).ToString();
    }

    public static bool IsInvalidFileNameChar(char value)
    {
        return InvalidFileNameChars.Value.Contains(value.ToString(CultureInfo.InvariantCulture));
    }

    public static string GetValidFileName([NotNull] this string value)
    {
        return GetValidFileName(value, @"_");
    }

    public static string GetValidFileName([NotNull] this string value, string replacementValue)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException(@"value should be non empty", nameof(value));
        }

        if (IsProhibitedName(value))
        {
            return (string.IsNullOrWhiteSpace(replacementValue) ? @"_" : replacementValue) + value; 
        }

        return ReplaceInvalidFileNameSymbols(value, replacementValue);
    }

    public static string GetFileNameError(string fileName)
    {
        if (string.IsNullOrWhiteSpace(fileName))
        {
            return CommonResources.SelectReportNameError;
        }

        if (IsProhibitedName(fileName))
        {
            return CommonResources.FileNameIsProhibited;
        }

        var invalidChars = fileName.Where(IsInvalidFileNameChar).Distinct().ToArray();

        if(invalidChars.Length > 0)
        {
            return string.Format(CultureInfo.CurrentCulture,
                invalidChars.Length == 1 ? CommonResources.InvalidCharacter : CommonResources.InvalidCharacters,
                StringExtensions.JoinQuoted(@",", @"'", invalidChars.Select(c => c.ToString(CultureInfo.CurrentCulture))));
        }

        return string.Empty;
    }
}

GetValidFileName方法会将所有不正确的数据替换为_


5

如果在项目中需要在多个地方使用该方法,您可以编写一个扩展方法用于字符串,并在项目中的任何地方调用它。

 public static class StringExtension
    {
        public static string RemoveInvalidChars(this string originalString)
        {            
            string finalString=string.Empty;
            if (!string.IsNullOrEmpty(originalString))
            {
                return string.Concat(originalString.Split(Path.GetInvalidFileNameChars()));
            }
            return finalString;            
        }
    }

你可以这样调用上述扩展方法:
string illegal = "\"M<>\"\\a/ry/ h**ad:>> a\\/:*?\"<>| li*tt|le|| la\"mb.?";
string afterIllegalChars = illegal.RemoveInvalidChars();

因为每个字符串都是一个路径。或者说,仅为了一个特殊情况扩展“字符串”是否有意义? - Andreas

5
抛出一个异常。
if ( fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1 )
            {
                throw new ArgumentException();
            }

1
我认为在这里抛出异常并没有什么价值,因为问题陈述是关于删除有问题的字符,而不仅仅是抛出异常。 - PHenry

4

我出于娱乐目的编写了这个程序,它可以让你进行往返旅行:

public static class FileUtility
{
    private const char PrefixChar = '%';
    private static readonly int MaxLength;
    private static readonly Dictionary<char,char[]> Illegals;
    static FileUtility()
    {
        List<char> illegal = new List<char> { PrefixChar };
        illegal.AddRange(Path.GetInvalidFileNameChars());
        MaxLength = illegal.Select(x => ((int)x).ToString().Length).Max();
        Illegals = illegal.ToDictionary(x => x, x => ((int)x).ToString("D" + MaxLength).ToCharArray());
    }

    public static string FilenameEncode(string s)
    {
        var builder = new StringBuilder();
        char[] replacement;
        using (var reader = new StringReader(s))
        {
            while (true)
            {
                int read = reader.Read();
                if (read == -1)
                    break;
                char c = (char)read;
                if(Illegals.TryGetValue(c,out replacement))
                {
                    builder.Append(PrefixChar);
                    builder.Append(replacement);
                }
                else
                {
                    builder.Append(c);
                }
            }
        }
        return builder.ToString();
    }

    public static string FilenameDecode(string s)
    {
        var builder = new StringBuilder();
        char[] buffer = new char[MaxLength];
        using (var reader = new StringReader(s))
        {
            while (true)
            {
                int read = reader.Read();
                if (read == -1)
                    break;
                char c = (char)read;
                if (c == PrefixChar)
                {
                    reader.Read(buffer, 0, MaxLength);
                    var encoded =(char) ParseCharArray(buffer);
                    builder.Append(encoded);
                }
                else
                {
                    builder.Append(c);
                }
            }
        }
        return builder.ToString();
    }

    public static int ParseCharArray(char[] buffer)
    {
        int result = 0;
        foreach (char t in buffer)
        {
            int digit = t - '0';
            if ((digit < 0) || (digit > 9))
            {
                throw new ArgumentException("Input string was not in the correct format");
            }
            result *= 10;
            result += digit;
        }
        return result;
    }
}

1
我喜欢这个方法,因为它避免了两个不同的字符串创建相同的路径。 - Kim

3
浏览这里的答案,它们似乎都涉及使用无效文件名字符的char数组。虽然这可能是微小的优化,但为了让任何想要检查大量值是否为有效文件名的人受益,值得注意的是构建一个无效字符的哈希集将带来明显更好的性能。
我曾经非常惊讶(震惊)过去哈希集(或字典)比遍历列表快得多。对于字符串,这个数字非常低(大约从记忆中的5-7项)。对于大多数其他简单数据(对象引用、数字等),神奇的交叉点似乎在20项左右。
Path.InvalidFileNameChars "列表"中有40个无效字符。今天进行了搜索,在StackOverflow上有一个相当不错的基准测试,显示哈希集将花费超过一半的时间用于40个项目:https://dev59.com/0XVC5IYBdhLWcg3w51hv#10762995 这是我用于清理路径的助手类。我现在忘记了为什么会有精美的替换选项,但作为一个可爱的奖励,它在那里。
还有额外的奖励方法“IsValidLocalPath” :)

(**那些不使用正则表达式的)

public static class PathExtensions
{
    private static HashSet<char> _invalidFilenameChars;
    private static HashSet<char> InvalidFilenameChars
    {
        get { return _invalidFilenameChars ?? (_invalidFilenameChars = new HashSet<char>(Path.GetInvalidFileNameChars())); }
    }


    /// <summary>Replaces characters in <c>text</c> that are not allowed in file names with the 
    /// specified replacement character.</summary>
    /// <param name="text">Text to make into a valid filename. The same string is returned if 
    /// it is valid already.</param>
    /// <param name="replacement">Replacement character, or NULL to remove bad characters.</param>
    /// <param name="fancyReplacements">TRUE to replace quotes and slashes with the non-ASCII characters ” and ⁄.</param>
    /// <returns>A string that can be used as a filename. If the output string would otherwise be empty, "_" is returned.</returns>
    public static string ToValidFilename(this string text, char? replacement = '_', bool fancyReplacements = false)
    {
        StringBuilder sb = new StringBuilder(text.Length);
        HashSet<char> invalids = InvalidFilenameChars;
        bool changed = false;

        for (int i = 0; i < text.Length; i++)
        {
            char c = text[i];
            if (invalids.Contains(c))
            {
                changed = true;
                char repl = replacement ?? '\0';
                if (fancyReplacements)
                {
                    if (c == '"') repl = '”'; // U+201D right double quotation mark
                    else if (c == '\'') repl = '’'; // U+2019 right single quotation mark
                    else if (c == '/') repl = '⁄'; // U+2044 fraction slash
                }
                if (repl != '\0')
                    sb.Append(repl);
            }
            else
                sb.Append(c);
        }

        if (sb.Length == 0)
            return "_";

        return changed ? sb.ToString() : text;
    }


    /// <summary>
    /// Returns TRUE if the specified path is a valid, local filesystem path.
    /// </summary>
    /// <param name="pathString"></param>
    /// <returns></returns>
    public static bool IsValidLocalPath(this string pathString)
    {
        // From solution at https://dev59.com/E3RC5IYBdhLWcg3wD83r#11636052
        Uri pathUri;
        Boolean isValidUri = Uri.TryCreate(pathString, UriKind.Absolute, out pathUri);
        return isValidUri && pathUri != null && pathUri.IsLoopback;
    }
}

3

考虑到 .net 是一个旨在允许程序在多个平台上运行的框架(例如 Linux/Unix 和 Windows),我认为 Path.GetInvalidFileNameChars() 是最好的选择,因为它将包含有关您的程序正在运行的文件系统上什么是有效或无效的字符的知识。即使您的程序永远不会在 Linux 上运行(也许它充满了 WPF 代码),未来总有可能出现一些新的 Windows 文件系统,并具有不同的有效/无效字符。使用正则表达式自己编写代码相当于重复造轮子,并将平台问题转移到您自己的代码中。 - Daniel Scott
我同意你关于在线正则表达式编辑器/测试工具的建议。我发现它们非常有用(因为正则表达式是棘手的东西,充满了容易让你犯错的微妙之处,给你一个在边缘情况下表现出某些极其意外行为的正则表达式)。我的最爱是https://regex101.com(我喜欢它如何将正则表达式分解并清楚地显示它期望匹配什么)。我也很喜欢https://www.debuggex.com,因为它具有紧凑的匹配组和字符类等可视化表示。 - Daniel Scott

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