Desinfetar o nome do arquivo em C #

174

Recentemente, mudei um monte de MP3s de vários locais para um repositório. Eu estava construindo os novos nomes de arquivos usando as tags ID3 (obrigado, TagLib-Sharp!) E notei que estava recebendo um System.NotSupportedException:

"O formato do caminho especificado não é suportado."

Isso foi gerado por um File.Copy()ou outro Directory.CreateDirectory().

Não demorou muito para perceber que meus nomes de arquivos precisavam ser limpos. Então eu fiz o óbvio:

public static string SanitizePath_(string path, char replaceChar)
{
    string dir = Path.GetDirectoryName(path);
    foreach (char c in Path.GetInvalidPathChars())
        dir = dir.Replace(c, replaceChar);

    string name = Path.GetFileName(path);
    foreach (char c in Path.GetInvalidFileNameChars())
        name = name.Replace(c, replaceChar);

    return dir + name;
}

Para minha surpresa, continuei recebendo exceções. Descobriu-se que ':' não está no conjunto de Path.GetInvalidPathChars(), porque é válido em uma raiz do caminho. Suponho que isso faça sentido - mas isso deve ser um problema bastante comum. Alguém tem algum código curto que limpa um caminho? O mais completo que eu vim com isso, mas parece que é provavelmente um exagero.

    // replaces invalid characters with replaceChar
    public static string SanitizePath(string path, char replaceChar)
    {
        // construct a list of characters that can't show up in filenames.
        // need to do this because ":" is not in InvalidPathChars
        if (_BadChars == null)
        {
            _BadChars = new List<char>(Path.GetInvalidFileNameChars());
            _BadChars.AddRange(Path.GetInvalidPathChars());
            _BadChars = Utility.GetUnique<char>(_BadChars);
        }

        // remove root
        string root = Path.GetPathRoot(path);
        path = path.Remove(0, root.Length);

        // split on the directory separator character. Need to do this
        // because the separator is not valid in a filename.
        List<string> parts = new List<string>(path.Split(new char[]{Path.DirectorySeparatorChar}));

        // check each part to make sure it is valid.
        for (int i = 0; i < parts.Count; i++)
        {
            string part = parts[i];
            foreach (char c in _BadChars)
            {
                part = part.Replace(c, replaceChar);
            }
            parts[i] = part;
        }

        return root + Utility.Join(parts, Path.DirectorySeparatorChar.ToString());
    }

Qualquer melhoria para tornar essa função mais rápida e menos barroca seria muito apreciada.

Jason Sundram
fonte

Respostas:

314

Para limpar um nome de arquivo, você pode fazer isso

private static string MakeValidFileName( string name )
{
   string invalidChars = System.Text.RegularExpressions.Regex.Escape( new string( System.IO.Path.GetInvalidFileNameChars() ) );
   string invalidRegStr = string.Format( @"([{0}]*\.+$)|([{0}]+)", invalidChars );

   return System.Text.RegularExpressions.Regex.Replace( name, invalidRegStr, "_" );
}
Andre
fonte
3
A pergunta era sobre caminhos, não nomes de arquivos, e os caracteres inválidos para eles são diferentes.
Dour High Arch
15
Talvez, mas este código certamente me ajudou quando eu tive o mesmo problema :)
MMR
8
E outro usuário SO potencialmente ótimo vai andando ... Essa função é ótima. Obrigado Adrevdm ...
Dan Rosenstark
19
Ótimo método. Não esqueça que as palavras reservadas ainda o morderão e você ficará coçando a cabeça. Fonte: Wikipedia Nome do Arquivo Palavras Reservadas
Spud
8
Os pontos são caracteres inválidos se estiverem no final do nome do arquivo, portanto GetInvalidFileNameCharsnão os incluem. Ele não lança uma exceção no Windows, apenas remove-os, mas pode causar um comportamento inesperado se você estiver esperando o período. Modifiquei o regex para lidar com esse caso, fazendo .com que seja considerado um dos caracteres inválidos se estiver no final da string.
Scott Chamberlain
120

Uma solução mais curta:

var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
DenNukem
fonte
1
@PeterMajeed: ATÉ que a contagem de linhas comece em zero :-)
Gary McGill
É melhor que a resposta principal, especialmente para o ASP.NET Core, que pode retornar caracteres diferentes com base na plataforma.
Alexei
79

