如何使用自定义字母生成GUID,其行为类似于MD5哈希在JavaScript中?

问题描述

我想知道如何在给定输入字符串的情况下生成GUID,以使相同的输入字符串产生相同的GUID(有点像MD5哈希)。 MD5散列的问题在于它们仅保证低碰撞率,而不是唯一性。相反,我想要这样的东西:

guid('v1.0.0') == 1231231231123123123112312312311231231231
guid('v1.0.1') == 6154716581615471658161547165816154716581
guid('v1.0.2') == 1883939319188393931918839393191883939319

您将如何实现这种事情(最好在JavaScript中)?有可能做吗?我不确定从哪里开始。像uuid module之类的东西不需要种子字符串,并且它们不允许您使用自定义格式/字母。

我不是在寻找canonical UUID format,而是在寻找GUID,理想情况下,它只是由整数组成。

解决方法

您需要定义文本字符串(例如“ v1.0.0”)到40位长字符串(例如“ 123123 ...”)的一对一映射。这也称为 bijection ,尽管在​​您的情况下, injection (从输入到输出(不一定是到输入的简单一对一映射))可能就足够了。如您所述,散列函数不一定能确保这种映射,但是还有其他可能性,例如全周期linear congruential generators(如果它们采用种子,您可以一对一映射到输入字符串值)或其他可逆函数。

但是,如果可能的输入字符串的集合大于可能的输出字符串的集合,则由于{,您不能将所有输入字符串与所有输出字符串一一对应(不创建重复项)。 {3}}。

例如,除非您以某种方式限制了120个字符的格式,否则通常无法将所有120个字符的字符串与所有40位数字的字符串一一对应。但是,如果您可以接受将输入字符串限制为不超过10 40 值(大约132位)的方法,或者您可以通过其他方式利用冗余,则可以解决创建40位输出字符串的问题。输入字符串,以确保将它们无损压缩到40个十进制数字(约132位)或更小,这可能是不可能的。另请参见pigeonhole principle

该算法涉及两个步骤:

  • 首先,通过建立字符串的BigInt值,类似于另一个答案中给出的charCodeAt()方法,将字符串转换为stringToInt。如果任何charCodeAt()为0x80或更大,或者结果BigInt等于或大于BigInt(alphabet_length)**BigInt(output_length),则会引发错误。
  • 然后,通过获取BigInt的mod和输出字母的大小并将每个余数替换为输出字母中的相应字符,将整数转换为另一个字符串,直到BigInt达到0。
,

一种方法是使用该答案中的方法:

/*
 * uuid-timestamp (emitter)
 * UUID v4 based on timestamp
 *
 * Created by tarkh
 * tarkh.com (C) 2020
 * https://stackoverflow.com/a/63344366/1261825
 */
const uuidEmit = () => {
  // Get now time
  const n = Date.now();
  // Generate random
  const r = Math.random(); // <- swap this
  // Stringify now time and generate additional random number
  const s = String(n) + String(~~(r*9e4)+1e4);
  // Form UUID and return it
  return `${s.slice(0,8)}-${s.slice(8,12)}-4${s.slice(12,15)}-${[8,9,'a','b'][~~(r*3)]}${s.slice(15,18)}-${s.slice(s.length-12)}`;
};

// Generate 5 UUIDs
console.log(`${uuidEmit()}
${uuidEmit()}
${uuidEmit()}
${uuidEmit()}
${uuidEmit()}`);

只需将Math.random()调用换成另一个可以获取您的种子值的随机函数。 (目前有很多算法可以创建可播种的随机方法,因此我不会尝试开出一个特定的方法。)

大多数随机种子期望数字,因此您可以通过将字符值相加(将每个字符值乘以10 ^位置,从而始终得到一个唯一的数字)来将种子字符串转换为整数:

const stringToInt = str => 
  Array.prototype.slice.call(str).reduce((result,char,index) => result += char.charCodeAt(0) * (10**(str.length - index)),0);
  
console.log(stringToInt("v1.0.0"));
console.log(stringToInt("v1.0.1"));
console.log(stringToInt("v1.0.2"));


