Как Stack Overflow генерирует свои SEO-дружественные URL-адреса? - PullRequest
241 голосов
/ 24 августа 2008

Что такое правильное полное регулярное выражение или какой-то другой процесс, который получил бы заголовок:

Как изменить заголовок, чтобы он стал частью URL, например переполнение стека?

и превратить его в

how-do-you-change-a-title-to-be-part-of-the-url-like-stack-overflow

что используется в SEO-ориентированных URL-адресах при переполнении стека?

Среда разработки, которую я использую: Ruby on Rails , но если есть какие-то другие решения для конкретных платформ (.NET, PHP, Django ), я бы хотел увидеть те тоже.

Я уверен, что я (или другой читатель) столкнусь с той же проблемой на другой платформе в будущем.

Я использую пользовательские маршруты, и я в основном хочу знать, как изменить строку, чтобы все специальные символы были удалены, все строчные буквы и все пробелы заменены.

Ответы [ 20 ]

284 голосов
/ 25 августа 2008

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

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

/// <summary>
/// Produces optional, URL-friendly version of a title, "like-this-one". 
/// hand-tuned for speed, reflects performance refactoring contributed
/// by John Gietzen (user otac0n) 
/// </summary>
public static string URLFriendly(string title)
{
    if (title == null) return "";

    const int maxlen = 80;
    int len = title.Length;
    bool prevdash = false;
    var sb = new StringBuilder(len);
    char c;

    for (int i = 0; i < len; i++)
    {
        c = title[i];
        if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
        {
            sb.Append(c);
            prevdash = false;
        }
        else if (c >= 'A' && c <= 'Z')
        {
            // tricky way to convert to lowercase
            sb.Append((char)(c | 32));
            prevdash = false;
        }
        else if (c == ' ' || c == ',' || c == '.' || c == '/' || 
            c == '\\' || c == '-' || c == '_' || c == '=')
        {
            if (!prevdash && sb.Length > 0)
            {
                sb.Append('-');
                prevdash = true;
            }
        }
        else if ((int)c >= 128)
        {
            int prevlen = sb.Length;
            sb.Append(RemapInternationalCharToAscii(c));
            if (prevlen != sb.Length) prevdash = false;
        }
        if (i == maxlen) break;
    }

    if (prevdash)
        return sb.ToString().Substring(0, sb.Length - 1);
    else
        return sb.ToString();
}

Чтобы увидеть предыдущую версию замененного кода (но функционально эквивалентного и в 5 раз быстрее), просмотрите историю изменений этого поста (нажмите ссылку даты).

Также, исходный код метода RemapInternationalCharToAscii можно найти здесь .

31 голосов
/ 19 июля 2011

