Mendapatkan teks dari penanda terakhir ke EOF di POSIX.2

8

Saya memiliki teks dengan garis penanda seperti:

aaa
---
bbb
---
ccc

Saya perlu mendapatkan teks dari penanda terakhir (tidak inklusif) ke EOF. Dalam hal ini akan terjadi

ccc

Apakah ada cara yang elegan dalam POSIX.2? Saat ini saya menggunakan dua run: pertama dengan nldan grepuntuk kejadian terakhir dengan nomor baris masing-masing. Lalu saya mengekstrak nomor baris dan menggunakannya seduntuk mengekstrak potongan yang dimaksud.

Segmen teks mungkin cukup besar, jadi saya takut untuk menggunakan beberapa metode penambahan teks seperti kita menambahkan teks ke buffer, jika kita menemukan penanda kita mengosongkan buffer, sehingga pada EOF kita memiliki potongan terakhir di penyangga.

aikipooh
sumber

Jawaban:

6

Kecuali jika segmen Anda sangat besar (seperti pada: Anda benar-benar tidak dapat menyimpan banyak RAM, mungkin karena ini adalah sistem tertanam kecil yang mengendalikan sistem file besar), satu pass adalah pendekatan yang lebih baik. Bukan hanya karena itu akan lebih cepat, tetapi yang paling penting karena memungkinkan sumber untuk menjadi aliran, dari mana data membaca dan tidak disimpan hilang. Ini benar-benar pekerjaan untuk awk, meskipun sed bisa melakukannya juga.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

