Справка по скриптам оболочки Unix — как заменить разделитель файла, если он существует внутри данных в двойных кавычках в CSV файле с разделителями?

Вопрос или проблема

Мои начальные данные файла:

"Per Sara Porras.|, LLC"|column2_data|column3_data
column1_data|"column2|data"|"column3|data"

Требуемый вывод:

"Per Sara Porras.@@@, LLC"|column2_data|column3_data
column1_data|"column2@@@data"|"column3@@@data"

Мне это нужно, чтобы я мог сортировать данные, используя IFS=’|’, в данный момент данные разделяются неправильно из-за наличия | внутри двойных кавычек. После того как данные будут отсортированы, мне потребуется заменить @@@ внутри двойных кавычек обратно на |.

После сортировки данных в алфавитном порядке, мой файл должен выглядеть так-

column2_data|column3_data|"Per Sara Porras.|, LLC"
column1_data|"column2|data"|"column3|data"

Пробовал команды awk, sed и другие, не сработало :(.

Miller (mlr) — это инструмент для работы со структурированными данными, такими как CSV (и в других форматах). Он понимает правила экранирования CSV и без проблем работает с данными, представленными в вопросе, которые являются корректно отформатированным CSV (хотя и без заголовков).

Использование Miller для прохода по каждому полю без заголовков в CSV данных, разделённых символом “|”, заменяя каждый символ “|” на три символа “@”:

$ mlr --csv -N --fs pipe put 'for (k,v in $*) { $[k] = gssub(v,"|","@@@") }' file
Per Sara Porras.@@@, LLC|column2_data|column3_data
column1_data|column2@@@data|column3@@@data

Я выбрал использование gssub() для замены. Это работает так же как gsub(), но не интерпретирует второй аргумент (шаблон) как регулярное выражение, а как текстовую строку.

Обратите внимание, что Miller не будет обрамлять поля в выводе, которые не нуждаются в обрамлении.

Это можно сделать и поэтапным обработчиком, заменяя символы-разделители на более часто используемую запятую, использовать sed для замены каждого оставшегося символа “|” на @@@ и затем вернуть изначальные разделители:

$ mlr --csv -N --ifs pipe cat file | sed 's/|/@@@/g' | mlr --csv -N --ofs pipe cat
Per Sara Porras.@@@, LLC|column2_data|column3_data
column1_data|column2@@@data|column3@@@data

… но если всё, что вам нужно, это отсортировать поля в каждой записи, я бы использовал одну команду в Miller:

$ mlr --csv -N --fs pipe put 'sorted = sort(get_values($*), "c"); for (k,v in sorted) { $[k] = v }' file
column2_data|column3_data|"Per Sara Porras.|, LLC"
column1_data|"column2|data"|"column3|data"

Это сортирует значения каждой записи по очереди (без учёта регистра; см. документы по функции sort()), переназначая отсортированные значения в поля записи перед продолжением с следующей записью.

В чуть более компактной форме:

$ mlr --csv -N --fs pipe put 'for (k,v in sort(get_values($*), "c")) { $[k] = v }' file
column2_data|column3_data|"Per Sara Porras.|, LLC"
column1_data|"column2|data"|"column3|data"

Я понимаю этот вопрос как:

  1. Как я могу заменить все вхождения разделителя, такого как |, когда он встречается внутри кавычек…
  2. заменив на временный, произвольный, и безопасный для вывода символ (например, @@@)…
  3. чтобы затем обработать таблицу и не беспокоиться о разделителях внутри кавычек…
  4. а затем заменить временный символ обратно на разделитель?

Если вы на самом деле не нуждаетесь в @@@ как во временном символе, это можно сделать с помощью csvquote:

csvquote -d'|' input.txt | (your code here) | csvquote -d'|' -u
  • Первая команда csvquote кодирует входные данные, используя непечатаемый символ разделитель полей (US) как временный символ.
  • Вторая команда (с флагом -u) возвращает | на свои места.
  • Аргумент -d'|' устанавливает разделитель как | вместо ,.
Кодирование с помощью временного символа

Замените цитированные случаи | символом US и сохраните результат в input_encoded.txt:

csvquote -d'|' input.txt > input_encoded.txt

Символ US является 0x1F в 16-ричной системе, или 037 в восьмеричной системе. Мы можем видеть, что делает csvquote, используя od -c:

$ od -c input.txt     # оригинал
0000000   "   P   e   r       S   a   r   a       P   o   r   r   a   s
0000020   .   |   ,       L   L   C   "   |   c   o   l   u   m   n   2
0000040   _   d   a   t   a   |   c   o   l   u   m   n   3   _   d   a
0000060   t   a  \n   c   o   l   u   m   n   1   _   d   a   t   a   |
0000100   "   c   o   l   u   m   n   2   |   d   a   t   a   "   |   "
0000120   c   o   l   u   m   n   3   |   d   a   t   a   "  \n
0000136

$ csvquote -d'|' input.txt | od -c  # закодированное
0000000   "   P   e   r       S   a   r   a       P   o   r   r   a   s
0000020   . 037   ,       L   L   C   "   |   c   o   l   u   m   n   2
0000040   _   d   a   t   a   |   c   o   l   u   m   n   3   _   d   a
0000060   t   a  \n   c   o   l   u   м   n   1   _   d   a   t   a   |
0000100   "   c   o   l   u   m   n   2 037   d   a   t   a   "   |   "
0000120   c   o   l   u   m   n   3 037   d   a   t   a   "  \n
0000136

Кстати, процитированные символы записи (по умолчанию новые строки) заменяются на непечатаемый символ Record Separator, который является 0x1E в 16-ричной и 036 в восьмеричной системах.

Декодирование

Замените кодированные символы US обратно на |:

csvquote -d'|' -u input_encoded.txt

Как мы видим, файл восстановлен:

$ csvquote -d'|' -u input_encoded.txt | od -c
0000000   "   P   e   r       S   a   r   a       P   o   r   r   a   s
0000020   .   |   ,       L   L   C   "   |   c   o   l   u   m   n   2
0000040   _   d   a   t   a   |   c   o   l   u   м   n   3   _   d   a
0000060   t   a  \n   c   o   l   u   m   n   1   _   d   a   т   a   |
0000100   "   c   o   л   u   м   m   n   2   |   d   a   t   а "   |   "
0000120   c   o   l   u   m   n   3   |   d   a   t   a   "  \n
0000136
Настройка символов-заполнителей

Эти символы (US и RS) жестко закодированы, но если вам действительно нужен другой символ-заполнитель, вы можете отредактировать csvquote.c в исходном коде, изменить эти строки, и скомпилировать самому:

#define NON_PRINTING_FIELD_SEPARATOR 0x1F
#define NON_PRINTING_RECORD_SEPARATOR 0x1E

Использование любого awk для замены символа или строки в пределах фигурных полей:

$ awk 'BEGIN{FS=OFS="\""} {for (i=2; i<=NF; i+=2) gsub(/\|/,"@@@",$i)} 1' file
"Per Sara Porras.@@@, LLC"|column2_data|column3_data
column1_data|"column2@@@data"|"column3@@@data"

Вот как сделать всю часть сортировки с использованием украшения-сортировки-раскраски с любым POSIX-совместимым ‘awk’ и ‘sort’:

$ cat tst.sh
#!/usr/bin/env bash

sep='|'
repl="@@@"

awk -v sep="$sep" -v repl="$repl" '
    BEGIN { FS=OFS="\"" }
    {
        for ( i=2; i<=NF; i+=2 ) {
            gsub("["sep"]", repl, $i)
        }
        n = split($0, f, sep)
        for ( i=1; i<=n; i++ ) {
            gsub(/^"|"$/, "", f[i])
            print NR sep i sep f[i]
        }
    }
' "${@:--}" |
sort -t"$sep" -k1,1n -k3,3f -k2,2n |
awk -v sep="$sep" -v repl="$repl" '
    BEGIN { FS=sep }
    $1 != prev {
        if ( NR > 1 ) { print rec }
        rec = ""
        prev = $1
    }
    { sub("([^"sep"]["sep"]){2}", "") }
    gsub(repl, sep) { $0 = "\"" $0 "\"" }
    { rec = (rec == "" ? "" : rec sep) $0 }
    END { print rec }
'

$ ./tst.sh file
column2_data|column3_data|"Per Sara Porras.|, LLC"
column1_data|"колонка2|данные"|"колонка3|данные"

Вышеупомянутое предполагает, что ваш разделитель полей (|) является одним символом, который может быть закрыт в конструкцию с квадратными скобками ([|]), чтобы быть буквальным, что ваша строка замены (@@@) не содержит метасимволов регулярных выражений, и что никакой из них не содержит метасимвола обратной ссылки &.

Если ваш ввод может содержать @@@, то вы можете использовать инструменты GNU, чтобы позволить \0 терминаторы записей для awk и sort, чтобы у вас могли быть многострочные записи и заменять каждый | на \n вместо @@@, или просто сделать это все в GNU awk, так как он имеет свою собственную функциональность обработки и сортировки CSV, например, с GNU awk:

$ cat tst.sh
#!/usr/bin/env bash

awk -v FPAT='[^|]*|("([^"]|"")*")' -v OFS='|' '
    BEGIN {
        IGNORECASE = 1
        PROCINFO["sorted_in"] = "@val_str_asc"
    }
    {
        delete f
        for ( i=1; i<=NF; i++ ) {
            f[i] = gensub(/^"|"$/, "", "g", $i)
        }
        rec = ""
        for ( i in f ) {
            q = ( f[i] ~ /\|/ ? "\"" : "" )
            rec = (rec == "" ? "" : rec OFS) q f[i] q
        }
        print rec
    }
' "${@:--}"

$ ./tst.sh file
column2_data|column3_data|"Per Sara Porras.|, LLC"
column1_data|"колонка2|данные"|"колонка3|данные"

Для получения дополнительной информации о работе с CSV файлами с использованием awk, см. whats-the-most-robust-way-to-efficiently-parse-csv-using-awk.

Можно сделать это с помощью awk, но это не очень красиво*. Вы можете установить флаг при встрече " и затем отменить его с закрывающим " и заменить | только если флаг установлен. Конечно, это также предполагает, что у вас не может быть " внутри ", так что ничего такого, как "some \"field\" like this". Как бы то ни было, это делает то, что вы запросили на ваших примерных данных:

{
  line="";
  c=""
  for(i=1; i<=length($0); i++){
    c=substr($0,i,1);
    if(c == "\""){
      a = a ? 0 : 1
    }
    else if(c == "|" && a == 1){
      c="@@@"
    }
    line=line ? line""c : c
  }
  print line
}  file

Сохраните это как foo.awk и запустите его так:

awk -f foo.awk < input.file

Или, вот то же самое в одну строку:

awk '{
  line="";
  c=""
  for(i=1; i<=length($0); i++){
    c=substr($0,i,1);
    if(c == "\""){
      a = a ? 0 : 1
    }
    else if(c == "|" && a == 1){
      c="@@@"
    }
    line=line ? line""c : c
  }
  print line
}'  file

