18

Oracle データベースにデータをインポートするために呼び出された最近のプロジェクト。これを行うプログラムは C# .Net 3.5 アプリであり、実際の挿入を処理するために Oracle.DataAccess 接続ライブラリを使用しています。

特定のフィールドを挿入すると、次のエラー メッセージが表示されるという問題が発生しました。

ORA-12899 列 X の値が大きすぎます

使用Field.Substring(0, MaxLength);しましたが、まだエラーが発生します(ただし、すべてのレコードではありません)。

最後に、明らかなはずだったことがわかりました。文字列は ANSI で、フィールドは UTF8 でした。その長さは、文字数ではなくバイト数で定義されます。

これで私の質問にたどり着きます。MaxLength を修正するために文字列をトリミングする最良の方法は何ですか?

私の部分文字列コードは、文字の長さで機能します。UT8 文字列をバイト長でインテリジェントにトリミングできる (つまり、文字の半分をハックしない) シンプルな C# 関数はありますか?

4

9 に答える 9

19

加算ごとに文字列の合計の長さを単純にカウントするよりも、うまくいくと思います。LINQ は優れていますが、効率の悪いコードを誤って助長する可能性があります。巨大な UTF 文字列の最初の 80,000 バイトが必要な場合はどうすればよいでしょうか? それは多くの不必要なカウントです。「私は 1 バイトを持っています。今は 2 を持っています。今は 13 を持っています...今は 52,384 を持っています...」

それはばかげている。ほとんどの場合、少なくとも言語では、そのバイトを正確にカットできます。nth別の言語でも、適切な切断点から 6 バイト未満離れています。

そこで、@Oren の提案から始めます。これは、UTF8 char 値の先頭ビットをキーオフすることです。バイトを右にカットすることから始めましょうn+1th。Oren のトリックを使用して、数バイト前にカットする必要があるかどうかを判断します。

3つの可能性

カット後の最初のバイト0の先頭ビットに a がある場合、1 バイト (従来の ASCII) 文字の前で正確にカットしていることがわかるので、きれいにカットできます。

カットに続くバイトがある場合11、カットの後の次のバイトはマルチバイト文字の開始であるため、ここもカットするのに適しています!

ただし、がある場合10は、マルチバイト文字の途中にいることがわかっているため、実際にどこから始まるかを確認するために戻って確認する必要があります。

つまり、n バイト目以降の文字列を切り取りたいのですが、その n+1 バイト目がマルチバイト文字の途中にある場合、切り取りによって無効な UTF8 値が作成されます。11で始まり、その直前でカットされるものに到達するまで、バックアップする必要があります。

コード

注:Convert.ToByte("11000000", 2)マスクしているビットを簡単に判別できるように、次のようなものを使用しています (ビット マスキングについては、ここでもう少し詳しく説明します)。簡単に言えば、バイトの最初の 2 ビットの内容を返し、残りの s を&返すということです。0次に、必要に応じてXXfromをチェックして、またはであるXX000000かどうかを確認します。1011

今日C# 6.0 がバイナリ表現を実際にサポートしている可能性があることを知りました。

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

最初はこれを文字列拡張として書きました。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 では 3 バイト長であることに注意してください (ただし、文字を ASCII でコンソールに書き込むと、ダム クォートが出力されます)。?出力内の各スマート クォートの 2 番目と 3 番目のバイトの s 出力に も注意してください。

私たちの最初の 5 文字はtestValueUTF8 のシングル バイトであるため、0 ~ 5 バイトの値は 0 ~ 5 文字である必要があります。次に、3 バイトのスマート クォートがあり、5 + 3 バイトになるまで全体を含めることはできません。案の定、8次のスマート クォートは 8 + 3 = 11 で表示され、14 までは 1 バイト文字に戻ります。

 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.

それはちょっと楽しいです。私は質問の 5 周年記念の直前にいます。オレンのビットの説明には小さな誤りがありましたが、それはまさにあなたが使いたいトリックです. ご質問ありがとうございます。きちんとした。

于 2014-06-28T20:31:07.817 に答える
19

入力を左から右に処理する LINQ ワンライナーとfor、入力を右から左に処理する従来のループの 2 つの解決策があります。どちらの処理方向が高速かは、文字列の長さ、許容されるバイト長、およびマルチバイト文字の数と分布に依存し、一般的な提案は困難です。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 に答える
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

これは、二分探索に基づく別のソリューションです。

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 に答える