Как закодировать url ссылками?

Я хочу создать службу сокращения URL, где вы можете записать длинный URL в поле ввода, и служба сокращает URL до"http://www.example.org/abcdef".

Edit: из-за постоянного интереса к этой теме у меня опубликовано эффективное решение для GitHub, С реализации JavaScript, PHP, Python и Java. Добавьте свои решения, если хотите :)

вместо "abcdef " может быть любая другая строка с шестью символами, содержащими a-z, A-Z and 0-9. Это составляет 56~57 миллиардов возможных строк.

мой подход:

у меня есть таблица базы данных с тремя столбцами:

  1. id, целое число, автоматическое приращение
  2. long, string, длинный URL, введенный пользователем
  3. short, string, сокращенный URL (или только шесть символов)

Я бы тогда вставьте длинный URL-адрес в таблице. Затем я бы выбрал значение автоматического приращения для"id" и построить хэш-кода. Затем этот хэш должен быть вставлен как "short". Но какой гашиш мне делать? Хэш-алгоритмы, такие как MD5, создают слишком длинные строки. Думаю, я не использую эти алгоритмы. Самодельный алгоритм тоже будет работать.

мои мысли:

для "http://www.google.de/ " Я получаю идентификатор автоматического приращения 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.

что может повторяться до тех пор, пока число не станет делимым. Как вы думаете, это хороший подход? У тебя есть идея получше?

27 ответов


Я бы продолжил ваш подход "преобразовать число в строку". Однако вы поймете, что ваш предлагаемый алгоритм терпит неудачу, если ваш ID является премьер-и больше, чем 52.

теоретические основы

