PowerShell 技能连载 - 将大文件拆分成小片段(第 1 部分)

PowerShell 可以将大文件拆分成多个小片段,例如将它们做为电子邮件附件发送。今天,我们关注如何分割文件。在下一个技能中,我们将演示如何将各个部分合并在一起。

要将大文件分割成小片段,我们创建了一个名为 Split-File 的函数。它工作起来类似这样:

1
2
3
4
5
6
7
PS> Split-File -Path "C:\Users\tobwe\Downloads\Woman putting gas in Tesla.mp4" -PartSizeBytes 6MB -Verbose
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.00.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.01.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.02.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.03.part...

PS C:\>

-PartSizeByte 参数设置最大的分片尺寸,在我们的例子中是 6MB。当您指定了 -Verbose 参数,该函数将在创建分片文件时显示分片文件名。

要使用 Split-File 函数,您需要运行以下代码:

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
function Split-File
{

param
(
[Parameter(Mandatory)]
[String]
$Path,

[Int32]
$PartSizeBytes = 1MB
)

try
{
# get the path parts to construct the individual part
# file names:
$fullBaseName = [IO.Path]::GetFileName($Path)
$baseName = [IO.Path]::GetFileNameWithoutExtension($Path)
$parentFolder = [IO.Path]::GetDirectoryName($Path)
$extension = [IO.Path]::GetExtension($Path)

# get the original file size and calculate the
# number of required parts:
$originalFile = New-Object System.IO.FileInfo($Path)
$totalChunks = [int]($originalFile.Length / $PartSizeBytes) + 1
$digitCount = [int][Math]::Log10($totalChunks) + 1

# read the original file and split into chunks:
$reader = [IO.File]::OpenRead($Path)
$count = 0
$buffer = New-Object Byte[] $PartSizeBytes
$moreData = $true

# read chunks until there is no more data
while($moreData)
{
# read a chunk
$bytesRead = $reader.Read($buffer, 0, $buffer.Length)
# create the filename for the chunk file
$chunkFileName = "$parentFolder\$fullBaseName.{0:D$digitCount}.part" -f $count
Write-Verbose "saving to $chunkFileName..."
$output = $buffer

# did we read less than the expected bytes?
if ($bytesRead -ne $buffer.Length)
{
# yes, so there is no more data
$moreData = $false
# shrink the output array to the number of bytes
# actually read:
$output = New-Object Byte[] $bytesRead
[Array]::Copy($buffer, $output, $bytesRead)
}
# save the read bytes in a new part file
[IO.File]::WriteAllBytes($chunkFileName, $output)
# increment the part counter
++$count
}
# done, close reader
$reader.Close()
}
catch
{
throw "Unable to split file ${Path}: $_"
}
}

明天我们将研究反向操作:如何将所有分片组合成原始文件。

今日知识点:

  • [IO.Path] 类来分割文件路径。
  • [IO.File] 类在字节级别处理文件内容。
  • Read() 函数将字节写入文件。

PowerShell 技能连载 - 通过 Outlook 发送邮件

您可以通过 Send-MailMessage 用 PowerShell 发送邮件。然而,这需要一个 SMTP 服务器,并且通过这种方式发送的邮件不会在您的邮箱中存档。

要通过 Outlook 发送邮件,请看这个函数:

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
function Send-OutlookMail
{

param
(
# the email address to send to
[Parameter(Mandatory=$true, Position=0, HelpMessage='The email address to send the mail to')]
[String]
$Recipient,

# the subject line
[Parameter(Mandatory=$true, HelpMessage='The subject line')]
[String]
$Subject,

# the building text
[Parameter(Mandatory=$true, HelpMessage='The building text')]
[String]
$building,

# a valid file path to the attachment file (optional)
[Parameter(Mandatory=$false)]
[System.String]
$FilePath = '',

# mail importance (0=low, 1=normal, 2=high)
[Parameter(Mandatory=$false)]
[Int]
[ValidateRange(0,2)]
$Importance = 1,

# when set, the mail is sent immediately. Else, the mail opens in a dialog
[Switch]
$SendImmediately
)

$o = New-Object -ComObject Outlook.Application
$Mail = $o.CreateItem(0)
$mail.importance = $Importance
$Mail.To = $Recipient
$Mail.Subject = $Subject
$Mail.building = $building
if ($FilePath -ne '')
{
try
{
$null = $Mail.Attachments.Add($FilePath)
}
catch
{
Write-Warning ("Unable to attach $FilePath to mail: " + $_.Exception.Message)
return
}
}
if ($SendImmediately -eq $false)
{
$Mail.Display()
}
else
{
$Mail.Send()
Start-Sleep -Seconds 10
$o.Quit()
Start-Sleep -Seconds 1
$null = [Runtime.Interopservices.Marshal]::ReleaseComObject($o)
}
}

