【问题标题】:PowerShell is slow (much slower than Python) in large Search/Replace operation?PowerShell 在大型搜索/替换操作中很慢(比 Python 慢得多)?
【发布时间】:2012-04-01 05:51:05
【问题描述】:

我有 265 个 CSV 文件,总记录(行)超过 400 万条,需要在所有 CSV 文件中进行搜索和替换。我在下面有一个我的 PowerShell 代码的 sn-p 可以执行此操作,但执行该操作需要 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 在文本操作方面只是一种更强大的编程语言?

【问题讨论】:

    标签: python performance powershell replace


    【解决方案1】:

    实际上,我现在面临着类似的问题。在我的新工作中,我必须解析巨大的文本文件以根据某些标准提取信息。 powershell 脚本(优化到边缘)需要 4 小时才能返回完全处理的 csv 文件。我们编写了另一个 Python 脚本,耗时不到 1 小时......

    尽管我很喜欢 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{} 中以使其保持“科学性”。

    另外,link,疯狂阅读..

    【讨论】:

    • 这与问题无关,并且在链接中:当然powershell cmdlet 速度较慢,它们会获得有关文件的更多信息(例如.Net DateTime 的创建时间和长度) 并构造 PSCustomObjects,它们下面有 PSProviders 的代码。 .Net 方法调用中有 @() += 模式,这绝对是非常慢的,并且从不推荐用于快速 PowerShell。声称 C# 已编译意味着 PowerShell 不是 - 它是由 PS 3 及更高版本中的 DLR 引擎提供的 - 但仅适用于保存的脚本和函数,而不是从 ISE 运行或键入的代码。
    • 您的“Mary had a little lamb”比较也有缺陷——两种语言中的.lower().upper() 调用都不会更改字符串,它们会生成并返回一个新字符串。在 Python 中,当您生成一个值但不使用它时,什么也不会发生。在 PowerShell 中,当您生成一个值并且不将其分配到输出管道的任何位置时 - 这意味着您的 PowerShell 脚本通过更复杂的机制(管道和输出格式化程序,而不是write-host 相当于print())
    • @TessellatingHeckler 谢谢你。更新了示例脚本。还将它们写入 .py 和 .ps1 文件并同时运行: C:\temp> $time1 = get-date >> python.exe .\mary.py >> $time2 = get-date >> New-TimeSpan - Start $time1 -End $time2 和 python 在 49.7 秒内完成,我已经厌倦了等待 PS ......但是,经过越来越多的使用,我确信任何与数据相关的活动,使用 Py,并用于操作,请使用 Ps。两者都是很棒且易于使用的语言,所以都喜欢它们。
    • 两个月后我发现您的评论的唯一原因是因为我正在追逐一个我想改进的 PowerShell 性能问题,所以我部分同意。即便如此,如果您注释掉打印行,两个脚本都会在 0.6 秒内运行。使用 print,Python 运行时间为 46 秒,使用 write-host,PowerShell 运行时间为 10 分钟(!)。但是将其切换为[console]::writeline($string) 只需 37 秒。 write-host 是 Write-Information 的包装器,这是 Python 没有的输出流,用于弥补 write-host 然后无法远程运行的所有脚本。
    【解决方案2】:

    您可能想尝试以下命令:

    gci C:\temp\csv\*.csv | % { (gc $_) -replace $SearchStr, $ReplaceStr | out-file $_}
    

    另外,有些字符串可能需要转义字符,因此您应该使用 [regex]Escape 来生成内置转义字符的字符串。代码如下所示:

    gci C:\temp\csv\*.csv | % { (gc $_) -replace $([regex]::Escape($SearchStr)) $([regex]::Escape($ReplaceStr)) | out-file $_}
    

    【讨论】:

      【解决方案3】:

      试试这个 PowerShell 脚本。它应该表现得更好。由于文件是在缓冲流中读取的,因此 RAM 的使用也大大减少。

      $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 )
      

      【讨论】:

      • 然而,在 Python 的情况下,它仍然一次处理每个文件(只需要更多的代码行才能到达那里),所以我想内存使用情况“大致相同”。 ..还是我错过了什么? :(
      • @pst 我没有测试过,但看起来s = f.read() 将整个内容加载到内存中。您也可以使用 PowerShell 使用 $reader.ReadToEnd() 来执行此操作。
      • 啊,我假设这就是Get-Content 的运作方式:-/
      • @pst Get-Content 允许流式传输,这意味着管道中的下一个 cmdlet 可以在它进入管道后立即开始工作 - 即使很快会有更多数据。当您分配给 $content 时,您将失去该功能,因为整个 get-content 必须在下一个操作发生之前完成。
      • 我运行了 ReadLine/WriteLine 解决方案,它把我的时间缩短到了 5 分钟!巨大的进步!今晚晚些时候我将不得不尝试使用 -ReadCount 和管道方法。
      【解决方案4】:

      我不了解 Python,但看起来您正在 Python 脚本中进行文字字符串替换。在 Powershell 中,-replace 运算符是正则表达式搜索/替换。我会将 Powershell 转换为在字符串类上使用 replace 方法(或者回答原始问题,我认为您的 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 版本似乎正在将整个文件读入 single 字符串。另一方面,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 而不是类似的 cmdlet,但它们会在您需要时将功能放在那里。

      【讨论】:

      • 但是正则表达式——对于任何简单的非过度回溯正则表达式——通常都有非常快的实现。可能不如普通字符串搜索快(尽管在某些情况下可能),但我怀疑它慢了 17 倍:(无论如何,差异和测试代码 +1。
      • @pst 正则表达式可以在 PowerShell 中编译,这样可以提供更好的性能。 $re = New-Object regex '\w+', 'Compiled'
      • 感谢上面的测试代码。我运行了它,仍然需要很长时间才能完成任务。
      • @pst 一般也许,但我不这么认为。另外我不知道Powershell在缓存正则表达式方面有多好,所以它可能会在每次迭代时重新创建正则表达式。安迪的想法可以避免这个问题。
      • @GideonEngelberth .NET BCL 缓存最后 20 个左右的正则表达式 [在大多数情况下]。看到上述各种方法的基准测试会很有趣。
      【解决方案5】:

      我经常看到这个:

      $content | foreach {$_ -replace $SearchStr, $ReplaceStr} 
      

      -replace 运算符将一次处理整个数组:

      $content -replace $SearchStr, $ReplaceStr
      

      并且比一次迭代一个元素要快得多。我怀疑这样做可能会让你更接近苹果对苹果的比较。

      【讨论】:

      • 得到这个处理一个 400MB 的文本文件 The '-replace' operator failed: Exception of type 'System.OutOfMemoryException' was thrown.. 正在做一些比较测试。
      • 是的,一次读取一个块对于大文件来说要好得多。
      • 然后你可以对每个块使用-match或-replace作为一个整体。
      猜你喜欢
      • 2014-10-11
      • 2012-02-09
      • 2012-12-17
      • 2021-10-19
      • 2020-02-12
      • 2014-10-08
      • 2018-07-20
      • 2015-10-05
      • 2020-03-01
      相关资源
      最近更新 更多