2009-08-03 62 views
13

最近调用一个将数据导入Oracle数据库的项目。这样做的程序是一个C#.Net 3.5应用程序,我使用Oracle.DataAccess连接库来处理实际的插入。根据字节长度缩短UTF8字符串的最佳方法

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

ORA-12899的价值太大,X

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

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

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

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

+1

P.S.我包括介绍,以防万一任何人在将来使用我的Oracle错误消息。希望这会为他们节省一些时间。 – 2009-08-03 23:05:55

回答

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

我喜欢LINQ示例。这是一个优雅的解决方案! – 2009-08-04 18:51:34

+0

+1喜欢这两个解决方案 – Feryt 2010-02-03 13:18:08

4

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

查看wikipedia article的描述部分了解更多详情。

+0

感谢您的提示。你能告诉我一个C#示例吗?这是否意味着没有任何内置的功能来处理这种需求?这似乎是一个普遍的问题。 – 2009-08-03 23:26:40

+0

如果你有一个C#字符串,你可以使用Encoding.UTF8.GetByteCount(string)来获得精确的字节数。如果需要,可以从字符串末尾修剪字符,直到字节数达到极限。 – 2009-08-03 23:54:29

+0

不*很*正确。如果它是一个字节,它确实以'0'开始,但是如果它的高位是'1',它可能是多字节字符的前导字符或“中间”字符(比如说“后面的”)。前导字节以“11”开头,多字节字符中的后续字节以“10”开头。所以如果你的头位是`1`,你是多字节字符,但**不一定是“中间”**。从'pedia':*前导字节有两个或更多高位1,后跟一个0,而连续字节在高位位置都有'10'。* – ruffin 2014-06-28 19:30:23

2

是否有理由需要按字节声明数据库列?这是默认值,但如果数据库字符集是可变宽度的话,它不是特别有用的默认值。我强烈希望用字符来声明列。

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

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

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

12

我想我们可以做得比天真地计算每个加法的字符串的总长度更好。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),这样可以很容易地告诉我什么屏蔽位(约多一点位屏蔽here)。简而言之,我是&将返回字节的前两位中的内容,并将其余0带回。然后我检查XXXX000000,看它是否为1011,在适当的情况下。

我今天发现了C# 6.0 might actually support binary representations,这很酷,但我们现在继续用这个kludge来说明发生了什么。

PadLeft只是因为我太过于OCD输出到控制台。

因此,这里有一个函数可以将您缩减为一个长度为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; 
} 

我最初写道这是一个字符串扩展。当然,只需在string str之前加上this即可将其恢复为扩展格式。我删除了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中的长度为3个字节(尽管当我们使用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. 

所以这是一种的乐趣,而我正处于问题五周年之前。尽管Oren对这些位的描述有一个小错误,那就是恰恰是你想要使用的技巧。感谢您的提问;整齐。

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

以下Oren Trutner's comment这里有两个解决问题的方案:
这里我们计算的字节数根据在字符串的结尾每个字符从字符串的结尾去掉,所以我们不”在每次迭代中评估整个字符串。

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谁想到第二个解决方案

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

短版ruffin's answer。利用the design of 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); 
    }