【问题标题】:Replace column comma separator in csv file and handle fields with single quotes around value替换 csv 文件中的列逗号分隔符并用单引号处理字段值
【发布时间】:2020-01-31 15:13:59
【问题描述】:

一个系统正在生成一个我无法影响的 csv 文件。

如果数据本身包含逗号,则有两列的值可以用一对单引号括起来。

示例数据 - 4 列

123,'abc,def,ghf',ajajaj,1 
345,abdf,'abc,def,ghi',2
556,abdf,def,3
999,'a,b,d','d,e,f',4

我想使用 powershell 的结果...

不属于数据的逗号 - 表示分隔字段的逗号被替换为指定的分隔符(在竖线星下面的情况下)。一对单引号之间的逗号保留为逗号。

结果

123|*'abc,def,ghf'|*ajajaj|*1 
345|*abdf|*'abc,def,ghi'|*2
556|*abdf|*def|*3
999|*'a,b,d'|*'d,e,f'|*4

如果可能的话,我想使用 reg 表达式来执行此操作,但我不知道如何执行此操作。

【问题讨论】:

  • 在 .net 中,我会使用一些简单的 CSV 读取器/写入器库来逐行读取,只需用逗号读入并用管道作为分隔符写出,但实际上我只是注意到其中一个的开发人员我使用的最古老和最常用的 CSV 库 Jouni Heikniemi 在 powershell 的 Import-Csv cmdlet 上有一些话 - heikniemi.net/hardcoded/2010/01/… - 与相关的 Export-Csv docs.microsoft.com/en-us/powershell/module/… 配对 - 在 powershell 中导入/导出可能很简单
  • 您真的需要 2 个字符的分隔符 |*,还是只是出于说明目的使用它?
  • |* 仅用于说明目的。字段分隔符可以只是一个管道。
  • 我刚刚找到了一个接近的答案,但是它只用新的分隔符将单引号括起来的字段值包围起来,它不会替换那些没有用单引号括起来的字段值的字段分隔符。 .stackoverflow.com/questions/31127547/…

标签: c# powershell


【解决方案1】:

虽然我认为这会创建一个格式奇怪的 CSV 文件,但使用 PowerShell,您可以将 switch-Regex-File 参数一起使用。这可能是处理大文件的最快方式,并且只需要几行代码:

# create a regex that will find comma's unless they are inside single quotes
$commaUnlessQuoted = ",(?=([^']*'[^']*')*[^']*$)"

$result = switch -Regex -File 'D:\test.csv' {
    # added -replace "'" to also remove the single quotes as commented
    default { $_ -replace "$commaUnlessQuoted", '|*' -replace "'" }
}

# output to console
$result

# output to new (sort-of) CSV file
$result | Set-Content -Path 'D:\testoutput.csv'


更新

作为mklement0 pointed out,上面的代码完成了这项工作,但代价是在写入输出文件之前完全在内存中将更新的数据创建为数组。
如果这是一个问题(文件太大而无法容纳可用内存),您还可以更改代码以读取/替换原始行并立即将该行写出到输出文件。

下一种方法几乎不会占用任何内存,但当然是以在磁盘上执行更多写入操作为代价的。

# make sure this is an absolute path for .NET
$outputFile = 'D:\output.csv'
$inputFile  = 'D:\input.csv'

# create a regex that will find comma's unless they are inside single quotes
$commaUnlessQuoted = ",(?=([^']*'[^']*')*[^']*$)"

# create a StreamWriter object. Uses UTF8Encoding without BOM (Byte Order Mark) by default.
# if you need a different encoding for the output file, use for instance
# $writer = [System.IO.StreamWriter]::new($outputFile, $false, [System.Text.Encoding]::Unicode)
$writer = [System.IO.StreamWriter]::new($outputFile)
switch -Regex -File $inputFile {
    default {
        # added -replace "'" to also remove the single quotes as commented
        $line = $_ -replace "$commaUnlessQuoted", '|*' -replace "'"
        $writer.WriteLine($line)
        # if you want, uncomment the next line to show on console
        # $line
    }
}