Вот моя версия кода Джеффа. Я сделал следующие изменения:

  • Дефисы были добавлены таким образом, что их можно было добавить, а затем их нужно удалить, так как это был последний символ в строке. То есть мы никогда не хотим «мой слизень». Это означает дополнительное выделение строки для ее удаления в этом крайнем случае. Я работал над этим путем переноса слов. Если вы сравните мой код с Джеффом, логика этого проста.
  • Его подход основан исключительно на поиске и пропустил множество символов, которые я нашел в примерах при исследовании переполнения стека. Чтобы противостоять этому, я сначала выполняю этап нормализации (сопоставление AKA, упомянутое в вопросе переполнения стека Meta Stack Не-US-ASCII-символы, отброшенные из полного (профиля) URL ), а затем игнорирую любые символы вне допустимых диапазонов. Это работает большую часть времени ...
  • ... Потому что, когда это не нужно, мне также нужно было добавить таблицу поиска. Как упоминалось выше, некоторые символы не отображаются на низкое значение ASCII при нормализации. Вместо того, чтобы отбрасывать их, у меня есть ручной список исключений, который, несомненно, полон дыр, но это лучше, чем ничего. Код нормализации был вдохновлен замечательным постом Джона Ханны в вопросе переполнения стека Как удалить акценты в строке? .
  • Преобразование регистра теперь также необязательно.

    public static class Slug
    {
        public static string Create(bool toLower, params string[] values)
        {
            return Create(toLower, String.Join("-", values));
        }
    
        /// <summary>
        /// Creates a slug.
        /// References:
        /// http://www.unicode.org/reports/tr15/tr15-34.html
        /// https://meta.stackexchange.com/questions/7435/non-us-ascii-characters-dropped-from-full-profile-url/7696#7696
        /// /29663/kak-stack-overflow-generiruet-svoi-seo-druzhestvennye-url-adresa#25486
        /// https://stackoverflow.com/questions/3769457/how-can-i-remove-accents-on-a-string
        /// </summary>
        /// <param name="toLower"></param>
        /// <param name="normalised"></param>
        /// <returns></returns>
        public static string Create(bool toLower, string value)
        {
            if (value == null)
                return "";
    
            var normalised = value.Normalize(NormalizationForm.FormKD);
    
            const int maxlen = 80;
            int len = normalised.Length;
            bool prevDash = false;
            var sb = new StringBuilder(len);
            char c;
    
            for (int i = 0; i < len; i++)
            {
                c = normalised[i];
                if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
                {
                    if (prevDash)
                    {
                        sb.Append('-');
                        prevDash = false;
                    }
                    sb.Append(c);
                }
                else if (c >= 'A' && c <= 'Z')
                {
                    if (prevDash)
                    {
                        sb.Append('-');
                        prevDash = false;
                    }
                    // Tricky way to convert to lowercase
                    if (toLower)
                        sb.Append((char)(c | 32));
                    else
                        sb.Append(c);
                }
                else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=')
                {
                    if (!prevDash && sb.Length > 0)
                    {
                        prevDash = true;
                    }
                }
                else
                {
                    string swap = ConvertEdgeCases(c, toLower);
    
                    if (swap != null)
                    {
                        if (prevDash)
                        {
                            sb.Append('-');
                            prevDash = false;
                        }
                        sb.Append(swap);
                    }
                }
    
                if (sb.Length == maxlen)
                    break;
            }
            return sb.ToString();
        }
    
        static string ConvertEdgeCases(char c, bool toLower)
        {
            string swap = null;
            switch (c)
            {
                case 'ı':
                    swap = "i";
                    break;
                case 'ł':
                    swap = "l";
                    break;
                case 'Ł':
                    swap = toLower ? "l" : "L";
                    break;
                case 'đ':
                    swap = "d";
                    break;
                case 'ß':
                    swap = "ss";
                    break;
                case 'ø':
                    swap = "o";
                    break;
                case 'Þ':
                    swap = "th";
                    break;
            }
            return swap;
        }
    }
    

Для получения более подробной информации, модульных тестов и объяснения того, почему Facebook URL схема немного умнее, чем Stack Overflows, у меня есть расширенный версия этого в моем блоге .

16 голосов
/ 24 августа 2008

Вы захотите настроить собственный маршрут, чтобы указать URL на контроллер, который будет его обрабатывать. Поскольку вы используете Ruby on Rails, вот введение в использовании их механизма маршрутизации.

В Ruby вам понадобится регулярное выражение, как вы уже знаете, и вот регулярное выражение для использования:

def permalink_for(str)
    str.gsub(/[^\w\/]|[!\(\)\.]+/, ' ').strip.downcase.gsub(/\ +/, '-')
end
11 голосов
/ 01 сентября 2008

Вы также можете использовать эту JavaScript функцию для генерации слагов в форме (эта основана или скопирована с Django ):

function makeSlug(urlString, filter) {
    // Changes, e.g., "Petty theft" to "petty_theft".
    // Remove all these words from the string before URLifying

    if(filter) {
        removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from",
        "is", "in", "into", "like", "of", "off", "on", "onto", "per",
        "since", "than", "the", "this", "that", "to", "up", "via", "het", "de", "een", "en",
        "with"];
    }
    else {
        removelist = [];
    }
    s = urlString;
    r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi');
    s = s.replace(r, '');
    s = s.replace(/[^-\w\s]/g, ''); // Remove unneeded characters
    s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
    s = s.replace(/[-\s]+/g, '-'); // Convert spaces to hyphens
    s = s.toLowerCase(); // Convert to lowercase
    return s; // Trim to first num_chars characters
}
8 голосов
/ 25 августа 2008

Для примера, вот функция PHP в WordPress, которая делает это ... Я думаю, что WordPress является одной из наиболее популярных платформ, использующих модные ссылки.

    function sanitize_title_with_dashes($title) {
            $title = strip_tags($title);
            // Preserve escaped octets.
            $title = preg_replace('|%([a-fA-F0-9][a-fA-F0-9])|', '---$1---', $title);
            // Remove percent signs that are not part of an octet.
            $title = str_replace('%', '', $title);
            // Restore octets.
            $title = preg_replace('|---([a-fA-F0-9][a-fA-F0-9])---|', '%$1', $title);
            $title = remove_accents($title);
            if (seems_utf8($title)) {
                    if (function_exists('mb_strtolower')) {
                            $title = mb_strtolower($title, 'UTF-8');
                    }
                    $title = utf8_uri_encode($title, 200);
            }
            $title = strtolower($title);
            $title = preg_replace('/&.+?;/', '', $title); // kill entities
            $title = preg_replace('/[^%a-z0-9 _-]/', '', $title);
            $title = preg_replace('/\s+/', '-', $title);
            $title = preg_replace('|-+|', '-', $title);
            $title = trim($title, '-');
            return $title;
    }

