Перехват Powershell и исправление значения c при чтении файла CSV - PullRequest
1 голос
/ 10 марта 2020

В скрипте 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
    }
}

Ответы [ 3 ]

4 голосов
/ 10 марта 2020

Учитывая, что производительность является проблемой :

  • Избегайте конвейера (хотя за счет необходимости помещать все данные в память).

  • Избегайте использования Write-Progress.

  • Избегайте повторного отражения через .psobject.Properties.

В качестве отступления: использование области действия $private: редко требуется и делает ваш код трудным для чтения; обратите внимание, что присвоение переменных по простому имени внутри функции неявно создает local переменных (например, $var = 42); вам понадобится $private:, только если вам нужно явно запретить потомкам областям видения этих переменных - см. этот ответ для получения дополнительной информации.

# 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 { ... }, но при этом извлекайте только имена свойств в first вызове блока скрипта ({ ... }).

3 голосов
/ 10 марта 2020

Это идеальный вариант использования для 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
  $_
}

Затем используйте как:

$csvData = @'
h1,h2,h3
NULL,something,-
,-,someother
'@
$csvData |ConvertFrom-Csv |Parse-Null
# or
Import-Csv ... |Parse-Null
0 голосов
/ 10 марта 2020

Мне нравится 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...