类似YouTube的GUID

72

是否可能生成像YouTube中的短GUID(例如N7Et6c9nL9w)?

如何实现?我想在Web应用程序中使用它。


请访问 https://www.nuget.org/packages/shortid 以获取有关 shortid 的信息。 - Sevenate
9个回答

76

你可以使用Base64:

string base64Guid = Convert.ToBase64String(Guid.NewGuid().ToByteArray());

这将生成一个类似于E1HKfn68Pkms5zsZsvKONw==的字符串。由于GUID始终为128位,因此可以省略你知道将始终出现在末尾的==,从而得到一个22个字符长的字符串。但这不如YouTube的短。


37
这种方法的缺点是,生成的值可能包含斜杠(/)符号,如果处理不当,在URL中使用时可能会不方便。 - Jhonny D. Cano -Leftware-
3
虽然我非常喜欢这个解决方案,但我同意Jhonny D.的看法,不仅会出现'/',而且还可能出现'+',这将完全破坏你的URL。 叹气 - c0d3p03t
6
将 '/' 和 '+' 替换为 URL 安全字符,例如 '-' 和 '_'。然后,在读取 guid 时,在解码之前将它们替换回来即可。 - Buns of Aluminum
3
将问题字符(如+和/)替换为URL友好的字符(例如-和_),仍然能够保证它是唯一的 GUID 吗? - neildt
1
@neildt 是的,因为这些字符永远不会出现在Base64中。这是一种“1:1”映射转换,因此不会发生冲突。 - Dai
显示剩余2条评论

32

URL友好的解决方案

如接受的答案所述,base64是一个不错的解决方案,但如果您想在URL中使用GUID,则可能会出现问题。这是因为+和/是有效的base64字符,但在URL中具有特殊含义。

幸运的是,在base64中有一些未使用的字符是URL友好的。以下是更完整的答案:

public string ToShortString(Guid guid)
{
    var base64Guid = Convert.ToBase64String(guid.ToByteArray());

    // Replace URL unfriendly characters
    base64Guid = base64Guid.Replace('+', '-').Replace('/', '_');

    // Remove the trailing ==
    return base64Guid.Substring(0, base64Guid.Length - 2);
}

public Guid FromShortString(string str)
{
    str = str.Replace('_', '/').Replace('-', '+');
    var byteArray = Convert.FromBase64String(str + "==");
    return new Guid(byteArray);
}

使用方法:

Guid guid = Guid.NewGuid();
string shortStr = ToShortString(guid);
// shortStr will look something like 2LP8GcHr-EC4D__QTizUWw
Guid guid2 = FromShortString(shortStr);
Assert.AreEqual(guid, guid2);

编辑:

我们能做得更好吗?(理论极限)

上面的方法可以生成22个字符、友好的URL GUID。 这是因为GUID使用了128位,所以在base64中表示需要 log_{64}2^128 个字符,即21.33,四舍五入为22。

实际上有 66 个 URL 友好的字符(我们不使用 . and ~)。因此,从理论上讲,我们可以使用 base66 来获得 log_{66}2^128 个字符,即21.17,也四舍五入为22。

因此,这对于完整的有效 GUID 是最优的。

然而,GUID 使用了 6 位来表示版本和变体,在我们的情况下是固定的。因此,我们技术上只需要 122 位,在两个基数中都是 21 (log_{64}2^122 = 20.33)。因此,通过更多的操作,我们可以再减少一个字符。这需要处理位数,所以我把这留给读者练习。

YouTube 如何做到的?

YouTube ID 使用了11个字符。他们是如何做到的?

GUID 使用了122位,这保证了碰撞几乎不可能发生。这意味着你可以生成一个随机 GUID,并且肯定是唯一的而无需检查。但是,我们对于普通 ID 并不需要这么多的位数。

我们可以使用较小的 ID。如果我们使用 66 位或更少的位数,就有更高的碰撞风险,但可以用 11 个字符(甚至在 base64 中)表示此 ID。你可以接受碰撞的风险,或者测试并重新生成。

使用 122 位(常规 GUID),你需要生成约 10^17 个 GUID 才能有 1% 的碰撞率。

使用 66 位,则需要生成约 10^9 或 10 亿个 ID 才有 1% 的碰撞几率。那不是很多 ID。

