Извлечение записей фиксированной ширины без разделителя из одной строки

8

Мне нужно извлечь строки текста из одного файла, содержащего одну очень длинную строку текста без разделителей. Используя пример строки ниже, это следующие известные факты:

A1XXXXXXXXXX ??????? ??????? B1XXXX ??????? A1XXXXXXXXXX ??????? C1XXXXXXX

1.  It contains 38 fixed width record types 
2.  The record marker is a 7 alphanumeric character followed by, for example, A1’.
3.  Each record type has varying widths, for example, A1 record type will have 10 characters following it, if B1 then 4, and if C1 then 7.
4.  The record types arent clumped together and can be in any order. As in the example, its A1,B1,A1,C1
5.  The example above has 4 records and each record type needs to go to separate files. In this case 38 of them.

A1XXXXXXXXXX ???????

B1XXXX ???????

A1XXXXXXXXXX ???????

C1XXXXXXX ???????

6.  The record identifier, e.g. ????????A1, can appear in the body of the record so cannot use grep. 
7.  With the last point in mind, I was proposing 3 solutions but not sure on how to script this and of course would greatly appreciate some help. 
a. Traverse through the file from the beginning and sequentially strip out the record to the appropriate output file. For example, strip out first record type A1 to A1file which I know is 10 characters long then re-interrogate the file which will then have B1 which I know is 4 chars long, strip this out to B1file etc.. <<< this seems painful >>
b. Traverse through the file and append some obscure character to each record marker within the same file. Much like above but not strip out. I understand it still will use the same logic but seems more elegant
c. I did think of simply using the proposed grep -oE solution but then re-interrogate the output files to see if any of the 38 record markers exist anywhere other than at the beginning. But this might not always work.
зазубрины
источник
Рефакторинг кода Perl для учета ваших обновлений. Пожалуйста, посмотрите, поможет ли это.
Джозеф Р.
Спасибо, Джозеф. Я не знаю Perl, но хотел пояснить, что файл содержит только 1 строку текста, то есть никаких возвратов каретки или разрывов строк. Просто хотел прояснить это, потому что я вижу, что в ваших комментариях вы подразумеваете, что файл содержит более 1 строки, если, как я уже сказал, я неправильно понял это. Большое спасибо.
зазубрины
Это не должно иметь значения. Код Perl будет работать одинаково, если он все в одной строке или если их несколько, если каждая строка содержит целое число правильно сформированных записей.
Джозеф Р.
Большое спасибо, Джозеф. Это сработало. Проверяется, находится ли маркер записи в теле записи, и обратная ссылка преодолевает это. Кто-нибудь может предложить Unix эквивалент, пожалуйста?
зазубрины
Пожалуйста, посмотрите на мой обновленный ответ.
Джозеф Р.

Ответы:

5

Как насчет

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt

Это печатает каждую запись каждого типа записи в отдельной строке. Для того, чтобы перенаправить grepвывод 3 файлов с именами A1, B1, C1соответственно,

grep -oE 'A1.{10}|B1.{4}|C1.{7}' input.txt| 
awk -v OFS= -v FS= '{f=$1$2; $1=$2=""; print>f}'
Iruvar
источник
Спасибо тебе большое за это. Не могли бы вы объяснить эти различные компоненты сценария и используемые переключатели, чтобы я мог протестировать и расширить, пожалуйста. Также, как мне добавить шаблон из 9 перед ним (который в действительности будет буквенно-цифровым символом длиной 7 символов). Большое спасибо.
зазубрины
Говорили слишком рано ... Я должен был также добавить одну важную информацию, которая заключалась в том, что pattern.recordmarker мог появиться в остальной части записи, поэтому было рекомендовано, чтобы мы вырезали запись за раз в файл и повторно запросили файл, который, вероятно, значит я не могу использовать grep.
зазубрины
Кроме того, у меня есть 2 возможных решения. - пройти через файл, пометить непонятным символом для обозначения начала действительной записи. Переместите символы X в зависимости от типа записи и используйте тот же непонятный символ для обозначения следующей записи. Однако настороженно относится к любым проблемам с буфером. Поэтому ожидаем новый выход опрашивать глядя , как это «\\ 9999999A1XXXXXXXXXX \\ 9999999B1XXXX \\ 9999999A1XXXXXXXXXX \\ 9999999C1XXXXXXX??» - использование текущего золе , но затем искать внутри каждого выходного файла , если другие модели появляются другие , чем в начале
зазубрины
@jags, возможно, вы захотите обновить исходный вопрос действительно репрезентативными образцами данных, все это немного сбивает с толку
iruvar
Спасибо 1_CR, я повторно отправил вопрос. Спасибо за вашу помощь. Наиболее ценится.
Зубцы
4

Вот возможное решение с использованием FPAT gawk

BEGIN { 
    FPAT="A1.{10}|B1.{4}|C1.{7}" #define field contents
} 
{
    for(i=1;i<=NF;i++) 
        print $i >> substr($i,0,2) #print the field to file A1,B1,etc
}

Как однострочник:

