多行数据:使用Powershell从CSV删除LF但不删除CRLF

问题描述

我需要清除一些CSV数据,方法删除内联换行符和特殊字符(如印刷引号)。我觉得我可以使用Python或Unix utils来工作,但是我被困在一个非常普通的Windows 2012机器上,所以尽管我缺乏使用它的经验,但是我还是给PowerShell v5开了一枪。

这是我想要实现的目标:

$InputFile

"INCIDENT_NUMBER","FirsT_NAME","LAST_NAME","DESCRIPTION"{CRLF}
"00020306","John","Davis","Employee was not dressed appropriately."{CRLF}
"00020307","Brad","Miller","Employee told customer,""Go shop somewhere else!"""{CRLF}
"00020308","Ted","Jones","Employee told supervisor,“That’s not my job”"{CRLF}
"00020309","Bob","Meyers","Employee did the following:{LF}
• Showed up late{LF}
• Did not complete assignments{LF}
• Left work early"{CRLF}
"00020310","Employee was not dressed appropriately."{CRLF}

$OutputFile

"INCIDENT_NUMBER","DESCRIPTION"{CRLF}
"00020307",""That's not my job"""{CRLF}
"00020309","Employee did the following: * Showed up late * Did not complete assignments * Left work early"{CRLF}
"00020310","Employee was not dressed appropriately."{CRLF}

以下代码有效:

