Bagaimana cara menggunakan regex dengan AWK untuk penggantian string?

13

Misalkan ada beberapa teks dari file:

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

Saya ingin menambahkan 11 ke setiap angka diikuti oleh "di setiap baris jika ada satu, yaitu

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

Ini solusi saya dengan menggunakan GNU AWK dan regex:

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

yaitu, saya ingin mengganti (\d+)\"dengan \1+10\", di mana \1grup mewakili (\d+). Tapi itu tidak berhasil. Bagaimana saya bisa membuatnya bekerja?

Jika gawk bukan solusi terbaik, apa lagi yang bisa digunakan?

Tim
sumber
Maaf tentang duplikasi. Tapi saya pertama kali bertanya pada stackoverflow, dan tidak mendapat jawaban yang memuaskan, jadi saya menandai untuk migrasi. Tapi itu tidak terjadi untuk sementara waktu, jadi saya tidak berharap itu terjadi dan kemudian bertanya pada Unix.SE.
Tim

Jawaban:

12

Coba ini (melongo diperlukan).

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

Uji dengan contoh Anda:

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

Perhatikan bahwa perintah ini tidak akan berfungsi jika dua angka (mis. 1 "dan" # 1 ") berbeda. Atau ada lebih banyak angka di baris yang sama dengan pola ini (mis. 23" ... 32 "..." # 123 ") dalam satu baris.


MEMPERBARUI

Karena @Tim (OP) mengatakan angka yang diikuti oleh "dalam baris yang sama bisa berbeda, saya melakukan beberapa perubahan pada solusi saya sebelumnya, dan membuatnya berfungsi untuk contoh baru Anda.

BTW, dari contoh saya merasa itu bisa menjadi tabel struktur konten, jadi saya tidak melihat bagaimana dua angka itu bisa berbeda. Pertama adalah nomor halaman yang dicetak, dan yang kedua dengan # akan menjadi indeks halaman. Apakah saya benar?

Bagaimanapun, Anda tahu kebutuhan Anda yang terbaik. Sekarang solusi baru, masih dengan gawk (saya memecah perintah menjadi beberapa baris untuk membuatnya lebih mudah dibaca):

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

uji dengan contoh baru Anda :

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 berdasarkan komentar @Tim

(1) Apakah FS = OFS = "\" \ "#" berarti pemisah bidang dalam input dan output adalah kuotasi ganda, spasi, kuotasi ganda, dan #? Mengapa menentukan penawaran ganda dua kali?

Anda tepat untuk pemisah di bagian input dan output. Ini didefinisikan pemisah sebagai:

" "#

Ada dua tanda kutip ganda, karena lebih mudah untuk menangkap dua angka yang Anda inginkan (berdasarkan contoh input Anda).

(2) Dalam /.* ([0-9] +) $ /, apakah $ berarti akhir dari string?

Persis!

(3) Dalam argumen ketiga gensub (), apa perbedaan antara "g" dan "G"? tidak ada perbedaan antara G dan g. Lihat ini:

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.

Ini dari http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . Anda dapat membaca untuk mendapatkan detail penggunaan gensub.

Kent
sumber
Terima kasih! Saya bertanya-tanya bagaimana cara membuatnya jika dua angka misalkan 1 "dan" # 1 "berbeda?
Tim
jawaban ini berfungsi untuk kebutuhan / contoh Anda saat ini. jika persyaratannya diubah, mungkin Anda bisa mengedit pertanyaan, dan memberikan contoh yang lebih baik. dan dari kode Anda awk -F'#', tampaknya Anda hanya ingin melakukan perubahan pada bagian setelah '#'?
Kent
Terima kasih atas saran Anda. Saya baru saja memodifikasi contoh saya sehingga kedua angka tersebut tidak sama.
Tim
@Tim lihat jawaban saya yang diperbarui, untuk contoh baru Anda.
Kent
Terima kasih! Beberapa pertanyaan: (1) apakah FS=OFS="\" \"#"pemisah bidang dalam input dan output adalah kuotasi ganda, spasi, kuotasi ganda, dan #? mengapa menentukan penawaran ganda dua kali? (2) dalam /.* ([0-9]+)$/, apakah $berarti akhir dari string? (3) dalam argumen ketiga gensub (), apa perbedaan antara "g"dan "G"?
Tim
7

Tidak seperti hampir setiap alat yang menyediakan substitusi pengganti, awk tidak mengizinkan backreferensi seperti \1pada teks pengganti. GNU Awk memberikan akses ke grup yang cocok jika Anda menggunakan matchfungsi , tetapi tidak dengan ~atau subatau gsub.

Perhatikan juga bahwa meskipun \1didukung, cuplikan Anda akan menambahkan string +11, tidak melakukan perhitungan numerik. Juga, regexp Anda tidak benar, Anda mencocokkan hal-hal seperti "42""dan tidak "#42".

Inilah solusi awk (peringatan, belum diuji). Ini hanya melakukan penggantian tunggal per baris.

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

Akan lebih sederhana di Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
Gilles 'SANGAT berhenti menjadi jahat'
sumber
Kalimat pertama jawaban Anda tepat seperti yang saya cari. Namun, fakta bahwa Anda mengatakan "... dalam teks pengganti" menimbulkan pertanyaan tindak lanjut: Apakah awk memungkinkan referensi kembali dalam pola regex itu sendiri?
Wildcard
1
@Wildcard Tidak, awk tidak melacak grup (kecuali untuk ekstensi GNU yang saya sebutkan).
Gilles 'SANGAT berhenti menjadi jahat'
5

awkdapat melakukannya, tetapi itu tidak langsung, bahkan menggunakan referensi ulang.
GNU awk memiliki (sebagian) backreferecing, dalam bentuk gensub .

Contoh dari 123"untuk sementara dibungkus \x01dan \x02untuk menandainya sebagai tidak dimodifikasi (untuk sub(). Co

Atau Anda bisa melangkah melalui loop mengubah kandidat saat Anda pergi, dalam hal ini, referensi ulang dan "kurung" tidak diperlukan; tetapi melacak indeks karakter diperlukan.

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

Berikut ini cara lain, menggunakan gensubdan array splitdan \x01sebagai pembatas bidang (untuk split ) .. \ x02 menandai elemen array sebagai kandidat untuk penambahan aritmatika.

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" }'
Peter.O
sumber
Terima kasih! Dalam kode pertama Anda, (1) apa "\x01\\1\"\x02"artinya? Saya masih tidak mengerti \x01dan \x02. (2) seberapa berbedakah pengembalian $0oleh gensubdan $0sebagai argumen terakhir gensub?
Tim
@Tim. Nilai hex \x01dan \x02digunakan sebagai penanda substitusi. Nilai-nilai ini sangat tidak mungkin ada dalam file teks normal , sehingga mereka sama-sama "sangat" aman untuk digunakan (mis. Tidak mengalami bentrok dengan yang sudah ada sebelumnya) .. Mereka hanya label sementara .. Re $0=gensub(... $0).. lihat ini Link String-Manipulation Functions , tetapi dalam ringkasan: Ini (gensub) mengembalikan string yang dimodifikasi sebagai hasil dari fungsi dan string target asli tidak diubah. ... Yang $0=sederhana memodifikasi target semula ..
Peter.O
2

Karena solusi dalam (g) awk tampaknya menjadi sangat kompleks, saya ingin menambahkan solusi alternatif di Perl:

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

Penjelasan:

  • Opsi -wmemungkinkan peringatan (yang akan memperingatkan Anda tentang kemungkinan efek yang tidak diinginkan).
  • Opsi -pmenyiratkan loop di sekitar kode yang bekerja mirip dengan sed atau awk, menyimpan setiap baris input secara otomatis dalam variabel default $_,.
  • Opsi -ememberi tahu perl bahwa kode program mengikuti pada baris perintah, bukan dalam file skrip.
  • Kode adalah substitusi regex ( s/.../.../) pada $_, di mana urutan digit, jika diikuti oleh ", akan diganti oleh urutan, ditafsirkan sebagai angka dalam penambahan, ditambah 11.
  • The zero-width penegasan melihat-depan positif (?=pattern) penampilan untuk "tanpa mengambil ke pertandingan, jadi kami tidak perlu mengulanginya di penggantian. Variabel MATCH $&dalam penggantian kemudian hanya akan berisi angka.
  • The /epengubah untuk regex memberitahu perluntuk "mengeksekusi" penggantian sebagai kode bukannya mengambil sebagai string.
  • The /gpengubah membuat penggantian "global", mengulanginya pada setiap pertandingan di baris.

Variabel MATCH $&sayangnya akan merusak kinerja kode dalam versi Perl sebelum 5.20. Solusi yang lebih cepat (dan tidak jauh lebih rumit) akan menggunakan pengelompokan dan backreference $1sebagai gantinya:

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

Dan jika pernyataan pandangan ke depan terlihat terlalu membingungkan, Anda juga dapat mengganti tanda kutip secara eksplisit:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Dubu
sumber