Thuật toán URL Slugify trong C #?


86

Vì vậy, tôi đã tìm kiếm và duyệt qua thẻ slug trên SO và chỉ tìm thấy hai giải pháp hấp dẫn:

Đó là nhưng giải pháp một phần cho vấn đề. Tôi có thể tự viết mã này theo cách thủ công nhưng tôi ngạc nhiên rằng vẫn chưa có giải pháp nào ở đó.

Vì vậy, có một triển khai alrogithm slugify trong C # và / hoặc .NET giải quyết các ký tự latin, unicode và các vấn đề ngôn ngữ khác một cách chính xác không?


"Slugify" nghĩa là gì?
Billy ONeal

7
slugify = làm cho chuỗi do người dùng gửi an toàn để sử dụng như một phần của URL ... hoặc cơ sở dữ liệu hoặc bất cứ điều gì ngoại trừ URL.
chakrit

Câu trả lời:


158

http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html

public static string GenerateSlug(this string phrase) 
{ 
    string str = phrase.RemoveAccent().ToLower(); 
    // invalid chars           
    str = Regex.Replace(str, @"[^a-z0-9\s-]", ""); 
    // convert multiple spaces into one space   
    str = Regex.Replace(str, @"\s+", " ").Trim(); 
    // cut and trim 
    str = str.Substring(0, str.Length <= 45 ? str.Length : 45).Trim();   
    str = Regex.Replace(str, @"\s", "-"); // hyphens   
    return str; 
} 

public static string RemoveAccent(this string txt) 
{ 
    byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt); 
    return System.Text.Encoding.ASCII.GetString(bytes); 
}

Liên kết được đăng đáp ứng tốt câu hỏi của OP.
Ian P

6
Mục đích của độ dài và cắt bớt quá 45 ký tự là gì?

11
Giải pháp sẽ không hoạt động đối với bảng chữ cái không phải Latinh. Phương thức RemoveAccent sẽ loại bỏ các ký tự Cyrillic chẳng hạn. Hãy thử một cái gì đó giống như RemoveAccent ( "Не работает") và kết quả sẽ là chuỗi rỗng: D
Evereq

yêu thích giải pháp này, đã tạo tiện ích mở rộng có tên 'ToSlug ()' bằng cách sử dụng này.
Yasser Shaikh

12
Vui lòng ... không sử dụng RemoveAccent. Kiểm tra câu hỏi SO này để biết cách thực hiện RemoveDiacritics. stackoverflow.com/questions/249087/…
Maxime Rouiller

21

Ở đây bạn tìm thấy một cách để tạo url slug trong c #. Hàm này loại bỏ tất cả các dấu (câu trả lời của Marcel), thay thế dấu cách, xóa các ký tự không hợp lệ, cắt bỏ dấu gạch ngang ở cuối và thay thế các dấu hiệu kép của "-" hoặc "_"

Mã:

public static string ToUrlSlug(string value){

        //First to lower case
        value = value.ToLowerInvariant();

        //Remove all accents
        var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value);
        value = Encoding.ASCII.GetString(bytes);

        //Replace spaces
        value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled);

        //Remove invalid chars
        value = Regex.Replace(value, @"[^a-z0-9\s-_]", "",RegexOptions.Compiled);

        //Trim dashes from end
        value = value.Trim('-', '_');

        //Replace double occurences of - or _
        value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled);

        return value ;
    }

17

Đây là bản trình diễn của tôi, dựa trên câu trả lời của Joan và Marcel. Những thay đổi tôi đã thực hiện như sau:

  • Sử dụng một phương pháp được chấp nhận rộng rãi để loại bỏ dấu.
  • Bộ nhớ đệm Regex rõ ràng để cải thiện tốc độ khiêm tốn.
  • Nhiều dấu tách từ hơn được nhận dạng và chuẩn hóa thành dấu gạch nối.

Đây là mã:

public class UrlSlugger
{
    // white space, em-dash, en-dash, underscore
    static readonly Regex WordDelimiters = new Regex(@"[\s—–_]", RegexOptions.Compiled);

