У меня очень большой текстовый файл, в котором отсутствуют некоторые записи. Логика постоянна, поскольку в первой строке каждого «раздела» есть правильные записи, в каждой строке после этой начальной строки эти записи отсутствуют. Я пытаюсь обновить каждую строку, в которой отсутствуют эти записи, информацией из исходной строки, пока не будет найдена новая «начальная информационная строка». После этого я продолжаю работу с новыми найденными данными.
Я создал решение в bash с помощью sed, но этот процесс очень, очень медленный и занимает несколько часов. Я предполагаю, что причина задержки в том, что я читаю построчно, обрабатываю их в bash и записываю в новый файл. Я предполагаю, что сценарий sed с переменными и самим файлом (-f) может значительно ускорить процесс. Я не эксперт в этом расширенном использовании sed. Я также открыт для других предложений или инструментов — при условии, что их можно вызвать из сценария bash, поскольку это часть автоматизации.
Пример входного файла:
{"Initial line with more information like headers, unimportant, really only one line"
"Alpha","OldTheme","Some more text"]
"","","Another rest text"]
"","","Yet another text"]
"Yadda","NewTheme","Crazy Text"]
"","","More crazy text"]
Ожидаемый результат:
"Alpha","OldTheme","Some more text"]
"Alpha","OldTheme","Another rest text"]
"Alpha","OldTheme","Yet another text"]
"Yadda","NewTheme","Crazy Text"]
"Yadda","NewTheme","More crazy text"]
А вот мой рабочий (но очень медленный) bash-скрипт:
#!/bin/bash
first=0
cat inputfile | \
while read line; do
if [ ${first} -eq 0 ]; then
first=1; continue
fi
partline=$(echo "${line}" | grep -o '","\(.*\)')
newinitial=$(echo "${line}" | sed 's/",".*//; s/^"//')
if [ ! -z "${newinitial}" ]; then
initial=${newinitial}
fi
newtheme=$(echo "${partline}" | sed 's/^","//; s/",".*//')
if [ ! -z "${newtheme}" ]; then
theme=${newtheme}
fi
restline=$(echo ${partline} | sed 's/^","//' | grep -o '","\(.*\)')
echo "\"${initial}\",\"${theme}${restline}"
done >outputfile
Пояснение: входные данные НЕ являются правильно сформированным CSV, первая строка действительно начинается с {
, а каждая строка заканчивается ]
, поэтому любая библиотека, использующая в качестве входных данных чистый стиль CSV, не будет работать. Я обновил входные данные, извините за путаницу.
Это действительно там. Исходные данные представляют собой что-то вроде json. Но, к сожалению, он некорректен, поэтому идея просто сохранить его в базе данных и обновлять там не сработала.
Вам следует скопировать/вставить свой bash-скрипт в shellcheck.net , как указано в теге bash, и исправить проблемы, о которых он вам сообщит.
Это действительно хакерство, но и форматы ввода и вывода тоже... Используйте с One True Awk или GNU awk 5.3+:
awk --csv -v OFS='","' \
'NR>1 {for (i=1;i<NF;i++) {a[i]=$i=$i?$i:a[i]; gsub(/"/,"\"\"",$i)} print "\"" $0}'
"Alpha","OldTheme","Some more text"]
"Alpha","OldTheme","Another rest text"]
"Alpha","OldTheme","Yet another text"]
"Yadda","NewTheme","Crazy Text"]
"Yadda","NewTheme","More crazy text"]
Вау, ваше решение очень быстрое. Но, как объяснено в этом комментарии , необходимо изменить только первые два поля, остальные оставить в покое. Есть ли способ сократить ваш сценарий awk, чтобы он учитывал только первые два поля? Общий процесс создания огромного файла занял 10 секунд против 2 часов для моего решения или 30 минут для этого решения
@StefanM Конечно, просто измените i<NF
на i<=2
. Также обратите внимание, что часть gsub(/"/,"\"\"",$i)
заботится об удвоении любых кавычек, появляющихся внутри значений (в соответствии с кодировкой CSV). Если в ваших данных этого не происходит, вы можете еще больше ускорить процесс, заменив {a[i]=$i=$i?$i:a[i]; gsub(/"/,"\"\"",$i)}
просто на a[i]=$i=$i?$i:a[i];
.
Это работает очень хорошо (с gawk 5.3.0), и я принял ваш ответ. Не могли бы вы рассказать подробнее, как работает эта часть a[i]=$i=$i?$i:a[i];
?
a
— массив, хранящий предыдущие значения, i
номер поля из итерации (по условию i<=2
только до 2) и $i
значение этого поля в текущей записи. Итак, прочитайте $i ? $i : a[i]
как «если значение поля i не пусто (на самом деле: имеет значение true), верните это значение, в противном случае верните значение массива в позиции i». Поскольку это оценивается как желаемое значение для данного поля, мы присваиваем его этому полю с помощью $i = …
, а поскольку само присвоение оценивается как присвоенное значение, мы можем использовать это выражение, чтобы также присвоить его с помощью a[i] = …
элементу массива для будущих записей. .
Это может сработать для вас (GNU sed):
sed -E '1d;N;s/^|\n/&\n/g
:a;s/\n("[^"]*",?)(.*\n.*)\n"",?/\1\n\2\1\n/;ta
/""/s/\n("[^"]*",?)(.*\n.*)\n("[^"]*",?)/\1\n\2\3\n/;ta
s/\n(.*)\n/\1/;P;D' file
Удалите первую строку.
Добавьте следующую строку и сформируйте двухстрочное окно по всему файлу.
Добавьте несколько новых строк, чтобы отслеживать, какое поле было сопоставлено.
Сформируйте цикл, сопоставляя поля первой строки с полями второй, заменяя пустые поля во второй соответствующими полями в первой строке, а также переходя к следующему полю.
Если была произведена замена, повторите цикл.
Если во второй строке все еще существуют несовпадающие поля, переместите следующее поле и повторите.
После того, как все поля будут заменены/не заменены, удалите каркас, распечатайте и удалите первую строку и повторите попытку.
Чтобы изменить только первые два пустых поля, используйте:
sed -E '1d;N;s/^((("[^"]*",){2}).*\n)"","",/\1\2/;P;D' file
Спасибо за этот умный способ, особенно за попытку заполнить все поля. К сожалению, следует учитывать только первые два поля, как я показал в своем примере файла. Часть «Еще немного текста» содержит еще несколько записей, и я пытался сделать небольшой пример, чтобы сузить проблему. Есть ли способ сохранить логику, но применить ее только к первым двум полям и просто скопировать остальную часть строки в том виде, в каком она была, даже если произошла замена?
Конечно, просто удалите третью строку. Я вставил только для полноты картины. При этом будут скопированы первые два пустых поля, если третье поле не пусто.
См. «Редактирование», чтобы точно скопировать только первые два пустых поля.
Я создал решение в bash с помощью sed, но процесс это очень, очень медленно и занимает несколько часов. Я думаю, причина в задержка связана с тем, что я читаю построчно, обрабатываю их в bash и запишите их в новый файл. Я предполагаю, что сценарий sed с переменные и сам файл (-f) могут ускорить процесс резко.
Многие способы чтения данных в оболочку являются дорогостоящими, особенно при чтении со стандартного ввода. Оболочка должна избегать чтения большего количества данных, чем предполагалось, что часто означает чтение по одному байту за раз. Ваш сценарий, вероятно, сильно пострадает от этого.
Сценарий также запускает множество дополнительных процессов для выполнения своей работы, по несколько в строке — много grep
, sed
и echo
. Запускать процессы дорого. Уже приняв удар по чтению данных, было бы лучше полностью выполнить обработку строки в оболочке. Оно на это способно.
Но еще лучше было бы выполнить всю работу за один запуск программы обработки текста, например sed
или awk
. Последнее гораздо читабельнее, но вы специально сказали sed
, а на awk
у вас уже есть ответ. Один из способов сделать это с помощью sed
:
sed -n -E '1n; s/^"","",//; t1; b2; :1; H; g; s/\n//; :2; p; s/^(([^"]*"){4},).*/\1/; h' \
< input \
> output
Вы, конечно, можете это написать.
Объяснение:
Выражение sed
представляет собой последовательность команд, разделенных точкой с запятой (;
). Я добавил пробел после каждой точки с запятой, чтобы было легче читать; это пространство не имеет значения для sed
. Команды:
1n
— для строки номер 1 просто прочитайте следующую строку. Если действует параметр командной строки -n
, первая строка полностью пропускается.
s/^"","",//
- удалить текст "","",
с начала строки (заменить его пустой строкой), если он присутствует
t1
- если замена действительно была выполнена, перейдите к метке 1
, пропуская команды между здесь и там. Это то, что происходит со строками с «отсутствующими» записями.
b2
- безоговорочный переход к метке 2, пропуская команды между здесь и там. Вот что происходит с новыми «начальными» строками.
:1
- ярлык 1
H
— добавьте новую строку и содержимое пространства шаблона в пространство Hold (см. также ниже). Первые пустые поля пространства шаблонов были просто удалены, поэтому копируется только хвост.
g
— получить содержимое пространства удержания, заменив им содержимое пространства шаблона
s/\n//
— удалить новую строку из пространства шаблонов. Это было введено предыдущей командой H
.
:2
- этикетка 2
. Когда управление достигает этой точки, содержимое пространства шаблонов выглядит как начальная строка, независимо от того, началось оно таким образом или нет.
p
— распечатать содержимое пространства шаблона.
s/^(([^"]*"){4},).*/\1/
— обрезать строку после второй строки в кавычках и ее завершающей запятой. Осталась та часть, которую мы хотим сохранить для использования в последующих неначальных строках.
Каждый ([^"]*")
соответствует и группирует ноль или более символов, отличных от "
, за которыми следует ровно один "
. {4}
требует, чтобы эта группа была сопоставлена ровно четыре раза. ,
соответствует буквальной запятой. Включаем все это в группы ()
и фиксируем это (это группа 1), а следующий .*
соответствует всему остальному до конца строки. Замена \1
— это обратная ссылка, представляющая содержимое группы 1. Поскольку шаблон соответствует всей строке, группа 1 — это все, что осталось после замены.
h
— скопировать содержимое пространства шаблона в пространство удержания
При действующей опции командной строки -n
автоматическая печать пространства шаблона в конце цикла не производится.
Обратите внимание: если вы хотите сохранить первую строку, а не удалять ее, вы можете очень легко изменить приведенное выше, чтобы сделать это. Просто добавьте команду d
в конце и запустите без опции -n
.
Большое спасибо, это отличное объяснение того, как вы использовали sed, и отличный пример для меток. Хотя это соответствует моему запросу, я не уточнил, что касается первых двух полей. Мой пример сценария анализирует каждое из двух полей отдельно, тогда как ваш пример sed проверяет оба поля одновременно. Мне жаль, что мое объяснение и мои примеры данных не были конкретными в этом пункте. Я надеялся, что пример сценария прояснит, что я имел в виду.
Пожалуйста, не думайте, что мы сможем понять, что вы хотите сделать, прочитав код, и не думайте, что мы найдем информацию, разбросанную по комментариям. Пожалуйста, отредактируйте свой вопрос, включив в него всю соответствующую информацию, и если вы хотите, чтобы третье поле обрабатывалось иначе, чем первые два, используйте образец ввода/вывода, который показывает это поведение, например. строка, в которой третье поле пусто при вводе и остается пустым при выводе.
Использование GNU awk для FPAT
и многосимвольных RS
и при условии, что у вас нет строк ]\n
внутри каких-либо полей:
$ cat ./tst.sh
#!/usr/bin/env bash
awk '
NR == 1 {
RS = ORS = "]" RS
OFS = ","
FPAT = "([^" OFS "]*)|(\"([^\"]|\"\")*\")"
next
}
{
for ( i=1; i<=NF; i++ ) {
$i = ( $i ~ /^("")?$/ ? prev[i] : $i )
prev[i] = $i
}
print
}
' "${@:--}"
$ ./tst.sh file
"Alpha","OldTheme","Some more text"]
"Alpha","OldTheme","Another rest text"]
"Alpha","OldTheme","Yet another text"]
"Yadda","NewTheme","Crazy Text"]
"Yadda","NewTheme","More crazy text"]
Если вы хотите, чтобы манипулировали только первыми двумя полями, просто измените i<=NF
на i<=2
или i<NF
, в зависимости от того, что вы предпочитаете.
См. Какой самый надежный способ эффективного анализа CSV с помощью awk? для получения дополнительной информации о анализе CSV-файлов с помощью awk.
Это { во входных данных действительно присутствует или это опечатка?