Вывод при запуске на вашем примере:

$ awk '{ line=""; c=""; for(i=1; i<=length($0); i++){ c=substr($0,i,1); if(c == "\""){ a = a ? 0 : 1 } else if(c == "|" && a == 1){ c="@@@" } line=line ? line""c : c } print line}'  file
"Per Sara Porras.@@@, LLC"|column2_data|column3_data
column1_data|"column2@@@data"|"column3@@@data"

Но честно говоря, это очень плохая идея. Лучше использовать инструмент, который понимает формат CSV вместо.


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

Ответ или решение

Вопрос, связанный с заменой разделителя внутри данных, заключённых в двойные кавычки в CSV-файле, особенно сложен, если вы используете язык скриптов Unix для выполнения этой задачи. Давайте разберёмся, как это можно сделать, рассматривая теорию, пример и практическое применение.

Теория

CSV (Comma-Separated Values) — это формат файлов, часто используемый для хранения табличных данных в виде текста. Внутри такого файла данные в строках разделяются запятыми или другими символами, но часто строки включаются в кавычки для сохранения целостности информации, особенно если данные внутри содержат символы-разделители.

Когда дело доходит до Unix-систем, инструменты командной строки, такие как awk, sed, или даже более новые утилиты вроде miller, часто используются для анализа и обработки CSV-файлов. Однако, стандартным инструментам сложно обрабатывать ситуации, когда разделитель встречается внутри заквотированных строк, так как они разработаны для работы с более простыми, неструктурированными текстовыми данными.

