Stack Overflow 如何生成其对 SEO 友好的 URL?

How does Stack Overflow generate its SEO-friendly URLs?

提问人:wusher 提问时间:8/25/2008 最后编辑:bb216b3acfd8f72cbc8f899d4d6963wusher 更新时间:9/7/2022 访问量:45381

问:

什么是一个好的完整正则表达式或其他一些可以取标题的过程:

如何将标题更改为 Stack Overflow 等 URL 的一部分?

并将其变成

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

用于 Stack Overflow 上的 SEO 友好 URL?

我使用的开发环境是 Ruby on Rails,但如果还有其他一些特定于平台的解决方案(.NET、PHP、Django),我也希望看到它们。

我相信我(或其他读者)会在不同的平台上遇到同样的问题。

我正在使用自定义路由,我主要想知道如何将字符串更改为删除所有特殊字符,全部为小写,并替换所有空格。

正则表达式 与语言无关的 SEO 友好 URL slug

评论

1赞 casperOne 11/19/2011
应该迁移到 meta;因为问题和答案都专门涉及 SO 实现,并且公认的答案来自 @JeffAtwood。
19赞 Paŭlo Ebermann 11/19/2011
@casperOne 你认为 Jeff 不被允许拥有一些非元声誉吗?问题在于“如何才能做这样的事情”,而不是具体地说“这里是怎么做到的”。
0赞 casperOne 11/22/2011
@PaŭloEbermann:这与杰夫获得一些非元声誉无关(他有多少声誉真的不是我关心的);问题正文特别引用了 StackOverflow 的实现,因此它在元上的理由。
0赞 Daren Thomas 8/25/2008
有趣的角色呢?你打算怎么做?变音符号?标点?这些都需要考虑。基本上,我会使用白名单方法,而不是上面的黑名单方法:描述您将允许哪些字符,您将转换哪些字符(转换为什么?),然后将其余部分更改为有意义的内容(“”)。我怀疑你能在一个正则表达式中做到这一点......为什么不直接遍历角色呢?

答:

16赞 Dale Ragan 8/25/2008 #1

您需要设置一个自定义路由,以将 URL 指向将处理它的控制器。由于您使用的是 Ruby on Rails,因此这里是使用其路由引擎的介绍

在 Ruby 中,您将需要一个您已经知道的正则表达式,以下是要使用的正则表达式:

def permalink_for(str)
    str.gsub(/[^\w\/]|[!\(\)\.]+/, ' ').strip.downcase.gsub(/\ +/, '-')
end
5赞 Vegard Larsen 8/25/2008 #2

我不熟悉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赞 Brian 8/25/2008 #3

我不太了解 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。

3赞 John Topley 8/25/2008 #4

假设模型类具有 title 属性,则只需重写模型中的 to_param 方法,如下所示:

def to_param
  title.downcase.gsub(/ /, '-')
end

这一集 Railscast 包含所有细节。您还可以使用以下命令确保标题仅包含有效字符:

validates_format_of :title, :with => /^[a-z0-9-]+$/,
                    :message => 'can only contain letters, numbers and hyphens'
2赞 Sören Kuklau 8/25/2008 #5

Brian 的代码,在 Ruby 中:

title.downcase.strip.gsub(/\ /, '-').gsub(/[^\w\-]/, '')

downcase将字符串转换为小写,删除前导和尾随空格,第一个调用 globally 用破折号代替空格,第二个调用删除所有不是字母或破折号的内容。stripgsub

322赞 Jeff Atwood 8/25/2008 #6

我们是这样做的。请注意,边缘条件可能比您乍一看意识到的要多。

这是第二个版本,推出后性能提高了 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

8赞 The How-To Geek 8/25/2008 #7

为了更好地衡量,这是 WordPress 中的 PHP 函数可以做到这一点......我认为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中找到。

评论

7赞 Nikola Loncar 6/11/2014
这不是完整的答案。您缺少以下功能: ,...remove_accentsseems_utf8
0赞 mickro 6/10/2017
要完成@The How-To Geek 答案,您仍然可以找到该文件git clone git://core.git.wordpress.org/wp-includes/formatting.php
2赞 Lau 9/1/2008 #8

有一个叫做 PermalinkFu 的小型 Ruby on Rails 插件可以做到这一点。转义方法将转换为适合 URL 的字符串。看一下代码;这种方法非常简单。

为了删除非 ASCII 字符,它使用 iconv 库从 'utf-8' 转换为 'ascii//ignore//translit'。然后将空格变成破折号,将所有内容缩小,等等。

