PowerShell работает медленнее (гораздо медленнее, чем Python) в большой операции поиска/замены?
У меня есть 265 CSV файлов с более чем 4 миллионами итоговых записей (строк), и вам нужно выполнить поиск и заменить во всех файлах CSV. У меня есть фрагмент моего кода PowerShell ниже, который делает это, но для выполнения действия требуется 17 минут:
ForEach ($file in Get-ChildItem C:\temp\csv\*.csv)
{
$content = Get-Content -path $file
$content | foreach {$_ -replace $SearchStr, $ReplaceStr} | Set-Content $file
}
Теперь у меня есть следующий код Python, который делает то же самое, но занимает менее 1 минуты:
import os, fnmatch
def findReplace(directory, find, replace, filePattern):
for path, dirs, files in os.walk(os.path.abspath(directory)):
for filename in fnmatch.filter(files, filePattern):
filepath = os.path.join(path, filename)
with open(filepath) as f:
s = f.read()
s = s.replace(find, replace)
with open(filepath, "w") as f:
f.write(s)
findReplace("c:/temp/csv", "Search String", "Replace String", "*.csv")
Почему метод Python намного эффективнее? Является ли мой код PowerShell неэффективным или Python просто более мощный язык программирования, когда дело доходит до обработки текста?
Ответы
Ответ 1
Дайте этой PowerShell script попытку. Он должен работать намного лучше. Гораздо меньше использования ОЗУ, так как файл читается в буферизованном потоке.
$reader = [IO.File]::OpenText("C:\input.csv")
$writer = New-Object System.IO.StreamWriter("C:\output.csv")
while ($reader.Peek() -ge 0) {
$line = $reader.ReadLine()
$line2 = $line -replace $SearchStr, $ReplaceStr
$writer.writeline($line2)
}
$reader.Close()
$writer.Close()
Этот процесс обрабатывает один файл, но вы можете проверить его производительность, и если его более приемлемо, добавьте его в цикл.
В качестве альтернативы вы можете использовать Get-Content
для чтения ряда строк в памяти, выполнить замену и затем записать обновленный фрагмент с использованием конвейера PowerShell.
Get-Content "C:\input.csv" -ReadCount 512 | % {
$_ -replace $SearchStr, $ReplaceStr
} | Set-Content "C:\output.csv"
Чтобы сжать немного больше производительности, вы также можете скомпилировать регулярное выражение (-replace
использует регулярные выражения) следующим образом:
$re = New-Object Regex $SearchStr, 'Compiled'
$re.Replace( $_ , $ReplaceStr )
Ответ 2
Я вижу это много:
$content | foreach {$_ -replace $SearchStr, $ReplaceStr}
Оператор -replace обрабатывает сразу весь массив:
$content -replace $SearchStr, $ReplaceStr
и делать это намного быстрее, чем повторение через один элемент за раз. Я подозреваю, что это может приблизиться к сопоставлению яблок и яблок.
Ответ 3
Я не знаю Python, но похоже, что вы выполняете буквальные замены строк в Python script. В Powershell оператор -replace
- это поиск/замена регулярного выражения. Я бы преобразовал Powershell в метод замены в классе строк (или для ответа на исходный вопрос, я думаю, что ваша Powershell неэффективна).
ForEach ($file in Get-ChildItem C:\temp\csv\*.csv)
{
$content = Get-Content -path $file
# look close, not much changes
$content | foreach {$_.Replace($SearchStr, $ReplaceStr)} | Set-Content $file
}
РЕДАКТИРОВАТЬ. После дальнейшего рассмотрения, я думаю, что вижу другие (возможно, более важные) различия в версиях. Кажется, что версия Python считывает весь файл в одну строку. Версия Powershell, с другой стороны, считывает массив строк.
В справке Get-Content
упоминается параметр ReadCount
, который может повлиять на производительность. Установка этого числа в -1, кажется, считывает весь файл в один массив. Это будет означать, что вы передаете массив через конвейер вместо отдельных строк, но простое изменение кода будет иметь дело с этим:
# $content is now an array
$content | % { $_ } | % {$_.Replace($SearchStr, $ReplaceStr)} | Set-Content $file
Если вы хотите прочитать весь файл в одну строку, например, версию Python, просто вызовите метод .NET напрямую:
# now you have to make sure to use a FULL RESOLVED PATH
$content = [System.IO.File]::ReadAllText($file.FullName)
$content.Replace($SearchStr, $ReplaceStr) | Set-Content $file
Это не совсем так, как "Powershell-y", поскольку вы используете .NET API напрямую, а не аналогичные командлеты, но они помещают туда там, где вам это нужно.
Ответ 4
Вы можете попробовать следующую команду:
gci C:\temp\csv\*.csv | % { (gc $_) -replace $SearchStr, $ReplaceStr | out-file $_}
Кроме того, для некоторых строк могут потребоваться escape-символы, поэтому вы должны использовать [regex] Escape для генерации строк с встроенными escape-символами. Код будет выглядеть так:
gci C:\temp\csv\*.csv | % { (gc $_) -replace $([regex]::Escape($SearchStr)) $([regex]::Escape($ReplaceStr)) | out-file $_}
Ответ 5
На самом деле, сейчас я столкнулся с подобной проблемой. С моей новой работой я должен разбирать огромные текстовые файлы, чтобы извлекать информацию по определенным критериям. Powerwill script (оптимизированный до краев) занимает 4 часа, чтобы вернуть полностью обработанный CSV файл. Мы написали еще один python script, который занял чуть меньше 1 часа...
Насколько я люблю powershell, я был разбит сердцем. Для вашего удовольствия попробуйте следующее:
Powershell:
$num = 0
$string = "Mary had a little lamb"
while($num -lt 1000000){
$string = $string.ToUpper()
$string = $string.ToLower()
Write-Host $string
$num++
}
Python:
num = 0
string = "Mary had a little lamb"
while num < 1000000:
string = string.lower()
string = string.upper()
print(string)
num+=1
и запустить два задания. Вы можете даже инкапсулировать в Measure-command {}, чтобы сохранить его "научным".
Кроме того, ссылка, сумасшедший читал..