Bagaimana cara mendapatkan beberapa baris dari file dengan regex?

10

Bagaimana cara mendapatkan beberapa baris dari file dengan regex?

Saya sering ingin mendapatkan beberapa baris / memodifikasi beberapa baris dengan regex. Contoh kasus:

Saya mencoba membaca bagian dari file XML / SGML (mereka belum tentu terbentuk dengan baik atau dalam sintaksis yang dapat diprediksi, sehingga regex akan lebih aman daripada parser yang tepat. Selain itu saya ingin dapat melakukan ini juga sedikit pun sepenuhnya file tidak terstruktur di mana hanya beberapa kata kunci yang dikenal.) dalam skrip shell (berjalan pada Solaris dan Linux).

XML Contoh:

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

Dari sini saya ingin membaca <tag1>jika berisi foosuatu tempat di dalamnya.

Regex seperti (<tag1>.*?foo.*?</tag1>)harus memberikan bagian yang tepat tetapi alat suka grepdan sedhanya bekerja untuk saya dengan garis tunggal. Bagaimana saya bisa dapatkan

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

dalam contoh ini?

Sarang
sumber
3
Tautan wajib
evilsoup
@ evilsoup Itu benar, tetapi pertanyaan saya tidak secara khusus tentang file XML / SGML, hanya tentang file teks.
Den

Jawaban:

7

Jika Anda telah menginstal GNU grep, Anda bisa melakukan pencarian multiline dengan mengirimkan -Pflag (perl-regex) dan mengaktifkannya PCRE_DOTALLdengan(?s)

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

Jika hal di atas tidak berfungsi pada platform Anda, coba lewati -zbendera sebagai tambahan, ini memaksa grep untuk memperlakukan NUL sebagai pemisah baris, menyebabkan seluruh file terlihat seperti satu baris.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
iruvar
sumber
Ini tidak memberikan output pada sistem saya ketika dijalankan pada file contoh OP.
terdon
Bekerja untukku. +1. Terima kasih atas (?s)tipnya
Nathan Wallace
@terdon, versi GNU grep apa yang Anda jalankan?
iruvar
@ 1_CR (GNU grep) 2.14pada Debian. Saya menyalin contoh OPs apa adanya (hanya menambahkan baris terakhir) dan menjalankannya greptetapi tidak mendapatkan hasil.
terdon
1
@slm, saya di pcre 6.6, GNU grep 2.5.1 di RHEL. Apakah Anda keberatan mencoba grep -ozPalih-alih grep -oPdi platform Anda?
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

Jika Anda melakukan hal di atas, mengingat data yang Anda perlihatkan, sebelum garis pembersihan terakhir di sana, Anda harus bekerja dengan sedruang pola yang terlihat seperti:

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

Anda dapat mencetak ruang pola Anda kapan pun Anda suka dengan look. Anda kemudian dapat mengatasi \nkarakter.

sed l <file

Akan menunjukkan kepada Anda setiap baris sedmemprosesnya pada tahap yang ldisebut.

Jadi saya baru saja mengujinya dan perlu satu lagi \backslashsetelah ,commadi baris pertama, tetapi jika tidak berfungsi sebagaimana mestinya. Di sini saya memasukkannya ke dalam _sed_functionsehingga saya dapat dengan mudah menyebutnya untuk tujuan demonstrasi di seluruh jawaban ini: (berfungsi dengan komentar yang disertakan, tetapi di sini dihapus karena singkatnya)

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

Sekarang kita akan mengganti pa lsehingga kita dapat melihat apa yang sedang kita kerjakan saat kita mengembangkan skrip kita dan menghapus demo non-op s?sehingga baris terakhir dari kita sed 3<<\SCRIPTterlihat seperti:

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

Maka saya akan menjalankannya lagi:

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

Baik! Jadi saya benar - itu perasaan yang bagus. Sekarang, mari kita mengocok look kita untuk melihat garis yang menarik tetapi menghapus. Kami akan menghapus arus kami ldan menambahkannya !{block}sehingga terlihat seperti:

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

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

Seperti apa itu sebelum kita memusnahkannya.

Satu hal terakhir yang ingin saya tunjukkan kepada Anda adalah Hruang lama saat kami membangunnya. Ada beberapa konsep kunci yang saya harap bisa saya tunjukkan. Jadi saya menghapus look terakhir lagi dan mengubah baris pertama untuk menambahkan mengintip ke Hruang lama di akhir:

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

Hruang lama bertahan siklus garis - karenanya namanya. Jadi yang sering membuat orang tersandung - ok, yang sering membuat saya tersandung - adalah perlu dihapus setelah Anda menggunakannya. Dalam hal ini saya hanya xmengubah sekali, jadi ruang penahanan menjadi pola ruang dan sebaliknya dan perubahan ini juga bertahan siklus garis.

Efeknya adalah bahwa saya perlu menghapus ruang palka saya yang dulunya ruang pola saya. Saya melakukan ini dengan terlebih dahulu membersihkan ruang pola saat ini dengan:

s/.*//

Yang cukup memilih setiap karakter dan menghapusnya. Saya tidak dapat menggunakan dkarena ini akan mengakhiri siklus baris saya saat ini dan perintah berikutnya tidak akan selesai, yang akan cukup banyak membuang skrip saya.

h

Ini bekerja dengan cara yang mirip dengan Htetapi itu menimpa ruang penyimpanan, jadi saya baru saja menyalin ruang pola kosong saya di atas ruang penyimpanan saya, secara efektif menghapusnya. Sekarang saya bisa:

b

di luar.

Dan itulah cara saya menulis sedskrip.

mikeserv
sumber
Terima kasih @slm! Kamu pria yang sangat baik, kamu tahu itu?
mikeserv
Terima kasih, kerja bagus, naik cepat ke 3k, selanjutnya 5k 8-)
slm
Saya tidak tahu, @slm. Saya mulai melihat saya belajar semakin sedikit di sini - mungkin ive tumbuh lebih besar manfaatnya. Saya harus memikirkannya. ive bahkan nyaris tidak datang ke situs beberapa minggu terakhir.
mikeserv
Setidaknya mencapai 10rb. Semuanya yang layak dibuka adalah pada level itu. Terus memotong, 5k akan datang cukup cepat sekarang.
slm
1
Yah, @slm - Anda adalah jenis yang langka. Saya setuju tentang beberapa jawaban. Itulah mengapa itu mengganggu saya ketika beberapa qs ditutup. Tapi itu jarang terjadi, sebenarnya. Terima kasih lagi, slm.
mikeserv
2

Jawaban @ jamespfinn akan bekerja dengan baik jika file Anda sesederhana contoh Anda. Jika Anda memiliki situasi yang lebih kompleks di mana <tag1>dapat menjangkau lebih dari 2 baris, Anda akan memerlukan trik yang sedikit lebih rumit. Sebagai contoh:

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

Script perl akan memproses setiap baris file input Anda dan

  • if(/<tag1>/){$a=1;}: variabel $adiatur ke 1jika tag pembuka ( <tag1>) ditemukan.

  • if($a==1){push @l,$_}: untuk setiap baris, jika $aada 1, tambahkan baris itu ke array @l.

  • if(/<\/tag1>/) : jika baris saat ini cocok dengan tag penutup:

    • if(grep {/foo/} @l){print "@l"}: jika ada garis yang disimpan dalam array @l(ini adalah garis antara <tag1>dan </tag1>) cocok dengan string foo, cetak konten @l.
    • $a=0; @l=(): kosongkan daftar ( @l=()) dan atur $akembali ke 0.
terdon
sumber
Ini berfungsi baik kecuali dalam kasus di mana ada lebih dari satu <tag1> yang mengandung "foo". Dalam hal ini, ia mencetak setiap hal dari awal <tag1> pertama hingga akhir yang terakhir </tag1> ...
Den
@den saya mengujinya dengan contoh yang ditunjukkan dalam jawaban saya yang berisi 3 <tag1>dengan foodan berfungsi dengan baik. Kapan itu gagal untuk Anda?
terdon
rasanya salah parsing xml menggunakan regex :)
Braiam
1

Inilah sedalternatifnya:

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

Penjelasan

  • -n berarti tidak mencetak garis kecuali diperintahkan.
  • /<tag1/ pertama cocok dengan tag pembuka
  • :x adalah label untuk mengaktifkan lompatan ke titik ini nanti
  • N menambahkan baris berikutnya ke ruang pola (buffer aktif).
  • /<\/tag1/!b xberarti jika ruang pola saat ini tidak berisi tag penutup, cabang ke xlabel yang dibuat sebelumnya. Dengan demikian, kami terus menambahkan garis ke ruang pola hingga kami menemukan tag penutup kami.
  • /foo/pberarti jika ruang pola saat ini cocok foo, itu harus dicetak.
Joseph R.
sumber
1

Anda bisa melakukannya dengan GNU awk saya pikir, dengan memperlakukan tag akhir sebagai pemisah rekaman misalnya untuk tag akhir yang dikenal </tag1>:

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

atau lebih umum (dengan regex untuk tag akhir)

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

Mengujinya di @ 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>
Steeldriver
sumber
0

Jika file Anda terstruktur persis seperti yang Anda tunjukkan di atas, Anda dapat menggunakan flag -A (baris setelah) & -B (baris sebelumnya) untuk grep ... misalnya:

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

Jika versi grepdukungan Anda, Anda juga bisa menggunakan opsi yang lebih sederhana -C(untuk konteks) yang mencetak garis N di sekitarnya:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
jamespfinn
sumber
Terima kasih, tapi tidak. Ini hanya sebuah contoh dan barang-barang nyata terlihat sangat tidak terduga ;-)
Den
1
Itu tidak menemukan tag dengan foo di dalamnya, itu hanya menemukan foo dan menampilkan garis konteks
Nathan Wallace
@NathanWallace ya, yang persis apa yang diminta OP, jawaban ini berfungsi dengan baik dalam kasus yang diberikan dalam pertanyaan.
terdon
@terdon itu sama sekali bukan pertanyaannya. Quote: "Saya ingin membaca <tag1> jika mengandung foo di suatu tempat di dalamnya." Solusi ini seperti "Saya ingin membaca 'foo' dan 1 baris konteks terlepas dari di mana 'foo' muncul". Mengikuti logika Anda, jawaban yang sama validnya untuk pertanyaan ini adalah tail -3 input_file.xml. Ya itu berfungsi untuk contoh khusus ini, tetapi itu bukan jawaban yang membantu untuk pertanyaan itu.
Nathan Wallace
@NathanWallace maksud saya adalah bahwa OP secara khusus menyatakan ini bukan format XML yang valid, dalam hal itu, bisa saja cukup untuk mencetak garis N di sekitar string yang dicari OP. Dengan informasi yang tersedia, jawaban ini cukup layak.
terdon