Лучший способ сократить строку UTF8 на основе длины байта

В недавнем проекте предусматривается импорт данных в базу данных Oracle. Программа, которая будет делать это, - это приложение С#.Net 3.5, и я использую библиотеку соединений Oracle.DataAccess для обработки фактической вставки.

У меня возникла проблема, когда я получил это сообщение об ошибке при вставке определенного поля:

ORA-12899 Значение слишком велико для столбца X

Я использовал Field.Substring(0, MaxLength);, но все еще получил ошибку (хотя и не для каждой записи).

Наконец, я увидел, что должно было быть очевидно, моя строка была в ANSI, а поле было UTF8. Его длина определяется в байтах, а не в символах.

Это подводит меня к моему вопросу. Каков наилучший способ обрезать мою строку, чтобы исправить MaxLength?

Мой код подстроки работает по длине символа. Есть ли простая функция С#, которая умеет обрезать строку UT8 разумно длиной байта (т.е. Не отрывать половину символа)?

Ответы

Ответ 1

Вот два возможных решения: однострочный 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;
}

Ответ 2

Я думаю, что мы можем сделать лучше, чем наивно подсчитывать общую длину строки с каждым добавлением. LINQ классный, но он может случайно поощрять неэффективный код. Что, если бы я хотел первые 80 000 байтов гигантской строки UTF? Это много ненужного подсчета. "У меня 1 байт. Теперь у меня есть 2. Теперь у меня 13... Теперь у меня 52,384..."

Это глупо. В большинстве случаев, по крайней мере, в l'anglais, мы можем вырезать именно этот байт nth. Даже на другом языке мы находимся на расстоянии менее 6 байт от хорошей точки резки.

Итак, я собираюсь начать с предложения @Oren, который должен отбить ведущий бит значения UTF8 char. Давайте начнем с правки в байте n+1th и используем трюк Орена, чтобы выяснить, нужно ли нам сначала сократить несколько байтов.

Три возможности

Если первый байт после разреза имеет 0 в ведущем бите, я знаю, что я режу точно перед символом одного байта (обычный ASCII) и умею чистить.

Если после вырезания есть 11, следующий байт после разреза - это начало многобайтового символа, так что хорошее место тоже вырезать!

Если у меня есть 10, я знаю, что я в середине многобайтового символа, и мне нужно вернуться, чтобы проверить, где он действительно начинается.

То есть, хотя я хочу вырезать строку после n-го байта, если этот n + 1-й байт входит в середину многобайтового символа, резка создаст недопустимое значение UTF8. Мне нужно выполнить резервное копирование до тех пор, пока не дойду до того, что начнется с 11 и вырезается непосредственно перед ним.

код

Примечания: Я использую такие вещи, как Convert.ToByte("11000000", 2), чтобы было легко определить, какие биты я маскирую (немного больше о маскировке здесь). Вкратце, я & ing, чтобы вернуть то, что в байте первые два бита и вернуть 0 для остальных. Затем я проверю XX на XX000000, чтобы увидеть, если это необходимо 10 или 11, где это необходимо.

Сегодня я узнал, что С# 6.0 может фактически поддерживать двоичные представления, что здорово, но мы будем продолжать использовать этот 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;
}

Я изначально написал это как расширение строки. Просто добавьте 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 имеют длину 3 байта в UTF8 (хотя, когда мы пишем символы в консоли в 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 было небольшой ошибкой, это именно трюк, который вы хотите использовать. Спасибо за вопрос; аккуратно.

Ответ 3

Если байт UTF-8 имеет нулевой бит высокого порядка, это начало символа. Если бит старшего порядка равен 1, он находится в "середине" символа. Возможность обнаружения начала символа была явной целью проектирования UTF-8.

Подробнее об этом читайте в разделе описания статьи в википедии.

Ответ 4

Есть ли причина, по которой вам нужно, чтобы столбец базы данных указывался в байтах? Это значение по умолчанию, но это не особенно полезно по умолчанию, если набор символов базы данных является переменной шириной. Я бы предпочел объявить столбец в терминах символов.

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

Это создаст таблицу, в которой COL1 будет хранить 10 байтов данных, а col2 будет хранить 10 символов. Семантика длины символов имеет больше смысла в базе данных UTF8.

Предполагая, что вы хотите, чтобы все созданные вами таблицы использовали семантику длины символов по умолчанию, вы можете установить параметр инициализации NLS_LENGTH_SEMANTICS на CHAR. В этот момент все созданные вами таблицы по умолчанию будут использовать семантику длины символов, а не семантику длины байта, если вы не укажете w981 > или BYTE в поле.

Ответ 5

Следуя комментарий Орена Трутнера, вот еще два решения проблемы:
здесь мы подсчитываем количество байтов для удаления из конца строки в соответствии с каждым символом в конце строки, поэтому мы не оцениваем всю строку на каждой итерации.

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, который подумал о втором решении

Ответ 6

Это другое решение, основанное на двоичном поиске:

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

Ответ 7

Более короткая версия ответа ruffin. Использует дизайн 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);
    }

Ответ 8

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