Come usare regex con AWK per la sostituzione di stringhe?


13

Supponiamo che ci sia del testo da un file:

(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)

Voglio aggiungere 11 a ciascun numero seguito da un "in ogni riga se ce n'è uno, vale a dire

(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)

Ecco la mia soluzione usando GNU AWK e regex:

awk -F'#' 'NF>1{gsub(/"(\d+)\""/, "\1+11\"")}'

cioè, voglio sostituire (\d+)\"con \1+10\", dove \1è il gruppo che rappresenta (\d+). Ma non funziona Come posso farlo funzionare?

Se gawk non è la soluzione migliore, cos'altro si può usare?


Mi dispiace per la duplicazione. Ma prima ho chiesto su StackOverflow e non ho ottenuto una risposta soddisfacente, quindi ho contrassegnato la migrazione. Ma non è successo per un po ', quindi non mi aspettavo che accadesse e poi ho chiesto a Unix.SE.
Tim

Risposte:


12

Prova questo (è necessario gawk).

awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}' YourFile

Prova con il tuo esempio:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 2" "#2")
("Exercises 30" "#30")
("Notes and References 34" "#34"))
)
'|awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}'   
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 13" "#13")
("Exercises 41" "#41")
("Notes and References 45" "#45"))
)

Si noti che questo comando non funzionerà se i due numeri (ad es. 1 "e" # 1 ") sono diversi. Oppure se ci sono più numeri nella stessa riga con questo modello (ad es. 23" ... 32 "..." # 123 ") in una riga.


AGGIORNARE

Poiché @Tim (OP) ha detto che il numero seguito dalla "stessa riga potrebbe essere diverso, ho apportato alcune modifiche alla mia soluzione precedente e l'ho fatto funzionare per il tuo nuovo esempio.

A proposito, dall'esempio sento che potrebbe essere una tabella della struttura dei contenuti, quindi non vedo come i due numeri potrebbero essere diversi. Il primo sarebbe il numero di pagina stampato e il secondo con # sarebbe l'indice di pagina. Ho ragione?

Ad ogni modo, conosci meglio le tue esigenze. Ora la nuova soluzione, sempre con gawk (spezzo il comando in righe per facilitarne la lettura):

awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}' yourFile

prova con il tuo nuovo esempio:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)
'|awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}'                        
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)


EDIT2 basato sul commento di @Tim

(1) FS = OFS = "\" \ "#" significa che il separatore di campo sia in input che in output è virgoletta doppia, spazio, virgoletta doppia e #? Perché specificare due virgolette due volte?

Hai ragione per il separatore sia nella parte di input che in quella di output. Ha definito il separatore come:

" "#

