簡體   English   中英

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

[英]How to Generate unique file names in C#

我已經實現了一種算法,該算法將為將保存在硬盤驅動器上的文件生成唯一名稱。 我正在附加DateTime : hours, Minutes,Second 和 Milliseconds但它仍然會生成文件的重復名稱,因為我一次上傳多個文件。

為要存儲在硬盤驅動器上的文件生成唯一名稱以便沒有 2 個文件相同的最佳解決方案是什么?

如果可讀性無關緊要,請使用GUIDs

例如:

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。

采用

Path.GetTempFileName()

或使用新的 GUID()。

MSDN 上的 Path.GetTempFilename()

System.IO.Path.GetRandomFileName()

MSDN 上的 Path.GetRandomFileName()

如果文件名的可讀性不重要,那么很多人建議使用 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 個字符。

這是一種基於提供的原始文件返回唯一可讀文件名的算法。 如果原始文件存在,它會逐漸嘗試將索引附加到文件名,直到找到不存在的索引。 它將現有文件名讀入 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();
    }
}

我使用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)) : "");
}

您可以在沒有任何自定義方法的情況下為您自動生成唯一的文件名。 只需將以下內容與StorageFolder ClassStorageFile Class 一起使用 這里的關鍵是: 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);
  1. 按照正常流程創建帶時間戳的文件名
  2. 檢查文件名是否存在
  3. 錯誤 - 保存文件
  4. True - 向文件附加額外的字符,可能是一個計數器
  5. 轉到第 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;
        }
    }

為什么我們不能創建一個唯一的 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。

我編寫了一個簡單的遞歸函數,它通過在文件擴展名之前附加一個序列號來像 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);
}

如何使用Guid.NewGuid()創建一個 GUID 並將其用作文件名(或文件名的一部分以及您的時間戳,如果您願意)。

您也可以使用 Random.Next() 生成隨機數。 你可以看到 MSDN 鏈接: http : //msdn.microsoft.com/en-us/library/9b3ta19y.aspx

如果您想要日期時間、小時、分鍾等。您可以使用靜態變量。 將此變量的值附加到文件名。 您可以從 0 開始計數器,並在創建文件時遞增。 這樣文件名肯定是唯一的,因為文件中還有幾秒鍾。

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

  • 以主干文件名work.dat1 (例如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 錯誤或其他文件系統錯誤,這將掛起,就目前而言,無限循環)。

我最終將 GUID 與 Day Month Year Second Millisecond 字符串連接起來,我認為這個解決方案在我的場景中非常好

我專門為此編寫了一個類。 它使用“基本”部分(默認為精確到分鍾的時間戳)進行初始化,然后附加字母以形成唯一名稱。 因此,如果生成的第一個戳記是 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();
    }
  }
}

DateTime.Now.Ticks不安全, Guid.NewGuid()太難看,如果您需要一些干凈且幾乎安全的東西(例如,如果您在 1ms 內調用它 1,000,000 次,它不是 100% 安全的),請嘗試:

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

安全我的意思是當你在很短的幾毫秒時間內多次調用它時安全是獨一無二的。

老問題,我知道,但這是對我有用的方法。 如果多個線程下載文件,則為每個線程分配一個唯一編號並在其前面添加文件名,例如 01_202107210938xxxx

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM