Як відобразити зворотну відповідність та виключити рядки "до" та "після"


26

Розглянемо текстовий файл із такими записами:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

З огляду на шаблон (наприклад fff), я хотів би отримати файл вище, щоб отримати у висновку:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

Наприклад, якщо B = 2і A = 1, вихід з шаблоном = fffповинен бути:

aaa
bbb
ccc
hhh
iii

Як я можу це зробити за допомогою grep або інших інструментів командного рядка?


Зауважте, коли я намагаюся:

grep -v 'fff'  -A1 -B2 file.txt

Я не отримую того, що хочу. Я натомість отримую:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii

Відповіді:


9

Дон може бути кращим у більшості випадків, але про всяк випадок, якщо файл дійсно великий, і ви не можете отримати sedобробку такого великого файлу сценарію (що може траплятися приблизно в 5000+ рядків сценарію) , ось це просто sed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Це приклад того, що називається розсувним вікном на вході. Він працює, будуючи буфер рядків з рахунком вперед,$B перш ніж щось намагатися надрукувати.

Насправді, мабуть, я повинен уточнити свій попередній пункт: основний обмежувач продуктивності і для цього рішення, і для дона буде безпосередньо пов'язаний з інтервалом. Це рішення сповільнюватиметься з більшими розмірами інтервалів , тоді як донські сповільнюватимуться з більшими інтервальними частотами . Іншими словами, навіть якщо вхідний файл дуже великий, якщо фактичний інтервал зустрічається ще дуже рідко, то його рішення, ймовірно, має пройти. Однак якщо розмір інтервалу є відносно керованим і, ймовірно, трапляється часто, то це рішення, яке ви повинні вибрати.

Ось ось робочий процес:

  • Якщо $matchвін знайдеться в просторі шаблону, який передує \newline, sedбуде рекурсивно вибирати Dкожну \nлінію ewline, яка їй передує.
    • $matchРаніше я повністю очищав простір шаблону - але, щоб легко впоратися з перекриттям, залишається орієнтир, який працює набагато краще.
    • Я також намагався s/.*\n.*\($match\)/\1/спробувати отримати його за один раз і відхилити цикл, але коли $A/$Bвони великі, Dелегантний цикл виявляється значно швидше.
  • Потім ми вводимо Nрядок вхідного сигналу, який передує \nроздільнику ewline, і намагаємося ще раз Dвибрати /\n.*$match/його, звернувшись до нашого нещодавно використовуваного регулярного виразу w / //.
  • Якщо простір шаблону збігається, $matchтоді це можна зробити лише вгорі лінії $match- всі $Bпопередні лінії були очищені.
    • Тож ми починаємо перебирати далі $A.
    • Кожен запуск цього циклу ми будемо намагатися s///ubstitute для &себе $Aго \nсимволу ewline в просторі картини, і, в разі успіху, tЕст виросте нас - і весь наш $Aвіслюку буфер - зі сценарію повністю запустити скрипт через зверху з наступним рядком введення, якщо такий є.
    • Якщо test не є успішним, ми bповернемося до :tмітки op і повторимо для іншого рядка введення - можливо, запустимо цикл, якщо він $matchвідбудеться під час збору $After.
  • Якщо пройти в $matchпетлю функції, то ми будемо намагатися pРінту в $останній рядок , якщо це, і якщо !не намагатися s///ubstitute для &себе $Bго \newline характер в просторі картини.
    • Ми також tоцінимо це, і якщо це буде успішно, ми перейдемо на :Pярлик rint.
    • Якщо ні, ми повернемось до :tоп і отримаємо інший рядок введення, доданий до буфера.
  • Якщо ми зробимо це для :Pрингту, ми будемо Pринг, а потім Dпіднімемо до першого \newline у ​​просторі шаблону і повторимо сценарій зверху з тим, що залишилося.

І так цього разу, якби ми робили A=2 B=2 match=5; seq 5 | sed...

Простір шаблону для першої ітерації в :Print виглядатиме так:

^1\n2\n3$

Ось як sedзбирає свій $Bпопередній буфер. І так sedдрукує $Bрядки виводу- рахунку за зібраним входом. Це означає , що, з огляду на наш попередній приклад, sedбуде PРінту 1до виходу, а потім Dдаліть що і відправити назад в початок сценарію шаблону простір , яке виглядає наступним чином :

^2\n3$

... а вгорі сценарію Nвиводиться рядок введення ext, і наступна ітерація виглядає так:

^2\n3\n4$

І тому, коли ми знаходимо перше виникнення 5введення, простір шаблону насправді виглядає так:

^3\n4\n5$

Потім Dзапускається цикл elete, і коли він проходить, це виглядає так:

^5$

І коли Nвиведений рядок введення ext sedвдаряє EOF і вимикається. На той час він лише колись Pпередзвонив рядки 1 і 2.

Ось приклад запуску:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Це відбитки:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100

Я фактично працюю з величезними файлами, і відповідь Дона була помітно повільнішою, ніж це рішення. Я спочатку вагався змінити свою прийняту відповідь, але різниця швидкостей цілком помітна.
Амеліо Васкес-Рейна

4
@Amelio - це буде працювати з потоком будь-якого розміру, і він не повинен читати файл для роботи. Найбільшим фактором продуктивності є розмір $Aта / або $B. Чим більше ви зробите ці цифри, тим повільніше вони стануть - але ви можете зробити їх досить великими.
mikeserv

