How do I create a URL shortener?
我想创建一个URL缩短器服务,您可以在其中将一个长的URL写入输入字段,该服务将URL缩短为"
除"
我的方法是:
我有一个数据库表,有三列:
然后我将把长URL插入到表中。然后我将为"
我的想法是:
对于"
1 2 3 4 | short = ''; if divisible by 2, add"a"+the result to short if divisible by 3, add"b"+the result to short ... until I have divisors for a-z and A-Z. |
这可以重复,直到数字不再可除。你认为这是个好方法吗?你有更好的主意吗?
Due to the ongoing interest in this topic, I've published an efficient solution to GitHub, with implementations for JavaScript, PHP, Python and Java. Add your solutions if you like :)
我将继续您的"将数字转换为字符串"方法。但是,如果您的ID是一个素数并且大于52,那么您将认识到您的建议算法失败。
理论背景您需要一个双目标函数f。这是必需的,这样您就可以为f(123)=abc函数找到一个反函数g("abc")=123。这意味着:
- 必须没有使f(x1)=f(x2)的x1,x2(x1≠x2),
- 对于每一个y,你必须能找到一个x,这样f(x)=y。
如何将ID转换为缩短的URL
取一个自动生成的唯一数字键(例如mysql表的自动递增
对于本例,我将使用12510(125 with a base of 10).
现在您必须将12510转换为x62(base 62)。
12510=2×621+1×620=
这需要使用整数除法和模。伪代码示例:
1 2 3 4 5 6 7 8 | digits = [] while num > 0 remainder = modulo(num, 62) digits.push(remainder) num = divide(num, 62) digits = digits.reverse |
现在将索引2和1映射到字母表中。这就是您的映射(例如数组)的外观:
1 2 3 4 5 6 7 | 0 → a 1 → b ... 25 → z ... 52 → 0 61 → 9 |
使用2→c和1→b,您将收到c b62as the shorted url.
1 | http://shor.ty/cb |
如何将缩短的URL解析为初始ID
反过来更容易。你只要用字母表做一个反向查找。
e9a62将解析为"字母表中的第4、61和0个字母"。
e9a62=
现在用
一些实现(由注释者提供)
- 红宝石
- Python
- 咖啡剧本
- 哈斯克尔
- 珀尔
- C.*
为什么要使用哈希?
您可以简单地将自动递增值转换为字母数字值。通过使用一些基本转换,可以很容易地做到这一点。假设字符空间(a-z、a-z、0-9等)有40个字符,将ID转换为以40为基数的数字,并将字符用作数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class UrlShortener { private static final String ALPHABET ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int BASE = ALPHABET.length(); public static String encode(int num) { StringBuilder sb = new StringBuilder(); while ( num > 0 ) { sb.append( ALPHABET.charAt( num % BASE ) ); num /= BASE; } return sb.reverse().toString(); } public static int decode(String str) { int num = 0; for ( int i = 0; i < str.length(); i++ ) num = num * BASE + ALPHABET.indexOf(str.charAt(i)); return num; } } |
不是您问题的答案,但我不会使用区分大小写的缩写URL。它们很难记住,通常不可读(许多字体呈现1和L、0和O以及其他非常相似的字符,几乎不可能分辨出区别),并且容易出现完全正确的错误。尽量只使用小写或大写。
另外,请尝试使用一种格式,将数字和字符以预定义的形式混合在一起。有研究表明,人们比其他人更容易记住一种形式(想想电话号码,电话号码按特定形式分组)。尝试类似num char char num char char的方法。我知道这会降低组合,特别是如果你没有大小写,但它会更有用,因此也更有用。
我的方法:获取数据库ID,然后base36对其进行编码。我不会同时使用大写和小写字母,因为这会使通过电话传输这些URL成为一场噩梦,但您当然可以轻松地将该功能扩展为一个基本的62 en/解码器。
这是我的php5类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <?php class Bijective { public $dictionary ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; public function __construct() { $this->dictionary = str_split($this->dictionary); } public function encode($i) { if ($i == 0) return $this->dictionary[0]; $result = ''; $base = count($this->dictionary); while ($i > 0) { $result[] = $this->dictionary[($i % $base)]; $i = floor($i / $base); } $result = array_reverse($result); return join("", $result); } public function decode($input) { $i = 0; $base = count($this->dictionary); $input = str_split($input); foreach($input as $char) { $pos = array_search($char, $this->dictionary); $i = $i * $base + $pos; } return $i; } } |
node.js和mongodb解决方案
因为我们知道MongoDB使用的格式来创建一个12字节的新objectid。
- 表示自Unix epoch以来的秒数的4字节值,
- 一个3字节的机器标识符,
- 2字节进程ID
- 一个3字节的计数器(在您的机器中),从一个随机值开始。
示例(我选择一个随机序列)A1B2C3D4E5F6G7H8I9J1K2L3
- a1b2c3d4表示自Unix时代以来的秒数,
- 4E5F6G7表示机器标识符,
- H8I9表示进程ID
- j1k2l3表示计数器,从随机值开始。
因为如果我们将数据存储在同一台机器中,计数器将是唯一的,所以我们可以毫无疑问地得到它是重复的。
因此,短URL将是计数器,这里是一个代码片段,假设您的服务器运行正常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | const mongoose = require('mongoose'); const Schema = mongoose.Schema; // Create a schema const shortUrl = new Schema({ long_url: { type: String, required: true }, short_url: { type: String, required: true, unique: true }, }); const ShortUrl = mongoose.model('ShortUrl', shortUrl); // The user can request to get a short URL by providing a long URL using a form app.post('/shorten', function(req ,res){ // Create a new shortUrl */ // The submit form has an input with longURL as its name attribute. const longUrl = req.body["longURL"]; const newUrl = ShortUrl({ long_url : longUrl, short_url :"", }); const shortUrl = newUrl._id.toString().slice(-6); newUrl.short_url = shortUrl; console.log(newUrl); newUrl.save(function(err){ console.log("the new URL is added"); }) }); |
您可以散列整个URL,但如果您只是想缩短ID,请按照Marcel的建议执行。我编写了这个python实现:
https://gist.github.com/778542
C版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class UrlShortener { private static String ALPHABET ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static int BASE = 62; public static String encode(int num) { StringBuilder sb = new StringBuilder(); while ( num > 0 ) { sb.Append( ALPHABET[( num % BASE )] ); num /= BASE; } StringBuilder builder = new StringBuilder(); for (int i = sb.Length - 1; i >= 0; i--) { builder.Append(sb[i]); } return builder.ToString(); } public static int decode(String str) { int num = 0; for ( int i = 0, len = str.Length; i < len; i++ ) { num = num * BASE + ALPHABET.IndexOf( str[(i)] ); } return num; } } |
1 2 3 4 5 6 7 | // simple approach $original_id = 56789; $shortened_id = base_convert($original_id, 10, 36); $un_shortened_id = base_convert($shortened_id, 36, 10); |
我不断增加数据库中每个域的整数序列,并使用哈希ID将整数编码为URL路径。
1 | static hashids = Hashids(salt ="my app rocks", minSize = 6) |
我运行了一个脚本,看看它需要多长时间,直到耗尽字符长度。对于6个字符,它可以执行
hashid可以将URL路径解码回整数,但更简单的解决方案是使用整个短链接
以下是完整的概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function addDomain(domain) { table("domains").insert("domain", domain,"seq", 0) } function addURL(domain, longURL) { seq = table("domains").where("domain = ?", domain).increment("seq") shortURL = domain +"/" + hashids.encode(seq) table("links").insert("short", shortURL,"long", longURL) return shortURL } // GET /:hashcode function handleRequest(req, res) { shortURL = req.host +"/" + req.param("hashcode") longURL = table("links").where("short = ?", shortURL).get("long") res.redirect(301, longURL) } |
如果你不想重新发明轮子…http://lilurl.sourceforge.net网站/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | alphabet = map(chr, range(97,123)+range(65,91)) + map(str,range(0,10)) def lookup(k, a=alphabet): if type(k) == int: return a[k] elif type(k) == str: return a.index(k) def encode(i, a=alphabet): '''Takes an integer and returns it in the given base with mappings for upper/lower case letters and numbers 0-9.''' try: i = int(i) except Exception: raise TypeError("Input must be an integer.") def incode(i=i, p=1, a=a): # Here to protect p. if i <= 61: return lookup(i) else: pval = pow(62,p) nval = i/pval remainder = i % pval if nval <= 61: return lookup(nval) + incode(i % pval) else: return incode(i, p+1) return incode() def decode(s, a=alphabet): '''Takes a base 62 string in our alphabet and returns it in base10.''' try: s = str(s) except Exception: raise TypeError("Input must be a string.") return sum([lookup(i) * pow(62,p) for p,i in enumerate(list(reversed(s)))])a |
这是我的版本,适合任何需要它的人。
你故意省略了O,0和我吗?
我刚刚根据Ryan的解决方案创建了一个PHP类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | <?php $shorty = new App_Shorty(); echo 'ID: ' . 1000; echo '<br/> Short link: ' . $shorty->encode(1000); echo '<br/> Decoded Short Link: ' . $shorty->decode($shorty->encode(1000)); /** * A nice shorting class based on Ryan Charmley's suggestion see the link on Stack Overflow below. * @author Svetoslav Marinov (Slavi) | http://WebWeb.ca * @see http://stackoverflow.com/questions/742013/how-to-code-a-url-shortener/10386945#10386945 */ class App_Shorty { /** * Explicitly omitted: i, o, 1, 0 because they are confusing. Also use only lowercase ... as * dictating this over the phone might be tough. * @var string */ private $dictionary ="abcdfghjklmnpqrstvwxyz23456789"; private $dictionary_array = array(); public function __construct() { $this->dictionary_array = str_split($this->dictionary); } /** * Gets ID and converts it into a string. * @param int $id */ public function encode($id) { $str_id = ''; $base = count($this->dictionary_array); while ($id > 0) { $rem = $id % $base; $id = ($id - $rem) / $base; $str_id .= $this->dictionary_array[$rem]; } return $str_id; } /** * Converts /abc into an integer ID * @param string * @return int $id */ public function decode($str_id) { $id = 0; $id_ar = str_split($str_id); $base = count($this->dictionary_array); for ($i = count($id_ar); $i > 0; $i--) { $id += array_search($id_ar[$i - 1], $this->dictionary_array) * pow($base, $i - 1); } return $id; } } ?> |
不知道是否有人会发现这一点有用——它更像是一个"hack n slash"方法,但是如果您只需要特定的字符,它很简单并且工作得很好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | $dictionary ="abcdfghjklmnpqrstvwxyz23456789"; $dictionary = str_split($dictionary); // Encode $str_id = ''; $base = count($dictionary); while($id > 0) { $rem = $id % $base; $id = ($id - $rem) / $base; $str_id .= $dictionary[$rem]; } // Decode $id_ar = str_split($str_id); $id = 0; for($i = count($id_ar); $i > 0; $i--) { $id += array_search($id_ar[$i-1], $dictionary) * pow($base, $i - 1); } |
下面是一个不错的PHP URL编码函数…
1 2 3 4 5 6 7 8 9 10 | // From http://snipplr.com/view/22246/base62-encode--decode/ private function base_encode($val, $base=62, $chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') { $str = ''; do { $i = fmod($val, $base); $str = $chars[$i] . $str; $val = ($val - $i) / $base; } while($val > 0); return $str; } |
Xeoncross类中的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function shortly($input){ $dictionary = ['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','0','1','2','3','4','5','6','7','8','9']; if($input===0) return $dictionary[0]; $base = count($dictionary); if(is_numeric($input)){ $result = []; while($input > 0){ $result[] = $dictionary[($input % $base)]; $input = floor($input / $base); } return join("", array_reverse($result)); } $i = 0; $input = str_split($input); foreach($input as $char){ $pos = array_search($char, $dictionary); $i = $i * $base + $pos; } return $i; } |
我有一个不同的问题,因为我存储了许多不同作者的网页,需要通过猜测来防止发现网页。因此,我的短URL为页码的base-62字符串添加了几个额外的数字。这些额外的数字是由页记录本身的信息生成的,它们确保3844个URL中只有1个有效(假设2位数的base-62)。您可以在http://mgscan.com/mbwl上看到大纲说明。
对于类似的项目,为了获得一个新的键,我在一个调用生成器的随机字符串生成器周围创建了一个包装函数,直到得到一个尚未在哈希表中使用的字符串为止。一旦您的名称空间开始满了,这个方法就会变慢,但是正如您所说,即使只有6个字符,您也有足够的名称空间可以使用。
这是我使用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # Generate a [0-9a-zA-Z] string ALPHABET = map(str,range(0, 10)) + map(chr, range(97, 123) + range(65, 91)) def encode_id(id_number, alphabet=ALPHABET): """Convert an integer to a string.""" if id_number == 0: return alphabet[0] alphabet_len = len(alphabet) # Cache result = '' while id_number > 0: id_number, mod = divmod(id_number, alphabet_len) result = alphabet[mod] + result return result def decode_id(id_string, alphabet=ALPHABET): """Convert a string to an integer.""" alphabet_len = len(alphabet) # Cache return sum([alphabet.index(char) * pow(alphabet_len, power) for power, char in enumerate(reversed(id_string))]) |
它非常快,可以用长整数。
很好的答案,我创建了一个bjf的golang实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | package bjf import ( "math" "strings" "strconv" ) const alphabet ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func Encode(num string) string { n, _ := strconv.ParseUint(num, 10, 64) t := make([]byte, 0) /* Special case */ if n == 0 { return string(alphabet[0]) } /* Map */ for n > 0 { r := n % uint64(len(alphabet)) t = append(t, alphabet[r]) n = n / uint64(len(alphabet)) } /* Reverse */ for i, j := 0, len(t) - 1; i < j; i, j = i + 1, j - 1 { t[i], t[j] = t[j], t[i] } return string(t) } func Decode(token string) int { r := int(0) p := float64(len(token)) - 1 for i := 0; i < len(token); i++ { r += strings.Index(alphabet, string(token[i])) * int(math.Pow(float64(len(alphabet)), p)) p-- } return r } |
在GitHub托管:https://github.com/xor-gate/go-bjf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /** * <p> * Integer to character and vice-versa * </p> * */ public class TinyUrl { private final String characterMap ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private final int charBase = characterMap.length(); public String covertToCharacter(int num){ StringBuilder sb = new StringBuilder(); while (num > 0){ sb.append(characterMap.charAt(num % charBase)); num /= charBase; } return sb.reverse().toString(); } public int covertToInteger(String str){ int num = 0; for(int i = 0 ; i< str.length(); i++) num += characterMap.indexOf(str.charAt(i)) * Math.pow(charBase , (str.length() - (i + 1))); return num; } } class TinyUrlTest{ public static void main(String[] args) { TinyUrl tinyUrl = new TinyUrl(); int num = 122312215; String url = tinyUrl.covertToCharacter(num); System.out.println("Tiny url: " + url); System.out.println("Id:" + tinyUrl.covertToInteger(url)); } } |
我的python 3版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | base_list = list("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") base = len(base_list) def encode(num: int): result = [] if num == 0: result.append(base_list[0]) while num > 0: result.append(base_list[num % base]) num //= base print("".join(reversed(result))) def decode(code: str): num = 0 code_list = list(code) for index, code in enumerate(reversed(code_list)): num += base_list.index(code) * base ** index print(num) if __name__ == '__main__': encode(341413134141) decode("60FoItT") |
这里有一个node.js实现,它可能会bit.ly。生成高度随机的七个字符串。
它使用node.js crypto生成高度随机的25个字符集,而不是随机选择7个字符。
1 2 3 4 5 6 7 8 9 10 11 | var crypto = require("crypto"); exports.shortURL = new function () { this.getShortURL = function () { var sURL = '', _rand = crypto.randomBytes(25).toString('hex'), _base = _rand.length; for (var i = 0; i < 7; i++) sURL += _rand.charAt(Math.floor(Math.random() * _rand.length)); return sURL; }; } |
有关quality node.js/javascript解决方案,请参阅ID shortener模块,该模块经过了全面测试,已在生产中使用数月。
它提供了一个高效的ID/URL缩短器,由默认为Redis的可插入存储支持,您甚至可以自定义短ID字符集,以及缩短是否是等量的。这是一个重要的区别,并不是所有的URL缩短器都要考虑到。
关于这里的其他答案,本模块实现了马塞尔·杰克沃思的上述优秀的公认答案。
解决方案的核心由以下redis-lua片段提供:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | local sequence = redis.call('incr', KEYS[1]) local chars = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz' local remaining = sequence local slug = '' while (remaining > 0) do local d = (remaining % 60) local character = string.sub(chars, d + 1, d + 1) slug = character .. slug remaining = (remaining - d) / 60 end redis.call('hset', KEYS[2], slug, ARGV[1]) return slug |
在scala中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Encoder(alphabet: String) extends (Long => String) { val Base = alphabet.size override def apply(number: Long) = { def encode(current: Long): List[Int] = { if (current == 0) Nil else (current % Base).toInt :: encode(current / Base) } encode(number).reverse .map(current => alphabet.charAt(current)).mkString } } class Decoder(alphabet: String) extends (String => Long) { val Base = alphabet.size override def apply(string: String) = { def decode(current: Long, encodedPart: String): Long = { if (encodedPart.size == 0) current else decode(current * Base + alphabet.indexOf(encodedPart.head),encodedPart.tail) } decode(0,string) } } |
scala测试的测试示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import org.scalatest.{FlatSpec, Matchers} class DecoderAndEncoderTest extends FlatSpec with Matchers { val Alphabet ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" "A number with base 10" should"be correctly encoded into base 62 string" in { val encoder = new Encoder(Alphabet) encoder(127) should be ("cd") encoder(543513414) should be ("KWGPy") } "A base 62 string" should"be correctly decoded into a number with base 10" in { val decoder = new Decoder(Alphabet) decoder("cd") should be (127) decoder("KWGPy") should be (543513414) } } |
为什么不把你的ID翻译成字符串呢?您只需要一个函数将介于0和61之间的数字映射到单个字母(大写/小写)或数字。然后应用这个来创建,比如说,4个字母的代码,你就有1470万个URL被覆盖了。