Jak grep-inverse-match i wykluczyć wiersze „przed” i „po”

26

Rozważ plik tekstowy z następującymi wpisami:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Biorąc pod uwagę wzorzec (np. fff), Chciałbym grep pliku powyżej, aby uzyskać wynik:

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

Na przykład, jeśli B = 2i A = 1, dane wyjściowe z wzorcem = fffpowinny wynosić:

aaa
bbb
ccc
hhh
iii

Jak mogę to zrobić za pomocą grep lub innych narzędzi wiersza poleceń?


Uwaga, kiedy próbuję:

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

Nie dostaję tego, czego chcę. Zamiast tego otrzymuję:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
Amelio Vazquez-Reina
źródło

Odpowiedzi:

9

don może być lepszy w większości przypadków, ale na wypadek, gdyby plik był naprawdę duży i nie możesz sobie sedporadzić z tak dużym plikiem skryptu (co może się zdarzyć przy ponad 5000 liniach skryptu) , oto proste 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'

Jest to przykład tak zwanego przesuwanego okna na wejściu. Działa poprzez budowanie bufora wyprzedzającego z $B-count linii przed próbą wydrukowania czegokolwiek.

I właściwie powinienem wyjaśnić moją poprzednią kwestię: główny ogranicznik wydajności zarówno dla tego rozwiązania, jak i dla dona, będzie bezpośrednio związany z interwałem. To rozwiązanie będzie spowalniać przy większych rozmiarach interwałów , podczas gdy don będzie spowalniać przy większych częstotliwościach interwałów . Innymi słowy, nawet jeśli plik wejściowy jest bardzo duży, jeśli faktyczne występowanie interwału jest nadal bardzo rzadkie, jego rozwiązanie jest prawdopodobnie dobrym rozwiązaniem. Jeśli jednak wielkość interwału jest względnie łatwa do zarządzania i często występuje, należy wybrać to rozwiązanie.

Oto przepływ pracy:

  • Jeśli $matchzostanie znaleziony w przestrzeni wzorca poprzedzonej \newline, sedrekurencyjnie usunie Dkażdą \newline, która go poprzedza.
    • Wyczyściłem $matchprzestrzeń wzorów całkowicie wcześniej - ale aby łatwo poradzić sobie z nakładaniem się, pozostawienie punktu orientacyjnego wydaje się działać znacznie lepiej.
    • Próbowałem też s/.*\n.*\($match\)/\1/spróbować za jednym razem i uniknąć pętli, ale gdy $A/$Bsą duże, Dpętla elete okazuje się znacznie szybsza.
  • Następnie wyciągnij w Nlinii EXT wejścia poprzedzone \nseparatora ewline i spróbuj ponownie DsuĹ /\n.*$match/ponownie odwołując się do naszej ostatnio używanego wyrażenia regularnego w / //.
  • Jeśli przestrzeń wzoru pasuje, $matchto można to zrobić tylko $matchna początku linii - wszystkie wcześniejsze $Blinie zostały usunięte.
    • Zaczynamy więc $Azapętlać.
    • Każdy przebieg tej pętli będziemy próbować s///ubstitute na &sobie $Ath \ncharakter ewline w przestrzeni wzorca, a jeśli się powiedzie, test nas oddział - i cały nasz $Abufor fter - z skryptu w całości, aby uruchomić skrypt w ciągu od góry z następnym wierszem wprowadzania, jeśli istnieje.
    • Jeśli test się nie powiedzie, bwrócimy do :tetykiety operacji i przejdziemy do innej linii danych wejściowych - być może zaczniemy pętlę, jeśli $matchwystąpi podczas zbierania $After.
  • Jeśli przejdziemy przez $matchpętlę funkcji, spróbujemy pprzeszukać $ostatnią linię, jeśli to jest, a jeśli !nie, postaramy się s///zbudować dla &siebie znak $Btej \nlinii w przestrzeni wzorca.
    • Będziemy test to także, a jeśli jest to sukces będziemy rozgałęziać na :Petykiecie rint.
    • Jeśli nie, wrócimy do :toperacji i otrzymamy kolejną linię wejściową dołączoną do bufora.
  • Jeśli zrobimy to do :Printowania, zrobimy Pto następnie Ddo pierwszej \newline w przestrzeni wzorów i ponownie uruchom skrypt od góry z tym, co pozostało.

