Вопрос или проблема
Мои начальные данные файла:
"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"
Я понимаю этот вопрос как:
- Как я могу заменить все вхождения разделителя, такого как
|
, когда он встречается внутри кавычек… - заменив на временный, произвольный, и безопасный для вывода символ (например,
@@@
)… - чтобы затем обработать таблицу и не беспокоиться о разделителях внутри кавычек…
- а затем заменить временный символ обратно на разделитель?
Если вы на самом деле не нуждаетесь в @@@
как во временном символе, это можно сделать с помощью 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='|'
.
Процесс включает несколько этапов:
- Предварительная замена внутри кавычек: Заменяем разделители на
@@@
внутри двойных кавычек. - Сортировка данных: Отсортировать строки или столбцы, если требуется.
- Обратная замена: Вернуть замененные символы внутренней обработки в исходный вид.
Применение
Для решения задачи с использованием инструментов командной строки 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.