如何在C#中生成唯一的文件名


131

我已经实现了一种算法,该算法将为要保存在硬盘上的文件生成唯一的名称。我要附加DateTime小时,分钟,秒和毫秒,但仍会生成重复的文件名,因为我一次上传了多个文件。

为要存储在硬盘驱动器上的文件生成唯一名称从而没有两个文件相同的最佳解决方案是什么?


这取决于其他要求;这个[旧]问题太模糊了。
user2864740

Answers:


240

如果可读性无关紧要,请使用GUID

例如:

var myUniqueFileName = string.Format(@"{0}.txt", Guid.NewGuid());

或者更短

var myUniqueFileName = $@"{Guid.NewGuid()}.txt";

在我的程序中,有时我尝试尝试10次以生成一个可读的名称(“ Image1.png”…“ Image10.png”),如果失败(因为该文件已经存在),我将退回到GUID。

更新:

最近,我还使用了DateTime.Now.Ticks代替GUID的方法:

var myUniqueFileName = string.Format(@"{0}.txt", DateTime.Now.Ticks);

要么

var myUniqueFileName = $@"{DateTime.Now.Ticks}.txt";

对我来说,好处是,与GUID相比,它生成的文件名更短,看上去更“漂亮”。

请注意,在某些情况下(例如,在很短的时间内生成大量随机名称时),这可能会产生不唯一的值。

如果要真正确保文件名是唯一的,请坚持使用GUID,即使将其传输到其他计算机时也是如此。


7
我喜欢使用Ticks作为GUID真的很丑。您还可以获取Ticks的哈希值,以减少文件名的字符长度。DateTime.Now.Ticks.GetHashCode().ToString("x").ToUpper()
WillMcKill

4
“滴答”是可预测的,并且不是线程安全的(因为可以从多个线程/进程中获得相同的“滴答”)。这使其不适用于临时文件名生成。生成X..1..N可能适合于面向用户的任务(即,在Explorer中进行复制),但对于服务器工作而言却是个疑问。
user2864740

90

Path.GetTempFileName()

或使用新的GUID()。

MSDN上的Path.GetTempFilename()


这里是链接到MSDN文档:msdn.microsoft.com/en-us/library/...
epotter

3
但是请注意,GetTempFileName()如果您创建许多此类文件而不删除它们,则可能会引发异常。
乔伊,

21
“如果将GetTempFileName方法用于创建超过65535个文件而不删除以前的临时文件,它将引发IOException。” MSDN文章说。
Çağdaş特勤

1
警告:GetTempFileName创建一个文件。这也意味着它选择了临时路径位置。另一方面,GetRandomFileName适用于生成可与其他路径一起使用的8.3 文件名。(我看过一些使用GetTempFileName和File.Delete只是在其他地方使用文件名的可怕代码。)
user2864740


54

如果文件名的可读性不重要,那么许多人建议使用GUID。但是,我发现查找包含1000个GUID文件名的目录非常困难。因此,我通常使用静态字符串的组合,该组合为文件名提供了一些上下文信息,时间戳和GUID。

例如:

public string GenerateFileName(string context)
{
    return context + "_" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + "_" + Guid.NewGuid().ToString("N");
}

filename1 = GenerateFileName("MeasurementData");
filename2 = GenerateFileName("Image");

这样,当我按文件名排序时,它将自动按上下文字符串将文件分组并按时间戳排序。

请注意,Windows中的文件名限制为255个字符。


1
+1对于建议包括有用的信息以及 GUID。-附带一点麻烦:只要可以,在文件名中包含日期和时间是多余的Right Click > Sort By > Date
蒂莫西·希尔兹

1
如果将一堆具有不同上下文的文件存储在同一目录中,那么时间将变得非常有用。当然,应根据您的特定需求调整文件名的生成。
2013年

应该是Guid.NewGuid().ToString();。缺少括号。+1否则
洛朗W.

这是非常光滑的。时间戳和向导。+1
JoshYates1980

我喜欢此解决方案+1,我添加了第二个参数字符串扩展名,并将其添加到fileName中,这进一步支持了上下文的概念,并允许在必要时双击默认应用程序轻松打开文件
shelbypereira

23

这是一种算法,该算法根据提供的原始文件返回唯一的可读文件名。如果原始文件存在,它将以增量方式尝试将索引添加到文件名,直到找到不存在的索引。它将现有的文件名读取到HashSet中以检查冲突,因此它非常快(我的机器上每秒几百个文件名),它也是线程安全的,并且不受竞争条件的影响。

