基于字节长度缩短UTF8字符串的最佳方法

Mic*_*oie 14 c# oracle utf-8 ora-12899

最近的一个项目要求将数据导入Oracle数据库.执行此操作的程序是C#.Net 3.5应用程序,我正在使用Oracle.DataAccess连接库来处理实际插入.

我遇到了一个问题,我在插入特定字段时收到此错误消息:

ORA-12899对于X列而言值太大

我用过Field.Substring(0, MaxLength);但仍然得到错误(虽然不是每个记录).

最后我看到了应该是显而易见的,我的字符串是ANSI,字段是UTF8.它的长度以字节为单位,而不是字符.

这让我想到了我的问题.修剪字符串以修复MaxLength的最佳方法是什么?

我的子字符串代码按字符长度工作.是否有简单的C#函数可以按字节长度智能地修剪UT8字符串(即不会破坏半个字符)?

Dan*_*ner 13

这里有两种可能的解决方案 - 一个LINQ for单线程处理从左到右的输入,一个传统的-loop处理从右到左的输入.哪个处理方向更快取决于字符串长度,允许的字节长度以及多字节字符的数量和分布,很难给出一般性建议.LINQ和传统代码之间的决定我可能是品味(或者速度)的问题.

如果速度很重要,可以考虑只累加每个字符的字节长度,直到达到最大长度,而不是计算每次迭代中整个字符串的字节长度.但我不确定这是否有效,因为我不太了解UTF-8编码.我理论上可以想象字符串的字节长度不等于所有字符的字节长度之和.

public static String LimitByteLength(String input, Int32 maxLength)
{
    return new String(input
        .TakeWhile((c, i) =>
            Encoding.UTF8.GetByteCount(input.Substring(0, i + 1)) <= maxLength)
        .ToArray());
}

public static String LimitByteLength2(String input, Int32 maxLength)
{
    for (Int32 i = input.Length - 1; i >= 0; i--)
    {
        if (Encoding.UTF8.GetByteCount(input.Substring(0, i + 1)) <= maxLength)
        {
            return input.Substring(0, i + 1);
        }
    }

    return String.Empty;
}
Run Code Online (Sandbox Code Playgroud)

  • 几年后。我想补充一点,这个函数分配了很多!线太长的话CG会很痛苦 (2认同)

ruf*_*fin 13

我认为我们可以做得比通过每次添加天真地计算字符串的总长度更好.LINQ很酷,但它可能会意外地鼓励低效的代码.如果我想要一个巨大的UTF字符串的前80,000字节怎么办?这是很多不必要的计算."我有1个字节.现在我有2.现在我有13个......现在我有52,384 ......"

那太傻了.大多数时候,至少在l'anglais中,我们可以精确地切割该nth字节.即使在另一种语言中,我们距离一个好的切割点还不到6个字节.

因此,我将从@Oren的建议开始,即关闭UTF8 char值的前导位.让n+1th我们从切换到字节开始,并使用Oren的技巧来确定我们是否需要提前减少几个字节.

三种可能性

如果切割后的第一个字节在0前导位中有一个,我知道我正在切换到单个字节(传统的ASCII)字符之前,并且可以干净地切割.

如果我有一个11继切,切后的下一个字节是开始一个多字节字符的,所以这是剪得的好地方!

10但是,如果我有一个,我知道我正处于一个多字节字符的中间,需要回去查看它真正开始的位置.

也就是说,虽然我想在第n个字节之后剪切字符串,但如果第n + 1个字节位于多字节字符的中间,则剪切将创建无效的UTF8值.我需要备份,直到我找到一个开始11并在它之前切割的那个.

注意:我正在使用这样的东西,Convert.ToByte("11000000", 2)以便很容易分辨出我正在屏蔽的是什么(稍微有点掩盖在这里).简而言之,我要&返回字节前两位中的内容,然后将其返回0s.然后我检查一下XXXX000000,看它是否是,10或者11在适当的时候.

