【问题标题】:How to make a valid Windows filename from an arbitrary string?如何从任意字符串生成有效的 Windows 文件名?
【发布时间】:2010-10-11 20:38:13
【问题描述】:

我有一个类似“Foo: Bar”的字符串,我想将其用作文件名,但在 Windows 上,文件名中不允许使用“:”字符。

有没有一种方法可以将“Foo: Bar”变成类似“Foo-Bar”的东西?

【问题讨论】:

  • 我今天做了同样的事情。由于某种原因我没有检查 SO,但无论如何都找到了答案。

标签: c# windows filenames


【解决方案1】:

试试这样的:

string fileName = "something";
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
{
   fileName = fileName.Replace(c, '_');
}

编辑:

由于GetInvalidFileNameChars() 将返回10 或15 个字符,因此最好使用StringBuilder 而不是简单的字符串;原始版本会花费更长的时间并消耗更多的内存。

【讨论】:

  • 如果您愿意,您可以使用 StringBuilder,但如果名称很短,我想这不值得。您还可以创建自己的方法来创建 char[] 并在一次迭代中替换所有错误的字符。总是最好保持简单,除非它不起作用,否则你的瓶颈可能会更糟
  • InvalidFileNameChars = new char[] { '"', '', '|', '\0', '\x0001', '\x0002', '\x0003' , '\x0004', '\x0005', '\x0006', '\a', '\b', '\t', '\n', '\v', '\f', '\r' , '\x000e', '\x000f', '\x0010', '\x0011', '\x0012', '\x0013', '\x0014', '\x0015', '\x0016', '\x0017' , '\x0018', '\x0019', '\x001a', '\x001b', '\x001c', '\x001d', '\x001e', '\x001f', ':', '*', ' ?', '\\', '/' };
  • 字符串中有 2+ 个不同的无效字符的概率是如此之小,以至于关心 string.Replace() 的性能是没有意义的。
  • 很好的解决方案,有趣的是,resharper 建议使用这个 Linq 版本:fileName = System.IO.Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c, ' _'));我想知道那里是否有任何可能的性能改进。出于可读性目的,我保留了原件,因为性能不是我最关心的问题。但如果有人感兴趣,可能值得进行基准测试
  • @AndyM 不需要。 file.name.txt.pdf 是有效的 pdf。 Windows 仅读取扩展名的最后一个 .
【解决方案2】:
fileName = fileName.Replace(":", "-") 

但是,“:”并不是 Windows 唯一的非法字符。您还必须处理:

/, \, :, *, ?, ", <, > and |

这些都包含在 System.IO.Path.GetInvalidFileNameChars();

还有(在 Windows 上)“.”不能是文件名中的唯一字符(“.”、“..”、“...”等均无效)。使用“.”命名文件时要小心,例如:

echo "test" > .test.

会生成一个名为“.test”的文件

最后,如果您真的想要正确地做事,那么您需要注意一些special file names在 Windows 上您不能创建名为:

的文件
CON, PRN, AUX, CLOCK$, NUL
COM0, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9
LPT0, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9.

【讨论】:

  • 我从来不知道保留名称。虽然有道理
  • 另外,无论如何,您不能创建以这些保留名称之一开头,后跟小数的文件名。即 con.air.avi
  • ".foo" 是一个有效的文件名。不知道“CON”文件名——它是干什么用的?
  • 从头开始。 CON 用于控制台。
  • 感谢配置者;我已经更新了答案,你是正确的“.foo”是有效的;但是“.foo”。导致可能的、不需要的结果。已更新。
【解决方案3】:

这不是更有效,但更有趣:)

var fileName = "foo:bar";
var invalidChars = System.IO.Path.GetInvalidFileNameChars();
var cleanFileName = new string(fileName.Where(m => !invalidChars.Contains(m)).ToArray<char>());