    // characters that are not valid
    static readonly Regex InvalidChars = new Regex(@"[^a-z0-9\-]", RegexOptions.Compiled);

    // multiple hyphens
    static readonly Regex MultipleHyphens = new Regex(@"-{2,}", RegexOptions.Compiled);

    public static string ToUrlSlug(string value)
    {
        // convert to lower case
        value = value.ToLowerInvariant();

        // remove diacritics (accents)
        value = RemoveDiacritics(value);

        // ensure all word delimiters are hyphens
        value = WordDelimiters.Replace(value, "-");

        // strip out invalid characters
        value = InvalidChars.Replace(value, "");

        // replace multiple hyphens (-) with a single hyphen
        value = MultipleHyphens.Replace(value, "-");

        // trim hyphens (-) from ends
        return value.Trim('-');
    }

    /// See: http://www.siao2.com/2007/05/14/2629747.aspx
    private static string RemoveDiacritics(string stIn)
    {
        string stFormD = stIn.Normalize(NormalizationForm.FormD);
        StringBuilder sb = new StringBuilder();

        for (int ich = 0; ich < stFormD.Length; ich++)
        {
            UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(stFormD[ich]);
            if (uc != UnicodeCategory.NonSpacingMark)
            {
                sb.Append(stFormD[ich]);
            }
        }

        return (sb.ToString().Normalize(NormalizationForm.FormC));
    }
}

Điều này vẫn không giải quyết được vấn đề ký tự không phải là ký tự latin. Một giải pháp hoàn toàn thay thế sẽ là sử dụng Uri.EscapeDataString để chuyển đổi chuỗi thành biểu diễn hex của nó:

string original = "测试公司";

// %E6%B5%8B%E8%AF%95%E5%85%AC%E5%8F%B8
string converted = Uri.EscapeDataString(original);

Sau đó, sử dụng dữ liệu để tạo siêu kết nối:

<a href="http://www.example.com/100/%E6%B5%8B%E8%AF%95%E5%85%AC%E5%8F%B8">
    测试公司
</a>

Nhiều trình duyệt sẽ hiển thị các ký tự Trung Quốc trên thanh địa chỉ (xem bên dưới), nhưng dựa trên thử nghiệm hạn chế của tôi, nó không được hỗ trợ hoàn toàn.

thanh địa chỉ với các ký tự Trung Quốc

LƯU Ý: Để Uri.EscapeDataString hoạt động theo cách này, iriParsing phải được bật.


BIÊN TẬP

Đối với những người đang tìm cách tạo URL Slugs trong C #, tôi khuyên bạn nên xem câu hỏi liên quan này:

Làm cách nào để Stack Overflow tạo ra các URL thân thiện với SEO?

Đó là những gì tôi đã sử dụng cho dự án của mình.


4

Một vấn đề tôi gặp phải với slugification (từ mới!) Là va chạm. Ví dụ: nếu tôi có một bài đăng trên blog, được gọi là "Stack-Overflow" và một bài có tên "Stack Overflow", thì slug của hai tiêu đề đó giống nhau. Do đó, trình tạo slug của tôi thường phải liên quan đến cơ sở dữ liệu theo một cách nào đó. Đây có thể là lý do tại sao bạn không thấy các giải pháp chung chung hơn ở đó.


6
Cá nhân tôi thích nối các sên với một số nhận dạng duy nhất (tức là một số nguyên) để đảm bảo chúng là duy nhất. Đó không phải là giải pháp thân thiện nhất nhưng nó giúp tôi tránh khỏi rắc rối.
Jeremy Cade

1
chỉ là một nhận xét, theo quan điểm SEO, url và tiêu đề phải là duy nhất cho mỗi trang.
Rafael Herscovici

3

