PowerShell 技能连载 - 读取文本文件(快速)

如果您需要加载大文本文件并使用 Get-Content,那么您可以节省大量时间。

如果您没有立即通过管道处理通过管道发出的结果,则可能需要添加参数 -ReadCount 0。这可以使读取文本文件的速度提升 100 倍。

如果没有此参数,Get-Content 会单独对每个文本行产生一次输出。如果这些行要传递给管道处理,那么没有问题。但是如果要将文本存储在变量中并使用其他处理方式,则这是浪费时间,例如经典的 foreach 循环。

PowerShell 技能连载 - Working with Get-WinEvent

如果您想从 Windows 事件日志中读取系统事件,Get-Eventlog 是个易于使用且简单的选择。这段代码能够获取最新的 10 个错误和警告事件:

1
PS> Get-EventLog -LogName System -EntryType Error,Warning -Newest 10

不幸的是,Get-Eventlog 已被弃用,并且它不再可用于 PowerShell 7。弃用有很明显的原因:该 cmdlet 只能从“经典”日志文件中读取,它很缓慢并且具有其他限制。

这就是为什么 PowerShell 在3.0版中推出更好的替代命令:Get-WinEvent。PowerShell 7 也有此 cmdlet。

Unfortunately, Get-WinEvent is much harder to use because there are no intuitive parameters, and instead your filter criteria needs to be specified as a hash table.
不幸的是,Get-Winevent 更难使用,因为没有直观的参数,而是需要通过哈希表指定过滤条件。

