PowerShell 技能连载 - 读取网站内容

通常情况下,通过 PowerShell 的 Invoke-WebRequest 命令来获取原始的 HTML 网站内容是很常见的情况。脚本可以处理 HTML 内容并对它做任意操作,例如用正则表达式从中提取信息:

1
2
3
$url = "www.tagesschau.de"
$w = Invoke-WebRequest -Uri $url -UseBasicParsing
$w.Content

然而,有些时候一个网站的内容是通过客户端脚本代码动态创建的。那么,Invoke-WebRequest` 并不能返回浏览器中所见的完整 HTML 内容。如果仍要获取 HTML 信息,您需要借助一个真实的 WEB 浏览器。一个简单的方法是使用内置的 Internet Explorer:

1
2
3
4
5
6
7
8
$ie = New-Object -ComObject InternetExplorer.Application
$ie.Navigate($url)
do
{
Start-Sleep -Milliseconds 200
} while ($ie.ReadyState -ne 4)

$ie.Document.building.innerHTML

PowerShell 技能连载 - 接受不同的参数类型

个别情况下,您可能会希望创建一个可以接受不同参数类型的函数。假设您希望用户既可以传入一个雇员姓名,也可以传入一个 Active Directory 对象。

在 PowerShell 中有一个固定的原则:变量不能在同一时刻有不同的数据类型。由于参数是变量,所以一个指定的参数只能有一个唯一的类型。

然而,您可以使用参数集来定义互斥的参数,这是一种解决多个输入类型的好方法。以下是一个既可以输入服务名,也可以输入服务对象的示例函数。这基本上是 Get-Service 内部的工作原理,以下示例展示了它的实现方式:

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
function Get-MyService
{
[CmdletBinding(DefaultParameterSetName="String")]
param
(
[String]
[Parameter(Mandatory,Position=0,ValueFromPipeline,ParameterSetName='String')]
$Name,

[Parameter(Mandatory,Position=0,ValueFromPipeline,ParameterSetName='Object')]
[System.ServiceProcess.ServiceController]
$Service
)

process
{
# if the user entered a string, get the real object
if ($PSCmdlet.ParameterSetName -eq 'String')
{
$Service = Get-Service -Name $Name
}
else
{
# else, if the user entered (piped) the expected object in the first place,
# you are good to go
}

# this call tells you which parameter set was invoked
$PSCmdlet.ParameterSetName

# at the end, you have an object
$Service
}

}

我们看看该函数的使用:

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
PS> Get-MyService -Name spooler
String

Status Name DisplayName
------ ---- -----------
Running spooler Print Spooler



PS> $spooler = Get-Service -Name Spooler

PS> Get-MyService -Service $spooler
Object

Status Name DisplayName
------ ---- -----------
Running Spooler Print Spooler



PS> "Spooler" | Get-MyService
String

Status Name DisplayName
------ ---- -----------
Running Spooler Print Spooler



PS> $spooler | Get-MyService
Object

Status Name DisplayName
------ ---- -----------
Running Spooler Print Spooler

如您所见,用户可以传入一个服务名或是 Service 对象。Get-MyService 函数模仿 Get-Service 内部的实现机制,并且返回一个 Service 对象,无论输入什么类型。以下是上述函数的语法:

1
2
3
**Syntax**
Get-MyService [-Name] <string> [<CommonParameters>]
Get-MyService [-Service] <ServiceController> [<CommonParameters>]

PowerShell 技能连载 - 将 VBScript 翻译为 PowerShell

大多数旧的 VBS 脚本可以容易地翻译为 PowerShell。VBS 中关键的命令是 “CreateObject“。它能让您操作系统的库。PowerShell 将 “CreateObject“ 翻译为 “New-Object -ComObject“,而对象模型和成员名称保持相同:

当把这段代码保存为扩展名为 “.vbs” 的文本文件后,这段 VBS 脚本就可以发出语音:

1
2
3
Set obj = CreateObject("Sapi.SpVoice")

obj.Speak "Hello World!"

对应的 PowerShell 代码类似这样:

1
2
$obj = New-Object -ComObject "Sapi.SpVoice"
$obj.Speak("Hello World!")

只有少量的 VBS 内置成员,例如 MsgBoxInputBox。要翻译这些代码,您需要引入 “Microsoft.VisualBasic.Interaction“ 类型。以下是调用 MsgBoxInputBox 的 PowerShell 代码:

1
2
3
4
5
6
Add-Type -AssemblyName Microsoft.VisualBasic

$result = [Microsoft.VisualBasic.Interaction]::MsgBox("Do you want to restart?", 3, "Question")
$result

$result = [Microsoft.VisualBasic.Interaction]::InputBox("Your name?", $env:username, "Enter Name")

以下是支持的 Visual Basic 成员的完整列表:

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
PS> [Microsoft.VisualBasic.Interaction] | Get-Member -Stati


TypeName: Microsoft.VisualBasic.Interaction

Name MemberType Definition
---- ---------- ----------
AppActivate Method static void AppActivate(int ProcessId), static vo...
Beep Method static void Beep()
CallByName Method static System.Object CallByName(System.Object Obj...
Choose Method static System.Object Choose(double Index, Params ...
Command Method static string Command()
CreateObject Method static System.Object CreateObject(string ProgId, ...
DeleteSetting Method static void DeleteSetting(string AppName, string ...
Environ Method static string Environ(int Expression), static str...
Equals Method static bool Equals(System.Object objA, System.Obj...
GetAllSettings Method static string[,] GetAllSettings(string AppName, s...
GetObject Method static System.Object GetObject(string PathName, s...
GetSetting Method static string GetSetting(string AppName, string S...
IIf Method static System.Object IIf(bool Expression, System....
InputBox Method static string InputBox(string Prompt, string Titl...
MsgBox Method static Microsoft.VisualBasic.MsgBoxResult MsgBox(...
Partition Method static string Partition(long Number, long Start, ...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, S...
SaveSetting Method static void SaveSetting(string AppName, string Se...
Shell Method static int Shell(string PathName, Microsoft.Visua...
Switch Method static System.Object Switch(Params System.Object[...



PS>

PowerShell 技能连载 - 通过 PowerShell 调用 Excel 宏

PowerShell 可以调用 Microsoft Excel 工作表,并执行其中的宏。由于这只对可见的 Excel 程序窗口有效,所以当您尝试进行安全敏感操作,例如宏时,最好保持 Excel 打开(参见以下代码)来查看警告信息:

1
2
3
4
5
6
7
8
9
# file path to your XLA file with macros
$FilePath = "c:\test\file.xla"
# macro name to run
$Macro = "AddData"

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$wb = $excel.Workbooks.Add($FilePath)
$excel.Run($Macro)

PowerShell 技能连载 - 编程列出所有 Cmdlet 或函数参数的列表

是否曾好奇如何列出一个函数或 cmdlet 暴露出的所有属性?以下是实现方法:

1
Get-Help Get-Service -Parameter * | Select-Object -ExpandProperty name

Get-Help 提供了一系列关于参数的有用的信息和元数据。如果您只希望转储支持管道输入的参数,以下是实现方法:

1
2
3
Get-Help Get-Service -Parameter * |
Where-Object { $_.pipelineInput.Length -gt 10 } |
Select-Object -Property name, pipelineinput, parameterValue

pipelineInput“ 属性暴露了通过管道接收到的一个属性的类型。不幸的是,它包含了一个本地化的字符串,所以一个区分的好方法是取字符串的长度。

输出的结果类似这样,并且可以从管道上游的命令中接受管道的输入,以及接受数据类型:

1
2
3
4
5
name         pipelineInput                  parameterValue
---- ------------- --------------
ComputerName True (ByPropertyName) String[]
InputObject True (ByValue) ServiceController[]
Name True (ByPropertyName, ByValue) String[]

PowerShell 技能连载 - 编程检查对象属性

当您用 Import-Csv 将一个 CSV 列表导入 PowerShell,或用任何其它类型的对象来处理时:如何自动确定对象的属性?以下是一个简单的方法:

1
2
3
4
5
# take any object, and dump a list of its properties
Get-Process -Id $pid |
Get-Member -MemberType *property |
Select-Object -ExpandProperty Name |
Sort-Object

为什么这种方法有用?有许多使用场景。例如,您可以检测一个注册表键的名称,支持用通配符转储所有的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"

# get actual registry values from path
$Values = Get-ItemProperty -Path $RegPath

# exclude default properties
$default = 'PSChildName','PSDrive','PSParentPath','PSPath','PSProvider'

# each value surfaces as object property
# get property (value) names
$keyNames = $Values |
Get-Member -MemberType *Property |
Select-Object -ExpandProperty Name |
Where-Object { $_ -notin $default } |
Sort-Object

# dump autostart programs
$keyNames | ForEach-Object {
$values.$_
}

PowerShell 技能连载 - 存取隐藏(私有)成员

对象和类型中包括方法和属性等成员,但只有少数是公开可见和可使用的。还有许多隐藏(私有)的成员。在生产系统上使用这些成员是不明智的,当它们更新版本的时候,您并不会得到通知,所以可能会工作不正常。所以对于高级的 PowerShell 开发者来说是一个很好奇的地方。

有一个免费的 PowerShell 模块名为 ImpliedReflection,能将私有的成员变为可见,甚至在 PowerShell ISE 和 Visual Studio Code 的 IntelliSense 中,而且您可以运行那些成员。

例如,以下公有的类型只暴露了一个公有的方法,您可以用它来构造 PowerShell 的模块路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PS> [System.Management.Automation.ModuleIntrinsics]::GetModulePath

OverloadDefinitions
-------------------
static string GetModulePath(string currentProcessModulePath, string hklmMachineModulePath, string
hkcuUserModulePath)




PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
Equals Method static bool Equals(System.Object objA, System.Object objB)
GetModulePath Method static string GetModulePath(string currentProcessModulePath, string hklmMa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB)

现在我们像这样安装 ImpliedReflection

1
Install-Module -Name ImpliedReflection -Scope CurrentUser

当该模块安装以后,您需要先允许该扩展:

1
PS> Enable-ImpliedReflection -Force

现在,当您重新访问该类型并查看它的成员时,仍然只显示其公有成员。只有当您交互式输出该类型时,该扩展才起作用:

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
PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
Equals Method static bool Equals(System.Object objA, System.Object objB)
GetModulePath Method static string GetModulePath(string currentProcessModulePath, string hklmMa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB)



PS> [System.Management.Automation.ModuleIntrinsics]

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ModuleIntrinsics System.Object



PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
AddToPath Method static string AddToPath(string basePath, string pathToAdd, ...
CombineSystemModulePaths Method static string CombineSystemModulePaths()
Equals Method static bool Equals(System.Object objA, System.Object objB)
ExportModuleMembers Method static void ExportModuleMembers(System.Management.Automatio...
GetDscModulePath Method static string GetDscModulePath()
GetExpandedEnvironmentVariable Method static string GetExpandedEnvironmentVariable(string name, S...
GetManifestGuid Method static guid GetManifestGuid(string manifestPath)
GetManifestModuleVersion Method static version GetManifestModuleVersion(string manifestPath)
GetModuleName Method static string GetModuleName(string path)
GetModulePath Method static string GetModulePath(string currentProcessModulePath...
GetPersonalModulePath Method static string GetPersonalModulePath()
GetSystemwideModulePath Method static string GetSystemwideModulePath()
IsModuleMatchingModuleSpec Method static bool IsModuleMatchingModuleSpec(psmoduleinfo moduleI...
IsPowerShellModuleExtension Method static bool IsPowerShellModuleExtension(string extension)
NewAliasInfo Method static System.Management.Automation.AliasInfo NewAliasInfo(...
PathContainsSubstring Method static int PathContainsSubstring(string pathToScan, string ...
PatternContainsWildcard Method static bool PatternContainsWildcard(System.Collections.Gene...
ProcessOneModulePath Method static string ProcessOneModulePath(System.Management.Automa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Obje...
RemoveNestedModuleFunctions Method static void RemoveNestedModuleFunctions(psmoduleinfo module)
SetModulePath Method static string SetModulePath()
SortAndRemoveDuplicates Method static void SortAndRemoveDuplicates[T](System.Collections.G...
_ctor Method static System.Management.Automation.ModuleIntrinsics _ctor(...
MaxModuleNestingDepth Property static int MaxModuleNestingDepth {get;}
PSModuleExtensions Property static string[] PSModuleExtensions {get;set;}
PSModuleProcessableExtensions Property static string[] PSModuleProcessableExtensions {get;set;}
SystemWideModulePath Property static string SystemWideModulePath {get;set;}
Tracer Property static System.Management.Automation.PSTraceSource Tracer {g...

现在您可以使用私有的成员了,好像它们是公有的一样:

1
2
3
4
5
PS> [System.Management.Automation.ModuleIntrinsics]::GetPersonalModulePath()
C:\Users\tobwe\Documents\WindowsPowerShell\Modules

PS> [System.Management.Automation.ModuleIntrinsics]::SystemWideModulePath
c:\windows\system32\windowspowershell\v1.0\Modules

再次强调,这仅仅适用于希望更深入了解对象和类型内部的工作机制的高级用户。ImpliedReflection 模块用于操作私有成员。在生产环境下,您需要十分谨慎地操作私有成员。

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
function Count-Stuff
{
param
(
# pipeline-aware input parameter
[Parameter(ValueFromPipeline)]
$InputElement
)

begin
{
# initialize counter
$c = 0
}
process
{
# increment counter for each incoming object
$c++
}
end
{
# output sum
$c
}
}

当您运行这个函数来统计一个非常大数量的对象时,可能会得到这样的结果:

1
2
3
4
5
6
7
8
9
PS> $start = Get-Date
1..1000000 | Count-Stuff
(Get-Date) - $start

1000000

...
TotalMilliseconds : 3895,5848
...

现在我们去掉属性,将这个函数转换为一个“简单函数”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Count-Stuff
{
begin
{
# initialize counter
$c = 0
}
process
{
# increment counter for each incoming object
$c++
}
end
{
# output sum
$c
}
}

由于没有定义管道参数,从管道的输入在 process 块中保存在 “$_“ 变量中以及在 end 块中,一个名为 “$input“ 的迭代器保存了所有收到的数据。请注意我们的计数示例并不需要这些变量,因为它只是统计了输入的数量。

以下是执行结果:

1
2
3
4
5
6
7
8
9
$start = Get-Date
1..1000000 | Count-Stuff
(Get-Date) - $start

1000000

...
TotalMilliseconds : 690,1558
...

显然,处理大量对象的时候,简单函数比管道函数的性能要高得多。

当然,仅当用管道传输大量对象的时候效果比较明显。但当做更多复杂操作的时候,效果会更加明显。例如,以下代码创建 5 位的服务器列表,用高级函数的时候,它在我们的测试系统上大约消耗 10 秒钟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Get-Servername
{
param
(
# pipeline-aware input parameter
[Parameter(ValueFromPipeline)]
$InputElement
)

process
{
"Server{0:n5}" -f $InputElement
}
}



$start = Get-Date
$list = 1..1000000 | Get-ServerName
(Get-Date) - $start

使用简单函数可以在 2 秒之内得到相同的结果(在 PowerShell 5.1 和 6.1 相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
function Get-ServerName
{
process
{
"Server{0:n5}" -f $InputElement
}
}



$start = Get-Date
$list = 1..1000000 | Get-ServerName
(Get-Date) - $start

PowerShell 技能连载 - 性能(第 2 部分):从 2 秒到 200 毫秒

在前一个技能中我们对一种常见的脚本模式提升了它的性能。现在,我们用一个更不常见的技巧进一步挤出更多性能。以下是我们上次的进展:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..100000 | ForEach-Object {
"I am adding $_"
}

$bucket.Count

(Get-Date) - $start

我们已经将执行时间从 6+ 分钟降到在 PowerShell 5.1 中 46 秒,在 PowerShell 6.1 中 1.46 秒。

现在我们看一看这个小改动——它返回完全相同的结果:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..100000 | & { process {
"I am adding $_"
} }

$bucket.Count

(Get-Date) - $start

这段神奇的代码在 PowerShell 5.1 中只花了 0.2 秒,在 PowerShell 6.1 中只花了 0.5 秒。

如您所见,这段代码只是将 ForEach-Object cmdlet 替换为等价的 $ { process { $_ }}。结果发现,由于 cmdlet 绑定的高级函数,管道操作符的执行效率被严重地拖慢了。如果使用一个简单的函数(或是一个纯脚本块),就可以显著地加速执行速度。结合昨天的技能,我们已经设法将处理从 6+ 分钟提速到 200 毫秒,而得到完全相同的结果。

请注意一件事情:这些优化技术只适用于大量迭代的循环。如果您的循环只迭代几百次,那么感受不到显著的区别。然而,一个循环迭代越多次,错误的设计就会导致越严重的消耗。

PowerShell 技能连载 - 性能(第 1 部分):从 6 分钟到 2 秒钟

以下是一个在许多 PowerShell 脚本中常见的错误:

1
2
3
4
5
6
7
8
9
10
11
$start = Get-Date

$bucket = @()

1..100000 | ForEach-Object {
$bucket += "I am adding $_"
}

$bucket.Count

(Get-Date) - $start

在这个设计中,脚本使用了一个空的数组,然后用某种循环向数组中增加元素。当运行它的时候,会发现它停不下来。实际上这段代码在我们的测试系统中需要超过 6 分钟时间,甚至有可能在您的电脑上要消耗更多时间。

以下是导致缓慢的元凶:对数组使用操作符 “+=“ 是有问题的。因为每次使用 “+=“ 时,它表面上动态扩展了该数组,实际上却是创建了一个元素数量更多一个的新数组。

要显著地提升性能,请让 PowerShell 来创建数组:当返回多个元素时,PowerShell 自动高速创建数组:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..10000 | ForEach-Object {
"I am adding $_"
}

$bucket.Count

(Get-Date) - $start

结果完全相同,但和消耗 6+ 分钟不同的是,它在 PowerShell 5.1 上只用了 46 秒钟,而在 PowerShell 6.1 上仅仅用了 1.46 秒。我们将会用另一个技巧来进一步提升性能。