Powershell: строка перенесена в массив подстрок, так что ни одна из них при кодировании в Uft8 не имеет длины байта > n

Как я могу передать строку в массив подстрок, чтобы ни одна подстрока при кодировании в Uft8 с конечным нулем не имела длину байта> n?

n всегда >= 5, поэтому подойдет 4-байтовый закодированный символ + ноль.

Единственные способы, о которых я могу думать на данный момент, это:

  • Разделите, закодируйте, проверьте длину и, если слишком большая, повторите с меньшим разделением.
  • Кодируйте, проходите, маскируйте и подсчитывайте байты, отслеживая границы, и разделяйте вручную

Есть ли способ лучше?

Выходной массив также может быть массивом байтов utf8, а не строками, если это проще.

Я уже знаю о кодовых точках, кодировках, суррогатах и ​​всем таком. Это конкретно о длине байта кодировки utf8.

Поскольку UTF-8 хранит данные переменной длины в старших битах, двоичный код говорит наверняка.

vonPryz 31.03.2023 14:50

Можете ли вы привести пример того, что вы пробовали?

js2010 31.03.2023 15:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
159
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Примечание:

  • Этот ответ создает список (.NET) подстрок входной строки путем разделения входной строки на подстроки, чье представление байтов UTF-8 не превышает заданного количества байтов.
  • Чтобы вместо этого создать список этих представлений байтов UTF-8, то есть массивов байтов, см. полезный ответ mclayton.

Следующее:

  • перебирает входную строку символ за символом или суррогатную пару суррогатную пару

    • Обратите внимание, что экземпляры .NET [char] , из которых состоят экземпляры .NET [string], представляют собой беззнаковые 16-битные единицы кода Unicode, поэтому они могут напрямую кодировать только глифы Unicode с кодовыми точками до 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 02.04.2023 00:26

@mklement0 - Верно. Я обнаружил, что самым большим фактором, определяющим производительность, был размер фрагментов — функция должна зацикливаться примерно byte array length / chunk length раз, поэтому, если длина фрагмента велика, почти ничего не нужно делать :-).

mclayton 02.04.2023 00:29

Я полагаю, что другой способ сделать это - просто прыгать на (n-1)/4 символа за раз.

Adamarla 04.04.2023 01:01

Пробовал прыгать по (n-1)/4 символам за раз: '𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹𒇹' -split "(.{7})" -ne '' | ForEach {$_}, но -split успешно разделяет суррогатные пары :( Есть ли способ разделить по границам, или нам все равно придется настраивать вручную?

Adamarla 04.04.2023 01:28

@Adamarla - я думаю, что самое близкое, что вы получите для этого, - это ответ @mklement0 - все, что нативное, в любом случае должно будет делать почти то же самое внутри. Проблема в том, что ваши ограничения определены в двух пространствах имен, т. е. не разделяют закодированные кодовые точки Unicode и не превышают максимальную длину кодовой единицы. Строки Dotnet изначально закодированы в UTF-16, поэтому одна или две (для суррогатных пар) единиц кода на кодовую точку, а UTF-8 — это 1-4 единицы кода на кодовую точку, что означает, что вы не можете просто выполнять базовые математические операции со строкой или потоком байтов. длины - вам нужно как-то разобрать данные...

mclayton 04.04.2023 10:37

Другие вопросы по теме