อัลกอริทึม URL Slugify ใน C #?


86

ดังนั้นฉันได้ค้นหาและเรียกดูแท็กslugบน SO และพบเพียงสองวิธีที่น่าสนใจ:

ซึ่งเป็นวิธีแก้ปัญหาเพียงบางส่วน ฉันสามารถเขียนโค้ดด้วยตัวเองได้ แต่ฉันแปลกใจที่ยังไม่มีวิธีแก้ปัญหา

ดังนั้นจึงมีการใช้ alrogithm slugify ใน C # และ / หรือ. NET ที่จัดการกับอักขระละตินยูนิโคดและปัญหาภาษาอื่น ๆ อย่างเหมาะสมหรือไม่?


"slugify" หมายความว่าอย่างไร?
Billy ONeal

7
slugify = ทำให้สตริงที่ผู้ใช้ส่งมานั้นปลอดภัยสำหรับใช้เป็นส่วนหนึ่งของ URL ... หรือฐานข้อมูลหรืออะไรก็ตาม แต่โดยปกติจะเป็น URL
chakrit

คำตอบ:


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

ลิงก์ที่โพสต์ตรงตามคำถามของ OP เป็นอย่างดี
Ian P

6
จุดประสงค์ของความยาวและการตัดให้เกิน 45 อักขระคืออะไร?

11
โซลูชันจะไม่ทำงานสำหรับตัวอักษรที่ไม่ใช่ภาษาละติน เมธอด RemoveAccent จะลบตัวอย่างเช่นอักขระซีริลลิก ลองใช้บางอย่างเช่น RemoveAccent ("Неработает") และผลลัพธ์จะเป็นสตริงว่างเปล่า: D
Evereq

รักโซลูชันนี้ได้สร้างส่วนขยายชื่อ 'ToSlug ()' โดยใช้สิ่งนี้
Yasser Shaikh

12
กรุณา ... อย่าใช้RemoveAccent. RemoveDiacriticsตรวจสอบคำถามนี้ดังนั้นสำหรับวิธีการ stackoverflow.com/questions/249087/…
Maxime Rouiller

21

ที่นี่คุณจะพบวิธีสร้าง url slug ใน c # ฟังก์ชันนี้จะลบการเน้นเสียงทั้งหมด (คำตอบของ Marcel) แทนที่ช่องว่างลบตัวอักษรที่ไม่ถูกต้องตัดขีดกลางจากจุดสิ้นสุดและแทนที่การเกิดซ้ำสองครั้งของ "-" หรือ "_"

รหัส:

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

นี่คือความหมายของฉันตามคำตอบของ Joan และ Marcel การเปลี่ยนแปลงที่ฉันทำมีดังนี้:

  • ใช้ วิธีที่ได้รับการยอมรับอย่างกว้างขวางเพื่อลบสำเนียง
  • การแคช Regex อย่างชัดเจนสำหรับการปรับปรุงความเร็วเล็กน้อย
  • ตัวคั่นคำอื่น ๆ ได้รับการยอมรับและทำให้เป็นเครื่องหมายขีดกลาง

นี่คือรหัส:

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

สิ่งนี้ยังไม่สามารถแก้ปัญหาอักขระที่ไม่ใช่ภาษาละตินได้ อีกทางเลือกหนึ่งคือการใช้Uri.EscapeDataStringเพื่อแปลงสตริงเป็นตัวแทนฐานสิบหก

string original = "测试公司";

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

จากนั้นใช้ข้อมูลเพื่อสร้างไฮเปอร์ลิงก์:

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

เบราว์เซอร์จำนวนมากจะแสดงอักษรจีนในแถบที่อยู่ (ดูด้านล่าง) แต่จากการทดสอบที่ จำกัด ของฉันมันไม่ได้รับการสนับสนุนอย่างสมบูรณ์

แถบที่อยู่พร้อมตัวอักษรจีน

หมายเหตุ: เพื่อให้Uri.EscapeDataStringทำงานในลักษณะนี้ต้องเปิดใช้งานiriParsing


แก้ไข

สำหรับผู้ที่ต้องการสร้าง URL Slugs ใน C # ขอแนะนำให้ตรวจสอบคำถามที่เกี่ยวข้องนี้:

Stack Overflow สร้าง URL ที่เป็นมิตรกับ SEO ได้อย่างไร

มันคือสิ่งที่ฉันใช้สำหรับโครงการของฉัน


4

ปัญหาอย่างหนึ่งที่ฉันเคยมีกับการทำให้เป็นก้อน (คำใหม่!) คือการชนกัน ตัวอย่างเช่นหากฉันมีบล็อกโพสต์เรียกว่า "Stack-Overflow" และอีกรายการหนึ่งเรียกว่า "Stack Overflow" กระสุนของทั้งสองชื่อจะเหมือนกัน ดังนั้นเครื่องกำเนิดกระสุนของฉันมักจะต้องเกี่ยวข้องกับฐานข้อมูลไม่ทางใดก็ทางหนึ่ง นี่อาจเป็นสาเหตุที่คุณไม่เห็นโซลูชันทั่วไปเพิ่มเติม


6
โดยส่วนตัวแล้วฉันชอบต่อท้ายทากด้วยตัวระบุเฉพาะ (เช่นจำนวนเต็ม) เพื่อให้แน่ใจว่าไม่ซ้ำกัน ไม่ใช่วิธีแก้ปัญหาที่เป็นมิตรที่สุด แต่ช่วยให้ฉันหลีกเลี่ยงปัญหาได้
Jeremy Cade

1
เพียงแค่ความคิดเห็นจาก SEO ชี้ให้เห็น URL และชื่อควรไม่ซ้ำกันสำหรับแต่ละหน้า
Rafael Herscovici

3

นี่คือช็อตของฉัน รองรับ:

  • การลบตัวกำกับเสียง (ดังนั้นเราจึงไม่เพียงแค่ลบอักขระ "ไม่ถูกต้อง")
  • ความยาวสูงสุดสำหรับผลลัพธ์ (หรือก่อนการลบตัวกำกับเสียง - "ตัดต้น")
  • ตัวคั่นที่กำหนดเองระหว่างชิ้นส่วนปกติ
  • ผลลัพธ์สามารถบังคับให้เป็นตัวพิมพ์ใหญ่หรือตัวพิมพ์เล็ก
  • รายการที่กำหนดได้ของหมวดหมู่ Unicode ที่รองรับ
  • รายการช่วงของอักขระที่อนุญาตที่กำหนดได้
  • รองรับกรอบ 2.0

รหัส:

/// <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();
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.