背景
某个场景,需要手动对业务系统替换几个文件,文件位于不同的路径下,且文件量大。
在替换结束后,需要检查文件是否真的被替换了。是否被替换了合适的版本。
实现
检查文件有没有货不对板有多种方式。如果是exe这类正经封装过的文件,一般会有FileVersionRaw属性,属性中会有版本号,我们基于此比较就可以。但如果是脚本、文本文件这种没有版本号的,这个方法则不适用了。另外版本号也有可能会骗人,版本起名全靠作者自觉。
基于此,可以使用PowerShell内置命令Get-FileHASH来比对每一个文件的哈希值,通过比对哈希值(默认SHA256),来判断文件是否被替换成正确的版本。
1
2
3
4
5
PS C:\windows\system32> Get-FileHash $a[888]
Algorithm Hash Path
--------- ---- ----
SHA256 B93A3F3DC478B4D167F26BCDEB4E4984DA997665AC79B91BCA3F1DF4E36A7E5E C:\windows\system32\dafWCN.dll
Get-FileHASH除了支持使用SHA256来计算外,也支持如下格式
- SHA1
- SHA256
- SHA384
- SHA512
- MD5
代码实现
首先准备两个函数,一个用来抓数据
1
2
3
4
5
6
7
8
function OutputHASHCheckFile ($Filename, $CheckPath,$OutputPath) {
$CheckPath=$CheckPath.replace(':','$')
$FullFile = Get-ChildItem $("\\" + $Filename + "\" + $CheckPath) -Recurse
$Files = $FullFile | Where-Object { $_.Attributes -eq "Archive" }
$FileHash = $Files | ForEach-Object { Get-FileHASH $_.FullName }
"HASH,path,FileVersion" | Out-File $($OutputPath + $Filename + ".csv") -Encoding utf8
$FileHash | ForEach-Object { $_.HASH + "," + $_.path.split('$')[-1] + "," + (Get-Item ($_.path)).VersionInfo.FileVersionRaw } | Out-File $($OutputPath + $Filename + ".csv") -Encoding utf8 -Append
}
这里有一个大问题,就是类似C:\windows\system32\dafWCN.dll
这种路径格式,当包含\
符号时,PowerShell的解析是有问题的,这个时候不能用match
,但是可以用EQ
,非常之神奇。
1
2
3
4
5
6
7
8
9
10
11
\Windows\System32\zh-CN\acledit.dll.mui
PS C:\windows\system32>
PS C:\windows\system32> $Basefile.path
\Windows\System32\zh-CN\aadtb.dll.mui
\Windows\System32\zh-CN\aadWamExtension.dll.mui
\Windows\System32\zh-CN\AboutSettingsHandlers.dll.mui
...........
PS C:\windows\system32> $Basefile.path -match $Targetfile[4].path
#不同操作系统的返回不太一样,但都是返回错误
PS C:\windows\system32> $Basefile.path -eq $Targetfile[4].path
\Windows\System32\zh-CN\acledit.dll.mui
再做一个函数用来比对哈希值,这里用到了相对很复杂的例子,首先从目标目录中找出和基线目录中一致的目录
(也就是挑出来),然后从整个目标目录中,找出来这条完整的数据
(包含目录、哈希值、版本号)
1
$Targetfile | Where-Object {$_.path -eq ($Targetfile.path -eq $temp.path) }
完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = Import-Csv $baselineFile
$Targetfile = Import-Csv $targetFile
$Basefile | ForEach-Object {
$temp=$_
$identicalfile = $Targetfile | Where-Object {$_.path -eq ($Targetfile.path -eq $temp.path) }
if ($identicalfile.HASH -ne $_.HASH) {
Write-Host ($_.path + " | " + $_.Fileversion) -ForegroundColor Red
}
}
}
优化1
上面代码是可以成功的,但是问题是速度太慢了。我插入了一条获取当前时间的逻辑,可以看到每一次比对文件都需要消耗1秒多,对于有1700多个文件的目录而言,就是需要消耗1700多秒。显然是无法接受的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = (Import-Csv $baselineFile)
$Targetfile = (Import-Csv $targetFile)
$Basefile | ForEach-Object {
$temp=$_
$identicalfile = $Targetfile | Where-Object {$_.path -eq ($Targetfile.path -eq $temp.path) }
(Get-Date).TimeOfDay.TotalSeconds #检查时间信息
if ($identicalfile.HASH -ne $_.HASH) {
Write-Host ($_.path + " | " + $_.Fileversion) -ForegroundColor Red
}
}
}
#可以看到输出结果,每一次查询,会消耗1秒多
64498.099711
64499.1767082
64500.2567158
为什么会这么慢的?这完全是查询方式的锅。
1
$Targetfile | Where-Object {$_.path -eq ($Targetfile.path -eq $temp.path) }
那么这里面是谁慢导致的呢?我们看下面的例子。当我们单纯用EQ
的方式来匹配数据的时候,只用了2ms,但是换成管道,然后在右侧使用Where-Object
来进行查询呢?达到了惊人的90ms
,这就差了几十倍。
1
2
3
4
5
6
7
8
9
$a=1..10000
PS C:\windows\system32> (Measure-Command {$a -eq 22}).TotalMilliseconds
2.1583
PS C:\windows\system32> (Measure-Command { $a | Where-Object { $_ -eq 22 } }).TotalMilliseconds
90.061
如果我们换成foreach
这种循环来做呢?只需要22ms。
1
2
3
4
5
6
7
(Measure-Command { foreach ($item in $a) {
if ($item -eq 22) {
Write-Host $item
}
} }).TotalMilliseconds
22
22.3433
解题思路就是尽量不要在这里用管道,实在需要遍历的话,用foreach也可以。由于路径格式,包含\
符号。所以至少需要使用一次循环,这里先尝试下用foreach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#脚本1
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = (Import-Csv $baselineFile)
$Targetfile = (Import-Csv $targetFile)
$Basefile | ForEach-Object {
$temp = $_
$identicalfile = foreach ($item in $Targetfile ) {
if ($item.path -eq ($Targetfile.path -eq $temp.path))
{
$item
}
}
# $Targetfile | Where-Object {$_.path -eq ($Targetfile.path -eq $temp.path) }
(Get-Date).TimeOfDay.TotalSeconds #检查时间信息
if ($identicalfile.HASH -ne $_.HASH) {
Write-Host ($_.path + " | " + $_.Fileversion) -ForegroundColor Red
}
}
}
试试看运行效果
1
2
3
4
5
6
PS C:\windows\system32> HASHCheckFile C:\localhost.csv C:\localhostold.csv
66598.8235475
66599.847084
66600.8921025
66601.9200949
看起来还是没什么改进。
优化2
仔细观察逻辑
1
2
3
4
5
6
7
8
$Basefile | ForEach-Object {
$temp = $_
$identicalfile = foreach ($item in $Targetfile ) {
if ($item.path -eq ($Targetfile.path -eq $temp.path))
{
$item
}
}
会发现, if ($item.path -eq ($Targetfile.path -eq $temp.path))
中的$Targetfile.path -eq $temp.path
无论再简单,运算再快,还是无可避免的在循环中计算了一次。我们把它存储在变量中,重复调用一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#脚本2
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = (Import-Csv $baselineFile)
$Targetfile = (Import-Csv $targetFile)
$Basefile | ForEach-Object {
$temp = $_
$temppath = $Targetfile.path -eq $temp.path
$identicalfile = foreach ($item in $Targetfile ) {
if ($item.path -eq $temppath) {
$item
}
}
(Get-Date).TimeOfDay.TotalSeconds #检查时间信息
if ($identicalfile.HASH -ne $_.HASH) {
Write-Host ($_.path + " | " + $_.Fileversion) -ForegroundColor Red
}
}
}
肉眼可见的速度飞快起来,执行一条大约需要0.01多一点秒(10ms)
1
2
3
4
PS C:\windows\system32> HASHCheckFile C:\localhost.csv C:\localhostold.csv
66844.7875269
66844.8005286
66844.8115265
再把逻辑里面的管道换成foreach
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#脚本3
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = (Import-Csv $baselineFile)
$Targetfile = (Import-Csv $targetFile)
foreach ($itemnew in $Basefile) {
$temp = $itemnew
$temppath = $Targetfile.path -eq $temp.path
$identicalfile = foreach ($item in $Targetfile ) {
if ($item.path -eq $temppath) {
$item
}
}
(Get-Date).TimeOfDay.TotalSeconds #检查时间信息
if ($identicalfile.HASH -ne $temp.HASH) {
Write-Host ($temp.path + " | " + $temp.Fileversion) -ForegroundColor Red
}
}
}
看起来接近0.006秒了(6ms)
1
2
3
4
5
PS C:\windows\system32> HASHCheckFile C:\localhost.csv C:\localhostold.csv
67540.2396194
67540.2476195
67540.2526189
可能是样本量太少,我将脚本完整跑一次得出结果。
脚本 | 执行时间 |
---|---|
脚本2 | 14.0271825 |
脚本3 | 10.4654441秒 |
脚本1 | 单条执行速度太慢,不参与评比 |
优化3
10秒跑一个1700多文件的对比,基本上可以算是符合要求了,生产中用起来也没问题。不过有没有更快的呢?由于我用的是import-csv
的方式加载的对象,每个对象包含了路径、哈希值以及版本号。
但是对我的需求而言,如果要检查某个文件的哈希值是否匹配
,那么他在(基准文件)中的记录,一定有一条完全一模一样
的数据同时存在于(待查询文件)中,但凡差一个字,都不是真的一样。
基于这个原理,只需要查询一次,不需要先查路径再查哈希值。也就是循环只需要做一次。所以导入数据的时候用GC
进行原样导入,不转换成对象。
1
2
3
4
5
6
7
8
9
10
11
12
#代码4
function HASHCheckFile ($baselineFile, $targetFile) {
Write-Host $_ -ForegroundColor Green
$Basefile = Get-Content $baselineFile
$Targetfile = Get-Content $targetFile
foreach ($item in $Basefile) {
$temppath = $Targetfile -eq $$item
if ([string]$temppath -eq "") {
Write-Host $$item -ForegroundColor Red
}
}
}
看看消耗时间,达到了惊人的0.48秒,把其他方式按在地上摩擦。
1
2
3
0E259FD07FF6D8D38BB529724E965A11037AF4ED260EEF3CB3692B53B8D3BDF4,\Windows\System32\zh-CN\HVDirectAD.ps1,0.0.0.0
F32626EE7F8FF0CB4A7CB5D5026E88F7F9857780CEA1D613B73D722375F8BCEF,\Windows\System32\zh-CN\PS代码格式化临时.ps1,0.0.0.0
0.4824708
当然这种输出格式需要一点点美化,不过和效率比起来也没什么了。
总结
- 不要反复的计算某一件事情,如果计算结果阶段一致,就一定要把它放在变量里。
- 用
foreach
替换管道,可以明显提升效率。需要注意,foreach
和foreach-object
是完全不同的两个东西 - 用蠢萌蠢萌的字符串处理,只要能保证循环的次数少,那效率就是最高的。