PowerShell 技能连载 - 并行处理实战

适用于 PowerShell 7.0 及以上版本

当我们需要对大量对象执行相同操作时,传统的顺序处理方式往往耗时过长。例如检查 100 台服务器的连通性,逐台 ping 可能需要几分钟,但如果同时发起多个 ping 操作,时间可以缩短到几秒钟。

PowerShell 7 引入了 ForEach-Object -Parallel 参数,让并行处理变得前所未有的简单。对于更高级的场景,还可以通过 Runspace 直接操控底层并发机制。本文将系统介绍 PowerShell 中的并行处理方法,帮助你在合适的场景下大幅提升脚本执行效率。

阅读更多

PowerShell 技能连载 - Windows 事件日志分析

适用于 PowerShell 5.1 及以上版本

Windows 事件日志是排查系统问题、监控安全事件的重要数据源。无论是排查服务崩溃、追踪登录失败,还是审计权限变更,事件日志都记录着关键的操作痕迹。传统的”事件查看器”图形界面虽然直观,但面对大规模日志筛选和批量分析时效率极低。

PowerShell 提供了强大的事件日志查询能力,可以像操作数据库一样对日志进行精确筛选和统计。本文将介绍如何使用 Get-WinEvent 高效查询事件日志,以及如何构建自动化的日志分析脚本。

阅读更多

PowerShell 技能连载 - 正则表达式实战

适用于 PowerShell 5.1 及以上版本

在日常运维和数据处理中,我们经常需要从大量文本中提取特定信息、验证输入格式或批量替换内容。正则表达式(Regular Expression)是处理这类任务的利器。PowerShell 基于 .NET 的正则引擎,提供了丰富且强大的文本处理能力。

许多管理员对正则表达式望而生畏,觉得语法晦涩难懂。但实际上,掌握少数几个核心模式就能解决大部分日常工作需求。本文将从基础匹配开始,逐步深入到捕获组、替换操作和常用验证模式。

阅读更多

PowerShell 技能连载 - 本地大模型 Ollama 集成

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

前一篇我们探讨了 PowerShell 调用云端 AI API 的方式。然而在很多场景下——企业内网环境、数据合规要求、或者仅仅是不想为每一次调试付费——本地运行大语言模型是更务实的选择。Ollama 把模型下载、推理服务、REST API 打包成一条命令就能跑起来的体验,而 PowerShell 作为胶水语言,可以快速将这些能力集成到日常工作流中。

阅读更多

PowerShell 技能连载 - JSON 与 YAML 配置管理

适用于 PowerShell 7.0 及以上版本

在 DevOps 和基础设施即代码的实践中,配置文件管理是核心能力。无论是应用部署、容器编排还是 CI/CD 流水线,JSON 和 YAML 格式的配置文件无处不在。PowerShell 原生支持 JSON 的读写与转换,配合 powershell-yaml 模块也能轻松处理 YAML,包括 Kubernetes 风格的多文档格式。

本文将从实际场景出发,逐步介绍如何用 PowerShell 完成 JSON 配置读取与修改、YAML 解析、Schema 验证、模板渲染以及环境配置切换。

阅读更多

PowerShell 技能连载 - Docker 容器管理

适用于 Docker 20.10+ 及 PowerShell 7.0+

容器化部署已经从尝鲜走向了主流。无论是微服务架构还是 CI/CD 流水线,Docker 几乎无处不在。然而,日常运维中频繁手动输入 docker rundocker logs 等命令不仅效率低下,而且容易出错。PowerShell 凭借其强大的对象管道和函数封装能力,非常适合用来编排 Docker 操作——把重复性工作交给脚本,让运维人员专注于真正重要的事情。

本文将从环境检查、容器生命周期管理、批量巡检、镜像清理、日志收集五个方面,逐步展示如何用 PowerShell 高效管理 Docker 容器,最后给出一个完整的 Nginx 部署实战示例。

阅读更多

PowerShell 技能连载 - Windows 安全基线审计

适用于 Windows Server 2016+ 及 PowerShell 5.1+

很多安全事件的根源并不是零日漏洞,而是基础配置疏忽——弱密码策略、高危端口暴露、补丁长期未更新。定期执行安全基线检查是运维最基本也是最有效的防线。本文用 PowerShell 编写一套轻量审计脚本,覆盖密码策略、防火墙、RDP、补丁和服务五大维度,最终汇总为一份 HTML 审计报告。

账户与密码策略

