Jak uzyskać wiele wierszy z pliku przez wyrażenie regularne?

10

Jak uzyskać wiele wierszy z pliku przez wyrażenie regularne?

Często chciałbym uzyskać wiele linii / zmodyfikować wiele linii za pomocą wyrażenia regularnego. Przykładowy przypadek:

Próbuję odczytać część pliku XML / SGML (niekoniecznie są one dobrze sformułowane lub mają przewidywalną składnię, więc wyrażenie regularne byłoby bezpieczniejsze niż odpowiedni analizator składni. Ponadto chciałbym móc to zrobić również całkowicie pliki nieustrukturyzowane, w których znane są tylko niektóre słowa kluczowe.) w skrypcie powłoki (działającym w systemach Solaris i Linux).

Przykładowy XML:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

Z tego chciałbym przeczytać, <tag1>czy zawiera foogdzieś w sobie.

Wyrażenie regularne (<tag1>.*?foo.*?</tag1>)powinno dać właściwą część, ale narzędzia takie jak grepi seddziałają tylko dla mnie z pojedynczymi liniami. Jak mogę zdobyć

<tag1>
 <tag2>foo</tag2>
</tag1>

w tym przykładzie?

Legowisko
źródło
3
Link obowiązkowy
evilsoup
@evilsoup To prawda, ale moje pytanie nie dotyczy konkretnie plików XML / SGML, tylko dowolnych plików tekstowych.
Den

Odpowiedzi:

7

Jeśli masz zainstalowany system GNU grep, możesz wyszukiwać wieloliniowo, przekazując -Pflagę (perl-regex) i aktywując za PCRE_DOTALLpomocą(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Jeśli powyższe nie działa na twojej platformie, spróbuj przekazać -zflagę dodatkowo, to zmusza grep do traktowania NUL jako separatora linii, powodując, że cały plik wygląda jak pojedyncza linia.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
iruvar
źródło
Nie daje to żadnego wyjścia w moim systemie, gdy jest uruchomiony na przykładowym pliku OP.
terdon
Pracuje dla mnie. +1. Dzięki za (?s)wskazówkę
Nathan Wallace
@terdon, jaką wersję GNU grep używasz?
iruvar
@ 1_CR (GNU grep) 2.14w Debianie. Skopiowałem przykład OP w takiej postaci, w jakiej jest (dodając tylko ostatnią nową linię) i uruchomiłem grepna nim, ale nie otrzymałem żadnych wyników.
terdon
1
@slm, jestem na PCre 6.6, GNU grep 2.5.1 na RHEL. Czy masz coś przeciwko próbowaniu grep -ozPzamiast grep -oPna swoich platformach?
iruvar
3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Jeśli zrobisz powyższe, biorąc pod uwagę wyświetlane dane, przed ostatnim wierszem czyszczenia, powinieneś pracować z sedprzestrzenią wzorów, która wygląda następująco:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Możesz wydrukować przestrzeń wzoru w dowolnym momencie za pomocą lOOK. Następnie możesz adresować \nznaki.

sed l <file

Pokaże, że każda linia sedprzetwarza ją na etapie, w którym ljest wywoływana.

Właśnie to przetestowałem i potrzebowałem jeszcze jednego \backslashpo ,commapierwszej linii, ale poza tym działa tak, jak jest. Tutaj umieściłem go, _sed_functionaby móc łatwo nazwać go w celach demonstracyjnych w całej tej odpowiedzi: (działa z dołączonymi komentarzami, ale zostały tu usunięte ze względu na zwięzłość)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Teraz zmienimy na pna l, abyśmy mogli zobaczyć, nad czym pracujemy, podczas opracowywania naszego skryptu i usuwania demonstracji non-op, s?dzięki czemu ostatni wiersz naszego sed 3<<\SCRIPTwygląda następująco:

l;s/.*//;h;b}}

Potem uruchomię to jeszcze raz:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Ok! Więc miałem rację - to dobre uczucie. Teraz potrząśnij naszym lokiem, aby zobaczyć linie, które wciąga, ale usuwa. Usuniemy nasz obecny li dodamy jeden do, !{block}aby wyglądał następująco:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

Tak to wygląda tuż przed usunięciem.

Ostatnią rzeczą, którą chcę wam pokazać, jest Hstara przestrzeń, w której ją budujemy. Mam kilka kluczowych pojęć, które, mam nadzieję, mogę wykazać. Więc lponownie usuwam ostatni ook i zmieniam pierwszy wiersz, aby dodać zerknięcie do Hstarej spacji na końcu:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Hstara przestrzeń przetrwa cykle liniowe - stąd nazwa. Więc co ludzie często potknąć się na - Ok, co ja często potknąć się na - jest to, że wymaga kasowania po użyciu. W tym przypadku xzmieniam się tylko raz, więc przestrzeń wstrzymania staje się przestrzenią wzorów i odwrotnie, a ta zmiana przetrwa również cykle linii.

W efekcie muszę usunąć przestrzeń wstrzymania, która była kiedyś przestrzenią wzorów. Robię to, najpierw czyszcząc bieżącą przestrzeń wzorców za pomocą:

s/.*//

Który po prostu wybiera każdą postać i usuwa ją. Nie mogę użyć, dponieważ to skończyłoby mój bieżący cykl linii i następne polecenie nie zostałoby ukończone, co praktycznie zniszczyłoby mój skrypt.

h

Działa to w podobny sposób, Hale zastępuje przestrzeń wstrzymania, więc właśnie skopiowałem swoją pustą przestrzeń wzoru na górze mojej przestrzeni wstrzymania, skutecznie ją usuwając. Teraz mogę po prostu:

b

na zewnątrz.

I tak piszę sedskrypty.

mikeserv
źródło
Dzięki @slm! Jesteś naprawdę w porządku facetem, wiesz o tym?
mikeserv
Dzięki, dobra robota, bardzo szybki wzrost do 3 tys., Następnie 5 tys. 8-)
slm
Nie wiem, @slm. Zaczynam widzieć, że uczę się tutaj coraz mniej - może przerosłem już jego przydatność. Muszę o tym pomyśleć. ledwo nawet pojawiłem się na stronie w ciągu ostatnich kilku tygodni.
mikeserv
Co najmniej dostać się do 10k. Wszystko, co warto odblokować, znajduje się na tym poziomie. Trzymaj się z dala, 5k przyjedzie teraz dość szybko.
slm
1
Cóż, @slm - i tak jesteś rzadką rasą. Zgadzam się co do wielu odpowiedzi. Dlatego to mnie wkurza, gdy niektóre qs się zamykają. Ale tak naprawdę rzadko się to zdarza. Jeszcze raz dziękuję, slm.
mikeserv
2

