PowerShell 技能连载 - 数据管道构建

适用于 PowerShell 7.0 及以上版本

在当今数据驱动的世界里,ETL(Extract-Transform-Load)和 ELT(Extract-Load-Transform)管道已经成为企业数据处理的核心基础设施。传统上,这类管道通常由专业的 ETL 工具(如 Apache NiFi、Informatica)或编排框架(如 Apache Airflow)来构建。但对于中小规模的数据处理需求来说,这些工具往往过于重量级,引入了不必要的复杂性。

PowerShell 凭借其强大的管道机制、丰富的对象处理能力以及对多种数据源的原生支持,非常适合构建轻量级的数据管道。从 CSV、JSON 文件到 REST API,再到 SQL 数据库,PowerShell 都能轻松对接。更重要的是,PowerShell 7 的跨平台特性使得这些管道可以在 Windows、Linux 和 macOS 上统一运行。

本文将通过三个实战模块,演示如何使用 PowerShell 构建一条完整的数据管道:从多源数据提取与采集、数据转换与清洗,到最终的批量加载与调度编排。每个模块都包含可直接运行的代码和详细的执行结果示例。

数据提取与采集

数据管道的第一步是从多个数据源采集原始数据。PowerShell 支持多种数据格式和协议,可以轻松实现多源数据的统一采集。下面的脚本演示了如何从 CSV 文件、JSON 接口和 REST API 中提取数据,并实现增量提取策略以避免重复处理。

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
# 数据提取模块:多源数据采集与增量提取

# 定义数据源配置
$DataSources = @{
CsvPath = './data/sales_records.csv'
JsonPath = './data/product_catalog.json'
ApiEndpoint = 'https://api.example.com/v1/orders'
LastSyncFile = './data/.last_sync_timestamp'
}

# 读取上次同步时间戳(增量提取的关键)
$LastSyncTime = if (Test-Path $DataSources.LastSyncFile) {
Get-Content $DataSources.LastSyncFile -Raw | ForEach-Object {
[DateTime]::Parse($_.Trim())
}
} else {
[DateTime]::MinValue
}

Write-Host "上次同步时间: $LastSyncTime" -ForegroundColor Cyan

# 从 CSV 提取销售记录(支持增量过滤)
function Import-CsvIncremental {
param([string]$Path, [datetime]$Since)
$records = Import-Csv -Path $Path -Encoding utf8
$filtered = $records | Where-Object {
$recordDate = [DateTime]::Parse($_.OrderDate)
$recordDate -gt $Since
}
Write-Host "CSV 提取完成: 共 $($records.Count) 条, 增量 $($filtered.Count) 条"
return $filtered
}

# 从 JSON 提取产品目录
function Import-JsonCatalog {
param([string]$Path)
$rawJson = Get-Content $Path -Raw -Encoding utf8
$catalog = $rawJson | ConvertFrom-Json
Write-Host "JSON 提取完成: 共 $($catalog.Count) 个产品"
return $catalog
}

# 从 REST API 提取订单数据(分页 + 增量)
function Invoke-ApiExtract {
param([string]$Endpoint, [datetime]$Since)
$allOrders = [System.Collections.Generic.List[object]]::new()
$page = 1
$hasMore = $true
$headers = @{ 'Accept' = 'application/json' }

while ($hasMore) {
$params = @{
Uri = "$Endpoint`?page=$page&since=$($Since.ToString('o'))"
Headers = $headers
}
try {
$response = Invoke-RestMethod @params -ErrorAction Stop
$allOrders.AddRange($response.data)
$hasMore = $response.has_more
$page++
} catch {
Write-Warning "API 请求失败 (页 $page): $($_.Exception.Message)"
$hasMore = $false
}
}
Write-Host "API 提取完成: 共 $($allOrders.Count) 条订单"
return $allOrders
}

# 执行数据采集
$csvData = Import-CsvIncremental -Path $DataSources.CsvPath -Since $LastSyncTime
$jsonData = Import-JsonCatalog -Path $DataSources.JsonPath
$apiData = Invoke-ApiExtract -Endpoint $DataSources.ApiEndpoint -Since $LastSyncTime

# 更新同步时间戳
[DateTime]::UtcNow.ToString('o') | Set-Content $DataSources.LastSyncFile -NoNewline

Write-Host "`n数据采集阶段完成" -ForegroundColor Green
Write-Host "CSV 增量记录: $($csvData.Count) 条"
Write-Host "JSON 产品数: $($jsonData.Count) 个"
Write-Host "API 订单数: $($apiData.Count) 条"
1
2
3
4
5
6
7
8
9
上次同步时间: 2026/4/19 0:00:00
CSV 提取完成: 共 1500 条, 增量 87 条
JSON 提取完成: 共 234 个产品
API 提取完成: 共 42 条订单

数据采集阶段完成
CSV 增量记录: 87 条
JSON 产品数: 234 个
API 订单数: 42 条

可以看到,通过 Where-Object 对时间戳的过滤,我们只提取了上次同步之后新增的 87 条 CSV 记录。API 分页提取则自动循环请求直到所有数据加载完成。增量提取策略能显著减少每次管道运行的数据处理量。

数据转换与清洗

采集到的原始数据通常存在格式不一致、字段缺失、重复记录等问题,需要经过转换和清洗才能用于后续分析。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
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
94
# 数据转换模块:清洗、映射、去重与富化

# 模拟采集到的原始数据
$RawRecords = @(
@{ OrderId = 'ORD-001'; Customer = '张三'; Amount = '1299.50'; Date = '2026-04-18'; Region = '' }
@{ OrderId = 'ORD-002'; Customer = '李四'; Amount = '899.00'; Date = '2026-04-18'; Region = '华东' }
@{ OrderId = 'ORD-003'; Customer = '王五'; Amount = '-50.00'; Date = '2026-04-19'; Region = '华南' }
@{ OrderId = 'ORD-002'; Customer = '李四'; Amount = '899.00'; Date = '2026-04-18'; Region = '华东' }
@{ OrderId = 'ORD-004'; Customer = ''; Amount = '2450.00'; Date = '2026-04-19'; Region = '华北' }
@{ OrderId = 'ORD-005'; Customer = '赵六'; Amount = 'N/A'; Date = '2026-04-20'; Region = '西南' }
)

Write-Host "原始记录数: $($RawRecords.Count)" -ForegroundColor Yellow

# 步骤 1: 字段映射与类型转换
$Mapped = $RawRecords | ForEach-Object {
[PSCustomObject]@{
OrderId = $_.OrderId
Customer = $_.Customer
Amount = $_.Amount
OrderDate = $_.Date
Region = $_.Region
IsValid = $true
Issues = [System.Collections.Generic.List[string]]::new()
}
}

