PowerShell 技能连载 - XML 处理与配置管理

适用于 PowerShell 5.1 及以上版本

XML 是 Windows 生态中最常见的配置格式——从 web.config、app.config 到 NuGet 的 packages.config,从 WIX 安装配置到 MSBuild 项目文件,XML 无处不在。PowerShell 对 XML 有一流的内置支持,[xml] 类型加速器可以将 XML 文档直接转换为可导航的对象图,比传统的正则表达式解析简单得多。

本文将讲解 XML 的读取、查询、修改和创建,以及常见的配置文件管理场景。

阅读更多

PowerShell 技能连载 - 日志分析与解析

适用于 PowerShell 5.1 及以上版本

日志分析是运维排障的核心技能。无论是 IIS 访问日志、应用程序错误日志、Windows 事件日志还是自定义的业务日志,快速定位问题需要高效的日志解析能力。PowerShell 的文本处理命令(Select-StringConvertFrom-Csv、正则表达式)结合对象管道,可以构建强大的日志分析工具链。

本文将讲解常见的日志格式解析方法、模式匹配技巧,以及实用的日志分析脚本。

阅读更多

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

适用于 PowerShell 5.1 及以上版本

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

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

阅读更多

PowerShell跨平台开发实战

智能路径转换

1
2
3
4
5
6
7
8
9
10
11
function Get-UniversalPath {
param([string]$Path)
if ($IsLinux) {
$Path.Replace('\', '/').TrimEnd('/')
} else {
$Path.Replace('/', '\').TrimEnd('\')
}
}

# 示例:转换C:/Users到Linux格式
Get-UniversalPath -Path 'C:\Users\Demo' # 输出 /mnt/c/Users/Demo

条件编译技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#region WindowsOnly
if ($PSVersionTable.Platform -eq 'Win32NT') {
Add-Type -AssemblyName PresentationCore
[System.Windows.Clipboard]::SetText($content)
}
#endregion

#region LinuxOnly
if ($IsLinux) {
$tempFile = New-TemporaryFile
$content | Out-File $tempFile
xclip -selection clipboard -in $tempFile
}
#endregion

原生命令封装

1
2
3
4
5
6
7
8
9
10
11
function Invoke-NativeCommand {
param([string]$Command)
if ($IsWindows) {
cmd.exe /c $Command
} else {
bash -c $Command
}
}

# 跨平台调用系统命令
Invoke-NativeCommand -Command 'echo $Env:COMPUTERNAME'

开发注意事项

  • 区分文件系统大小写敏感特性
  • 处理CRLF/LF行尾差异
  • 避免平台特定别名使用
  • 统一字符编码为UTF-8

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

正则表达式是文本处理的核心工具,PowerShell通过-match-replace运算符提供原生支持。

1
2
3
4
5
6
# 提取日志中的IP地址
$logContent = Get-Content app.log -Raw
$ipPattern = '\b(?:\d{1,3}\.){3}\d{1,3}\b'

$matches = [regex]::Matches($logContent, $ipPattern)
$matches.Value | Select-Object -Unique

模式匹配进阶技巧

  1. 使用命名捕获组提取结构化数据:
1
2
3
4
5
6
7
8
9
$text = '订单号: INV-2024-0456 金额: ¥1,234.56'
$pattern = '订单号:\s+(?<OrderID>INV-\d+-\d+)\s+金额:\s+¥(?<Amount>[\d,]+\.[\d]{2})'

if ($text -match $pattern) {
[PSCustomObject]@{
OrderID = $matches['OrderID']
Amount = $matches['Amount'] -replace ',',''
}
}
  1. 多行模式处理复杂文本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$multiLineText = @'
Server: svr01
CPU: 85%
Memory: 92%
---
Server: svr02
CPU: 63%
Memory: 78%
'@

$pattern = '(?m)^Server:\s+(.+)\nCPU:\s+(.+)\nMemory:\s+(.+)$'
[regex]::Matches($multiLineText, $pattern) | ForEach-Object {
[PSCustomObject]@{
Server = $_.Groups[1].Value
CPU = $_.Groups[2].Value
Memory = $_.Groups[3].Value
}
}

最佳实践:

  • 使用[regex]::Escape()处理特殊字符
  • 通过(?:)语法优化非捕获组
  • 利用RegexOptions枚举加速匹配
  • 使用在线正则测试工具验证模式

字幕整理脚本

从看过的电影、美剧里学英语是一件很棒的事。因为你曾经被带入过那个场景,曾经和主角一同喜怒哀乐。如果能将电影里的中英文对白整理出来,对做笔记和搜索回顾将大有帮助。

我们可以从网上(例如射手网)下载视频的中英文字幕,需要是 .srt 格式的。它实际上是一个文本文件,内容类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
13
00:04:42,050 --> 00:04:45,010
{\an2}{\pos(212,240)}第三季 第一集

14
00:01:56,000 --> 00:01:56,990

Hey, Pop.

15
00:01:56,880 --> 00:02:04,020
{\an8}凯文·安德伍德

16
00:01:59,750 --> 00:02:01,510
好久不见啊
Been a while, hasn't it?

我们希望将它整理成这样的格式:

1
2
3
4
5
6
7
8
9
第三季  第一集


Hey, Pop.

凯文·安德伍德

好久不见啊
Been a while, hasn't it?

这个任务可以用 PowerShell + 正则表达式轻松搞定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (!(Test-Path dst)) {
md dst | Out-Null
}

Get-ChildItem src\*.srt | ForEach-Object {
$srcFile = $_
Write-Output "Processing $($srcFile.Name)"
$dstFile = (Join-Path 'dst' $srcFile.BaseName) + '.txt'
Get-Content $srcFile | ForEach-Object {
$line = $_
if ($line -cmatch '\A\d+\z') { return }
if ($line -cmatch '\d\d:\d\d:\d\d,\d\d\d --> \d\d:\d\d:\d\d,\d\d\d') { return }
$line = $line -creplace '\s*\{\\.*?\}\s*', ''
return $line
} | Out-File $dstFile
}

只需要将字幕源文件放在_src_目录下,运行脚本,就可以在_dst_目录下得到期望的文本文件。执行效果如下:

文件目录如下:

您也可以在这里下载完整的脚本。

常见的乱码

这是一个程序员段子,实际上是几种常见的乱码:

手持两把锟斤拷,
口中疾呼烫烫烫。
脚踏千朵屯屯屯,
笑看万物锘锘锘。

锟斤拷的来历

Unicode和老编码体系的转化过程中,肯定有一些字,用Unicode是没法表示的,Unicode官方用了一个占位符来表示这些文字,这就是:U+FFFD REPLACEMENT CHARACTER。

那么U+FFFD的UTF-8编码出来,恰好是 ‘\xef\xbf\xbd’。如果这个’\xef\xbf\xbd’,重复多次,例如 ‘\xef\xbf\xbd\xef\xbf\xbd’,然后放到GBK/CP936/GB2312/GB18030的环境中显示的话,一个汉字2个字节,最终的结果就是:锟斤拷——锟(0xEFBF),斤(0xBDEF),拷(0xBFBD)

烫烫烫的来历

在windows平台下,ms的编译器(也就是vc带的那个)在 Debug 模式下,会把未初始化的栈内存全部填成 0xcc,用字符串来看就是”烫烫烫烫烫烫烫”,也就是说出现了烫烫烫,赶紧检查初始化吧。。。

屯屯屯的来历

同上,未初始化的堆内存全部填成0xcd,字符串看就是“屯屯屯屯屯屯屯屯”。

锘的来历

微软在 UTF-8 文件头部加上了 EF BB BF BOM 标记。在不支持 BOM 的环境下对其进行 UTF-8 解码得到“锘”字。

如何用 PowerShell 撰写心灵鸡汤

关于励志段子

微信上传着一个励志段子,大意是:

如果26个英文字母 A B C D EF G H I J K L M N O P Q R S T U V W X Y Z 分别等于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1516 17 18 19 20 21 22 23 24 25 26。那么:

  • Knowledge (知识): K+N+O+W+L+E+D+G+E= 11+14+15+23+12+5+4+7+5=96%
  • Workhard (努力工作):W+O+R+K+H+A+R+D= 23+15+18+11+8+1+18+4 =98%
  • Luck(好运) L+U+C+K=12+21+3+11=47%
  • Love(爱情) L+O+V+E=12+15+22+5=54%
  • Money(金钱) M+O+N+E+Y=13+15+14+5+25=72%
  • Leadership(领导能力)L+E+A+D+E+R+S+H+I+P=12+5+1+4+5+18+19+9+16=89%
  • ATTITUDE(心态)A+T+T+I+T+U+D+E=1+20+20+9+20+21+4+5=100%

于是得出结论:用什么样的态度去看待人生,就会得到什么样的人生。

分析

这样的心灵鸡汤是怎样来的呢?我们用 PowerShell 脚本来琢磨一下。

1
2
3
4
5
6
7
8
function Get-Weight([string]$word) {
$word = $word.ToLower()
#Write-Host ([System.Text.Encoding]::ASCII.GetBytes($word) |
# ForEach-Object { $_ - 96 })
return ([System.Text.Encoding]::ASCII.GetBytes($word) |
ForEach-Object { $_ - 96 } |
Measure-Object -Sum).Sum
}

这个函数可以对任意字符串求值,例如以下测试代码将返回 6(abc = 1+2+3):

1
Get-Weight 'abc'

现在可以测试一下段子里用到的几个单词,并对结果进行排序:

1
2
'Knowledge', 'Workhard', 'Luck', 'Love', 'Money', 'Leadership', 'ATTITUDE' |
Sort-Object -Property @{Expression = { Get-Weight $_ }}

结果符合预期:

Luck
Love
Money
Knowledge
Leadership
Workhard
ATTITUDE

如何撰写鸡汤

以上实现了输入任意字符串数组,对它们进行求值和排序。但是如何选出这些单词呢?我们可以找一篇长文,例如从麻省理工找到莎士比亚的《哈姆雷特》全文,将它输进去拆解成单词试试:

1
2
3
4
5
$resp = Invoke-WebRequest 'http://shakespeare.mit.edu/hamlet/full.html'
$fullText = $resp.ParsedHtml.documentElement.innerText
$words = [regex]::Matches($fullText, '\b\w+\b') |
ForEach-Object { $_.Value } |
Sort-Object -Unique

这样几行代码,就可以将《哈姆雷特》全文的所有单词挑出来进行排序,并将结果保存在 $words 变量中。

最后套用我们上面写好的函数即可实现对所有单词求值排序:

1
2
3
4
5
$words |
Sort-Object -Property @{Expression = { Get-Weight $_ }} |
ForEach-Object {
"$_`t$(Get-Weight $_)"
}

结果大概是这样:

word weight
a 1
c 3
d 4
e 5
bad 7
be 7
I 9
letters 99
firmament 99
temperance 100
Writing 100
prosperously 199
unproportioned 200

有了这个长长的表格之后,撰写鸡汤就容易多了。只要按顺序挑出一些单词,设计一下台词即可。

完整的代码如下:

function Get-Weight([string]$word) {
    $word = $word.ToLower()
    #Write-Host ([System.Text.Encoding]::ASCII.GetBytes($word) |
    #    ForEach-Object { $_ - 96 })
    return ([System.Text.Encoding]::ASCII.GetBytes($word) |
        ForEach-Object { $_ - 96 } |
        Measure-Object -Sum).Sum
}

# Test
# Get-Weight 'abc'

if (!$resp) {
    $resp = Invoke-WebRequest 'http://shakespeare.mit.edu/hamlet/full.html'
}

$fullText = $resp.ParsedHtml.documentElement.innerText
$words = [regex]::Matches($fullText, '\b\w+\b') |
    ForEach-Object { $_.Value } |
    Sort-Object -Unique

# The following code will procuce output:
# Luck
# Love
# Money
# Knowledge
# Leadership
# Workhard
# ATTITUDE
'Knowledge', 'Workhard', 'Luck', 'Love', 'Money', 'Leadership', 'ATTITUDE' |
    Sort-Object -Property @{Expression = { Get-Weight $_ }}

$words |
    Sort-Object -Property @{Expression = { Get-Weight $_ }} |
    ForEach-Object {
        "$_`t$(Get-Weight $_)"
    }

后记

完整的代码可以在这里下载。鸡汤的原文请参见《是哪位高人琢磨出的这条微信,太牛了》。顺便发现了原文中的一个计算 bug——Leadership(领导能力)应是L+E+A+D+E+R+S+H+I+P=12+5+1+4+5+18+19+8+9+16=97%,而不是 89%。

怎么样,有没有一点理工男秒杀心灵鸡汤的味道?

用 PowerShell 显示 黑客帝国数码雨动画

请在 PowerShell 控制台中执行本脚本

今天在群里看到一个数码雨的课题,试着实现了一下:

【话痨】powershell传教士(1328486072) 12:58:11
话说有人用bat写出了数码雨,谁也用powershell写一个,我用powershell写了几个,总感觉不对。
【话痨】powershell传教士(1328486072) 12:58:52
有人对命令行数码雨,感兴趣么?

根据传教士的提示,改了一下,避免了闪烁。

实现效果

Matrix

源代码

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
## Prepare the screen
$host.UI.RawUI.BackgroundColor = "Black"
$host.UI.RawUI.ForegroundColor = "Green"

$charSet = '0123456789'.ToCharArray()

$width = 75
$height = [Console]::WindowHeight
$maxStringLength = 7
$minStringLength = 2
$maxSpaceLength = 20
$minSpaceLength = 6

$lines = New-Object System.Collections.ArrayList
$symbols = @()

for ($i = 0; $i -lt $width; $i++) {
$symbols += ''
}

function AddLine([string]$line) {
$lines.insert(0, $line)
if ($lines.Count -eq $height) {
$lines.RemoveAt($lines.Count - 1)
}
}

function ShowFrame() {
Write-Host ($lines.ToArray() -join "`n")
}

function TryGenerateSymbol() {
for ($i = 0; $i -lt $width; $i++) {
$column = $symbols[$i]
if ($column -eq '') {
# initial state, generate spaces
$symbols[$i] = New-Object String ' ', (Get-Random -Minimum $minSpaceLength -Maximum $maxSpaceLength)
} elseif ($column -eq ' ') {
# last space
$randomCount = Get-Random -Minimum $minStringLength -Maximum $maxStringLength
$chars = Get-Random -InputObject $charSet -Count $randomCount
$symbols[$i] = $column + ($chars -join '')
} elseif ($column.Length -eq 1) {
# last char
$symbols[$i] = $column + (New-Object String ' ', (Get-Random -Minimum $minSpaceLength -Maximum $maxSpaceLength))
}
}
}

function UpdateFrame() {
TryGenerateSymbol

$line = @()
for ($i = 0; $i -lt $width; $i++) {
$column = $symbols[$i]
$line += $column[0]
$symbols[$i] = $column.Substring(1, $column.Length - 1)
}
$line = $line -join ''
AddLine $line
}

try
{
$host.UI.RawUI.WindowSize = New-Object System.Management.Automation.Host.Size $width + 1, $height + 1
}
catch {}

try
{
$host.UI.RawUI.BufferSize = New-Object System.Management.Automation.Host.Size $width + 1, $height + 1
} catch {}

try
{
while($true)
{
if([Console]::KeyAvailable)
{
$key = [Console]::ReadKey()
if(($key.Key -eq 'Escape') -or
($key.Key -eq 'Q') -or
($key.Key -eq 'C'))
{
break
}
}

# Clear-Host

$host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 0,0

UpdateFrame
ShowFrame

$host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates `
0,([Console]::WindowHeight - 1)
Write-Host -NoNewLine 'Q or ESC to Quit'

Start-Sleep -m 100
}
}
finally
{
## Clean up, display exit screen
Clear-Host
"`n"
" Happy Scripting from PowerShell..."
" by Victor.Woo!"
"`n`n`n"
}

您也可以在这里下载 Matrix.ps1

在 PowerShell 中利用正则表达式来解析文本块

需求

给定一段文本,如:

1, abcd [xxxx]
vkjl gas kje asld
gew wef
2, bbb [wefs]
oioias wmfjalkjs
3, ccc [wegas]
kzxlj kjlwiewe ii

要求分割成多段以数字开头的文本块,如:

第一块:

1, abcd [xxxx]
vkjl gas kje asld
gew wef

第二块:

2, bbb [wefs]
oioias wmfjalkjs

第三块:

3, ccc [wegas]
kzxlj kjlwiewe ii

思路

  • 定义我们要东西为 n 个“block”。
  • 每个“block”的特征是:
    • 以数字开头
    • block 之前可能是整段文本的起始也有可能是一个回车符。
    • block 之后可能是一个回车符+下一行的数字也有可能是整段文本的结束。
  • block 之前和之后的回车符是不需要的
  • block 应该尽可能“非贪婪”,遇到下一个符合条件的,算作一个新的 block 开始。

其中,“block 之前和之后的回车符是不需要的”可以用正则表达式的“零宽断言”来解决。

代码

$subject = @'
1, abcd [xxxx]
vkjl gas kje asld
gew wef
2, bbb [wefs]
oioias wmfjalkjs
3, ccc [wegas]
kzxlj kjlwiewe ii
'@

$resultlist = new-object System.Collections.Specialized.StringCollection
$regex = [regex]@'
(?snx)(^|(?<=\n))
(?<block>\d, .*?)
((?=\n\d, )|$)
'@
$match = $regex.Match($subject)
while ($match.Success) {
    $resultlist.Add($match.Groups['block'].Value) | out-null
    $match = $match.NextMatch()
}

$resultlist | ForEach-Object {
    echo $_
    echo ---
}

输出结果

1, abcd [xxxx]
vkjl gas kje asld
gew wef
---
2, bbb [wefs]
oioias wmfjalkjs
---
3, ccc [wegas]
kzxlj kjlwiewe ii
---
PowerShell 技术 QQ 群