今天发现C#6.0可能实际上支持二进制表示,这很酷,但我们现在将继续使用这个kludge来说明正在发生的事情.

PadLeft只是因为我对控制台的输出过于强迫.

所以这里有一个函数,它会把你切成一个n长度为字节的字符串,或者n用一个"完整的"UTF8字符结束的最大数字.

public static string CutToUTF8Length(string str, int byteLength)
{
    byte[] byteArray = Encoding.UTF8.GetBytes(str);
    string returnValue = string.Empty;

    if (byteArray.Length > byteLength)
    {
        int bytePointer = byteLength;

        // Check high bit to see if we're [potentially] in the middle of a multi-byte char
        if (bytePointer >= 0 
            && (byteArray[bytePointer] & Convert.ToByte("10000000", 2)) > 0)
        {
            // If so, keep walking back until we have a byte starting with `11`,
            // which means the first byte of a multi-byte UTF8 character.
            while (bytePointer >= 0 
                && Convert.ToByte("11000000", 2) != (byteArray[bytePointer] & Convert.ToByte("11000000", 2)))
            {
                bytePointer--;
            }
        }

        // See if we had 1s in the high bit all the way back. If so, we're toast. Return empty string.
        if (0 != bytePointer)
        {
            returnValue = Encoding.UTF8.GetString(byteArray, 0, bytePointer); // hat tip to @NealEhardt! Well played. ;^)
        }
    }
    else
    {
        returnValue = str;
    }

    return returnValue;
}
Run Code Online (Sandbox Code Playgroud)

我最初把它写成字符串扩展名.当然,只需将this之前的内容添加回string str扩展格式即可.我删除了this所以我们可以将方法打入Program.cs一个简单的控制台应用程序来演示.

测试和预期输出

这是一个很好的测试用例,下面创建的输出,写Main在一个简单的控制台应用程序中Program.cs.

static void Main(string[] args)
{
    string testValue = "12345“”67890”";

    for (int i = 0; i < 15; i++)
    {
        string cutValue = Program.CutToUTF8Length(testValue, i);
        Console.WriteLine(i.ToString().PadLeft(2) +
            ": " + Encoding.UTF8.GetByteCount(cutValue).ToString().PadLeft(2) +
            ":: " + cutValue);
    }

    Console.WriteLine();
    Console.WriteLine();

    foreach (byte b in Encoding.UTF8.GetBytes(testValue))
    {
        Console.WriteLine(b.ToString().PadLeft(3) + " " + (char)b);
    }

    Console.WriteLine("Return to end.");
    Console.ReadLine();
}
Run Code Online (Sandbox Code Playgroud)

输出如下.请注意,testValueUTF8 中的"智能引号" 长度为三个字节(但是当我们以ASCII格式将字符写入控制台时,它会输出哑引号).另请注意?输出中每个智能引号的第二个和第三个字节的s输出.

我们的前五个字符testValue是UTF8中的单个字节,因此0-5字节值应为0-5个字符.然后我们有一个三字节的智能引号,直到5 + 3字节才能完整地包含它.果然,我们看到在呼叫时8弹出.我们的下一个智能引号弹出8 + 3 = 11,然后我们回到单字节字符到14.

 0:  0::
 1:  1:: 1
 2:  2:: 12
 3:  3:: 123
 4:  4:: 1234
 5:  5:: 12345
 6:  5:: 12345
 7:  5:: 12345
 8:  8:: 12345"
 9:  8:: 12345"
10:  8:: 12345"
11: 11:: 12345""
12: 12:: 12345""6
13: 13:: 12345""67
14: 14:: 12345""678


 49 1
 50 2
 51 3
 52 4
 53 5
226 â
128 ?
156 ?
226 â
128 ?
157 ?
 54 6
 55 7
 56 8
 57 9
 48 0
226 â
128 ?
157 ?
Return to end.
Run Code Online (Sandbox Code Playgroud)