Đây là bức ảnh của tôi về nó. Nó hỗ trợ:

  • loại bỏ các dấu phụ (vì vậy chúng tôi không chỉ xóa các ký tự "không hợp lệ")
  • độ dài tối đa cho kết quả (hoặc trước khi xóa dấu phụ - "dấu cắt ngắn sớm")
  • dấu phân cách tùy chỉnh giữa các phần được chuẩn hóa
  • kết quả có thể buộc phải viết hoa hoặc viết thường
  • danh sách có thể định cấu hình của các danh mục unicode được hỗ trợ
  • danh sách có thể định cấu hình của phạm vi ký tự được phép
  • hỗ trợ khuôn khổ 2.0

Mã:

/// <summary>
/// Defines a set of utilities for creating slug urls.
/// </summary>
public static class Slug
{
    /// <summary>
    /// Creates a slug from the specified text.
    /// </summary>
    /// <param name="text">The text. If null if specified, null will be returned.</param>
    /// <returns>
    /// A slugged text.
    /// </returns>
    public static string Create(string text)
    {
        return Create(text, (SlugOptions)null);
    }

    /// <summary>
    /// Creates a slug from the specified text.
    /// </summary>
    /// <param name="text">The text. If null if specified, null will be returned.</param>
    /// <param name="options">The options. May be null.</param>
    /// <returns>A slugged text.</returns>
    public static string Create(string text, SlugOptions options)
    {
        if (text == null)
            return null;

        if (options == null)
        {
            options = new SlugOptions();
        }

        string normalised;
        if (options.EarlyTruncate && options.MaximumLength > 0 && text.Length > options.MaximumLength)
        {
            normalised = text.Substring(0, options.MaximumLength).Normalize(NormalizationForm.FormD);
        }
        else
        {
            normalised = text.Normalize(NormalizationForm.FormD);
        }
        int max = options.MaximumLength > 0 ? Math.Min(normalised.Length, options.MaximumLength) : normalised.Length;
        StringBuilder sb = new StringBuilder(max);
        for (int i = 0; i < normalised.Length; i++)
        {
            char c = normalised[i];
            UnicodeCategory uc = char.GetUnicodeCategory(c);
            if (options.AllowedUnicodeCategories.Contains(uc) && options.IsAllowed(c))
            {
                switch (uc)
                {
                    case UnicodeCategory.UppercaseLetter:
                        if (options.ToLower)
                        {
                            c = options.Culture != null ? char.ToLower(c, options.Culture) : char.ToLowerInvariant(c);
                        }
                        sb.Append(options.Replace(c));
                        break;

                    case UnicodeCategory.LowercaseLetter:
                        if (options.ToUpper)
                        {
                            c = options.Culture != null ? char.ToUpper(c, options.Culture) : char.ToUpperInvariant(c);
                        }
                        sb.Append(options.Replace(c));
                        break;

                    default:
                        sb.Append(options.Replace(c));
                        break;
                }
            }
            else if (uc == UnicodeCategory.NonSpacingMark)
            {
                // don't add a separator
            }
            else
            {
                if (options.Separator != null && !EndsWith(sb, options.Separator))
                {
                    sb.Append(options.Separator);
                }
            }

            if (options.MaximumLength > 0 && sb.Length >= options.MaximumLength)
                break;
        }

        string result = sb.ToString();

        if (options.MaximumLength > 0 && result.Length > options.MaximumLength)
        {
            result = result.Substring(0, options.MaximumLength);
        }

        if (!options.CanEndWithSeparator && options.Separator != null && result.EndsWith(options.Separator))
        {
            result = result.Substring(0, result.Length - options.Separator.Length);
        }

        return result.Normalize(NormalizationForm.FormC);
    }

    private static bool EndsWith(StringBuilder sb, string text)
    {
        if (sb.Length < text.Length)
            return false;

        for (int i = 0; i < text.Length; i++)
        {
            if (sb[sb.Length - 1 - i] != text[text.Length - 1 - i])
                return false;
        }
        return true;
    }
}

/// <summary>
/// Defines options for the Slug utility class.
/// </summary>
public class SlugOptions
{
    /// <summary>
    /// Defines the default maximum length. Currently equal to 80.
    /// </summary>
    public const int DefaultMaximumLength = 80;

