使用 Powershell 对 CSV 进行逻辑操作

Logical operations on CSV with Powershell

我需要根据 Col03 中的值对 Col01 应用一些更改

Col01,Col02,Col03
empty,empty,6
empty,empty,19
empty,empty,75
empty,empty,87
empty,red,145
empty,empty,625
empty,empty,abc

将Col01中的内容设为:

结果:

Col01,Col02,Col03
small,empty,6
small,empty,19
medium,empty,75
medium,empty,87
large,empty,145
large,empty,625
text,empty,abc

这是正确的吗?有多种方法可以实现这一目标。例如:

#Sample data
$csv = @"
Col01,Col02,Col03
empty,empty,6
empty,empty,19
empty,empty,75
empty,empty,87
empty,red,145
empty,empty,625
empty,empty,abc
"@ | ConvertFrom-Csv

#Uncomment to read from file
#$csv = Import-CSV -Path C:\MyFile.csv

$csv | ForEach-Object {

    #Get current Col03 value
    $col3 = $_.Col03.Trim()

    #Calculate new Col01 value 
    $val = if($col3 -match '^\d+$') {
        #Col03 is integer
        if([int]$col3 -le 50) { "small" }
        elseif ([int]$col3 -le 100) { "medium" }
        else { "large" }
    } else { "text" }

    #Replace Col01-value
    $_.Col01 = $val

    #Output modified object
    $_
}

或者使用开关。此示例还将结果保存到文件中:

$csv = @"
Col01,Col02,Col03
empty,empty,6
empty,empty,19
empty,empty,75
empty,empty,87
empty,red,145
empty,empty,625
empty,empty,abc
"@ | ConvertFrom-Csv

#Uncomment to read from file
#$csv = Import-CSV -Path C:\MyFile.csv

$csv | ForEach-Object {

    $_.Col01 = switch($_.Col03.Trim()) {
        #Contains non-digit - text
        {$_ -match '\D'} { 'text'; break; }
        #Number - pick category
        {[int]$_ -le 50} { 'small'; break; }
        {[int]$_ -le 100} { 'medium'; break; }
        {[int]$_ -gt 100} { 'large'; break; } 
    }  

    #Output modified object
    $_
} | Export-CSV -Path MyOuput.csv -NoTypeInformation 

输出:

Col01  Col02 Col03
-----  ----- -----
small  empty 6    
small  empty 19   
medium empty 75   
medium empty 87   
large  red   145  
large  empty 625  
text   empty abc  

完美的练习草稿,可以写成多种写法,核心是:

  • 正在读取 CSV 行
    • 您可以将其作为文本行来完成,但 PowerShell 中的 'right' 方式是 Import-Csv)。
  • 测试 text/number 部分

    1. 首先(在您的代码中)处理文本条件,任何超过 的内容都将是 数字。
    2. 首先尝试处理数字案例,超过的任何内容都将是文本。
    3. 假设全是数字,如果断了,就是文本。这是可行的,但它使用异常来控制流,这很糟糕。这很糟糕,因为异常是针对特殊情况的,并且您期望文本是程序操作的正常部分,而且它有点糟糕,因为异常有很多开销。但是,您是程序员,您可以根据需要选择使用它们 - 例如特别是 readable/clear.
  • 导出 CSV 行

    • 同样,您可以将其作为文本行来完成,但是 Export-Csv 需要与 Import-Csv 配对,所以这就是 PowerShell 中的 'right' 方式。

并且在 PowerShell 术语中,您对其进行排列是有意义的:

Import-Csv | ForEach-Object { process one CSV row at a time } | Export-Csv

