PowerShell5.Найти / заменить текст.Коммутатор и .NET Framework или командлеты и конвейер?Что быстрее?Что легче читать? - PullRequest
0 голосов
/ 20 февраля 2019

Как найти и заменить текст в текстовом файле Windows с помощью поиска и замены строк, которые легко читаются и которые легко добавлять / изменять / удалять.Этот сценарий проанализирует 6800-строчный файл, найдет 70 экземпляров строк, перенумерует их и перезапишет оригинал менее чем за 400 мс.

Поиск строк "AROUND LINE {1-9999}" и "LINE2{1-9999} "и замените {1-9999} на {номер строки}, в котором включен код.Вокруг струн есть ведущее и замыкающее пространство.Последние два теста выполняются со всей исходной пакетной копией и вставляются в sample.bat.

sample.bat содержит две строки:

ECHO AROUND LINE 5936
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 5937

текущий код включает поиск AROUND LINE и @mklement0 solution.:

copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
$file = "$env:temp\sample.bat"
$lc = 0
$updatedLines = switch -Regex ([IO.File]::ReadAllLines($file)) {
  '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$lc + $Matches[2] }
  default { ++$lc; $_ }
}
[IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)

Ожидаемые результаты:

ECHO AROUND LINE 1
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 2

Фактические результаты:

ECHO AROUND LINE 1 
TITLE %TIME%   DISPLAY TCP-IP SETTINGS   LINE2 2 

Измерение с использованием коммутатора, каркасов .NET и всего вставленного пакетного файлаinto sample.bat:

Measure-command {
copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
    $file = "$env:temp\sample.bat"
    $lc = 0
    $updatedLines = switch -Regex ([IO.File]::ReadAllLines($file)) {
      '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$lc + $Matches[2] }
      default { ++$lc; $_ }
    }
    [IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)}

Результаты: 75 мс-386 мс за десять прогонов.

Измерьте, используя Get-Content + -replace + Set-Content и весь пакетный файл, вставленный в образец.bat:

Measure-command {
copy-item $env:temp\sample.bat -d $env:temp\sample.bat.$((get-date).tostring("HHmmss"))
(gc $env:temp\sample.bat) | foreach -Begin {$lc = 1} -Process {
  $_ -replace 'AROUND LINE \d+', "AROUND LINE $lc" -replace 'LINE2 \d+', "LINE2 $lc"
  ++$lc
} | sc -Encoding Ascii $env:temp\sample.bat}

Результаты: 363ms-451ms за десять запусков.

Строка поиска - это простое для понимания регулярное выражение.

Вы можете искать дополнительные строки, добавив еще один -relace.

-replace 'AROUND LINE \d+', "AROUND LINE $lc" -replace 'LINE2 \d+', "LINE2 $lc" -replace 'LINE3 \d+', "LINE3 $lc"

Примечание редактора : Это следующеевопрос до Выполните итерацию резервного текстового файла ascii, найдите все экземпляры {LINE2 1-9999} и замените на {LINE2 "номер строки, в которой находится код".Перезапись.Быстрее?

Эволюция этого вопроса от младшего к старшему: 1. 54757890 2. 54737787 3. 54712715 4. 54682186

Обновление : я использовал решение регулярных выражений @ mklement0.

Ответы [ 2 ]

0 голосов
/ 20 февраля 2019

Альтернативное решение:

$regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
$lc = 0
$updatedLines = & {foreach ($line in [IO.File]::ReadLines($file)) {
    $lc++
    $m = $regex.Match($line)
    if ($m.Success) {
        $g = $m.Groups
        $g[1].Value + $lc + $g[2].Value
    } else { $line }
}}
[IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)
0 голосов
/ 20 февраля 2019
switch -Regex -File $file {
  '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$lc + $Matches[2] }
  default { ++$lc; $_ }
}
  • Учитывая, что регулярное выражение ^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$ содержит только 2 групп захвата - часть строки перед заменяемым числом (\d+) и часть строки после , вы должны ссылаться на эти группы с индексами 1 и 2 в автоматической $Matches переменной в выходных данных (не 2и 3).

    • Обратите внимание, что (?:...) - это группа без захвата , поэтому по конструкции она не отражена в $Matches.
  • Вместо чтения файла с помощью [IO.File]::ReadAllLines($file), я использую параметр -File с switch, который непосредственно считывает строки из файла $file.

  • ++$lc внутри default { ++$lc; $_ } гарантирует, что счетчик строк также увеличивается для несоответствующих строк перед тем, как проходить линию через ($_).


Замечания по производительности

  • Вы можете немного улучшить производительность с помощью следующей неясной оптимизации :

    # Enclose the switch statement in & { ... } to speed it up slightly.
    $updatedLines = & { switch -Regex -File ... }
    
  • С большим количеством итераций (большое количество строк), с использованием предварительно скомпилированного [regex] экземпляра вместо строкового литерала , который преобразует PowerShellрегулярное выражение за кулисами может еще больше ускорить процесс - см. контрольные показатели ниже.

  • Кроме того, если достаточно совпадения с учетом регистра , вы можете выжать немного больше производительности за счет добавления опции -CaseSensitive в оператор switch.

  • На высоком уровне, , что делает решение быстрым, это использованиеswitch -File для обработки строк и, как правило, использование типов .NET для файлового ввода-вывода (вместо командлетов) (IO.File]::WriteAllLines() в данном случае, как показано навопрос) - см. также этот связанный ответ .

    • Тем не менее, ответ Марша предлагает высокооптимизированный цикл foreach, основанный на предварительно скомпилированном регулярном выражении, котороебыстрее с большим числом итераций - однако, оно более многословно.