密码策略是安全基线的第一道门。如果系统允许空密码或密码永不过期,攻击者可以轻松暴力破解。同时管理员组成员过多、Guest 账户处于启用状态,也会显著增加横向移动的风险。下面这段代码通过 net accounts 获取当前密码策略,并检查本地管理员组和 Guest 账户的状态。

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
function Get-SecurityBaselineAccount {
$result = @()
$pwdPolicy = net accounts | Out-String

# 密码最短期限:防止用户改完密码后立刻改回旧密码
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "密码最短期限"
Value = if ($pwdPolicy -match '密码最短期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" }
Expected = "≥ 1 天"
}
# 密码最长期限:长期不换密码会增加凭据泄露的风险
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "密码最长期限"
Value = if ($pwdPolicy -match '密码最长期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" }
Expected = "≤ 90 天"
}
# 最小密码长度:14 位以下容易被暴力破解
$result += [PSCustomObject]@{
Category = "密码策略"; Check = "最小密码长度"
Value = if ($pwdPolicy -match '最小密码长度:\s*(\d+)') { $Matches[1] + " 字符" } else { "未知" }
Expected = "≥ 14 字符"
}
# 管理员组成员应尽量精简,避免权限滥用
$adminMembers = Get-LocalGroupMember -SID "S-1-5-32-544" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "账户安全"; Check = "本地管理员组成员数"
Value = "$($adminMembers.Count) 个"; Expected = "尽量精简"
}
# Guest 账户启用后任何人可匿名访问系统资源
$guest = Get-LocalUser -Name "Guest" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "账户安全"; Check = "Guest 账户状态"
Value = if ($guest.Enabled) { "已启用 (风险)" } else { "已禁用" }; Expected = "已禁用"
}
return $result
}

执行后输出类似如下结果,可以看到最小密码长度仅为 7 字符,不满足基线要求,需要重点关注:

1
2
3
4
5
6
7
Category  Check              Value        Expected
-------- ----- ----- --------
密码策略 密码最短期限 1 天 ≥ 1 天
密码策略 密码最长期限 42 天 ≤ 90 天
密码策略 最小密码长度 7 字符 ≥ 14 字符
账户安全 本地管理员组成员数 3 个 尽量精简
账户安全 Guest 账户状态 已禁用 已禁用

防火墙与高危端口检测

防火墙是系统级的网络屏障,任何一个配置文件被关闭都会让对应网络场景下的所有端口暴露。FTP(21)、Telnet(23)、SMB(445)、RDP(3389)、VNC(5800/5900)等高危端口如果监听在 0.0.0.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
function Get-SecurityBaselineNetwork {
$result = @()

# 逐个检查域、专用、公用三个防火墙配置文件
foreach ($profile in @("Domain", "Private", "Public")) {
$fw = Get-NetFirewallProfile -Name $profile -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "防火墙"; Check = "$profile 配置文件"
Value = if ($fw.Enabled) { "已启用" } else { "已禁用 (风险)" }
Expected = "已启用"
}
}
# 扫描监听在所有网卡上的高危端口
$listeningPorts = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalAddress -eq "0.0.0.0" -or $_.LocalAddress -eq "::" } |
Select-Object LocalPort, OwningProcess -Unique

$highRiskPorts = @(21, 23, 445, 3389, 5800, 5900)
$riskyPorts = $listeningPorts | Where-Object { $_.LocalPort -in $highRiskPorts }

$result += [PSCustomObject]@{
Category = "网络"; Check = "高危端口开放"
Value = if ($riskyPorts) {
"发现 $($riskyPorts.Count) 个: $($riskyPorts.LocalPort -join ', ')"
} else { "无" }
Expected = "无"
}
return $result
}

下面的执行结果显示 Public 配置文件的防火墙已关闭,同时 445(SMB)和 3389(RDP)对外开放,存在较高的被利用风险:

1
2
3
4
5
6
Category  Check           Value                 Expected
-------- ----- ----- --------
防火墙 Domain 配置文件 已启用 已启用
防火墙 Private 配置文件 已启用 已启用
防火墙 Public 配置文件 已禁用 (风险) 已启用
网络 高危端口开放 发现 2 个: 445, 3389 无

RDP 安全配置

RDP 是远程管理 Windows 服务器的常用方式,但如果不加限制就会成为暴力破解的入口。需要关注两个关键点:RDP 是否开启,以及是否启用了网络级别身份验证(NLA)。NLA 在建立连接之前就要求身份验证,能有效防御未认证的拒绝服务攻击和凭据窃取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Get-SecurityBaselineRDP {
$result = @()

# 通过注册表检查 RDP 是否启用(0 = 启用,1 = 禁用)
$rdp = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" `
-Name "fDenyTSConnections" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "RDP 安全"; Check = "RDP 状态"
Value = if ($rdp.fDenyTSConnections -eq 0) { "已启用" } else { "已禁用" }
Expected = "按需启用"
}

# NLA 未启用时连接建立前不做身份验证,存在安全风险
$nla = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" `
-Name "UserAuthentication" -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "RDP 安全"; Check = "NLA(网络级别身份验证)"
Value = if ($nla.UserAuthentication -eq 1) { "已启用" } else { "未启用 (风险)" }
Expected = "已启用"
}
return $result
}

