基于字节长度最好的方式来缩短UTF8字符串(Best way to shorten UTF8 str

2019-06-27 17:52发布

一个名为导入数据到Oracle数据库最近的一个项目。 这将做到这一点的程序是一个C#.NET 3.5的应用程序,我使用Oracle.DataAccess连接库来处理实际的插入。

我跑到这里插入一个特定的领域,当我收到此错误消息的问题:

ORA-12899的价值太大列X

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

最后,我看到了什么应该是显而易见的,我的字符串是ANSI和领域是UTF8。 它的长度以字节,而不是字符定义。

这让我对我的问题。 什么是修剪我的字符串来解决的MaxLength最好的方法是什么?

我的子代码工作的字符长度。 有没有简单的C#功能,可以通过字节长度智能裁剪UT8串(即不是砍掉一半的角色)?

Answer 1:

这里有两种可能的解决方案-一个LINQ单行处理左至右输入和一个传统的for -loop处理从右到左输入端。 其处理方向为更快取决于字串长度,所允许的字节长度,而且数量和多字节字符分布和很难给出一个一般的建议。 的味道(或者速度)的问题LINQ和传统的代码,我大概的决定。

如果速度的问题,一个能想到的只是积累每个字符的字节长度,直到达到最大长度,而不是计算在每个迭代整个字符串的字节长度。 但我不知道这是否会工作,因为我不知道UTF-8编码不够好做。 我能想象theoreticaly一个字符串的字节长度不等于所有字符的字节长度的总和。

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


Answer 2:

我认为我们可以做的比天真地计数,每次添加一个字符串的总长度更好。 LINQ是很酷,但它可能会意外地鼓励低效的代码。 如果我想第80000个字节的巨型UTF串的? 这是一个很多不必要的计数。 “我有1个字节。现在我已经得到2。现在我已经得到了13 ...现在我有52384 ......”

这是愚蠢的。 在大多数情况下,至少在L'英语,我们可以准确地对切割nth字节。 即使是在另一种语言,我们从良好的切削加工点不到6个字节的路程。

所以我打算从@奥伦的建议,这是掀起了UTF8字符值的前导位来钥匙启动。 让我们通过削减在一开始n+1th个字节,并使用奥伦的伎俩搞清楚,如果我们需要提前砍几字节。

三种可能性

如果切割后的第一个字节有0的领先位,我知道我在切割精确的单个字节(常规ASCII)字符之前,能干净地切割。

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

如果我有一个10 ,但是,我知道,我在一个多字节字符的中间,需要回去检查,看看它真正开始。

也就是说,虽然我要的第n个字节后绳剪断,如果第n + 1个字节是在一个多字节字符的中间,切割将创建一个无效的UTF8值。 我需要备份,直到我得到一个开头11 ,只是之前削减。

注:我使用的东西像Convert.ToByte("11000000", 2)这样可以很容易地告诉我遮蔽(约多一点位屏蔽什么位在这里 )。 概括地说,我&荷兰国际集团返回什么是在字节的前两位,并带回0 S为休息。 然后我检查XXXX000000 ,看它是否是1011 ,在适当情况下。

今天发现, C#6.0实际上可能支持二进制表示 ,这是很酷,但我们会继续使用这种杂牌,现在来说明发生了什么事情。

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

我最初写这个作为一个字符串扩展。 只需添加回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();
}

输出如下。 请注意,在“智能引号” testValue是三个字节长的UTF8(尽管当我们写的字符以ASCII控制台,它输出哑引号)。 另外要注意的? 的输出在输出每个智能报价的第二和第三字节。

我们的前五个字符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.

所以这是一种乐趣,我在短短的问题的五周年之前。 虽然位奥伦的描述了一个小错误,这正是你想要使用的伎俩。 谢谢你的问题; 整齐。



Answer 3:

如果一个UTF-8 字节有一个零值高位,这是一个字符的开始。 如果它的高位是1,它是在一个角色的“中间”。 检测字符开头的能力是UTF-8的一个明确的设计目标。

退房的说明部分维基百科文章的更多细节。



Answer 4:

