Эффективность замены нескольких строк PowerShell

Я пытаюсь заменить 600 различных строк в очень большом текстовом файле 30Mb+. Я создаю сценарий, который делает это; следуя этому вопрос:

сценарий:

$string = gc $filePath 
$string | % {
    $_ -replace 'something0','somethingelse0' `
       -replace 'something1','somethingelse1' `
       -replace 'something2','somethingelse2' `
       -replace 'something3','somethingelse3' `
       -replace 'something4','somethingelse4' `
       -replace 'something5','somethingelse5' `
       ...
       (600 More Lines...)
       ...
}
$string | ac "C:log.txt"

но так как это будет проверять каждую строку 600 раз, и в текстовом файле есть более 150 000 строк, это означает, что есть много времени обработки.

есть ли лучшая альтернатива этому, которая более эффективна?

любой совет будет оценили, спасибо.

4 ответов


Итак, вы говорите, что хотите заменить любую из 600 строк в каждой из 150 000 строк, и вы хотите запустить одну операцию замены в строке?

Да, есть способ сделать это, но не в PowerShell, по крайней мере, я не могу придумать. Это можно сделать в Perl.


Методика:

  1. построить хэш, где ключи являются somethings и значения somethingelses.
  2. соедините ключи хэша с | символ и используйте его как группу совпадений в регулярном выражении.
  3. в замене интерполируйте выражение, которое извлекает значение из хэша, используя переменную match для группы захвата

Проблема:

к сожалению, PowerShell не предоставляет переменные соответствия вне вызова regex replace. Это не работает с -заменить оператор и он не работает с [регулярное выражение]:: заменить.

в Perl, вы можете сделать это, например:

$string =~ s/(1|2|3)/@{[ + 5]}/g;

это добавит 5 к цифрам 1, 2 и 3 по всей строке, поэтому, если строка "1224526123 [2] [6]", он превращается в "6774576678 [7] [6]".

однако, в PowerShell, оба из них терпят крах:

$string -replace '(1|2|3)',"$( + 5)"

[regex]::replace($string,'(1|2|3)',"$( + 5)")

в обоих случаях 1$ принимает значение null, а выражение принимает значение plain old 5. Переменные соответствия в заменах только значение в результирующей строке, т. е. строка с одной кавычкой или независимо от того, что оценивает строка с двумя кавычками. Они в основном просто обратные ссылки, которые выглядят как переменные соответствия. Конечно, вы можете процитировать $ перед числом в строке с двойными кавычками, поэтому он будет оценивать соответствующую группу совпадений, но это побеждает цель - он не может участвовать в выражении.


Решение:

[этот ответ был изменен от оригинала. Он был отформатирован, чтобы соответствовать строкам соответствия с метасимволами regex. И экран телевизора, конечно.]

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

$filePath = $ARGV[0]; # Or hard-code it or whatever
open INPUT, "< $filePath";
open OUTPUT, '> C:\log.txt';
%replacements = (
  'something0' => 'somethingelse0',
  'something1' => 'somethingelse1',
  'something2' => 'somethingelse2',
  'something3' => 'somethingelse3',
  'something4' => 'somethingelse4',
  'something5' => 'somethingelse5',
  'X:\Group_14\DACU' => '\DACU$',
  '.*[^xyz]' => 'oO{xyz}',
  'moresomethings' => 'moresomethingelses'
);
foreach (keys %replacements) {
  push @strings, qr/\Q$_\E/;
  $replacements{$_} =~ s/\/\\/g;
}
$pattern = join '|', @strings;
while (<INPUT>) {
  s/($pattern)/$replacements{}/g;
  print OUTPUT;
}
close INPUT;
close OUTPUT;