评论

0赞 WhyNotHugo 7/20/2012
虽然这很完美,但我不知何故觉得它不是很有效。
11赞 fijter 9/1/2008 #9

你也可以使用这个 JavaScript 函数来生成 slug 的表单(这个函数基于 Django /复制自 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
}
4赞 Sören Kuklau 9/7/2008 #10

T-SQL 实现,改编自 dbo。网址编码

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
5赞 Thibaut Barrère 12/30/2008 #11

如果你使用的是 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>

此外,如果你需要处理更奇特的字符,比如以前版本的 Rails 中的重音 (éphémère),你可以混合使用 PermalinkFuDiacriticsFu

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

DiacriticsFu::escape("räksmörgås")
=> "raksmorgas"
0赞 Colin Thomas-Arnold 3/14/2009 #12

不 不 不。你们都大错特错了。除了变音符号的东西,你已经到了那里,但是亚洲字符呢(Ruby 开发人员没有考虑他们的 nihonjin 兄弟,真是太可惜了)。

Firefox 和 Safari 都在 URL 中显示非 ASCII 字符,坦率地说,它们看起来很棒。支持像“http://somewhere.com/news/read/ お前たちはアホじゃないかい”这样的链接是很好的。

所以这里有一些PHP代码可以做到这一点,但我只是写了它,还没有对它进行压力测试。

<?php
    function slug($str)
    {
        $args = func_get_args();
        array_filter($args);  //remove blanks
        $slug = mb_strtolower(implode('-', $args));

        $real_slug = '';
        $hyphen = '';
        foreach(SU::mb_str_split($slug) as $c)
        {
            if (strlen($c) > 1 && mb_strlen($c)===1)
            {
                $real_slug .= $hyphen . $c;
                $hyphen = '';
            }
            else
            {
                switch($c)
                {
                    case '&':
                        $hyphen = $real_slug ? '-and-' : '';
                        break;
                    case 'a':
                    case 'b':
                    case 'c':
                    case 'd':
                    case 'e':
                    case 'f':
                    case 'g':
                    case 'h':
                    case 'i':
                    case 'j':
                    case 'k':
                    case 'l':
                    case 'm':
                    case 'n':
                    case 'o':
                    case 'p':
                    case 'q':
                    case 'r':
                    case 's':
                    case 't':
                    case 'u':
                    case 'v':
                    case 'w':
                    case 'x':
                    case 'y':
                    case 'z':

                    case 'A':
                    case 'B':
                    case 'C':
                    case 'D':
                    case 'E':
                    case 'F':
                    case 'G':
                    case 'H':
                    case 'I':
                    case 'J':
                    case 'K':
                    case 'L':
                    case 'M':
                    case 'N':
                    case 'O':
                    case 'P':
                    case 'Q':
                    case 'R':
                    case 'S':
                    case 'T':
                    case 'U':
                    case 'V':
                    case 'W':
                    case 'X':
                    case 'Y':
                    case 'Z':

                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        $real_slug .= $hyphen . $c;
                        $hyphen = '';
                        break;

                    default:
                       $hyphen = $hyphen ? $hyphen : ($real_slug ? '-' : '');
                }
            }
        }
        return $real_slug;
    }

例:

$str = "~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 コリン ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 トーマス ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 アーノルド ~!@#$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04";
echo slug($str);

输出: コリン-and-トーマス-and-アーノルド

“-and-”是因为 &' 被更改为 '-and-'。

评论

4赞 sjas 7/16/2012
我真的不知道该怎么说这条信息。
3赞 NickG 12/19/2014
这是一个很好的例子,说明何时不使用 switch case 语句。
36赞 DanH 7/19/2011 #13

这是我的 Jeff 代码版本。我进行了以下更改:

  • 连字符的附加方式可以添加一个连字符,然后需要删除,因为它是字符串中的最后一个字符。也就是说,我们永远不想要“我的蛞蝓”。这意味着在此边缘情况下需要额外的字符串分配来删除它。我通过延迟连字符解决了这个问题。如果你将我的代码与 Jeff 的代码进行比较,那么它的逻辑很容易理解。
  • 他的方法纯粹是基于查找的,在研究 Stack Overflow 时,我错过了很多在示例中发现的字符。为了解决这个问题,我首先执行规范化传递(又名 Meta Stack Overflow 问题从完整(配置文件)URL 中删除的非 US-ASCII 字符中提到的排序规则),然后忽略可接受范围之外的任何字符。这在大多数时候都有效......
  • ...如果没有,我还必须添加一个查找表。如上所述,某些字符在规范化时不会映射到低 ASCII 值。我没有放弃这些,而是得到了一个手动的例外列表,这无疑充满了漏洞,但总比没有好。规范化代码的灵感来自 Jon Hanna 在 Stack Overflow 问题中发表的精彩文章:如何删除字符串上的重音?
  • 大小写转换现在也是可选的。

    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
        /// https://stackoverflow.com/questions/25259/how-do-you-include-a-webpage-title-as-part-of-a-webpage-url/25486#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;
        }
    }
    

