意外截断字符串时拆分Unicode字符

7
我需要将第三方提供的一些字符串保存到我的数据库(Postgres)中。有时候这些字符串太长了,需要截断以适应我的数据表中的列。
但是有时我会不小心在Unicode字符处截断字符串,这会导致我无法将“损坏”的字符串保存到数据库中。我会收到以下错误:Unable to translate Unicode character \uD83D at index XXX to specified code page。
我创建了一个最简示例来说明我的意思。这里有一个包含Unicode字符(“小蓝钻石” U+1F539)的字符串。根据我截取的位置不同,它会给我一个有效的字符串或无效的字符串。
var myString = @"This is a string before an emoji: This is after the emoji.";

var brokenString = myString.Substring(0, 34);
// Gives: "This is a string before an emoji:☐"

var test3 = myString.Substring(0, 35);
// Gives: "This is a string before an emoji:"

有没有办法在不破坏任何Unicode字符的情况下截断字符串?

4个回答

6

一个Unicode字符可能由多个char表示,这就是你使用string.Substring遇到的问题所在。

你可以将你的string转换为一个StringInfo对象,然后使用SubstringByTextElements()方法来根据Unicode字符数获取子字符串,而不是char计数。

参见C#演示

Console.WriteLine("".Length); // => 2
Console.WriteLine(new StringInfo("").LengthInTextElements); // => 1

var myString = @"This is a string before an emoji:This is after the emoji.";
var teMyString = new StringInfo(myString);
Console.WriteLine(teMyString.SubstringByTextElements(0, 33));
// => "This is a string before an emoji:"
Console.WriteLine(teMyString.SubstringByTextElements(0, 34));
// => This is a string before an emoji:
Console.WriteLine(teMyString.SubstringByTextElements(0, 35));
// => This is a string before an emoji:T

好的!谢谢。我实际上找到了这个:https://dev59.com/OVwZ5IYBdhLWcg3wBcaz#31936096。那个跟你的解决方案相比怎么样?一样吗? - Joel
@Joel 我已经学习了被接受的答案,并且与当前任务进行了比较。那个子字符串方法是为特定问题量身定制的,详见Xanatos的解释:因此,初始/最终“分割”的代理对将被删除,初始组合标记将被删除,缺少其组合标记的最终字符将被删除。 - Wiktor Stribiżew
我正想写同样的 :). 我选择了被接受的答案。 - Joel
如果我这样做var newStr = new StringInfo(text).SubstringByTextElements(0, maxChars);,那么 newStr.Length 不等于 maxChars。我错过了什么吗? - Toolkit
1
@Toolkit 你正在计算一个 string 的长度,为了获取 newStr 中字符的数量,你需要再次创建一个 StringInfo 实例,然后使用 LengthInTextElements 属性,请参见此 C# 演示 - Wiktor Stribiżew

1
我最终使用了xanatos答案的修改版这里。与原版不同的是,如果添加一个字符会使字符串长度大于length,则此版本将剥离最后一个字素。
    public static string UnicodeSafeSubstring(this string str, int startIndex, int length)
    {
        if (str == null)
        {
            throw new ArgumentNullException(nameof(str));
        }

        if (startIndex < 0 || startIndex > str.Length)
        {
            throw new ArgumentOutOfRangeException(nameof(startIndex));
        }

        if (length < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length));
        }

        if (startIndex + length > str.Length)
        {
            throw new ArgumentOutOfRangeException(nameof(length));
        }

        if (length == 0)
        {
            return string.Empty;
        }

        var stringBuilder = new StringBuilder(length);

        var enumerator = StringInfo.GetTextElementEnumerator(str, startIndex);

        while (enumerator.MoveNext())
        {
            var grapheme = enumerator.GetTextElement();
            startIndex += grapheme.Length;

            if (startIndex > str.Length)
            {
                break;
            }

            // Skip initial Low Surrogates/Combining Marks
            if (stringBuilder.Length == 0)
            {
                if (char.IsLowSurrogate(grapheme[0]))
                {
                    continue;
                }

                var cat = char.GetUnicodeCategory(grapheme, 0);

                if (cat == UnicodeCategory.NonSpacingMark || cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.EnclosingMark)
                {
                    continue;
                }
            }

            // Do not append the grapheme if the resulting string would be longer than the required length
            if (stringBuilder.Length + grapheme.Length <= length)
            {
                stringBuilder.Append(grapheme);
            }

            if (stringBuilder.Length >= length)
            {
                break;
            }
        }

        return stringBuilder.ToString();
    }
}

1

这是一个截断示例(startIndex = 0):

string truncatedStr = (str.Length > maxLength)
    ? str.Substring(0, maxLength - (char.IsLowSurrogate(str[maxLength]) ? 1 : 0))
    : str;

0

最好按字节数截断而不是字符串长度

   public static string TruncateByBytes(this string text, int maxBytes)
    {
        if (string.IsNullOrEmpty(text) || Encoding.UTF8.GetByteCount(text) <= maxBytes)
        {
            return text;
        }
        var enumerator = StringInfo.GetTextElementEnumerator(text);
        var newStr = string.Empty;
        do
        {
            enumerator.MoveNext();
            if (Encoding.UTF8.GetByteCount(newStr + enumerator.Current) <= maxBytes)
            {
                newStr += enumerator.Current;
            }
            else
            {
                break;
            }
        } while (true);
        return newStr;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接