Jika Anda harus menggunakan pendekatan dua lintasan, tentukan offset garis pemisah terakhir dan cetak dari itu. Atau tentukan byte offset dan cetak dari itu.

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Tambahan: Jika Anda memiliki lebih dari POSIX, berikut ini adalah versi sekali jalan sederhana yang bergantung pada ekstensi umum untuk awk yang memungkinkan pemisah rekaman RSmenjadi ekspresi reguler (POSIX hanya mengizinkan satu karakter). Itu tidak sepenuhnya benar: jika file berakhir dengan pemisah rekaman, ia mencetak potongan sebelum pemisah rekaman terakhir, bukan catatan kosong. Versi kedua menggunakan RTmenghindari yang cacat, tetapi RTkhusus untuk GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'
Gilles 'SANGAT berhenti menjadi jahat'
sumber
@Gilles: sedberfungsi dengan baik, tapi saya tidak bisa mendapatkan awkcontoh untuk dijalankan; hang ... dan saya mendapatkan kesalahan pada contoh ke-3: cut -f ':' -t 1 ... cut: opsi tidak valid - 't'
Peter.O
@ fred.bear: Saya tidak tahu bagaimana itu terjadi - Saya menguji semua cuplikan saya, tetapi entah bagaimana mengacaukan edit post-copy-paste pada cutcontoh. Saya melihat tidak ada yang salah dengan awkcontoh, versi awk apa yang Anda gunakan, dan apa input pengujian Anda?
Gilles 'SANGAT berhenti menjadi jahat'
... sebenarnya awkversi ini berfungsi .. ini hanya memakan waktu yang sangat lama pada file besar .. sedversi memproses file yang sama dalam 0,470an .. Data pengujian saya sangat berbobot ... hanya dua potongan dengan satu-satunya '---' tiga baris dari akhir 1 juta baris ...
Peter.O
@Gilles .. (saya pikir saya harus berhenti menguji pada jam 3 pagi. Saya entah bagaimana menguji ketiga dari "dua lulus" awks sebagai satu unit :( ... Saya sekarang telah menguji masing-masing secara individual dan yang kedua sangat cepat pada 0,204 detik ... Howerver, output awk "dua-pass" pertama saja: " (input standar) " (-l tampaknya menjadi biang keladinya) ... seperti untuk awk "dua-pass" ketiga, saya tidak menghasilkan apa-apa ... tetapi "dua-pass" kedua adalah yang tercepat dari semua metode yang disajikan (POSIX atau yang lain
:)
@ fred.bear: Diperbaiki, dan diperbaiki. QA saya tidak terlalu bagus untuk cuplikan singkat ini - saya biasanya menyalin-menempelkan dari baris perintah, memformat, kemudian melihat bug, dan mencoba untuk memperbaiki inline daripada memformat ulang. Saya ingin tahu apakah menghitung karakter lebih efisien daripada menghitung garis (metode dua-pass kedua vs ketiga)
Gilles 'SO- stop being evil'
3

Strategi dua lulus tampaknya menjadi hal yang benar. Alih-alih sed saya akan menggunakan awk(1). Dua operan bisa terlihat seperti ini:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

untuk mendapatkan nomor baris. Dan kemudian gema semua teks mulai dari nomor baris itu dengan:

$ awk "NR>$LINE" file

Seharusnya ini tidak membutuhkan buffering yang berlebihan.

Mackie Messer
sumber
dan kemudian mereka dapat digabungkan:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
glenn jackman
Melihat bahwa saya sudah lama menguji kiriman lainnya, saya sekarang juga telah menguji cuplikan "glen jackman" di atas. Butuh 0,352 detik (dengan file data yang sama yang disebutkan dalam jawaban saya) ... Saya mulai mendapatkan pesan bahwa awk bisa lebih cepat dari yang semula saya pikir mungkin (saya pikir sed sama baiknya dengan yang didapatnya, tapi itu tampaknya menjadi kasus "kuda untuk kursus") ...
Peter.O
Sangat menarik untuk melihat semua skrip ini dipatok. Kerja bagus Fred.
Mackie Messer
Solusi tercepat menggunakan tac dan tail yang sebenarnya membaca file input mundur. Sekarang, jika saja orang awk bisa membaca file input mundur ...
Mackie Messer
3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

Nomor sedbaris ouputs pertama dari baris "---" ...
Yang kedua sedmengekstrak angka terakhir dari output sed pertama ...
Tambahkan 1 ke nomor itu untuk memulai blok "ccc" Anda ...
Yang ketiga output 'sed' dari awal blok "ccc" ke EOF

Perbarui (dengan info yang diubah metode Gilles)

Yah saya bertanya-tanya tentang bagaimana kinerja glenn jackman tac , jadi saya menguji waktu tiga jawaban (pada saat penulisan) ... File uji masing-masing berisi 1 juta baris (dari nomor baris mereka sendiri).
Semua jawaban melakukan apa yang diharapkan ...

Inilah saatnya ..


Gilles sed (single pass)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Gilles awk (single pass)

# very slow, but my data had a very large data block which awk needed to cache.

Gilles 'two-pass' (metode pertama)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Gilles 'two-pass' (metode kedua) ... sangat cepat

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Gilles 'two-pass' (metode ketiga)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Gilles 'gawk' (metode RT) ... sangat cepat , tetapi bukan POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

glenn jackman ... sangat cepat , tetapi tidak POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Mackie Messer

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s
Peter.O
sumber
Karena penasaran, versi dua pass saya mana yang Anda uji, dan versi awk apa yang Anda gunakan?
Gilles 'SO- berhenti bersikap jahat'
@Gilles: Saya menggunakan GNU Awk 3.1.6 (di Ubuntu 10.04 dengan 4 GB RAM). Semua tes memiliki 1 juta baris dalam "chunk" pertama, lalu "marker" diikuti oleh 2 "data" lines ... Butuh 15,540 detik untuk memproses file yang lebih kecil dari 100.000 baris, tetapi untuk 1.000.000 baris, saya jalankan sekarang, dan sudah lebih dari 25 menit sejauh ini. Ia menggunakan satu inti hingga 100% ... membunuhnya sekarang ... Berikut adalah beberapa tes tambahan lainnya: baris = 100000 (0m16.026s) - baris = 200000 (2m29.990s) - baris = 300000 (5m23. 393d) - baris = 400000 (11m9.938d)
Peter.O
Ups .. Dalam komentar saya di atas, saya melewatkan referensi awk "dua jalan" Anda. Detail di atas adalah untuk awk "single-pass" ... Versi awk benar ... Saya sudah membuat komentar lebih lanjut tentang versi "dua-pass" yang berbeda di bawah jawaban Anda (sebuah modifikasi hasil waktu di atas)
Peter.O
2

Gunakan " tac " yang menampilkan baris file dari ujung ke awal:

tac afile | awk '/---/ {exit} {print}' | tac
glenn jackman
sumber
tacbukan POSIX, itu Linux-spesifik (itu di GNU coreutils, dan di beberapa instalasi busybox).
Gilles 'SO- stop being evil'
0

Anda bisa menggunakannya ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

Cara kerjanya: tmenduplikasi baris saat ini ( .) - yang selalu merupakan baris terakhir saat eddimulai (kalau-kalau pembatas hadir di baris terakhir), 1,?===?dhapus semua baris hingga dan termasuk pertandingan sebelumnya ( edmasih di baris terakhir) ) kemudian $dmenghapus baris terakhir (duplikat), ,pmencetak buffer teks (ganti dengan wuntuk mengedit file pada tempatnya) dan akhirnya qberhenti ed.


Jika Anda tahu ada setidaknya satu pembatas di input (dan tidak peduli apakah itu juga dicetak) maka

sed 'H;/===/h;$!d;x' infile

akan menjadi lebih pendek.
Cara kerjanya: itu menambahkan semua baris ke Hbuffer lama, itu menimpa hbuffer lama ketika menemukan kecocokan, itu dmenghapus semua baris kecuali yang $t ketika xmengubah buffer (dan cetak otomatis).

don_crissti
sumber