PowerShell 技能连载 - 通过 PowerShell 调用 Excel 宏
PowerShell 可以调用 Microsoft Excel 工作表,并执行其中的宏。由于这只对可见的 Excel 程序窗口有效,所以当您尝试进行安全敏感操作,例如宏时,最好保持 Excel 打开(参见以下代码)来查看警告信息:
1 | # file path to your XLA file with macros |
PowerShell 可以调用 Microsoft Excel 工作表,并执行其中的宏。由于这只对可见的 Excel 程序窗口有效,所以当您尝试进行安全敏感操作,例如宏时,最好保持 Excel 打开(参见以下代码)来查看警告信息:
1 | # file path to your XLA file with macros |
是否曾好奇如何列出一个函数或 cmdlet 暴露出的所有属性?以下是实现方法:
1 | Get-Help Get-Service -Parameter * | Select-Object -ExpandProperty name |
Get-Help
提供了一系列关于参数的有用的信息和元数据。如果您只希望转储支持管道输入的参数,以下是实现方法:
1 | Get-Help Get-Service -Parameter * | |
“pipelineInput
“ 属性暴露了通过管道接收到的一个属性的类型。不幸的是,它包含了一个本地化的字符串,所以一个区分的好方法是取字符串的长度。
输出的结果类似这样,并且可以从管道上游的命令中接受管道的输入,以及接受数据类型:
1 | name pipelineInput parameterValue |
当您用 Import-Csv
将一个 CSV 列表导入 PowerShell,或用任何其它类型的对象来处理时:如何自动确定对象的属性?以下是一个简单的方法:
1 | # take any object, and dump a list of its properties |
为什么这种方法有用?有许多使用场景。例如,您可以检测一个注册表键的名称,支持用通配符转储所有的命令:
1 | $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" |
对象和类型中包括方法和属性等成员,但只有少数是公开可见和可使用的。还有许多隐藏(私有)的成员。在生产系统上使用这些成员是不明智的,当它们更新版本的时候,您并不会得到通知,所以可能会工作不正常。所以对于高级的 PowerShell 开发者来说是一个很好奇的地方。
有一个免费的 PowerShell 模块名为 ImpliedReflection
,能将私有的成员变为可见,甚至在 PowerShell ISE 和 Visual Studio Code 的 IntelliSense 中,而且您可以运行那些成员。
例如,以下公有的类型只暴露了一个公有的方法,您可以用它来构造 PowerShell 的模块路径。
1 | PS> [System.Management.Automation.ModuleIntrinsics]::GetModulePath |
现在我们像这样安装 ImpliedReflection
:
1 | Install-Module -Name ImpliedReflection -Scope CurrentUser |
当该模块安装以后,您需要先允许该扩展:
1 | PS> Enable-ImpliedReflection -Force |
现在,当您重新访问该类型并查看它的成员时,仍然只显示其公有成员。只有当您交互式输出该类型时,该扩展才起作用:
1 | PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static |
现在您可以使用私有的成员了,好像它们是公有的一样:
1 | PS> [System.Management.Automation.ModuleIntrinsics]::GetPersonalModulePath() |
再次强调,这仅仅适用于希望更深入了解对象和类型内部的工作机制的高级用户。ImpliedReflection
模块用于操作私有成员。在生产环境下,您需要十分谨慎地操作私有成员。
在之前的技能中我们演示了如何改进循环,尤其是管道操作。要将这个知识点转为函数,请看一下这个简单的管道相关的函数,它的功能是统计管道传送过来的元素数量:
1 | function Count-Stuff |
当您运行这个函数来统计一个非常大数量的对象时,可能会得到这样的结果:
1 | PS> $start = Get-Date |
现在我们去掉属性,将这个函数转换为一个“简单函数”
1 | function Count-Stuff |
由于没有定义管道参数,从管道的输入在 process 块中保存在 “$_
“ 变量中以及在 end 块中,一个名为 “$input
“ 的迭代器保存了所有收到的数据。请注意我们的计数示例并不需要这些变量,因为它只是统计了输入的数量。
以下是执行结果:
1 | $start = Get-Date |
显然,处理大量对象的时候,简单函数比管道函数的性能要高得多。
当然,仅当用管道传输大量对象的时候效果比较明显。但当做更多复杂操作的时候,效果会更加明显。例如,以下代码创建 5 位的服务器列表,用高级函数的时候,它在我们的测试系统上大约消耗 10 秒钟:
1 | function Get-Servername |
使用简单函数可以在 2 秒之内得到相同的结果(在 PowerShell 5.1 和 6.1 相同):
1 | function Get-ServerName |
在前一个技能中我们对一种常见的脚本模式提升了它的性能。现在,我们用一个更不常见的技巧进一步挤出更多性能。以下是我们上次的进展:
1 | $start = Get-Date |
我们已经将执行时间从 6+ 分钟降到在 PowerShell 5.1 中 46 秒,在 PowerShell 6.1 中 1.46 秒。
现在我们看一看这个小改动——它返回完全相同的结果:
1 | $start = Get-Date |
这段神奇的代码在 PowerShell 5.1 中只花了 0.2 秒,在 PowerShell 6.1 中只花了 0.5 秒。
如您所见,这段代码只是将 ForEach-Object
cmdlet 替换为等价的 $ { process { $_ }}
。结果发现,由于 cmdlet 绑定的高级函数,管道操作符的执行效率被严重地拖慢了。如果使用一个简单的函数(或是一个纯脚本块),就可以显著地加速执行速度。结合昨天的技能,我们已经设法将处理从 6+ 分钟提速到 200 毫秒,而得到完全相同的结果。
请注意一件事情:这些优化技术只适用于大量迭代的循环。如果您的循环只迭代几百次,那么感受不到显著的区别。然而,一个循环迭代越多次,错误的设计就会导致越严重的消耗。
以下是一个在许多 PowerShell 脚本中常见的错误:
1 | $start = Get-Date |
在这个设计中,脚本使用了一个空的数组,然后用某种循环向数组中增加元素。当运行它的时候,会发现它停不下来。实际上这段代码在我们的测试系统中需要超过 6 分钟时间,甚至有可能在您的电脑上要消耗更多时间。
以下是导致缓慢的元凶:对数组使用操作符 “+=
“ 是有问题的。因为每次使用 “+=
“ 时,它表面上动态扩展了该数组,实际上却是创建了一个元素数量更多一个的新数组。
要显著地提升性能,请让 PowerShell 来创建数组:当返回多个元素时,PowerShell 自动高速创建数组:
1 | $start = Get-Date |
结果完全相同,但和消耗 6+ 分钟不同的是,它在 PowerShell 5.1 上只用了 46 秒钟,而在 PowerShell 6.1 上仅仅用了 1.46 秒。我们将会用另一个技巧来进一步提升性能。
以下是一段演示如何在 Windows 注册表中存储私人信息的代码:
1 | # store settings here |
当运行这段代码时,它将返回一个对象,该对象告诉您上次运行此脚本是什么时候,以及从那以后运行了多长时间。
If you use Outlook to organize your calendar events, here is a useful PowerShell function that connects to Outlook and dumps your calendar entries:
Function Get-OutlookCalendar
{
# load the required .NET types
Add-Type -AssemblyName 'Microsoft.Office.Interop.Outlook'
# access Outlook object model
$outlook = New-Object -ComObject outlook.application
# connect to the appropriate location
$namespace = $outlook.GetNameSpace('MAPI')
$Calendar = [Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderCalendar
$folder = $namespace.getDefaultFolder($Calendar)
# get calendar items
$folder.items |
Select-Object -Property Start, Categories, Subject, IsRecurring, Organizer
}
Try this:
PS> Get-OutlookCalendar | Out-GridView
How would you query for all AD users with names that start with a “e”-“g”? You shouldn’t use a client-side filter such as Where-Object. One thing you can do is use the -Filter parameter with logical operators such as -and and -or:
Get-ADUser -filter {(name -lt 'E') -or (name -gt 'G')} |
Select-Object -ExpandProperty Name
this example requires the free RSAT tools from Microsoft to be installed)