是否有你所需要的数据库列以字节为单位被宣布的理由? 这是默认的,但如果数据库字符集是可变宽度它不是一个特别有用的默认值。 我强烈喜欢在文字方面宣布列。

CREATE TABLE length_example (
  col1 VARCHAR2( 10 BYTE ),
  col2 VARCHAR2( 10 CHAR )
);

这将创建一个表,其中COL1将存储10个字节的数据和COL2将存储有价值的数据的10个字符。 字符长度语义使得在UTF8数据库更为明智。

假设你希望所有创建默认使用的字符长度语义的表,你可以设置初始化参数NLS_LENGTH_SEMANTICS为CHAR。 在这一点上,您创建的任何表将默认使用字符长度语义,而不是字节长度语义,如果你不指定字段长度CHAR或BYTE。



Answer 5:

短版鲁芬的答案 。 注意到的优势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);
    }


Answer 6:

继奥伦Trutner的评论这里的问题有两种解决方案的更多:
在这里,我们计算的字节数从根据在字符串的结尾每个字符的字符串的结尾去掉,所以我们不评价在每次迭代整个字符串。

string str = "朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣" 
int maxBytesLength = 30;
var bytesArr = Encoding.UTF8.GetBytes(str);
int bytesToRemove = 0;
int lastIndexInString = str.Length -1;
while(bytesArr.Length - bytesToRemove > maxBytesLength)
{
   bytesToRemove += Encoding.UTF8.GetByteCount(new char[] {str[lastIndexInString]} );
   --lastIndexInString;
}
string trimmedString = Encoding.UTF8.GetString(bytesArr,0,bytesArr.Length - bytesToRemove);
//Encoding.UTF8.GetByteCount(trimmedString);//get the actual length, will be <= 朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣潬昣昸昸慢正 

和更有效(和维护)的解决方案:根据所希望的长度得到的字节阵列的串并切割的最后一个字符,因为它可能会被破坏

string str = "朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣" 
int maxBytesLength = 30;    
string trimmedWithDirtyLastChar = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str),0,maxBytesLength);
string trimmedString = trimmedWithDirtyLastChar.Substring(0,trimmedWithDirtyLastChar.Length - 1);

与第二个解决方案唯一的缺点是,我们可能会削减完全正常的最后一个字符,但我们已经切割字符串,因此它可能会要求适应。
由于Shhade谁想到第二个解决方案



Answer 7:

这是一种基于二进制搜索另一种解决方案:

public string LimitToUTF8ByteLength(string text, int size)
{
    if (size <= 0)
    {
        return string.Empty;
    }

    int maxLength = text.Length;
    int minLength = 0;
    int length = maxLength;

    while (maxLength >= minLength)
    {
        length = (maxLength + minLength) / 2;
        int byteLength = Encoding.UTF8.GetByteCount(text.Substring(0, length));

        if (byteLength > size)
        {
            maxLength = length - 1;
        }
        else if (byteLength < size)
        {
            minLength = length + 1;
        }
        else
        {
            return text.Substring(0, length); 
        }
    }

    // Round down the result
    string result = text.Substring(0, length);
    if (size >= Encoding.UTF8.GetByteCount(result))
    {
        return result;
    }
    else
    {
        return text.Substring(0, length - 1);
    }
}


Answer 8:

所有其他的答案似乎错过了事实,这个功能已经内置到.NET,在Encoder类。 对于加分,这种方法也将适用于其他编码。

public static String LimitByteLength(string input, int maxLength)
{
    if (string.IsNullOrEmpty(input) || Encoding.UTF8.GetByteLength(input) <= 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(bytes, 0, bytesUsed);
}


Answer 9:

public static string LimitByteLength3(string input, Int32 maxLenth)
    {
        string result = input;

        int byteCount = Encoding.UTF8.GetByteCount(input);
        if (byteCount > maxLenth)
        {
            var byteArray = Encoding.UTF8.GetBytes(input);
            result = Encoding.UTF8.GetString(byteArray, 0, maxLenth);
        }

        return result;
    }


文章来源: Best way to shorten UTF8 string based on byte length