I tym razem, gdybyśmy to robili A=2 B=2 match=5; seq 5 | sed...

Przestrzeń wzorów dla pierwszej iteracji w :Print wyglądałaby następująco:

^1\n2\n3$

I w ten sposób sedgromadzi swój $Bbufor. I tak seddrukuje do wyjścia $B-count linii za wejście to zebrał. Oznacza to, że biorąc pod uwagę nasz poprzedni przykład, sedby Prukuj 1do wyjścia, a następnie DsuĹ że i wysłać z powrotem do górnej części skryptu przestrzeń która wygląda jak wzór:

^2\n3$

... a na górze skryptu Npobierana jest linia wejściowa ext, więc następna iteracja wygląda następująco:

^2\n3\n4$

Kiedy więc znajdziemy pierwsze wystąpienie danych 5wejściowych, przestrzeń wzoru wygląda tak:

^3\n4\n5$

Następnie Duruchamia się pętla elete i po jej zakończeniu wygląda następująco:

^5$

A kiedy Nlinia wejściowa ext zostanie wyciągnięta, seduderza EOF i kończy pracę. Do tego czasu ma tylko Plinie w odcieniach 1 i 2.

Oto przykładowy przebieg:

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'

To drukuje:

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
mikeserv
źródło
Właściwie pracuję z dużymi plikami, a odpowiedź dona była zauważalnie wolniejsza niż to rozwiązanie. Początkowo wahałem się, czy zmienić przyjętą odpowiedź, ale różnica prędkości jest dość widoczna.
Amelio Vazquez-Reina
4
@Amelio - to będzie działać ze strumieniem dowolnej wielkości i nie musi czytać pliku do pracy. Największym czynnikiem wydajności jest rozmiar $Ai / lub $B. Im większe zrobisz te liczby, tym wolniej będzie ono rosło - ale możesz je zrobić dość duże.
mikeserv
1
@ AmelioVazquez-Reina - jeśli używasz starszego, myślę, że lepiej.
mikeserv
11

Można korzystać gnu grepz -Ai -Bdrukować dokładnie te części pliku, który chcesz wykluczyć jednak dodać -nprzełącznik również drukować numery linii, a następnie sformatować wyjście i przekazać go jako skrypt polecenia, aby sedusunąć te linie:

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

Powinno to również działać z plikami wzorców przekazywanymi np. grepPrzez -f:

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

Myślę, że można to nieco zoptymalizować, jeśli zwinie dowolne trzy lub więcej kolejnych numerów linii w zakresy, tak aby mieć np. 2,6dZamiast 2d;3d;4d;5d;6d... chociaż jeśli dane wejściowe zawierają tylko kilka dopasowań, nie warto tego robić.


Inne sposoby, które nie zachowują kolejności linii i są najprawdopodobniej wolniejsze:
z comm:

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

commwymaga posortowanych danych wejściowych, co oznacza, że ​​kolejność linii nie zostanie zachowana na końcowym wyjściu (chyba że plik jest już posortowany), więc nljest używana do numerowania linii przed sortowaniem, comm -13drukuje tylko linie unikalne dla 2. PLIKU, a następnie cutusuwa część dodaną przez nl(to znaczy pierwsze pole i separator :)
z join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-
don_crissti
źródło
Dzięki Don! Szybkie pytanie, czy spodziewałbyś się, że rozwiązanie commbędzie szybsze niż oryginalne z sedi grep?
Amelio Vazquez-Reina
1
@ AmelioVazquez-Reina - nie sądzę, ponieważ nadal czyta plik wejściowy dwa razy (plus sortuje), w przeciwieństwie do rozwiązania Mike'a, które przetwarza plik tylko raz.
don_crissti
9

