2009-08-03 36 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); 
    }