Пример

Рассмотрим CSV-данные из вопроса:

"Per Sara Porras.|, LLC"|column2_data|column3_data
column1_data|"column2|data"|"column3|data"

Задача состоит в том, чтобы заменить все символы |, находящиеся внутри кавычек, на @@@. Это необходимо для корректной работы обработки данных с использованием IFS='|'.

Процесс включает несколько этапов:

  1. Предварительная замена внутри кавычек: Заменяем разделители на @@@ внутри двойных кавычек.
  2. Сортировка данных: Отсортировать строки или столбцы, если требуется.
  3. Обратная замена: Вернуть замененные символы внутренней обработки в исходный вид.

Применение

Для решения задачи с использованием инструментов командной строки Unix, можно использовать несколько подходов:

Использование awk

awk является мощным текстовым процессором, который, среди прочего, может использоваться для обработки CSV-файлов.

awk 'BEGIN{FS=OFS="\""} {for (i=2; i<=NF; i+=2) gsub(/\|/,"@@@",$i)} 1' file

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

Использование утилиты miller

miller — это современный инструмент для обработки табличных данных, который поддерживает работу с CSV и другими форматами. Он умеет обрабатывать кавычки в CSV, что делает его более подходящим для подобных задач.

mlr --csv -N --fs pipe put 'for (k,v in $*) { $[k] = gssub(v,"|","@@@") }' file

Этот скрипт проходит по всем полям и заменяет | на @@@ в пределах строк с учётом кавычек.

Использование sed

Если вы всё-таки предпочитаете sed, необходимо комбинировать его с другими инструментами для корректной обработки кавычек и разделителей. Однако sed менее подойдёт без сторонней помощи ввиду своей ограниченной работы с CSV структурами.

Комбинирование с csvquote

Утилита csvquote может быть полезна для преобразования содержимого файла, временно заменив | внутри кавычек на нечто другое (например, управляющие символы), а затем восстановить исходный вид данных:

csvquote -d'|' input.txt | (your processing code) | csvquote -d'|' -u

Итак, при помощи awk и miller можно достичь необходимого результата. Использование этих инструментов с учётом специфики их обработки позволяет справиться со сложной структурой CSV и обеспечить качественную обработку данных. Это иллюстрирует, как мощные средства Unix-линуксовых систем могут использоваться для сложных задач текстовой обработки, доступных каждому специалисту в IT.

Оцените материал
Добавить комментарий

Капча загружается...