2015-08-14 3 views
-1

фонЧтение из трубопровода потока в PowerShell

Я надеюсь писать код, который использует Microsoft.VisualBasic.FileIO.TextFieldParser для синтаксического анализа некоторых данных в формате CSV. Система, в которой я генерирую эти данные, не понимает кавычек; поэтому я не могу избежать разделителя; а скорее заменить его. Я нашел решение, используя вышеприведенный текстовый синтаксический анализатор, но я видел, как люди его используют с помощью ввода из файлов. Вместо того, чтобы записывать свои данные в файл только для его повторного импорта, я предпочитаю хранить вещи в памяти/использовать конструктор этого класса, который принимает поток в качестве входных данных.

В идеале, он мог бы получать канал напрямую из любого используемого для конвейера потока памяти; но я не мог понять, как получить доступ к этому. В моем текущем коде я создаю свой собственный поток памяти и подаю данные на него из конвейера; затем попытайтесь прочитать это. К сожалению, я что-то пропустил.

Вопросы

  1. Как вы читаете из/записи в потоки памяти в PowerShell?
  2. Можно ли читать непосредственно из потока, который подается в конвейер функции?

Код

clear-host 
[Reflection.Assembly]::LoadWithPartialName("System.IO") | out-null 
#[Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") | out-null 

function Clean-CsvStream { 
    [CmdletBinding()] 
    param (
     [Parameter(Mandatory = $true, ValueFromPipeline=$true)] 
     [string]$Line 
     , 
     [Parameter(Mandatory = $false)] 
     [char]$Delimiter = ',' 
    ) 
    begin { 
     [System.IO.MemoryStream]$memStream = New-Object System.IO.MemoryStream 
     [System.IO.StreamWriter]$writeStream = New-Object System.IO.StreamWriter($memStream) 
     [System.IO.StreamReader]$readStream = New-Object System.IO.StreamReader($memStream) 
     #[Microsoft.VisualBasic.FileIO.TextFieldParser]$Parser = new-object Microsoft.VisualBasic.FileIO.TextFieldParser($memStream) 
     #$Parser.SetDelimiters($Delimiter) 
     #$Parser.HasFieldsEnclosedInQuotes = $true 
     #$writeStream.AutoFlush = $true 
    } 
    process { 
     $writeStream.WriteLine($_) 
     #$writeStream.Flush() #maybe we need to flush it before the reader will see it? 
     write-output $readStream.ReadLine() 
     #("Line: {0:000}" -f $Parser.LineNumber) 
     #write-output $Parser.ReadFields() 
    } 
    end { 
     #close streams and dispose (dodgy catch all's in case object's disposed before we call Dispose) 
     #try {$Parser.Close(); $Parser.Dispose()} catch{} 
     try {$readStream.Close(); $readStream.Dispose()} catch{} 
     try {$writeStream.Close(); $writeStream.Dispose()} catch{} 
     try {$memStream.Close(); $memStream.Dispose()} catch{} 
    } 
} 
1,2,3,4 | Clean-CsvStream -$Delimiter ';' #nothing like the real data, but I'm not interested in actual CSV cleansing at this point 

Обход

В то же время мое решение просто сделать это заменить на объектах недвижимости, а игровая, чем строки CSV.

$cols = $objectArray | Get-Member | ?{$_.MemberType -eq 'NoteProperty'} | select -ExpandProperty name 
$objectArray | %{$csvRow =$_; ($cols | %{($csvRow.$_ -replace "[`n,]",':')}) -join ',' } 

Update

я понял недостающий код был $memStream.Seek(0, [System.IO.SeekOrigin]::Begin) | out-null;

Однако это не ведет себя полностью, как и ожидалось; т.е. первая строка моего CSV отображается дважды, а другой вывод - в неправильном порядке; поэтому, по-видимому, я неправильно понял, как использовать Seek.

