Замените разделитель запятой в столбце csv-файла и обрабатывайте поля одинарными кавычками вокруг значения - PullRequest
1 голос
/ 31 января 2020

Система создает 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

Результат I хотите использовать powershell ...

Запятые, которые не являются частью данных - то есть те запятые, которые разделяют поля, заменяются указанным разделителем (в случае ниже pipe-star). Те запятые, которые находятся между парой одинарных кавычек, остаются запятыми.

Результат

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

Я хотел бы сделать это как power-shell или c# net, если это возможно используя выражение reg, однако я не знаю, как это сделать.

Ответы [ 2 ]

2 голосов
/ 31 января 2020

Хотя я думаю, что это создаст файл 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'


Обновление

As mklement0 указал приведенный выше код делает работу, но за счет создания обновленных данных в виде массива в памяти полностью до записи в выходной файл.
Если это проблема (файл тоже большой, чтобы вместить доступную память), вы также можете изменить код для чтения / замены строки из оригинала и сразу записать эту строку в выходной файл.

Этот следующий подход вряд ли будет использовать какую-либо память, но, конечно, за счет выполнения гораздо большего количества действий записи на диск ..

# 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

Regex детали:

,                 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)
)
1 голос
/ 31 января 2020

Полезный ответ Тео является кратким и эффективным.

Позвольте мне дополнить следующее решение, которое:

  • показывает как для анализа каждой строки 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, оператор разделения строк на основе регулярных выражений используется для разделения строк на поля.
* Встроенный параметр (?x) используется для того, чтобы сделать регулярное выражение более читабельным, как объяснено в этом ответе .

Как отмечается в комментариях к коду, приведенное выше решение по-прежнему загружает весь файл в память .

Использование трубопровода необходимо, чтобы избежать этого , что значительно замедляет решение , однако:

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

При любом решении выходной файл содержит следующее (обратите внимание на отсутствие ' c hars.):

123|abc,def,ghf|ajajaj|1
345|abdf|abc,def,ghi|2
556|abdf|def|3
999|a,b,d|d,e,f|4
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...