# remove the StreamWriter object from memory when done
$writer.Dispose()

结果:

123|*abc,def,ghf|*ajajaj|*1 
345|*abdf|*abc,def,ghi|*2
556|*abdf|*def|*3
999|*a,b,d|*d,e,f|*4

正则表达式详细信息:

,                 Match the character “,” literally
(?=               Assert that the regex below can be matched, starting at this position (positive lookahead)
   (              Match the regular expression below and capture its match into backreference number 1
      [^']        Match any character that is NOT a “'”
         *        Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
      '           Match the character “'” literally
      [^']        Match any character that is NOT a “'”
         *        Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
      '           Match the character “'” literally
   )*             Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
   [^']           Match any character that is NOT a “'”
      *           Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
   $              Assert position at the end of the string (or before the line break at the end of the string, if any)
)

【讨论】:

  • 这行得通 - 非常感谢!我刚刚在一个小文件上进行了测试,它工作正常。我目前正在针对我拥有的最大文件运行。我必须将结果写到原始文件中 - 我使用您的代码所做的。在我写回原始文件之前,我确实需要删除单引号 - 有什么建议吗?
  • @IanCarrick 当然只需在该行中添加另一个替换:-replace "'",使其变为default { $_ -replace "$commaUnlessQuoted", '|*' -replace "'" }
  • @IanCarrick 我添加了新的更新代码作为替代方法,而不会对系统中的可用内存产生如此大的影响。
  • 非常感谢更新我正在针对我最大的文件运行完整的脚本...
  • 我已接受您的回答作为回答问题的人,并且您已经超越了 - 感谢一位全新的 PowerShell 开发人员。
【解决方案2】:

Theo's helpful answer简洁高效。

让我补充以下解决方案:

  • 显示如何将每个 CSV 行解析为字段值数组,基于识别嵌入的 '...' 引用(它可以很容易地适应 "..." 引用),没有包括' 字符。在输出中(如果使用 | 等分隔符,则不再需要在语法上。

  • 展示了一种更快的写入输出文件的方法,使用System.IO.File.WriteAllLines

# In and output file paths.
# IMPORTANT: To use file paths with .NET methods, as below, always use
#            FULL PATHS, because .NET's current directory differs from PowerShell's
$inPath = "$PWD/input.csv"
$outPath = "$PWD/output.csv"

[IO.File]::WriteAllLines(
  $outPath,
  # CAVEAT: Even though ReadLines() enumerates *lazily* itself,
  #         applying PowerShell's .ForEach() method to it causes the lines
  #         to all be collected in memory  first.
  [IO.File]::ReadLines($inPath).ForEach({
    # Parse the row into field values, whether they're single-quoted or not.
    $fieldValues = $_ -split "(?x) ,? ( '[^']*' | [^,]* ) ,?" -ne '' -replace "'"
    # Join the field values - without single quotes - to form a row with the
    # new delimiter.
    $fieldValues -join '|'
  })
)

* 为简洁起见,我省略了一个重要的优化:if (-not $_.Contains("'")) { $_.Replace(",", "|") } 可用于处理不包含 ' 字符的行。更快。
* -split,基于正则表达式的string splitting operator 用于将行拆分为字段。
* 内联选项(?x) 用于使正则表达式更具可读性,如this answer 中所述。

正如代码 cmets 所述,上述解决方案仍然将整个文件加载到内存中

需要使用 管道 来避免这种情况,这会大大降低解决方案速度,但是:

& {
 foreach ($line in [IO.File]::ReadLines($inPath)) {
    $fieldValues = $line -split "(?x) ,? ( '[^']*' | [^,]* ) ,?" -ne '' -replace "'"
    $fieldValues -join '|'
  }
} | Set-Content -Encoding Utf8 $outPath

无论哪种解决方案,输出文件最终都会包含以下内容(注意缺少' 字符。):

123|abc,def,ghf|ajajaj|1
345|abdf|abc,def,ghi|2
556|abdf|def|3
999|a,b,d|d,e,f|4

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-18
    • 2016-09-23
    • 1970-01-01
    • 1970-01-01
    • 2020-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多