Odpowiedź @ jamespfinn będzie działać idealnie, jeśli Twój plik jest tak prosty jak twój przykład. Jeśli masz bardziej złożoną sytuację, która <tag1>może obejmować więcej niż 2 linie, potrzebujesz nieco bardziej złożonej sztuczki. Na przykład:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

Skrypt perla przetworzy każdą linię pliku wejściowego i

  • if(/<tag1>/){$a=1;}: zmienna $ajest ustawiona na, 1jeśli <tag1>znaleziono otwierający tag ( ).

  • if($a==1){push @l,$_}: jeśli $ajest 1, dodaj tę linię do tablicy @l.

  • if(/<\/tag1>/) : jeśli bieżący wiersz pasuje do tagu zamykającego:

    • if(grep {/foo/} @l){print "@l"}: jeśli którakolwiek z linii zapisanych w tablicy @l(są to linie pomiędzy <tag1>i </tag1>) pasuje do ciągu foo, wypisz zawartość @l.
    • $a=0; @l=(): opróżnij listę ( @l=()) i ustaw z $apowrotem na 0.
terdon
źródło
Działa to dobrze, z wyjątkiem przypadku, gdy istnieje więcej niż jeden <tag1> zawierający „foo”. W takim przypadku wydruk jest wszystko, co od początku pierwsza <tag1> do końca ostatniego </ tag1> ...
Den
@den Przetestowałem to na przykładzie pokazanym w mojej odpowiedzi, który zawiera 3 <tag1>z fooi działa dobrze. Kiedy ci się to nie udaje?
terdon
tak źle
parsuje
1

Oto sedalternatywa:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Wyjaśnienie

  • -n oznacza, że ​​nie drukuj wierszy, chyba że otrzymałeś takie polecenie.
  • /<tag1/ pierwszy pasuje do tagu otwierającego
  • :x to etykieta umożliwiająca późniejsze przejście do tego punktu
  • N dodaje następny wiersz do obszaru wzorów (aktywny bufor).
  • /<\/tag1/!b xoznacza, że ​​jeśli bieżąca przestrzeń wzorcowa nie zawiera znacznika zamykającego, przejdź do xutworzonej wcześniej etykiety. W ten sposób dodajemy linie do przestrzeni wzorów, dopóki nie znajdziemy naszego tagu zamykającego.
  • /foo/poznacza, że ​​jeśli bieżąca przestrzeń wzoru pasuje foo, należy ją wydrukować.
Joseph R.
źródło
1

Myślę, że można to zrobić za pomocą GNU awk, traktując znacznik końcowy jako separator rekordów, np. Dla znanego znacznika końcowego </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

lub bardziej ogólnie (z wyrażeniem regularnym dla tagu końcowego)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Testowanie na @ terdon's foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
steeldriver
źródło
0

Jeśli plik ma strukturę dokładnie taką, jak pokazano powyżej, możesz użyć flag -A (wiersze po) i -B (wiersze przed) dla grep ... na przykład:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Jeśli twoja wersja grepgo obsługuje, możesz także użyć prostszej -C(dla kontekstu) opcji, która drukuje otaczające N wiersze:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
jamespfinn
źródło
Dziękuje ale nie. To tylko przykład, a prawdziwe rzeczy wyglądają dość nieprzewidywalnie ;-)
Den
1
To nie jest znalezienie tagu z foo, to tylko znalezienie foo i wyświetlenie linii kontekstu
Nathan Wallace
@NathanWallace tak, dokładnie o to prosił PO, ta odpowiedź działa idealnie dobrze w przypadku podanym w pytaniu.
terdon
@terdon wcale nie o to pyta. Cytat: „Chciałbym przeczytać <tag1>, jeśli zawiera on gdzieś foo.” To rozwiązanie jest jak „Chciałbym przeczytać„ foo ”i 1 linię kontekstu niezależnie od tego, gdzie pojawia się„ foo ”. Zgodnie z twoją logiką, równie ważna byłaby odpowiedź na to pytanie tail -3 input_file.xml. Tak, działa w tym konkretnym przykładzie, ale nie jest to pomocna odpowiedź na pytanie.
Nathan Wallace
@NathanWallace miałem na myśli to, że OP wyraźnie stwierdził, że nie jest to prawidłowy format XML, w takim przypadku może być wystarczające wydrukowanie N linii wokół szukanego ciągu przez OP. Przy dostępnych informacjach odpowiedź była wystarczająco przyzwoita.
terdon