Nom du fichier C # Sanitize

174

Récemment, j'ai déplacé un tas de MP3 de divers endroits dans un référentiel. J'avais construit les nouveaux noms de fichiers en utilisant les balises ID3 (merci, TagLib-Sharp!), Et j'ai remarqué que j'obtenais un System.NotSupportedException:

"Le format du chemin donné n'est pas pris en charge."

Cela a été généré par File.Copy()ou par Directory.CreateDirectory().

Il n'a pas fallu longtemps pour réaliser que mes noms de fichiers devaient être nettoyés. Alors j'ai fait la chose évidente:

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

À ma grande surprise, j'ai continué à recevoir des exceptions. Il s'est avéré que «:» n'est pas dans l'ensemble de Path.GetInvalidPathChars(), car il est valide dans une racine de chemin. Je suppose que cela a du sens - mais cela doit être un problème assez courant. Quelqu'un a-t-il un code court qui nettoie un chemin? Le plus approfondi que j'ai proposé, mais j'ai l'impression que c'est probablement exagéré.

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

Toute amélioration pour rendre cette fonction plus rapide et moins baroque serait très appréciée.

Jason Sundram
la source

Réponses:

314

Pour nettoyer un nom de fichier, vous pouvez le faire

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, "_" );
}
André
la source
3
La question concernait les chemins, pas les noms de fichiers, et les caractères non valides pour ceux-ci sont différents.
Dour High Arch
15
Peut-être, mais ce code m'a certainement aidé quand j'ai eu le même problème :)
mmr
8
Et un autre utilisateur de SO potentiellement génial va marcher ... Cette fonction est géniale. Merci Adrevdm ...
Dan Rosenstark
19
Excellente méthode. N'oubliez pas que les mots réservés vous mordront toujours et que vous vous gratterez la tête. Source: Wikipédia Nom de fichier mots réservés
Spud
8
Les points sont des caractères non valides s'ils se trouvent à la fin du nom de fichier et GetInvalidFileNameCharsne les incluent donc pas. Il ne lève pas d'exception dans les fenêtres, il les supprime simplement, mais cela pourrait provoquer un comportement inattendu si vous vous attendez à ce que la période soit là. J'ai modifié l'expression rationnelle pour gérer ce cas afin .d'être considéré comme l'un des caractères invalides s'il se trouve à la fin de la chaîne.
Scott Chamberlain
120

Une solution plus courte:

var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
DenNukem
la source
1
@PeterMajeed: TIL que le comptage de lignes commence à zéro :-)
Gary McGill
C'est mieux que la réponse principale, en particulier pour ASP.NET Core, qui peut renvoyer différents caractères en fonction de la plate-forme.
Alexei
79

Sur la base de l'excellente réponse d'André mais en tenant compte du commentaire de Spud sur les mots réservés, j'ai fait cette version:

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

Et ce sont mes tests unitaires

[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);
}
décret
la source
1
C'est une réponse extrêmement complète, au moins à la partie nom de fichier de la question, et mérite plus de votes positifs.
Brian MacKay
2
Suggestion mineure car il semble que la méthode allait dans cette direction: ajoutez un mot-clé this et cela devient une méthode d'extension pratique. public static String CoerceValidFileName (this String filename)
Ryan McArthur
2
Petit bogue: cette méthode ne change pas les mots réservés sans extensions de fichier (par exemple COM1), qui sont également interdites. La solution suggérée serait de changer le reservedWordPattern en "^{0}(\\.|$)"et la chaîne de remplacement en"_reservedWord_$1"
Dehalion
31
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
Les données
la source
5
considérer String.Concat(dirty...)au lieu deJoin(String.Empty...
drzaus
DenNukem a déjà suggéré cette réponse: stackoverflow.com/a/13617375/244916 (même commentaire, cependant).
Mec Pascalou
4

J'utilise la System.IO.Path.GetInvalidFileNameChars() méthode pour vérifier les caractères invalides et je n'ai aucun problème.

J'utilise le code suivant:

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

Je voulais conserver les caractères d'une certaine manière, pas simplement remplacer le caractère par un trait de soulignement.

Une façon dont j'ai pensé était de remplacer les caractères par des caractères similaires qui sont (dans ma situation) peu susceptibles d'être utilisés comme caractères normaux. J'ai donc pris la liste des caractères invalides et j'ai trouvé des sosies.

Ce qui suit sont des fonctions pour encoder et décoder avec les look-a-likes.

Ce code n'inclut pas une liste complète pour tous les caractères System.IO.Path.GetInvalidFileNameChars (). C'est donc à vous d'étendre ou d'utiliser le remplacement de soulignement pour tous les caractères restants.

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

Vous pouvez sélectionner vos propres looks-a-likes. J'ai utilisé l'application Character Map dans Windows pour sélectionner la mienne%windir%\system32\charmap.exe

Au fur et à mesure que je fais des ajustements grâce à la découverte, je mettrai à jour ce code.

Valamas
la source
notez qu'il existe de nombreux caractères qui ressemblent plus à ceux-ci, comme la forme pleine largeur !"#$%&'()*+,-./:;<=>?@{|}~ ou d'autres formes de ceux-ci comme /SOLIDUS et `⁄` FRACTION SLASH qui peuvent être utilisés directement dans les noms de fichiers sans problème
phuclv
2

Je pense que le problème est que vous appelez d'abord Path.GetDirectoryNamela mauvaise chaîne. Si cela contient des caractères sans nom de fichier, .Net ne peut pas dire quelles parties de la chaîne sont des répertoires et des renvois. Vous devez faire des comparaisons de chaînes.

En supposant que seul le nom de fichier est incorrect, pas le chemin complet, essayez ceci:

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();
}
Dour High Arch
la source
2

J'ai eu du succès avec cela dans le passé.

Sympa, court et statique :-)

    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
la source
2

il y a beaucoup de solutions de travail ici. juste pour des raisons d'exhaustivité, voici une approche qui n'utilise pas regex, mais utilise LINQ:

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

C'est aussi une solution très courte;)

kappadoky
la source
1
J'adore une doublure :)
Larry
1

Voici une méthode d'extension de chargement différé efficace basée sur le code d'André:

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 Légende
la source
0

Votre code serait plus propre si vous ajoutiez le répertoire et le nom de fichier ensemble et les nettoyiez plutôt que de les nettoyer indépendamment. Pour nettoyer le:, prenez simplement le 2ème caractère de la chaîne. S'il est égal à "replacechar", remplacez-le par deux points. Puisque cette application est pour votre propre usage, une telle solution devrait être parfaitement suffisante.

Brian
la source
-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
la source