【问题标题】:PowerShell: Find unique values from multiple CSV filesPowerShell:从多个 CSV 文件中查找唯一值
【发布时间】:2022-01-27 02:18:53
【问题描述】:

假设我有几个 CSV 文件,我需要检查特定列并查找存在于一个文件中但不存在于其他任何文件中的值。由于我想使用Compare-Object 并可能保留所有列,而不仅仅是包含我正在检查的值的列,所以我在想出最好的方法时遇到了一些麻烦。

所以我确实有几个 CSV 文件,它们都有一个 Service Code 列,我正在尝试为每个 Service Code 创建一个仅出现在一个文件中的列表。所以我会有“仅在 CSV1 中的服务代码”、“仅在 CSV2 中的服务代码”等。

基于一些测试和semi-related question,我想出了一个可行的解决方案,但是对于所有的嵌套和For 循环,我想知道是否有更优雅的方法。

这是我所拥有的:

$files = Get-ChildItem -LiteralPath "C:\temp\ItemCompare" -Include "*.csv"
$HashList = [System.Collections.Generic.List[System.Collections.Generic.HashSet[String]]]::New()
For ($i = 0; $i -lt $files.Count; $i++){
    $TempHashSet = [System.Collections.Generic.HashSet[String]]::New([String[]](Import-Csv $files[$i])."Service Code")
    $HashList.Add($TempHashSet)
}

$FinalHashList = [System.Collections.Generic.List[System.Collections.Generic.HashSet[String]]]::New()
For ($i = 0; $i -lt $HashList.Count; $i++){
    $UniqueHS = [System.Collections.Generic.HashSet[String]]::New($HashList[$i])
    For ($j = 0; $j -lt $HashList.Count; $j++){
        #Skip the check when the HashSet would be compared to itself
        If ($j -eq $i){Continue}
        $UniqueHS.ExceptWith($HashList[$j])
    }
    $FinalHashList.Add($UniqueHS)
}

使用这么多不同的 .NET 引用对我来说似乎有点混乱,我知道我可以用一个标签说using namespace System.Collections.Generic 让它更干净,但我想知道是否有办法让它工作使用Compare-Object 这是我的第一次尝试,甚至只是过滤每个文件的更简单/更有效的方法。

【问题讨论】:

  • 您拥有的 CSV 中的哪一个是“参考 CSV”,或者您是否希望比较所有这些 CSV 并找到唯一值?另外,您是在寻找效率还是优雅,这是个人意见,但hashshetCompare-Object 更优雅和高效(再次,个人喜好)
  • 一个文件中是否可以有重复的服务代码?
  • 在 99.9% 的情况下,单个文件中很可能不会有重复项。 @SantiagoSquarzon - 我正在尝试将每个人与其他人进行比较。因此,如果我使用Compare-Object,它会变得非常嵌套,并且可能不符合任何定义的优雅
  • 我没有看到您的hashset 上有[System.StringComparer]::OrdinalIgnoreCase,这是您应该担心的事情吗? (除了区分大小写不同之外,还有可能具有相同值的代码)
  • 我可以将大小写逻辑作为安全检查,但所有内容都应在源文件中转换为大写,所以这不是一个大问题。 HashSet 过程还不错,而且相当快,但现在我开始认为我可能会导出结果,包括“服务代码”以外的列,所以在我将所有内容转换为 HashTablesPSCustomObject s 作为键,我想我会检查是否有更好的方法,因为我无法让Compare-Object 开心

标签: powershell


【解决方案1】:

我相信我找到了一个基于Group-Object 的“优雅”解决方案,只使用一个管道:

# Import all CSV files. 
Get-ChildItem $PSScriptRoot\csv\*.csv -File -PipelineVariable file | Import-Csv | 

    # Add new column "FileName" to distinguish the files.
    Select-Object *, @{ label = 'FileName'; expression = { $file.Name } } |

    # Group by ServiceCode to get a list of files per distinct value. 
    Group-Object ServiceCode |

    # Filter by ServiceCode values that exist only in a single file.
    # Sort-Object -Unique takes care of possible duplicates within a single file.
    Where-Object { ( $_.Group.FileName | Sort-Object -Unique ).Count -eq 1 } |

    # Expand the groups so we get the original object structure back.
    ForEach-Object Group |

    # Format-Table requires sorting by FileName, for -GroupBy.
    Sort-Object FileName |

    # Finally pretty-print the result.
    Format-Table -Property ServiceCode, Foo -GroupBy FileName 

