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

时间:2009-08-03 23:05:15

标签: 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字符串(即不会破坏半个字符)?

9 个答案:

答案 0 :(得分:13)

以下是两种可能的解决方案 - 从左到右处理输入的LINQ单线程和从右到左处理输入的传统for循环。哪个处理方向更快取决于字符串长度,允许的字节长度以及多字节字符的数量和分布,很难给出一般性建议。 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;
}

答案 1 :(得分:13)

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

那是愚蠢的。大多数时候,至少在英语中,我们可以在nth字节上完全 。即使是另一种语言,我们距离一个好的切割点还不到6个字节。

所以我要从@Oren的建议开始,这是关键的UTF8 char值的前导位。让我们从n+1th字节右侧开始,并使用Oren的技巧来确定我们是否需要提前减少几个字节。

三种可能性

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

如果切割后有11,则切割后的下一个字节是多字节字符的 start ,因此这也是切割的好地方!

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

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

<强>代码

注意:我使用Convert.ToByte("11000000", 2)这样的内容,以便轻松分辨我屏蔽的内容(稍微更多一点掩码here)。简而言之,我&返回字节的前两位中的内容,然后返回0秒。然后,我会XX检查XX000000 10,看看它是11还是PadLeft

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

n只是因为我对OCD输出到控制台过度。

因此,这是一项功能,可以将您切换为长n个字节的字符串或者小于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; } 的最大数字。用&#34;完成&#34; UTF8字符。

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

输出如下。请注意&#34;智能引号&#34;在?中,UTF8中的字节长度为三个字节(但是当我们用ASCII将字符写入控制台时,它会输出哑引号)。另请注意输出中每个智能引号的第二个和第三个字节的testValue输出。

我们的8的前五个字符是UTF8中的单个字节,因此0-5个字节的值应该是0-5个字符。然后我们有一个三字节的智能引用,它不能完整地包含在5 + 3字节之内。果然,我们看到 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. 的呼叫突然爆发。我们的下一个智能引号弹出8 + 3 = 11,然后我们回到单字节字符到14。

{{1}}

这样有趣,我就在问题五周年之前就已经开始了。虽然Oren对这些位的描述有一个小错误,但正是你想要使用的技巧。谢谢你的提问;整齐。

答案 2 :(得分:4)

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

查看wikipedia article的“说明”部分,了解更多详情。

答案 3 :(得分:3)

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

答案 4 :(得分:2)

是否有理由要求以字节为单位声明数据库列?这是默认值,但如果数据库字符集是可变宽度,则它不是特别有用的默认值。我非常希望用字符来声明列。

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

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

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

答案 5 :(得分: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 <= 朣楢琴执执 瑩浻牡楧硰执执獧浻牡楧敬瑦 瀰 絸朣杢执獧扻捡杫潲湵 潣潬昣昸昸慢正 

更高效(可维护)的解决方案: 根据所需长度从bytes数组中获取字符串并剪切最后一个字符,因为它可能已损坏

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

答案 6 :(得分:1)

这是另一种基于二分法搜索的解决方案:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<section id="home">
  <div class="container">
    <div class="row">
      <div class="col-lg-12">
        <h1>Heading sample</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <button type="button" name="button" class="btn btn-primary">Click me</button>
      </div>
    </div>
  </div>
	<div class="hat"></div>
</section>

<section id="about">
  <div class="container">
    <div class="row">
      <div class="col-lg-12">
        <h1>Heading sample</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Perferendis repudiandae illum ipsum voluptatibus eos magni est rem, quae recusandae. Sequi, quae voluptatum alias adipisci, quas repudiandae eveniet et quis placeat.</p>
      </div>
    </div>
  </div>
</section>

答案 7 :(得分:0)

所有其他答案似乎都错过了Encoder类中此功能已内置于.NET中的事实。对于奖励积分,此方法也适用于其他编码。

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

答案 8 :(得分:-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;
    }