【讨论】:

    【解决方案4】:

    如果有人想要基于StringBuilder 的优化版本,请使用它。包括 rkagerer's trick 作为选项。

    static char[] _invalids;
    
    /// <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 simply remove bad characters.</param>
    /// <param name="fancy">Whether 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, returns "_".</returns>
    public static string MakeValidFileName(string text, char? replacement = '_', bool fancy = true)
    {
        StringBuilder sb = new StringBuilder(text.Length);
        var invalids = _invalids ?? (_invalids = Path.GetInvalidFileNameChars());
        bool changed = false;
        for (int i = 0; i < text.Length; i++) {
            char c = text[i];
            if (invalids.Contains(c)) {
                changed = true;
                var repl = replacement ?? '\0';
                if (fancy) {
                    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;
    }
    

    【讨论】:

    • +1 以获得漂亮且可读的代码。使阅读和注意错误变得非常容易:P.. 此函数应始终返回原始字符串,因为更改永远不会是真的。
    • 谢谢,我觉得现在好多了。你知道他们对开源的看法,“很多人的眼睛让所有的错误都变得浅薄,所以我不必编写单元测试”......
    【解决方案5】:

    这是使用Linq 的已接受答案的版本,它使用Enumerable.Aggregate

    string fileName = "something";
    
    Path.GetInvalidFileNameChars()
        .Aggregate(fileName, (current, c) => current.Replace(c, '_'));
    

    【讨论】:

      【解决方案6】:

      迭戈的回答略有不同。

      如果您不害怕 Unicode,您可以通过将无效字符替换为与其相似的有效 Unicode 符号来保持更高的保真度。这是我在最近一个涉及木材切割清单的项目中使用的代码:

      static string MakeValidFilename(string text) {
        text = text.Replace('\'', '’'); // U+2019 right single quotation mark
        text = text.Replace('"',  '”'); // U+201D right double quotation mark
        text = text.Replace('/', '⁄');  // U+2044 fraction slash
        foreach (char c in System.IO.Path.GetInvalidFileNameChars()) {
          text = text.Replace(c, '_');
        }
        return text;
      }
      

      这会产生像1⁄2” spruce.txt 这样的文件名而不是1_2_ spruce.txt

      是的,它确实有效:

      告售者

      我知道这个技巧适用于 NTFS,但惊讶地发现它也适用于 FAT 和 FAT32 分区。那是因为long filenamesstored in Unicode,甚至as far back 作为Windows 95/NT。我在 Win7、XP 甚至基于 Linux 的路由器上进行了测试,结果显示正常。不能在 DOSBox 内部说同样的话。

      也就是说,在你发疯之前,请考虑一下你是否真的需要额外的保真度。 Unicode 相似可能会混淆人们或旧程序,例如旧操作系统依赖codepages

      【讨论】:

        【解决方案7】:

        Diego 确实有正确的解决方案,但其中存在一个非常小的错误。使用的string.Replace的版本应该是string.Replace(char, char),没有string.Replace(char, string)

        我无法编辑答案,否则我只会做些小改动。

        应该是这样的:

        string fileName = "something";
        foreach (char c in System.IO.Path.GetInvalidFileNameChars())
        {
           fileName = fileName.Replace(c, '_');
        }
        

        【讨论】:

          【解决方案8】:

          简单的一行代码:

          var validFileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));
          

          如果你想重用它,你可以将它包装在一个扩展方法中。

          public static string ToValidFileName(this string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));
          

          【讨论】:

            【解决方案9】:

            这是一个使用 StringBuilderIndexOfAny 以及批量追加的版本以提高效率。它还返回原始字符串,而不是创建重复的字符串。

            最后但并非最不重要的一点是,它有一个 switch 语句,可以返回相似的字符,您可以根据需要自定义这些字符。查看Unicode.org's confusables lookup 了解您可能有哪些选项,具体取决于字体。

            public static string GetSafeFilename(string arbitraryString)
            {
                var invalidChars = System.IO.Path.GetInvalidFileNameChars();
                var replaceIndex = arbitraryString.IndexOfAny(invalidChars, 0);
                if (replaceIndex == -1) return arbitraryString;
            
                var r = new StringBuilder();
                var i = 0;
            
                do
                {
                    r.Append(arbitraryString, i, replaceIndex - i);
            
                    switch (arbitraryString[replaceIndex])
                    {
                        case '"':
                            r.Append("''");
                            break;
                        case '<':
                            r.Append('\u02c2'); // '˂' (modifier letter left arrowhead)
                            break;
                        case '>':
                            r.Append('\u02c3'); // '˃' (modifier letter right arrowhead)
                            break;
                        case '|':
                            r.Append('\u2223'); // '∣' (divides)
                            break;
                        case ':':
                            r.Append('-');
                            break;
                        case '*':
                            r.Append('\u2217'); // '∗' (asterisk operator)
                            break;
                        case '\\':
                        case '/':
                            r.Append('\u2044'); // '⁄' (fraction slash)
                            break;
                        case '\0':
                        case '\f':
                        case '?':
                            break;
                        case '\t':
                        case '\n':
                        case '\r':
                        case '\v':
                            r.Append(' ');
                            break;
                        default:
                            r.Append('_');
                            break;
                    }
            
                    i = replaceIndex + 1;
                    replaceIndex = arbitraryString.IndexOfAny(invalidChars, i);
                } while (replaceIndex != -1);
            
                r.Append(arbitraryString, i, arbitraryString.Length - i);
            
                return r.ToString();
            }
            

            它不检查 ... 或保留名称(如 CON),因为不清楚替换应该是什么。

            【讨论】:

              【解决方案10】:

              另一个简单的解决方案:

              private string MakeValidFileName(string original, char replacementChar = '_')
              {
                var invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
                return new string(original.Select(c => invalidChars.Contains(c) ? replacementChar : c).ToArray());
              }
              

              【讨论】:

                【解决方案11】:

                清理一点我的代码并进行一点重构...我为字符串类型创建了一个扩展:

                public static string ToValidFileName(this string s, char replaceChar = '_', char[] includeChars = null)
                {
                  var invalid = Path.GetInvalidFileNameChars();
                  if (includeChars != null) invalid = invalid.Union(includeChars).ToArray();
                  return string.Join(string.Empty, s.ToCharArray().Select(o => o.In(invalid) ? replaceChar : o));
                }
                

                现在它更易于使用:

                var name = "Any string you want using ? / \ or even +.zip";
                var validFileName = name.ToValidFileName();
                

                如果您想用不同于“_”的字符替换,您可以使用:

                var validFileName = name.ToValidFileName(replaceChar:'#');
                

                你可以添加字符来替换..例如你不想要空格或逗号:

                var validFileName = name.ToValidFileName(includeChars: new [] { ' ', ',' });
                

                希望对你有帮助……

                干杯

                【讨论】:

                  【解决方案12】:

                  我需要一个不会产生冲突的系统,因此我无法将多个字符映射到一个字符。我最终得到:

                  public static class Extension
                  {
                      /// <summary>
                      /// Characters allowed in a file name. Note that curly braces don't show up here
                      /// becausee they are used for escaping invalid characters.
                      /// </summary>
                      private static readonly HashSet<char> CleanFileNameChars = new HashSet<char>
                      {
                          ' ', '!', '#', '$', '%', '&', '\'', '(', ')', '+', ',', '-', '.',
                          '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '=', '@',
                          'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
                          'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
                          '[', ']', '^', '_', '`',
                          'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
                          'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
                      };
                  
                      /// <summary>
                      /// Creates a clean file name from one that may contain invalid characters in 
                      /// a way that will not collide.
                      /// </summary>
                      /// <param name="dirtyFileName">
                      /// The file name that may contain invalid filename characters.
                      /// </param>
                      /// <returns>
                      /// A file name that does not contain invalid filename characters.
                      /// </returns>
                      /// <remarks>
                      /// <para>
                      /// Escapes invalid characters by converting their ASCII values to hexadecimal
                      /// and wrapping that value in curly braces. Curly braces are escaped by doubling
                      /// them, for example '{' => "{{".
                      /// </para>
                      /// <para>
                      /// Note that although NTFS allows unicode characters in file names, this
                      /// method does not.
                      /// </para>
                      /// </remarks>
                      public static string CleanFileName(this string dirtyFileName)
                      {
                          string EscapeHexString(char c) =>
                              "{" + (c > 255 ? $"{(uint)c:X4}" : $"{(uint)c:X2}") + "}";
                  
                          return string.Join(string.Empty,
                                             dirtyFileName.Select(
                                                 c =>
                                                     c == '{' ? "{{" :
                                                     c == '}' ? "}}" :
                                                     CleanFileNameChars.Contains(c) ? $"{c}" :
                                                     EscapeHexString(c)));
                      }
                  }
                  

                  【讨论】:

                    【解决方案13】:

                    我今天需要这样做...就我而言,我需要将客户名称与最终 .kmz 文件的日期和时间连接起来。我的最终解决方案是:

                     string name = "Whatever name with valid/invalid chars";
                     char[] invalid = System.IO.Path.GetInvalidFileNameChars();
                     string validFileName = string.Join(string.Empty,
                                                string.Format("{0}.{1:G}.kmz", name, DateTime.Now)
                                                .ToCharArray().Select(o => o.In(invalid) ? '_' : o));
                    

                    如果将空格字符添加到无效数组中,您甚至可以使其替换空格。

                    也许它不是最快的,但由于性能不是问题,我发现它优雅且易于理解。

                    干杯!

                    【讨论】:

                      【解决方案14】:

                      此主题尚无有效答案。作者说:“......我想用作文件名......”。删除/替换无效字符不足以将某些内容用作文件名。你至少应该检查一下:

                      1. 您的文件夹中还没有同名的文件,您想在其中创建一个新文件
                      2. 文件的总路径(文件夹路径 + 文件名 + 扩展名)不超过 MAX_PATH(260 个符号)。是的,有一些技巧可以在最新的 Windows 上破解此问题,但如果您希望您的应用正常运行 - 您应该检查一下
                      3. 您没有使用任何特殊的文件名(请参阅@Phil Price 的回答)

                      可能最好的方法是:

                      1. 使用此处的其他答案之一删除坏字符。
                      2. 确保总路径少于 260 个字符(如果不是 - 删除最后 N 个字符)
                      3. 确保具有给定文件名的文件不存在(如果存在 - 替换最后 N 个字符,直到找到可用的文件名)
                      4. 确保不使用任何保留的文件名(如果使用 - 替换最后 N 个字符,直到找到正确且可用的文件名)

                      与往常一样,事情变得更加复杂,然后看起来。最好使用一些已经存在的函数,比如GetTempFileNameW

                      【讨论】:

                        【解决方案15】:

                        您可以使用sed 命令执行此操作:

                         sed -e "
                         s/[?()\[\]=+<>:;©®”,*|]/_/g
                         s/"$'\t'"/ /g
                         s/–/-/g
                         s/\"/_/g
                         s/[[:cntrl:]]/_/g"
                        

                        【讨论】:

                        • 另见更复杂但相关的问题:stackoverflow.com/questions/4413427/…
                        • 为什么这需要在 C# 而不是 Bash 中完成?我现在在原始问题上看到了 C# 的标签,但为什么呢?
                        • 我知道,对,为什么不直接从 C# 应用程序中取出可能无法安装的 Bash 来完成此操作?
                        猜你喜欢
                        • 1970-01-01
                        • 2011-07-12
                        • 1970-01-01
                        • 1970-01-01
                        • 2018-06-23
                        • 2012-03-31
                        • 1970-01-01
                        • 2023-03-26
                        • 1970-01-01
                        相关资源
                        最近更新 更多