例如,如果传递它test.txt,它将尝试按以下顺序创建文件:

test.txt
test (2).txt
test (3).txt

等等。您可以指定最大尝试次数,或仅将其保留为默认值。

这是一个完整的示例:

class Program
{
    static FileStream CreateFileWithUniqueName(string folder, string fileName, 
        int maxAttempts = 1024)
    {
        // get filename base and extension
        var fileBase = Path.GetFileNameWithoutExtension(fileName);
        var ext = Path.GetExtension(fileName);
        // build hash set of filenames for performance
        var files = new HashSet<string>(Directory.GetFiles(folder));

        for (var index = 0; index < maxAttempts; index++)
        {
            // first try with the original filename, else try incrementally adding an index
            var name = (index == 0)
                ? fileName
                : String.Format("{0} ({1}){2}", fileBase, index, ext);

            // check if exists
            var fullPath = Path.Combine(folder, name);
            if(files.Contains(fullPath))
                continue;

            // try to create the file
            try
            {
                return new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write);
            }
            catch (DirectoryNotFoundException) { throw; }
            catch (DriveNotFoundException) { throw; }
            catch (IOException) 
            {
                // Will occur if another thread created a file with this 
                // name since we created the HashSet. Ignore this and just
                // try with the next filename.
            } 
        }

