Come ottenere più righe da un file da una regex?


10

Come ottenere più righe da un file da una regex?

Spesso vorrei ottenere più righe / modificare più righe con una regex. Un esempio:

Sto cercando di leggere parte di un file XML / SGML (non sono necessariamente ben formati o in una sintassi prevedibile, quindi una regex sarebbe più sicura di un vero parser. Inoltre vorrei poterlo fare anche completamente file non strutturati in cui sono note solo alcune parole chiave.) in uno script di shell (in esecuzione su Solaris e Linux).

Esempio XML:

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

Da questo vorrei leggere <tag1>se contiene fooda qualche parte al suo interno.

Un regex come (<tag1>.*?foo.*?</tag1>)dovrebbe dare la parte giusta ma strumenti simili grepe sedfunzionano solo per me con linee singole. Come posso avere

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

in questo esempio?



@evilsoup È vero, ma la mia domanda non riguarda specificamente i file XML / SGML, ma solo i file di testo.
Den

Risposte:


7

Se hai GNU grep installato puoi fare una ricerca su più righe passando il -Pflag (perl-regex) e attivando PCRE_DOTALLcon(?s)

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

Se quanto sopra non funziona sulla tua piattaforma, prova anche a passare il -zflag, questo costringe grep a trattare NUL come separatore di linea, facendo apparire l'intero file come una singola linea.

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

Ciò non fornisce alcun output sul mio sistema quando eseguito sul file di esempio dell'OP.
terdon

Per me va bene. +1. Grazie per la (?s)punta
Nathan Wallace,

@terdon, quale versione di GNU grep stai usando?
Iruvar,

@ 1_CR (GNU grep) 2.14su Debian. Ho copiato l'esempio dei PO così com'è (aggiungendo solo la nuova riga finale) e grepho eseguito il tuo su di esso ma non ho ottenuto risultati.
terdon

1
@slm, sono su PC 6.6, GNU grep 2.5.1 su RHEL. Ti dispiace provare grep -ozPinvece che grep -oPsulle tue piattaforme?
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

Se fai quanto sopra, dati i dati che mostri, prima dell'ultima riga di pulizia lì, dovresti lavorare con uno sedspazio modello simile a:

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

Puoi stampare lo spazio del tuo motivo ogni volta che vuoi con look. È quindi possibile indirizzare i \ncaratteri.

sed l <file

Ti mostrerà ogni linea lo sedelabora nella fase in cui lviene chiamato.

Quindi l'ho appena testato e ne ho avuto bisogno \backslashdopo uno ,commanella prima riga, ma per il resto funziona così. Qui lo inserisco in _sed_functionmodo da poterlo facilmente chiamare a scopo dimostrativo in tutta questa risposta: (funziona con commenti inclusi, ma qui sono rimossi per brevità)

_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>

Ora cambiamo il pper un lcosì possiamo vedere con cosa stiamo lavorando mentre sviluppiamo il nostro script e rimuoviamo la demo non-op in s?modo che l'ultima riga del nostro sed 3<<\SCRIPTassomigli a:

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

Quindi lo eseguirò di nuovo:

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

Ok! Quindi avevo ragione, è una bella sensazione. Ora, mescoliamo il nostro look in giro per vedere le linee che tira dentro ma cancella. Rimuoveremo la nostra corrente le ne aggiungeremo una in !{block}modo che assomigli a:

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

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

Ecco come appare poco prima di cancellarlo.

Un'ultima cosa che voglio mostrarti è il Hvecchio spazio mentre lo costruiamo. Ci sono un paio di concetti chiave che spero di poter dimostrare. Quindi rimuovo di nuovo l'ultimo look e modifico la prima riga per aggiungere una sbirciatina nel Hvecchio spazio alla fine:

