PowerShell 脚本读取文件的性能太慢
Performance of PowerShell script reading files is too slow
我目前正在编写将在 TeamCity 中作为构建步骤的一部分使用的 PowerShell 脚本。脚本必须:
- 递归检查文件夹中具有特定扩展名 (.item) 的所有文件,
- 读取每个文件的第三行(其中包含一个 GUID)并检查这些行中是否有任何重复项,
- 记录包含重复 GUID 的文件的路径并记录 GUID 本身,
- 如果发现一个或多个重复项,则使 TeamCity 构建失败
我对 PowerShell 脚本完全陌生,但到目前为止,我已经做了一些符合我预期的事情:
Write-Host "Start checking for Unicorn serialization errors."
$files = get-childitem "%system.teamcity.build.workingDir%\Sitecore\serialization" -recurse -include *.item | where {! $_.PSIsContainer} | % { $_.FullName }
$arrayOfItemIds = @()
$NrOfFiles = $files.Length
[bool] $FoundDuplicates = 0
Write-Host "There are $NrOfFiles Unicorn item files to check."
foreach ($file in $files)
{
$thirdLineOfFile = (Get-Content $file)[2 .. 2]
if ($arrayOfItemIds -contains $thirdLineOfFile)
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
Write-Host "Duplicate item ID found!"
Write-Host "Item file path: $file"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "-------------"
Write-Host ""
}
else
{
$arrayOfItemIds += $thirdLineOfFile
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID's were detected in Sitecore serialised items. Check the build log to see which files and ID's are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors."
问题是:速度很慢!此脚本必须检查的文件夹当前包含超过 14.000 个 .item-files,并且该数量很可能在未来只会继续增加。我知道打开和读取这么多文件是一项耗时的操作,但没想到大约需要半个小时才能完成。这太长了,因为这意味着每个(快照)构建的构建时间都会延长半小时,这是不可接受的。我曾希望脚本最多能在几分钟内完成。
我无法相信没有更快的方法来做到这一点..所以非常感谢这方面的任何帮助!
解决方案
好吧,我不得不说,到目前为止,我收到的所有 3 个答案都对我有所帮助。我首先开始直接使用 .NET 框架 类,然后也使用字典来解决不断增长的数组问题。 运行 我自己的脚本花费的时间大约是 30 分钟,然后通过使用 .NET 框架 类 减少到仅 2 分钟。在使用字典解决方案后,它也下降到只有 6 或 7 秒!我使用的最终脚本:
Write-Host "Start checking for Unicorn serialization errors."
[String[]] $allFilePaths = [System.IO.Directory]::GetFiles("%system.teamcity.build.workingDir%\Sitecore\serialization", "*.item", "AllDirectories")
$IdsProcessed = New-Object 'system.collections.generic.dictionary[string,string]'
[bool] $FoundDuplicates = 0
$NrOfFiles = $allFilePaths.Length
Write-Host "There are $NrOfFiles Unicorn item files to check."
Write-Host ""
foreach ($filePath in $allFilePaths)
{
[System.IO.StreamReader] $sr = [System.IO.File]::OpenText($filePath)
$unused1 = $sr.ReadLine() #read the first unused line
$unused2 = $sr.ReadLine() #read the second unused line
[string]$thirdLineOfFile = $sr.ReadLine()
$sr.Close()
if ($IdsProcessed.ContainsKey($thirdLineOfFile))
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
$otherFileWithSameId = $IdsProcessed[$thirdLineOfFile]
Write-Host "---------------"
Write-Host "Duplicate item ID found!"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "Item file path 1: $filePath"
Write-Host "Item file path 2: $otherFileWithSameId"
Write-Host "---------------"
Write-Host ""
}
else
{
$IdsProcessed.Add($thirdLineOfFile, $filePath)
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID|'s were detected in Sitecore serialised items. Check the build log to see which files and ID|'s are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors. No duplicate ID's were found."
非常感谢大家!
尝试用 [System.IO.File]::ReadLines
替换 Get-Content
。如果这仍然太慢,请考虑使用 System.IO.StreamReader
- 这会导致您编写更多代码,但只允许您阅读前 3 行。
不清楚当您使用 Get-ChildItem 和 Get-Content 等高级命令时 PowerShell 究竟做了什么。所以我会更明确一点,直接使用 .NET 框架 classes。
使用
获取文件夹中文件的路径
[String[]] $files = [System.IO.Directory]::GetFiles($folderPath, "*.yourext")
然后,不要使用 Get-Content,而是打开每个文件并阅读前三行。像这样:
[System.IO.StreamReader] $sr = [System.IO.File]::OpenText(path)
[String]$line = $sr.ReadLine()
while ($line -ne $null)
{
# do your thing, break when you know enough
# ...
[String]$line = $sr.ReadLine()
}
$sr.Close()
我可能犯了一两个错误,我懒得起来在电脑上测试了。
并且您可能需要考虑重新设计构建系统以使用更少的文件。 14000 个文件和增长似乎是不必要的。如果能将一些数据合并到更少的文件中,或许对性能也有很大的帮助。
要检查重复的 guid,请使用 Dictionary class,其中字符串是您的文件名。然后,如果发现任何重复项,您可以报告重复项的位置。
我想你的问题可能是你的Array引起的,应该不是文件读取的问题。
PowerShell 中数组的大小是不可变的,因此每次向数组添加一个项目时,它都会创建一个新数组并复制所有项目。
您的数组通常不会包含正在查找的值,并且必须将 $thirdLineOfFile
与不断增长的数组中的每个项目进行比较。
我一直在使用.Net Dictionaries 来解决这个问题。 (或者当我没有进行大量查找时使用 ArrayLists)MSDN Dictionary Reference
注意:PowerShell 提供了一个名为“Measure-Command
”的 Cmdlet,您可以使用它来确定脚本的哪一部分实际上 运行 很慢。我会测试文件读取时间和时间以增加数组和查找值。根据文件的大小,您实际上也可能遇到性能问题。
这是为使用 .Net 词典而改编的代码。我重命名了你的变量,因为它不再是一个数组。
Write-Host "Start checking for Unicorn serialization errors."
$files = get-childitem "%system.teamcity.build.workingDir%\Sitecore\serialization" -recurse -include *.item | where {! $_.PSIsContainer} | % { $_.FullName }
#$arrayOfItemIds = @()
$IdsProcessed = New-Object 'system.collections.generic.dictionary[string,string]' # A .Net Dictionary will be faster for inserts and lookups.
$NrOfFiles = $files.Length
[bool] $FoundDuplicates = 0
Write-Host "There are $NrOfFiles Unicorn item files to check."
foreach ($file in $files)
{
$thirdLineOfFile = (Get-Content -path $file -TotalCount 3)[2] # TotalCount param will let us pull in just the beginning of the file.
#if ($arrayOfItemIds -contains $thirdLineOfFile)
if($IdsProcessed.ContainsKey($thirdLineOfFile))
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
Write-Host "Duplicate item ID found!"
Write-Host "Item file path: $file"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "-------------"
Write-Host ""
}
else
{
#$arrayOfItemIds += $thirdLineOfFile
$IdsProcessed.Add($thirdLineOfFile,$null)
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID's were detected in Sitecore serialised items. Check the build log to see which files and ID's are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors."
我目前正在编写将在 TeamCity 中作为构建步骤的一部分使用的 PowerShell 脚本。脚本必须:
- 递归检查文件夹中具有特定扩展名 (.item) 的所有文件,
- 读取每个文件的第三行(其中包含一个 GUID)并检查这些行中是否有任何重复项,
- 记录包含重复 GUID 的文件的路径并记录 GUID 本身,
- 如果发现一个或多个重复项,则使 TeamCity 构建失败
我对 PowerShell 脚本完全陌生,但到目前为止,我已经做了一些符合我预期的事情:
Write-Host "Start checking for Unicorn serialization errors."
$files = get-childitem "%system.teamcity.build.workingDir%\Sitecore\serialization" -recurse -include *.item | where {! $_.PSIsContainer} | % { $_.FullName }
$arrayOfItemIds = @()
$NrOfFiles = $files.Length
[bool] $FoundDuplicates = 0
Write-Host "There are $NrOfFiles Unicorn item files to check."
foreach ($file in $files)
{
$thirdLineOfFile = (Get-Content $file)[2 .. 2]
if ($arrayOfItemIds -contains $thirdLineOfFile)
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
Write-Host "Duplicate item ID found!"
Write-Host "Item file path: $file"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "-------------"
Write-Host ""
}
else
{
$arrayOfItemIds += $thirdLineOfFile
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID's were detected in Sitecore serialised items. Check the build log to see which files and ID's are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors."
问题是:速度很慢!此脚本必须检查的文件夹当前包含超过 14.000 个 .item-files,并且该数量很可能在未来只会继续增加。我知道打开和读取这么多文件是一项耗时的操作,但没想到大约需要半个小时才能完成。这太长了,因为这意味着每个(快照)构建的构建时间都会延长半小时,这是不可接受的。我曾希望脚本最多能在几分钟内完成。
我无法相信没有更快的方法来做到这一点..所以非常感谢这方面的任何帮助!
解决方案
好吧,我不得不说,到目前为止,我收到的所有 3 个答案都对我有所帮助。我首先开始直接使用 .NET 框架 类,然后也使用字典来解决不断增长的数组问题。 运行 我自己的脚本花费的时间大约是 30 分钟,然后通过使用 .NET 框架 类 减少到仅 2 分钟。在使用字典解决方案后,它也下降到只有 6 或 7 秒!我使用的最终脚本:
Write-Host "Start checking for Unicorn serialization errors."
[String[]] $allFilePaths = [System.IO.Directory]::GetFiles("%system.teamcity.build.workingDir%\Sitecore\serialization", "*.item", "AllDirectories")
$IdsProcessed = New-Object 'system.collections.generic.dictionary[string,string]'
[bool] $FoundDuplicates = 0
$NrOfFiles = $allFilePaths.Length
Write-Host "There are $NrOfFiles Unicorn item files to check."
Write-Host ""
foreach ($filePath in $allFilePaths)
{
[System.IO.StreamReader] $sr = [System.IO.File]::OpenText($filePath)
$unused1 = $sr.ReadLine() #read the first unused line
$unused2 = $sr.ReadLine() #read the second unused line
[string]$thirdLineOfFile = $sr.ReadLine()
$sr.Close()
if ($IdsProcessed.ContainsKey($thirdLineOfFile))
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
$otherFileWithSameId = $IdsProcessed[$thirdLineOfFile]
Write-Host "---------------"
Write-Host "Duplicate item ID found!"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "Item file path 1: $filePath"
Write-Host "Item file path 2: $otherFileWithSameId"
Write-Host "---------------"
Write-Host ""
}
else
{
$IdsProcessed.Add($thirdLineOfFile, $filePath)
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID|'s were detected in Sitecore serialised items. Check the build log to see which files and ID|'s are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors. No duplicate ID's were found."
非常感谢大家!
尝试用 [System.IO.File]::ReadLines
替换 Get-Content
。如果这仍然太慢,请考虑使用 System.IO.StreamReader
- 这会导致您编写更多代码,但只允许您阅读前 3 行。
不清楚当您使用 Get-ChildItem 和 Get-Content 等高级命令时 PowerShell 究竟做了什么。所以我会更明确一点,直接使用 .NET 框架 classes。
使用
获取文件夹中文件的路径[String[]] $files = [System.IO.Directory]::GetFiles($folderPath, "*.yourext")
然后,不要使用 Get-Content,而是打开每个文件并阅读前三行。像这样:
[System.IO.StreamReader] $sr = [System.IO.File]::OpenText(path)
[String]$line = $sr.ReadLine()
while ($line -ne $null)
{
# do your thing, break when you know enough
# ...
[String]$line = $sr.ReadLine()
}
$sr.Close()
我可能犯了一两个错误,我懒得起来在电脑上测试了。
并且您可能需要考虑重新设计构建系统以使用更少的文件。 14000 个文件和增长似乎是不必要的。如果能将一些数据合并到更少的文件中,或许对性能也有很大的帮助。
要检查重复的 guid,请使用 Dictionary
我想你的问题可能是你的Array引起的,应该不是文件读取的问题。
PowerShell 中数组的大小是不可变的,因此每次向数组添加一个项目时,它都会创建一个新数组并复制所有项目。
您的数组通常不会包含正在查找的值,并且必须将
$thirdLineOfFile
与不断增长的数组中的每个项目进行比较。
我一直在使用.Net Dictionaries 来解决这个问题。 (或者当我没有进行大量查找时使用 ArrayLists)MSDN Dictionary Reference
注意:PowerShell 提供了一个名为“Measure-Command
”的 Cmdlet,您可以使用它来确定脚本的哪一部分实际上 运行 很慢。我会测试文件读取时间和时间以增加数组和查找值。根据文件的大小,您实际上也可能遇到性能问题。
这是为使用 .Net 词典而改编的代码。我重命名了你的变量,因为它不再是一个数组。
Write-Host "Start checking for Unicorn serialization errors."
$files = get-childitem "%system.teamcity.build.workingDir%\Sitecore\serialization" -recurse -include *.item | where {! $_.PSIsContainer} | % { $_.FullName }
#$arrayOfItemIds = @()
$IdsProcessed = New-Object 'system.collections.generic.dictionary[string,string]' # A .Net Dictionary will be faster for inserts and lookups.
$NrOfFiles = $files.Length
[bool] $FoundDuplicates = 0
Write-Host "There are $NrOfFiles Unicorn item files to check."
foreach ($file in $files)
{
$thirdLineOfFile = (Get-Content -path $file -TotalCount 3)[2] # TotalCount param will let us pull in just the beginning of the file.
#if ($arrayOfItemIds -contains $thirdLineOfFile)
if($IdsProcessed.ContainsKey($thirdLineOfFile))
{
$FoundDuplicates = 1
$itemId = $thirdLineOfFile.Split(":")[1].Trim()
Write-Host "Duplicate item ID found!"
Write-Host "Item file path: $file"
Write-Host "Detected duplicate ID: $itemId"
Write-Host "-------------"
Write-Host ""
}
else
{
#$arrayOfItemIds += $thirdLineOfFile
$IdsProcessed.Add($thirdLineOfFile,$null)
}
}
if ($foundDuplicates)
{
"##teamcity[buildStatus status='FAILURE' text='One or more duplicate ID's were detected in Sitecore serialised items. Check the build log to see which files and ID's are involved.']"
exit 1
}
Write-Host "End script checking for Unicorn serialization errors."