有关更多详细信息,单元测试,以及为什么FacebookURL方案比Stack Overflows更聪明的解释,我在我的博客上得到了一个扩展版本

2赞 Peyman Mehrabani 3/28/2012 #14

您可以使用以下帮助程序方法。它可以转换 Unicode 字符。

public static string ConvertTextToSlug(string s)
{
    StringBuilder sb = new StringBuilder();

    bool wasHyphen = true;

    foreach (char c in s)
    {
        if (char.IsLetterOrDigit(c))
        {
            sb.Append(char.ToLower(c));
            wasHyphen = false;
        }
        else
            if (char.IsWhiteSpace(c) && !wasHyphen)
            {
                sb.Append('-');
                wasHyphen = true;
            }
    }

    // Avoid trailing hyphens
    if (wasHyphen && sb.Length > 0)
        sb.Length--;

    return sb.ToString().Replace("--","-");
}
1赞 user1765062 12/6/2013 #15

我喜欢在不使用正则表达式的情况下完成此操作的方式,因此我将其移植到 PHP。我刚刚添加了一个用于检查字符的函数:is_between

function is_between($val, $min, $max)
{
    $val = (int) $val; $min = (int) $min; $max = (int) $max;

    return ($val >= $min && $val <= $max);
}

function international_char_to_ascii($char)
{
    if (mb_strpos('àåáâäãåa', $char) !== false)
    {
        return 'a';
    }

    if (mb_strpos('èéêëe', $char) !== false)
    {
        return 'e';
    }

    if (mb_strpos('ìíîïi', $char) !== false)
    {
        return 'i';
    }

    if (mb_strpos('òóôõö', $char) !== false)
    {
        return 'o';
    }

    if (mb_strpos('ùúûüuu', $char) !== false)
    {
        return 'u';
    }

    if (mb_strpos('çccc', $char) !== false)
    {
        return 'c';
    }

    if (mb_strpos('zzž', $char) !== false)
    {
        return 'z';
    }

    if (mb_strpos('ssšs', $char) !== false)
    {
        return 's';
    }

    if (mb_strpos('ñn', $char) !== false)
    {
        return 'n';
    }

    if (mb_strpos('ýÿ', $char) !== false)
    {
        return 'y';
    }

    if (mb_strpos('gg', $char) !== false)
    {
        return 'g';
    }

    if (mb_strpos('r', $char) !== false)
    {
        return 'r';
    }

    if (mb_strpos('l', $char) !== false)
    {
        return 'l';
    }

    if (mb_strpos('d', $char) !== false)
    {
        return 'd';
    }

    if (mb_strpos('ß', $char) !== false)
    {
        return 'ss';
    }

    if (mb_strpos('Þ', $char) !== false)
    {
        return 'th';
    }

    if (mb_strpos('h', $char) !== false)
    {
        return 'h';
    }

    if (mb_strpos('j', $char) !== false)
    {
        return 'j';
    }
    return '';
}