所以这很有趣,我就在问题五周年之前.虽然Oren对这些位的描述有一个小错误,但这正是你想要使用的技巧.谢谢你的提问; 整齐.

  • 太棒了,你在O(N)做到了!谢谢,这对于长串非常棒. (2认同)

fir*_*rda 6

ruffin 答案的简短版本。利用UTF8 的设计

    public static string LimitUtf8ByteCount(this string s, int n)
    {
        // quick test (we probably won't be trimming most of the time)
        if (Encoding.UTF8.GetByteCount(s) <= n)
            return s;
        // get the bytes
        var a = Encoding.UTF8.GetBytes(s);
        // if we are in the middle of a character (highest two bits are 10)
        if (n > 0 && ( a[n]&0xC0 ) == 0x80)
        {
            // remove all bytes whose two highest bits are 10
            // and one more (start of multi-byte sequence - highest bits should be 11)
            while (--n > 0 && ( a[n]&0xC0 ) == 0x80)
                ;
        }
        // convert back to string (with the limit adjusted)
        return Encoding.UTF8.GetString(a, 0, n);
    }
Run Code Online (Sandbox Code Playgroud)


can*_*on7 5

所有其他答案似乎都忽略了这个功能已经内置到 .NET 中的事实Encoder。对于奖励积分,这种方法也适用于其他编码。

public static string LimitByteLength(string message, int maxLength)
{
    if (string.IsNullOrEmpty(message) || Encoding.UTF8.GetByteCount(message) <= maxLength)
    {
        return message;
    }

    var encoder = Encoding.UTF8.GetEncoder();
    byte[] buffer = new byte[maxLength];
    char[] messageChars = message.ToCharArray();
    encoder.Convert(
        chars: messageChars,
        charIndex: 0,
        charCount: messageChars.Length,
        bytes: buffer,
        byteIndex: 0,
        byteCount: buffer.Length,
        flush: false,
        charsUsed: out int charsUsed,
        bytesUsed: out int bytesUsed,
        completed: out bool completed);

    // I don't think we can return message.Substring(0, charsUsed)
    // as that's the number of UTF-16 chars, not the number of codepoints
    // (think about surrogate pairs). Therefore I think we need to
    // actually convert bytes back into a new string
    return Encoding.UTF8.GetString(buffer, 0, bytesUsed);
}
Run Code Online (Sandbox Code Playgroud)

如果您使用 .NET Standard 2.1+,您可以稍微简化一下:

public static string LimitByteLength(string message, int maxLength)
{
    if (string.IsNullOrEmpty(message) || Encoding.UTF8.GetByteCount(message) <= maxLength)
    {
        return message;
    }

    var encoder = Encoding.UTF8.GetEncoder();
    byte[] buffer = new byte[maxLength];
    encoder.Convert(message.AsSpan(), buffer.AsSpan(), false, out _, out int bytesUsed, out _);
    return Encoding.UTF8.GetString(buffer, 0, bytesUsed);
}
Run Code Online (Sandbox Code Playgroud)

其他答案都没有考虑扩展的字形簇,例如?. 它由 4 个 Unicode 标量(、零宽度连接符和)组成,因此您需要了解 Unicode 标准以避免在中间拆分它并生成

.NET 5以后,你可以这样写:

public static string LimitByteLength(string message, int maxLength)
{
    if (string.IsNullOrEmpty(message) || Encoding.UTF8.GetByteCount(message) <= maxLength)
    {
        return message;
    }
    
    var enumerator = StringInfo.GetTextElementEnumerator(message);
    var result = new StringBuilder();
    int lengthBytes = 0;
    while (enumerator.MoveNext())
    {
        lengthBytes += Encoding.UTF8.GetByteCount(enumerator.GetTextElement());
        if (lengthBytes <= maxLength)
        {
            result.Append(enumerator.GetTextElement()); 
        }
    }
    
    return result.ToString();
}
Run Code Online (Sandbox Code Playgroud)

(同样的代码在 .NET 的早期版本上运行,但由于一个错误,它不会在 .NET 5 之前产生正确的结果)。