执行结果示例中 RDP 已启用且 NLA 也已启用,处于合规状态。如果 NLA 未启用,攻击者可在身份验证之前消耗服务器资源,建议立刻启用:

1
2
3
4
Category  Check                    Value    Expected
-------- ----- ----- --------
RDP 安全 RDP 状态 已启用 按需启用
RDP 安全 NLA(网络级别身份验证) 已启用 已启用

补丁状态检查

未及时安装补丁是导致系统被入侵的常见原因。这段代码检查最近一次补丁安装的日期、Windows Update 服务状态,以及当前有多少重要更新待安装。补丁超过 30 天未更新或存在待安装的重要更新,都说明更新策略可能存在问题。

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
function Get-SecurityBaselineUpdates {
$result = @()
# 获取最近安装的补丁,按时间降序排列
$recentUpdates = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5
$latestUpdate = $recentUpdates | Select-Object -First 1
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "最新补丁日期"
Value = if ($latestUpdate.InstalledOn) {
$latestUpdate.InstalledOn.ToString("yyyy-MM-dd")
} else { "未知" }
Expected = "30 天内"
}
# 检查 Windows Update 服务是否正常运行
$wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "Windows Update 服务"
Value = "$($wuService.Status)"; Expected = "Running"
}
# 通过 COM 对象查询待安装的重要更新
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searchResult = $updateSession.CreateUpdateSearcher().Search(
"IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
)
$result += [PSCustomObject]@{
Category = "系统更新"; Check = "待安装重要更新"
Value = "$($searchResult.Updates.Count) 个"; Expected = "0 个"
}
return $result
}

以下执行结果显示还有 3 个重要更新待安装,建议尽快安排补丁窗口完成更新:

1
2
3
4
5
Category  Check               Value        Expected
-------- ----- ----- --------
系统更新 最新补丁日期 2025-03-10 30 天内
系统更新 Windows Update 服务 Running Running
系统更新 待安装重要更新 30

危险服务检查

某些服务在大多数生产环境中并不需要,反而会增大攻击面。Telnet 明文传输凭据,远程注册表允许远程修改注册表,SSDP 和 UPnP 可能被用于网络发现和反射攻击。下面逐一检查这些服务的运行状态,并统计系统级自启动项数量——自启动项过多可能意味着恶意软件的持久化驻留。

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
function Get-SecurityBaselineServices {
$result = @()
# 常见高危服务列表:名称 => 服务标识
$dangerousServices = @{
"Telnet" = "tlntsvr"
"远程注册表" = "RemoteRegistry"
"SSDP 发现" = "ssdpsrv"
"UPnP 设备主机" = "upnphost"
}
foreach ($svc in $dangerousServices.GetEnumerator()) {
$service = Get-Service -Name $svc.Value -ErrorAction SilentlyContinue
$result += [PSCustomObject]@{
Category = "服务检查"; Check = "$($svc.Key) ($($svc.Value))"
Value = if ($service) {
"$($service.Status), 启动类型: $($service.StartType)"
} else { "未安装" }
Expected = "未安装或已禁用"
}
}
# 系统级自启动项过多可能意味着恶意软件持久化
$startupItems = Get-CimInstance -ClassName Win32_StartupCommand |
Where-Object { $_.Location -eq "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" }
$result += [PSCustomObject]@{
Category = "启动项"; Check = "系统级自启动项"
Value = "$($startupItems.Count) 个"; Expected = "审查后保留必要项"
}
return $result
}

执行结果显示远程注册表服务处于自动启动且正在运行,在生产环境中建议禁用:

1
2
3
4
5
6
7
Category  Check                         Value                         Expected
-------- ----- ----- --------
服务检查 Telnet (tlntsvr) 未安装 未安装或已禁用
服务检查 远程注册表 (RemoteRegistry) Running, 启动类型: Automatic 未安装或已禁用
服务检查 SSDP 发现 (ssdpsrv) Stopped, 启动类型: Manual 未安装或已禁用
服务检查 UPnP 设备主机 (upnphost) Stopped, 启动类型: Manual 未安装或已禁用
启动项 系统级自启动项 4 个 审查后保留必要项

生成 HTML 审计报告