Esistono due virgolette doppie, perché è più facile catturare i due numeri desiderati (in base all'input di esempio).

(2) In /.* ([0-9] +) $ /, $ indica la fine della stringa?

Esattamente!

(3) Nel terzo argomento di gensub (), qual è la differenza tra "g" e "G"? non c'è differenza tra G e g. Controllalo:

gensub(regexp, replacement, how [, target]) #
    Search the target string target for matches of the regular expression regexp. 
    If "how" is a string beginning with g or G (short for global”), then 
        replace all matches of regexp with replacement.

Questo è da http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . puoi leggere per ottenere un uso dettagliato di gensub.


Grazie! Mi chiedo come farlo funzionare se i due numeri, ad esempio 1 "e" # 1 "sono diversi?
Tim

questa risposta funziona per il tuo requisito / esempio attuale. se il requisito viene modificato, potresti modificare la domanda e dare un esempio migliore. e dal tuo codice awk -F'#', sembra che tu voglia fare la modifica sulla parte solo dopo '#'?
Kent,

Grazie per il tuo suggerimento Ho appena modificato il mio esempio in modo che i due numeri non siano uguali.
Tim

@Tim vedi la mia risposta aggiornata, per il tuo nuovo esempio.
Kent,

Grazie! Alcune domande: (1) FS=OFS="\" \"#"significa che il separatore di campo sia in input che in output è double quote, space, double quote e #? perché specificare due volte la virgoletta doppia? (2) in /.* ([0-9]+)$/, $significa la fine della stringa? (3) nel terzo argomento di gensub (), qual è la differenza tra "g"e "G"?
Tim

7

A differenza di quasi tutti gli strumenti che forniscono sostituzioni regexp, awk non consente riferimenti indietro come \1nel testo sostitutivo. GNU Awk dà accesso ai gruppi corrispondenti se si utilizza la matchfunzione , ma non con ~o subo gsub.

Si noti inoltre che anche se \1fosse supportato, lo snippet aggiungerebbe la stringa +11, non eseguirà un calcolo numerico. Inoltre, il tuo regexp non è del tutto corretto, stai abbinando cose come "42""e non "#42".

Ecco una soluzione awk (avviso, non testato). Esegue solo una singola sostituzione per riga.

awk '
  match($0, /"#[0-9]+"/) {
    n = substr($0, RSTART+2, RLENGTH-3) + 11;
    $0 = substr($0, 1, RSTART+1) n substr($0, RSTART+RLENGTH-1)
  }
  1 {print}'

Sarebbe più semplice in Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'

La prima frase della tua risposta è esattamente quello che stavo cercando. Tuttavia, il fatto che tu abbia detto "... nel testo sostitutivo" solleva una domanda di follow-up: awk consente riferimenti indietro nel modello regex stesso?
Wildcard il

1
@Wildcard No, awk non tiene traccia dei gruppi (ad eccezione dell'estensione GNU che menziono).
Gilles 'SO- smetti di essere malvagio' il

5

awkpuò farlo, ma non è diretto, anche usando il backreferencing.
GNU awk ha un backreferecing (parziale), sotto forma di gensub .

Le istanze di 123"sono temporaneamente inserite \x01e \x02contrassegnate come non modificate (per sub(). Co

Oppure puoi semplicemente passare da un ciclo all'altro cambiando i candidati mentre procedi, nel qual caso non è necessario il back-referencing e le "parentesi"; ma è necessario tenere traccia dell'indice dei caratteri.

awk '{$0=gensub(/([0-9]+)\"/, "\x01\\1\"\x02", "g", $0 )
      while ( match($0, /\x01[0-9]+\"\x02/) ) {
        temp=substr( $0, RSTART, RLENGTH )
        numb=substr( temp, 2, RLENGTH-3 ) + 11
        sub( /\x01[0-9]+\"\x02/, numb "\"" ) 
      } print }'

Ecco un altro modo, usando gensube array splite \x01come delimitatore di campo (per la divisione ). \ X02 contrassegna un elemento array come candidato per l'aggiunta aritmetica.

awk 'BEGIN{ ORS="" } {
     $0=gensub(/([0-9]+)\"/, "\x01\x02\\1\x01\"", "g", $0 )
     split( $0, a, "\x01" )
     for (i=0; i<length(a); i++) { 
       if( substr(a[i],1,1)=="\x02" ) { a[i]=substr(a[i],2) + 11 }
       print a[i]
     } print "\n" }'

Grazie! Nel tuo primo codice, (1) cosa "\x01\\1\"\x02"significa? Ancora non capisco \x01e \x02. (2) quanto è diverso il ritorno $0da gensube $0l'ultimo come argomento gensub?
Tim

@ Tim. I valori esadecimali \x01e \x02vengono utilizzati come marcatori di sostituzione. Questi valori sono altamente improbabile che siano in qualsiasi normale file di testo, in modo che siano ugualmente "altamente" sicuro da usare (es. Non incontrare uno scontro con quelli pre-esistenti) .. Sono le etichette solo temporanei .. Re $0=gensub(... $0).. vedi questo link Funzioni di manipolazione delle stringhe , ma in breve: restituisce la stringa modificata come risultato della funzione e la stringa di destinazione originale non viene modificata. ... $0=Modifica semplicemente l'obiettivo originale ...
Peter.O

2

Poiché le soluzioni in (g) awk sembrano diventare piuttosto complesse, volevo aggiungere una soluzione alternativa in Perl:

perl -wpe 's/\d+(?=")/$&+11/eg' < in.txt > out.txt

Spiegazione:

  • L'opzione -wabilita gli avvisi (che ti avviseranno di possibili effetti indesiderati).
  • Opzione -pimplica il perimetro codice che funziona in modo simile a sed o awk, salvando ogni riga di input automaticamente nella variabile predefinito $_.
  • L'opzione -edice a perl che il codice del programma sta seguendo sulla riga di comando, non in un file di script.
  • Il codice è una sostituzione regex ( s/.../.../) su $_, in cui una sequenza di cifre, se seguita da una ", verrà sostituita dalla sequenza, interpretata come un numero nell'aggiunta, più 11.
  • L' asserzione di previsione positiva a larghezza zero (?=pattern) cerca la "senza prenderla nella partita, quindi non dobbiamo ripeterla nella sostituzione. La variabile MATCH $&nella sostituzione conterrà quindi solo il numero.
  • Il /emodificatore di regex dice perldi "eseguire" la sostituzione come codice invece di prenderla come stringa.
  • Il /gmodificatore rende la sostituzione "globale", ripetendola su ogni corrispondenza della riga.

$&Purtroppo la variabile MATCH sarà dannosa per le prestazioni del codice nelle versioni Perl precedenti alla 5.20. Una soluzione più veloce (e non molto più complessa) userebbe invece il raggruppamento e il backreference $1:

perl -wpe 's/(\d+)?="/$1+11/eg' < in.txt > out.txt

E se l'affermazione prospettica sembra troppo confusa, potresti anche sostituire esplicitamente le virgolette:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
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.