在 powershell 中切片数组(或列表)的更好方法

A better way to slice an array (or a list) in powershell

我如何在 CSV 文件中为每个用户导出 30 个用户的邮件地址。 我已经试过了

    $users = Get-ADUser -Filter * -Properties Mail 
    $nbCsv = [int][Math]::Ceiling($users.Count/30)
    For($i=0; $i -le $nbCsv; $i++){
        $arr=@()
        For($j=(0*$i);$j -le ($i + 30);$j++){
            $arr+=$users[$j]
        }
        $arr|Export-Csv -Path ($PSScriptRoot + "\ASSFAM" + ("{0:d2}" -f ([int]$i)) + ".csv") -Delimiter ";" -Encoding UTF8 -NoTypeInformation
    }

它有效,但我认为有更好的方法来完成此任务。 你有什么想法吗?

谢谢。

如果你想要一个数组的子集,你可以只使用 ..,范围运算符。数组的前 30 个元素为:

$users[0..29]

您也不必担心超出数组的末尾。如果有 100 个项目并且您正在调用 $array[90..119],您将获得数组中的最后 10 个项目并且没有错误。您也可以在那里使用变量和表达式:

$users[$i..($i + 29)]

这是第 $i 个值和第 $i 个值之后的下 29 个值(如果它们存在)。

此外,在 PowerShell 中应避免这种模式:

$array = @()
loop-construct {
   $array += $value
}

数组在 .Net 中是不可变的,因此在 PowerShell 中也是不可变的。这意味着用 += 向数组添加一个元素意味着 "create a brand new array, copy every element over, and then put this one new item on it, and then delete the old array." 它会产生巨大的内存压力,如果您处理的项目超过几百个,速度会明显变慢。

相反,只需这样做:

$array = loop-construct {
   $value
}

字符串同样是不可变的,并且与 += 运算符存在相同的问题。如果您需要通过连接构建一个字符串,您应该使用 StringBuilder class.

然而,最终,我会这样写:

$users = Get-ADUser -Filter * -Properties Mail 
$exportFileTemplate = Join-Path -Path $PSScriptRoot -ChildPath 'ASSFAM{0:d2}.csv'

$batchSize = 30

$batchNum = 0
$row = 0
while ($row -lt $users.Count) {
    $users[$row..($row + $batchSize - 1)] | Export-Csv ($exportFileTemplate -f $batchNum) -Encoding UTF8 -NoTypeInformation
    $row += $batchSize
    $batchNum++
}
从技术上讲,

$row$batchNum 可以合并到一个变量中,但这更具可读性,IMO。

我相信你也可以用 Select-ObjectGroup-Object 来写这个,但与上面的相比,这会相当复杂,而且 Group-Object 并不完全为人所知它是 PowerShell 6 之前的性能。

shows how to simplify your code with the help of .., the range operator,但是如果有一个通用的分块(partitioning,batching)机制就好了;但是,从 PowerShell 7.0 开始,没有内置功能

GitHub feature suggestion #8270 建议向 Select-Object 添加一个 -ReadCount <int> 参数,类似于已经为 Get-Content.
定义的同名参数 如果您希望看到此功能的实现,请在此处显示您对链接问题的支持。

有了该功能,您可以执行以下操作:

$i = 0
Get-ADUser -Filter * -Properties Mail |
  Select-Object -ReadCount 30 |  # WISHFUL THINKING: output 30-element arrays
   ForEach-Object {
     $_ | Export-Csv -Path ($PSScriptRoot + "\ASSFAM" + ("{0:d2}" -f ++$i) + ".csv") -Delimiter ";" -Encoding UTF8 -NoTypeInformation
   }

在此期间,您可以使用自定义函数 Select-Chunk(下面的源代码):将上面代码段中的 Select-Object -ReadCount 30 替换为 Select-Chunk -ReadCount 30

下面是其工作原理的简单演示:

PS> 1..7 | Select-Chunk -ReadCount 3 | ForEach-Object { "$_" }
  
1 2 3
4 5 6
7

以上显示ForEach-Object脚本块收到以下内容 三个数组,通过 $_,顺序为:
1, 2, 34, 5, 6, 7

(当您对数组进行字符串化时,默认情况下您会得到一个以 space 分隔的元素列表;例如,"$(1, 2, 3)" 会产生 1 2 3)。


Select-Chunk源代码:

实施使用 [System.Collections.Generic.Queue[object]] 实例以固定大小的批次收集输入。

function Select-Chunk {
  <#
  .SYNOPSIS
  Chunks pipeline input.
  
  .DESCRIPTION
  Chunks (partitions) pipeline input into arrays of a given size.
  
  By design, each such array is output as a *single* object to the pipeline,
  so that the next command in the pipeline can process it as a whole.
  
  That is, for the next command in the pipeline $_ contains an *array* of
  (at most) as many elements as specified via -ReadCount.
  
  .PARAMETER InputObject
  The pipeline input objects binds to this parameter one by one.
  Do not use it directly.
  
  .PARAMETER ReadCount
  The desired size of the chunks, i.e., how many input objects to collect
  in an array before sending that array to the pipeline.
  
  0 effectively means: collect *all* inputs and output a single array overall.
  
  .EXAMPLE
  1..7 | Select-Chunk 3 | ForEach-Object { "$_" }
  
  1 2 3
  4 5 6
  7
  
  The above shows that the ForEach-Object script block receive the following
  three arrays: (1, 2, 3), (4, 5, 6), and (, 7)
  #>
  
  [CmdletBinding(PositionalBinding = $false)]
  [OutputType([object[]])]
  param (
    [Parameter(ValueFromPipeline)] 
    $InputObject
    ,
    [Parameter(Mandatory, Position = 0)]
    [ValidateRange(0, [int]::MaxValue)]
    [int] $ReadCount
  )
      
  begin {
    $q = [System.Collections.Generic.Queue[object]]::new($ReadCount)
  }
      
  process {
    $q.Enqueue($InputObject)
    if ($q.Count -eq $ReadCount) {
      , $q.ToArray()
      $q.Clear()
    }
  }
      
  end {
    if ($q.Count) {
      , $q.ToArray()
    }
  }

}