将前面所有检查函数汇总为一份带风险标记的 HTML 报告。不合规的行以红色背景高亮显示,便于快速定位问题。报告顶部汇总检查总数和风险项数量,帮助管理层一眼掌握整体安全态势。

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
function New-SecurityBaselineReport {
param(
[string]$OutputPath = ".\SecurityBaseline_$(Get-Date -Format 'yyyyMMdd').html"
)

# 依次调用各维度的检查函数,汇总所有结果
$allChecks = @(Get-SecurityBaselineAccount) + @(Get-SecurityBaselineNetwork) +
@(Get-SecurityBaselineRDP) +
@(Get-SecurityBaselineUpdates) + @(Get-SecurityBaselineServices)

$totalChecks = $allChecks.Count
$riskItems = ($allChecks | Where-Object {
$_.Value -match "风险|FAIL|未启用"
}).Count

$html = @"
<!DOCTYPE html><html><head><title>安全基线审计报告</title>
<style>
body{font-family:Microsoft YaHei;margin:20px}
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ddd;padding:8px;text-align:left}
th{background-color:#4CAF50;color:white}
.risk{background-color:#ffcccc}
</style></head><body>
<h1>安全基线审计报告 - $(Get-Date -Format 'yyyy-MM-dd')</h1>
<p>检查项: $totalChecks | 风险项: <span style="color:red">$riskItems</span></p>
<table><tr><th>类别</th><th>检查项</th><th>当前值</th><th>期望值</th></tr>
"@
foreach ($item in $allChecks) {
$class = if ($item.Value -match "风险|FAIL|未启用") {
' class="risk"'
} else { "" }
$html += "<tr$class><td>$($item.Category)</td><td>$($item.Check)</td>" +
"<td>$($item.Value)</td><td>$($item.Expected)</td></tr>"
}
$html += "</table></body></html>"
$html | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "报告已生成: $OutputPath"
}

执行后在当前目录生成 HTML 文件,浏览器打开即可看到完整的审计表格。不合规的行以红色背景标出:

1
报告已生成: .\SecurityBaseline_20250331.html
1
2
3
4
5
6
7
8
检查项: 19 | 风险项: 4

| 类别 | 检查项 | 当前值 | 期望值 |
|----------|-----------------------------|-------------------------------|-----------------|
| 密码策略 | 最小密码长度 | 7 字符 | ≥ 14 字符 |
| 防火墙 | Public 配置文件 | 已禁用 (风险) | 已启用 |
| 网络 | 高危端口开放 | 发现 2 个: 445, 3389 ||
| 服务检查 | 远程注册表 (RemoteRegistry) | Running, 启动类型: Automatic | 未安装或已禁用 |

建议在运维流程中定期执行 New-SecurityBaselineReport,并将历次报告归档对比,跟踪风险项的变化趋势。安全基线不是一次性的工作,而是持续改进的过程。

PowerShell 技能连载 - 浏览器自动化实战

适用于 PowerShell 7.0 及以上版本

2025 年,AI + 浏览器(Browser Using)成为技术热点,各大模型厂商纷纷推出浏览器操控能力。浏览器自动化不再只是测试工程师的专属工具——数据采集、运维巡检、表单自动填写、可用性监控等场景都离不开它。

PowerShell 作为 Windows 和 macOS 上广泛使用的脚本语言,同样可以胜任浏览器自动化任务。本文将分别介绍 Selenium 和 Playwright 两种方案,从环境搭建到实际应用,帮助你快速上手。

方案一:Selenium

Selenium 是老牌的浏览器自动化框架,生态成熟,社区资源丰富。它的核心思路是通过 WebDriver 协议控制浏览器,PowerShell 可以直接调用其 .NET 绑定。

安装环境

首先安装 Selenium 模块和 Chrome WebDriver。以下函数会自动完成这些步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Install-SeleniumDriver {
param(
[ValidateSet("Chrome", "Firefox", "Edge")]
[string]$Browser = "Chrome"
)

# 安装 Selenium PowerShell 模块
Install-Module -Name Selenium -Scope CurrentUser -Force

# 创建 WebDriver 存放目录
$driverPath = "$env:TEMP\selenium"
if (-not (Test-Path $driverPath)) {
New-Item -Path $driverPath -ItemType Directory | Out-Null
}

Write-Host "$Browser WebDriver 已就绪"
}

执行安装:

1
2
PS> Install-SeleniumDriver
Chrome WebDriver 已就绪

创建浏览器会话

安装完成后,需要一个函数来启动浏览器。我们支持无头模式(Headless),这样在服务器或 CI 环境中也能正常运行。关键参数 --headless=new 使用 Chrome 新版无头模式,兼容性更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Start-BrowserSession {
param(
[ValidateSet("Chrome", "Firefox", "Edge")]
[string]$Browser = "Chrome",

[switch]$Headless
)

$options = New-Object OpenQA.Selenium.Chrome.ChromeOptions
if ($Headless) {
$options.AddArgument("--headless=new")
}
$options.AddArgument("--disable-gpu")
$options.AddArgument("--no-sandbox")

$driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver($options)
return $driver
}

页面操作封装

每次都手动管理 Driver 的生命周期容易遗漏资源释放。下面封装一个通用的页面操作函数,自动处理会话创建和销毁。你只需传入一个脚本块来定义具体操作。

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
function Invoke-WebPageTask {
param(
[Parameter(Mandatory)]
[string]$Url,

[scriptblock]$Action,

[switch]$Headless,

[int]$TimeoutSeconds = 30
)

$driver = Start-BrowserSession -Headless:$Headless
try {
# 设置隐式等待,避免元素未加载就操作
$driver.Manage().Timeouts().ImplicitWait = [TimeSpan]::FromSeconds($TimeoutSeconds)
$driver.Navigate().GoToUrl($Url)
Write-Host "已打开: $Url"

$result = & $Action -Driver $driver
return $result
}
finally {
# 务必释放浏览器进程
$driver.Quit()
}
}

使用示例——抓取页面标题和所有 HTTPS 链接:

1
2
3
4
5
6
7
8
9
10
11
12
$links = Invoke-WebPageTask -Url "https://learn.microsoft.com/powershell" -Headless -Action {
param($Driver)

$title = $Driver.Title
$links = $Driver.FindElements([OpenQA.Selenium.By]::TagName("a")) |
Where-Object { $_.GetAttribute("href") -match "^https://" } |
Select-Object @{ N = "Text"; E = { $_.Text } },
@{ N = "Href"; E = { $_.GetAttribute("href") } }

Write-Host "页面: $title, 链接数: $($links.Count)"
return $links
}

执行结果示例:

1
2
3
4
5
6
7
已打开: https://learn.microsoft.com/powershell
页面: PowerShell | Microsoft Learn, 链接数: 42

Text Href
---- ----
PowerShell 文档 https://learn.microsoft.com/powershell/
安装 PowerShell https://learn.microsoft.com/powershell/script...

截图功能

用 Selenium 可以轻松截取完整页面截图。以下函数设置视口大小后导航到目标页面,等待 2 秒让动态内容加载完毕,再保存截图。

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
function Save-WebPageScreenshot {
param(
[Parameter(Mandatory)]
[string]$Url,

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

[int]$Width = 1920,
[int]$Height = 1080
)

$driver = Start-BrowserSession -Headless
try {
$driver.Manage().Window.Size = New-Object System.Drawing.Size($Width, $Height)
$driver.Navigate().GoToUrl($Url)
Start-Sleep -Seconds 2

$screenshot = $driver.GetScreenshot()
$screenshot.SaveAsFile($OutputPath, [OpenQA.Selenium.ScreenshotImageFormat]::Png)
Write-Host "截图已保存: $OutputPath"
}
finally {
$driver.Quit()
}
}
1
2
PS> Save-WebPageScreenshot -Url "https://example.com" -OutputPath "$HOME/Desktop/demo.png"
截图已保存: /Users/wubo/Desktop/demo.png

方案二:Playwright(推荐)

Playwright 是微软推出的新一代浏览器自动化工具,相比 Selenium 有三大优势:自动等待(不需要手写 Sleep)、内置多浏览器支持、原生支持 PDF 导出。如果你是新项目,强烈推荐直接使用 Playwright。

安装 Playwright

Playwright 是基于 Node.js 的工具,需要先安装 Node.js 环境,然后通过 npm 安装 Playwright 并下载浏览器内核。

1
2
3
4
5
# 全局安装 Playwright 包
npm install -g playwright

# 下载 Chromium 浏览器内核
npx playwright install chromium
1
2
PS> npx playwright install chromium
Downloading Chromium 131.0.6778.33 (darwin) ... done

通用调用函数

PowerShell 本身没有 Playwright 的原生绑定,但可以通过生成临时 JS 脚本文件来调用。以下函数封装了这个流程:接收 URL 和 JS 脚本片段,自动拼接完整的 Playwright 程序,执行后删除临时文件。

注意 here-string 内不要嵌入三反引号,否则会导致 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
function Invoke-PlaywrightScript {
param(
[Parameter(Mandatory)]
[string]$Url,

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

# 拼接完整的 Node.js 脚本
$fullScript = @"
const { chromium } = require('playwright');

(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('$Url');

$Script

await browser.close();
})();
"@

# 写入临时文件并执行
$scriptPath = [System.IO.Path]::GetTempFileName() + ".js"
$fullScript | Out-File -FilePath $scriptPath -Encoding UTF8
node $scriptPath
Remove-Item $scriptPath
}

截图与 PDF 导出

Playwright 的截图和 PDF 功能比 Selenium 更强大。特别是 PDF 导出,Selenium 需要借助 Chrome DevTools Protocol,而 Playwright 直接提供 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
function Save-PlaywrightScreenshot {
param(
[Parameter(Mandatory)]
[string]$Url,

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

$jsCode = "await page.screenshot({ path: '$($OutputPath.Replace('\','/'))', fullPage: true });"
Invoke-PlaywrightScript -Url $Url -Script $jsCode
Write-Host "截图已保存: $OutputPath"
}

function Save-PlaywrightPdf {
param(
[Parameter(Mandatory)]
[string]$Url,

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

$jsCode = "await page.pdf({ path: '$($OutputPath.Replace('\','/'))', format: 'A4' });"
Invoke-PlaywrightScript -Url $Url -Script $jsCode
Write-Host "PDF 已保存: $OutputPath"
}
1
2
3
4
5
PS> Save-PlaywrightScreenshot -Url "https://example.com" -OutputPath "$HOME/Desktop/pw.png"
截图已保存: /Users/wubo/Desktop/pw.png

PS> Save-PlaywrightPdf -Url "https://example.com" -OutputPath "$HOME/Desktop/page.pdf"
PDF 已保存: /Users/wubo/Desktop/page.pdf

网站可用性监控

无论选择哪种方案,浏览器自动化最常见的运维场景就是网站健康检查。以下函数使用 Selenium 批量检测多个 URL 的可访问性和加载耗时,输出结构化的监控数据。

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
function Test-WebsiteHealth {
param(
[Parameter(Mandatory)]
[string[]]$Urls,

[int]$TimeoutSeconds = 15
)

$driver = Start-BrowserSession -Headless
$results = @()

try {
foreach ($url in $Urls) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$driver.Manage().Timeouts().PageLoad = [TimeSpan]::FromSeconds($TimeoutSeconds)
$driver.Navigate().GoToUrl($url)
$sw.Stop()

$results += [PSCustomObject]@{
Url = $url
Status = "OK"
LoadTimeMs = $sw.ElapsedMilliseconds
Title = $driver.Title
Timestamp = Get-Date
}
}
catch {
$sw.Stop()
$results += [PSCustomObject]@{
Url = $url
Status = "FAIL"
LoadTimeMs = $sw.ElapsedMilliseconds
Title = ""
Timestamp = Get-Date
Error = $_.Exception.Message
}
}
}
}
finally {
$driver.Quit()
}

return $results | Format-Table -AutoSize
}
1
2
3
4
5
6
7
PS> Test-WebsiteHealth -Urls "https://example.com","https://httpbin.org/status/500","https://nonexist.invalid"

Url Status LoadTimeMs Title Timestamp
--- ------ ---------- ----- ---------
https://example.com OK 312 Example Domain 2025/3/28 10:00:00
https://httpbin.org/st... OK 587 2025/3/28 10:00:01
https://nonexist.invalid FAIL 5001 2025/3/28 10:00:06

小结

Selenium 适合简单的页面交互场景,依赖 .NET 生态,PowerShell 调用起来比较自然。Playwright 更现代,自动等待、原生 PDF 支持、多浏览器覆盖等特性让它在复杂场景下更可靠。无头模式下务必注意内存回收,长时间运行的任务一定要在 finally 块中调用 Quit()close() 释放浏览器进程。

PowerShell 技能连载 - 调用大语言模型 API

适用于 PowerShell 7.0 及以上版本

大语言模型(LLM)已经渗透到开发工作的方方面面。当我们需要在自动化脚本中集成 AI 能力时,直接调用 OpenAI 兼容的 REST API 是最灵活的方式。PowerShell 内置的 Invoke-RestMethod cmdlet 天然适合完成这项工作——无需安装额外 SDK,几行代码即可实现与 LLM 的交互。

本文将从零开始,逐步带你完成 API Key 配置、单次问答封装、多轮对话、代码审查场景以及 Token 用量估算。

准备工作:配置 API Key

调用任何 OpenAI 兼容接口都需要一个 API Key。出于安全考虑,我们通过环境变量来管理它,避免在代码中硬编码。

1
2
# 在 PowerShell 配置文件中添加(只需执行一次)
Add-Content -Path $PROFILE -Value '`$env:OPENAI_API_KEY = "sk-your-key-here"'

如果你使用的是国内中转代理或其他 OpenAI 兼容服务(如 Azure OpenAI、DeepSeek),还需要额外设置基础 URL:

1
2
# 设置自定义 API 端点(可选)
$env:OPENAI_API_BASE = "https://your-proxy.example.com/v1"

配置完成后,重新打开 PowerShell 会话即可生效。你可以通过以下方式验证:

1
2
PS> $env:OPENAI_API_KEY
sk-proj-xxxxxxxxxxxxxx

单次问答:封装通用函数

下面我们封装一个 Invoke-LLMChat 函数,它接受用户提问,返回模型的回复。这个函数是后续所有场景的基础。

函数内部会自动读取环境变量中的 API Key,将用户消息和系统提示组装成 OpenAI Chat Completion 格式的 JSON,然后通过 Invoke-RestMethod 发送 POST 请求。

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
function Invoke-LLMChat {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "gpt-4o-mini",

[string]$SystemMessage = "你是一个有帮助的 PowerShell 助手。",

[double]$Temperature = 0.7,

[int]$MaxTokens = 2048
)

# 检查 API Key 是否已配置
$apiKey = $env:OPENAI_API_KEY
if (-not $apiKey) {
throw "请先设置环境变量 OPENAI_API_KEY"
}

# 确定请求地址:优先使用自定义端点
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 构造请求体
$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $SystemMessage }
@{ role = "user"; content = $Prompt }
)
temperature = $Temperature
max_tokens = $MaxTokens
} | ConvertTo-Json -Depth 5 -Compress