但是,通过“代理函数”,您可以指导 Get-WinEvent 使用您所熟悉的旧式参数。以下是代码:

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<#Suggestion to improve Get-WinEvent in order to make it compatible to the commonly used Get-EventLog callsBelow is a prototype using a proxy function. Run it to enhance Get-WinEvent.To get rid of the enhancement, either restart PowerShell or run:Remove-Item -Path function:Get-WinEventNote that the prototype emits the composed hash table to the console (green)
\#>function Get-WinEvent
{ [CmdletBinding(DefaultParameterSetName='GetLogSet', HelpUri='https://go.microsoft.com/fwlink/?LinkID=138336')]
param(

[Parameter(ParameterSetName='ListLogSet', Mandatory=$true, Position=0)]
[AllowEmptyCollection()]
[string[]]
${ListLog}, [Parameter(ParameterSetName='LogNameGetEventlog', Mandatory=$true, Position=0)] <#NEW\#>
[Parameter(ParameterSetName='GetLogSet', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${LogName}, [Parameter(ParameterSetName='ListProviderSet', Mandatory=$true, Position=0)]
[AllowEmptyCollection()]
[string[]]
${ListProvider}, <# Get-EventLog supports wildcards, Get-WinEvent does not. Needs to be corrected. #> [Parameter(ParameterSetName='GetProviderSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[string[]]
${ProviderName}, [Parameter(ParameterSetName='FileSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${Path}, [Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[ValidateRange(1, 9223372036854775807)]
[long]
${MaxEvents},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateRange(0, 2147483647)]
[int]
${Newest},
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[Parameter(ParameterSetName='LogNameGetEventlog')] <#NEW\#>
[Alias('Cn')]
[ValidateNotNullOrEmpty()] <#CORRECTED\#> [string] <# used to be [String[]], Get-WinEvent accepts [string] only, should be changed to accept string arrays \#> ${ComputerName}, [Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[Parameter(ParameterSetName='FileSet')]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential}, [Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='GetLogSet')]
[ValidateNotNull()]
[string]
${FilterXPath}, [Parameter(ParameterSetName='XmlQuerySet', Mandatory=$true, Position=0)]
[xml]
${FilterXml}, [Parameter(ParameterSetName='HashQuerySet', Mandatory=$true, Position=0)]
[hashtable[]]
${FilterHashtable}, [Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[switch]
${Force}, [Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[switch]
${Oldest},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[datetime]
${After}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[datetime]
${Before},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[string[]]
${UserName}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog', Position=1)]
[ValidateRange(0, 9223372036854775807)]
[ValidateNotNullOrEmpty()]
[long[]]
${InstanceId}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[ValidateRange(1, 2147483647)]
[int[]]
${Index}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('ET')]
[ValidateNotNullOrEmpty()]
[ValidateSet('Error','Information','FailureAudit','SuccessAudit','Warning')]
[string[]]
${EntryType}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('ABO')]
[ValidateNotNullOrEmpty()]
[string[]]
${Source}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('MSG')]
[ValidateNotNullOrEmpty()]
[string]
${Message}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[switch]
${AsBaseObject},
[Parameter(ParameterSetName='ListGetEventlog')]
[switch]
${List}, [Parameter(ParameterSetName='ListGetEventlog')]
[switch]
${AsString}



)

begin {
try {
$outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1 }
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Diagnostics\Get-WinEvent', [System.Management.Automation.CommandTypes]::Cmdlet)

\# if the user chose the Get-EventLog compatible parameters, \# compose the appropriate filter hash table $scriptCmd = if ($PSCmdlet.ParameterSetName -eq 'LogNameGetEventlog')
{
\# mandatory parameter $filter = @{
LogName = $PSBoundParameters['Logname']
}
$null = $PSBoundParameters.Remove('LogName')

if ($PSBoundParameters.ContainsKey('Before'))
{
$filter['EndTime'] = $PSBoundParameters['Before']
$null = $PSBoundParameters.Remove('Before')
}
if ($PSBoundParameters.ContainsKey('After'))
{
$filter['StartTime'] = $PSBoundParameters['After']
$null = $PSBoundParameters.Remove('After')
}
if ($PSBoundParameters.ContainsKey('EntryType'))
{
\# severity is translated to an integer array
$levelFlags = [System.Collections.Generic.List[int]]@()

\# string input converted to integer array if ($PSBoundParameters['EntryType'] -contains 'Error')
{
$levelFlags.Add(1) \# critical $levelFlags.Add(2) \# error }
if ($PSBoundParameters['EntryType'] -contains 'Warning')
{
$levelFlags.Add(3) \# warning }
if ($PSBoundParameters['EntryType'] -contains 'Information')
{
$levelFlags.Add(4) \# informational $levelFlags.Add(5) \# verbose }


\# default to 0 if ($levelFlags.Count -gt 0)
{
$filter['Level'] = [int[]]$levelFlags }

\# audit settings stored in Keywords key if ($PSBoundParameters['EntryType'] -contains 'FailureAudit')
{
$filter['Keywords'] += 0x10000000000000 }
if ($PSBoundParameters['EntryType'] -contains 'SuccessAudit')
{
$filter['Keywords'] += 0x20000000000000 }
$null = $PSBoundParameters.Remove('EntryType')
}
if ($PSBoundParameters.ContainsKey('InstanceId'))
{
$filter['ID'] = $PSBoundParameters['InstanceId']
$null = $PSBoundParameters.Remove('InstanceId')
}
if ($PSBoundParameters.ContainsKey('Source'))
{
$filter['ProviderName'] = $PSBoundParameters['Source']
$null = $PSBoundParameters.Remove('Source')
}

$PSBoundParameters['FilterHashtable'] = $filter Write-Host ($filter | Out-String) -ForegroundColor Green
if ($PSBoundParameters.ContainsKey('Newest'))
{
$PSBoundParameters['MaxEvents'] = $PSBoundParameters['Newest']
$null = $PSBoundParameters.Remove('Newest')
}
}


$scriptCmd =
{
& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw }
}

process {
try {
$steppablePipeline.Process($_)
} catch {
throw }
}

end {
try {
$steppablePipeline.End()
} catch {
throw }
}
<#

.ForwardHelpTargetName Microsoft.PowerShell.Diagnostics\Get-WinEvent .ForwardHelpCategory Cmdlet \#>}

当您执行完这个函数,就可以通过您在 Get-EventLog 中熟悉的相同参数使用 Get-WinEvent

1
2
3
4
5
6
PS> Get-WinEvent -LogName System -EntryType Error,Warning -Newest 10

Name Value
---- -----
LogName {System}
Level {1, 2, 3}

您还可以获得基于参数使用的哈希表过滤器的键和值。

PowerShell 技能连载 - 调整脚本性能

如果脚本运行速度速度慢,找出导致延迟的地方并优化并不是一件很简单的事。使用名为 “psprofiler” 的 PowerShell 模块,可以测试脚本中的每行所需的时间。它在 Windows PowerShell 和 PowerShell 7 中都能运行。

首先安装模块:

1
PS> Install-Module -Name PSProfiler -Scope CurrentUser

下一步,用 Measure-Script 执行脚本:

1
PS> Measure-Script -Path 'C:\Users\tobias\test123.ps1'

Once your script completes, you get a sophisticated report telling you exactly how often each line of your script was executed, and how long it took:
脚本完成后,就会获得一份复杂的报告,告诉您脚本每行执行的次数,以及消耗的时间:

  Count  Line       Time Taken Statement
  -----  ----       ---------- ---------
      1     1    00:00.0033734 $Path = "$env:temp\tv.json"
      0     2    00:00.0000000
      1     3    00:28.1602885 $data = Get-Content -Path $Path -Raw |
      0     4    00:00.0000000 ConvertFrom-Json |
      1     5    00:26.6558438 ForEach-Object { $_ } |
      0     6    00:00.0000000 ForEach-Object {
      0     7    00:00.0000000
 101000     8    00:01.4408993   $title = '{0,5} [{2}] "{1}" ({3})' -f ([Object[]]$_)
 101000     9    00:13.6815132   $title | Add-Member -MemberType NoteProperty -Name Data -Value $_ -PassThru
      0    10    00:00.0000000 }
      0    11    00:00.0000000
...

PowerShell 技能连载 - 将 Ticks 转换为 DateTime

偶尔,日期和时间信息以所谓的“缺陷”的格式存储为 “Ticks”。 Ticks 是自 01/01/1601 以来,100 纳秒的单位数。Active Directory 在内部使用此格式,但您也可以在其他地方找到它。 以下是以 “Ticks” 为单位的 Windows 安装时间的示例:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateTicks = $values.InstallTime

$installDateTicks

结果是(非常)大的 64 比特数字:

132457820129777032

要将 Ticks 转换为 DateTime,请使用 [DateTimeOffset]

1
2
3
4
5
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateTicks = $values.InstallTime

$installDate = [DateTimeOffset]::FromFileTime($installDateTicks)
$installDate.DateTime

PowerShell 技能连载 - 将 UNIX 时间转为 DateTime

“UNIX时间”计算自 01/01/1970 以来经过的秒数。

例如,在 Windows 中,您可以从 Windows 注册表中读取安装日期,返回的值为 “Unix时间”:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateUnix = $values.InstallDate

$installDateUnix

结果是类似这样的大数字:

1601308412

To convert “Unix time” to a real DateTime value, .NET Framework provides a type called [DateTimeOffset]:
要将“UNIX时间”转换为真实的 DateTime 值,请使用 .NET Framework 提供的 [DateTimeOffset] 类:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateUnix = $values.InstallDate

[DateTimeOffset]::FromUnixTimeSeconds($installDateUnix)

现在您能得到不同的日期和时间表示:

DateTime      : 28.09.2020 15:53:32
UtcDateTime   : 28.09.2020 15:53:32
LocalDateTime : 28.09.2020 17:53:32
Date          : 28.09.2020 00:00:00
Day           : 28
DayOfWeek     : Monday
DayOfYear     : 272
Hour          : 15
Millisecond   : 0
Minute        : 53
Month         : 9
Offset        : 00:00:00
Second        : 32
Ticks         : 637369052120000000
UtcTicks      : 637369052120000000
TimeOfDay     : 15:53:32
Year          : 2020

要获取本地格式的安装时间,您可以在一行代码中写完它:

1
2
3
PS> [DateTimeOffset]::FromUnixTimeSeconds((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').InstallDate).DateTime

Moday, September 28, 2020 15:53:32

PowerShell 技能连载 - 创建动态参数

动态参数是一种特殊的参数,可以根据运行时条件显示或隐藏。 您的 PowerShell 函数可以例如具有一个参数,并基于用户选择的操作,将显示其他参数。或者,只有在用户具有管理员权限时才能显示参数。

不幸的是,组合动态参数并不是一件轻松的事。借助称为 “dynpar” 的模块,使用动态参数变得同样简单,就像使用“普通”静态参数一样简单,然后您可以简单地使用名为 [Dynamic()] 的新属性指定动态参数,该属性告诉 PowerShell 需要满足以哪些条件以显示参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
param
(
# regular static parameter
[string]
$Normal,

# show -Lunch only at 11 a.m. or later
[Dynamic({(Get-Date).Hour -ge 11})]
[switch]
$Lunch,

# show -Mount only when -Path refers to a local path (and not a UNC path)
[string]
$Path,

[Dynamic({$PSBoundParameters['Path'] -match '^[a-z]:'})]
[switch]
$Mount
)

您可以在此处找到一份详细的操作指南:https://github.com/tobiaspsp/modules.dynpar

PowerShell 技能连载 - 分割文本行(第 3 部分)

在上一个技能中,我们将一大块多行文本拆分为单独的行,并删除了所有空行。

然而,当一行不是真正的空,而是包含空格(空格或制表符)时,它仍然会被输出:

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
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

$data = $data.Trim()

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'
$array = $data -split $regex

$array.Count

$c = 0
Foreach ($_ in $array)
{
'{0:d2} {1}' -f $c, $_
$c++
}

在这里,我们在 “Server2” 正上方的行中添加了几个空格(当然,在列表中看不到)。以下是执行结果:

00 Server1
01
02 Server2
03 Cluster4

由于我们要在任意数量的 CR 和 LF 字符处拆分,因此空格会破坏该模式。

与其将正则表达式变成一个更复杂的野兽,不如为这些事情附加一个简单的 Where-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
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

$data = $data.Trim()

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'
$array = $data -split $regex |
Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false }

$array.Count

$c = 0
Foreach ($_ in $array)
{
'{0:d2} {1}' -f $c, $_
$c++
}

[string]::IsNullOrEmpty() 代表我们所追求的情况,因此符合条件的行被 Where-Object 删除。结果就是所需要的理想结果:

00 Server1
01 Server2
02 Cluster4

PowerShell 技能连载 - 分割文本行(第 2 部分)

假设您的脚本获取文本输入数据,并且您需要将文本拆分为单独的行。在上一个技能中,我们建议了一些正则表达式来完成这项工作。但是如果输入文本包含空行怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'

如您所见,我们使用的正则表达式会自动处理文本中间的空白行,但文本开头或结尾的空白行会保留。

00
01 Server1
02 Server2
03 Cluster4
04

那是因为我们在任意数量的新行处分割,所以我们也在文本的开头和结尾处分割。我们实际上是自己生成了这两个剩余的空白行。

为了避免这些空行,我们必须确保文本的开头和结尾没有换行符。这就是 Trim() 可以做的事情:

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
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

$data = $data.Trim()

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'
$array = $data -split $regex

$array.Count

$c = 0
Foreach ($_ in $array)
{
'{0:d2} {1}' -f $c, $_
$c++
}



00 Server1
01 Server2
02 Cluster4

PowerShell 技能连载 - 分割文本行(第 1 部分)

有时,您需要逐行处理多行文本。下面是一个多行字符串的示例:

1
2
3
4
5
6
7
8
9
10
11
# working with 1-dimensional input

# $data is a single string
$data = @'
Server1
Server2
Cluster4
'@

$data.GetType().FullName
$data.Count

将文本拆分为单独的行的一种有效方法是使用带有正则表达式的 -split 运算符,该表达式可以处理各种依赖于平台的行终止符:

1
2
3
4
5
6
7
8
9
10
# split the string in individual lines
# $array is an array with individual lines now

$regex = '[\r\n]{1,}'
$array = $data -split $regex

$array.GetType().FullName
$array.Count

$array

以下是您在脚本中遇到的 $regex 中的正则表达式的一些替代方案:

分隔用的正则表达式 说明
/r 类似于“回车”(ASCII 13)。如果操作系统不是用该字符作为换行符,则分割将会失败。如果操作系统使用该字符加上“换行”(ASCII 10),那么多出来的不可见的换行符会破坏字符串。
/n 类似上面的情况,只是相反。
[\r\n]+ 与上面的示例代码相同。如果有一个或多个字符,PowerShell 会在两个字符处拆分。这样,CR、LF 或 CRLF、LFCR 在拆分时都被删除。但是,多个连续的新行也将全部删除:CRCRCR 或 CRLFCRLF。
(\r\n \r

如果您从文本文件中读取文本,Get-Content 会自动将文本拆分为行。要将整个文本内容作为单个字符串读取,则需要添加 -Raw 参数。

PowerShell 技能连载 - 截屏

借助 System.Windows.Forms 中的类型,PowerShell 可以轻松捕获屏幕并将屏幕截图保存到文件中。下面的代码捕获整个虚拟屏幕,将屏幕截图保存到文件中,然后在相关程序中打开位图文件(如果有):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$Path = "$Env:temp\screenshot.bmp"
Add-Type -AssemblyName System.Windows.Forms

$screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
$width = $screen.Width
$height = $screen.Height
$left = $screen.Left
$top = $screen.Top
$bitmap = [System.Drawing.Bitmap]::new($width, $height)
$MyDrawing = [System.Drawing.Graphics]::FromImage($bitmap)
$MyDrawing.CopyFromScreen($left, $top, 0, 0, $bitmap.Size)

$bitmap.Save($Path)
Start-Process -FilePath $Path