clear-host 
[Reflection.Assembly]::LoadWithPartialName("System.IO") | out-null 
[Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") | out-null 

function Clean-CsvStream { 
    [CmdletBinding()] 
    param (
     [Parameter(Mandatory = $true, ValueFromPipeline=$true)] 
     [string]$CsvRow 
     , 
     [Parameter(Mandatory = $false)] 
     [char]$Delimiter = ',' 
     , 
     [Parameter(Mandatory = $false)] 
     [regex]$InvalidCharRegex 
     , 
     [Parameter(Mandatory = $false)] 
     [string]$ReplacementString 

    ) 
    begin { 
     [System.IO.MemoryStream]$memStream = New-Object System.IO.MemoryStream 
     [System.IO.StreamWriter]$writeStream = New-Object System.IO.StreamWriter($memStream) 
     [Microsoft.VisualBasic.FileIO.TextFieldParser]$Parser = new-object Microsoft.VisualBasic.FileIO.TextFieldParser($memStream) 
     $Parser.SetDelimiters($Delimiter) 
     $Parser.HasFieldsEnclosedInQuotes = $true 
     $writeStream.AutoFlush = $true 
    } 
    process { 
     if ($InvalidCharRegex) { 
      $writeStream.WriteLine($CsvRow) 
      #flush here if not auto 
      $memStream.Seek(0, [System.IO.SeekOrigin]::Begin) | out-null; 
      write-output (($Parser.ReadFields() | %{$_ -replace $InvalidCharRegex,$ReplacementString }) -join $Delimiter) 
     } else { #if we're not replacing anything, keep it simple 
      $CsvRow 
     } 
    } 
    end { 
     "end {" 
     try {$Parser.Close(); $Parser.Dispose()} catch{} 
     try {$writeStream.Close(); $writeStream.Dispose()} catch{} 
     try {$memStream.Close(); $memStream.Dispose()} catch{} 
     "} #end" 
    } 
} 
$csv = @(
    (new-object -TypeName PSCustomObject -Property @{A="this is regular text";B="nothing to see here";C="all should be good"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text2";B="what the`nLine break!";C="all should be good2"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text3";B="ooh`r`nwindows line break!";C="all should be good3"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text4";B="I've got;a semi";C="all should be good4"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text5";B="""You're Joking!"" said the Developer`r`n""No honestly; it's all about the secret VB library"" responded the Google search result";C="all should be good5"}) 
) | convertto-csv -Delimiter ';' -NoTypeInformation 
$csv | Clean-CsvStream -Delimiter ';' -InvalidCharRegex "[`r`n;]" -ReplacementString ':' 

ответ

1

После много играть вокруг него, кажется, что это работает:

clear-host 
[Reflection.Assembly]::LoadWithPartialName("System.IO") | out-null 
[Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") | out-null 

function Clean-CsvStream { 
    [CmdletBinding()] 
    param (
     [Parameter(Mandatory = $true, ValueFromPipeline=$true)] 
     [string]$CsvRow 
     , 
     [Parameter(Mandatory = $false)] 
     [char]$Delimiter = ',' 
     , 
     [Parameter(Mandatory = $false)] 
     [regex]$InvalidCharRegex 
     , 
     [Parameter(Mandatory = $false)] 
     [string]$ReplacementString 
    ) 
    begin { 
     [bool]$IsSimple = [string]::IsNullOrEmpty($InvalidCharRegex) 
     if(-not $IsSimple) { 
      [System.IO.MemoryStream]$memStream = New-Object System.IO.MemoryStream 
      [System.IO.StreamWriter]$writeStream = New-Object System.IO.StreamWriter($memStream) 
      [Microsoft.VisualBasic.FileIO.TextFieldParser]$Parser = new-object Microsoft.VisualBasic.FileIO.TextFieldParser($memStream) 
      $Parser.SetDelimiters($Delimiter) 
      $Parser.HasFieldsEnclosedInQuotes = $true 
     } 
    } 
    process { 
     if ($IsSimple) { 
      $CsvRow 
     } else { #if we're not replacing anything, keep it simple 
      [long]$seekStart = $memStream.Seek(0, [System.IO.SeekOrigin]::Current) 
      $writeStream.WriteLine($CsvRow) 
      $writeStream.Flush() 
      $memStream.Seek($seekStart, [System.IO.SeekOrigin]::Begin) | out-null 
      write-output (($Parser.ReadFields() | %{$_ -replace $InvalidCharRegex,$ReplacementString }) -join $Delimiter) 
     } 
    } 
    end { 
     if(-not $IsSimple) { 
      try {$Parser.Close(); $Parser.Dispose()} catch{} 
      try {$writeStream.Close(); $writeStream.Dispose()} catch{} 
      try {$memStream.Close(); $memStream.Dispose()} catch{} 
     } 
    } 
} 
$csv = @(
    (new-object -TypeName PSCustomObject -Property @{A="this is regular text";B="nothing to see here";C="all should be good"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text2";B="what the`nLine break!";C="all should be good2"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text3";B="ooh`r`nwindows line break!";C="all should be good3"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text4";B="I've got;a semi";C="all should be good4"}) 
    ,(new-object -TypeName PSCustomObject -Property @{A="this is regular text5";B="""You're Joking!"" said the Developer`r`n""No honestly; it's all about the secret VB library"" responded the Google search result";C="all should be good5"}) 
) | convertto-csv -Delimiter ';' -NoTypeInformation 
$csv | Clean-CsvStream -Delimiter ';' -InvalidCharRegex "[`r`n;]" -ReplacementString ':' 

т.е.

  1. искать текущую позицию перед записью
  2. затем написать
  3. затем промойте (если не авто)
  4. затем искать начало данных
  5. затем читать
  6. повтор

Я не уверен, что это правильно; поскольку я не могу найти никаких хороших примеров или объяснений, поэтому просто играл, пока что-то не сработало, что смутно имело смысл.

Меня также интересует, кто знает, как читать прямо из потока трубопровода; то есть удалить дополнительные накладные расходы бонусных потоков.


Для комментария @ и.п. в

К сожалению, это так поздно; в случае, если это использования другим:

Если конец строки разделителем является CRLF (\r\n), а не только Cr (\r), то это легко неоднозначность между окончанием записи/строки, и разрывы строк в пределах поля :

Get-Content -LiteralPath 'D:\test\file to clean.csv' -Delimiter "`r`n" | 
%{$_.ToString().TrimEnd("`r`n")} | #the delimiter is left on the end of the string; remove it 
%{('"{0}"' -f $_) -replace '\|','"|"'} | #insert quotes at start and end of line, as well as around delimeters 
ConvertFrom-Csv -Delimiter '|' #treat the pipeline content as a valid pipe delimitted csv 

Однако, если не вы будете иметь способ сказать, который Cr является концом записи, и которые просто перерыв в тексте. Вы можете обойти это немного, подсчитав количество труб; то есть, как будто у вас есть 5 столбцов, любые CR перед четвертым разделителем являются разрывами строк, а не концом записи. Однако, если есть еще один разрыв строки, вы не можете быть уверены, что это разрыв строки в данных последнего столбца или в конце этой строки. Если вы знаете, что либо первый, либо последний столбец не содержат разрывов строк (или обоих), вы можете обойти это. Для всех этих более сложных сценариев я подозреваю, что регулярное выражение будет лучшим вариантом; используя что-то вроде select-string, чтобы применить его. Если это требуется; разместите здесь вопрос, указав свои точные требования. & информация о том, что вы уже пытались, и другие могут помочь вам.

+0

Это может быть именно то, что мне нужно, но не знакомы с сценариями PowerShell. Я не знаю, как использовать код. Если у меня есть файл csv в следующем месте D: \ test \ file to clean.csv, то как мне запустить скрипт? Разделитель - это труба "|" , спецификатор текста не имеет значения, и после каждой строки есть CR. Некоторые поля имеют CR где-то посередине, и этот CR необходимо удалить и заменить пробелом ... –

+0

@ M.R. Если вас все еще интересует; см. обновленный раздел в конце этого ответа. К сожалению, я пропустил этот комментарий изначально, как было, когда вы прокомментировали. – JohnLBevan

+1

Спасибо, Джон, да, меня все еще интересует сценарий, я попробую! –

Смежные вопросы