(Get-Content $InputFile -Raw) `
    -replace '(?<!\x0d)\x0a',' ' `
    -replace "[‘’´]","'" `
    -replace '[“”]','""' `
    -replace "\xa0"," " `
    -replace '[•·]','*' | Set-Content $OutputFile -Encoding ASCII

但是,我要处理的实际数据是一个超过1百万行的4GB文件Get-Content -Raw内存不足。我尝试了Get-Content -ReadCount 10000,但是删除 all 换行符,大概是因为它是逐行读取的。

更多Google搜索使我进入了从here获得的Import-Csv:

Import-Csv $InputFile | ForEach {
    $_.notes = $_.notes -replace '(?<!\x0d)\x0a',' '
    $_
} | Export-Csv $OutputFile -NoType@R_138_4045@ion -Encoding ASCII

但是我的对象似乎没有notes属性

Exception setting "notes": "The property 'notes' cannot be found on this object. Verify that the property exists and can be set."
At C:\convert.ps1:53 char:5
+     $_.notes= $_.notes -replace '(?<!\x0d)\x0a',' '
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [],SetValueInvocationException
    + FullyQualifiedErrorId : ExceptionWhenSetting

我发现了另一个使用Value属性的示例,但出现了相同的错误

我尝试在每个对象上运行Get-Member,看起来它是基于文件头分配属性的,就像我可以用$_.DESCRIPTION获取它一样,但是我不知道足够的PowerShell在属性 all 上运行替换项:(

请帮助?谢谢!

更新:

我最终放弃了PS,并在AutoIT中对此进行了编码。它不是很好,而且维护起来会更加困难,尤其是因为2.5年内还没有新版本发布。但是它可以工作,并且可以在4分钟内处理prod文件

不幸的是,我也不能轻易地键入LF,所以我最终还是根据^"[^",]创建新行的逻辑(行以引号开头,第二个字符不是引号,或者逗号)。

这是AutoIT代码

#include <FileConstants.au3>

If $CmdLine[0] <> 2 Then
   ConsoleWriteError("Error in parameters" & @CRLF)
   Exit 1
EndIf

Local Const $sInputFilePath = $CmdLine[1]
Local Const $sOutputFilePath = $CmdLine[2]

ConsoleWrite("Input file: " & $sInputFilePath & @CRLF)
ConsoleWrite("Output file: " & $sOutputFilePath & @CRLF)
ConsoleWrite("***** WARNING *****" & @CRLF)
ConsoleWrite($sOutputFilePath & " is being OVERWRITTEN!" & @CRLF & @CRLF)

Local $bFirstLine = True

Local $hInputFile = FileOpen($sInputFilePath,$FO_ANSI)
   If $hInputFile = -1 Then
        ConsoleWriteError("An error occurred when reading the file.")
        Exit 1
     EndIf

Local $hOutputFile = FileOpen($sOutputFilePath,$FO_OVERWRITE + $FO_ANSI)
   If $hOutputFile = -1 Then
        ConsoleWriteError"An error occurred when opening the output file.")
        Exit 1
     EndIf

ConsoleWrite("Processing..." &@CRLF)

While True
   $sLine = FileReadLine($hInputFile)
   If @error = -1 Then ExitLoop

   ;Replace typographic single quotes and backtick with apostrophe
   $sLine = StringRegExpReplace($sLine,"[‘’´]","'")

   ;Replace typographic double quotes with normal quote (doubled for in-field CSV)
   $sLine = StringRegExpReplace($sLine,'[“”]','""')

   ;Replace bullet and middot with asterisk
   $sLine = StringRegExpReplace($sLine,'[•·]','*')

   ;Replace non-breaking space (0xA0) and delete (0x7F) with space
   $sLine = StringRegExpReplace($sLine,"[\xa0\x7f]"," ")

   If $bFirstLine = False Then
      If StringRegExp($sLine,'^"[^",]') Then
         $sLine = @CRLF & $sLine
      Else
         $sLine = " " & $sLine
      EndIf
   Else
      $bFirstLine = False
   EndIf

   FileWrite($hOutputFile,$sLine)

WEnd

ConsoleWrite("Done!" &@CRLF)
FileClose($hInputFile)
FileClose($hOutputFile)

解决方法

注意:

  • 请参阅我的other answer,以获取健壮解决方案。

  • 对于性能良好的常规逐行处理解决方案,下面的答案可能仍然很有趣,尽管它始终将纯LF实例也视为行分隔符(它具有已更新为使用相同的正则表达式来区分在行的开头和在问题中添加的AutoIt解决方案中使用的行的延续)。


鉴于文件的大小,出于性能考虑,我建议坚持使用纯文本处理:

  • switch语句可实现快速的逐行处理;它像PowerShell一般一样将CRLF和LF都识别为换行符。但是请注意,由于返回的每一行都剥去了其尾随的换行符,因此您将无法判断输入行是否仅以LF结尾。

  • 直接使用.NET类型System.IO.StreamWriter,绕过管道并允许快速写入输出文件。

  • 有关PowerShell性能的常规技巧,请参见this answer

$inputFile = 'in.csv'
$outputFile = 'out.csv'

# Create a stream writer for the output file.
# Default to BOM-less UTF-8,but you can pass a [System.Text.Encoding]
# instance as the second argument.
# Note: Pass a *full* path,because .NET's working dir. usually differs from PowerShell's
$outFileWriter = [System.IO.StreamWriter]::new("$PWD/$outputFile")

# Use a `switch` statement to read the input file line by line.
$outLine = ''
switch -File $inputFile -Regex {
  '^"[^",]' { # (Start of) a new row.
    if ($outLine) { # write previous,potentially synthesized line
      $outFileWriter.WriteLine($outLine)
    }
    $outLine = $_ -replace "[‘’´]","'" -replace '[“”]','""' -replace '\u00a0',' '
  }
  default { # Continuation of a row.
    $outLine += ' ' + $_ -replace "[‘’´]",' ' `
      -replace '[•·]','*' -replace '\n'
  }
}
# Write the last line.
$outFileWriter.WriteLine($outLine)

$outFileWriter.Close()

注意:以上假设没有行 continuation 也与正则表达式模式'^"[^",]' 相匹配,希望它足够健壮(您认为它已经是,假设您基于AutoIt解决方案)。

行首与后续行的继续之间的这种简单区别消除了对较低级别文件I / O的需求,以便区分CRLF和LF换行符,这是我的other answer所做的。

,

第一个答案可能比这更好,因为我不确定PS是否需要以这种方式将所有内容加载到内存中(尽管我认为是这样),但是从上面的内容出发,我一直在思考这条线...

# Import CSV into a variable
$InputFile = Import-Csv $InputFilePath

# Gets all field names,stores in $Fields
$InputFile | Get-Member -MemberType NoteProperty | 
Select-Object Name | Set-Variable Fields

