首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 字串精確截取:中英混排、HTML 標籤過濾、字元寬度計算

     編輯:關於C語言

前言

列表頁標題、摘要、搜尋結果常需截取固定寬度字串。中英混排下單純 Substring 會導致中文截半、寬度不一。本文提供精確計算字元顯示寬度的截取方法,含 HTML 標籤過濾、尾碼附加、效能優化。

核心邏輯:字元寬度計算


public static class StringTruncate
{
    /// 
    /// 取得字元顯示寬度:中文/全形=2,英文/半形=1
    /// 
    public static int GetCharWidth(char c)
    {
        // Unicode 區塊判斷比單純 > 255 更精確
        return c switch
        {
            // CJK 統一表意文字
            >= '\u4E00' and <= '\u9FFF' => 2,
            // CJK 符號和標點
            >= '\u3000' and <= '\u303F' => 2,
            // 全形英文/數字/符號
            >= '\uFF01' and <= '\uFF5E' => 2,
            // 半形片假名
            >= '\uFF61' and <= '\uFF9F' => 1,
            // 控制字元、格式字元視為 0
            _ when char.IsControl(c) || char.GetUnicodeCategory(c) == System.Globalization.UnicodeCategory.Format => 0,
            _ => 1 // ASCII 可列印字元、其他視為半形
        };
    }

    /// 
    /// 精確截取至指定顯示寬度
    /// 
    public static string TruncateByWidth(string input, int maxWidth, string suffix = "...")
    {
        if (string.IsNullOrEmpty(input) || maxWidth <= 0) return string.Empty;
        
        int suffixWidth = suffix.Sum(GetCharWidth);
        if (suffixWidth >= maxWidth) return string.Empty; // 寬度不足放尾碼
        
        int availableWidth = maxWidth - suffixWidth;
        int currentWidth = 0;
        int lastValidIndex = -1;
        
        for (int i = 0; i < input.Length; i++)
        {
            int charWidth = GetCharWidth(input[i]);
            if (currentWidth + charWidth > availableWidth)
                break;
            
            currentWidth += charWidth;
            lastValidIndex = i;
        }
        
        if (lastValidIndex == -1) return suffix; // 連第一個字都放不下
        if (lastValidIndex == input.Length - 1) return input; // 無需截取
        
        return input.Substring(0, lastValidIndex + 1) + suffix;
    }
}

HTML 標籤過濾


public static class HtmlSanitizer
{
    // 簡易版:移除所有標籤,保留文字內容
    private static readonly System.Text.RegularExpressions.Regex _tagRegex = 
        new(@"<[^>]*>", System.Text.RegularExpressions.RegexOptions.Compiled);

    public static string StripTags(string html)
    {
        if (string.IsNullOrEmpty(html)) return string.Empty;
        return _tagRegex.Replace(html, string.Empty);
    }

    // 進階版:保留安全標籤 (b, i, u, span 等),移除 script/style/iframe
    public static string Sanitize(string html, string[] allowedTags = null)
    {
        // 建議使用 HtmlSanitizer 套件 (NuGet: Ganss.XSS)
        // 此處略過複雜實作
        return StripTags(html);
    }
}

完整整合範例


public static class SummaryGenerator
{
    public static string GenerateSummary(string htmlContent, int maxWidth = 80, string suffix = "...")
    {
        // 1. 移除 HTML 標籤
        string plainText = HtmlSanitizer.StripTags(htmlContent);
        
        // 2. 合併連續空白字元
        plainText = System.Text.RegularExpressions.Regex.Replace(plainText, @"\s+", " ");
        
        // 3. 精確截取
        return StringTruncate.TruncateByWidth(plainText.Trim(), maxWidth, suffix);
    }
}

// 使用
string html = "

這是一個測試內容,包含 English text。

"; string summary = SummaryGenerator.GenerateSummary(html, 40); // 結果: "這是一個測試內容,包含 English..."

效能優化:避免 LINQ/字串連接


public static class FastTruncate
{
    // 預計算 ASCII 字元寬度查找表
    private static readonly byte[] _asciiWidth = new byte[128];
    
    static FastTruncate()
    {
        for (int i = 0; i < 128; i++)
        {
            char c = (char)i;
            _asciiWidth[i] = (byte)(char.IsControl(c) ? 0 : 1);
        }
    }

