我如何从一个字符串在PowerShell子字符串的特定字节数?



我有一个场景,我需要获得嵌入在base64编码的JSON REST响应中的安装程序。由于JSON字符串的大小相当大(180 MB),使用标准PowerShell工具解码REST响应时会导致问题,因为它会导致OutOfMemoryException在有限的内存场景中经常抛出(例如达到WinRM内存配额)。

在我们的环境中提高一次安装的内存配额是不理想的,而且我们没有标准的工具来准备一个包,它的有效载荷在一个简单的HTTP端点上不存在(我没有发布没有通过我们的构建系统执行的包的直接权限)。在这种情况下,我的解决方案是将base64字符串解码成块。然而,当我有这个工作时,我被困在这个过程的最后一点优化。


目前我正在使用MemoryStream从字符串中读取,但我需要提供byte[]:

# $Base64String is a [ref] type
$memStream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($Base64String.Value))

毫无疑问,这会导致复制整个base64编码字符串的byte[]表示,并且比当前形式的内置工具的内存效率更低。您在这里没有看到的代码一次以1024字节块的形式从$memStream读取,解码base64字符串并使用BinaryWriter将字节写入磁盘。这一切都运行得很好,虽然有些慢,因为我经常强制进行垃圾收集。但是,我想将这个字节计数扩展到初始的MemoryStream,并且一次只有从字符串中读取n字节。我的理解是base64字符串必须被解码成能被4整除的字节块。

问题是[string].Substring([int], [int])基于字符串长度,而不是每个字符的字节数。JSON响应可以假设为UTF-8编码,但即使这样假设,UTF-8字符的长度也在1-4字节之间变化。我如何(直接或间接)子字符串在PowerShell中特定的字节数,这样我就可以从这个子字符串创建MemoryStream,而不是完整的$Base64String? 我将注意到我已经探索了[Text.Encoding].GetBytes([string], [int], [int])重载的使用,然而,我面临同样的问题,因为该方法期望字符串长度的字符计数,而不是字节计数,以便从开始索引中获得byte[]

要回答基本问题"如何在powershell中从字符串中提取特定字节数的子字符串",我可以编写以下函数:

function Get-SubstringByByteCount {
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[ValidateScript({ $null -ne $_ -and $_.Value -is [string] })]
[ref]$InputString,
[int]$FromIndex = 0,
[Parameter(Mandatory)]
[int]$ByteCount,
[ValidateScript({ [Text.Encoding]::$_ })]
[string]$Encoding = 'UTF8'
)

[long]$byteCounter = 0
[System.Text.StringBuilder]$sb = New-Object System.Text.StringBuilder $ByteCount
try {
while ( $byteCounter -lt $ByteCount -and $i -lt $InputString.Value.Length ) {
[char]$char = $InputString.Value[$i++]
[void]$sb.Append($char)
$byteCounter += [Text.Encoding]::$Encoding.GetByteCount($char)
}
$sb.ToString()
} finally {
if( $sb ) {
$sb = $null
[System.GC]::Collect()
}
}
}

调用的工作原理如下:

Get-SubstringByByteCount -InputString ( [ref]$someString ) -ByteCount 8

关于这个实现的一些注意事项:

  • 将字符串作为[ref]类型,因为最初的目标是避免在内存有限的情况下复制完整的字符串。此函数可以使用[string]类型重新实现。
  • 这个函数实际上是将每个字符添加到StringBuilder中,直到指定的字节数被写入。
  • 每个字符的字节数是通过使用[Text.Encoding]::GetByteCount过载之一来确定的。编码可以通过参数指定,但是编码值应该匹配[Text.Encoding]中可用的静态编码属性之一。默认为UTF8
  • $sb = $null[System.GC]::Collect()用于在内存受限的环境中强制清除StringBuilder,但如果不关心这一点,可以省略。
  • -FromIndex-InputString内开始子串操作的起始位置。默认为0,从-InputString开始计算。

最新更新