вам понадобится Биективная Функция f. Это необходимо для того, чтобы вы могли найти обратную функцию g ('abc') = 123 для f (123) = 'abc'.

  • возьмите автоматически сгенерированный уникальный числовой ключ (автоматический инкремент id таблицы MySQL например).

    для этого примера я буду использовать 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

    обратный еще проще. Ты просто ... обратный поиск в алфавите.

    1. e9a62 будет разрешено "4-я, 61-я и 0-я буква в алфавите".

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

    2. теперь найдите свою базу данных-запись с WHERE id = 19158 и сделать переадресацию.

    некоторые реализации (предоставляемые х)


    Почему вы хотите использовать хэш?
    Вы можете просто использовать простой перевод значения автоматического приращения в буквенно-цифровое значение. Вы можете сделать это легко, используя некоторые базовые преобразования. Скажем, Пространство символов (A-Z,a-z,0-9 и т. д.) имеет 40 символов, преобразует идентификатор в базовое число-40 и использует символы-цифры.


    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 и другие символы очень похожи, что они почти невозможно отличить) и прямо подвержены ошибкам. Попробуйте использовать только нижний или верхний регистр.

    кроме того, попробуйте иметь формат, в котором вы смешиваете числа и символы в предопределенной форме. Есть исследования, которые показывают, что люди склонны помнить одно форма лучше других (подумайте о телефонных номерах, где номера сгруппированы в определенной форме). Попробуй что-нибудь вроде ням-чар-чар-ням-чар-чар. Я знаю, что это снизит комбинации, особенно если у вас нет верхнего и нижнего регистра, но это было бы более полезно и поэтому полезно.


    мой подход: возьмите идентификатор базы данных, затем Base36 кодирует его. Я бы не стал использовать как верхние, так и строчные буквы, потому что это делает передачу этих URL по телефону кошмаром, но вы, конечно, можете легко расширить функцию, чтобы быть базовым 62 en/decoder.


    вот мой класс 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;
        }
    }
    

    вы можете хэшировать весь URL, но если вы просто хотите сократить идентификатор, сделайте так, как предложил Марсель. Я написал эту реализацию python:

    https://gist.github.com/778542


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

    Если вы не хотите заново изобрести колесо ... http://lilurl.sourceforge.net/


    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
    

    вот моя версия для тех, кто в ней нуждается.


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

    узел js и решение mongodb

    поскольку мы знаем формат, который mongodb использует для создания нового ObjectId с 12 байтами.

    • 4-байтовое значение, представляющее секунды с эпохи Unix,
    • 3-байтовый идентификатор машины,
    • 2-байтовый идентификатор процесса
    • 3-байтовый счетчик (в вашем компьютере), начиная со случайного значения.

    пример (Я выбираю случайную последовательность) a1b2c3d4e5f6g7h8i9j1k2l3

    • a1b2c3d4 представляет секунды с эпохи Unix,
    • 4e5f6g7 представляет идентификатор машины,
    • h8i9 представляет id процесса
    • j1k2l3 представляет счетчик, начиная со случайного значения.

    поскольку счетчик будет уникальным, если мы храним данные на одной машине, мы можем получить его без сомнений, что это будет дубликат.

    таким образом, короткий URL будет счетчиком и вот фрагмент кода, предполагая, что ваш сервер работает правильно.

    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 для PHP...

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

    Не знаю, найдет ли кто - нибудь это полезным-это скорее метод "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);
    } 
    

    Я продолжаю увеличивать целочисленную последовательность на домен в базе данных и использовать Hashids для кодирования целого числа в URL-адрес.

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

    Я запустил скрипт, чтобы узнать, сколько времени потребуется, пока он не исчерпает длину символа. За 6 символов он может сделать 164,916,224 ссылки, а затем идет до 7 символов. Bitly используются 7 символов. До 5 символов выглядит странно для меня.

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

    Почему бы просто не перевести свой id в строку? Вам просто нужна функция, которая отображает цифру между, скажем, 0 и 61 в одну букву (верхний / нижний регистр) или цифру. Затем примените это для создания, скажем, 4-буквенных кодов,и вы получите 14,7 миллиона URL-адресов.


    Это то, что я использую:

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

    Это очень быстро и может занять длинные целые числа.


    для аналогичного проекта, чтобы получить новый ключ, я делаю функцию обертки вокруг генератор случайных строк это вызывает генератор, пока я не получу строку, которая еще не использовалась в моей хэш-таблице. Этот метод замедлится, как только ваше пространство имен начнет заполняться, но, как вы сказали, даже с 6 символами у вас есть много пространства имен для работы.


    вы нарочно опустили O, 0, i ?

    только что создал класс 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 stackoverflow 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 {
            /**
             * Explicitely 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;
            }
        }
    
    ?>
    

    У меня есть вариант проблемы, в том, что я храню веб-страницы от многих разных авторов и должен предотвратить обнаружение страниц догадками. Поэтому мои короткие URL-адреса добавляют пару дополнительных цифр в строку Base-62 для номера страницы. Эти дополнительные цифры генерируются из информации в самой записи страницы, и они гарантируют, что только 1 из 3844 URL-адресов действительны (предполагая, что 2-значная база-62). Вы видите схему описание на http://mgscan.com/MBWL.


    очень хороший ответ, я создал реализацию Golang bjf:

    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


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

    моя версия python3

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

    вот узел.реализация JS, что скорее всего бит.лы. генерировать случайные 7 строку символов. использование 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;
        };
    }
    

    для качественного решения NodeJS / Javascript см. id-сокращение модуль, который тщательно испытан и использован в продукции на месяцы.

    он обеспечивает эффективное сокращение id / url, поддерживаемое подключаемым хранилищем по умолчанию Рэдис, и вы даже можете настроить свой короткий набор символов id и является ли или нет сокращение идемпотентных. Это важное различие, которое не все сокращатели URL принимают в счет.

    по отношению к другим ответы здесь, этот модуль реализует отличные принял Марсель Jackwerth выше ответ.

    ядро решения обеспечивается следующим Redis Lua фрагмент:

    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:

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

    функция, основанная на классе 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;
    }