        throw new Exception("Could not create unique filename in " + maxAttempts + " attempts");
    }

    static void Main(string[] args)
    {
        for (var i = 0; i < 500; i++)
        {
            using (var stream = CreateFileWithUniqueName(@"c:\temp\", "test.txt"))
            {
                Console.WriteLine("Created \"" + stream.Name + "\"");
            }
        }

        Console.ReadKey();
    }
}

线程安全的?不可static readonly 也不lock
Kiquenet '17

该方法本身是静态的,因此不共享任何内容,因此我相信多个线程可以安全地同时进入此方法。也许线程安全不是一个正确的术语-我试图传达的是,如果另一个线程/进程在执行过程中创建了一个名称冲突的文件,则此方法将恢复并尝试下一个可用名称。如果您认为可以改进,请随时进行编辑。
Mike Chamberlain

也许“不遭受比赛条件”是一种更好的表达方式。
Mike Chamberlain

10

我使用GetRandomFileName

GetRandomFileName方法返回一个加密强度高的随机字符串,可以用作文件夹名称或文件名称。与GetTempFileName不同,GetRandomFileName不会创建文件。如果文件系统的安全性至关重要,则应使用此方法代替GetTempFileName。

例:

public static string GenerateFileName(string extension="")
{
    return string.Concat(Path.GetRandomFileName().Replace(".", ""),
        (!string.IsNullOrEmpty(extension)) ? (extension.StartsWith(".") ? extension : string.Concat(".", extension)) : "");
}

GetRandomFileName()方法是否总是每次都类似于GUID()生成唯一文件名?
Ashish Shukla

1
@AshishShukla实际上我不知道。msdn说“生成了加密强度高的随机字符串”。到目前为止,我没有任何问题。如果唯一性很关键,那么额外检查可能是个好主意。
Koray

3
  1. 按照常规过程创建带时间戳的文件名
  2. 检查文件名是否存在
  3. 错误-保存文件
  4. True-将其他字符附加到文件中,也许是计数器
  5. 前往步骤2

10
这种算法是vunerable并发
的Jader迪亚斯

3

您可以为您自动生成一个唯一的文件名,而无需任何自定义方法。只需将以下内容与StorageFolder类StorageFile类一起使用。这里的关键是:CreationCollisionOption.GenerateUniqueNameNameCollisionOption.GenerateUniqueName

创建具有唯一文件名的新文件:

var myFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("myfile.txt", NameCollisionOption.GenerateUniqueName);

要将文件复制到具有唯一文件名的位置:

var myFile2 = await myFile1.CopyAsync(ApplicationData.Current.LocalFolder, myFile1.Name, NameCollisionOption.GenerateUniqueName);

要在目标位置移动具有唯一文件名的文件:

await myFile.MoveAsync(ApplicationData.Current.LocalFolder, myFile.Name, NameCollisionOption.GenerateUniqueName);

重命名目标位置中具有唯一文件名的文件:

await myFile.RenameAsync(myFile.Name, NameCollisionOption.GenerateUniqueName);

2

我一直在使用以下代码及其正常工作。希望对您有所帮助。

我首先使用时间戳来指定唯一的文件名-

“ context_” + DateTime.Now.ToString(“ yyyyMMddHHmmssffff”)

C#代码-

public static string CreateUniqueFile(string logFilePath, string logFileName, string fileExt)
    {
        try
        {
            int fileNumber = 1;

            //prefix with . if not already provided
            fileExt = (!fileExt.StartsWith(".")) ? "." + fileExt : fileExt;

            //Generate new name
            while (File.Exists(Path.Combine(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt)))
                fileNumber++;

            //Create empty file, retry until one is created
            while (!CreateNewLogfile(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt))
                fileNumber++;

            return logFileName + "-" + fileNumber.ToString() + fileExt;
        }
        catch (Exception)
        {
            throw;
        }
    }

    private static bool CreateNewLogfile(string logFilePath, string logFile)
    {
        try
        {
            FileStream fs = new FileStream(Path.Combine(logFilePath, logFile), FileMode.CreateNew);
            fs.Close();
            return true;
        }
        catch (IOException)   //File exists, can not create new
        {
            return false;
        }
        catch (Exception)     //Exception occured
        {
            throw;
        }
    }

1

您是否需要文件名中的日期时间戳?

您可以将文件名设为GUID。


@downvoter有任何否决理由吗?文件名的GUID似乎是这个问题的流行答案。
Larry Hipp

这是一个重复的回答,我没有足够的声誉可以拒绝投票
Jader Dias

@XMLforDummies我的回答是第一个。现在可能看起来不太像,因为它只显示了现在的时间。这是重复的答案,因为它可能是正确的答案。
拉里·希普


1

如何使用Guid.NewGuid()创建GUID并将其用作文件名(如果愿意,可以使用文件名的一部分以及时间戳)。


1

我编写了一个简单的递归函数,该函数通过在文件扩展名之前附加序列号来生成与Windows相似的文件名。

给定所需的文件路径为C:\MyDir\MyFile.txt,并且该文件已存在,它将返回最终文件路径C:\MyDir\MyFile_1.txt

它的名称如下:

var desiredPath = @"C:\MyDir\MyFile.txt";
var finalPath = UniqueFileName(desiredPath);

private static string UniqueFileName(string path, int count = 0)
{
    if (count == 0)
    {
        if (!File.Exists(path))
        {
            return path;
        }
    }
    else
    {
        var candidatePath = string.Format(
            @"{0}\{1}_{2}{3}",
            Path.GetDirectoryName(path),
            Path.GetFileNameWithoutExtension(path),
            count,
            Path.GetExtension(path));

        if (!File.Exists(candidatePath))
        {
            return candidatePath;
        }
    }

    count++;
    return UniqueFileName(path, count);
}

这不是线程安全或进程安全的。File.Exists检查和任何(后来认为)文件创建都存在竞争条件。通常,当连续两次调用而不创建文件时,它将返回相同的结果。
user2864740

1

我们为什么不能按以下方式创建唯一的ID。

我们可以使用DateTime.Now.Ticks和Guid.NewGuid()。ToString()组合在一起并创建唯一的ID。

添加DateTime.Now.Ticks后,我们可以找到创建唯一ID的日期和时间(以秒为单位)。

请查看代码。

var ticks = DateTime.Now.Ticks;
var guid = Guid.NewGuid().ToString();
var uniqueSessionId = ticks.ToString() +'-'+ guid; //guid created by combining ticks and guid

var datetime = new DateTime(ticks);//for checking purpose
var datetimenow = DateTime.Now;    //both these date times are different.

我们甚至可以使用唯一ID中的刻度线部分,并在以后检查日期和时间以供将来参考。

您可以将创建的唯一ID附加到文件名中,或者可以用于创建唯一的会话ID,以便用户登录到我们的应用程序或网站。


如果有GUID,为什么还要打扰“滴答声”?
user2864740

在任何情况下,如果需要检查何时生成uniqueSessionId,则将获得准确的时间。而且在那个特定的滴答声中,一生只会发生一次。
Jineesh Uvantavida

琐碎地讲,关于滴答的假设是无效的:1)如果足够快地查询,多个观察者可以看到相同的“滴答”(认为线程/进程),并且2)同一观察者可以多次观察相同的“滴答”。
user2864740