function url_friendly_title($url_title)
{
    if (empty($url_title))
    {
        return '';
    }

    $url_title = mb_strtolower($url_title);

    $url_title_max_length   = 80;
    $url_title_length       = mb_strlen($url_title);
    $url_title_friendly     = '';
    $url_title_dash_added   = false;
    $url_title_char = '';

    for ($i = 0; $i < $url_title_length; $i++)
    {
        $url_title_char     = mb_substr($url_title, $i, 1);

        if (strlen($url_title_char) == 2)
        {
            $url_title_ascii    = ord($url_title_char[0]) * 256 + ord($url_title_char[1]) . "\r\n";
        }
        else
        {
            $url_title_ascii    = ord($url_title_char);
        }

        if (is_between($url_title_ascii, 97, 122) || is_between($url_title_ascii, 48, 57))
        {
            $url_title_friendly .= $url_title_char;

            $url_title_dash_added = false;
        }
        elseif(is_between($url_title_ascii, 65, 90))
        {
            $url_title_friendly .= chr(($url_title_ascii | 32));

            $url_title_dash_added = false;
        }
        elseif($url_title_ascii == 32 || $url_title_ascii == 44 || $url_title_ascii == 46 || $url_title_ascii == 47 || $url_title_ascii == 92 || $url_title_ascii == 45 || $url_title_ascii == 47 || $url_title_ascii == 95 || $url_title_ascii == 61)
        {
            if (!$url_title_dash_added && mb_strlen($url_title_friendly) > 0)
            {
                $url_title_friendly .= chr(45);

                $url_title_dash_added = true;
            }
        }
        else if ($url_title_ascii >= 128)
        {
            $url_title_previous_length = mb_strlen($url_title_friendly);

            $url_title_friendly .= international_char_to_ascii($url_title_char);

            if ($url_title_previous_length != mb_strlen($url_title_friendly))
            {
                $url_title_dash_added = false;
            }
        }

        if ($i == $url_title_max_length)
        {
            break;
        }
    }

    if ($url_title_dash_added)
    {
        return mb_substr($url_title_friendly, 0, -1);
    }
    else
    {
        return $url_title_friendly;
    }
}
2赞 giammin 7/31/2014 #16

stackoverflow 解决方案很棒,但现代浏览器(像往常一样不包括 IE)现在可以很好地处理 utf8 编码:

enter image description here

因此,我升级了建议的解决方案:

public static string ToFriendlyUrl(string title, bool useUTF8Encoding = false)
{
    ...

        else if (c >= 128)
        {
            int prevlen = sb.Length;
            if (useUTF8Encoding )
            {
                sb.Append(HttpUtility.UrlEncode(c.ToString(CultureInfo.InvariantCulture),Encoding.UTF8));
            }
            else
            {
                sb.Append(RemapInternationalCharToAscii(c));
            }
    ...
}

Pastebin上的完整代码

编辑:这是方法的代码(pastebin中缺少)。RemapInternationalCharToAscii

评论

0赞 Muhammad Rehan Saeed 4/24/2015
根据维基百科,Mozilla 1.4、Netscape 7.1、Opera 7.11 是最早支持 IDNA 的应用程序之一。Internet Explorer 6 提供了一个浏览器插件来提供 IDN 支持。Internet Explorer 7.0 和 Windows Vista 的 URL API 提供对 IDN 的本机支持。听起来删除 UTF-8 字符是浪费时间。UTF-8 万岁!!
2赞 Ronnie Overby 4/10/2015 #17

这是我的 Jeff 代码版本(速度较慢,但写起来很有趣):

public static string URLFriendly(string title)
{
    char? prevRead = null,
        prevWritten = null;

    var seq = 
        from c in title
        let norm = RemapInternationalCharToAscii(char.ToLowerInvariant(c).ToString())[0]
        let keep = char.IsLetterOrDigit(norm)
        where prevRead.HasValue || keep
        let replaced = keep ? norm
            :  prevWritten != '-' ? '-'
            :  (char?)null
        where replaced != null
        let s = replaced + (prevRead == null ? ""
            : norm == '#' && "cf".Contains(prevRead.Value) ? "sharp"
            : norm == '+' ? "plus"
            : "")
        let _ = prevRead = norm
        from written in s
        let __ = prevWritten = written
        select written;

    const int maxlen = 80;  
    return string.Concat(seq.Take(maxlen)).TrimEnd('-');
}

public static string RemapInternationalCharToAscii(string text)
{
    var seq = text.Normalize(NormalizationForm.FormD)
        .Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark);

    return string.Concat(seq).Normalize(NormalizationForm.FormC);
}

我的测试字符串:

" I love C#, F#, C++, and... Crème brûlée!!! They see me codin'... they hatin'... tryin' to catch me codin' dirty... "

1赞 ikourfaln 5/15/2015 #18

现在所有浏览器都很好地处理了 utf8 编码,因此您可以使用 WebUtility.UrlEncode 方法,它类似于 @giamin使用的 HttpUtility.UrlEncode,但它在 Web 应用程序之外工作。

4赞 Rotem 9/26/2015 #19

我知道这是一个非常古老的问题,但由于大多数浏览器现在都支持 unicode url,我在 XRegex 中找到了一个很好的解决方案,它可以转换除字母之外的所有内容(在所有语言中都转换为“-”)。

这可以用几种编程语言来完成。

模式是,然后你只需要用它来将所有非字母替换为“-”。\\p{^L}+

