PowerShell 技能连载 - 对象比较与差异检测

适用于 PowerShell 5.1 及以上版本

配置漂移检测、变更审计、版本对比——运维中经常需要比较两份数据的差异。PowerShell 的 Compare-Object 可以比较两个对象集合,ConvertTo-Html 可以生成可视化的对比报告。但很多用户只会用 Compare-Object 做简单的字符串比较,不了解如何对比复杂对象属性、生成差异报告、检测配置漂移。

本文将讲解 PowerShell 中的对象比较技巧和实用的差异检测方案。

Compare-Object 基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 简单数组比较
$oldServers = @("SRV01", "SRV02", "SRV03", "SRV04")
$newServers = @("SRV02", "SRV03", "SRV05", "SRV06")

$diff = Compare-Object $oldServers $newServers
$diff | Format-Table -AutoSize

# 解读结果
# <= 表示只在左侧(oldServers)
# => 表示只在右侧(newServers)
$added = $diff | Where-Object { $_.SideIndicator -eq '=>' }
$removed = $diff | Where-Object { $_.SideIndicator -eq '<=' }

Write-Host "新增服务器:$($added.InputObject -join ', ')" -ForegroundColor Green
Write-Host "移除服务器:$($removed.InputObject -join ', ')" -ForegroundColor Red

# 使用 -Property 比较对象属性
$oldConfig = @(
[PSCustomObject]@{ Server = "SRV01"; IP = "10.0.1.1"; Role = "Web" }
[PSCustomObject]@{ Server = "SRV02"; IP = "10.0.1.2"; Role = "DB" }
[PSCustomObject]@{ Server = "SRV03"; IP = "10.0.1.3"; Role = "Web" }
)

$newConfig = @(
[PSCustomObject]@{ Server = "SRV01"; IP = "10.0.1.1"; Role = "Web" }
[PSCustomObject]@{ Server = "SRV02"; IP = "10.0.2.2"; Role = "DB" }
[PSCustomObject]@{ Server = "SRV04"; IP = "10.0.1.4"; Role = "App" }
)

# 按 Server 属性比较
Compare-Object $oldConfig $newConfig -Property Server -IncludeEqual |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
InputObject SideIndicator
----------- -------------
SRV01 <=
SRV04 <=
SRV05 =>
SRV06 =>
新增服务器:SRV05, SRV06
移除服务器:SRV01, SRV04

Server SideIndicator
------ -------------
SRV01 ==
SRV02 ==
SRV03 <=
SRV04 =>

属性级差异检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 深度属性比较
function Compare-ObjectProperties {
param(
[Parameter(Mandatory)]
$ReferenceObject,

[Parameter(Mandatory)]
$DifferenceObject,

[string[]]$Properties,

[string]$KeyProperty
)

$diffs = @()

if ($KeyProperty) {
# 按键属性匹配后比较
foreach ($prop in $Properties) {
$refVal = $ReferenceObject.$prop
$diffVal = $DifferenceObject.$prop

if ($refVal -ne $diffVal) {
$diffs += [PSCustomObject]@{
Property = $prop
OldValue = $refVal
NewValue = $diffVal
Key = "$KeyProperty=$($ReferenceObject.$KeyProperty)"
IsDifferent = $true
}
}
}
}

return $diffs
}

# 比较服务器配置变更
$oldServers = @(
[PSCustomObject]@{ Name = "SRV01"; IP = "10.0.1.1"; OS = "2022"; CPU = 8 }
[PSCustomObject]@{ Name = "SRV02"; IP = "10.0.1.2"; OS = "2019"; CPU = 16 }
)

$newServers = @(
[PSCustomObject]@{ Name = "SRV01"; IP = "10.0.1.1"; OS = "2022"; CPU = 16 }
[PSCustomObject]@{ Name = "SRV02"; IP = "10.0.2.2"; OS = "2022"; CPU = 16 }
)