gawk 'BEGIN{FPAT="A1.{10}|B1.{4}|C1.{7}"} {for(i=1;i<=NF;i++)print $i >> substr($i,0,2)}' < datafile
rzymek
источник
Обратите внимание, что FPATтребуется gawk версии 4. См .: linuxjournaldigital.com/linuxjournal/201109#pg98
Хакон Хагланд,
4

В Perl:

#!/usr/bin/env perl

use strict;
use warnings;
use re qw(eval);

my %field_widths = (
    A1 => 10,
    B1 =>  4,
    C1 =>  7,
    #...(fill this up with the widths of your 38 record types)
);

# Make a regex of record types; sort with longest first as appropriate for
# ... regex alternation:
my $record_type_regex = join '|', sort { length($b) <=> length($a) } keys %field_widths; 

my %records;
my $marker_length=7; #Assuming the marker is 7 characters long
while(<>){
    chomp;
    while( # Parse each line of input
      m!
        (.{$marker_length})          # Match the record marker (save in $1)
        ($record_type_regex)         # Match any record type (save in $2)
        (
         (??{'.'x$field_widths{$2})} # Match a field of correct width
        )                            # Save in $3
       !xg){
        $records{$2}.="$1$2$3\n";
      }
}
for my $file (sort keys %records){
    open my $OUT,'>',$file or die "Failed to open $file for writing: $!\n";
    print $OUT $records{$file};
    close $OUT
}

Вызовите это как:

[user@host]$ ./myscript.pl file_of_data

Код проверен и работает с вашим заданным входом.

Обновить

В ваших комментариях вы запросили «Unix-эквивалент» вышеупомянутого. Я очень сомневаюсь, что такая вещь существует, поскольку выражение Perl, используемое для разбора вашей строки, является крайне нерегулярным выражением, и я сомневаюсь, что регулярные выражения ванили могут анализировать заданный вами формат данных: он слишком похож на известный тип выражения, который может использовать регулярное выражение 't parse (соответствует любому числу a' s, за которым следует такое же количество b's).

В любом случае, самый близкий подход «Unix», который я могу найти, - это обобщение ответа 1_CR . Вы должны отметить, что этот подход специфичен для реализации GNU grepи поэтому не будет работать на большинстве Unices. Подход Perl, напротив, должен работать одинаково на любой платформе, на которой работает Perl. Вот мой предложенный grepподход GNU :

cat <<EOF \
| while read -r record width;do
    grep -oE ".{7}$record.{$width}" input_file\ #replace 7 with marker length
     >> "$record"
done
A1 10
B1 4
# enter your 38 record types
EOF

Обновить

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

open my $IN,'<',$input_file_name or die "Failed to open $input_file: $!\n";
while(<$IN>){ #instead of while(<>)
...

Это предполагает, что вы объявили переменную, которая $input_file_nameбудет содержать имя входного файла.

Что касается добавления временной метки к имени выходного файла, вы можете использовать qx{}синтаксис: между фигурными скобками вы можете поместить любую команду Unix, которую вы хотите, и она будет выполнена, и ее стандартный вывод будет считан вместо qx{}оператора:

open my $OUT,'>',"$file_".qx{date +%Y-%m-%d--%I:%M:%S%P}

qxОператор не ограничивается фигурными скобками, используйте ваш любимый персонаж в качестве разделителя, просто убедитесь , что он не в команде , которую нужно выполнить:

qx<...>
qx(...)    
qx!...!    
qx@...@

и так далее...

В некотором коде Perl вы можете увидеть backticks ( ` `), используемый вместо этой функции, аналогично тому, что делает оболочка. Просто думайте об qxоператоре как об обобщении обратных к любому разделителю.

Кстати, это даст немного различную временную метку каждому файлу (если разница времени их создания будет конечным числом секунд). Если вы не хотите этого, вы можете сделать это в два этапа:

my $tstamp = qx{...};
open my $OUT,'>',"$file_$tstamp" or die...;
Джозеф Р.
источник
Привет снова .... начинаю по-настоящему любить Perl. Просто сделайте пару маленьких кусочков. 1 . Как читать в файле, а не передавать в аргумент командной строки. Попытка, но не в состоянии использовать конфигурацию запуска Eclipse. 2 . Как добавить текст к выходному имени файла $ file. Наиболее ценится.
Зубцы
@jags Добро пожаловать в клуб :). Ответ обновлен. Посмотрите, поможет ли это.
Джозеф Р.
Спасибо, Джозеф. Однако для последнего запроса я хотел добавить, например, дату / время к имени выходного файла. Текущий код выводит файлы A1, B1 и C1. Большое спасибо еще раз.
Зубцы
@ Джагс, я вижу. Посмотрите, поможет ли обновление.
Джозеф Р.
Спасибо, как всегда, Джозеф. Однако я имел в виду добавление к фактическому имени выходного файла, в данном случае это A1, B1, C1, т.е. я хочу добавить дату / метку времени, A1_ <todays_date>, B1_ <todays_date>, C1_ <todays_date>. Большое спасибо.
Зубцы