测试输入

a.csv:

ServiceCode,Foo
1,fop
2,fip
3,fap

b.csv:

ServiceCode,Foo
6,bar
6,baz
3,bam
2,bir
4,biz

c.csv:

ServiceCode,Foo
2,bla
5,blu
1,bli

输出

   FileName: b.csv    

ServiceCode Foo       
----------- ---       
4           biz       
6           bar       
6           baz       

   FileName: c.csv    

ServiceCode Foo       
----------- ---       
5           blu  

在我看来是正确的。 123 的值在多个文件之间重复,因此它们被排除在外。 456 仅存在于单个文件中,而 6 是仅在单个文件中的重复值。

理解代码

通过查看Group-Object 行产生的管道的中间输出,也许更容易理解这段代码的工作原理:

Count Name                      Group
----- ----                      -----
    2 1                         {@{ServiceCode=1; Foo=fop; FileName=a.csv}, @{ServiceCode=1; Foo=bli; FileName=c.csv}}
    3 2                         {@{ServiceCode=2; Foo=fip; FileName=a.csv}, @{ServiceCode=2; Foo=bir; FileName=b.csv}, @{ServiceCode=2; Foo=bla; FileName=c.csv}}
    2 3                         {@{ServiceCode=3; Foo=fap; FileName=a.csv}, @{ServiceCode=3; Foo=bam; FileName=b.csv}}
    1 4                         {@{ServiceCode=4; Foo=biz; FileName=b.csv}}
    1 5                         {@{ServiceCode=5; Foo=blu; FileName=c.csv}}
    2 6                         {@{ServiceCode=6; Foo=bar; FileName=b.csv}, @{ServiceCode=6; Foo=baz; FileName=b.csv}}

这里Name 包含唯一的ServiceCode 值,而Group 将数据“链接”到文件。

从这里应该已经清楚了如何查找仅存在于单个文件中的值。如果不允许在单个文件中重复 ServiceCode 值,我们甚至可以将过滤器简化为 Where-Object Count -eq 1。由于单个文件中可能存在重复文件,因此我们需要 Sort-Object -Unique 将组内的多个相同文件名计为一个。

【讨论】:

  • Group-Object - 当我遇到Compare-Object 时,我原本想过做类似的事情。当我昨晚放弃让Compare 工作时,我直接去了hash set,完全忘记了Group-Object!我会试一试,但它看起来很有希望,并且不会多次循环遍历所有文件
  • @immobile2 这对我来说是一个有趣的练习。我希望它对你有用。
  • 是的,这实际上是完美的——如果需要,可以进行大量扩展,但这是一个很好的方法。我通常什至不尝试将事物保存在单个管道中,这可能解释了为什么我以前从未使用过-PipelineVariable。您对中间输出的解释也非常有帮助。 Group-Object 考虑到它的强大程度,肯定有点被低估了
  • 这有点跑题了,但你显然知道你在做什么,我不知道如何将它转移到聊天中。 1) 我似乎总是对Add-Member 以及我是否需要使用ForEach-Object{$_|Add-Member} 或者我可以直接通过管道输入它感到困惑。为什么你使用Select-Object 来计算属性而不是Add-Member
  • @immobile2 一个很大的区别是Add-Member 就地修改对象,而Select-Object 在添加成员之前创建一个副本。在我们的例子中,它可能无关紧要,但总的来说,我喜欢保持原始对象不被修改,这使得程序更容易推理。所以我很少使用Add-Member
【解决方案2】:

您对输出的期望并不完全清楚。
如果这只是 ServiceCodes that intersect 那么这实际上是重复的:

但如果您确实想要相关的对象和文件,您可能会使用这种方法:

$HashTable = @{}
ForEach ($File in Get-ChildItem .\*.csv) {
    ForEach ($Object in (Import-Csv $File)) {
        $HashTable[$Object.ServiceCode] = $Object |Select-Object *,
            @{ n='File'; e={ $File.Name } },
            @{ n='Count'; e={ $HashTable[$Object.ServiceCode].Count + 1 } }
    }
}
$HashTable.Values |Where-Object Count -eq 1

【讨论】:

  • 这看起来很简洁,并且可能比我的长管道更快。该代码当前仅在单个文件中复制时不会输出 ServiceCode 值。这可以通过将文件名存储在HashSet 中而不是递增计数器来解决。然后在Where-Object 条件中使用HashSet.Count
  • 不幸的是,当我在寻找一种疯狂的方法时,我没有找到那些相关的问题,但我确实认为我的练习本质上是那些重复的,因为我想要的输出甚至不是当我开始这个项目时,我完全清楚。起初我只是想要相交值的列表(或者我想那些不相交的值),但意识到返回完整的对象并通过将它们写入文件来完成将比仅提供值更有帮助。感谢您链接那些很棒的相关答案并解决我的问题!
【解决方案3】:

这是我对这个有趣练习的看法,我使用与您类似的方法来处理HashSet,但添加[System.StringComparer]::OrdinalIgnoreCase 以利用.Contains(..) 方法:

using namespace System.Collections.Generic

# Generate Random CSVs:
$charset = 'abABcdCD0123xXyYzZ'
$ran = [random]::new()
$csvs = @{}
foreach($i in 1..50) # Create 50 CSVs for testing
{
    $csvs["csv$i"] = foreach($z in 1..50) # With 50 Rows
    {
        $index = (0..2).ForEach({ $ran.Next($charset.Length) })
        
        [pscustomobject]@{
            ServiceCode = [string]::new($charset[$index])
            Data = $ran.Next()
        }
    }
}

# Get Unique 'ServiceCode' per CSV:
$result = @{}
foreach($key in $csvs.Keys)
{
    # Get all unique `ServiceCode` from the other CSVs
    $tempHash = [HashSet[string]]::new(
        [string[]]($csvs[$csvs.Keys -ne $key].ServiceCode),
        [System.StringComparer]::OrdinalIgnoreCase
    )
    # Filter the unique `ServiceCode`
    $result[$key] = foreach($line in $csvs[$key])
    {
        if(-not $tempHash.Contains($line.ServiceCode))
        {
            $line
        }
    }
}

# Test if the code worked,
# If something is returned from here means it didn't work
foreach($key in $result.Keys)
{
    $tmp = $result[$result.Keys -ne $key].ServiceCode
    foreach($val in $result[$key])
    {
        if($val.ServiceCode -in $tmp)
        {
            $val
        }
    }
}

【讨论】:

  • @immobile2 天哪,你是对的,不知道为什么$result = @{} 不在这里。复制粘贴时我可能错过了它......我已经更新了。很抱歉。
【解决方案4】:

我能够获得如下独特的物品

# Get all items of CSVs in a single variable with adding the file name at the last column
$CSVs = Get-ChildItem "C:\temp\ItemCompare\*.csv" | ForEach-Object {
    $CSV = Import-CSV -Path $_.FullName
    $FileName = $_.Name
    $CSV | Select-Object *,@{N='Filename';E={$FileName}}
}
Foreach($line in $CSVs){
$ServiceCode = $line.ServiceCode
$file = $line.Filename
if (!($CSVs | where {$_.ServiceCode -eq $ServiceCode -and $_.filename -ne $file})){
$line
}
}

【讨论】:

  • 谢谢!这种方法确实有效,但它似乎比我使用Compare-ObjectGroup-Object 或使用大量哈希表和哈希集时要慢得多。这是有道理的,因为我倾向于使用某种字典/哈希表来提高查找和比较的速度,而我在文件夹中的文件数量Where-Object 在这里过滤似乎使事情接近停滞
猜你喜欢
  • 1970-01-01
  • 2016-10-07
  • 1970-01-01
  • 1970-01-01
  • 2017-06-07
  • 2022-11-22
  • 2016-03-28
  • 2020-06-18
  • 1970-01-01
相关资源
最近更新 更多