Как сделать действительное имя файла Windows из произвольной строки?

У меня есть строка типа «Foo: Bar», которую я хочу использовать в качестве имени файла, но в Windows символ «:» не допускается в имени файла.

Есть ли метод, который превратит «Foo: Bar» во что-то вроде «Foo-Bar»?


person Ken    schedule 06.03.2009    source источник
comment
Я сделал то же самое сегодня. Я почему-то не проверял ТАК, но все равно нашел ответ.   -  person Aaron Smith    schedule 07.03.2009


Ответы (14)


Попробуйте что-то вроде этого:

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

Изменить:

Поскольку GetInvalidFileNameChars() вернет 10 или 15 символов, лучше использовать StringBuilder вместо простой строки; исходная версия займет больше времени и потребляет больше памяти.

person Diego Jancic    schedule 06.03.2009
comment
Хороший звонок на S.I.P.GIFNC. Цикл - это примерно то, чем я закончил, но я не без ума от вызова string.Replace в цикле - я надеялся, что будет встроенная функция, которая будет одновременно простой и эффективной. - person Ken; 09.03.2009
comment
Вы можете использовать StringBuilder, если хотите, но если имена короткие, и я думаю, это того не стоит. Вы также можете создать свой собственный метод для создания char [] и замены всех неправильных символов за одну итерацию. Всегда лучше сохранять простоту, если это не работает, у вас могут быть более узкие места - person Diego Jancic; 10.03.2009
comment
Я не знаю C #, но нельзя ли использовать метод remove (), который принимает набор символов? Этот набор символов, по-видимому, легко предоставляется GetInvalidFileNameChars (). Кроме того, реально, сколько раз будет повторяться этот цикл? 6 обычно, максимум 40, если функция также возвращает непечатаемый ascii, возможно? предостережение: в msdn для этой функции также упоминается, что вы должны использовать GetInvalidPathChars, поскольку GIFNC не возвращает '\' или '/', которые являются недопустимыми символами имени файла. - person Pod; 09.09.2009
comment
Я не знаю ни одного метода удаления, похожего на тот, о котором вы говорите; даже если он существует, как он может разрешиться быстрее? Единственное, что он мог сделать, - это скопировать результат GIFNC в массив, чтобы избежать накладных расходов на вызов (если они есть). Что касается другого комментария, вам следует использовать GIFNC, потому что он возвращает \ и /. Используйте Reflector, чтобы проверить статический конструктор Path, если хотите. Вот объявление в Windows (в Mono Linux может быть иначе): - person Diego Jancic; 09.09.2009
comment
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 ',': ',' * ','? ',' \\ ',' / '}; - person Diego Jancic; 09.09.2009
comment
Вероятность иметь 2+ разных недопустимых символа в строке настолько мала, что заботиться о производительности string.Replace () бессмысленно. - person Serge Wautier; 14.03.2011
comment
Создание объекта StringBuilder требует дополнительных затрат, что дороже, чем объявление строки типа значения. Я сомневаюсь, что в этом конкретном сценарии стоит использовать StringBuilder, поскольку размер строки и количество циклов настолько малы. - person NickG; 09.10.2014
comment
@NickG, это хороший аргумент. Об этом говорили и другие. Все зависит от среднего количества недопустимых символов, которые вы ожидаете иметь. Если он будет близок к нулю, используйте строку. Если вы ожидаете, что всегда будет один или несколько, я бы выбрал StringBuilder. - person Diego Jancic; 09.10.2014
comment
Отличное решение, помимо интересного, resharper предложил эту версию Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) = ›current.Replace (c, '_')); Интересно, есть ли там возможные улучшения производительности. Я сохранил оригинал для удобства чтения, так как производительность не является моей самой большой проблемой. Но если кому-то интересно, может быть стоит провести сравнительный анализ - person chrispepper1989; 24.03.2015
comment
Это не изменится. (точка / точка) символы. Вы можете убедиться, что есть только один из них для окончательного расширения файла. - person AndyM; 15.05.2016
comment
@AndyM В этом нет необходимости. file.name.txt.pdf - это действующий PDF-файл. Windows читает только последний . для расширения. - person Diego Jancic; 25.05.2016

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.
person Phil Price    schedule 06.03.2009
comment
Я никогда не знал о зарезервированных именах. Имеет смысл, хотя - person Greg Dean; 07.03.2009
comment
Кроме того, как бы то ни было, вы не можете создать имя файла, начинающееся с одного из этих зарезервированных имен, за которым следует десятичная дробь. т.е. con.air.avi - person John Conrad; 07.03.2009
comment
.foo - допустимое имя файла. Не знал о имени файла CON - для чего он нужен? - person configurator; 07.03.2009
comment
Сотрите это. CON для консоли. - person configurator; 07.03.2009
comment
Спасибо конфигуратору; Я обновил ответ, вы правы .foo действительно; однако .foo. приводит к возможным нежелательным результатам. Обновлено. - person Phil Price; 07.03.2009