现在在 Outlook 中很容易:

1
PS> Send-OutlookMail -Recipient frank@test.com -Subject 'Hi Frank!' -building 'Trying a new PS script. See attachment.' -FilePath 'c:\stuff\sample.zip' -Importance 0

假设您安装了 Outlook 并且设置了用户配置文件,这行代码将在一个对话框窗口中打开写好的邮件,这样您可以再次确认并做最终修改,然后按下“发送”按钮将邮件发送出去。

如果您指定了 -SendImmediately 开关参数,PowerShell 将会试图立即发送邮件。是否能够正确发送取决于您的 Outlook 关于自动操作的安全设置。自动发送邮件可能被禁用,或是会弹出一个对话框来征得您的同意。
z

PowerShell 技能连载 - 智力游戏生成器

人脑可以阅读每个单词头尾字母正确而其它字母顺序错乱的橘子。以下是一个可以自己实验的 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
function Convert-Text
{
param
(
$Text = 'Early to bed and early to rise makes a man healthy, wealthy and wise.'
)
$words = $Text -split ' '

$newWords = foreach($word in $words)
{
if ($word.Length -le 2)
{
$word
}
else
{
$firstChar = $word[0]
$lastChar = $word[-1]
$charLen = $word.Length -2
$inbetween = $word[1..$charLen]
$chars = $inbetween | Get-Random -Count $word.Length
$inbetweenScrambled = $chars -join ''
"$firstChar$inbetweenScrambled$lastChar"
}
}

$newWords -join ' '
}

如果没有输入文本,那么将采用默认文本。您可以猜出它的意思吗?

1
2
PS C:\> Convert-Text
Ealry to bed and erlay to rsie maeks a man hylhtea, wlhtaey and wies.

PowerShell 技能连载 - 提升 PowerShell 管道的速度

当 a) 需要处理许多项目时 b) 使用 PowerShell 管道时,PowerShell 脚本可能会变得非常缓慢。今天让我们找出它的原因,以及解决的方法。

要重现这个问题,让我们先创建一个用例,体现 PowerShell 如何明显变慢。我们需要准备许多项目。这里我们用代码生成 Windows 文件夹下所有文件的列表,这需要几秒钟才能生成完。

1
2
3
# get large data sets
$files = Get-ChildItem -Path c:\windows -File -Recurse -ErrorAction SilentlyContinue
$files.Count

我们将这些文件发送到管道,并且只挑出大于 1MB 的文件。在以下栗子中,我们将 $file 的内容全部发到管道,是为了有可复制的数据。实际情况中,当然不应该使用变量,而应该直接将结果输出到管道。

1
2
3
4
Measure-Command {
$largeFiles = $files | Where-Object { $_.Length -gt 1MB }
}
$largeFiles.Count

在我们的测试中,以上代码需要消耗 3-4 秒,并且产生了 3485 个“大”文件。在您的机器上结果可能不同。

Where-Object 实际上只是一个包含了 If 语句的 ForEach-Object 命令,那么让我们试着将 Where-Object 替换成 If

1
2
3
4
5
6
7
Measure-Command {
$largeFiles = $Files | ForEach-Object {
if ($_.Length -gt 1MB)
{ $_ }
}
}
$largeFiles.Count

结果是一样的,而时间减少到一半。

ForEach-Object 实际上只是一个有 process 块的匿名脚本块,所以接下来请试试这段代码:

1
2
3
4
5
6
7
8
9
10
Measure-Command {
$largeFiles = $Files | & {
process
{
if ($_.Length -gt 1MB)
{ $_ }
}
}
}
$largeFiles.Count

结果再次相同,但是结果从原来的 4 秒减少到大约 100 毫秒(四十分之一)。

可见,当通过管道传入数据时,PowerShell 对每个传入的对象调用绑定的参数方法,这将显著地增加时间开销。由于 ForEach-ObjectWhere-Object 使用参数,所以会激活绑定。

当您不使用内部包含 process 脚本块的匿名脚本块时,将忽略所有的参数绑定并显著加速 PowerShell 管道的执行速度。

PowerShell 技能连载 - Where-Object: 只是一个带管道的 IF 语句

