Как мне создать сокращатель URL?

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

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

Мой подход:

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

  1. id, целое число, автоинкремент
  2. long, string, длинный URL, введенный пользователем
  3. короткий, строка, сокращенный 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.

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

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

Ответ 1

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

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

Вам нужна биективная функция f. Это необходимо для того, чтобы вы могли найти обратную функцию g ('abc') = 123 для вашей функции f (123) = 'abc'. Это означает:

  • Не должно быть x1, x2 (с x1 ≠ x2), из-за которых f (x1) = f (x2),
  • и для каждого y вы должны быть в состоянии найти x, так что f (x) = y.

Как преобразовать идентификатор в сокращенный URL

  1. Подумайте об алфавите, который мы хотим использовать. В вашем случае это [a-zA-Z0-9]. Он содержит 62 буквы.
  2. Возьмите автоматически сгенерированный уникальный числовой ключ (например, автоматически увеличенный id таблицы MySQL).

    Для этого примера я буду использовать 125 10 (125 с основанием 10).

  3. Теперь вам нужно конвертировать 125 10 в X 62 (база 62).

    125 10= 2 × 62 1 + 1 × 62 0= [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 62 в качестве сокращенного URL.

    http://shor.ty/cb
    

Как разрешить сокращенный URL к начальному идентификатору

Обратное еще проще. Вы просто делаете обратный поиск в вашем алфавите.

  1. e9a 62 будет преобразован в "4-ю, 61-ю и 0-ю букву в алфавите".

    e9a 62= [4,61,0]= 4 × 62 2 + 61 × 62 1 + 0 × 62 0= 19158 10

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

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

Ответ 2

Почему вы хотите использовать хеш?

Вы можете просто использовать простой перевод значения автоинкремента в буквенно-цифровое значение. Вы можете сделать это легко с помощью некоторого базового преобразования. Допустим, у вас есть пространство символов (AZ, az, 0-9 и т.д.), Состоящее из 40 символов, преобразуйте идентификатор в число с основанием 40 и используйте символы в качестве цифр.

Ответ 3

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

Не ответ на ваш вопрос, но я бы не использовал сокращенные URL-адреса с учетом регистра. Их трудно запомнить, обычно нечитабельно (многие шрифты визуализируют 1 и 1, 0 и O и другие персонажи очень похожи, что почти невозможно сказать разницу) и прямой склонностью к ошибкам. Попробуйте использовать только нижний или верхний регистр.

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

Ответ 5

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

Ответ 6

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

Решение Node.js и MongoDB

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

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

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

  • a1b2c3d4 представляет секунды с начала эпохи Unix,
  • 4e5f6g7 представляет идентификатор машины,
  • h8i9 представляет идентификатор процесса
  • 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");
    })
});

Ответ 8

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

https://gist.github.com/778542

Ответ 9

Версия С#:

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

Ответ 10

// simple approach

$original_id = 56789;

$shortened_id = base_convert($original_id, 10, 36);

$un_shortened_id = base_convert($shortened_id, 36, 10);

Ответ 12

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

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

Ответ 13

Я продолжаю увеличивать целочисленную последовательность для каждого домена в базе данных и использую 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

Вот неплохая функция кодирования 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;
}

Ответ 15

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

Вы пропустили O, 0, а я специально?

Я только что создал класс 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 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;
        }
    }
?>

Ответ 17

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

Ответ 18

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

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

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

Ответ 19

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

Ответ 20

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

Ответ 21

Очень хороший ответ, я создал 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

Ответ 22

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

Ответ 23

Моя версия 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")

Ответ 24

Вот реализация Node.js, которая, вероятно, будет bit.ly. генерировать очень случайную строку из семи символов.

Он использует криптографию Node.js для генерации очень случайного набора символов из 25 символов, а не случайного выбора семи символов.

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

Ответ 25

Качественное решение Node.js/JavaScript см. В модуле id-shorttener, который тщательно протестирован и уже несколько месяцев используется в производстве.

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

В отношении других ответов здесь, этот модуль реализует превосходно принятый ответ Марселя Джекверта выше.

Основу решения предоставляет следующий фрагмент 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

Ответ 26

Реализация в 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)
  }

}

Ответ 27

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

Ответ 28

Реализация Javascript

function encode(num){
  const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789';
  const base = alphabet.length;  
  let encoded = [];
  while(Math.floor(num) > 0){
    encoded.push(alphabet.charAt( num % base ));
    num /= base;
  }
  return encoded.reverse().join("");
}