Эта функция, а также некоторые вспомогательные функции можно найти в wp-includes / formatting.php.

5 голосов
/ 30 декабря 2008

Если вы используете Rails edge, вы можете положиться на Inflector.parametrize - вот пример из документации:

  class Person
    def to_param
      "#{id}-#{name.parameterize}"
    end
  end

  @person = Person.find(1)
  # => #<Person id: 1, name: "Donald E. Knuth">

  <%= link_to(@person.name, person_path(@person)) %>
  # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>

Также, если вам нужно обрабатывать более экзотические символы, такие как акценты (éphémère) в предыдущей версии Rails, вы можете использовать смесь PermalinkFu и DiacriticsFu :

DiacriticsFu::escape("éphémère")
=> "ephemere"

DiacriticsFu::escape("räksmörgås")
=> "raksmorgas"
5 голосов
/ 24 августа 2008

Я не знаком с Ruby on Rails, но ниже приведен (непроверенный) код PHP. Возможно, вы сможете очень быстро перевести это на Ruby on Rails, если сочтете это полезным.

$sURL = "This is a title to convert to URL-format. It has 1 number in it!";
// To lower-case
$sURL = strtolower($sURL);

// Replace all non-word characters with spaces
$sURL = preg_replace("/\W+/", " ", $sURL);

// Remove trailing spaces (so we won't end with a separator)
$sURL = trim($sURL);

// Replace spaces with separators (hyphens)
$sURL = str_replace(" ", "-", $sURL);

echo $sURL;
// outputs: this-is-a-title-to-convert-to-url-format-it-has-1-number-in-it

Надеюсь, это поможет.

4 голосов
/ 24 августа 2008

Я не очень разбираюсь в Ruby или Rails, но в Perl я бы так и сделал:

my $title = "How do you change a title to be part of the url like Stackoverflow?";

my $url = lc $title;   # Change to lower case and copy to URL.
$url =~ s/^\s+//g;     # Remove leading spaces.
$url =~ s/\s+$//g;     # Remove trailing spaces.
$url =~ s/\s+/\-/g;    # Change one or more spaces to single hyphen.
$url =~ s/[^\w\-]//g;  # Remove any non-word characters.

print "$title\n$url\n";

Я только что сделал быстрый тест, и это похоже на работу. Надеюсь, это относительно легко перевести на Ruby.

4 голосов
/ 06 сентября 2008

Реализация T-SQL, адаптированная из dbo.UrlEncode :

CREATE FUNCTION dbo.Slug(@string varchar(1024))
RETURNS varchar(3072)
AS
BEGIN
    DECLARE @count int, @c char(1), @i int, @slug varchar(3072)

    SET @string = replace(lower(ltrim(rtrim(@string))),' ','-')

    SET @count = Len(@string)
    SET @i = 1
    SET @slug = ''

    WHILE (@i <= @count)
    BEGIN
        SET @c = substring(@string, @i, 1)

        IF @c LIKE '[a-z0-9--]'
            SET @slug = @slug + @c

        SET @i = @i +1
    END

    RETURN @slug
END
3 голосов
/ 26 сентября 2015

Я знаю, что это очень старый вопрос, но так как большинство браузеров сейчас поддерживают URL-адреса Unicode Я нашел отличное решение в XRegex , которое преобразует все, кроме букв (во всех языках в '- «).

Это можно сделать на нескольких языках программирования.

Шаблон \\p{^L}+, и тогда вам просто нужно использовать его для замены всех не букв на '-'.

Рабочий пример в файле node.js с модулем xregex .

var text = 'This ! can @ have # several $ letters % from different languages such as עברית or Español';

var slugRegEx = XRegExp('((?!\\d)\\p{^L})+', 'g');

var slug = XRegExp.replace(text, slugRegEx, '-').toLowerCase();

console.log(slug) ==> "this-can-have-several-letters-from-different-languages-such-as-עברית-or-español"
...