Учитывая неизвестную строку неизвестного размера, например. выражение ScriptBlock или что-то вроде:
$Text = @'
LOREM IPSUM
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
'@
Я хотел бы свести строку к одной строке (заменить все последовательные пробелы на один пробел) и усечь ее до определенного $Length
:
$Length = 32
$Text = $Text -Replace '\s+', ' '
if ($Text.Length -gt $Length) { $Text = $Text.SubString(0, $Length) }
$Text
LOREM IPSUM Lorem Ipsum is simpl
Проблема в том, что если речь идет о большой строке, она не очень эффективна для замены пробелов: она заменяет все пробелы во всей строке $Text
, где нужно заменить только первые несколько пробелов, пока у меня не будет строки необходимый размер ($Length = 32
).
Менять местами операции -replace
и SubString
нежелательно, так как это вернет меньшую длину, чем требуется, или даже один пробел для любой строки $Text
, которая начинается примерно с 32 пробелов.
Вопрос:
Как я могу эффективно объединить две операции (-replace
и SubString
), чтобы не заменять больше пробелов, чем необходимо, и получать строку необходимой длины (в случае, если строка $Text
больше требуемой длины)?
Обновлять
Думаю, я близок к этому, используя делегат MatchEvaluator:
$Length = 8
$TotalSpaces = 0
$Delegate = {
if ($Args[0].Index - $TotalSpaces -gt $Length) {
'{break}'
([Ref]$TotalSpaces).Value = [int]::MaxValue
}
else { ([Ref]$TotalSpaces).Value += $Args[0].Value.Length }
}
[regex]::Replace('test 0 1 2 3 4 5 6 7 8 9', '\s+', $Delegate)
test01234{break}56789
Теперь вопрос в том, как я могу прервать обработку регулярных выражений на {break}
?
Обратите внимание, что из соображений производительности я действительно хочу вырваться, а не заменять <regular-expression>
найденным совпадением (из-за этого создается впечатление, что оно остановилось).
Или даже перегрузка Match(String, Int32, Int32)
- Learn.microsoft.com/en-us/dotnet/api/…, которая делает то же самое, но останавливается после считывания заданного количества входных символов, поэтому вы не обрабатываете всю строку, если больше нет пробелов...
@mclayton, спасибо за подсказку. Я мог бы даже использовать [Regex]::Replace($String, $String, $Length), который, по крайней мере, остановится после $Length
замены.
Нп. Обратите внимание, что это все равно может привести к дорогостоящему копированию строки в возвращаемое значение для оставшейся части строки, которую он оставляет «незамещенной». От того, какая производительность вам нужна, зависит, стоит ли накатывать что-то посложнее или нет.
вы не можете выйти из средства оценки соответствия регулярных выражений, лучшее, что вы можете сделать, очень похоже на то, что вы сделали бы в ForEach-Object
, if ($imDoneReplacingCondition) { return }
Я попробовал решение, в котором используется Match(String, Int32, Int32)
, и производительность странно низкая, если входная строка длинная - как будто оно что-то делает для сканирования всей строки, независимо от того, насколько коротко окно поиска. Например, попробуйте $regex = [regex] "\s+"; $text = "LOREM IPSUM" * 2000; $regex.Match($text, 0, 100)
, а затем измените * 2000
на * 20000000
. Ручной анализ @SantiagoSquarzon работает намного лучше на очень длинных строках...
Вы не можете.
Ваш лучший способ повысить эффективность (и я не уверен, насколько сильно) — это сначала сократить исходную строку до подстроки, потому что вы уже знаете, что в любом случае собираетесь уменьшить ее размер, поэтому нет смысла разрабатывать файл размером 10 МБ, если вы только в конечном итоге понадобятся первые 100 КБ.
Что-то вроде ($Text.Substring(0, $Length * 2) -replace '\ +', ' ').Substring(0, $Lenght)
Я использовал $Length * 2
, но вы можете использовать любое измерение, которое захотите, в зависимости от того, сколько пробелов вы реально ожидаете в исходной (под) строке.
Я думаю, что-нибудь от $Length * 1.25
и выше должно быть достаточно
Возможно, более ручной подход быстрее, чем попытка сделать это с помощью регулярного выражения, конечно, это намного больше кода.
$Text = @'
LOREM IPSUM
Lorem Ipsum is
simply dummy text
'@
$Length = 32
$sb = [System.Text.StringBuilder]::new($Length)
foreach ($char in $Text.GetEnumerator()) {
if ($sb.Length -eq $Length) {
break
}
if ([char]::IsWhiteSpace($char)) {
if (-not $prevSpace) {
$sb = $sb.Append(' ')
}
$prevSpace = $true
continue
}
$sb = $sb.Append($char)
$prevSpace = $false
}
$sb.ToString()
Очень похожий подход с использованием String.Create , возможно, даже быстрее, но потребуется предварительная компиляция или Add-Type
ее. Пример вы можете найти здесь.
Хороший. В качестве настройки производительности вы можете отслеживать позицию первого символа без пробелов в запуске, а затем использовать substring
для копирования всего цикла, а не посимвольно в каждой итерации - хотя это колебания и обходные пути, поскольку это делает код немного более сложным. сложно, но если $Length
большое, возможно, оно того стоит...
Кроме того, в классе пробелов - \s
- есть несколько дополнительных символов - Learn.microsoft.com/en-us/dotnet/standard/base-types/…, если это имеет значение для ОП...
@mclayton не уверен, что использование подстроки повысит производительность, поскольку он уже обрабатывает символ за символом, а добавление к построителю строк уже очень эффективно. substring придется еще раз перебрать исходную строку, когда это уже происходит. что касается остальных символов пробелов, то есть табуляции, я не уверен, как iRon хочет с ними справиться. будем ждать его отзыва.
Отлично, зашёл сюда, чтобы предложить то же самое ^_^ Кстати, вы можете упростить логику ветвления с помощью одного блока elseif ([char]::IsWhiteSpace()){ if (!$prevSpace){$sb.Append(' ')} $prevSpace = $true }
посередине.
это намного лучше @MathiasR.Jessen! Спасибо
@mclayton, Сантьяго, спасибо за ваши предложения и помощь, я поигрался с использованием [Regex]::match('\s+', $Index)
и добавлением подстрок, но это не быстрее, к тому же результаты очень нестабильны. Я поместил эталон в отдельный ответ (вы можете использовать его в своем, тогда я удалю свой ответ). Точка безубыточности при общем подходе, как в вопросе, составляет размер текста в 3000
символов. Это означает, что я, вероятно, реализую гибридное решение, то есть: if ($Text.Length -gt 3000) { <# use your solution #> }
@iRon Я также сделал несколько скамеек, чтобы посмотреть, насколько хорошо string.Create
работает (спойлер, чертовски хорошо). подход с использованием регулярных выражений, безусловно, лучше подходит для небольших строк. добавил скамейки в ту же суть: gist.github.com/santisq/…
$Length = 32
$Sizes = 50, 100, 200, 400, 800, 1600 # words
$Strings = @(
foreach ($Size in $Sizes) {
-Join @('Word ') * $Size
}
)
$Iterations = 1000
@(
$Results = [Ordered]@{ Name = 'Question' }
for ($i = 1; $i -le $Iterations; $i++) {
foreach ($String in $Strings) {
$Results["$($String.Length)"] += (Measure-Command {
$Text = $String -Replace '\s+', ' '
if ($Text.Length -gt $Length) { $Text = $Text.SubString(0, $Length) }
$Void = $Text
}).TotalMilliseconds
}
}
$Void.Length | Should -be $Length
[PSCustomObject]$Results
$Results = [Ordered]@{ Name = 'Santiago' }
for ($i = 1; $i -le $Iterations; $i++) {
foreach ($String in $Strings) {
$Results["$($String.Length)"] += (Measure-Command {
$sb = [System.Text.StringBuilder]::new($Length)
foreach ($char in $Text.GetEnumerator()) {
if ($sb.Length -eq $Length) {
break
}
if ([char]::IsWhiteSpace($char)) {
if (-not $prevSpace) {
$sb = $sb.Append(' ')
}
$prevSpace = $true
continue
}
$sb = $sb.Append($char)
$prevSpace = $false
}
$Void = $sb.ToString()
}).TotalMilliseconds
}
}
$Void.Length | Should -be $Length
[PSCustomObject]$Results
) | Format-Table
Name 250 500 1000 2000 4000 8000
---- --- --- ---- ---- ---- ----
Question 28.15 12.18 20.19 35.81 68.50 129.03
Santiago 79.05 57.73 54.25 55.33 54.73 54.12
Сейчас у меня нет надлежащего доступа к компьютеру, но если вы обрабатываете строку произвольной длины, возможно, вам захочется взглянуть на перегрузку
Match(String, Int32)
классаRegex
— см. Learn.microsoft.com/en-us /dotnet/api/… — и просто вытяните все до первого совпадения пробелов в начале строки, затем вызовите его снова, начиная с текущего совпадения, пока не получите желаемую длину вывода...