![Обработка больших записей/абзацев](https://rvso.com/image/178469/%D0%9E%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B8%D1%85%20%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B5%D0%B9%2F%D0%B0%D0%B1%D0%B7%D0%B0%D1%86%D0%B5%D0%B2.png)
У меня есть большой текстовый файл (300 МБ) с записями с \n\n
разделителем. Каждая строка является полем и начинается с числа (тег/имя поля), за которым следует символ TAB и содержимое/значение поля:
110 something from record 1, field 110
149 something else
111 any field could be repeatable
111 any number of times
120 another field
107 something from record 2, field 107
149 fields could be repeatable
149 a lot of times
149 I mean a LOT!
130 another field
107 something from record 3
149 something else
Каждая запись не должна превышать 100 КБ.
Я смог найти некоторые проблемные записи (больше лимита)удаление окончаний строк из этих записей/"абзацев"иполучение его длины:
cat records.txt | awk ' /^$/ { print; } /./ { printf("%s ", $0); } ' | awk '{print length+1}' | sort -rn | grep -P "^\d{6,}$"
Я пытаюсь найти способ обработки этих недействительных записей:
- удаление записей, превышающих лимит.
- удаление всех вхождений определенного известного проблемного тега (149 в примере выше). Приемлемы ли гипотезы о том, что ни одна запись не будет превышать лимит, если все строки, начинающиеся с поля 149, будут удалены.
Вероятно, удаление достаточного количества вхождений определенного поля/тега, чтобы соответствовать лимиту, заслуживает полноценного скрипта. Было бы еще лучше удалить сначала последние.
Это связано с древним библиотечным форматом файлов, который называетсяИСО 2709.
решение1
Если вы просто хотите пропустить проблемные записи:
awk 'BEGIN { ORS=RS="\n\n" } length <= 100*1000' file
Будет напечатана каждая запись, содержащая менее или равно 100 тыс. символов.
Чтобы удалить поля, начинающиеся с определенного положительного целого числа, если запись слишком большая:
awk -v number=149 'BEGIN { ORS=RS="\n\n"; OFS=FS="\n" }
length <= 100*1000 { print; next }
{
# This is a too long record.
# Re-create it without any fields whose first tab-delimited
# sub-field is the number in the variable number.
# Split the record into an array of fields, a.
nf = split($0,a)
# Empty the record.
$0 = ""
# Go through the fields and add back the ones that we
# want to the output record.
for (i = 1; i <= nf; ++i) {
split(a[i],b,"\t")
if (b[1] != number) $(NF+1) = a[i]
}
# Print the output record.
print
}' file
Это печатает короткие записи, как и раньше. Более длинные записи обрезаются от всех полей, первое подполе которых, разделенное табуляцией, является числом number
(указанным в командной строке здесь как 149).
Для больших записей запись пересоздается без полей, которые нам не нужны. Внутренний цикл пересоздает выходную запись, разделяя поля на табуляции и добавляя те, чье первое подполе, разделенное табуляцией, не является number
:
for (i = 1; i <= nf; ++i) {
split(a[i],b,"\t")
if (b[1] != number) $(NF+1) = a[i]
}
Поскольку спецификация POSIX для awk
оставляет неопределенным, что происходит, когда у вас есть многосимвольное значение RS
(большинство реализаций рассматривают его как регулярное выражение), вы можете использовать RS=""; ORS="\n\n"
вместо ORS=RS="\n\n"
при использовании вашей строго соответствующей awk
реализации. Если вы это сделаете, обратите внимание, что несколько пустых строк в данных больше не будут разделять пустые записи.
решение2
Другой awk
подход:
awk -v lim=99999 'BEGIN{RS=""; ORS="\n\n"}\
{while (length()>=lim) {if (!sub(/\n149\t[^\n]*/,"")) break;}} length()<lim' file
Это постепенно удалит строки, начиная с 149
, если длина записи превышает предел, указанный в переменной lim
, заменяя их на "ничего", пока либо предел не будет соблюден, либо дальнейшее сокращение не станет возможным (на что указывает количество фактических замен, равное 0). Затем будут напечатаны только записи, где конечная длина меньше предела.
Недостаток:Он удалит 149
строки, начиная с первой, поэтому, если они представляют собой отдельные элементы непрерывного текста, этот текст станет несколько непонятным.
Примечание:Указание RS=""
вместо явного RS="\n\n"
являетсяпортативныйспособ использования awk
в "режиме абзаца", так как поведение многосимвольного RS
не определено спецификацией POSIX. Однако, если может бытьпустойзаписи в вашем файле, они будут проигнорированы awk
и, следовательно, не появятся в выводе. Если это не то, что вам нужно, вам, возможно, придется использовать явную RS="\n\n"
нотацию — большинство awk
реализаций будут воспринимать ее как регулярное выражение и делать то, что можно было бы «наивно» ожидать.
решение3
\n\n
Всякий раз, когда в качестве разделителя записей вы используете Perl и режим абзаца (из man perlrun
):
-0[octal/hexadecimal]
specifies the input record separator ($/) as an octal or hexadecimal number.
[...]
The special value 00 will cause Perl to slurp files in paragraph mode.
Используя это, вы можете сделать:
Удалить все записи длиннее 100 000персонажи(обратите внимание, что это может не совпадать с байтами, в зависимости от кодировки вашего файла):
perl -00 -ne 'print unless length()>100000' file
Обрежьте все записи, длина которых превышает 100000 символов, удалив все символы после первых 100000:
perl -00 -lne 'print substr($_,0,100000)' file
Удалить строки, начинающиеся с
149
:perl -00 -pe 's/(^|\n)149\s+[^\n]+//g;' file
Удалить строки, начинающиеся с,
149
но только если эта запись длиннее 100000 символов:perl -00 -pe 's/(^|\n)149\s+[^\n]+//g if length()>100000; ' file
Если длина записи превышает 100000 символов, удалите строки, начинающиеся с
149
, пока длина записи не станет меньше 100000 символов или не останется строк, содержащих 149:perl -00 -pe 'while(length()>100000 && /(^|\n)149\s/){s/(^|\n)149\s+[^\n]+//}' file
Если длина записи превышает 100000 символов, удаляйте строки, начинающиеся с
149
, пока длина записи не станет меньше 100000 символов или не останется строк со 149 символами, и если это таквсе ещедлиннее 100000 символов, вывести только первые 100000:perl -00 -lne 'while(length()>100000 && /(^|\n)149\s/){ s/(^|\n)149\s+[^\n]+// } print substr($_,0,100000)' file
Наконец, как и выше, но удаляйте целые строки, а не только символы, пока не получите нужный размер, чтобы не было обрезанных записей:
perl -00 -ne 'while(length()>100000 && /(^|\n)149\s/){ s/(^|\n)149\s+[^\n]+// } map{ $out.="$_\n" if length($out . "\n$_")<=100000 }split(/\n/); print "$out\n"; $out="";' file
решение4
Возможно, можно было бы сделать это более элегантно, но вот решение:
cat records.txt | awk -v RS='' '{if (length>99999) {gsub(/\n149\t[^\n]*\n/,"\n");print $0"\n"} else {print $0"\n"} }'
Я знаю о бесполезном использовании кошки, я считаюболее понятен поток слева направо.
Где 99999 — пороговый размер, а 149 — начало строки (имя поля), которую следует удалить в этом случае.
Я использую нежадный метод, \n149\t[^\n]*\n/
чтобы удалить только то, что будет ^149\t.*$
.
gsub
заменяет шаблон указанной строкой и возвращает количество выполненных замен/замен.
Это было вдохновленоэтот ответ.