{ 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>$

Hil vecchio spazio sopravvive ai cicli di linea - da qui il nome. Quindi quello su cui la gente inciampa spesso - ok, quello su cui inciampo spesso - è che deve essere cancellato dopo averlo usato. In questo caso xcambio solo una volta, quindi lo spazio di mantenimento diventa lo spazio del modello e viceversa e questo cambiamento sopravvive anche ai cicli di linea.

L'effetto è che devo eliminare il mio spazio di attesa che era il mio spazio modello. Lo faccio cancellando prima lo spazio del pattern corrente con:

s/.*//

Che seleziona semplicemente ogni personaggio e lo rimuove. Non posso usarlo dperché questo finirebbe il mio attuale ciclo di linee e il prossimo comando non sarebbe stato completato, il che avrebbe praticamente rovinato il mio script.

h

Funziona in modo simile Hma sovrascrive lo spazio di trattenimento, quindi ho appena copiato il mio spazio modello vuoto sopra lo spazio di trattenimento, eliminandolo in modo efficace. Ora posso solo:

b

su.

Ed è così che scrivo sedscript.


Grazie @slm! Sei un ragazzo davvero ok, lo sai?
mikeserv,

Grazie, bel lavoro, ascesa molto rapida a 3k, prossimo su 5k Cool-
slm

Non lo so, @slm. Sto iniziando a vedere che sto imparando sempre meno qui - forse ne ho superato la sua utilità. Ci devo pensare. Sono arrivato a malapena sul sito nelle ultime due settimane.
mikeserv,

Almeno arriva a 10k. Tutto ciò che vale la pena sbloccare è a quel livello. Continua a scheggiare, 5k arriverà abbastanza velocemente ora.
slm

1
Bene, @slm - sei comunque una razza rara. Sono d'accordo sulle risposte multiple però. Ecco perché mi dà fastidio quando alcune domande vengono chiuse. Ma ciò accade raramente, in realtà. Grazie ancora, slm.
mikeserv,

2

La risposta di @jamespfinn funzionerà perfettamente se il tuo file è semplice come il tuo esempio. Se hai una situazione più complessa in cui <tag1>potresti estendere più di 2 linee, avrai bisogno di un trucco leggermente più complesso. Per esempio:

$ 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>

Lo script perl elaborerà ogni riga del file di input e

  • if(/<tag1>/){$a=1;}: la variabile $aè impostata su 1se <tag1>viene trovato un tag di apertura ( ).

  • if($a==1){push @l,$_}: per ogni riga, se $apresente 1, aggiungere quella riga all'array @l.

  • if(/<\/tag1>/) : se la riga corrente corrisponde al tag di chiusura:

    • if(grep {/foo/} @l){print "@l"}: se una delle righe salvate nell'array @l(queste sono le linee tra <tag1>e </tag1>) corrisponde alla stringa foo, stampa il contenuto di @l.
    • $a=0; @l=(): svuota l'elenco ( @l=()) e $atorna a 0.

Funziona bene tranne nel caso in cui ci siano più di un <tag1> contenente "pippo". In quel caso stampa ogni cosa dall'inizio del primo <tag1> alla fine dell'ultimo </tag1> ...
Den

@den L'ho provato con l'esempio mostrato nella mia risposta che contiene 3 <tag1>con fooe funziona benissimo. Quando fallisce per te?
terdon

sembra così sbagliato analizzare xml usando regex :)
Braiam,

1

Ecco sedun'alternativa:

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

Spiegazione

  • -n significa non stampare le linee se non diversamente indicato.
  • /<tag1/ prima corrisponde al tag di apertura
  • :x è un'etichetta per consentire di saltare a questo punto in seguito
  • N aggiunge la riga successiva allo spazio del pattern (buffer attivo).
  • /<\/tag1/!b xsignifica che se lo spazio modello corrente non contiene tag di chiusura, si dirama verso l' xetichetta creata in precedenza. Continuiamo quindi ad aggiungere linee allo spazio del motivo fino a quando non troviamo il nostro tag di chiusura.
  • /foo/psignifica che se lo spazio del motivo corrente corrisponde foo, dovrebbe essere stampato.

1

Potresti farlo con GNU awk penso, trattando il tag di fine come un separatore di record, ad esempio per un tag di fine noto </tag1>:

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

o più in generale (con una regex per il tag end)

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

Test su @ terdon 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>

0

Se il tuo file è strutturato esattamente come mostrato sopra, puoi utilizzare i flag -A (righe dopo) e -B (righe prima) per grep ... ad esempio:

$ 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>

Se la tua versione greplo supporta, puoi anche usare l' -Copzione più semplice (per il contesto) che stampa le linee N circostanti:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>

Grazie, ma no. Questo è solo un esempio e le cose reali sembrano piuttosto imprevedibili ;-)
Den,

1
Non è trovare un tag con foo al suo interno, è solo trovare foo e mostrare linee di contesto
Nathan Wallace,

@NathanWallace sì, che è esattamente ciò che l'OP stava chiedendo, questa risposta funziona perfettamente nel caso indicato nella domanda.
terdon

@terdon non è affatto quello che fa la domanda. Citazione: "Vorrei leggere il <tag1> se contiene foo da qualche parte al suo interno." Questa soluzione è come "Vorrei leggere 'pippo' e 1 riga di contesto indipendentemente da dove appare 'pippo'". Seguendo la tua logica, sarebbe una risposta altrettanto valida a questa domanda tail -3 input_file.xml. Sì, funziona per questo esempio specifico, ma non è una risposta utile alla domanda.
Nathan Wallace,

@NathanWallace il mio punto era che l'OP ha affermato specificamente che questo non è un formato XML valido, in tal caso, avrebbe potuto essere sufficiente stampare le linee N attorno alla stringa che l'OP sta cercando. Con le informazioni disponibili, questa risposta è stata abbastanza decente.
terdon
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.