Where-Object 是一个最常用的 PowerShell 命令,不过新手可能对它不太熟悉。对于熟悉 “SQL” 数据库查询语言的人可以像 SQL 中的 Where 从句一样使用它它是一个客户端的过滤器,能去除不需要的项目。以下这行代码将处理所有服务并只显示当前正在运行的服务:

1
Get-Service | Where-Object { $_.Status -eq "Running" }

要更好地理解 Where-Object 如何工作,实际上它只是一个对管道发生作用的 IF 语句。以上代码等同于这个:

1
2
3
4
Get-Service | ForEach-Object {
if ($_.Status -eq 'Running')
{ $_ }
}

或者,完全不用代码的传统实现方式:

1
2
3
4
5
6
7
8
$services = Get-Service
Foreach ($_ in $services)
{
if ($_.Status -eq 'Running')
{
$_
}
}

PowerShell 技能连载 - 对必选参数使用自定义提示

当您在 PowerShell 定义了必选参数,那么当用户没有传入这个参数时将会收到提示。如您所见,当您运行这段代码时,该提示只使用了参数的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
param
(
[Parameter(Mandatory)]
[string]
$UserName

)

"You entered $Username"



UserName: tobi
You entered tobi

To get more descriptive prompts, you can use more explicit variable names:
要获得描述的更具体的提示,您需要使用更明确的变量名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
param
(
[Parameter(Mandatory)]
[string]
${Please provide a user name}

)

$username = ${Please provide a user name}
"You entered $Username"



Please provide a user name: tobi
You entered tobi

只需要在一个函数中使用 param() 块就可以将函数转为命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function New-CorporateUser
{
param
(
[Parameter(Mandatory)]
[string]
${Please provide a user name}
)

$username = ${Please provide a user name}
"You entered $Username"
}



PS C:\> New-CorporateUser
Cmdlet New-CorporateUser at command pipeline position 1
Supply values for the following parameters:
Please provide a user name: Tobi
You entered Tobi

它的副作用是参数名中含有空格和特殊字符,将使它无法通过命令行指定值,因为参数无法用双引号包起来:

1
PS C:\> New-CorporateUser -Please  provide a user name

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 3 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

在之前的部分中我们创建了一个函数,它能够获取某个脚本中的所有外部命令。只需要再做一些额外努力,这就可以变成一个有用的兼容性报告:来自某个模块的所有 cmdlet,或者随着 PowerShell 发行的所有以 “Microsoft.PowerShell” 开头的模块。任何其它模块都属于具体的 Windows 版本或第三方扩展。

检查这个函数:

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
function Get-ExternalCommand
{
param
(
[Parameter(Mandatory)][string]
$Path
)
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

}


$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command
$commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
ForEach-Object { $_.GetCommandName() } |
Sort-Object -Unique |
ForEach-Object {
$module = (Get-Command -name $_).Source
$builtIn = $module -like 'Microsoft.PowerShell.*';

[PSCustomObject]@{
Command = $_
BuiltIn = $builtIn
Module = $module
}
}
}

以下是根据一个 PowerShell 脚本生成的一个示例报告,它列出了所有外部的 cmdlet,以及它们是否是 PowerShell 的一部分或来自外部模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS> Get-ExternalCommand -Path $Path


Command BuiltIn Module
------- ------- ------
ConvertFrom-StringData True Microsoft.PowerShell.Utility
Get-Acl True Microsoft.PowerShell.Security
Get-ItemProperty True Microsoft.PowerShell.Management
Get-Service True Microsoft.PowerShell.Management
Get-WmiObject True Microsoft.PowerShell.Management
New-Object True Microsoft.PowerShell.Utility
out-default True Microsoft.PowerShell.Core
Test-Path True Microsoft.PowerShell.Management
Where-Object True Microsoft.PowerShell.Core
write-host True Microsoft.PowerShell.Utility

PS>

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 2 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

在前一部分中我们处理了一个脚本并且读出这个脚本所使用的所有外部命令。我们用这个函数合并了找到的所有外部命令:

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
function Get-ExternalCommand
{
param
(
[Parameter(Mandatory)][string]
$Path
)
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

}


$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command
$commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
ForEach-Object { $_.GetCommandName() } |
Sort-Object -Unique
}

您可以向这个函数传入任何 PowerShell 脚本路径并且得到这个脚本使用的所有外部命令(只需要确保在 $path 中传入了一个脚本的合法路径):