    /// <summary>
    /// Defines the default separator. Currently equal to "-".
    /// </summary>
    public const string DefaultSeparator = "-";

    private bool _toLower;
    private bool _toUpper;

    /// <summary>
    /// Initializes a new instance of the <see cref="SlugOptions"/> class.
    /// </summary>
    public SlugOptions()
    {
        MaximumLength = DefaultMaximumLength;
        Separator = DefaultSeparator;
        AllowedUnicodeCategories = new List<UnicodeCategory>();
        AllowedUnicodeCategories.Add(UnicodeCategory.UppercaseLetter);
        AllowedUnicodeCategories.Add(UnicodeCategory.LowercaseLetter);
        AllowedUnicodeCategories.Add(UnicodeCategory.DecimalDigitNumber);
        AllowedRanges = new List<KeyValuePair<short, short>>();
        AllowedRanges.Add(new KeyValuePair<short, short>((short)'a', (short)'z'));
        AllowedRanges.Add(new KeyValuePair<short, short>((short)'A', (short)'Z'));
        AllowedRanges.Add(new KeyValuePair<short, short>((short)'0', (short)'9'));
    }

    /// <summary>
    /// Gets the allowed unicode categories list.
    /// </summary>
    /// <value>
    /// The allowed unicode categories list.
    /// </value>
    public virtual IList<UnicodeCategory> AllowedUnicodeCategories { get; private set; }

    /// <summary>
    /// Gets the allowed ranges list.
    /// </summary>
    /// <value>
    /// The allowed ranges list.
    /// </value>
    public virtual IList<KeyValuePair<short, short>> AllowedRanges { get; private set; }

    /// <summary>
    /// Gets or sets the maximum length.
    /// </summary>
    /// <value>
    /// The maximum length.
    /// </value>
    public virtual int MaximumLength { get; set; }

    /// <summary>
    /// Gets or sets the separator.
    /// </summary>
    /// <value>
    /// The separator.
    /// </value>
    public virtual string Separator { get; set; }

    /// <summary>
    /// Gets or sets the culture for case conversion.
    /// </summary>
    /// <value>
    /// The culture.
    /// </value>
    public virtual CultureInfo Culture { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the string can end with a separator string.
    /// </summary>
    /// <value>
    ///   <c>true</c> if the string can end with a separator string; otherwise, <c>false</c>.
    /// </value>
    public virtual bool CanEndWithSeparator { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the string is truncated before normalization.
    /// </summary>
    /// <value>
    ///   <c>true</c> if the string is truncated before normalization; otherwise, <c>false</c>.
    /// </value>
    public virtual bool EarlyTruncate { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether to lowercase the resulting string.
    /// </summary>
    /// <value>
    ///   <c>true</c> if the resulting string must be lowercased; otherwise, <c>false</c>.
    /// </value>
    public virtual bool ToLower
    {
        get
        {
            return _toLower;
        }
        set
        {
            _toLower = value;
            if (_toLower)
            {
                _toUpper = false;
            }
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether to uppercase the resulting string.
    /// </summary>
    /// <value>
    ///   <c>true</c> if the resulting string must be uppercased; otherwise, <c>false</c>.
    /// </value>
    public virtual bool ToUpper
    {
        get
        {
            return _toUpper;
        }
        set
        {
            _toUpper = value;
            if (_toUpper)
            {
                _toLower = false;
            }
        }
    }

    /// <summary>
    /// Determines whether the specified character is allowed.
    /// </summary>
    /// <param name="character">The character.</param>
    /// <returns>true if the character is allowed; false otherwise.</returns>
    public virtual bool IsAllowed(char character)
    {
        foreach (var p in AllowedRanges)
        {
            if (character >= p.Key && character <= p.Value)
                return true;
        }
        return false;
    }

    /// <summary>
    /// Replaces the specified character by a given string.
    /// </summary>
    /// <param name="character">The character to replace.</param>
    /// <returns>a string.</returns>
    public virtual string Replace(char character)
    {
        return character.ToString();
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.