Тесты

  • В следующем коде сравнивается эффективность подхода switch этого ответа с подходом foreach Марса.

  • Обратите внимание, что для того, чтобы сделать два решения полностью эквивалентными, были сделаны следующие изменения:

    • В * 1115 была добавлена ​​оптимизация & { ... }* и команда.
    • Опции IgnoreCase и CultureInvariant были добавлены к подходу foreach для соответствия опциям с регулярными выражениями PS неявно use.

Вместо примера файла из 6 строк производительность тестируется с помощью файла из 600 строк, 3000 и 30000 строк соответственно, чтобы показать влияние числа итераций на производительность.

100 прогонов усредняются.

Пример результатов с моего компьютера с Windows 10 под управлением Windows PowerShell v5.1 - абсолютные значения не важны, но, надеюсь, производительность относительно показана в столбце Factorn обычно является репрезентативным:

VERBOSE: Averaging 100 runs with a 600-line file of size 0.03 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.023               # switch -Regex -File with regex string literal...
1.16   0.027               # foreach with precompiled regex and [regex].Match...
1.23   0.028               # switch -Regex -File with precompiled regex...


VERBOSE: Averaging 100 runs with a 3000-line file of size 0.15 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.063               # foreach with precompiled regex and [regex].Match...
1.11   0.070               # switch -Regex -File with precompiled regex...
1.15   0.073               # switch -Regex -File with regex string literal...


VERBOSE: Averaging 100 runs with a 30000-line file of size 1.47 MB...

Factor Secs (100-run avg.) Command
------ ------------------- -------
1.00   0.252               # foreach with precompiled regex and [regex].Match...
1.24   0.313               # switch -Regex -File with precompiled regex...
1.53   0.386               # switch -Regex -File with regex string literal...

Обратите внимание, что при более низкой итерации считается switch -regex со строковым литералом , но примерно при 1500 строках решение foreach с предварительно скомпилированным [regex] экземпляр начинает становиться быстрее;использование предварительно скомпилированного экземпляра [regex] с switch -regex окупается в меньшей степени, только с большим числом итераций.

Код теста , с использованием функции Time-Command :

# Sample file content (6 lines)
$fileContent = @'
TITLE %TIME%   NO "%zmyapps1%\*.*" ARCHIVE ATTRIBUTE   LINE2 1243
TITLE %TIME%   DOC/SET YQJ8   LINE2 1887
SET ztitle=%TIME%: WINFOLD   LINE2 2557
TITLE %TIME%   _*.* IN WINFOLD   LINE2 2597
TITLE %TIME%   %%ZDATE1%% YQJ25   LINE2 3672
TITLE %TIME%   FINISHED. PRESS ANY KEY TO SHUTDOWN ... LINE2 4922

'@

# Determine the full path to a sample file.
# NOTE: Using the *full* path is a *must* when calling .NET methods, because
#       the latter generally don't see the same working dir. as PowerShell.
$file = "$PWD/test.bat"

# Note: input is the number of 6-line blocks to write to the sample file,
#       which amounts to 600 vs. 3,000 vs. 30,0000 lines.
100, 500, 5000 | % { 

  # Create the sample file with the sample content repeated N times.
  $repeatCount = $_ 
  [IO.File]::WriteAllText($file, $fileContent * $repeatCount)

  # Warm up the file cache and count the lines.
  $lineCount = [IO.File]::ReadAllLines($file).Count

  # Define the commands to compare as an array of scriptblocks.
  $commands =
    { # switch -Regex -File with regex string literal
      & { 
        $i = 0
        $updatedLines = switch -Regex -File $file {
          '^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$' { $Matches[1] + ++$i + $Matches[2] }
          default { ++$i; $_ }
        } 
        [IO.File]::WriteAllLines($file, $updatedLines, [text.encoding]::ASCII)
      }
    }, { # switch -Regex -File with precompiled regex
      & {
        $i = 0
        $regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
        $updatedLines = switch -Regex -File $file {
          $regex { $Matches[1] + ++$i + $Matches[2] }
          default { ++$i; $_ }
        } 
        [IO.File]::WriteAllLines($file, $updatedLines, [text.encoding]::ASCII)
      }
    }, { # foreach with precompiled regex and [regex].Match
      & {
        $regex = [Regex]::new('^(.*? (?:AROUND LINE|LINE2) )\d+(.*)$', 'Compiled, IgnoreCase, CultureInvariant')
        $i = 0
        $updatedLines = foreach ($line in [IO.File]::ReadLines($file)) {
            $i++
            $m = $regex.Match($line)
            if ($m.Success) {
                $g = $m.Groups
                $g[1].Value + $i + $g[2].Value
            } else { $line }
        }
        [IO.File]::WriteAllLines($file, $updatedLines, [Text.Encoding]::ASCII)    
      }
    }

  # How many runs to average.
  $runs = 100

  Write-Verbose -vb "Averaging $runs runs with a $lineCount-line file of size $('{0:N2} MB' -f ((Get-Item $file).Length / 1mb))..."

  Time-Command -Count $runs -ScriptBlock $commands | Out-Host

}
...