265

タイトルを取得する適切な完全な正規表現またはその他のプロセスは次のとおりです。

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など) があれば、それも見てみたいです。

私 (または別の読者) は、別のプラットフォームで同じ問題に直面することになると確信しています。

私はカスタムルートを使用しています。主に、すべての特殊文字が削除され、すべて小文字になり、すべての空白が置き換えられるように文字列を変更する方法を知りたいです。

4

21 に答える 21

315

これが私たちのやり方です。一見しただけでわかるよりも多くのエッジ条件が存在することに注意してください。

これは 2 番目のバージョンで、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メソッドのソース コードはここにあります。

于 2008-08-25T00:11:43.763 に答える
34

これが私のバージョンの Jeff のコードです。次の変更を加えました。

  • ハイフンは、追加できるように追加されましたが、文字列の最後の文字であるため、削除する必要があります。つまり、「my-slug-」は絶対に必要ありません。これは、このエッジ ケースでそれを削除するための余分な文字列の割り当てを意味します。遅延ハイフンでこれを回避しました。私のコードを Jeff のコードと比較すると、このロジックは簡単に理解できます。
  • 彼のアプローチは純粋にルックアップ ベースであり、スタック オーバーフローの調査中に例で見つけた多くの文字を見逃していました。これに対抗するために、最初に正規化パスを実行し (メタ スタック オーバーフローの質問Non US-ASCII characters drops from full (profile) URLで言及されている別名照合)、次に許容範囲外の文字を無視します。これはほとんどの場合機能します...
  • ...そうでない場合は、ルックアップテーブルも追加する必要がありました。前述のように、一部の文字は、正規化されたときに低い ASCII 値にマップされません。これらを削除するのではなく、間違いなく穴だらけの手動の例外リストを用意しましたが、何もないよりはましです。正規化コードは、Jon Hanna の Stack Overflow の質問How can I removeescents on a string?のすばらしい投稿に触発されました。.
  • 大文字と小文字の変換もオプションになりました。

    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スキームがスタック オーバーフローよりも少しスマートである理由の説明については、私のブログでこれの拡張版を入手しました。

于 2011-07-18T23:11:34.793 に答える
16

URLを処理するコントローラーを指すようにカスタム ルートを設定する必要があります。Ruby on Rails を使っているので、ルーティング エンジンの使い方を紹介します。

Ruby では、ご存知のように正規表現が必要になります。使用する正規表現は次のとおりです。

def permalink_for(str)
    str.gsub(/[^\w\/]|[!\(\)\.]+/, ' ').strip.downcase.gsub(/\ +/, '-')
end
于 2008-08-24T18:24:10.917 に答える
11

この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
}
于 2008-09-01T13:16:17.957 に答える
8

これがWordPressのPHP関数です...WordPressはファンシーリンクを使用する最も人気のあるプラットフォームの1つだと思います。

    関数sanitize_title_with_dashes($ title){
            $ title = strip_tags($ title);
            //エスケープされたオクテットを保持します。
            $ title = preg_replace('|%([a-fA-F0-9] [a-fA-F0-9])|'、'--- $ 1 ---'、$ title);
            //オクテットの一部ではないパーセント記号を削除します。
            $ title = str_replace('%'、''、$ title);
            //オクテットを復元します。
            $ 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); //エンティティを強制終了します
            $ title = preg_replace('/ [^%a-z0-9 _-] /'、''、$ title);
            $ title = preg_replace('/ \ s + /'、'-'、$ title);
            $ title = preg_replace('|-+ |'、'-'、$ title);
            $ title = Trim($ title、'-');
            $titleを返します。
    }

この関数といくつかのサポート関数は、wp-includes/formatting.phpにあります。

于 2008-08-25T01:20:35.113 に答える
5

Rails エッジを使用している場合は、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"
于 2008-12-30T09:59:43.550 に答える
5

私は 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

これが役立つことを願っています。

于 2008-08-24T18:41:43.463 に答える
4

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 に変換するのは比較的簡単です。

于 2008-08-24T18:48:59.890 に答える
4

dbo.UrlEncodeから適応された T-SQL 実装:

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
于 2008-09-06T16:29:20.680 に答える
3

モデル クラスに 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'
于 2008-08-24T18:49:42.027 に答える
2

次のヘルパーメソッドを使用できます。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("--","-");
}
于 2012-03-27T22:28:08.510 に答える
2

Ruby での Brian のコード:

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

downcase文字列を小文字に変換stripし、先頭と末尾の空白を削除し、最初のgsub呼び出しでグローバルにスペースをダッシュ​​に置き換え、2 番目の呼び出しで文字またはダッシュ以外のすべてを削除します。

于 2008-08-24T19:03:47.530 に答える
2

これを行うPermalinkFuと呼ばれる小さな Ruby on Rails プラグインがあります。エスケープ メソッドは、URLに適した文字列に変換します。コードを見てください。その方法は非常に簡単です。

非ASCII文字を削除するには、iconv lib を使用して、「utf-8」から「ascii//ignore//translit」に変換します。スペースはダッシュに変換され、すべてが小文字化されます。

于 2008-09-01T13:13:39.197 に答える
2

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

于 2015-04-10T00:31:01.083 に答える
1

正規表現を使わずにこれを行う方法が気に入ったので、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;
    }
}
于 2013-12-06T12:17:46.467 に答える
1

コードを TypeScript に移植しました。JavaScript に簡単に適応させることができます。

代わりに使用できる最新のブラウザーまたは ES6 をターゲットにしている場合は.contains、プロトタイプにメソッドを追加しています。String.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;
        }
于 2018-04-18T21:39:01.607 に答える
0
于 2009-03-14T01:12:39.093 に答える