我猜测 YouTube 使用了 64 位(比 66 位更节省内存),并检查碰撞以重新生成 ID,如果必要的话。

如果你想放弃 GUID,转而使用较小的 ID,则可以使用以下代码:

class IdFactory
{
    private Random random = new Random();
    public int CharacterCount { get; }
    public IdFactory(int characterCount)
    {
        CharacterCount = characterCount;
    }

    public string Generate()
    {
        // bitCount = characterCount * log (targetBase) / log(2)
        var bitCount = 6 * CharacterCount;
        var byteCount = (int)Math.Ceiling(bitCount / 8f);
        byte[] buffer = new byte[byteCount];
        random.NextBytes(buffer);

        string guid = Convert.ToBase64String(buffer);
        // Replace URL unfriendly characters
        guid = guid.Replace('+', '-').Replace('/', '_');
        // Trim characters to fit the count
        return guid.Substring(0, CharacterCount);
    }
}

用法:

var factory = new IdFactory(characterCount: 11);
string guid = factory.Generate();
// guid will look like Mh3darwiZhp

这使用了64个字符,虽然不是最佳选择,但需要的代码量较少(因为我们可以重复使用Convert.ToBase64String)。 如果您使用此操作,请更加小心碰撞。


既然您知道Base64字符串的长度始终为22个字符而不需要填充,那么您是否可以直接使用base64Guid.Substring(0, 22)而不是base64Guid.Substring(0, base64Guid.Length - 2)呢? - crush
1
是的,这两个选项是等价的。我认为我的版本使操作更清晰一些。 - Gilthans
字符串替换是对现有答案的修复,那么为什么不使用Guid.NewGuid().ToString("N")呢? - Vinod Srivastav
guid.ToString("N") 将返回一个长度为32个字符的字符串,这并不是真正的迷你。 替换是为了对该GUID进行Base64编码,因为Base64默认使用/和+作为字符,而我们不想要它们。 - Gilthans

11

9个字符不是GUID。鉴于此,您可以使用int的十六进制表示形式,从而获得8个字符的字符串。

您可以使用可能已经拥有的ID。此外,您可以针对不同的简单类型使用.GetHashCode方法,从而获得不同的int。您还可以异或不同的字段。如果您感兴趣,甚至可以使用随机数 - 嘿,如果您坚持使用正值,您将拥有超过20亿个可能的值;)


10

这不是一个GUID,而是一个随机的base64编码字符串

那么关于冲突呢? 由于他们拥有庞大的基础设施和遍布全球的服务器,在每分钟上传数千个视频的情况下,这是一种安全的方式。在将您的视频后处理为不同分辨率期间,系统可以进行检查。

请参见以下代码,我正在尝试相同的操作,它使用EPOCHTotalMilliseconds生成带有有效字符集的字符串,其独特性由每过去的毫秒数控制。这是基于来自 Tom Scott的视频的思路。

另一种方法是使用数字计数器,但这种方法维护起来很耗费,并且会创建一个系列,可以枚举与系统中以前或下一个唯一字符串的+-值,我们不希望发生这种情况。

请记住:
  • 这不是全局唯一的,而是在定义它的实例中唯一
  • 它使用Thread.Sleep()来处理多线程问题
public static string YoutubeLikeId()
{
    Thread.Sleep(1);
    string characterSet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    var charSet = characterSet.ToCharArray();   
    int targetBase= charSet.Length;
    long ticks = (long)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    
    string output = null;

    do{
        output += charSet[ticks % targetBase];
        ticks = ticks/targetBase;
    }while(ticks > 0);
    
    output = new string(output.Reverse().ToArray());
    
    return Convert.ToBase64String(Encoding.UTF8.GetBytes(output)).Replace("/", "_")
        .Replace("+", "-").Replace("==","");
}

在这里,我们将删除填充字符==以及非URL友好字符/+,并用-_替换它们。

因此,输出将类似于:

VFlRTFk4Mw
VFlRTFk4SQ
VFlRTFk4WA
VFlRTFk4bQ
VFlRTFk5Mg
VFlRTFk5SQ
VFlRTFk5WA
VFlRTFk5bg
VFlRTFlBMw
VFlRTFlBSQ

还有一个名为ShortGuid的项目,可以获得一个友好的URL GUID,它可以从/转换为常规的Guid

当我深入了解时,我发现它通过将Guid编码为Base64来实现,如下所示:

