18

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

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

ORA-12899对于列 X 的值太大

我使用过Field.Substring(0, MaxLength);但仍然出现错误(尽管不是针对每条记录)。

最后我看到了应该很明显的东西,我的字符串是 ANSI,字段是 UTF8。它的长度以字节而不是字符定义。

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

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

4

9 回答 9

19

这里有两种可能的解决方案 - LINQfor单线处理从左到右的输入和传统的循环处理从右到左的输入。哪个处理方向更快取决于字符串长度、允许的字节长度以及多字节字符的数量和分布,很难给出一般性的建议。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;
}
于 2009-08-04T01:01:50.480 回答
19

我认为我们可以做得更好,而不是在每次添加时天真地计算字符串的总长度。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)这样的东西,以便很容易分辨出我正在屏蔽哪些位(更多关于位掩码的信息)。简而言之,我要&返回字节的前两位中的内容,并0为其余部分返回 s。然后我检查XXfromXX000000以查看它是否是10or 11,在适当的情况下。

今天发现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();
}

输出如下。请注意,UTF8 中的“智能引号”testValue长度为三个字节(尽管当我们以 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.

所以这很有趣,我就在这个问题五周年之前。尽管 Oren 对这些位的描述有一个小错误,但这正是您想要使用的技巧。感谢您的提问;整洁的。

于 2014-06-28T20:31:07.817 回答
7

鲁芬答案的较短版本。利用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);
    }
于 2017-05-17T13:30:31.013 回答
6
于 2019-03-04T13:50:23.380 回答
4

如果 UTF-8字节的高位为零值,​​则它是字符的开头。如果它的高位为 1,则它位于字符的“中间”。检测字符开头的能力是 UTF-8 的明确设计目标。

查看维基百科文章的描述部分以获取更多详细信息。

于 2009-08-03T23:19:05.980 回答
1

是否有理由需要以字节为单位声明数据库列?这是默认值,但如果数据库字符集是可变宽度的,它就不是特别有用的默认值。我强烈希望以字符的形式声明该列。

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

这将创建一个表,其中 COL1 将存储 10 个字节的数据,而 col2 将存储 10 个字符的数据。字符长度语义在 UTF8 数据库中更有意义。

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

于 2009-08-04T07:13:26.717 回答
1

以下是Oren 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);

第二种解决方案的唯一缺点是我们可能会剪切一个完美的最后一个字符,但我们已经剪切了字符串,因此它可能符合要求。
感谢考虑第二个解决方案的Shade

于 2016-10-06T07:06:35.827 回答
1

这是另一种基于二分查找的解决方案:

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);
    }
}
于 2016-10-23T17:05:31.230 回答
-1
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;
    }
于 2015-09-03T10:13:20.690 回答