# 发送请求(使用 UTF8 编码避免中文乱码)
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.choices[0].message.content
}

这里有两个值得注意的细节:一是用 -Compress 参数减少 JSON 体积(去掉多余空白),二是用 UTF8.GetBytes 确保中文字符不会在传输中变成乱码。如果你不需要自定义端点,也可以省略 $env:OPENAI_API_BASE 相关逻辑。

调用示例:

1
2
3
4
PS> Invoke-LLMChat -Prompt "用一行 PowerShell 代码获取本机所有 IP 地址"

你可以使用以下命令获取本机所有 IP 地址:
(Get-NetIPAddress -AddressFamily IPv4).IPAddress

多轮对话:维护上下文历史

单次问答没有”记忆”。要让模型理解上下文,我们需要把完整的对话历史(包括之前的用户消息和助手回复)都发给 API。下面这个函数实现了一个交互式的多轮对话循环。

关键点在于 $messages 数组——每次用户发言后追加一条 user 消息,收到模型回复后追加一条 assistant 消息,这样上下文就在数组中不断累积。

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
function Start-LLMConversation {
param(
[string]$Model = "gpt-4o-mini"
)

$apiKey = $env:OPENAI_API_KEY
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 初始化对话历史,包含系统提示
$messages = @(
@{ role = "system"; content = "你是一个有帮助的助手,请用中文回答。" }
)

Write-Host "多轮对话已启动,输入 'exit' 退出" -ForegroundColor Cyan

while ($true) {
$userInput = Read-Host "你"
if ($userInput -eq "exit") { break }
if ([string]::IsNullOrWhiteSpace($userInput)) { continue }

# 追加用户消息到历史
$messages += @{ role = "user"; content = $userInput }

$body = @{
model = $Model
messages = $messages
} | ConvertTo-Json -Depth 10 -Compress

$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$assistantMessage = $response.choices[0].message.content

# 追加助手回复到历史,保持上下文连续
$messages += @{ role = "assistant"; content = $assistantMessage }

Write-Host "`n助手: $assistantMessage`n" -ForegroundColor Green

# 显示 Token 消耗,帮助控制成本
$usage = $response.usage
Write-Host "[Token] 输入: $($usage.prompt_tokens) | 输出: $($usage.completion_tokens)" -ForegroundColor DarkGray
}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS> Start-LLMConversation
多轮对话已启动,输入 'exit' 退出
你: 写一个函数检查磁盘空间

助手: 这是一个检查磁盘空间的函数:

function Get-DiskSpace {
param([string]$ComputerName = $env:COMPUTERNAME)
Get-CimInstance -ClassName Win32_LogicalDisk ...
}

[Token] 输入: 28 | 输出: 156
你: 再加上邮件告警功能

助手: 好的,在原有函数基础上增加邮件告警:

function Get-DiskSpace {
param(
[string]$ComputerName = $env:COMPUTERNAME,
[double]$ThresholdGB = 10,
...

[Token] 输入: 210 | 输出: 203
你: exit

可以看到第二轮对话中,输入 Token 从 28 涨到 210,因为整个对话历史都被带上了。这也提醒我们:多轮对话的 Token 消耗是递增的,长对话时需要考虑截断历史。

实战场景:代码审查助手

将 LLM 集成到日常工作流中,最实用的场景之一就是代码审查。我们读取一个脚本文件的内容,让模型从安全性、性能、可维护性等维度进行分析。

注意这里用数组拼接代替了 here-string,避免在内容中嵌入三反引号导致语法冲突。

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
function Request-CodeReview {
param(
[Parameter(Mandatory)]
[string]$ScriptPath
)

# 读取脚本内容
$code = Get-Content $ScriptPath -Raw

# 用数组拼接构造提示词,避免 here-string 中出现三反引号
$promptParts = @(
"请审查以下 PowerShell 脚本,指出潜在问题并提供改进建议:"
""
"--- 脚本内容开始 ---"
$code
"--- 脚本内容结束 ---"
""
"请从以下角度分析:"
"1. 安全性(注入风险、凭据处理)"
"2. 性能(循环优化、管道使用)"
"3. 可维护性(命名规范、错误处理)"
"4. 兼容性(PowerShell 版本差异)"
)
$prompt = $promptParts -join "`n"

$review = Invoke-LLMChat -Prompt $prompt -Model "gpt-4o" -MaxTokens 4096

Write-Host "`n========== 代码审查报告 ==========`n" -ForegroundColor Yellow
Write-Host $review
Write-Host "`n==================================`n" -ForegroundColor Yellow
}

假设我们有一个脚本 cleanup.ps1,内容是简单的临时文件清理,执行审查后输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS> Request-CodeReview -ScriptPath .\cleanup.ps1

========== 代码审查报告 ==========

## 代码审查结果

### 1. 安全性
- 第 3 行使用了硬编码路径 `C:\Temp`,建议改为参数化配置
- 缺少 `-WhatIf` 支持,建议添加 `[SupportsShouldProcess()]`

### 2. 性能
- `Get-ChildItem` 未使用 `-File` 参数,可能误删目录
- 建议添加 `-ErrorAction SilentlyContinue` 避免权限异常中断

### 3. 可维护性
- 缺少注释和帮助文档(Comment-Based Help)
- 变量 `$d` 命名不清晰,建议改为 `$daysOld`

### 4. 兼容性
- 使用了 PowerShell 7 的 `Ternernary` 运算符,Windows PowerShell 5.1 不兼容

==================================

Token 用量估算

在频繁调用 API 的场景下,了解 Token 消耗非常重要。下面这个函数基于响应中的 usage 字段进行累计统计,帮你掌握每次调用的开销。

不同模型的单价不同,函数中提供了常见模型的参考价格(美元/千 Token),你可以根据实际情况调整。

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
function Get-LLMTokenCost {
param(
[int]$PromptTokens,
[int]$CompletionTokens,
[string]$Model = "gpt-4o-mini"
)

# 常见模型价格表(美元/千 Token,仅供参考)
$pricing = @{
"gpt-4o" = @{ Input = 0.005; Output = 0.015 }
"gpt-4o-mini" = @{ Input = 0.00015; Output = 0.0006 }
"gpt-3.5-turbo" = @{ Input = 0.0005; Output = 0.0015 }
}

$rate = $pricing[$Model]
if (-not $rate) {
Write-Warning "未找到模型 $Model 的定价信息"
return
}

$inputCost = [math]::Round($PromptTokens * $rate.Input / 1000, 6)
$outputCost = [math]::Round($CompletionTokens * $rate.Output / 1000, 6)
$totalCost = [math]::Round($inputCost + $outputCost, 6)

[PSCustomObject]@{
模型 = $Model
输入Token = $PromptTokens
输出Token = $CompletionTokens
总Token = $PromptTokens + $CompletionTokens
输入费用 = "`$$inputCost"
输出费用 = "`$$outputCost"
总费用 = "`$$totalCost"
}
}

使用示例:

1
2
3
4
5
PS> Get-LLMTokenCost -PromptTokens 210 -CompletionTokens 203 -Model gpt-4o-mini

模型 输入Token 输出Token 总Token 输入费用 输出费用 总费用
---- -------- -------- ------- -------- -------- ------
gpt-4o-mini 210 203 413 $0.000032 $0.000122 $0.000154

可以看到,一次典型的多轮对话调用成本极低。但如果每天执行数百次自动化任务,费用仍然会累积,因此建议在脚本中加入 Token 上限控制。

注意事项

  • API Key 安全:切勿将 Key 硬编码在脚本中或提交到代码仓库。使用环境变量或 Azure Key Vault 等密钥管理服务。可以在 .gitignore 中排除包含敏感信息的配置文件。
  • Token 限制:每个模型有最大上下文窗口(如 gpt-4o-mini 为 128K)。多轮对话时注意累积的消息长度,必要时截断早期历史。建议在发送前估算 Token 数量(粗略规则:1 个汉字约 1-2 个 Token)。
  • 国内代理:如果无法直接访问 OpenAI API,可以设置 $env:OPENAI_API_BASE 指向国内代理。选择代理服务时注意数据隐私条款,避免敏感代码经第三方中转。
  • 错误处理:生产环境中建议在 Invoke-RestMethod 外包裹 try/catch,处理网络超时、API 限流(429 状态码)等异常情况。
  • 流式响应:本文示例使用非流式调用(等待完整响应后再返回)。如需实现打字机效果,可以使用 SSE(Server-Sent Events)模式,但实现复杂度较高,适合单独封装。

PowerShell 博客文章汇总 (2024-04 ~ 2025-03)

2025 年 03 月

2025 年 02 月

2025 年 01 月

2024 年 12 月

2024 年 11 月

2024 年 10 月

2024 年 09 月

2024 年 08 月

2024 年 07 月

2024 年 06 月

2024 年 05 月

2024 年 04 月

PowerShell 技术 QQ 群