он ищет ключи хэша (слева от =>), и заменяет их соответствующими значениями. Вот что происходит:

  • на foreach цикл проходит через все элементы хэша и создайте массив под названием @strings, который содержит ключи %замены хэш, с метасимволами, цитируемыми с помощью \Q и \E, и результат этого цитируется для использования в качестве шаблона регулярных выражений ( qr = регулярное выражение цитаты). В том же проходе он избегает всех обратных косых черт в строках замены, удваивая их.
  • далее элементы массива соединяются с |'s, чтобы сформировать шаблон поиска. Вы можете включить скобки группировки в $ pattern если хотите, но я думаю, что таким образом становится яснее, что происходит.
  • на пока loop считывает каждую строку из входного файла, заменяет любую из строк в шаблоне поиска соответствующими строками замены в хэше и записывает строку в выходной файл.

кстати, вы могли заметить несколько других модификаций оригинального сценария. Мой Perl собрал пыль во время моего недавнего удара PowerShell, и на втором взгляде я заметил несколько вещей, которые можно было бы сделать лучше.

  • while (<INPUT>) чтение файла по одной строке за раз. Гораздо разумнее, чем читать все 150 000 строк в массив, особенно когда ваша цель-эффективность.
  • я упростил @{[$replacements{}]} до $replacements{}. Perl не имеет встроенного способа вычисления выражения например, PowerShell $(), так что @{[ ]} используется в качестве обходного пути - он создает буквальный массив из одного элемента, содержащего выражение. Но я понял, что это не обязательно, если выражение является всего лишь одной скалярной переменной (у меня было это как пережиток моего первоначального тестирования, где я применял вычисления к 1$ переменная матч).
  • на закрыть заявления не являются строго необходимыми, но это считается хорошая практика, чтобы явно закрыть свои работы.
  • я изменил на аббревиатура foreach, чтобы сделать его более ясным и знакомым программистам PowerShell.

объединение метода хэша из ответ Ади Инбара, и оценщик матча от ответ кита Хилла к другому недавнему вопросу, вот как вы можете выполнить замену в PowerShell:

# Build hashtable of search and replace values.
$replacements = @{
  'something0' = 'somethingelse0'
  'something1' = 'somethingelse1'
  'something2' = 'somethingelse2'
  'something3' = 'somethingelse3'
  'something4' = 'somethingelse4'
  'something5' = 'somethingelse5'
  'X:\Group_14\DACU' = '\DACU$'
  '.*[^xyz]' = 'oO{xyz}'
  'moresomethings' = 'moresomethingelses'
}

# Join all (escaped) keys from the hashtable into one regular expression.
[regex]$r = @($replacements.Keys | foreach { [regex]::Escape( $_ ) }) -join '|'

[scriptblock]$matchEval = { param( [Text.RegularExpressions.Match]$matchInfo )
  # Return replacement value for each matched value.
  $matchedValue = $matchInfo.Groups[0].Value
  $replacements[$matchedValue]
}

# Perform replace over every line in the file and append to log.
Get-Content $filePath |
  foreach { $r.Replace( $_, $matchEval ) } |
  Add-Content 'C:\log.txt'

Я также не знаю, как решить это в powershell, но я знаю, как решить это в Bash, и это с помощью инструмента под названием sed. К счастью, есть также Sed для Windows. Если все, что вы хотите сделать, это заменить" что-то# "на" somethingelse# " везде, то эта команда сделает трюк для вас

sed -i "s/something([0-9]+)/somethingelse/g" c:\log.txt

в Bash вам действительно нужно будет избежать нескольких этих символов с обратными косыми чертами, но я не уверен, что вам нужно в windows. Если первая команда жалуется, что вы можете попробовать

sed -i "s/something\([0-9]\+\)/somethingelse/g" c:\log.txt

Я бы использовал оператор powershell switch:

$string = gc $filePath 
$string | % {
    switch -regex ($_)  {
        'something0' { 'somethingelse0' }
        'something1' { 'somethingelse1' }
        'something2' { 'somethingelse2' }
        'something3' { 'somethingelse3' }
        'something4' { 'somethingelse4' }
        'something5' { 'somethingelse5' }
        'pattern(?<a>\d+)' { $matches['a'] } # sample of more complex logic
   ...
   (600 More Lines...)
   ...
        default { $_ }
   }
} | ac "C:\log.txt"