Como usar o regex com o AWK para substituição de strings?

13

Suponha que haja algum texto de um arquivo:

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

Eu quero adicionar 11 a cada número seguido por um " em cada linha, se houver um, ou seja,

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

Aqui está a minha solução usando o GNU AWK e o regex:

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

i.e., desejo substituir (\d+)\" por +10\" , em que é o grupo que representa (\d+) . Mas isso não funciona. Como posso fazer isso funcionar?

Se o gawk não é a melhor solução, o que mais pode ser usado?

    
por Tim 20.11.2011 / 00:32

4 respostas

12

Tente isso (o gawk é necessário).

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

Teste com o seu exemplo:

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"))
)

Note que este comando não funcionará se os dois números (por exemplo, 1 "e" # 1 ") forem diferentes, ou se houver mais números na mesma linha com este padrão (por exemplo, 23" ... 32 ". "# 123") em uma linha.


UPDATE

Como @Tim (OP) disse que o número seguido por " na mesma linha poderia ser diferente, fiz algumas alterações na minha solução anterior e fiz com que funcionasse para o seu novo exemplo.

BTW, pelo exemplo, sinto que poderia ser uma tabela de estrutura de conteúdo, então não vejo como os dois números poderiam ser diferentes. O primeiro seria o número da página impressa e o segundo com o # seria o índice da página. Estou certo?

De qualquer forma, você conhece melhor sua exigência. Agora a nova solução, ainda com o gawk (eu quebro o comando em linhas para facilitar a leitura):

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

test com seu exemplo novo :

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 baseado no comentário de @Tim

(1) Does FS=OFS="\" \"#" mean the separator of field in both input and output is double quote, space, double quote and #? Why specify double quote twice?

Você está certo para o separador na parte de entrada e saída. Definiu o separador como:

" "#

Existem duas aspas duplas, porque é mais fácil capturar os dois números desejados (com base na sua entrada de exemplo).

(2) In /.* ([0-9]+)$/, does $ mean the end of the string?

Exatamente!

(3) In the third argument of gensub(), what is the difference between "g" and "G"? there is no difference between G and g. Check this out:

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.

Isso é do link . você pode ler para obter um uso detalhado do gensub.

    
por 20.11.2011 / 00:51
7

Ao contrário de praticamente todas as ferramentas que fornecem substituições de expressões regulares, o awk não permite referências anteriores como no texto de substituição. O GNU Awk dá acesso a grupos combinados se você usar o match função , mas não com ~ ou sub ou gsub .

Note também que mesmo que fosse suportado, o seu fragmento acrescentaria a string +11 , e não executaria um cálculo numérico. Além disso, seu regexp não está certo, você está combinando coisas como "42"" e não "#42" .

Aqui está uma solução awk (aviso, não testado). Ele realiza apenas uma única substituição por linha.

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}'

Seria mais simples em Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
    
por 20.11.2011 / 03:19
5

awk pode fazer isso, mas não é direto, mesmo usando backreferencing.
GNU awk tem backreferecing (parcial), na forma de gensub .

Instâncias de 123" são agrupadas temporariamente em \x01 e \x02 para marcá-las como não modificadas (para sub() . co

Ou você pode simplesmente percorrer o loop alterando os candidatos, e nesse caso, o backreferencing e os "colchetes" não são necessários; mas é necessário acompanhar o índice de caracteres.

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 }'

Aqui está outra maneira, usando gensub e array split e \x01 como um delimitador de campo (para divisão ). \ x02 marca um elemento de matriz como um candidato para a aritmética Adição.

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" }'
    
por 20.11.2011 / 06:28
2

Como as soluções em (g) awk parecem se tornar bastante complexas, eu queria adicionar uma solução alternativa em Perl:

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

Explicação:

  • A opção -w ativa avisos (que avisam sobre possíveis efeitos indesejados).
  • A opção -p implica em um loop em torno do código que funciona como sed ou awk, salvando cada linha de entrada automaticamente na variável padrão, $_ .
  • A opção -e diz ao perl que o código do programa está seguindo na linha de comando, não em um arquivo de script.
  • O código é uma substituição de regex ( s/.../.../ ) em $_ , em que uma sequência de dígitos, se for seguida por um " , será substituída pela sequência, interpretada como um número na adição, mais 11.
  • A declaração de antecipação positiva de largura zero (?=pattern) procura pelo " sem levá-lo para o jogo, por isso não temos que repeti-lo na substituição. A variável MATCH $& na substituição conterá apenas o número.
  • O modificador /e para a expressão regular diz perl para "executar" a substituição como código em vez de tomá-la como uma string.
  • O modificador /g torna a substituição "global", repetindo-a em todas as correspondências na linha.

A variável% MATCH $& infelizmente será prejudicial para o desempenho do código nas versões Perl antes de 5.20. Uma solução mais rápida (e não muito mais complexa) usaria o agrupamento e a referência de referência $1 :

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

E se a asserção de look-ahead parecer muito confusa, você também poderá substituir as aspas explicitamente:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
    
por 30.12.2015 / 12:47