Лучший способ сократить строку 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;
}