Как я могу передать строку в массив подстрок, чтобы ни одна подстрока при кодировании в Uft8 с конечным нулем не имела длину байта> n?
n всегда >= 5, поэтому подойдет 4-байтовый закодированный символ + ноль.
Единственные способы, о которых я могу думать на данный момент, это:
Есть ли способ лучше?
Выходной массив также может быть массивом байтов utf8, а не строками, если это проще.
Я уже знаю о кодовых точках, кодировках, суррогатах и всем таком. Это конкретно о длине байта кодировки utf8.
Можете ли вы привести пример того, что вы пробовали?
Примечание:
Следующее:
перебирает входную строку символ за символом или суррогатную пару суррогатную пару
U+FFFF
( глифы в так называемой BMP (базовой многоязычной плоскости) и требуют (суррогатной) пары кодовых единиц для кодирования символов Unicode за пределами этой плоскости, т. как U+FFFF
.извлекает подстроки на основе их длины последовательности байтов UTF-8, выведенной из кодовой точки Unicode каждого отдельного символа / независимо от того, образуют ли символ и последующий символ суррогатную пару (что подразумевает кодовую точку больше, чем 👍
, что, в свою очередь, подразумевает U+FFFF
байты) , на основе этой таблицы Википедии.
Tip of the hat to mclayton for pointing out that keeping track of indices in the original string combined with 4
calls is sufficient to extract chunks - no need for a string builder.
использует экземпляр System.Collections.Generic.List`1 для сбора всех фрагментов в списке.
# Sample string
$str = '1234abc€défg👍'
$maxChunkLen = 4 # the max. *byte* count, excluding the trailing NUL
$chunks = [System.Collections.Generic.List[string]]::new()
$chunkByteCount = 0
$chunkStartNdx = 0
for ($i = 0; $i -lt $str.Length; ++$i) {
$codePoint = [int] $str[$i]
$isSurrogatePair = [char]::IsSurrogatePair($str, $i)
# Note: A surrogate pair encoded as UTF-8 is a *single* non-BMP
# character encoded in 4 bytes, not 2 3-byte *surrogates*.
$thisByteCount =
if ($isSurrogatePair) { 4 }
elseif ($codePoint -ge 0x800) { 3 }
elseif ($codePoint -ge 0x80) { 2 }
else { 1 }
if ($chunkByteCount + $thisbyteCount -gt $maxChunkLen) {
# Including this char. / surrogate pair would make the chunk too long.
# Add the current chunk plus a trailing NUL to the list...
$chunks.Add($str.Substring($chunkStartNdx, ($i - $chunkStartNdx)) + "`0")
# ... and start a new chunk with this char. / surrogate pair.
$chunkStartNdx = $i
$chunkByteCount = $thisByteCount
}
else {
# Still fits into the current chunk.
$chunkByteCount += $thisByteCount
}
if ($isSurrogatePair) { ++$i }
}
# Add a final chunk to the list, if present.
if ($chunkStartNdx -lt $str.Length) { $chunks.Add($str.Substring($chunkStartNdx) + "`0") }
# Output the resulting chunks
$chunks
Вывод (аннотированный):
1234 # 1 + 1 + 1 + 1 bytes (+ NUL)
abc # 1 + 1 + 1 bytes (+ NUL)
€d # 3 + 1 bytes (+ NUL)
éfg # 2 + 1 + 1 bytes (+ NUL)
👍 # 4 bytes (+ NUL)
Обратите внимание, что все фрагменты, кроме первого, имеют менее 4 символов (предел количества байтов, за исключением NUL), из-за содержания многобайтовых символов в формате UTF-8 и/или из-за того, что следующий символ является таким символом. и, следовательно, не вписывается в текущий фрагмент.
Итак, во-первых, все, что я знаю о процессе двоичного кодирования для UTF-8, я только что узнал из UTF-8 страницы Википедии , так что будьте осторожны :-). Тем не менее, приведенный ниже код, похоже, дает правильный результат для набора тестовых данных по сравнению с ответом @mklement0 (в котором я не сомневаюсь), так что, возможно, в нем есть какой-то пробег...
Рад слышать, если в нем есть какие-то кричалки - я думаю, что в принципе это должно работать, даже если реализация ниже где-то неверна :-).
В любом случае, основной функцией является приведенная ниже функция, которая возвращает позиции фрагментов в массиве байтов в кодировке utf-8. Я подумал, что как только вы узнаете позиции, вы, возможно, захотите использовать байты на месте (например, с Stream.Write(byte[] buffer, int offset, int count)
или подобным), поэтому я избегал извлечения копии фрагментов в новый список, но довольно легко использовать вывод для сделайте это, если требуется (см. далее).
# extracts the *positions* of chunks of bytes in a ut8 byte array
# such that no multi-byte codepoints are split across chunks, and
# all chunks are a maximum of $MaxLen bytes
#
# note - assumes $Utf8Bytes is a *valid* utf8 byte array, so may
# need error handling if you expect invalid data to be passed in.
function Get-UTF8ChunkPositions
{
param( [byte[]] $Utf8Bytes, [int] $MaxLen )
# from https://en.wikipedia.org/wiki/UTF-8
#
# Code point ↔ UTF-8 conversion
# -----------------------------
# First code point Last code point Byte 1 Byte 2 Byte 3 Byte 4 Code points
# U+0000 U+007F 0xxxxxxx 128
# U+0080 U+07FF 110xxxxx 10xxxxxx 1920
# U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx 61440
# U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 1048576
# stores the start position of each chunk
$startPositions = [System.Collections.Generic.List[int]]::new();
$i = 0;
while( $i -lt $Utf8Bytes.Length )
{
# remember the start position for the current chunk
$startPositions.Add($i);
# jump past the end of the current chunk, optimistically assuming we won't land in
# the middle of a multi-byte codepoint (but we'll deal with that in a minute)
$i += $MaxLen;
# if we've gone past the end of the array then we're done as there's no more
# chunks after the current one, so there's no more start positions to record
if ( $i -ge $Utf8Bytes.Length )
{
break;
}
# if we're in the middle of a multi-byte codepoint, backtrack until we're not.
# we're then at the start of the *next* chunk, and the chunk length is definitely
# smaller then $MaxLen
#
# 0xC0 = [Convert]::ToInt32("11000000", 2);
# 0x80 = [Convert]::ToInt32("10000000", 2);
while( ($Utf8Bytes[$i] -band 0xC0) -eq 0x80 )
{
$i -= 1;
}
}
# we know all the start positions now, so turn them into ranges.
# (we'll add a dummy item to help build the last range)
$startPositions.Add($utf8Bytes.Length);
for( $i = 1; $i -lt $startPositions.Count; $i++ )
{
[PSCustomObject] @{
"Start" = $startPositions[$i-1];
"Length" = $startPositions[$i] - $startPositions[$i-1];
};
}
}
Эта функция использует тот факт, что действительный поток байтов utf8 является самосинхронизирующимся, что на практике означает, что нам не нужно проходить через каждый байт в потоке — мы можем прыгать куда угодно и находить начало закодированной кодовой точки путем нахождения ближайшего байта, который начинается 00xxxxxx
, 01xxxxxx
или 11xxxxxx
(или, что то же самое, не начинается 10xxxxxx
), и это начало следующего фрагмента.
Например, если мы проскочили n
байт от начала текущего фрагмента и нашли байт, начинающийся с 10xxxxxx
:
n-2 n-1 n n+1
... 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ...
^^^^^^^^
затем мы возвращаемся к n-2
как к началу следующего фрагмента (поэтому конец текущего фрагмента логически находится на один байт раньше, чем в n-3
):
n-2 n-1 n n+1
... 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ...
^^^^^^^^
Пример:
# set up some test data
$str = "1234abc€défg👍abüb";
$utf8 = [System.Text.Encoding]::UTF8.GetBytes($str);
"$utf8"
# 49 50 51 52 97 98 99 226 130 172 100 195 169 102 103 240 159 145 141 97 98 195 188 98
$maxLen = 5 - 1; # exclude NUL terminator
$positions = Get-UTF8ChunkPositions -Utf8Bytes $utf8 -MaxLen $maxLen;
$positions
# Start Length
# ----- ------
# 0 4
# 4 3
# 7 4
# 11 4
# 15 4
# 19 4
# 23 1
Если вам действительно нужны отдельные массивы байтов с нулевым терминатором, вы можете преобразовать позиции в фрагменты следующим образом:
$chunks = $positions | foreach-object {
$chunk = new-object byte[] ($_.Length + 1); # allow room for NUL terminator
[Array]::Copy($utf8, $_.Start, $chunk, 0, $_.Length);
$chunk[$chunk.Length - 1] = 0; # utf-8 encoded NUL is 0
@(, $chunk); # send the chunk to the pipeline
};
После того, как у вас есть отдельные фрагменты с кодировкой utf-8 с нулевым завершением, вы можете вернуть их обратно к строкам с нулевым завершением, подобным этому, если они вам действительно нужны:
$strings = $chunks | foreach-object { [System.Text.Encoding]::UTF8.GetString($_) }
$strings
С точки зрения производительности, это, кажется, достаточно хорошо масштабируется, даже если вам нужно специально кодировать строку в массив байтов utf8 только для ее вызова. Он также работает намного быстрее с большими числами для $MaxLen
, поскольку он может прыгать на большие расстояния для каждого фрагмента...
Грубый тест на моей машине:
$sample = "1234abc€défg👍abüb" * 1000000;
$sample / 1mb;
# 17
Measure-Command {
$utf8 = [System.Text.Encoding]::UTF8.GetBytes($sample);
$maxlen = 1024;
$positions = Get-UTF8ChunkPositions $utf8 $maxlen;
$chunks = $positions | foreach-object {
$chunk = [byte[]]::new($_.Length + 1); # allow room for NUL terminator
[Array]::Copy($utf8, $_.Start, $chunk, 0, $_.Length);
$chunk[$chunk.Length - 1] = 0; # utf-8 encoded NUL is 0
@(, $chunk); # send the chunk to the pipeline
};
};
# TotalMilliseconds : 7986.131
Хотя, если вас больше всего беспокоит производительность, возможно, PowerShell вам не подходит :-)...
Хорошо сделано. Производительность будет зависеть от символов, из которых состоит входная строка: лучше всего она будет работать со всеми символами диапазона ASCII, а хуже всего — со всеми символами, отличными от BMP.
@mklement0 - Верно. Я обнаружил, что самым большим фактором, определяющим производительность, был размер фрагментов — функция должна зацикливаться примерно byte array length / chunk length
раз, поэтому, если длина фрагмента велика, почти ничего не нужно делать :-).
Я полагаю, что другой способ сделать это - просто прыгать на (n-1)/4 символа за раз.
Пробовал прыгать по (n-1)/4 символам за раз: '𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹' -split "(.{7})" -ne '' | ForEach {$_}
, но -split
успешно разделяет суррогатные пары :( Есть ли способ разделить по границам, или нам все равно придется настраивать вручную?
@Adamarla - я думаю, что самое близкое, что вы получите для этого, - это ответ @mklement0 - все, что нативное, в любом случае должно будет делать почти то же самое внутри. Проблема в том, что ваши ограничения определены в двух пространствах имен, т. е. не разделяют закодированные кодовые точки Unicode и не превышают максимальную длину кодовой единицы. Строки Dotnet изначально закодированы в UTF-16, поэтому одна или две (для суррогатных пар) единиц кода на кодовую точку, а UTF-8 — это 1-4 единицы кода на кодовую точку, что означает, что вы не можете просто выполнять базовые математические операции со строкой или потоком байтов. длины - вам нужно как-то разобрать данные...
Поскольку UTF-8 хранит данные переменной длины в старших битах, двоичный код говорит наверняка.