但是,通过使用 Guid.NewGuid(忽略该事实(在某些情况下可能不是有趣的事实),我们就可以断言,如果我们有足够高的概率不关心其他情况,则唯一ID将被生成-这比“滴答声”要高得多。因此,“滴答”没有任何价值/用途,因为“次要”数据被推入文件名。
user2864740

(FWIW:我刚刚用前面提到的关于“唯一时间”的断言修复了一些代码。)
user2864740

0

如果您想获取日期时间,小时,分钟等,则可以使用静态变量。将此变量的值附加到文件名。您可以从0开始计数,并在创建文件后递增。这样,文件名肯定是唯一的,因为文件中还有秒数。


0

我通常按​​照以下方式做一些事情:

  • 以词干文件名开头(work.dat1例如)
  • 尝试使用CreateNew创建它
  • 如果可行,那么您已经有了文件,否则...
  • 将当前日期/时间混入文件名(work.2011-01-15T112357.dat例如)
  • 尝试创建文件
  • 如果可行,那么您已经有了文件,否则...
  • 将单调计数器混入文件名(work.2011-01-15T112357.0001.dat例如。(我不喜欢GUID。我更喜欢顺序/可预测性。)
  • 尝试创建文件。继续勾选计数器,然后重试,直到为您创建文件为止。

这是一个示例类:

static class DirectoryInfoHelpers
{
    public static FileStream CreateFileWithUniqueName( this DirectoryInfo dir , string rootName )
    {
        FileStream fs = dir.TryCreateFile( rootName ) ; // try the simple name first

        // if that didn't work, try mixing in the date/time
        if ( fs == null )
        {
            string date = DateTime.Now.ToString( "yyyy-MM-ddTHHmmss" ) ;
            string stem = Path.GetFileNameWithoutExtension(rootName) ;
            string ext  = Path.GetExtension(rootName) ?? ".dat" ;

            ext = ext.Substring(1);

            string fn = string.Format( "{0}.{1}.{2}" , stem , date , ext ) ;
            fs = dir.TryCreateFile( fn ) ;

            // if mixing in the date/time didn't work, try a sequential search
            if ( fs == null )
            {
                int seq = 0 ;
                do
                {
                    fn = string.Format( "{0}.{1}.{2:0000}.{3}" , stem , date , ++seq , ext ) ;
                    fs = dir.TryCreateFile( fn ) ;
                } while ( fs == null ) ;
            }

        }

        return fs ;
    }

    private static FileStream TryCreateFile(this DirectoryInfo dir , string fileName )
    {
        FileStream fs = null ;
        try
        {
            string fqn = Path.Combine( dir.FullName , fileName ) ;

            fs = new FileStream( fqn , FileMode.CreateNew , FileAccess.ReadWrite , FileShare.None ) ;
        }
        catch ( Exception )
        {
            fs = null ;
        }
        return fs ;
    }

}

您可能需要调整算法(例如,始终使用所有可能的组件作为文件名)。取决于上下文-例如,如果我正在创建日志文件,我可能想轮换使用,那么您希望它们全部共享与该名称相同的模式。

代码不是完美的(例如,不检查传入的数据)。而且该算法也不是完美的(例如,如果您填满硬盘驱动器或遇到权限,实际的I / O错误或其他文件系统错误,则这种情况将无限期地挂起)。




0

我为此专门编写了一个类。它使用“基本”部分进行初始化(默认为精确到分钟的时间戳),之后再附加字母以组成唯一的名称。因此,如果生成的第一个戳记是1907101215a,则第二个戳记是1907101215b,然后是1907101215c等。

如果我需要超过25张独特的邮票,那么我会使用一元“ z”来计数25个。因此,它进入1907101215y,1907101215za,1907101215zb,... 1907101215zy,1907101215zza,1907101215zzb等,依此类推。这保证了图章将始终按其生成顺序按字母数字顺序排序(只要图章后面的下一个字符不是字母)。

它不是线程安全的,不会自动更新时间,并且在需要数百个邮票时会很快膨胀,但是我发现它足以满足我的需要。

/// <summary>
/// Class for generating unique stamps (for filenames, etc.)
/// </summary>
/// <remarks>
/// Each time ToString() is called, a unique stamp is generated.
/// Stamps are guaranteed to sort alphanumerically in order of generation.
/// </remarks>
public class StampGenerator
{
  /// <summary>
  /// All the characters which could be the last character in the stamp.
  /// </summary>
  private static readonly char[] _trailingChars =
  {
    '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'
  };

  /// <summary>
  /// How many valid trailing characters there are.
  /// </summary>
  /// <remarks>Should always equal _trailingChars.Length</remarks>
  public const int TRAILING_RANGE = 25;

  /// <summary>
  /// Maximum length of the stamp. Hard-coded for laziness.
  /// </summary>
  public const int MAX_LENGTH_STAMP = 28;

  /// <summary>
  /// Base portion of the stamp. Will be constant between calls.
  /// </summary>
  /// <remarks>
  /// This is intended to uniquely distinguish between instances.
  /// Default behavior is to generate a minute-accurate timestamp.
  /// </remarks>
  public string StampBase { get; }

  /// <summary>
  /// Number of times this instance has been called.
  /// </summary>
  public int CalledTimes { get; private set; }

  /// <summary>
  /// Maximum number of stamps that can be generated with a given base.
  /// </summary>
  public int MaxCalls { get; }

  /// <summary>
  /// Number of stamps remaining for this instance.
  /// </summary>
  public int RemainingCalls { get { return MaxCalls - CalledTimes; } }

  /// <summary>
  /// Instantiate a StampGenerator with a specific base.
  /// </summary>
  /// <param name="stampBase">Base of stamp.</param>
  /// <param name="calledTimes">
  /// Number of times this base has already been used.
  /// </param>
  public StampGenerator(string stampBase, int calledTimes = 0)
  {
    if (stampBase == null)
    {
      throw new ArgumentNullException("stampBase");
    }
    else if (Regex.IsMatch(stampBase, "[^a-zA-Z_0-9 \\-]"))
    {
      throw new ArgumentException("Invalid characters in Stamp Base.",
                                  "stampBase");
    }
    else if (stampBase.Length >= MAX_LENGTH_STAMP - 1)
    {
      throw new ArgumentException(
        string.Format("Stamp Base too long. (Length {0} out of {1})",
                      stampBase.Length, MAX_LENGTH_STAMP - 1), "stampBase");
    }
    else if (calledTimes < 0)
    {
      throw new ArgumentOutOfRangeException(
        "calledTimes", calledTimes, "calledTimes cannot be negative.");
    }
    else
    {
      int maxCalls = TRAILING_RANGE * (MAX_LENGTH_STAMP - stampBase.Length);
      if (calledTimes >= maxCalls)
      {
        throw new ArgumentOutOfRangeException(
          "calledTimes", calledTimes, string.Format(
            "Called Times too large; max for stem of length {0} is {1}.",
            stampBase.Length, maxCalls));
      }
      else
      {
        StampBase = stampBase;
        CalledTimes = calledTimes;
        MaxCalls = maxCalls;
      }
    }
  }

  /// <summary>
  /// Instantiate a StampGenerator with default base string based on time.
  /// </summary>
  public StampGenerator() : this(DateTime.Now.ToString("yMMddHHmm")) { }

  /// <summary>
  /// Generate a unique stamp.
  /// </summary>
  /// <remarks>
  /// Stamp values are orered like this:
  /// a, b, ... x, y, za, zb, ... zx, zy, zza, zzb, ...
  /// </remarks>
  /// <returns>A unique stamp.</returns>
  public override string ToString()
  {
    int zCount = CalledTimes / TRAILING_RANGE;
    int trailing = CalledTimes % TRAILING_RANGE;
    int length = StampBase.Length + zCount + 1;

    if (length > MAX_LENGTH_STAMP)
    {
      throw new InvalidOperationException(
        "Stamp length overflown! Cannot generate new stamps.");
    }
    else
    {
      CalledTimes = CalledTimes + 1;
      var builder = new StringBuilder(StampBase, length);
      builder.Append('z', zCount);
      builder.Append(_trailingChars[trailing]);
      return builder.ToString();
    }
  }
}

-1

DateTime.Now.Ticks不安全,Guid.NewGuid()太丑陋,如果您需要清洁且几乎安全的物品(例如,如果在1ms内调用1,000,000次,则不是100%安全),请尝试:

Math.Abs(Guid.NewGuid().GetHashCode())

保险柜是指您在短短几毫秒的时间内多次调用保险柜时就变得独一无二。


我的解决方案下载人有问题吗?请告诉我。
Mehdi Dehghani

GetHashCode方法返回int范围为32位的,而范围GUID为128位的,因此更有可能是唯一的。如果您不喜欢GUID值的格式,只需调用ToString("N")它即可删除破折号。
4thex
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.