Jeśli nie masz nic przeciwko użyciu vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Neswłącza niekompatybilny, cichy tryb ex. Przydatne do pisania skryptów.
  • +{command}każ vimowi uruchomić {command}na pliku.
  • g/${PAT}/- na wszystkich pasujących liniach /fff/. Staje się to trudne, jeśli wzorzec zawiera znaki specjalne wyrażeń regularnych, których nie zamierzałeś traktować w ten sposób.
  • .-${B} - od 1 linii powyżej tego
  • .+${A}- do 2 wierszy poniżej tego (patrz :he cmdline-rangeste dwa)
  • d - usuń linie.
  • +w !tee następnie zapisuje na standardowe wyjście.
  • +q! kończy pracę bez zapisywania zmian.

Możesz pominąć zmienne i bezpośrednio użyć wzorca i liczb. Użyłem ich tylko dla jasności celu.

muru
źródło
3

Co powiesz na (używając GNU grepi bash):

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

Tutaj znajdujemy linie, które mają zostać odrzucone grep -B2 -A1 'fff' file.txt, a następnie wykorzystujemy to jako plik wejściowy, aby znaleźć żądane linie odrzucające je.

heemayl
źródło
Hmm, to nie wyświetla niczego na moim komputerze (OS X)
Amelio Vazquez-Reina
@ AmelioVazquez-Reina przepraszam za to .. wcześniej nie znałem twojego systemu operacyjnego .. w każdym razie przetestowałem to na Ubuntu ..
heemayl
2
Miałoby to ten sam problem, co rozwiązanie kos(teraz usunięte), tak jakby w pliku wejściowym były zduplikowane linie, a niektóre z nich wykraczają poza zakres, a inne znajdują się w tym zakresie, spowoduje to ich usunięcie. Ponadto, przy wielu wystąpieniach wzorca , jeśli --w pliku wejściowym znajdują się linie (poza zakresami), zostaną one usunięte, ponieważ separator --pojawia się na grepwyjściu, gdy więcej niż jedna linia pasuje do wzorca (ten drugi jest wysoce nieprawdopodobny, ale warty wspomnienie chyba).
don_crissti
@don_crissti Dzięki .. masz rację .. chociaż dosłownie brałem przykład OP .. zostawię go na wypadek, gdyby ktoś uznał, że jest pomocny później ..
heemayl
1

Możesz uzyskać wystarczająco dobry wynik, używając plików tymczasowych:

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

Wynik jest wystarczający, ponieważ możesz utracić wcięcie w procesie, ale jeśli jest to plik niewrażliwy na XML lub wcięcie, nie powinno to stanowić problemu. Ponieważ ten skrypt używa pamięci RAM, zapisywanie i odczytywanie plików tymczasowych jest tak szybkie, jak praca w pamięci.

RafDouglas
źródło
1

Ponadto, jeśli chcesz wykluczyć niektóre wiersze przed danym znacznikiem, możesz użyć:

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

(glenn jackman na /programming//a/1492538 )

Pipingując niektóre polecenia, możesz uzyskać zachowanie przed / po:

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
RafDouglas
źródło
1
Genialnie, użyj awkna odwróconym pliku do obsługi kolejnych linii, jeśli chcesz wpłynąć na linie przed i ponownie odwrócić wynik.
karmakaze
0

Jednym ze sposobów na osiągnięcie tego, być może najłatwiejszym sposobem jest utworzenie zmiennej i wykonanie następujących czynności:

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

W ten sposób nadal masz swoją strukturę. I możesz łatwo zobaczyć z jednej wkładki, co próbujesz usunąć.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii
lordpavel
źródło
to samo rozwiązanie co heemayl i ten sam problem, jak opisany przez don_crissti: Miałoby to taki sam problem jak rozwiązanie kos (teraz usunięte), tak jakby w pliku wejściowym były zduplikowane linie, a niektóre z nich wykraczały poza zakres, a inne znajdowały się w tym zakresie spowoduje to ich usunięcie. Ponadto, przy wielu wystąpieniach wzorca, jeśli istnieją takie linie - w pliku wejściowym (poza zakresami) spowoduje to ich usunięcie, ponieważ separator - pojawia się na wyjściu grep, gdy więcej niż jedna linia pasuje do wzorca (ten drugi jest wysoce mało prawdopodobne, ale chyba warte wspomnienia).
Bodo Thiesen
0

Jeśli jest tylko 1 dopasowanie:

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

W przeciwnym razie (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)
dedowsdi
źródło