如果您想每次都生成相同的提取字符串,则可以采用与tarkh的uuidEmit()方法类似的方法,但要摆脱那些变化的位:

const strToInt = str => 
      Array.prototype.slice.call(str).reduce((result,0);

const strToId = (str,len = 40) => {
  // Generate random
  const r = strToInt(str);
  // Multiply the number by some things to get it to the right number of digits
  const rLen = `${r}`.length; // length of r as a string
  
  // If you want to avoid any chance of collision,you can't provide too long of a string
  // If a small chance of collision is okay,you can instead just truncate the string to
  //  your desired length
  if (rLen > len) throw new Error('String too long');
  
  // our string length is n * (r+m) + e = len,so we'll do some math to get n and m
  const mMax = 9; // maximum for the exponent,too much longer and it might be represented as an exponent. If you discover "e" showing up in your string,lower this value
  let m = Math.floor(Math.min(mMax,len / rLen)); // exponent
  let n = Math.floor(len / (m + rLen)); // number of times we repeat r and m
  let e = len - (n * (rLen + m)); // extra to pad us to the right length
    
  return (new Array(n)).fill(0).map((_,i) => String(r * (i * 10**m))).join('')
    + String(10**e);
};

console.log(strToId("v1.0.0"));
console.log(strToId("v1.0.1"));
console.log(strToId("v1.0.2"));
console.log(strToId("v1.0.0") === strToId("v1.0.0")); // check they are the same
console.log(strToId("v1.0.0") === strToId("v1.0.1")); // check they are different

请注意,这仅适用于较小的字符串(可能大约10个字符),但是它应该能够避免所有冲突。您可以对其进行调整以处理较大的字符串(从stringToInt中删除乘法位),但随后可能会发生冲突。

,

我建议使用MD5 ...

在经典的生日问题之后,在所有事物都相等的情况下,在23个人中,有2个人分享生日的几率是(参见https://en.wikipedia.org/wiki/Birthday_problem)...

enter image description here

为了估算MD5碰撞,我将简化生日问题公式,以帮助预测碰撞的可能性更高。

enter image description here

请注意,尽管在生日问题中,碰撞是肯定的结果,但在MD5问题中,碰撞是否定的结果,因此,提供比预期的更高的碰撞几率可以保守估计MD5碰撞的机会。再加上在某种程度上,这种较高的预测机会可以被认为是MD5输出中任何不均匀分布的忽悠因素,尽管我不相信没有上帝计算机也无法量化这一点...

MD5哈希长度为1​​6个字节,导致范围为256 ^ 16个可能的值。假设MD5算法的结果总体上是一致的,那么假设我们创建一个四千万(即一万亿或10 ^ 15)唯一的字符串来运行哈希算法。然后使用修改后的公式(以简化碰撞计算并添加保守的软糖因子),则碰撞的几率是...

enter image description here

因此,在10 ^ 15或一个四千万个唯一输入字符串之后,哈希冲突的估计赔率与赢得强力球或超级百万大奖的赔率相提并论(每{ {3}})。

还要注意256 ^ 16是340282366920938938463463374607431768211456,它是39位数字,落在40位数字的期望范围内。

因此,建议使用MD5哈希(转换为BigInt),如果确实发生冲突,我将很高兴为您找到一张彩票,只是有机会利用您的运气并拆分收益...

注意:我使用https://www.engineeringbigdata.com/odds-winning-powerball-grand-prize-r/进行计算。

,

虽然 UUID v4 仅用于生成随机 ID,但 UUID v5 更像是给定输入字符串和命名空间的散列。非常适合您的描述。

正如你已经提到的,你可以使用这个 npm 包:

npm install uuid

而且它很容易使用。

import {v5 as uuidv5} from 'uuid';

// use a UUIDV4 as a unique namespace for your application.
// you can generate one here: https://www.uuidgenerator.net/version4
const UUIDV5_NAMESPACE = '...';

// Finally,provide the input and namespace to get your unique id.
const uniqueId = uuidv5(input,namespace);