public static string Encode(Guid guid)
{
    string encoded = Convert.ToBase64String(guid.ToByteArray());

    encoded = encoded
        .Replace("/", "_")
        .Replace("+", "-");
    return encoded.Substring(0, 22);
}

它的好处是可以再次解码以获取Guid

public static Guid Decode(string value)
{
    // avoid parsing larger strings/blobs
    if (value.Length != 22)
    {
        throw new ArgumentException("A ShortGuid must be exactly 22 characters long. Receive a character string.");
    }

    string base64 = value
        .Replace("_", "/")
        .Replace("-", "+") + "==";

    byte[] blob = Convert.FromBase64String(base64);
    var guid = new Guid(blob);

    var sanityCheck = Encode(guid);
    if (sanityCheck != value)
    {
        throw new FormatException(
            @"Invalid strict ShortGuid encoded string. The string '{value}' is valid URL-safe Base64, " +
            @"but failed a round-trip test expecting '{sanityCheck}'."
        );
    }

    return guid;
}

所以一个 Guid 4039124b-6153-4721-84dc-f56f5b057ac2 将被编码为 SxI5QFNhIUeE3PVvWwV6wg,输出将类似于以下内容。
ANf-MxRHHky2TptaXBxcwA
zpjp-stmVE6ZCbOjbeyzew
jk7P-XYFokmqgGguk_530A
81t6YZtkikGfLglibYkDhQ
qiM2GmqCK0e8wQvOSn-zLA

正如Leonadro在评论中提到的那样,如果您不想自己实现,还有一个叫做nanoid的东西可以用于此目的。


1
我不喜欢在没有提供证据的情况下对YouTube ID进行递增假设。此外,nanoid相当好地解决了OP的问题。 - Leonardo Herrera

7
正如其他人所提到的,YouTube的“VideoId”并不是技术上的GUID,因为它本身并不唯一。
根据维基百科的说法:
总唯一键数为2的128次方或3.4×10的38次方。这个数字非常大,随机生成两次相同的数字的概率很小。
YouTube的“VideoId”的唯一性是由它们的生成算法维护的。
您可以编写自己的算法,或者使用某种随机字符串生成器,并利用SQL中的“UNIQUE CONSTRAINT”约束来强制实施其唯一性。
首先,在数据库中创建一个“UNIQUE CONSTRAINT”:
ALTER TABLE MyTable
ADD CONSTRAINT UniqueUrlId
UNIQUE (UrlId);

例如,生成一个随机字符串(来自philipproplesch的答案):

string shortUrl = System.Web.Security.Membership.GeneratePassword(11, 0);

如果生成的UrlId足够随机且足够长,当SQL遇到重复的UrlId时,你应该很少遇到抛出的异常。在这种情况下,您可以轻松处理Web应用程序中的异常。

GeneratePassword 方法唯一的问题在于第二个参数实际上是用来指定非字母和数字字符最小数量的。当我尝试使用 0 时,会得到多个这样的符号... - Azimuth

4

从技术上来说,这不是一个Guid。Youtube有一个简单的随机字符串生成器,您可以使用允许字符的数组和随机数生成器在几分钟内轻松创建它。


3

这可能不是最好的解决方案,但你可以尝试类似这样的方法:

string shortUrl = System.Web.Security.Membership.GeneratePassword(11, 0);

1
这种方法唯一的问题是第二个参数实际上是用于非字母和非数字字符的最小数量。当我尝试使用 0 时,会得到几个这样的符号... - Azimuth

2

2
这个id可能不是全局唯一的。GUID应该是全局唯一的,因为它们包括其他地方不会出现的元素(生成ID的机器的MAC地址、生成ID的时间等)。
如果您需要的是在应用程序内唯一的ID,请使用数字喷泉 - 可以将值编码为十六进制数。每次需要一个ID时,请从数字喷泉中获取它。
如果您有多台服务器分配ID,则可以获取一系列数字(几十或几千个,具体取决于您分配ID的速度),这样就可以完成任务了。8位十六进制数将给您提供40亿个ID - 但您的第一个ID将要短得多。

抱歉,打扰了,什么是数字喷泉?在网上找不到定义。它只是一个整数,在每次请求新的ID时原子地递增,还是有一些更深层次的逻辑? - alsed42

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接