Baseado na excelente resposta de Andre, mas levando em consideração o comentário de Spud sobre palavras reservadas, fiz esta versão:

/// <summary>
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
/// </summary>
/// <remarks>
/// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name
/// </remarks>
public static string CoerceValidFileName(string filename)
{
    var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
    var invalidReStr = string.Format(@"[{0}]+", invalidChars);

    var reservedWords = new []
    {
        "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4",
        "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
        "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
    };

    var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_");
    foreach (var reservedWord in reservedWords)
    {
        var reservedWordPattern = string.Format("^{0}\\.", reservedWord);
        sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase);
    }

    return sanitisedNamePart;
}

E estes são meus testes de unidade

[Test]
public void CoerceValidFileName_SimpleValid()
{
    var filename = @"thisIsValid.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual(filename, result);
}

[Test]
public void CoerceValidFileName_SimpleInvalid()
{
    var filename = @"thisIsNotValid\3\\_3.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("thisIsNotValid_3__3.txt", result);
}

[Test]
public void CoerceValidFileName_InvalidExtension()
{
    var filename = @"thisIsNotValid.t\xt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("thisIsNotValid.t_xt", result);
}

[Test]
public void CoerceValidFileName_KeywordInvalid()
{
    var filename = "aUx.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("_reservedWord_.txt", result);
}