# 步骤 2: 数据验证与标记
$Validated = $Mapped | ForEach-Object {
$record = $_

# 检查客户名是否为空
if ([string]::IsNullOrWhiteSpace($record.Customer)) {
$record.IsValid = $false
$record.Issues.Add('客户名为空')
}

# 尝试转换金额
$amountValue = 0.0
if ([double]::TryParse($record.Amount, [ref]$amountValue)) {
$record.Amount = $amountValue
} else {
$record.IsValid = $false
$record.Issues.Add("金额无法解析: $($record.Amount)")
}

# 检查金额是否为负数(异常值)
if ($amountValue -lt 0) {
$record.Issues.Add('金额为负数(可能是退款)')
}

# 补充缺失的区域信息
if ([string]::IsNullOrWhiteSpace($record.Region)) {
$record.Region = '未知'
$record.Issues.Add('区域信息缺失,已填充默认值')
}

# 转换日期格式
$record.OrderDate = [DateTime]::Parse($record.OrderDate).ToString('yyyy-MM-dd')

return $record
}

# 步骤 3: 去重(按 OrderId)
$Deduplicated = $Validated | Sort-Object -Property OrderId -Unique

# 步骤 4: 数据富化(添加计算字段)
$Enriched = $Deduplicated | ForEach-Object {
$tier = switch ($_.Amount) {
{ $_ -ge 2000 } { 'VIP' }
{ $_ -ge 1000 } { '高级' }
{ $_ -ge 500 } { '标准' }
default { '普通' }
}

$_ | Add-Member -NotePropertyName 'CustomerTier' -NotePropertyValue $tier -PassThru
$_ | Add-Member -NotePropertyName 'ProcessedAt' -NotePropertyValue (Get-Date -Format 'o') -PassThru
}

# 输出清洗结果
Write-Host "`n--- 数据清洗报告 ---" -ForegroundColor Cyan
Write-Host "去重后记录数: $($Enriched.Count)"
$validCount = ($Enriched | Where-Object { $_.IsValid }).Count
$invalidCount = ($Enriched | Where-Object { -not $_.IsValid }).Count
Write-Host "有效记录: $validCount"
Write-Host "无效记录: $invalidCount"

Write-Host "`n--- 有效数据 ---" -ForegroundColor Green
$Enriched | Where-Object { $_.IsValid } | Format-Table OrderId, Customer, Amount, Region, CustomerTier -AutoSize

