【问题标题】:Powershell intercept and fix specific value when reading a CSV file读取 CSV 文件时,Powershell 拦截并修复特定值
【发布时间】:2020-06-22 15:25:51
【问题描述】:

在 PowerShell 脚本中,我读取了一个 CSV 文件。

我必须“修复”一些价值观。具体来说,CSV 可能包含一个空值,即字面意义上的NULL 或有时-。所有这些值都将被视为$null

有没有办法拦截 CSV 解析来处理它?

实际上我有一个可行的解决方案,但该解决方案非常缓慢。迭代 2500 多个项目需要 20 分钟,而按原样读取 CSV 文件只需几秒钟。

这个想法是迭代每个属性:

$private:result = @{}
foreach($private:prop in $private:line.PSObject.Properties){
    $private:value = $null
    $private:result.Add($private:prop.Name, ($private:value | Filter-Value))
}
$private:result
...

function Filter-Value{
    param(
        [Parameter(Position=0, ValueFromPipeline=$true)]
        [object]$In
    )

    if(-not $In){
        $null
    }
    elseif(($In -is [string]) -and ($In.Length -eq 0)) {
        $null
    }
    elseif(($In -eq "NULL") -or ($In -eq "-")) {
        $null
    }
    else{
        $In
    }
}

完整代码:


function Import-CsvEx{
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [string]$Path,
        [Parameter()]
        [string]$Delimiter
    )
    begin{
        Write-Verbose "Begin read of file $Path"
    }
    process{
        # We use file stream and stream reader to automatically detect encoding
        $private:fileStream = [System.IO.File]::OpenRead($Path)

        $private:streamReader = New-Object System.IO.StreamReader($private:fileStream, [System.Text.Encoding]::Default, $true)

        $private:fileContent = $private:streamReader.ReadToEnd()

        $private:streamReader.Dispose()
        $private:fileStream.Dispose()        

        $private:csv = ConvertFrom-Csv $private:fileContent  -Delimiter $Delimiter

        for($private:i=0; $private:i -lt $private:csv.Count ; $private:i++){
            Write-Progress -Id 1003 -Activity "Reading  CSV" -PercentComplete ($private:i*100/$private:csv.count)
            $private:line = $private:csv[$private:i]
            $private:result = @{}
            foreach($private:prop in $private:line.PSObject.Properties){
                $private:value = $null
                $private:result.Add($private:prop.Name, ($private:value | Filter-Value))
            }

            # actually outputs the object to the pipeline
            New-Object psobject -Property $private:result

        }
        Write-Progress -Id 1003 -Activity "Reading CSV" -Completed

    }
    end{
        Write-Verbose "End read of file $Path"
    }
}

function Filter-Value{
    param(
        [Parameter(Position=0, ValueFromPipeline=$true)]
        [object]$In
    )

    if(-not $In){
        $null
    }
    elseif(($In -is [string]) -and ($In.Length -eq 0)) {
        $null
    }
    elseif(($In -eq "NULL") -or ($In -eq "-")) {
        $null
    }
    else{
        $In
    }
}