    public static string Truncate(string input, int maxWidth, string suffix = "...")
    {
        if (string.IsNullOrEmpty(input) || maxWidth <= 0) return string.Empty;
        
        int suffixWidth = 0;
        foreach (char c in suffix) suffixWidth += GetWidth(c);
        if (suffixWidth >= maxWidth) return string.Empty;
        
        int avail = maxWidth - suffixWidth;
        int width = 0;
        int end = 0;
        
        for (int i = 0; i < input.Length; i++)
        {
            char c = input[i];
            int w = c < 128 ? _asciiWidth[c] : GetWidth(c);
            
            if (width + w > avail) break;
            width += w;
            end = i + 1;
        }
        
        if (end == 0) return suffix;
        if (end >= input.Length) return input;
        
        return input.Substring(0, end) + suffix;
    }

    private static int GetWidth(char c)
    {
        // 同 StringTruncate.GetCharWidth 邏輯
        if (c >= '\u4E00' && c <= '\u9FFF') return 2;
        if (c >= '\u3000' && c <= '\u303F') return 2;
        if (c >= '\uFF01' && c <= '\uFF5E') return 2;
        if (c >= '\uFF61' && c <= '\uFF9F') return 1;
        if (char.IsControl(c)) return 0;
        return 1;
    }
}

單元測試驗證


[TestClass]
public class TruncateTests
{
    [TestMethod]
    public void PureChinese()
    {
        Assert.AreEqual("中文...", StringTruncate.TruncateByWidth("中文字符串", 6)); // 3字*2=6
    }
    
    [TestMethod]
    public void PureEnglish()
    {
        Assert.AreEqual("Hello...", StringTruncate.TruncateByWidth("Hello World", 7)); // 5字*1+3=8? Wait: "Hello"=5, "..."=3, total=8. maxWidth=7 -> "Hel..."=3+3=6
    }
    
    [TestMethod]
    public void Mixed()
    {
        // "中English" = 2+1+1+1+1+1+1+1 = 9
        // maxWidth=10, suffix=3 => avail=7 => "中Engl" (2+1+1+1+1+1=7) + "..."
        Assert.AreEqual("中Engl...", StringTruncate.TruncateByWidth("中English", 10));
    }
    
    [TestMethod]
    public void HtmlStripped()
    {
        string html = "
內容
"; Assert.AreEqual("內容...", SummaryGenerator.GenerateSummary(html, 6)); } [TestMethod] public void EmojiAndSpecial() { // Emoji 通常佔 2 字元位置 (surrogate pair) string s = "😀Hello"; // 😀 = 2 char (surrogate), 視為寬度 2 // 實際寬度計算需處理 surrogate pair } }

Emoji / Surrogate Pair 處理


public static int GetTextElementWidth(string s, int index)
{
    // 使用 StringInfo 處理字素簇
    var info = new System.Globalization.StringInfo(s);
    int textElementCount = info.LengthInTextElements;
    
    // 簡化:取得指定索引的文字元素
    string element = info.SubstringByTextElements(index, 1);
    
    // 判斷寬度:EastAsianWidth 屬性
    // 需 NuGet: NodaTime 或自建查表
    // 此處略過複雜實作
    return element.Length switch
    {
        1 => GetCharWidth(element[0]),
        2 => 2, // Surrogate pair (Emoji 等) 通常顯示寬度 2
        _ => element.Length
    };
}

現代替代:TextElementEnumerator


using System.Globalization;

public static string TruncateByTextElements(string input, int maxElements, string suffix = "...")
{
    var enumerator = StringInfo.GetTextElementEnumerator(input);
    var elements = new List();
    
    while (enumerator.MoveNext())
        elements.Add(enumerator.GetTextElement());
    
    if (elements.Count <= maxElements) return input;
    
    return string.Join("", elements.Take(maxElements)) + suffix;
}

總結

精確字串截取需考慮:字元顯示寬度 (半形/全形)、HTML 標籤、Surrogate Pair (Emoji)、字素簇。生產代碼建議:

  1. 先 HtmlSanitizer.StripTags 移除標籤
  2. 使用查表法 GetCharWidth 計算寬度
  3. 預留尾碼寬度,避免截半字元
  4. 高頻呼叫用 FastTruncate 避免 LINQ/配置
  5. 複雜排版需求引入 NodaTime 或 SkiaSharp 文字度量
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved