代码之家  ›  专栏  ›  技术社区  ›  caw

如何创建URL缩短器?

  •  613
  • caw  · 技术社区  · 15 年前

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

    而不是“ abcdef “可以有任何其他包含六个字符的字符串 a-z, A-Z and 0-9 . 这使得56~57亿个弦成为可能。

    我的方法是:

    我有一个数据库表,有三列:

    1. ID,整数,自动递增
    2. long,string,用户输入的长URL
    3. 短、字符串、缩短的URL(或仅六个字符)

    然后我将把长URL插入到表中。然后我将选择“的自动增量值。” id “然后建立一个散列。然后应将此哈希插入为“ 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.
    

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

    由于对这个话题的兴趣,我已经 published an efficient solution to GitHub ,实现 JavaScript , PHP , Python Java . 如果愿意,请添加解决方案:)

    27 回复  |  直到 6 年前
        1
  •  749
  •   Marcel Jackwerth    6 年前

    我将继续您的“将数字转换为字符串”方法。但是,如果您的ID是 素数大于52 .

    理论背景

    你需要一个 Bijective Function f . 这是必要的,这样你就可以找到一个反函数 g(‘ABC’)=123 为了你 F(123)=ABC 功能。这意味着:

    • 一定没有 x1,x2(带x1__x2) 这将使 f(x1)=f(x2) ,
    • 为每一个 Y 你必须能找到一个 X 以便 f(x)=y .

    如何将ID转换为缩短的URL

    1. 想想我们要用的字母表。在你的情况下,这是 [a-zA-Z0-9] . 它包含 62封信 .
    2. 使用自动生成的唯一数字键(自动递增 id 例如,mysql表)。

      对于这个例子,我将使用125 (125,底座10)。

    3. 现在你必须转换125 到X 六十二 (基部62)。

      一百二十五 2=62 + 1×62 = [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,您将收到CB 六十二 作为缩短的URL。

      http://shor.ty/cb
      

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

    反过来更容易。你只要用字母表做一个反向查找。

    1. E9A 六十二 将被解析为“字母表中的第4、61和0个字母”。

      E9A 六十二 = [4,61,0] 4=62 + 61×62 + 0×62 = 19158

    2. 现在查找数据库记录 WHERE id = 19158 并执行重定向。

    一些实现(由注释者提供)

        2
  •  51
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    为什么要使用哈希?

    您可以简单地将自动递增值转换为字母数字值。通过使用一些基本转换,可以很容易地做到这一点。假设字符空间(a-z、a-z、0-9等)有40个字符,将ID转换为以40为基数的数字,并将字符用作数字。

        3
  •  46
  •   Boris    9 年前
    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;
        }   
    }
    
        4
  •  31
  •   Ash    15 年前

    不是您问题的答案,但我不会使用区分大小写的缩写URL。它们很难记住,通常不可读(许多字体呈现1和L、0和O以及其他非常相似的字符,几乎不可能分辨出区别),并且容易出现完全正确的错误。尽量只使用小写或大写。

    另外,请尝试使用一种格式,将数字和字符以预定义的形式混合在一起。有研究表明,人们比其他人更容易记住一种形式(想想电话号码,电话号码按特定形式分组)。尝试类似num char char num char char的方法。我知道这会降低组合,特别是如果你没有大小写,但它会更有用,因此也更有用。

        5
  •  26
  •   Michael Stum    15 年前

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

        6
  •  7
  •   Xeoncross    13 年前

    这是我的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;
        }
    }
    
        7
  •  3
  •   user1477388    10 年前

    C版本:

    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;
        }   
    }
    
        8
  •  3
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    您可以散列整个URL,但如果您只是想缩短ID,请按照Marcel的建议执行。我编写了这个python实现:

    https://gist.github.com/778542

        9
  •  2
  •   Alister Bulman    15 年前

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

        10
  •  2
  •   MrChrisRodriguez plam4u    15 年前
    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
    

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

        11
  •  2
  •   phirschybar    13 年前
    // simple approach
    
    $original_id = 56789;
    
    $shortened_id = base_convert($original_id, 10, 36);
    
    $un_shortened_id = base_convert($shortened_id, 36, 10);
    
        12
  •  2
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    node.js和mongodb解决方案

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

    • 表示自Unix epoch以来的秒数的4字节值,
    • 一个3字节的机器标识符,
    • 2字节进程ID
    • 一个3字节的计数器(在您的机器中),从一个随机值开始。

    示例(我选择一个随机序列) A1B2C3D4E5F6G7H8I9J1K2L3

    • a1b2c3d4表示自Unix时代以来的秒数,
    • 4E5F6G7表示机器标识符,
    • H8I9表示进程ID
    • j1k2l3表示计数器,从随机值开始。

    因为如果我们将数据存储在同一台机器中,计数器将是唯一的,所以我们可以毫无疑问地得到它是重复的。

    所以短网址就是计数器 这里有一个代码片段,假设您的服务器运行正常。

    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");
        })
    });
    
        13
  •  2
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    我不断增加数据库中每个域的整数序列,并使用 Hashids 将整数编码为URL路径。

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

    我运行了一个脚本,看看它需要多长时间,直到耗尽字符长度。六个字符就可以了 164,916,224 链接,然后最多七个字符。位使用七个字符。在我看来,不到五个字符看起来很奇怪。

    哈希德 可以将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)
    }
    
        14
  •  1
  •   Simon E. pbs    13 年前

    下面是一个不错的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;
    }
    
        15
  •  1
  •   Ryan Charmley    12 年前

    不知道是否有人会发现这一点有用——它更像是一个“hack n slash”方法,但是如果您只需要特定的字符,它很简单并且工作得很好。

    $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);
    } 
    
        16
  •  0
  •   cr333    15 年前

    为什么不把你的ID翻译成字符串呢?您只需要一个函数将介于0和61之间的数字映射到单个字母(大写/小写)或数字。然后应用这个来创建,比如说,4个字母的代码,你就有1470万个URL被覆盖了。

        17
  •  0
  •   Davide Muzzarelli    13 年前

    这是我使用的:

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

    它非常快,可以用长整数。

        18
  •  0
  •   Joel Berger    13 年前

    对于类似的项目,为了获得一个新的键,我在 random string generator 它调用生成器,直到得到一个尚未在哈希表中使用的字符串。一旦您的名称空间开始满了,这个方法就会变慢,但是正如您所说,即使只有6个字符,您也有足够的名称空间可以使用。

        19
  •  0
  •   Graham    9 年前

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

        20
  •  0
  •   Jerry Jacobs    9 年前

    很好的答案,我创建了一个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

        21
  •  0
  •   Hrishikesh Mishra    8 年前
    /**
     * <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));
        }
    }
    
        22
  •  0
  •   adrift    7 年前

    在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)
      }
    
    }
    
        23
  •  0
  •   Luis Neighbur    6 年前

    Xeoncross类中的函数

    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;
    }
    
        24
  •  0
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

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

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

    <?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;
            }
        }
    ?>
    
        25
  •  0
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    这里有一个node.js实现,它可能会bit.ly。生成高度随机的七个字符串。

    它使用node.js crypto生成高度随机的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;
        };
    }
    
        26
  •  0
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    我的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")
    
        27
  •  0
  •   Peter Mortensen Abd Al-Kareem Attiya    6 年前

    有关quality node.js/javascript解决方案,请参见 id-shortener 模块,经过彻底测试,已在生产中使用数月。

    它提供了一个高效的ID/URL缩短器,支持可插入存储,默认为 雷迪斯 您甚至可以自定义短ID字符集 幂等元 . 这是一个重要的区别,并不是所有的URL缩短器都要考虑到。

    关于这里的其他答案,本模块实现了马塞尔·杰克沃思的上述优秀的公认答案。

    解决方案的核心由以下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