1
@ AmelioVazquez-Reina - якщо ви використовуєте старіший, це краще, я думаю.
mikeserv

11

Ви можете використовувати gnu grepз -Aі -Bдрукувати точно частини файлу , який ви хочете виключити , але додати -nперемикач друкувати номери рядків , а потім формат виведення і передати його в якості сценарію командного рядка для sedвидалення цих рядків:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Це також має працювати з файлами шаблонів, переданих grepчерез, -fнаприклад:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Я думаю, що це може бути дещо оптимізовано, якби він згорнув будь-які три чи більше послідовних номерів рядків на діапазони, щоб мати, наприклад, 2,6dзамість 2d;3d;4d;5d;6d... хоча, якщо вхід має лише кілька збігів, робити це не варто.


Інші способи, які не зберігають порядок рядків і, швидше за все, повільніші:
з comm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

commвимагає відсортованого введення, що означає, що порядок рядків не зберігався б у кінцевому результаті (якщо ваш файл вже не відсортований), тому nlвін використовується для нумерації рядків перед сортуванням, comm -13друкує лише рядки, унікальні для другого файлу, а потім cutвидаляє частину, яку додав nl(тобто перше поле та роздільник :)
з join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-

Дякую Дон! Швидке запитання, чи очікуєте ви, що рішення з commшвидким буде швидше, ніж оригінальне з sedі grep?
Амеліо Васкес-Рейна

1
@ AmelioVazquez-Reina - Я не думаю, що він як і раніше читає вхідний файл двічі (плюс це робить деякі сортування) на відміну від рішення Майка, який обробляє файл лише один раз.
don_crissti

9

Якщо ви не проти використовувати vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesвмикає несумісний, безшумний колишній режим. Корисно для сценаріїв.
  • +{command}скажіть vim запустити {command}файл.
  • g/${PAT}/- на всіх лініях, що відповідають /fff/. Це стає складним, якщо візерунок містить спеціальні символи регулярного вираження, до яких ви не збиралися ставитися таким чином.
  • .-${B} - від 1 рядка вище цього
  • .+${A}- на 2 рядки нижче цього (див. :he cmdline-rangesдля цих двох)
  • d - видалити рядки.
  • +w !tee потім записує на стандартний вихід.
  • +q! закривається без збереження змін.

Ви можете пропустити змінні та безпосередньо використовувати шаблон і числа. Я використовував їх просто для ясності мети.


3

Як щодо (з використанням GNU grepта bash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

Тут ми знаходимо рядки, від яких слід відкинути grep -B2 -A1 'fff' file.txt, а потім використовуємо це як вхідний файл, щоб знайти потрібні рядки, відкинувши їх.


Гм, це нічого не видає на моїй машині (OS X)
Амеліо Васкес-Рейна

@ AmelioVazquez-Reina вибачте з цього приводу .. я раніше не знав вашої ОС..завжди я перевірив це на Ubuntu ..
heemayl

2
У цій проблемі виникне та сама проблема, що kosі у (тепер видаленому) рішенні, як якщо б у вхідному файлі є дублікати рядків, а деякі з них виходять за межі діапазону, а інші знаходяться всередині цього діапазону, це видалить їх усіх. Крім того, при наявності декількох випадків шаблону , якщо --у вхідному файлі (за межами діапазонів) є рядки, такі будуть видаляти їх, оскільки роздільник --відображається у grepвихідних даних, коли більше одного рядка відповідає шаблону (останній дуже малоймовірний, але варто згадую, я здогадуюсь).
don_crissti

@don_crissti Дякую .. ви маєте рацію ... хоч я буквально брав приклад ОП. Я покину його, якщо хтось пізніше знайде це корисним ..
heemayl

1

Ви можете досягти достатньо хорошого результату, скориставшись тимчасовими файлами:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

Результат досить хороший, оскільки ви можете втратити деякий відступ у процесі, але якщо це нечутливий файл xml або відступ, це не повинно бути проблемою. Оскільки цей сценарій використовує оперативні пристрої, запис та читання цих тимчасових файлів відбувається так само швидко, як і робота в пам'яті.


1

Крім того, якщо ви просто хочете виключити деякі рядки перед заданим маркером, ви можете використовувати:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(Глен Джекман на /programming//a/1492538 )

Прокладаючи деякі команди, ви можете отримати поведінку до / після:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac

1
Блискуче, використовуйте awkна зворотному файлі, щоб обробляти наступні рядки, коли ви хочете впливати на рядки до та повернути результат назад.
karmakaze

0

Один із способів цього досягти, можливо, найпростішим способом було б створити змінну і зробити наступне:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

Таким чином ви все ще маєте свою структуру. І ви можете легко побачити з одного вкладиша, що ви намагаєтесь видалити.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii

те саме рішення, що і heemayl, і та сама проблема, як описана в don_crissti: У цьому випадку буде така ж проблема, як у рішенні коса (тепер видалено), як якщо б у вхідному файлі є дублікати рядків, деякі з них виходять за межі діапазону, а інші знаходяться всередині цього діапазону це видалить їх усіх. Крім того, при наявності декількох випадків шаблону, якщо у вхідному файлі (за межами діапазонів) є рядки на зразок - це видалить їх, оскільки роздільник - відображається у висновку grep, коли більше одного рядка відповідає шаблону (останній сильно мабуть, але варто згадати, я думаю).
Бодо Тісен

0

Якщо є лише 1 матч:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

Інакше (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.