Обработка больших записей/абзацев

Обработка больших записей/абзацев

У меня есть большой текстовый файл (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. 
        

Используя это, вы можете сделать:

  1. Удалить все записи длиннее 100 000персонажи(обратите внимание, что это может не совпадать с байтами, в зависимости от кодировки вашего файла):

     perl -00 -ne 'print unless length()>100000' file
    
  2. Обрежьте все записи, длина которых превышает 100000 символов, удалив все символы после первых 100000:

     perl -00 -lne 'print substr($_,0,100000)' file
    
  3. Удалить строки, начинающиеся с 149:

     perl -00 -pe 's/(^|\n)149\s+[^\n]+//g;' file
    
  4. Удалить строки, начинающиеся с, 149но только если эта запись длиннее 100000 символов:

     perl -00 -pe 's/(^|\n)149\s+[^\n]+//g if length()>100000; ' file
    
  5. Если длина записи превышает 100000 символов, удалите строки, начинающиеся с 149, пока длина записи не станет меньше 100000 символов или не останется строк, содержащих 149:

     perl -00 -pe 'while(length()>100000 && /(^|\n)149\s/){s/(^|\n)149\s+[^\n]+//}' file
    
  6. Если длина записи превышает 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
    
  7. Наконец, как и выше, но удаляйте целые строки, а не только символы, пока не получите нужный размер, чтобы не было обрезанных записей:

     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заменяет шаблон указанной строкой и возвращает количество выполненных замен/замен.

Это было вдохновленоэтот ответ.

Связанный контент