C # Oczyść nazwę pliku

174

Niedawno przenosiłem kilka plików MP3 z różnych lokalizacji do repozytorium. Konstruowałem nowe nazwy plików za pomocą tagów ID3 (dzięki, TagLib-Sharp!) I zauważyłem, że otrzymuję System.NotSupportedException:

„Format podanej ścieżki nie jest obsługiwany”.

Zostało to wygenerowane przez albo File.Copy()lub Directory.CreateDirectory().

Nie trwało długo, zanim zdałem sobie sprawę, że moje nazwy plików wymagają oczyszczenia. Zrobiłem więc oczywistą rzecz:

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

Ku mojemu zdziwieniu nadal otrzymywałem wyjątki. Okazało się, że ':' nie znajduje się w zbiorze Path.GetInvalidPathChars(), ponieważ jest poprawne w katalogu głównym ścieżki. Myślę, że to ma sens - ale to musi być dość powszechny problem. Czy ktoś ma jakiś krótki kod, który oczyszcza ścieżkę? Najdokładniejszy, jaki wymyśliłem, ale wydaje mi się, że to prawdopodobnie przesada.

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

Wszelkie ulepszenia, które sprawią, że ta funkcja będzie szybsza i mniej barokowa, będą bardzo mile widziane.

Jason Sundram
źródło

Odpowiedzi:

314

Aby wyczyścić nazwę pliku, możesz to zrobić

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
źródło
3
Pytanie dotyczyło ścieżek, a nie nazw plików, a nieprawidłowe znaki są różne.
Dour High Arch
15
Może, ale ten kod na pewno pomógł mi, gdy miałem ten sam problem :)
mmr
8
I kolejny potencjalnie świetny użytkownik SO idzie na spacer ... Ta funkcja jest świetna. Dziękuję Adrevdm ...
Dan Rosenstark
19
Świetna metoda. Nie zapominaj jednak, że powściągliwe słowa nadal cię gryzą i będziesz drapać się po głowie. Źródło: Wikipedia Słowa zastrzeżone nazw plików
Spud
8
Kropki są nieprawidłowymi znakami, jeśli znajdują się na końcu nazwy pliku, więc GetInvalidFileNameCharsich nie uwzględnia. Nie zgłasza wyjątku w oknach, po prostu je usuwa, ale może spowodować nieoczekiwane zachowanie, jeśli spodziewasz się, że wystąpi okres. Zmodyfikowałem wyrażenie regularne, aby obsłużyć ten przypadek, aby .było uważane za jeden z nieprawidłowych znaków, jeśli znajduje się na końcu ciągu.
Scott Chamberlain
120

Krótsze rozwiązanie:

var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
DenNukem
źródło
1
@PeterMajeed: TIL, że liczenie linii zaczyna się od zera :-)
Gary McGill
Jest to lepsze niż najlepsza odpowiedź, szczególnie w przypadku ASP.NET Core, które mogą zwracać różne znaki na podstawie platformy.
Alexei
79

Opierając się na doskonałej odpowiedzi Andre, ale biorąc pod uwagę komentarz Spuda na temat słów zastrzeżonych, stworzyłem tę wersję:

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

A to są moje testy jednostkowe

[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);
}
placet
źródło
1
Jest to niezwykle kompletna odpowiedź, przynajmniej jeśli chodzi o część pytania dotyczącą nazwy pliku, i zasługuje na więcej głosów.
Brian MacKay
2
Drobna sugestia, ponieważ wygląda na to, że metoda zmierza w tym kierunku: Dodaj słowo kluczowe this, a stanie się ono przydatną metodą rozszerzenia. public static String CoerceValidFileName (nazwa tego pliku String)
Ryan McArthur
2
Mały błąd: ta metoda nie zmienia zarezerwowanych słów bez rozszerzeń plików (np. COM1), Które również są niedozwolone. Sugerowanym rozwiązaniem byłaby zmiana reservedWordPattern na "^{0}(\\.|$)"i zastępującego ciągu na"_reservedWord_$1"
Dehalion
31
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
dane
źródło
5
rozważ String.Concat(dirty...)zamiastJoin(String.Empty...
drzaus
DenNukem już zasugerował tę odpowiedź: stackoverflow.com/a/13617375/244916 (choć ten sam komentarz rozważ).
Koleś Pascalou,
4

Używam System.IO.Path.GetInvalidFileNameChars() metody do sprawdzania nieprawidłowych znaków i nie mam żadnych problemów.

Używam następującego kodu:

foreach( char invalidchar in System.IO.Path.GetInvalidFileNameChars())
{
    filename = filename.Replace(invalidchar, '_');
}
André Leal
źródło
3

Chciałem w jakiś sposób zachować postacie, a nie po prostu zastąpić je podkreśleniem.

Pomyślałem, że jednym ze sposobów było zastąpienie postaci podobnymi postaciami, które (w mojej sytuacji) raczej nie będą używane jako zwykłe postacie. Więc wziąłem listę nieprawidłowych znaków i znalazłem coś podobnego.

Poniżej znajdują się funkcje do kodowania i dekodowania za pomocą typu look-a-like.

Ten kod nie zawiera pełnej listy wszystkich znaków System.IO.Path.GetInvalidFileNameChars (). Więc to do Ciebie należy rozszerzenie lub wykorzystanie podkreślenia dla pozostałych znaków.

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

Możesz wybrać swój własny look-a-like. Użyłem aplikacji Mapa znaków w systemie Windows, aby wybrać moją%windir%\system32\charmap.exe

W miarę wprowadzania zmian poprzez wykrywanie zaktualizuję ten kod.

Valamas
źródło
zwróć uwagę, że istnieje wiele znaków, które wyglądają bardziej podobnie do tych, takich jak pełna szerokość !"#$%&'()*+,-./:;<=>?@{|}~ lub inne ich formy, takie jak /SOLIDUS i `` ⁄ '' FRACTION SLASH, których można bez problemu używać bezpośrednio w nazwach plików
phuclv
2

Myślę, że problem polega na tym, że najpierw wywołujesz Path.GetDirectoryNamezły ciąg. Jeśli zawiera on znaki niebędące nazwami plików, .Net nie może powiedzieć, które części ciągu są katalogami i rzutami. Musisz robić porównania ciągów.

Zakładając, że tylko nazwa pliku jest zła, a nie cała ścieżka, spróbuj tego:

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
źródło
2

W przeszłości odniosłem sukces.

Ładnie, krótko i statycznie :-)

    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
źródło
2

jest tu wiele działających rozwiązań. tylko ze względu na kompletność, oto podejście, które nie używa wyrażenia regularnego, ale używa LINQ:

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

To też bardzo krótkie rozwiązanie;)

kappadoky
źródło
1
Uwielbiam jeden liniowiec :)
Larry
1

Oto wydajna metoda rozszerzenia ładowania z opóźnieniem, oparta na kodzie 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
źródło
0

Twój kod byłby czystszy, gdybyś dołączył katalog i nazwę pliku razem i wyczyścił to, zamiast czyścić je niezależnie. Jeśli chodzi o odkażanie:, po prostu weź drugi znak w ciągu. Jeśli jest równe „replaceechar”, zamień go na dwukropek. Ponieważ ta aplikacja jest na własny użytek, takie rozwiązanie powinno w zupełności wystarczyć.

Brian
źródło
-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
źródło