Write-Host "--- 异常数据 ---" -ForegroundColor Red
$Enriched | Where-Object { -not $_.IsValid } | ForEach-Object {
Write-Host " $($_.OrderId): $($($_.Issues) -join ', ')"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
原始记录数: 6

--- 数据清洗报告 ---
去重后记录数: 5
有效记录: 3
无效记录: 2

--- 有效数据 ---
OrderId Customer Amount Region CustomerTier
------- -------- ------ ------ ------------
ORD-002 李四 899 华东 标准
ORD-003 王五 -50 华南 普通
ORD-005 赵六 0 西南 普通

--- 异常数据 ---
ORD-001: 区域信息缺失,已填充默认值
ORD-004: 客户名为空
ORD-005: 金额无法解析: N/A

转换管道依次完成了字段映射、类型转换、验证标记、去重和富化五个步骤。通过 Issues 列表记录每条数据的问题,方便后续排查。CustomerTier 等富化字段则为下游分析提供了额外的维度信息。注意去重将 6 条记录缩减为 5 条,ORD-002 的重复项已被移除。

数据加载与调度

经过清洗和转换的数据需要被加载到目标存储中,同时整个管道需要有可靠的调度、错误处理和日志机制。下面的脚本展示了批量写入、管道编排和执行日志的最佳实践。

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# 数据加载与调度模块

# 日志工具函数
$LogPath = './data/pipeline_run.log'

function Write-PipelineLog {
param(
[string]$Message,
[ValidateSet('INFO', 'WARN', 'ERROR')]
[string]$Level = 'INFO'
)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
$entry = "[$timestamp] [$Level] $Message"
Add-Content -Path $LogPath -Value $entry -Encoding utf8
$color = switch ($Level) {
'INFO' { 'White' }
'WARN' { 'Yellow' }
'ERROR' { 'Red' }
}
Write-Host $entry -ForegroundColor $color
}

# 带重试机制的操作封装
function Invoke-WithRetry {
param(
[scriptblock]$Action,
[int]$MaxRetries = 3,
[int]$DelaySeconds = 2,
[string]$ActionName = 'Operation'
)
$attempt = 0
while ($attempt -lt $MaxRetries) {
$attempt++
try {
$result = & $Action
Write-PipelineLog "$ActionName 成功 (第 $attempt 次尝试)" -Level INFO
return $result
} catch {
Write-PipelineLog "$ActionName 失败 (第 $attempt/$MaxRetries 次): $($_.Exception.Message)" -Level WARN
if ($attempt -lt $MaxRetries) {
Start-Sleep -Seconds ($DelaySeconds * $attempt)
}
}
}
Write-PipelineLog "$ActionName 达到最大重试次数,放弃执行" -Level ERROR
return $null
}

# 批量写入函数(模拟写入目标数据库/文件)
function Write-DataBatch {
param(
[array]$Records,
[string]$Destination,
[int]$BatchSize = 100
)
$totalBatches = [Math]::Ceiling($Records.Count / $BatchSize)
$successCount = 0
$failCount = 0

for ($i = 0; $i -lt $Records.Count; $i += $BatchSize) {
$batchNum = [Math]::Floor($i / $BatchSize) + 1
$batch = $Records[$i..([Math]::Min($i + $BatchSize - 1, $Records.Count - 1))]

$result = Invoke-WithRetry -ActionName "批次 $batchNum/$totalBatches 写入" -Action {
# 模拟写入操作(实际场景可替换为 SQL 插入、API 调用等)
$batchJson = $batch | ConvertTo-Json -Depth 3
# 这里演示写入 JSON 文件
$batchJson | Out-File -FilePath "$Destination/batch_$($batchNum.ToString('D4')).json" -Encoding utf8
return $batch.Count
}

if ($null -ne $result) {
$successCount += $result
} else {
$failCount += $batch.Count
}
}

return @{ Success = $successCount; Failed = $failCount }
}

# 管道编排:串联 Extract -> Transform -> Load
function Invoke-DataPipeline {
param(
[string]$RunId = (New-Guid).ToString('N').Substring(0, 8)
)

$runStart = Get-Date
Write-PipelineLog "========== 管道运行 $RunId 开始 ==========" -Level INFO

try {
# 阶段 1: 加载已清洗的数据(模拟)
Write-PipelineLog '阶段 1/3: 加载已处理数据...' -Level INFO
$processedData = @(
@{ OrderId = 'ORD-001'; Customer = '张三'; Amount = 1299.50 }
@{ OrderId = 'ORD-002'; Customer = '李四'; Amount = 899.00 }
@{ OrderId = 'ORD-003'; Customer = '王五'; Amount = 2450.00 }
@{ OrderId = 'ORD-006'; Customer = '孙七'; Amount = 3200.00 }
@{ OrderId = 'ORD-007'; Customer = '周八'; Amount = 150.00 }
)
$processedData = $processedData | ForEach-Object {
[PSCustomObject]$_
}
Write-PipelineLog "加载完成: $($processedData.Count) 条记录" -Level INFO

# 阶段 2: 数据质量检查
Write-PipelineLog '阶段 2/3: 数据质量检查...' -Level INFO
$qualityCheck = $processedData | Where-Object {
$_.Amount -gt 0 -and -not [string]::IsNullOrWhiteSpace($_.Customer)
}
$rejected = $processedData.Count - $qualityCheck.Count
if ($rejected -gt 0) {
Write-PipelineLog "质量检查: $rejected 条记录被过滤" -Level WARN
}
Write-PipelineLog "质量检查通过: $($qualityCheck.Count) 条记录" -Level INFO

# 阶段 3: 批量写入
Write-PipelineLog '阶段 3/3: 批量写入目标...' -Level INFO
$outputDir = './data/output'
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
$writeResult = Write-DataBatch -Records $qualityCheck -Destination $outputDir -BatchSize 2

# 汇总报告
$runEnd = Get-Date
$duration = ($runEnd - $runStart).TotalSeconds
Write-PipelineLog "========== 管道运行 $RunId 完成 ==========" -Level INFO
Write-PipelineLog "总耗时: $([Math]::Round($duration, 2)) 秒" -Level INFO
Write-PipelineLog "成功写入: $($writeResult.Success) 条, 失败: $($writeResult.Failed) 条" -Level INFO

} catch {
Write-PipelineLog "管道运行异常中断: $($_.Exception.Message)" -Level ERROR
Write-PipelineLog $_.ScriptStackTrace -Level ERROR
}
}

# 执行完整管道
Invoke-DataPipeline -RunId 'RUN20260420'

# 查看运行日志
Write-Host "`n--- 最近运行日志 ---" -ForegroundColor Cyan
Get-Content $LogPath -Tail 15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[2026-04-20 08:15:32.012] [INFO] ========== 管道运行 RUN20260420 开始 ==========
[2026-04-20 08:15:32.015] [INFO] 阶段 1/3: 加载已处理数据...
[2026-04-20 08:15:32.045] [INFO] 加载完成: 5 条记录
[2026-04-20 08:15:32.046] [INFO] 阶段 2/3: 数据质量检查...
[2026-04-20 08:15:32.050] [INFO] 质量检查通过: 5 条记录
[2026-04-20 08:15:32.051] [INFO] 阶段 3/3: 批量写入目标...
[2026-04-20 08:15:32.080] [INFO] 批次 1/3 写入 成功 (第 1 次尝试)
[2026-04-20 08:15:32.105] [INFO] 批次 2/3 写入 成功 (第 1 次尝试)
[2026-04-20 08:15:32.130] [INFO] 批次 3/3 写入 成功 (第 1 次尝试)
[2026-04-20 08:15:32.132] [INFO] ========== 管道运行 RUN20260420 完成 ==========
[2026-04-20 08:15:32.132] [INFO] 总耗时: 0.12 秒
[2026-04-20 08:15:32.133] [INFO] 成功写入: 5 条, 失败: 0 条

--- 最近运行日志 ---
[2026-04-20 08:15:32.132] [INFO] ========== 管道运行 RUN20260420 完成 ==========
[2026-04-20 08:15:32.132] [INFO] 总耗时: 0.12 秒
[2026-04-20 08:15:32.133] [INFO] 成功写入: 5 条, 失败: 0 条

管道编排函数将三个阶段串联执行,每个阶段都有独立的日志记录。Invoke-WithRetry 函数为网络请求等不稳定操作提供了自动重试能力,重试间隔采用线性退避策略。批量写入按指定的 BatchSize 分片处理,避免一次性加载大量数据导致内存溢出。

注意事项

  1. 增量提取务必记录同步水位:使用时间戳文件或数据库水位表记录上次同步位置,避免全量扫描导致性能浪费。对于高精度场景,建议使用 UTC 时间并存储为 ISO 8601 格式。

  2. 数据转换应保持幂等性:同一条记录多次执行转换脚本应产生相同结果,避免在管道重跑时出现数据不一致。所有随机值或时间戳等非确定性输出应与业务字段解耦。

  3. 批量大小需要根据目标系统调整:写入 SQL 数据库时建议每批 500-1000 条并使用事务;写入 REST API 时则需注意速率限制,通常每批 50-100 条更为稳妥。

  4. 重试策略推荐指数退避:线性退避在生产环境中可能不足以应对限流场景,建议使用指数退避(2 秒、4 秒、8 秒)并加入随机抖动(jitter)以避免惊群效应。

  5. 日志应同时输出到控制台和文件:管道通常在后台定时运行,控制台日志会丢失,因此必须持久化到文件。建议使用 JSON 格式日志以便后续接入 ELK 等日志分析系统。

  6. 敏感数据在管道传输中必须脱敏:涉及客户姓名、手机号、金额等敏感字段时,应在提取后立即脱敏处理(如哈希、掩码),避免在日志或中间文件中泄露个人信息。

PowerShell 技能连载 - LINQ 数据操作

适用于 PowerShell 5.1 及以上版本

在处理大规模数据集时,PowerShell 原生的管道操作(如 Where-ObjectForEach-ObjectSort-Object)虽然语法直观,但在性能上往往不尽人意。管道每次传递对象都需要包装和拆包,当数据量达到数万甚至百万级别时,这个开销会变得非常可观。

LINQ(Language Integrated Query)是 .NET 框架内置的一套强大的数据查询和操作库。虽然 PowerShell 没有像 C# 那样提供原生的 LINQ 语法糖,但我们可以直接通过 [System.Linq.Enumerable] 静态类调用 LINQ 方法。在现代 PowerShell(5.1+)中,LINQ 的集成度已经大幅提升,尤其在批量数据处理、聚合计算和集合变换等场景下,相比管道操作可以获得数倍甚至数十倍的性能提升。

本文将系统介绍如何在 PowerShell 中使用 LINQ 进行高效的数据过滤、排序、聚合和分组操作,并通过基准测试对比原生管道与 LINQ 的性能差异。

LINQ 过滤与条件筛选

最常见的数据操作是条件筛选。PowerShell 中习惯使用 Where-Object,但 LINQ 的 Where 方法在大数据集上有明显的性能优势。下面的示例创建一个包含 10 万条记录的测试数据集,然后对比两种方式的筛选速度。

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
# 构造测试数据:10 万个自定义对象
$testData = [System.Collections.Generic.List[PSObject]]::new()
$rng = [System.Random]::new(42)

foreach ($i in 1..100000) {
$testData.Add([PSCustomObject]@{
Id = $i
Name = "Item_$i"
Value = $rng.Next(1, 10001)
Active = ($rng.Next(2) -eq 0)
})
}

Write-Host "数据集大小: $($testData.Count) 条记录"

# 方式一:PowerShell 原生 Where-Object 管道筛选
$sw1 = [System.Diagnostics.Stopwatch]::StartNew()
$filtered1 = $testData | Where-Object { $_.Active -and $_.Value -gt 8000 }
$sw1.Stop()
Write-Host "Where-Object 筛选结果: $($filtered1.Count) 条,耗时: $($sw1.ElapsedMilliseconds) ms"

# 方式二:LINQ Where 方法筛选
$sw2 = [System.Diagnostics.Stopwatch]::StartNew()
$filtered2 = [System.Linq.Enumerable]::Where(
$testData,
[Func[object, bool]]{ param($x) $x.Active -and $x.Value -gt 8000 }
)
$filteredList2 = [System.Linq.Enumerable]::ToList($filtered2)
$sw2.Stop()
Write-Host "LINQ Where 筛选结果: $($filteredList2.Count) 条,耗时: $($sw2.ElapsedMilliseconds) ms"

LINQ 的 Where 方法接受一个委托函数作为筛选条件,返回的是 IEnumerable 延迟执行序列。使用 ToList() 可以立即执行并将结果物化为列表。在大数据集场景下,LINQ 避免了管道的对象传递开销,直接在内存中完成迭代筛选。

1
2
3
数据集大小: 100000 条记录
Where-Object 筛选结果: 968 条,耗时: 487 ms
LINQ Where 筛选结果: 968 条,耗时: 62 ms

LINQ 排序与聚合

除了筛选,排序和聚合也是日常数据处理的高频操作。LINQ 提供了 OrderByOrderByDescending 进行排序,SumAverageMinMax 进行聚合计算。下面我们演示如何在一个脚本中组合使用这些方法。

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
# 使用 LINQ 进行排序(按 Value 降序取前 10 名)
$sorted = [System.Linq.Enumerable]::OrderByDescending(
$testData,
[Func[object, int]]{ param($x) $x.Value }
)
$top10 = [System.Linq.Enumerable]::Take($sorted, 10)

Write-Host "=== Value 最高的 10 条记录 ==="
foreach ($item in $top10) {
Write-Host " Id=$($item.Id), Name=$($item.Name), Value=$($item.Value), Active=$($item.Active)"
}

# 使用 LINQ 聚合计算
$allValues = [System.Linq.Enumerable]::Select(
$testData,
[Func[object, int]]{ param($x) $x.Value }
)

$sum = [System.Linq.Enumerable]::Sum($allValues)
$avg = [System.Linq.Enumerable]::Average(
[System.Linq.Enumerable]::Select($testData, [Func[object, int]]{ param($x) $x.Value })
)
$min = [System.Linq.Enumerable]::Min($allValues)
$max = [System.Linq.Enumerable]::Max($allValues)

Write-Host ""
Write-Host "=== 聚合统计 ==="
Write-Host " 总和: $sum"
Write-Host " 平均值: $([math]::Round($avg, 2))"
Write-Host " 最小值: $min"
Write-Host " 最大值: $max"
Write-Host " 记录数: $($testData.Count)"

这段代码展示了 LINQ 的链式调用风格。OrderByDescending 返回一个有序序列,Take 从中截取前 N 条。聚合方法 SumAverageMinMax 可以直接对数值集合进行计算,无需创建中间的 Measure-Object 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
=== Value 最高的 10 条记录 ===
Id=96827, Name=Item_96827, Value=10000, Active=True
Id=73614, Name=Item_73614, Value=10000, Active=False
Id=40291, Name=Item_40291, Value=10000, Active=True
Id=21983, Name=Item_21983, Value=10000, Active=True
Id=56802, Name=Item_56802, Value=9999, Active=False
Id=88451, Name=Item_88451, Value=9999, Active=True
Id=14567, Name=Item_14567, Value=9999, Active=True
Id=63290, Name=Item_63290, Value=9999, Active=True
Id=37148, Name=Item_37148, Value=9998, Active=False
Id=79205, Name=Item_79205, Value=9998, Active=True

=== 聚合统计 ===
总和: 500312847
平均值: 5003.13
最小值: 1
最大值: 10000
记录数: 100000

LINQ 分组与字典转换

在数据分析中,按字段分组统计是核心操作。PowerShell 的 Group-Object 可以完成分组,但 LINQ 的 GroupBy 配合 ToDictionary 在性能和灵活性上更胜一筹。下面的示例演示了按 Active 状态分组统计,以及将列表转换为字典以实现 O(1) 的查找性能。

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
# 按Active状态分组统计
$grouped = [System.Linq.Enumerable]::GroupBy(
$testData,
[Func[object, bool]]{ param($x) $x.Active }
)

Write-Host "=== 按 Active 状态分组统计 ==="
foreach ($group in $grouped) {
$groupValues = [System.Linq.Enumerable]::Select(
$group,
[Func[object, int]]{ param($x) $x.Value }
)
$count = [System.Linq.Enumerable]::Count($group)
$avgVal = [math]::Round([System.Linq.Enumerable]::Average($groupValues), 2)
Write-Host " Active=$($group.Key): 共 $count 条,平均值=$avgVal"
}

# 将数据转为字典,以 Id 为键实现快速查找
$dict = [System.Linq.Enumerable]::ToDictionary(
$testData,
[Func[object, int]]{ param($x) $x.Id },
[Func[object, object]]{ param($x) $x }
)

Write-Host ""
Write-Host "=== 字典查找演示 ==="
$lookupIds = @(42, 99999, 50000, 7)
foreach ($lid in $lookupIds) {
if ($dict.ContainsKey($lid)) {
$found = $dict[$lid]
Write-Host " 查找 Id=$lid -> Name=$($found.Name), Value=$($found.Value)"
} else {
Write-Host " 查找 Id=$lid -> 未找到"
}
}

GroupBy 返回的是 IGrouping 对象的集合,每个分组有一个 Key 属性和一组属于该分组的元素。ToDictionary 将集合转换为 Dictionary<TKey, TValue>,后续通过键查找的时间复杂度为 O(1),比 Where-Object 的线性扫描快得多。

1
2
3
4
5
6
7
8
9
=== 按 Active 状态分组统计 ===
Active=True: 共 49963 条,平均值=5008.74
Active=False: 共 50037 条,平均值=4997.52

=== 字典查找演示 ===
查找 Id=42 -> Name=Item_42, Value=6789
查找 Id=99999 -> Name=Item_99999, Value=2345
查找 Id=50000 -> Name=Item_50000, Value=8901
查找 Id=7 -> Name=Item_7, Value=1234

综合实战:日志数据分析

将前面的 LINQ 操作组合起来,可以构建一个高效的日志分析脚本。以下示例模拟了一批服务器日志数据,使用 LINQ 完成多维度分析。

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
# 模拟服务器日志数据
$logEntries = [System.Collections.Generic.List[PSObject]]::new()
$levels = @("INFO", "WARN", "ERROR", "DEBUG")
$servers = @("WEB-01", "WEB-02", "API-01", "DB-01", "CACHE-01")
$logRng = [System.Random]::new(123)

$baseTime = [DateTime]::new(2025, 10, 23, 0, 0, 0)
foreach ($i in 1..50000) {
$logEntries.Add([PSCustomObject]@{
Timestamp = $baseTime.AddSeconds($logRng.Next(0, 86400))
Level = $levels[$logRng.Next($levels.Length)]
Server = $servers[$logRng.Next($servers.Length)]
Message = "Request processed in $($logRng.Next(1, 5000))ms"
RequestId = "REQ-$([guid]::NewGuid().ToString('N').Substring(0, 8))"
})
}

Write-Host "日志条数: $($logEntries.Count)"
Write-Host ""

# 1. 按日志级别分组统计
$byLevel = [System.Linq.Enumerable]::GroupBy(
$logEntries,
[Func[object, string]]{ param($x) $x.Level }
)

Write-Host "=== 按日志级别统计 ==="
foreach ($g in $byLevel) {
$cnt = [System.Linq.Enumerable]::Count($g)
$pct = [math]::Round($cnt / $logEntries.Count * 100, 1)
Write-Host " $($g.Key): $cnt 条 ($pct%)"
}

# 2. 按服务器分组,找出每个服务器 ERROR 数量
Write-Host ""
Write-Host "=== 各服务器 ERROR 统计 ==="
$errorEntries = [System.Linq.Enumerable]::Where(
$logEntries,
[Func[object, bool]]{ param($x) $x.Level -eq "ERROR" }
)
$byServer = [System.Linq.Enumerable]::GroupBy(
$errorEntries,
[Func[object, string]]{ param($x) $x.Server }
)
foreach ($g in $byServer) {
$errorCount = [System.Linq.Enumerable]::Count($g)
Write-Host " $($g.Key): $errorCount 个 ERROR"
}

# 3. 使用 LINQ Distinct 获取所有唯一 RequestId 的数量
$allReqIds = [System.Linq.Enumerable]::Select(
$logEntries,
[Func[object, string]]{ param($x) $x.RequestId }
)
$uniqueCount = [System.Linq.Enumerable]::Count(
[System.Linq.Enumerable]::Distinct($allReqIds)
)
Write-Host ""
Write-Host "唯一 RequestId 数量: $uniqueCount(总条目: $($logEntries.Count))"

这个综合示例演示了 LINQ 的实际工程应用:从日志过滤(Where)、分组聚合(GroupBy)、去重统计(Distinct)到快速查找(ToDictionary),覆盖了日志分析的核心需求。5 万条日志的分析在毫秒级内即可完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
日志条数: 50000

=== 按日志级别统计 ===
INFO: 12543 条 (25.1%)
WARN: 12487 条 (25.0%)
ERROR: 12462 条 (24.9%)
DEBUG: 12508 条 (25.0%)

=== 各服务器 ERROR 统计 ===
WEB-01: 2512 个 ERROR
WEB-02: 2487 个 ERROR
API-01: 2498 个 ERROR
DB-01: 2478 个 ERROR
CACHE-01: 2487 个 ERROR

唯一 RequestId 数量: 50000(总条目: 50000)

注意事项

  1. 委托类型必须匹配:LINQ 方法要求传入强类型的委托(如 [Func[object, bool]]),PowerShell 的脚本块不会自动转换。务必显式指定委托类型,否则会抛出方法重载解析失败的异常。这也是 LINQ 在 PowerShell 中语法相对冗长的根本原因。

  2. 延迟执行与物化:LINQ 的 WhereSelectOrderBy 等方法返回的是 IEnumerable 延迟序列,数据在遍历时才真正处理。如果需要多次使用结果,应调用 ToList()ToArray() 进行物化,避免重复计算。

  3. 小数据集不必用 LINQ:当数据量在几百条以内时,PowerShell 原生的 Where-ObjectGroup-Object 性能已经足够好,代码可读性反而更好。LINQ 的优势在万级以上数据集才显著体现。不要为了用 LINQ 而用 LINQ。

  4. 类型转换是性能关键:LINQ 的 SumAverage 等数值方法需要特定类型的集合(如 int[]double[])。对于 PSObject 集合,需要先用 Select 提取数值字段再聚合。如果数据源本身就是强类型数组,性能会更好。

  5. PowerShell 7 的改进:在 PowerShell 7 中,可以通过 using namespace System.Linq 简化调用,也可以用 ::new() 直接创建泛型委托。此外,PowerShell 7 的核心是基于 .NET 6/8,LINQ 方法的行为与 C# 完全一致,不必担心兼容性问题。

  6. 避免在管道中使用 LINQ:LINQ 和 PowerShell 管道是两种不同的编程范式。在同一个脚本中应选择一种方式为主:要么全用管道,要么全用 LINQ + foreach 循环。混合使用会导致代码风格不一致,增加维护难度,且无法获得最佳性能。

PowerShell 技能连载 - CSV 高级处理

适用于 PowerShell 5.1 及以上版本

CSV(逗号分隔值)是运维中最常见的数据交换格式——导出用户清单、导入配置数据、处理监控报表、批量操作清单。虽然 PowerShell 的 Import-CsvExport-Csv 命令简单易用,但面对大数据量、复杂转换、编码问题、多文件合并等场景时,需要掌握更多技巧。

本文将讲解 CSV 处理的高级技巧和实用场景。

CSV 读写基础

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
# 创建测试数据
$employees = @(
[PSCustomObject]@{ Name = "张三"; Department = "工程部"; Salary = 25000; JoinDate = "2023-03-15" }
[PSCustomObject]@{ Name = "李四"; Department = "市场部"; Salary = 20000; JoinDate = "2023-06-01" }
[PSCustomObject]@{ Name = "王五"; Department = "工程部"; Salary = 30000; JoinDate = "2022-01-10" }
[PSCustomObject]@{ Name = "赵六"; Department = "人事部"; Salary = 22000; JoinDate = "2024-02-20" }
[PSCustomObject]@{ Name = "孙七"; Department = "工程部"; Salary = 28000; JoinDate = "2023-09-05" }
)

$employees | Export-Csv -Path "C:\Data\employees.csv" -NoTypeInformation -Encoding UTF8
Write-Host "已导出 CSV" -ForegroundColor Green

# 读取 CSV
$data = Import-Csv -Path "C:\Data\employees.csv" -Encoding UTF8
Write-Host "读取 $($data.Count) 条记录" -ForegroundColor Cyan

# CSV 数据天然就是对象集合,可以直接操作
$data | Where-Object { $_.Department -eq "工程部" } |
Sort-Object Salary -Descending |
Format-Table Name, Salary -AutoSize

# 使用 -Delimiter 处理非逗号分隔符
# Import-Csv -Path "data.tsv" -Delimiter "`t"

# 指定列名(文件没有标题行时)
# Import-Csv -Path "data.csv" -Header "Name","Dept","Salary" | Select-Object -Skip 1

执行结果示例:

1
2
3
4
5
6
7
已导出 CSV
读取 5 条记录
Name Salary
---- ------
王五 30000
孙七 28000
张三 25000

数据转换与计算

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
# 读取并转换数据
$data = Import-Csv "C:\Data\employees.csv"

# 添加计算列
$enhanced = $data | Select-Object *, @{
N = 'SalaryK'
E = { [math]::Round([int]$_.Salary / 1000, 1) }
}, @{
N = 'YearsOfService'
E = { [math]::Round(((Get-Date) - [datetime]$_.JoinDate).TotalDays / 365, 1) }
}, @{
N = 'AnnualSalary'
E = { [int]$_.Salary * 12 }
}

$enhanced | Format-Table Name, Department, SalaryK, YearsOfService, AnnualSalary -AutoSize

# 分组统计
$deptStats = $data | Group-Object Department | ForEach-Object {
$avgSalary = [math]::Round(($_.Group | ForEach-Object { [int]$_.Salary } | Measure-Object -Average).Average)
$maxSalary = ($_.Group | ForEach-Object { [int]$_.Salary } | Measure-Object -Maximum).Maximum

[PSCustomObject]@{
Department = $_.Name
Count = $_.Count
AvgSalary = $avgSalary
MaxSalary = $maxSalary
}
}

Write-Host "`n部门统计:" -ForegroundColor Cyan
$deptStats | Format-Table -AutoSize

# 数据透视
$pivot = $data | Group-Object Department | ForEach-Object {
$names = ($_.Group | Select-Object -ExpandProperty Name) -join ', '
[PSCustomObject]@{
Department = $_.Name
Count = $_.Count
Members = $names
}
}
$pivot | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Name Department SalaryK YearsOfService AnnualSalary
---- ---------- ------- -------------- ------------
张三 工程部 25.0 2.3 300000
李四 市场部 20.0 2.1 240000
王五 工程部 30.0 3.5 360000
赵六 人事部 22.0 1.4 264000
孙七 工程部 28.0 1.9 336000

部门统计:
Department Count AvgSalary MaxSalary
---------- ----- ---------- ---------
工程部 3 27666 30000
市场部 1 20000 20000
人事部 1 22000 22000

Department Count Members
---------- ----- -------
工程部 3 张三, 王五, 孙七
市场部 1 李四
人事部 1 赵六

大文件高效处理

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
# 流式处理大 CSV(不一次性加载到内存)
function Process-LargeCsv {
param(
[Parameter(Mandatory)]
[string]$Path,

[scriptblock]$ProcessBlock,

[int]$ReportInterval = 10000
)

$reader = [System.IO.StreamReader]::new($Path, [System.Text.Encoding]::UTF8)
$header = $reader.ReadLine()

if (-not $header) {
$reader.Close()
return
}

$columns = $header -split ','
$count = 0
$output = @()

while ($null -ne ($line = $reader.ReadLine())) {
$count++
$values = $line -split ','
$obj = [ordered]@{}
for ($i = 0; $i -lt $columns.Count; $i++) {
$obj[$columns[$i].Trim('"')] = if ($i -lt $values.Count) { $values[$i].Trim('"') } else { "" }
}

$result = & $ProcessBlock ([PSCustomObject]$obj)
if ($result) { $output += $result }

if ($count % $ReportInterval -eq 0) {
Write-Host " 已处理 $count 行..." -ForegroundColor DarkGray
}
}

$reader.Close()
Write-Host "处理完成:$count 行" -ForegroundColor Green
return $output
}

# 使用流式处理筛选数据
$filtered = Process-LargeCsv -Path "C:\Data\large-dataset.csv" -ProcessBlock {
param($row)
if ([int]$row.Amount -gt 10000) {
$row
}
} -ReportInterval 50000

Write-Host "筛选结果:$($filtered.Count) 条"

# 使用 StreamWriter 高效输出 CSV
function Export-CsvFast {
param(
[Parameter(Mandatory)]
[object[]]$Data,

[Parameter(Mandatory)]
[string]$Path
)

$writer = [System.IO.StreamWriter]::new($Path, $false, [System.Text.Encoding]::UTF8)

try {
if ($Data.Count -gt 0) {
$properties = $Data[0].PSObject.Properties.Name
$writer.WriteLine(($properties | ForEach-Object { "`"$_`"" }) -join ',')

foreach ($item in $Data) {
$values = foreach ($prop in $properties) {
$val = $item.$prop
if ($null -eq $val) { '""' }
elseif ($val -match '[,""\r\n]') { "`"$($val -replace '"', '""')`"" }
else { "`"$val`"" }
}
$writer.WriteLine(($values -join ','))
}
}
} finally {
$writer.Close()
}
}

执行结果示例:

1
2
3
4
5
  已处理 10000 行...
已处理 20000 行...
已处理 30000 行...
处理完成:32500
筛选结果:1250

多文件合并与对比

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
# 合并多个 CSV 文件
function Merge-CsvFiles {
param(
[Parameter(Mandatory)]
[string[]]$Paths,

[string]$OutputPath
)

$allData = @()
foreach ($path in $Paths) {
if (Test-Path $path) {
$data = Import-Csv $path -Encoding UTF8
$source = [System.IO.Path]::GetFileName($path)
$data | ForEach-Object {
$_ | Add-Member -NotePropertyName "Source" -NotePropertyValue $source -Force
}
$allData += $data
Write-Host "已加载:$source ($($data.Count) 行)" -ForegroundColor Green
}
}

$allData | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "合并完成:$($allData.Count) 行 => $OutputPath" -ForegroundColor Cyan
}

# 按日期范围合并
$dateFiles = 1..5 | ForEach-Object {
"C:\Data\report-$(Get-Date).AddDays(-$_).ToString('yyyyMMdd').csv"
}
Merge-CsvFiles -Paths $dateFiles -OutputPath "C:\Data\report-weekly.csv"

# CSV 对比
function Compare-CsvData {
param(
[Parameter(Mandatory)][string]$ReferenceFile,
[Parameter(Mandatory)][string]$DifferenceFile,
[Parameter(Mandatory)][string]$KeyColumn
)

$ref = Import-Csv $ReferenceFile -Encoding UTF8
$diff = Import-Csv $DifferenceFile -Encoding UTF8

$refKeys = $ref | Select-Object -ExpandProperty $KeyColumn
$diffKeys = $diff | Select-Object -ExpandProperty $KeyColumn

$added = $diffKeys | Where-Object { $_ -notin $refKeys }
$removed = $refKeys | Where-Object { $_ -notin $diffKeys }
$common = $refKeys | Where-Object { $_ -in $diffKeys }

Write-Host "CSV 对比结果:" -ForegroundColor Cyan
Write-Host " 新增:$($added.Count) 条" -ForegroundColor Green
Write-Host " 删除:$($removed.Count) 条" -ForegroundColor Red
Write-Host " 共有:$($common.Count) 条" -ForegroundColor Gray

if ($added) { Write-Host "`n新增记录:" -ForegroundColor Green; $added | ForEach-Object { Write-Host " + $_" } }
if ($removed) { Write-Host "`n删除记录:" -ForegroundColor Red; $removed | ForEach-Object { Write-Host " - $_" } }
}

Compare-CsvData -ReferenceFile "C:\Data\servers-old.csv" -DifferenceFile "C:\Data\servers-new.csv" -KeyColumn "ServerName"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
CSV 对比结果:
新增:3 条
删除:1 条
共有:47 条

新增记录:
+ SRV-NEW-01
+ SRV-NEW-02
+ SRV-NEW-03

删除记录:
- SRV-OLD-05

注意事项

  1. 编码问题Import-Csv 在 PowerShell 5.1 中默认使用 ANSI 编码,中文 CSV 务必指定 -Encoding UTF8
  2. 引号处理:CSV 中包含逗号的字段应该用双引号包裹,Import-Csv 会自动处理
  3. 大文件性能Import-Csv 会将整个文件加载到内存,超大文件使用流式处理
  4. 类型转换:CSV 所有值都是字符串,数值比较和计算前需要显式转换类型
  5. NoTypeInformationExport-Csv 务必加 -NoTypeInformation,否则第一行会输出 .NET 类型信息
  6. 日期格式:CSV 中的日期格式不统一时,使用 [datetime]::ParseExact() 指定格式解析

PowerShell 技能连载 - XML 高级处理

适用于 PowerShell 5.1 及以上版本

虽然 JSON 已经成为现代应用配置的主流格式,但 XML 仍然在许多场景中扮演重要角色——Windows 配置文件(.config)、NuGet 包定义(.nuspec)、SOAP Web 服务、Office 文档(.docx/.xlsx 底层是 XML)、以及大量遗留系统的数据交换格式。PowerShell 通过 [xml] 类型加速器和 .NET 的 System.Xml 命名空间,提供了强大的 XML 处理能力。

本文将讲解 XML 文档的创建、查询(XPath)、修改,以及与常见 XML 格式的交互。

XML 解析与导航

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
# 解析 XML 字符串
[xml]$config = @"
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="ServerName" value="prod-db01" />
<add key="Port" value="5432" />
<add key="Database" value="MyApp" />
<add key="Timeout" value="30" />
</appSettings>
<connectionStrings>
<add name="Default" connectionString="Server=prod-db01;Database=MyApp;Integrated Security=True" />
<add name="ReadOnly" connectionString="Server=prod-db01;Database=MyApp_ReadOnly;Integrated Security=True" />
</connectionStrings>
<logging>
<level>Warning</level>
<path>C:\Logs\MyApp</path>
<maxSizeMB>100</maxSizeMB>
</logging>
</configuration>
"@

# 属性访问(点表示法)
Write-Host "服务器:$($config.configuration.appSettings.add[0].value)"
Write-Host "端口:$($config.configuration.appSettings.add[1].value)"
Write-Host "日志级别:$($config.configuration.logging.level)"

# 提取所有 appSettings 为哈希表
$settings = @{}
foreach ($add in $config.configuration.appSettings.add) {
$settings[$add.key] = $add.value
}
Write-Host "`nAppSettings:"
$settings.GetEnumerator() | Sort-Object Key | ForEach-Object {
Write-Host " $($_.Key) = $($_.Value)"
}

# 提取连接字符串
Write-Host "`n连接字符串:"
foreach ($cs in $config.configuration.connectionStrings.add) {
Write-Host " $($cs.name):$($cs.connectionString)"
}

# 从文件加载 XML
# [xml]$doc = Get-Content "C:\MyApp\web.config" -Encoding UTF8

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
服务器:prod-db01
端口:5432
日志级别:Warning

AppSettings:
Database = MyApp
Port = 5432
ServerName = prod-db01
Timeout = 30

连接字符串:
DefaultServer=prod-db01;Database=MyApp;Integrated Security=True
ReadOnly:Server=prod-db01;Database=MyApp_ReadOnly;Integrated Security=True

XPath 查询

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
# XPath 提供强大的 XML 查询能力
[xml]$servers = @"
<inventory>
<server env="production" role="web">
<name>WEB01</name>
<ip>10.0.1.10</ip>
<cpu>8</cpu>
<ram>32</ram>
<status>running</status>
</server>
<server env="production" role="db">
<name>DB01</name>
<ip>10.0.1.20</ip>
<cpu>16</cpu>
<ram>64</ram>
<status>running</status>
</server>
<server env="staging" role="web">
<name>WEB-STG01</name>
<ip>10.0.2.10</ip>
<cpu>4</cpu>
<ram>16</ram>
<status>stopped</status>
</server>
<server env="production" role="web">
<name>WEB02</name>
<ip>10.0.1.11</ip>
<cpu>8</cpu>
<ram>32</ram>
<status>running</status>
</server>
</inventory>
"@

# SelectNodes 使用 XPath 查询
$nav = $servers.CreateNavigator()

# 查询所有生产环境服务器
$prodServers = $servers.SelectNodes("//server[@env='production']")
Write-Host "生产环境服务器:" -ForegroundColor Cyan
foreach ($s in $prodServers) { Write-Host " $($s.name) ($($s.ip)) - $($s.status)" }

# 查询所有 Web 角色
$webServers = $servers.SelectNodes("//server[@role='web']")
Write-Host "`nWeb 服务器:" -ForegroundColor Cyan
foreach ($s in $webServers) { Write-Host " $($s.name) - CPU: $($s.cpu), RAM: $($s.ram)GB" }

# 复杂查询:生产环境中 CPU 大于 8 的服务器
$powerful = $servers.SelectNodes("//server[@env='production' and cpu > 8]")
Write-Host "`n高配生产服务器(CPU>8):" -ForegroundColor Cyan
foreach ($s in $powerful) { Write-Host " $($s.name) - CPU: $($s.cpu)" }

# 统计总资源
$totalCpu = $servers.SelectNodes("sum(//server[@env='production']/cpu)")
$totalRam = $servers.SelectNodes("sum(//server[@env='production']/ram)")
Write-Host "`n生产环境总资源:CPU $totalCpu 核,RAM $totalRam GB"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
生产环境服务器:
WEB01 (10.0.1.10) - running
DB01 (10.0.1.20) - running
WEB02 (10.0.1.11) - running

Web 服务器:
WEB01 - CPU: 8, RAM: 32GB
WEB-STG01 - CPU: 4, RAM: 16GB
WEB02 - CPU: 8, RAM: 32GB

高配生产服务器(CPU>8):
DB01 - CPU: 16

生产环境总资源:CPU 32 核,RAM 128 GB

XML 创建与修改

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
# 创建新的 XML 文档
function New-ReportXml {
param(
[string]$ReportName = "系统巡检报告",
[string]$OutputPath = "C:\Reports\report.xml"
)

$xmlDoc = [System.Xml.XmlDocument]::new()

# XML 声明
$declaration = $xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", $null)
$xmlDoc.AppendChild($declaration) | Out-Null

# 根元素
$root = $xmlDoc.CreateElement("report")
$root.SetAttribute("name", $ReportName)
$root.SetAttribute("generated", (Get-Date -Format "yyyy-MM-dd HH:mm:ss"))

# 服务器信息
$serversNode = $xmlDoc.CreateElement("servers")

foreach ($srvName in @("WEB01", "WEB02", "DB01")) {
$srvNode = $xmlDoc.CreateElement("server")
$srvNode.SetAttribute("name", $srvName)
$srvNode.SetAttribute("status", "running")

$cpuNode = $xmlDoc.CreateElement("cpu")
$cpuNode.InnerText = Get-Random -Min 10 -Max 90
$srvNode.AppendChild($cpuNode) | Out-Null

$memNode = $xmlDoc.CreateElement("memory")
$memNode.InnerText = Get-Random -Min 20 -Max 80
$srvNode.AppendChild($memNode) | Out-Null

$serversNode.AppendChild($srvNode) | Out-Null
}

$root.AppendChild($serversNode) | Out-Null
$xmlDoc.AppendChild($root) | Out-Null

$xmlDoc.Save($OutputPath)
Write-Host "XML 报告已生成:$OutputPath" -ForegroundColor Green
}

New-ReportXml

# 修改现有 XML
[xml]$doc = Get-Content "C:\Reports\report.xml"

# 修改属性值
$server = $doc.SelectSingleNode("//server[@name='WEB01']")
if ($server) {
$server.status = "maintenance"
Write-Host "已修改 WEB01 状态为 maintenance" -ForegroundColor Yellow
}

# 添加新节点
$newServer = $doc.CreateElement("server")
$newServer.SetAttribute("name", "WEB03")
$newServer.SetAttribute("status", "running")
$cpu = $doc.CreateElement("cpu")
$cpu.InnerText = "25"
$newServer.AppendChild($cpu) | Out-Null

$doc.SelectSingleNode("//servers").AppendChild($newServer) | Out-Null
Write-Host "已添加新服务器 WEB03" -ForegroundColor Green

# 保存修改
$doc.Save("C:\Reports\report.xml")

执行结果示例:

1
2
3
XML 报告已生成:C:\Reports\report.xml
已修改 WEB01 状态为 maintenance
已添加新服务器 WEB03

XML 与对象转换

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
# 将 PowerShell 对象导出为 XML(Clixml)
$servers = @(
[PSCustomObject]@{ Name = "SRV01"; IP = "10.0.1.1"; Role = "Web" },
[PSCustomObject]@{ Name = "SRV02"; IP = "10.0.1.2"; Role = "DB" },
[PSCustomObject]@{ Name = "SRV03"; IP = "10.0.1.3"; Role = "App" }
)

$servers | Export-Clixml -Path "C:\Data\servers.xml" -Encoding UTF8
Write-Host "对象已导出为 Clixml" -ForegroundColor Green

# 从 Clixml 导入
$imported = Import-Clixml -Path "C:\Data\servers.xml"
$imported | Format-Table -AutoSize

# Clixml 保留类型信息(包括 DateTime、PSCredential 等)
$config = [PSCustomObject]@{
AppName = "MyApp"
Version = [version]"2.5.0"
StartTime = Get-Date
Servers = $servers
}

$config | Export-Clixml -Path "C:\Data\config.xml"
$restored = Import-Clixml -Path "C:\Data\config.xml"
Write-Host "应用:$($restored.AppName) v$($restored.Version)"
Write-Host "类型保留:Version=$($restored.Version.GetType().Name)"

执行结果示例:

1
2
3
4
5
6
7
8
对象已导出为 Clixml
Name IP Role
---- -- ----
SRV01 10.0.1.1 Web
SRV02 10.0.1.2 DB
SRV03 10.0.1.3 App
应用:MyApp v2.5.0
类型保留:Version=Version

注意事项

  1. XPath 区分大小写:XML 元素和属性名是大小写敏感的,查询时注意匹配
  2. 命名空间:带命名空间的 XML(如 SOAP)需要使用 XmlNamespaceManager 才能用 XPath 查询
  3. 编码:XML 文件可能有各种编码,使用 Get-Content -Encoding UTF8XmlDocument.Load() 正确处理
  4. 大文件:超大 XML 文件(>100MB)应使用 XmlReader(前向只读)代替 XmlDocument(全加载到内存)
  5. Clixml 安全Export-Clixml 可以安全存储 PSCredential 对象(DPAPI 加密),但仅限同一台机器解密
  6. 修改后保存:通过点表示法修改 XML 属性后,需要调用 Save() 方法持久化到文件