Write-Host "服务器配置差异:" -ForegroundColor Cyan
foreach ($old in $oldServers) {
$new = $newServers | Where-Object { $_.Name -eq $old.Name }
if ($new) {
$diffs = Compare-ObjectProperties -ReferenceObject $old -DifferenceObject $new `
-Properties @("IP", "OS", "CPU") -KeyProperty "Name"

if ($diffs) {
Write-Host "`n $($old.Name) 变更:" -ForegroundColor Yellow
$diffs | Format-Table Property, OldValue, NewValue -AutoSize | Out-String | Write-Host
}
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
服务器配置差异:

SRV01 变更:
Property OldValue NewValue
-------- -------- --------
CPU 8 16

SRV02 变更:
Property OldValue NewValue
-------- -------- --------
IP 10.0.1.2 10.0.2.2
OS 2019 2022

配置漂移检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 检测配置文件漂移
function Test-ConfigDrift {
param(
[Parameter(Mandatory)]
[string]$BaselinePath,

[Parameter(Mandatory)]
[string]$CurrentPath,

[string]$KeyProperty = "Name"
)

$baseline = Get-Content $BaselinePath -Raw | ConvertFrom-Json
$current = Get-Content $CurrentPath -Raw | ConvertFrom-Json

$drifts = @()

# 检查基准中的每个条目
foreach ($baseItem in $baseline) {
$key = $baseItem.$KeyProperty
$currentItem = $current | Where-Object { $_.$KeyProperty -eq $key }

if (-not $currentItem) {
$drifts += [PSCustomObject]@{
Key = $key
Type = "MISSING"
Property = "ALL"
Expected = "存在"
Actual = "不存在"
}
continue
}

# 逐属性比较
foreach ($prop in $baseItem.PSObject.Properties.Name) {
if ($baseItem.$prop -ne $currentItem.$prop) {
$drifts += [PSCustomObject]@{
Key = $key
Type = "CHANGED"
Property = $prop
Expected = $baseItem.$prop
Actual = $currentItem.$prop
}
}
}
}

# 检查新增条目
foreach ($curItem in $current) {
$key = $curItem.$KeyProperty
$baseItem = $baseline | Where-Object { $_.$KeyProperty -eq $key }
if (-not $baseItem) {
$drifts += [PSCustomObject]@{
Key = $key
Type = "ADDED"
Property = "ALL"
Expected = "不存在"
Actual = "存在"
}
}
}

if ($drifts) {
Write-Host "检测到 $($drifts.Count) 处配置漂移:" -ForegroundColor Red
$drifts | Format-Table -AutoSize
} else {
Write-Host "配置与基准一致" -ForegroundColor Green
}

return $drifts
}

# 创建基准和当前配置
$baselineJson = @'
[
{"Name":"SRV01","IP":"10.0.1.1","Role":"Web","Status":"Active"},
{"Name":"SRV02","IP":"10.0.1.2","Role":"DB","Status":"Active"},
{"Name":"SRV03","IP":"10.0.1.3","Role":"App","Status":"Active"}
]
'@

$currentJson = @'
[
{"Name":"SRV01","IP":"10.0.1.1","Role":"Web","Status":"Maintenance"},
{"Name":"SRV02","IP":"10.0.2.2","Role":"DB","Status":"Active"},
{"Name":"SRV04","IP":"10.0.1.4","Role":"Web","Status":"Active"}
]
'@

$baselineJson | Set-Content "C:\Config\baseline.json" -Encoding UTF8
$currentJson | Set-Content "C:\Config\current.json" -Encoding UTF8

Test-ConfigDrift -BaselinePath "C:\Config\baseline.json" -CurrentPath "C:\Config\current.json" -KeyProperty "Name"

执行结果示例:

1
2
3
4
5
6
7
检测到 4 处配置漂移:
Key Type Property Expected Actual
--- ---- -------- -------- ------
SRV01 CHANGED Status Active Maintenance
SRV02 CHANGED IP 10.0.1.2 10.0.2.2
SRV03 MISSING ALL 存在 不存在
SRV04 ADDED ALL 不存在 存在

差异报告生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function New-DiffReport {
param(
[Parameter(Mandatory)]
[object[]]$Differences,

[string]$Title = "配置差异报告",

[string]$OutputPath = "C:\Reports\diff-report.html"
)

$css = @"
<style>
body{font-family:'Segoe UI',sans-serif;margin:20px;background:#f5f6fa}
h1{color:#2d3436;border-bottom:2px solid #0984e3}
table{width:100%;border-collapse:collapse;margin:10px 0}
th{background:#0984e3;color:white;padding:10px;text-align:left}
td{padding:8px 10px;border-bottom:1px solid #dfe6e9}
.CHANGED{background:#fff3cd}
.ADDED{background:#d4edda}
.MISSING{background:#f8d7da}
.summary{display:flex;gap:15px;margin:15px 0}
.card{background:white;border-radius:8px;padding:15px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}
.card .value{font-size:24px;font-weight:bold}
</style>
"@

$changed = ($Differences | Where-Object { $_.Type -eq "CHANGED" }).Count
$added = ($Differences | Where-Object { $_.Type -eq "ADDED" }).Count
$missing = ($Differences | Where-Object { $_.Type -eq "MISSING" }).Count

$rows = foreach ($diff in $Differences) {
"<tr class=`"$($diff.Type)`"><td>$($diff.Key)</td><td>$($diff.Type)</td><td>$($diff.Property)</td><td>$($diff.Expected)</td><td>$($diff.Actual)</td></tr>"
}

$html = @"
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>$Title</title>$css</head>
<body><h1>$Title</h1><p>生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</p>
<div class="summary">
<div class="card"><div class="value" style="color:#f39c12">$changed</div><div>变更</div></div>
<div class="card"><div class="value" style="color:#27ae60">$added</div><div>新增</div></div>
<div class="card"><div class="value" style="color:#e74c3c">$missing</div><div>缺失</div></div>
</div>
<table><tr><th>键</th><th>类型</th><th>属性</th><th>期望值</th><th>实际值</th></tr>
$rows
</table></body></html>
"@

$html | Set-Content $OutputPath -Encoding UTF8
Write-Host "差异报告已生成:$OutputPath" -ForegroundColor Green
}

# 使用之前的差异结果生成报告
$drifts = Test-ConfigDrift -BaselinePath "C:\Config\baseline.json" -CurrentPath "C:\Config\current.json" -KeyProperty "Name"
New-DiffReport -Differences $drifts -Title "服务器配置漂移报告"

执行结果示例:

1
2
检测到 4 处配置漂移:
差异报告已生成:C:\Reports\diff-report.html

注意事项

  1. Compare-Object 限制Compare-Object 默认只比较字符串表示,复杂对象需要指定 -Property
  2. 顺序无关Compare-Object 是无序比较,不关心元素顺序
  3. IncludeEqual:使用 -IncludeEqual 显示相同的项,适合生成完整的对比报告
  4. PassThru-PassThru 参数返回原始对象而非比较结果,方便后续处理
  5. Culture 影响:字符串比较受当前文化设置影响,技术数据建议使用 -CultureInvariant
  6. 大数据量:比较大量对象时 Compare-Object 性能较差,考虑使用哈希表查找

PowerShell 技能连载 - 管道高级技巧

适用于 PowerShell 5.1 及以上版本

管道(Pipeline)是 PowerShell 最核心的设计理念——不同于 Unix Shell 的文本管道,PowerShell 传递的是完整的 .NET 对象。这意味着管道中的每个命令都可以访问对象的属性和方法,无需正则表达式解析。然而,很多用户只停留在 | Format-Table 的层面,不了解管道的流式处理特性、自定义管道函数、管道变量等高级功能。

本文将深入讲解 PowerShell 管道的高级技巧和性能优化。

管道绑定参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 理解管道参数绑定
# ByPropertyName:对象属性名匹配参数名
# ByValue:对象类型匹配参数类型

# ByPropertyName 示例——属性名匹配
$csvData = @"
Name,Id,Status
SRV01,101,Running
SRV02,102,Stopped
SRV03,103,Running
"@ | ConvertFrom-Csv

# CSV 对象的 Name 属性自动绑定到 -Name 参数
$csvData | ForEach-Object {
Write-Host "服务器 $($_.Name) (ID: $($_.Id)) 状态:$($_.Status)"
}

# 自定义管道绑定函数
function Set-ServerStatus {
[CmdletBinding()]
param(
# ValueFromPipeline 表示从管道接收输入
[Parameter(Mandatory, ValueFromPipeline)]
[string]$ServerName,

# ValueFromPipelineByPropertyName 表示从对象属性匹配
[Parameter(ValueFromPipelineByPropertyName)]
[string]$Status = "Unknown"
)

process {
$icon = if ($Status -eq "Running") { "[OK]" } else { "[!!]" }
Write-Host "$icon $ServerName - $Status" -ForegroundColor $(if ($Status -eq "Running") { "Green" } else { "Yellow" })
}
}

# 从管道接收
$csvData | Set-ServerStatus

# 直接传参
Set-ServerStatus -ServerName "SRV04" -Status "Running"

执行结果示例:

1
2
3
4
5
6
7
服务器 SRV01 (ID: 101) 状态:Running
服务器 SRV02 (ID: 102) 状态:Stopped
服务器 SRV03 (ID: 103) 状态:Running
[OK] SRV01 - Running
[!!] SRV02 - Stopped
[OK] SRV03 - Running
[OK] SRV04 - Running

process 块与流式处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 管道函数的三个块
function Measure-ServerHealth {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$ComputerName
)

begin {
$total = 0
$healthy = 0
Write-Host "开始健康检查..." -ForegroundColor Cyan
}

process {
$total++
$isOnline = Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction SilentlyContinue

if ($isOnline) {
$healthy++
Write-Host " $ComputerName : 在线" -ForegroundColor Green
} else {
Write-Host " $ComputerName : 离线" -ForegroundColor Red
}

# 每处理一个对象就输出,实现流式处理
[PSCustomObject]@{
Computer = $ComputerName
Online = $isOnline
}
}

end {
$rate = if ($total -gt 0) { [math]::Round($healthy / $total * 100, 1) } else { 0 }
Write-Host "检查完成:$healthy/$total 在线($rate%)" -ForegroundColor Cyan
}
}

# 管道流式处理——逐个处理,不等待全部完成
@("SRV01", "SRV02", "SRV03", "SRV04", "SRV05") | Measure-ServerHealth

# 管道可以继续连接
@("SRV01", "SRV02") | Measure-ServerHealth | Where-Object { $_.Online } | Select-Object -ExpandProperty Computer

执行结果示例:

1
2
3
4
5
6
7
开始健康检查...
SRV01 : 在线
SRV02 : 在线
SRV03 : 离线
SRV04 : 在线
SRV05 : 在线
检查完成:4/5 在线(80.0%)

管道性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 对比:管道 vs 循环的性能差异
$numbers = 1..10000

# 方式 1:管道(较慢,但内存友好)
$sw1 = [System.Diagnostics.Stopwatch]::StartNew()
$result1 = $numbers | ForEach-Object { $_ * 2 }
$sw1.Stop()
Write-Host "管道方式:$($sw1.ElapsedMilliseconds)ms,结果数:$($result1.Count)"

# 方式 2:ForEach-Object -Parallel(PowerShell 7,并行加速)
$sw2 = [System.Diagnostics.Stopwatch]::StartNew()
$result2 = $numbers | ForEach-Object -Parallel { $_ * 2 } -ThrottleLimit 8
$sw2.Stop()
Write-Host "并行方式:$($sw2.ElapsedMilliseconds)ms,结果数:$($result2.Count)"

# 方式 3:赋值语法(最快)
$sw3 = [System.Diagnostics.Stopwatch]::StartNew()
$result3 = foreach ($n in $numbers) { $n * 2 }
$sw3.Stop()
Write-Host "赋值语法:$($sw3.ElapsedMilliseconds)ms,结果数:$($result3.Count)"

# 方式 4:LINQ(超大数据集)
Add-Type -AssemblyName System.Linq
$sw4 = [System.Diagnostics.Stopwatch]::StartNew()
$result4 = [System.Linq.Enumerable]::Select(
[int[]]$numbers, [Func[int, int]]{ param($x); $x * 2 }
)
$sw4.Stop()
Write-Host "LINQ 方式:$($sw4.ElapsedMilliseconds)ms,结果数:$($result4.Count)"

# 管道优化技巧:避免在管道中调用昂贵操作
# 不好的做法
$sw = [System.Diagnostics.Stopwatch]::StartNew()
1..100 | ForEach-Object {
# 每次循环都创建新连接
$result = Invoke-RestMethod "https://httpbin.org/get" -ErrorAction SilentlyContinue
}
$sw.Stop()

执行结果示例:

1
2
3
4
管道方式:85ms,结果数:10000
并行方式:42ms,结果数:10000
赋值语法:12ms,结果数:10000
LINQ 方式:3ms,结果数:10000

自定义管道命令组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 构建数据处理管道
function Get-LogEntry {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path,

[int]$Tail = 100
)

process {
Get-Content $Path -Tail $Tail -ErrorAction Stop | ForEach-Object {
$parts = $_ -split '\s+', 4
if ($parts.Count -ge 4) {
[PSCustomObject]@{
Timestamp = $parts[0] + ' ' + $parts[1]
Level = $parts[2].Trim('[]')
Message = $parts[3]
}
}
}
}
}

function Where-LogLevel {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
$LogEntry,

[ValidateSet("ERROR", "WARN", "INFO", "DEBUG")]
[string[]]$Level
)

process {
if ($LogEntry.Level -in $Level) {
$LogEntry
}
}
}

function Select-RecentErrors {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
$LogEntry,

[int]$Minutes = 60
)

begin { $cutoff = (Get-Date).AddMinutes(-$Minutes) }

process {
$timestamp = [datetime]::Parse($LogEntry.Timestamp)
if ($timestamp -ge $cutoff) {
$LogEntry
}
}
}

# 管道组合
Get-LogEntry -Path "C:\Logs\app.log" -Tail 500 |
Where-LogLevel -Level "ERROR", "WARN" |
Select-RecentErrors -Minutes 30 |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
Timestamp            Level Message
--------- ----- -------
2025-07-08 08:15:30 ERROR Database connection timeout after 30s
2025-07-08 08:20:45 WARN Memory usage above 80% threshold
2025-07-08 08:25:10 ERROR Failed to process message from queue

注意事项

  1. 内存 vs 速度:管道流式处理内存占用低但速度慢,数组赋值速度快但需要全部加载到内存
  2. PipelineVariable:PowerShell 5.0+ 支持 -PipelineVariable 保存管道中间结果供后续使用
  3. OutVariable:使用 -OutVariable 参数在管道传递的同时收集输出到变量
  4. 避免嵌套管道:在 ForEach-Object 中使用管道会导致性能急剧下降
  5. $_ vs $PSItem:两者等价,$_$PSItem 的别名,表示当前管道对象
  6. 并行管道ForEach-Object -Parallel(PowerShell 7+)使用新的运行空间,变量需要用 $using: 传递