Это не эффективнее, но веселее :)

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

Если кому-то нужна оптимизированная версия на основе 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;
}
person Qwertie    schedule 09.08.2014
comment
+1 за красивый и читаемый код. Облегчает чтение и обнаружение ошибок: P .. Эта функция всегда должна возвращать исходную строку, так как изменение никогда не будет истинным. - person Erti-Chris Eelmaa; 24.08.2014
comment
Спасибо, думаю, теперь лучше. Вы знаете, что они говорят об открытом исходном коде, многие глаза делают все ошибки поверхностными, поэтому мне не нужно писать модульные тесты ... - person Qwertie; 25.08.2014

Вот небольшой поворот в ответе Диего.

Если вы не боитесь 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

Да, действительно работает:

Образец проводника

Caveat Emptor

Я знал, что этот трюк будет работать с NTFS, но был удивлен, обнаружив, что он также работает с разделами FAT и FAT32. Это потому, что длинные имена файлов - это хранится в Unicode, даже еще в Windows 95 / NT. Я тестировал Win7, XP и даже маршрутизатор на базе Linux, и они показали себя нормально. Ничего не могу сказать о DOSBox.

Тем не менее, прежде чем вы сходите с ума от этого, подумайте, действительно ли вам нужна дополнительная точность. Двойники Unicode могут запутать людей или старые программы, например старые ОС полагаются на кодовые страницы.

person rkagerer    schedule 01.08.2014

Вот версия принятого ответа с использованием Linq, который использует Enumerable.Aggregate :

string fileName = "something";

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

У Диего действительно есть правильное решение, но есть одна очень маленькая ошибка. Используемая версия string.Replace должна быть string.Replace (char, char), строки нет.Replace (char, string)

Я не могу отредактировать ответ, иначе я бы просто внес незначительное изменение.

Так и должно быть:

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

Вот версия, в которой используются StringBuilder и IndexOfAny с массовым добавлением для полной эффективности. Он также возвращает исходную строку, а не создает повторяющуюся строку.

И последнее, но не менее важное: в нем есть оператор switch, который возвращает похожие символы, которые вы можете настроить по своему усмотрению. Ознакомьтесь с поиском confusables в 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, потому что неясно, какой должна быть замена.

person jnm2    schedule 08.05.2015

Еще одно простое решение:

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());
}
person GDemartini    schedule 05.04.2017

Простой однострочный код:

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, '_'));
person Moch Yusup    schedule 12.03.2020

Немного почистив свой код и сделав небольшой рефакторинг ... Я создал расширение для строкового типа:

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

Надеюсь, поможет...

Ваше здоровье

person Joan Vilariño    schedule 01.08.2013

Мне нужна была система, которая не могла создавать коллизии, поэтому я не мог сопоставить несколько символов одному. В итоге я получил:

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)));
    }
}
person mheyman    schedule 28.08.2019

Мне нужно было сделать это сегодня ... в моем случае мне нужно было объединить имя клиента с датой и временем для окончательного файла .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));

Вы даже можете заставить его заменить пробелы, если вы добавите пробел в недопустимый массив.

Возможно, он не самый быстрый, но, поскольку производительность не была проблемой, я нашел его элегантным и понятным.

Ваше здоровье!

person Joan Vilariño    schedule 01.08.2013

Вы можете сделать это с помощью команды sed:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
person D W    schedule 11.12.2010
comment
также см. более сложный, но связанный вопрос по адресу: stackoverflow.com/questions/4413427/ - person D W; 11.12.2010
comment
Почему это нужно делать на C #, а не на Bash? Теперь я вижу тег C # в исходном вопросе, но почему? - person D W; 18.10.2016
comment
Я знаю, верно, почему бы просто не передать оболочку из приложения C # в Bash, который может не быть установлен для этого? - person Peter Ritchie; 19.10.2016