2009-08-03 2 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. Я включил интро на всякий случай, если кто-то из Google взорвет мое сообщение об ошибке оракула в будущем. Надеюсь, это сэкономит их некоторое время. –

ответ

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. Это изящное решение! –

+0

+1 как оба решения – Feryt

4

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

Для более подробной информации ознакомьтесь с разделом описания wikipedia article.

+0

Спасибо за подсказку. Можете ли вы показать мне пример C#? Означает ли это, что для решения этой проблемы нет встроенных функций? Это похоже на общую проблему. –

+0

Если у вас есть строка C#, вы можете использовать Encoding.UTF8.GetByteCount (string), чтобы получить точное количество байтов. Вы можете обрезать символы с конца строки, если вам нужно, до тех пор, пока количество байтов не достигнет предела. –

+0

Не * совершенно * правый. Если это один байт, он начинается с '0', но если его старший бит равен« 1 », это может быть старший или« средний »(скажем,« следующий ») байт многобайтового символа.Ведущий байт начинается с '11' и следующих байтов в многобайтовом символе начинается с' 10'. Поэтому, если ваш ведущий бит равен «1», вы используете многобайтовый символ, но ** не обязательно «средний» **. Из 'pedia: * Ведущий байт имеет два или более высоких 1-го уровня, за которыми следует 0, а в байтах продолжения все имеют «10» в позиции высокого порядка. * – ruffin

2

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

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

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

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

+0

Отличная идея. Я не контролирую Oracle DB, но я предлагаю это. Возможно, это был просто недосмотр. Это было бы лучшим решением моей проблемы, если они захотят внести изменения. –

12

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

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

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

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

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

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

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

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

Код

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

Я узнал сегодня, что 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; 
} 

Первоначально я написал это как расширение строки. Просто добавьте обратно 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, он выводит немые кавычки). Также обратите внимание на вывод ? s для второго и третьего байтов каждой интеллектуальной цитаты на выходе.

Первые пять символов нашего testValue являются одиночными байтами в UTF8, поэтому значения 0-5 байтов должны быть 0-5 символов. Тогда у нас есть трехбайтная интеллектуальная цитата, которая не может быть включена полностью до 5 + 3 байта. Конечно же, мы видим, что выскочить на призыв к 8 .our следующий смарт цитата выскакивает на 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. 

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

+2

Удивительно, вы сделали это в O (N)! Спасибо, это отлично подходит для длинных строк. –

+0

Вы можете избежать окончания последнего байта [] с помощью 'Encoding.UTF8.GetString (byteArray, 0, bytePointer)'. –

+0

@NealEhardt А, молодец. Хорошее предложение; благодаря! – ruffin

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

это может испортить последний символ и на самом деле не работает, так как перекодированный массив байтов снова опустится до предела – firda

1

После Oren Trutner's comment вот еще два решения этой проблемы:
здесь мы посчитаем количество байт, чтобы удалить из конца строки в зависимости от каждого символа в конце строки, так что мы не» t оценивать всю строку на каждой итерации.

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); 
    } 
Смежные вопросы