【问题标题】:Logical operations on CSV with Powershell使用 Powershell 对 CSV 进行逻辑操作
【发布时间】:2017-02-22 18:27:22
【问题描述】:

我需要根据 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 中的内容为:

  • 'small' 如果 Col03 值小于或等于 50
  • 'medium' 如果 Col03 值在 51 和 100 之间
  • 'large' 如果 Col03 值在 51 和 100 之间
  • 'text' 如果 Col03 值是文本(不是数字)

结果:

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

【问题讨论】:

  • 你试过什么? Col03 只能是整数或文本吗?中型和大型有相同的要求..

标签: powershell csv logical-operators


【解决方案1】:

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

#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 = Import-CSV -Path C:\marketing\sandbox\MyFile.csv $csv | ForEach-Object { #获取当前 Col03 值 $col3 = $_.Col03.Trim() #计算新的 Col01 值 $val = if($col3 -match '^\d+$') { #Col03 为整数 if([int ]$col3 -le 50) { "small" } elseif ([int]$col3 -le 100) { "medium" } else { "large" } } else { "text" } #替换 Col01-value $_.Col01 = $val $_ Out-File -Encoding "utf8" -Path "C:\marketing\sandbox\out.csv" }
  • | Export-CSV -Path MyOuput.csv -NoTypeInformation添加到我答案的最后一行
  • 一个 cmd 类型的窗口打开 C:\marketing\sandbox>PowerShell.exe -NoProfile -ExecutionPolicy Bypass -File rules.ps1 cmdlet Export-Csv 在命令管道位置 1 提供以下参数的值:输入对象:
  • 看起来你没有将它添加到最后一行(} 之后),而是在它自己的下面一行。查看替代解决方案,它包括导出
  • switch 版本效果更好,但由于某种原因,50 到 100 之间的介质不起作用。 100 以内的所有东西都被标记为小
【解决方案2】:

它是完美的练习素材,你可以写成几种方式,它的核心是:

  • 读取 CSV 行
    • 您可以以文本行的形式执行此操作,但 PowerShell 中的“正确”方式是 Import-Csv)。
  • 测试文本/数字部分

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

    • 同样,您可以将其作为文本行进行,但 Export-Csv 可以与 Import-Csv 配对,因此这是 PowerShell 中的“正确”方式。

在 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)

好的,我们已经处理了读/处理/写部分的结构。现在您将数值分配给存储桶。

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 和单/双测试的混杂。但是因为你的桶很好地相互碰撞,你可以利用/误用跌落测试来:

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

好的,有些会被分配为“中”,然后是“小”,但这种布局更易于阅读,看看它在做什么,不是吗?

除非你停止它,否则失败会隐式发生在switch 中,因此会有带有或不带break 的切换案例来阻止失败。

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

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

这不是很漂亮。进行正则表达式/切换失败组合,我得到:

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 存储桶检查 - 因此他使用 if/elseif/if 而不是 if/if/如果。这有点好,可以格式化为:

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

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

【讨论】:

  • 很好(而且很长!)的解释:-)。这次没有花太多时间让我的代码看起来不错,因为无论如何所有的 if 都很丑......
猜你喜欢
  • 2019-08-26
  • 2018-04-01
  • 1970-01-01
  • 2021-03-08
  • 2012-10-27
  • 2015-08-12
  • 2015-03-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多