(as opposed to:
$foo = import-csv
$bar = @()
foreach ($line in $foo) {
   #...
   $bar += $line
}
which is workable but ugly and wasteful of memory and CPU and won't scale nicely)

好的,我们已经处理了 read/process/write 部分的结构。现在您要将数字值分配给存储桶。

0-10    11-20    21-30    31-40
\__/    \___/    \___/    \___/

或任何大小范围/桶条件。

那个模式尖叫 if/elseswitch

所以剩下的部分是,您选择 1. 2. 3. 方法中的哪一种,在何处以及如何将文本与数字分开,以及如何将数字分配到桶中。

大部分选择与可读性和您的偏好有关。

有开始和结束的桶意味着像 start -le $num -and $num -lt end 这样的双重测试,然后两个边缘情况只有一个测试。但是你的三个桶意味着一个需要双重测试,两个需要一个单一测试。

if ($foo -gt 100)
elseif (51 -lt $foo -and $foo -le 100)
elseif ($foo -lt 50)

看看 if/elseif 和 single/double 测试的混杂。但是因为你的桶很好地相互碰撞,你可以使用/误用跌落测试来获得:

if ($foo -gt 100) { big    }
if ($foo -le 100) { medium }
if ($foo -le 50 ) { small  }

好的,有些会被分配 'medium',然后 'small',但是这种布局更易于阅读,更易于查看它在做什么,不是吗?

除非您停止它,否则 fallthrough 会隐式地发生在 switch 中,因此将有带或不带 break 的 switch case 来停止 fallthrough。

如果您选择首先匹配 文本 ,您可能会使用正则表达式来识别非数字的东西(向后),所以我明白了:

Import-Csv .\t.csv | ForEach-Object { 

    if ($_.Col03 -notmatch '^\d+$')
    {
        $_.Col01 = 'text'
    }
    else 
    {
        if ([int]$_.Col03 -gt 100) { $_.Col01 = 'large'  }
        if ([int]$_.Col03 -le 100) { $_.Col01 = 'medium' }
        if ([int]$_.Col03 -le 50)  { $_.Col01 = 'small'  }
    }

    $_

} # | Export-Csv out.csv -NoTypeInformation

这是可行的,但是嗯。如果您选择首先识别数字,您可以使用相同的正则表达式模式,或者告诉 .Net 框架将文本 TryParse-ing 为数字。我得到:

Import-Csv .\t.csv | ForEach-Object { 

    [int]$n = 0

    if ([int]::TryParse($_.Col03, [ref]$n)) 
    {
        if ($n -gt 100) { $_.Col01 = 'large'  }
        if ($n -le 100) { $_.Col01 = 'medium' }    
        if ($n -le 50)  { $_.Col01 = 'small'  }
    }
    else 
    {
        $_.Col01 = 'text'
    }

    $_

} # | Export-Csv out.csv -NoTypeInformation

这不是很漂亮。选择正则表达式/开关 fallthrough 组合,我得到:

Import-Csv .\t.csv | ForEach-Object { 

    switch -regex ($_)
    {
        {$_.Col03 -notmatch '^\d+$'} { $_.Col01 = 'text'   ; break }
        {[int]$_.Col03 -gt 100}      { $_.Col01 = 'large'          }
        {[int]$_.Col03 -le 100}      { $_.Col01 = 'medium'         }
        {[int]$_.Col03 -le 50 }      { $_.Col01 = 'small'          }
    }

    $_
} # | Export-Csv out.csv -NoTypeInformation

这很漂亮,但是部分 ; break / fallthrough 只是在等待有人在阅读它时犯错误。并使用异常处理作为控制流,我得到了这个,(由于 catch 块内 $_ 的范围问题,我已经将其更改为首先将整个文件读入内存):

$Rows = foreach ($Row in Import-Csv .\t.csv)
{ 
    try {
        if ([int]$Row.Col03 -gt 100) { $Row.Col01 = 'large'  }
        if ([int]$Row.Col03 -le 100) { $Row.Col01 = 'medium' }
        if ([int]$Row.Col03 -le 50)  { $Row.Col01 = 'small'  }
    } catch {
        $row.Col01 = 'text'
    }

    $row
}

$Rows # | Export-Csv out.csv -NoTypeInformation

但他们基本上都在做同样的事情,我想不出更好的分桶方式,所以我所有的答案几乎都是相同的形状,即使他们这样做了 运行通过完全不同的代码。

Frode F 使用从 if 分配的 PowerShell 再次以不同的方式工作,但在那种方法中我不能使用 fallthrough bucket 检查 - 因此他使用 if/elseif/if而不是 if/if/if。这有点不错,可以格式化为:

$row.Col01 =     if (                $n -le  50) { 'small'  }
             elseif ( 51 -lt $n -and $n -le 100) { 'medium' }
             elseif (100 -lt $n                ) { 'large'  }

然后更清楚它们是 start/end 范围,实际上是的,我最喜欢 Frode 的方法,只是没有格式化他的格式化方式,希望我在写这篇文章之前阅读他的回答。