首页 文章

如何创建URL缩短器?

提问于
浏览
599

我想创建一个URL缩短服务,您可以在其中将长URL写入输入字段,该服务将URL缩短为“ http://www.example.org/abcdef ” .

可以是包含 a-z, A-Z and 0-9 的六个字符的任何其他字符串,而不是“ abcdef ” . 这使得56到570亿个可能的字符串 .

我的方法:

我有一个包含三列的数据库表:

  • id,整数,自动递增

  • long,string,用户输入的长URL

  • short,string,缩短的URL(或只是六个字符)

然后我会将长URL插入表中 . 然后我会选择“ id " and build a hash of it. This hash should then be inserted as " short ”的自动增量值 . 但是我应该构建什么样的哈希?像MD5这样的散列算法会创建太长的字符串 . 我想,我不使用这些算法 . 自建算法也可以工作 .

我的想法:

对于“ http://www.google.de/ ”,我得到自动增量ID 239472 . 然后我执行以下步骤:

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.

这可以重复,直到数字不再可分 . 你认为这是一个好方法吗?你有更好的主意吗?

由于对该主题的持续关注,我发布了一个有效的GitHub解决方案,包括JavaScript,PHP,Python和Java的实现 . 如果你愿意,添加你的解决方案

27 回答

  • 46

    这是一个适合PHP的URL编码功能...

    // 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;
    }
    
  • 732

    我会继续你的"convert number to string"方法 . 但是,如果您的ID是素数并且大于52,您将意识到您提出的算法会失败 .

    理论背景

    你需要Bijective Function f . 这是必要的,这样你就可以找到f(123)= 'abc'函数的反函数g('abc')= 123 . 这意味着:

    • 必须没有x1,x2(x1≠x2)才能使f(x1)= f(x2),

    • 并且对于每个y,您必须能够找到x,以便f(x)= y .

    如何将ID转换为缩短的URL

    • 想想我们想要使用的字母 . 在你的情况下是 [a-zA-Z0-9] . 它包含62个字母 .

    • 获取自动生成的唯一数字键(例如,MySQL表的自动递增 id ) .

    对于这个例子,我将使用12510(125的基数为10) .

    • 现在您必须将12510转换为X62(基准62) .

    12510 = 2×621 1×620 = [2,1]

    这需要使用整数除法和模数 . 一个伪代码示例:

    digits = []
    
    while num > 0
      remainder = modulo(num, 62)
      digits.push(remainder)
      num = divide(num, 62)
    
    digits = digits.reverse
    

    现在将索引2和1映射到您的字母表 . 这是您的映射(例如,使用数组)的样子:

    0  → a
    1  → b
    ...
    25 → z
    ...
    52 → 0
    61 → 9
    

    使用2→c和1→b,您将收到cb62作为缩短的URL .

    http://shor.ty/cb
    

    如何将缩短的URL解析为初始ID

    反过来更容易 . 您只需在字母表中执行反向查找 .

    • e9a62将被解析为"4th, 61st, and 0th letter in alphabet" .

    e9a62 = [4,61,0] = 4×622 61×621 0×620 = 1915810

    • 现在使用 WHERE id = 19158 找到您的数据库记录并执行重定向 .

    一些实现(由评论者提供)

  • 0

    你为什么要使用哈希?

    您可以使用自动增量值的简单转换为字母数字值 . 您可以使用一些基本转换轻松完成此操作 . 假设您的字符空间(A-Z,a-z,0-9等)有40个字符,将id转换为base-40数字并将字符用作数字 .

  • 2
    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;
        }   
    }
    
  • 3

    不是您问题的答案,但我不会使用区分大小写的缩短网址 . 它们很难记住,通常是不可读的(许多字体渲染1和l,0和O以及其他字符非常相似,几乎不可能分辨出来)并且容易出错 . 尽量只使用小写或大写 .

    此外,尝试使用一种格式,以预定义的形式混合数字和字符 . 有研究表明,人们倾向于比其他人更好地记住一种形式(想想电话号码,其中数字以特定形式分组) . 尝试使用num-char-char-num-char-char之类的东西 . 我知道这会降低组合,特别是如果你没有大小写,但它会更有用,因此更有用 .

  • 0

    我的方法:获取数据库ID,然后Base36 Encode it . 我不会同时使用大写和小写字母,因为这会使通过电话传输这些URL成为一场噩梦,但您当然可以轻松地将该功能扩展为基本62 /解码器 .

  • 0

    这是我的PHP 5课程 .

    <?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;
        }
    }
    
  • 2

    C# version:

    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

    您可以对整个URL进行哈希处理,但如果您只想缩短ID,请按照marcel的建议进行操作 . 我写了这个Python实现:

    https://gist.github.com/778542

  • 0

    如果你不想重新发明轮子...... http://lilurl.sourceforge.net/

  • 30
    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
    

    这是我的版本,适合任何需要它的人 .

  • 0
    // simple approach
    
    $original_id = 56789;
    
    $shortened_id = base_convert($original_id, 10, 36);
    
    $un_shortened_id = base_convert($shortened_id, 36, 10);
    
  • 0

    Node.js和MongoDB解决方案

    因为我们知道MongoDB用来创建一个12字节的新ObjectId的格式 .

    • 一个4字节的值,表示自Unix纪元以来的秒数,

    • 一个3字节的机器标识符,

    • 一个2字节的进程ID

    • 一个3字节计数器(在您的机器中),以随机值开始 .

    示例(我选择随机序列) a1b2c3d4e5f6g7h8i9j1k2l3

    • a1b2c3d4表示自Unix纪元以来的秒数,

    • 4e5f6g7代表机器标识符,

    • h8i9表示进程ID

    • j1k2l3表示计数器,以随机值开头 .

    由于如果我们将数据存储在同一台机器中,计数器将是唯一的,我们可以毫不怀疑它将是重复的 .

    So the short URL will be the counter 这是一个代码片段,假设您的服务器正常运行 .

    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");
        })
    });
    
  • 2

    不知道是否有人会发现这有用 - 它更像是一种'黑客n斜线'方法,但如果你只想要特定的字符,它很简单并且效果很好 .

    $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);
    }
    
  • 50

    我不断在数据库中为每个域递增一个整数序列,并使用Hashids将整数编码为URL路径 .

    static hashids = Hashids(salt = "my app rocks", minSize = 6)
    

    我运行了一个脚本,看看它耗尽了多长时间才能消耗字符长度 . 对于六个字符,它可以执行 164,916,224 链接,然后最多可以输出七个字符 . 有点使用七个字符 . 五个字符对我来说很奇怪 .

    Hashids可以将URL路径解码回整数,但更简单的解决方案是使用整个短链接 sho.rt/ka8ds3 作为主键 .

    这是完整的概念:

    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)
    }
    
  • 3

    为什么不将你的id翻译成字符串?您只需要一个将数字(例如0到61)之间的数字映射到单个字母(大写/小写)或数字的函数 . 然后将其应用于创建4个字母的代码,并且您已覆盖了1470万个URL .

  • 2

    这是我用的:

    # 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))])
    

    它非常快,可以采用长整数 .

  • 1

    对于一个类似的项目,为了得到一个新的密钥,我围绕一个调用生成器的random string generator创建一个包装器函数,直到我得到一个尚未在我的哈希表中使用的字符串 . 一旦你的名字空间开始变满,这个方法会变慢,但正如你所说,即使只有6个字符,你也有足够的命名空间可供使用 .

  • 0

    我有一个问题的变体,因为我存储来自许多不同作者的网页,需要通过猜测来防止发现页面 . 所以我的短网址为Base-62字符串添加了几个额外的数字作为页码 . 这些额外数字是根据页面记录本身中的信息生成的,它们确保3844个URL中只有1个有效(假设2位Base-62) . 您可以在http://mgscan.com/MBWL查看大纲说明 .

  • 0

    非常好的答案,我已经创建了bjf的Golang实现:

    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

  • 0
    /**
     * <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));
        }
    }
    
  • 0

    在Scala中实现:

    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测试的测试示例:

    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)
      }
    
    }
    
  • 1

    基于Xeoncross Class的函数

    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;
    }
    
  • 0

    你故意省略O,0和我吗?

    我刚刚根据Ryan的解决方案创建了一个PHP类 .

    <?php
    
        $shorty = new App_Shorty();
    
        echo 'ID: ' . 1000;
        echo '
    Short link: ' . $shorty->encode(1000); echo '
    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; } } ?>
  • 26

    这是一个很可能是bit.ly的Node.js实现 . 生成一个高度随机的七个字符的字符串 .

    它使用Node.js加密来生成高度随机的25个字符集,而不是随机选择7个字符 .

    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;
        };
    }
    
  • 0

    我的Python 3版本

    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")
    
  • 7

    有关高质量的Node.js / JavaScript解决方案,请参阅id-shortener模块,该模块经过全面测试,已在 生产环境 中使用了数月 .

    它提供了一个有效的id / URL缩短器,由可插入存储默认 Redis 支持,您甚至可以自定义短ID字符集以及缩短是否是幂等的 . 这是一个重要的区别,并非所有URL缩短程序都考虑在内 .

    关于此处的其他答案,本模块实现了Marcel Jackwerth在上面的优秀接受答案 .

    该解决方案的核心由以下Redis Lua提供snippet

    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
    

相关问题