【问题讨论】:

    标签: performance powershell csv


    【解决方案1】:

    鉴于性能是问题

    • 避免使用管道(尽管代价是必须将所有数据放入内存中)。

    • 避免使用Write-Progress

    • 通过.psobject.Properties避免重复反射。

    顺便说一句:很少需要使用$private: 范围,这会使您的代码难以阅读;请注意,在函数内仅按名称分配变量会隐式创建本地变量(例如,$var = 42);如果您需要明确阻止 descendant 作用域看到这些变量,则只需要 $private: - 请参阅 this answer 了解更多信息。

    # Import the CSV data into a collection in memory.
    # NOTE: In Windows PowerShell, Import-Csv defaults to ASCII(!) encoding.
    #       Use -Encoding Default to use the system's ANSI code page, for instance.
    #       PowerShell [Core] 6+ consistently defaults to (BOM-less) UTF-8.
    $objects = Import-Csv $Path -Delimiter $Delimiter
    
    # Extract the property (column) names from the 1st imported object.
    $propNames = $objects[0].psobject.Properties.Name
    
    # Loop over all objects...
    foreach ($object in $objects) {
    
      # ... and make the quasi-null properties $null.
      foreach ($propName in $propNames) {
        if ($object.$propName -in '', '-', 'NULL') {
          $object.$propName = $null
        }
      }
    
      # Output the modified object right away, if desired.
      # Alternatively, operate on the $objects collection later.
      $object
    
    }
    

    如果您无法将所有数据都放入内存,请使用Import-Csv ... | ForEach-Object { ... },同时仍仅在第一次 调用脚本块({ ... }) 中提取属性名称。

    【讨论】:

    • 关于$private 范围,我有faced scoping issues。 Import-CsvEX 函数是可重用模块的一部分,我需要不惜一切代价避免范围问题。
    • 我更新了我的代码。我知道 CSV 文件的处理速度非常快。我保留了 Write-Progress,但更改了代码以更新初始对象而不是发出新对象。我还将反射移出 for 循环。一切都很好知道。谢谢!
    • 很高兴听到这个消息,@SteveB;我的荣幸。关于范围问题:确保所有局部变量都已初始化并因此显式创建就足够了,您不需要 $private: 范围,这对您的功能没有影响,因为没有子范围被装箱;看到my answer链接的帖子。
    【解决方案2】:

    我喜欢Import-CSV,操纵然后Export-CSV

    C:\> $ted = import-csv -Path ted.csv
    C:\> $ted
    
    Name Desc
    ---- ----
    ted
    fred Dash-Dash
    ned  NULL
    
    
    C:\> $ted | ? { $_.Desc -match 'NULL|-|""' -or $_.Desc.Length -eq 0 -or $Null -eq $_.Desc} | %
     {$_.Desc = "In" }
    C:\> $ted
    
    Name Desc
    ---- ----
    ted  In
    fred In
    ned  In
    
    C:\> Export-CSV -Path ted.csv -NoTypeInformation
    

    【讨论】:

    • 是的,Import-Csv 是对 OP 的文件流 + ConvertFrom-Csv 方法的改进,但请注意,要求是遍历所有属性(列)以检查值要修复,并且要快速完成。
    • 需要文件流方法来处理编码问题。使用此构造函数,它匹配系统默认编码,其中 Import-Csv 从文件使用 ASCII 编码。
    • @SteveB, Import-Csv 有一个 -Encoding 参数,就像所有文件处理标准 cmdlet 一样;使用-Encoding Default 使用系统的ANSI 代码页。不幸的是,Windows PowerShell 在默认字符编码方面非常不一致,这与 PowerShell [Core] 6+ 不同,后者现在一致地默认为无 BOM 的 UTF-8 - 请参阅this answer 的底部部分。
    【解决方案3】:

    这是 filter 的完美用例 - 一个仅实现 process 块的管道友好函数:

    filter Parse-Null {
      # iterate over all properties, look for "null-like" values, replace with empty string
      foreach($prop in $_.psobject.Properties){
        if($prop.Value -in '-','NULL'){
          $prop.Value = ''
        }
      }
    
      # pass the (potentially modified) object along
      $_
    }
    

    然后使用like:

    $csvData = @'
    h1,h2,h3
    NULL,something,-
    ,-,someother
    '@
    $csvData |ConvertFrom-Csv |Parse-Null
    # or
    Import-Csv ... |Parse-Null
    

    【讨论】:

    • 不确定是否理解...应该如何加快进程?仍然存在所有属性的迭代(我认为这是热点)
    • @SteveB 确实如此,但你不会绕过那部分 - 这里的区别是我修改现有对象而不是创建一个新对象:)
    • 更新对象而不是发射一个新对象对全局速度非常有利。谢谢你
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-07
    • 2022-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-14
    相关资源
    最近更新 更多