# Updates each field entry
$InputFile | ForEach-Object {
    $thisLine = $_
    $Fields | ForEach-Object {
            ($thisLine).($_.Name) = ($thisLine).($_.Name) `
                -replace '(?<!\x0d)\x0a',' ' `
                -replace "[‘’´]","'" `
                -replace '[“”]','""' `
                -replace "\xa0"," " `
                -replace '[•·]','*'
            }
    $thisLine | Export-Csv $OutputFile -NoTypeInformation -Encoding ASCII -Append
} 
,

这是另一种“逐行”尝试,有点类似于mklement0的回答。 假定没有“行继续”行以“ 开头。希望它的性能好得多!

# Clear contents of file (Not sure if you need/want this...)
if (Test-Path -type leaf $OutputFile) { Clear-Content $OutputFile }

# Flag for first entry,since no data manipulation needed there
$firstEntry = $true

foreach($line in [System.IO.File]::ReadLines($InputFile)) {
    if ($firstEntry) {
        Add-Content -Path $OutputFile -Value $line -NoNewline
        $firstEntry = $false
    }
    else {
        if ($line[0] -eq '"') { Add-Content -Path $OutputFile "`r`n" -NoNewline}
        else { Add-Content -Path $OutputFile " " -NoNewline}
        $sanitizedLine = $line -replace '(?<!\x0d)\x0a',' ' `
                               -replace "[‘’´]","'" `
                               -replace '[“”]','""' `
                               -replace "\xa0"," " `
                               -replace '[•·]','*'
        Add-Content -Path $OutputFile -Value $sanitizedLine -NoNewline
    }
}

该技术基于其他答案及其评论:https://stackoverflow.com/a/47146987/7649168

(也感谢mklement0解释了我先前回答的性能问题。)

,

以下两种方法在原理上可以正常工作 ,但是对于大型输入文件(例如您的文件)来说太慢

  • 面向对象的处理,使用Import-Csv / Export-Csv

    • 使用Import-Csv将CSV解析为对象,修改对象的DESCRIPTION属性值,然后使用Export-Csv重新导出。由于行内部仅LF换行符位于双引号字段内,因此它们被识别为同一行的一部分。

    • 尽管它是一种健壮且概念上优雅的方法,但它是迄今为止最慢且非常占用内存的方法-请参见GitHub issue #7603,其中讨论了原因,以及GiHub feature request #11027通过以下方法改善了情况输出哈希表而不是自定义对象[pscustomobject])。

  • 使用Get-Content / Set-Content的纯文本处理

    • 使用Get-Content -Delimiter "`r`n"仅通过CRLF(而不是LF)将文本文件分为几行,根据需要转换每一行,并使用Set-Content将其保存到输出文件中。

    • 尽管您通常会因使用管道而在概念上的优雅而付出性能上的损失,但这会使得逐行保存Set-Content的结果变得有些缓慢, Get-Content尤其慢,因为它用有关原始文件的其他属性来装饰每个输出字符串(行),这非常昂贵。请查看绿灯但尚未实现的GitHub feature request #7537,以通过省略这种装饰来提高性能(和内存使用)。


解决方案

  • 出于性能原因,因此需要直接使用.NET API
    • 注意:如果PowerShell解决方案仍然太慢,请考虑使用Add-Type通过C#代码的临时编译来创建一个帮助器类。最终,当然,仅使用编译后的代码将效果最佳。
  • 虽然没有直接等效于Get-Content -Delimiter "`r`n"的内容,但是您可以使用固定大小的字符块(数组)读取文本文件 ,使用 System.IO.StreamReader.ReadBlock() 方法(.NET Framework 4.5+ / .NET Core 1+),然后可以在其上执行所需的转换,如下所示。

注意:

  • 为获得最佳性能,请在下面选择一个较高的$BUFSIZE值以最大程度地减少读取次数和处理迭代次数;显然,必须选择该值,以免耗尽内存。

  • 甚至不需要解析读取到CRLF换行符中的块,因为您只需使用正则表达式(仅针对原始方法{{1})的正则表达式来定位仅LF的行即可。 }(请参见下面的代码注释)。

  • 为简洁起见,省略了错误处理,但是通常应将'(?<!\r|^)\n'调用关闭文件放在try / catch / finally语句的.Close()块中。

finally