1
2
3
4
5
6
7
8
9
10
11
PS C:\> Get-ExternalCommand -Path $Path
ConvertFrom-StringData
Get-Acl
Get-ItemProperty
Get-Service
Get-WmiObject
New-Object
out-default
Test-Path
Where-Object
write-host

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 1 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

要查看您的脚本的兼容性,在第一部分中我们先看看如何查找一个脚本实际使用哪些命令。以下是一个帮助函数,能够利用 PowerShell 内部的抽象语法树 (AST) 来检测命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

Get-ContainedCommand 可以解析一个脚本中定义的函数,或是一个脚本中使用的命令。以下是获取某个脚本中所有定义的函数的代码:

1
2
3
4
5
6
$Path = "C:\scriptToPS1File\WithFunctionDefinitionsInIt.ps1"

$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$functionNames

以下是脚本内部使用的命令列表:

1
2
$commands = Get-ContainedCommand $Path -ItemType Command
$commands.Foreach{$_.CommandElements[0].Extent.Text}

要找出使用外部命令的地方,只需要从命令列表中减掉所有内部定义的函数,然后移除重复。以下将获取某个脚本用到的所有外部命令:

1
2
3
4
5
6
7
8
9
10
11
12
$Path = "C:\scriptToPS1File\WithFunctionDefinitionsInIt.ps1"

$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command

$externalCommands = $commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
Sort-Object -Property { $_.GetCommandName() } -Unique

PowerShell 技能连载 - 从图片中创建彩色 ASCII 艺术

在之前的技能中我们介绍了如何读取任何图片或照片,并将它转换为一个黑白 ASCII 艺术。今天,我们将修改 Convert-ImageToAsciiArt 函数:它输入一个函数并将它转换为彩色的 ASCII 艺术!

像素的亮度值将被转换为合适的 ASCII 字符,并且像素的颜色值将应用到该字符。ASCII 艺术将写入 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
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
function Convert-ImageToAsciiArt
{
param(
[Parameter(Mandatory)][String]
$ImagePath,

[Parameter(Mandatory)][String]
$OutputHtmlPath,

[ValidateRange(20,20000)]
[int]$MaxWidth=80
)

,

# character height:width ratio
[float]$ratio = 1.5

# load drawing functionality
Add-Type -AssemblyName System.Drawing

# characters from dark to light
$characters = '$#H&@*+;:-,. '.ToCharArray()
$c = $characters.count

# load image and get image size
$image = [Drawing.Image]::FromFile($ImagePath)
[int]$maxheight = $image.Height / ($image.Width / $maxwidth) / $ratio

# paint image on a bitmap with the desired size
$bitmap = new-object Drawing.Bitmap($image,$maxwidth,$maxheight)


# use a string builder to store the characters
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas""&#39;>"


# take each pixel line...
for ([int]$y=0; $y -lt $bitmap.Height; $y++) {
# take each pixel column...
$null = $sb.Append("<nobr>")
for ([int]$x=0; $x -lt $bitmap.Width; $x++) {
# examine pixel
$color = $bitmap.GetPixel($x,$y)
$brightness = $color.GetBrightness()
# choose the character that best matches the
# pixel brightness
[int]$offset = [Math]::Floor($brightness*$c)
$ch = $characters[$offset]
if (-not $ch) { $ch = $characters[-1] }
$col = "#{0:x2}{1:x2}{2:x2}" -f $color.r, $color.g, $color.b
if ($ch -eq &#39; &#39;) { $ch = " "}
$null = $sb.Append( "<span style=""color:$col""; ""white-space: nowrap;"">$ch</span>")
}
# add a new line
$null = $sb.AppendLine("</nobr><br/>")
}

# close html document
$null = $sb.AppendLine("</building></html>")

# clean up and return string
$image.Dispose()

Set-Content -Path $OutputHtmlPath -Value $sb.ToString() -Encoding UTF8
}

还有以下是如何将一张图片转换为一个漂亮的 ASCII 艺术,并在浏览器VS显示,甚至在彩色打印机中打印出来:

1
2
3
4
5
$ImagePath = "C:\someInputPicture.jpg"
$OutPath = "$home\desktop\ASCIIArt.htm"

Convert-ImageToAsciiArt -ImagePath $ImagePath -OutputHtml $OutPath -MaxWidth 150
Invoke-Item -Path $OutPath

可以通过调整 -MaxWidth 来控制细节。如果增加了宽度,那么也必须调整字体大小并增加字符数。对于更小的字符,您可能需要调整这行:

1
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas""&#39;>"

例如将它改为这行:

1
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas"";font-size:4px&#39;>"