[Test]
public void CoerceValidFileName_KeywordValid()
{
    var filename = "auxillary.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("auxillary.txt", result);
}
decreto
fonte
1
Esta é uma resposta extremamente completa, pelo menos para a parte do nome do arquivo da pergunta, e merece mais votos.
Brian MacKay
2
Sugestão secundária, pois parece que o método estava indo nessa direção: adicione uma palavra-chave this e ela se tornará um método de extensão útil. public estático String CoerceValidFileName (este nome de arquivo de string)
Ryan McArthur
2
Erro pequeno: esse método não altera as palavras reservadas sem as extensões de arquivo (por exemplo COM1,), que também não são permitidas. Correção sugerida seria mudar o reservedWordPattern para "^{0}(\\.|$)"eo texto de substituição para"_reservedWord_$1"
Dehalion
31
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
dados
fonte
5
considere em String.Concat(dirty...)vez deJoin(String.Empty...
drzaus 13/05
DenNukem já sugeriu esta resposta: stackoverflow.com/a/13617375/244916 (apesar de considerar o mesmo comentário).
Dude Pascalou
4

Estou usando o System.IO.Path.GetInvalidFileNameChars() método para verificar caracteres inválidos e não tenho problemas.

Estou usando o seguinte código:

foreach( char invalidchar in System.IO.Path.GetInvalidFileNameChars())
{
    filename = filename.Replace(invalidchar, '_');
}
André Leal
fonte
3

Eu queria manter os caracteres de alguma forma, não apenas substituí-los por um sublinhado.

Uma maneira que pensei foi substituir os personagens por personagens com aparência semelhante, que são (na minha situação), improváveis ​​de serem usados ​​como caracteres regulares. Então, peguei a lista de caracteres inválidos e encontrei parecidos.

A seguir, são apresentadas funções para codificar e decodificar com os itens similares.

Este código não inclui uma lista completa de todos os caracteres System.IO.Path.GetInvalidFileNameChars (). Portanto, é sua responsabilidade estender ou utilizar a substituição de sublinhado para os caracteres restantes.

private static Dictionary<string, string> EncodeMapping()
{
    //-- Following characters are invalid for windows file and folder names.
    //-- \/:*?"<>|
    Dictionary<string, string> dic = new Dictionary<string, string>();
    dic.Add(@"\", "Ì"); // U+OOCC
    dic.Add("/", "Í"); // U+OOCD
    dic.Add(":", "¦"); // U+00A6
    dic.Add("*", "¤"); // U+00A4
    dic.Add("?", "¿"); // U+00BF
    dic.Add(@"""", "ˮ"); // U+02EE
    dic.Add("<", "«"); // U+00AB
    dic.Add(">", "»"); // U+00BB
    dic.Add("|", "│"); // U+2502
    return dic;
}

public static string Escape(string name)
{
    foreach (KeyValuePair<string, string> replace in EncodeMapping())
    {
        name = name.Replace(replace.Key, replace.Value);
    }

    //-- handle dot at the end
    if (name.EndsWith(".")) name = name.CropRight(1) + "°";

    return name;
}

public static string UnEscape(string name)
{
    foreach (KeyValuePair<string, string> replace in EncodeMapping())
    {
        name = name.Replace(replace.Value, replace.Key);
    }

    //-- handle dot at the end
    if (name.EndsWith("°")) name = name.CropRight(1) + ".";

    return name;
}

Você pode selecionar suas próprias aparências. Eu usei o aplicativo Mapa de Caracteres no Windows para selecionar o meu%windir%\system32\charmap.exe

À medida que faço os ajustes por meio da descoberta, atualizarei esse código.

Valamas
fonte
observe que existem muitos caracteres que se parecem mais com esses, como a forma !"#$%&'()*+,-./:;<=>?@{|}~ de /largura total ou outras formas como SOLIDUS e `⁄` FRACTION SLASH que podem ser usados ​​diretamente em nomes de arquivos sem problemas
phuclv
2

Acho que o problema é que você primeiro chama Path.GetDirectoryNamea string incorreta. Se houver caracteres que não sejam de nome de arquivo, o .Net não poderá dizer quais partes da string são diretórios e lançamentos. Você precisa fazer comparações de strings.

Supondo que apenas o nome do arquivo esteja ruim, não o caminho inteiro, tente o seguinte:

public static string SanitizePath(string path, char replaceChar)
{
    int filenamePos = path.LastIndexOf(Path.DirectorySeparatorChar) + 1;
    var sb = new System.Text.StringBuilder();
    sb.Append(path.Substring(0, filenamePos));
    for (int i = filenamePos; i < path.Length; i++)
    {
        char filenameChar = path[i];
        foreach (char c in Path.GetInvalidFileNameChars())
            if (filenameChar.Equals(c))
            {
                filenameChar = replaceChar;
                break;
            }

        sb.Append(filenameChar);
    }

    return sb.ToString();
}
Arco Alto de Dour
fonte
2

Eu tive sucesso com isso no passado.

Bom, curto e estático :-)

    public static string returnSafeString(string s)
    {
        foreach (char character in Path.GetInvalidFileNameChars())
        {
            s = s.Replace(character.ToString(),string.Empty);
        }

        foreach (char character in Path.GetInvalidPathChars())
        {
            s = s.Replace(character.ToString(), string.Empty);
        }

        return (s);
    }
Helix 88
fonte
2

existem muitas soluções de trabalho aqui. apenas por uma questão de integridade, aqui está uma abordagem que não usa regex, mas usa LINQ:

var invalids = Path.GetInvalidFileNameChars();
filename = invalids.Aggregate(filename, (current, c) => current.Replace(c, '_'));

Além disso, é uma solução muito curta;)

kappadoky
fonte
1
Eu amo forros :)
Larry
1

Aqui está um método eficiente de extensão de carregamento lento, baseado no código de Andre:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LT
{
    public static class Utility
    {
        static string invalidRegStr;

        public static string MakeValidFileName(this string name)
        {
            if (invalidRegStr == null)
            {
                var invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()));
                invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);
            }

            return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "_");
        }
    }
}
Bryan Legend
fonte
0

Seu código seria mais limpo se você anexasse o diretório e o nome do arquivo e os limpasse, em vez de higienizá-los independentemente. Quanto à limpeza do:, basta pegar o segundo caractere na string. Se for igual a "replaceechar", substitua-o por dois pontos. Como esse aplicativo é para seu próprio uso, essa solução deve ser perfeitamente suficiente.

Brian
fonte
-1
using System;
using System.IO;
using System.Linq;
using System.Text;

public class Program
{
    public static void Main()
    {
        try
        {
            var badString = "ABC\\DEF/GHI<JKL>MNO:PQR\"STU\tVWX|YZA*BCD?EFG";
            Console.WriteLine(badString);
            Console.WriteLine(SanitizeFileName(badString, '.'));
            Console.WriteLine(SanitizeFileName(badString));
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    private static string SanitizeFileName(string fileName, char? replacement = null)
    {
        if (fileName == null) { return null; }
        if (fileName.Length == 0) { return ""; }

        var sb = new StringBuilder();
        var badChars = Path.GetInvalidFileNameChars().ToList();

        foreach (var @char in fileName)
        {
            if (badChars.Contains(@char)) 
            {
                if (replacement.HasValue)
                {
                    sb.Append(replacement.Value);
                }
                continue; 
            }
            sb.Append(@char);
        }
        return sb.ToString();
    }
}
Ralf
fonte