如何从任意字符串制作有效的Windows文件名?


97

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

有没有一种方法可以将“ Foo:Bar”变成“ Foo- Bar”?


1
我今天也做了同样的事情。我出于某种原因没有检查SO,但是无论如何找到了答案。
亚伦·史密斯2009年

Answers:


154

尝试这样的事情:

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

编辑:

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


1
如果愿意,可以使用StringBuilder,但如果名称简短,我认为这是不值得的。您也可以创建自己的方法来创建char []并在一次迭代中替换所有错误的char。除非它不起作用,否则总是最好保持简单,否则瓶颈可能会更糟
Diego Jancic 2009年

2
InvalidFileNameChars =新的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',':','*','?','\\', '/'};
Diego Jancic

9
字符串中有2个以上不同的无效字符的可能性很小,以至于关心string.Replace()的性能是没有意义的。
Serge Wautier'3

1
很好的解决方案,除了有趣的问题之外,resharper建议使用此Linq版本:fileName = System.IO.Path.GetInvalidFileNameChars()。Aggregate(fileName,(current,c)=> current.Replace(c,'_')); 我想知道那里是否有任何可能的性能改进。我出于性能的考虑保留了原件,因为性能并不是我最大的担心。但是,如果有人感兴趣,可能值得进行基准测试
chrispepper1989

1
@AndyM不需要。file.name.txt.pdf是有效的pdf。Windows仅读取.扩展名的最后一个。
迭戈·扬西奇

33
fileName = fileName.Replace(":", "-") 

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

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

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

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

echo "test" > .test.

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

最后,如果您确实想正确执行操作,则需要注意一些特殊的文件名在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.

3
我从不知道保留名称。尽管很有道理
Greg Dean

4
此外,就其价值而言,您不能创建以以下保留名之一开头的文件名,后跟十进制数。即con.air.avi
John Conrad 2009年

“ .foo”是有效的文件名。不知道“ CON”文件名是什么?
配置器

刮一下。CON用于控制台。
配置器

感谢配置器;我已经更新了答案,您正确的“ .foo”有效;但是“ .foo”。导致可能的不良结果。更新。
Phil Price 2009年

13

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

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

12

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

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.。此函数应始终返回原始字符串,因为更改将永远不会成立。
2014年

谢谢,我认为现在更好了。您知道他们对开放源代码所说的话,“很多眼睛使所有错误都变得浅浅,因此我不必编写单元测试” ...
Qwertie 2014年

8

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

string fileName = "something";

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

7

迭戈确实有正确的解决方案,但是其中只有一个很小的错误。string.Replace的版本应该是string.Replace(char,char),没有字符串。Replace(char,string)

我无法编辑答案,否则我将进行较小的更改。

所以应该是:

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

7

迭戈的答案略有不同。

如果您不担心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分区上使用。这是因为长文件名以Unicode格式存储,甚至可以追溯到与Windows 95 / NT。我在Win7,XP甚至是基于Linux的路由器上进行了测试,结果显示还可以。对于DOSBox,不能说相同。

就是说,在对此付诸实践之前,请考虑是否真的需要额外的保真度。Unicode相似性可能会使人或旧程序感到困惑,例如,旧版OS依赖于代码页


5

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

最后但并非最不重要的一点是,它具有一个switch语句,该语句返回相似的字符,您可以根据需要自定义任何方式。查看Unicode.org的易混淆查询,以查看可能有哪些选项,具体取决于字体。

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,因为它没有明确的更换应该是什么。


3

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

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 [] { ' ', ',' });

希望能帮助到你...

干杯


3

另一个简单的解决方案:

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());
}

3

一个简单的单行代码:

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, '_'));

1

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

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)));
    }
}

0

我今天需要这样做...就我而言,我需要将客户名称与最终.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));

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

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

干杯!


-2

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

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

还看到一个更复杂,但相关的问题:stackoverflow.com/questions/4413427/...
DW

为什么需要用C#而不是Bash来完成?我现在在原始问题上看到一个C#标签,但是为什么呢?
DW

1
我知道,对,为什么不仅仅从C#应用程序外壳到可能无法安装以完成此操作的Bash?
彼得·里奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.