带有 xregex 模块的 node.js 中的工作示例。

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"
1赞 Sam 4/19/2018 #20

我将代码移植到 TypeScript。它可以很容易地适应 JavaScript。

我正在向原型添加一种方法,如果您的目标是最新的浏览器或 ES6,则可以改用。.containsString.includes

if (!String.prototype.contains) {
    String.prototype.contains = function (check) {
        return this.indexOf(check, 0) !== -1;
    };
}

declare interface String {
    contains(check: string): boolean;
}

export function MakeUrlFriendly(title: string) {
            if (title == null || title == '')
                return '';

            const maxlen = 80;
            let len = title.length;
            let prevdash = false;
            let result = '';
            let c: string;
            let cc: number;
            let remapInternationalCharToAscii = function (c: string) {
                let s = c.toLowerCase();
                if ("àåáâäãåą".contains(s)) {
                    return "a";
                }
                else if ("èéêëę".contains(s)) {
                    return "e";
                }
                else if ("ìíîïı".contains(s)) {
                    return "i";
                }
                else if ("òóôõöøőð".contains(s)) {
                    return "o";
                }
                else if ("ùúûüŭů".contains(s)) {
                    return "u";
                }
                else if ("çćčĉ".contains(s)) {
                    return "c";
                }
                else if ("żźž".contains(s)) {
                    return "z";
                }
                else if ("śşšŝ".contains(s)) {
                    return "s";
                }
                else if ("ñń".contains(s)) {
                    return "n";
                }
                else if ("ýÿ".contains(s)) {
                    return "y";
                }
                else if ("ğĝ".contains(s)) {
                    return "g";
                }
                else if (c == 'ř') {
                    return "r";
                }
                else if (c == 'ł') {
                    return "l";
                }
                else if (c == 'đ') {
                    return "d";
                }
                else if (c == 'ß') {
                    return "ss";
                }
                else if (c == 'Þ') {
                    return "th";
                }
                else if (c == 'ĥ') {
                    return "h";
                }
                else if (c == 'ĵ') {
                    return "j";
                }
                else {
                    return "";
                }
            };

            for (let i = 0; i < len; i++) {
                c = title[i];
                cc = c.charCodeAt(0);

                if ((cc >= 97 /* a */ && cc <= 122 /* z */) || (cc >= 48 /* 0 */ && cc <= 57 /* 9 */)) {
                    result += c;
                    prevdash = false;
                }
                else if ((cc >= 65 && cc <= 90 /* A - Z */)) {
                    result += c.toLowerCase();
                    prevdash = false;
                }
                else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=') {
                    if (!prevdash && result.length > 0) {
                        result += '-';
                        prevdash = true;
                    }
                }
                else if (cc >= 128) {
                    let prevlen = result.length;
                    result += remapInternationalCharToAscii(c);
                    if (prevlen != result.length) prevdash = false;
                }
                if (i == maxlen) break;
            }

            if (prevdash)
                return result.substring(0, result.length - 1);
            else
                return result;
        }
-1赞 David 9/10/2020 #21

重写 Jeff 的代码,使其更简洁

    public static string RemapInternationalCharToAscii(char c)
    {
        var s = c.ToString().ToLowerInvariant();

        var mappings = new Dictionary<string, string>
        {
            { "a", "àåáâäãåą" },
            { "c", "çćčĉ" },
            { "d", "đ" },
            { "e", "èéêëę" },
            { "g", "ğĝ" },
            { "h", "ĥ" },
            { "i", "ìíîïı" },
            { "j", "ĵ" },
            { "l", "ł" },
            { "n", "ñń" },
            { "o", "òóôõöøőð" },
            { "r", "ř" },
            { "s", "śşšŝ" },
            { "ss", "ß" },
            { "th", "Þ" },
            { "u", "ùúûüŭů" },
            { "y", "ýÿ" },
            { "z", "żźž" }
        };

        foreach(var mapping in mappings)
        {
            if (mapping.Value.Contains(s))
                return mapping.Key;
        }

        return string.Empty;
    }

评论

0赞 Skary 9/22/2021
如果你必须完全扫描它,我真的看不到字典的品脱。为什么不只是一个列表?如果那个结构化数据列表(我看到你像元组一样使用字典)。如果列表也是一个常量,为什么不在方法之外将其定义为 follow?恕我直言,在这种情况下,常量字典比您的字典更有效,但键和值切换了: 'à'-> 'a' , ... , 'ą'->'a', ..., 'ç' -> 'c', ... ,'ž' -> 'z'