cat line X ke line Y pada file besar

132

Katakanlah saya memiliki file teks besar (> 2GB) dan saya hanya ingin catbaris Xke Y(misalnya 57890000 hingga 57890010).

Dari apa yang saya mengerti saya bisa melakukan ini dengan menyalurkan headke tailatau sebaliknya, yaitu

head -A /path/to/file | tail -B

atau sebagai alternatif

tail -C /path/to/file | head -D

di mana A, B, Cdan Ddapat dihitung dari jumlah baris dalam file, Xdan Y.

Tetapi ada dua masalah dengan pendekatan ini:

  1. Anda harus menghitung A, B, Cdan D.
  2. Perintah dapat pipeuntuk satu sama lain lebih banyak baris daripada yang saya tertarik untuk membaca (misalnya jika saya membaca hanya beberapa baris di tengah file besar)

Apakah ada cara agar shell berfungsi dengan baik dan menampilkan garis yang saya inginkan? (sambil hanya menyediakan Xdan Y)?

Amelio Vazquez-Reina
sumber
1
FYI, perbandingan tes kecepatan aktual dari 6 metode ditambahkan ke jawaban saya.
Kevin

Jawaban:

119

Saya menyarankan sedsolusinya, tetapi demi kelengkapan,

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Untuk memotong setelah baris terakhir:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Tes kecepatan:

  • 100.000.000 baris file dihasilkan oleh seq 100000000 > test.in
  • Garis bacaan 50.000.000-50.000.010
  • Tes tanpa urutan tertentu
  • realwaktu seperti yang dilaporkan oleh bashbuiltintime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

Ini sama sekali bukan tolok ukur yang tepat, tetapi perbedaannya jelas dan cukup berulang * untuk memberikan pemahaman yang baik tentang kecepatan relatif dari masing-masing perintah ini.

*: Kecuali di antara dua yang pertama, sed -n p;qdan head|tail, yang pada dasarnya tampak sama.

Kevin
sumber
11
Karena penasaran: bagaimana Anda mem-flush cache disk di antara tes?
Paweł Rumian
2
Bagaimana tail -n +50000000 test.in | head -n10, yang tidak seperti itu tail -n-50000000 test.in | head -n10akan memberikan hasil yang benar?
Gilles
4
Ok, saya pergi dan melakukan beberapa tolok ukur. ekor | kepala jauh lebih cepat daripada sed, perbedaannya jauh lebih banyak daripada yang saya harapkan.
Gilles
3
@Gilles kau benar, salahku. tail+|headlebih cepat 10-15% dari sed, saya telah menambahkan patokan itu.
Kevin
1
Saya menyadari bahwa pertanyaannya meminta baris, tetapi jika Anda menggunakan -cuntuk melewati karakter, tail+|headitu instan. Tentu saja, Anda tidak dapat mengatakan "50000000" dan mungkin harus mencari secara manual bagian awal yang Anda cari.
Danny Kirchmeier
51

Jika Anda ingin garis X ke Y inklusif (mulai penomoran pada 1), gunakan

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailakan membaca dan membuang baris X-1 pertama (tidak ada jalan lain untuk itu), kemudian membaca dan mencetak baris berikut. headakan membaca dan mencetak jumlah baris yang diminta, kemudian keluar. Ketika headkeluar, tailmenerima sinyal SIGPIPE dan mati, sehingga tidak akan membaca lebih dari nilai ukuran buffer (biasanya beberapa kilobyte) dari baris dari file input.

Atau, seperti yang disarankan gorkypl , gunakan sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

Solusi sed secara signifikan lebih lambat (setidaknya untuk utilitas GNU dan utilitas Busybox; sed mungkin lebih kompetitif jika Anda mengekstrak sebagian besar file pada OS di mana pemipaannya lambat dan sednya cepat). Berikut adalah tolok ukur cepat di Linux; data yang dihasilkan oleh seq 100000000 >/tmp/a, lingkungannya adalah Linux / amd64, /tmpadalah tmpfs dan mesinnya idle dan tidak bertukar.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Jika Anda tahu rentang byte yang ingin Anda gunakan, Anda bisa mengekstraknya lebih cepat dengan melompati langsung ke posisi awal. Tetapi untuk baris, Anda harus membaca dari awal dan menghitung baris baru. Untuk mengekstrak blok dari x inklusif ke y mulai dari 0, dengan ukuran blok b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file
Gilles
sumber
1
Apakah Anda yakin tidak ada caching di antara? Perbedaan antara ekor | kepala dan sed tampaknya terlalu besar bagi saya.
Paweł Rumian
@ Gorkypl Saya melakukan beberapa langkah dan waktunya sebanding. Seperti yang saya tulis, ini semua terjadi di RAM (semuanya ada di cache).
Gilles
1
@Gilles tail will read and discard the first X-1 linetampaknya harus dihindari ketika jumlah baris diberikan dari akhir, Dalam kasus seperti itu, ekor tampaknya membaca mundur dari ujung sesuai dengan waktu pelaksanaan. Silakan baca: http://unix.stackexchange.com/a/216614/79743.
1
@ BinZebra Ya, jika input adalah file biasa, beberapa implementasi dari tail(termasuk ekor GNU) memiliki heuristik untuk dibaca dari akhir. Itu meningkatkan tail | headsolusi dibandingkan dengan metode lain.
Gilles
22

The head | tailpendekatan adalah salah satu yang terbaik dan paling "idiomatik" cara untuk melakukan ini:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Seperti yang ditunjukkan oleh Gilles dalam komentar, cara yang lebih cepat adalah

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

Alasan ini lebih cepat adalah X - 1 baris pertama tidak perlu melalui pipa dibandingkan dengan head | tailpendekatan.

Pertanyaan Anda sebagai ungkapan agak menyesatkan dan mungkin menjelaskan beberapa kekhawatiran Anda yang tidak berdasar terhadap pendekatan ini.

  • Anda mengatakan Anda harus menghitung A, B, C, Dtapi seperti yang Anda lihat, jumlah baris dari file tidak diperlukan dan paling banyak 1 perhitungan diperlukan, yang shell dapat melakukannya untuk Anda anyways.

  • Anda khawatir perpipaan akan membaca lebih banyak baris daripada yang diperlukan. Sebenarnya ini tidak benar: tail | headadalah tentang seefisien yang Anda dapatkan dari segi file I / O. Pertama, pertimbangkan jumlah minimum pekerjaan yang diperlukan: untuk menemukan baris ke - X dalam file, satu-satunya cara umum untuk melakukannya adalah membaca setiap byte dan berhenti ketika Anda menghitung simbol baris baru X karena tidak ada cara untuk membuat ilahi file offset dari garis X '. Setelah Anda mencapai garis * X * th, Anda harus membaca semua baris untuk mencetaknya, berhenti di baris Y '. Dengan demikian tidak ada pendekatan yang bisa lolos dengan membaca kurang dari Y baris. Sekarang, head -n $Ybaca tidak lebih dari Ygaris (dibulatkan ke unit penyangga terdekat, tetapi buffer jika digunakan dengan benar meningkatkan kinerja, jadi tidak perlu khawatir tentang overhead itu). Selain itu, tailtidak akan membaca lebih dari head, jadi dengan demikian kami telah menunjukkan bahwa head | tailmembaca jumlah baris paling sedikit mungkin (sekali lagi, ditambah beberapa buffering diabaikan yang kita abaikan). Satu-satunya keuntungan efisiensi dari pendekatan alat tunggal yang tidak menggunakan pipa adalah lebih sedikit proses (dan dengan demikian lebih sedikit overhead).

jw013
sumber
1
Belum pernah melihat pengalihan pertama di telepon sebelumnya. Keren, itu membuat aliran pipa lebih jernih.
klak
14

Cara yang paling ortodoks (tetapi bukan yang tercepat, seperti dicatat oleh Gilles di atas) adalah menggunakan sed.

Dalam kasus Anda:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

The -npilihan menyiratkan bahwa hanya pada baris yang bersangkutan dicetak ke stdout.

The p pada akhir finishing nomor baris berarti untuk mencetak baris dalam kisaran yang diberikan. The q di bagian kedua dari script menghemat waktu dengan melompati sisa file.

Paweł Rumian
sumber
1
Saya berharap seddan tail | headkira-kira setara, tetapi ternyata tail | headsecara signifikan lebih cepat (lihat jawaban saya ).
Gilles
1
Aku tak tahu, dari apa yang saya baca, tail/ headdianggap lebih "ortodoks", karena pemangkasan kedua ujung file justru apa yang mereka dibuat untuk. Dalam bahan-bahan itu, sedhanya tampak memasuki gambar ketika diperlukan penggantian - dan dengan cepat didorong keluar dari gambar ketika sesuatu yang jauh lebih kompleks mulai terjadi, karena sintaksnya untuk tugas-tugas kompleks jauh lebih buruk daripada AWK, yang kemudian mengambil alih .
underscore_d
7

Jika kita tahu rentang untuk dipilih, dari baris pertama: lStartke baris terakhir: lEndkita bisa menghitung:

lCount="$((lEnd-lStart+1))"

Jika kita tahu jumlah total baris: lAllkita juga bisa menghitung jarak ke akhir file:

toEnd="$((lAll-lStart+1))"

Maka kita akan tahu keduanya:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Memilih yang terkecil dari semua itu: tailnumberkarena ini:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Mengizinkan kami menggunakan perintah eksekusi tercepat yang konsisten:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Harap perhatikan tanda tambah tambah ("+") saat $linestartdipilih.

Satu-satunya peringatan adalah bahwa kita memerlukan jumlah total garis, dan itu mungkin memerlukan waktu tambahan untuk menemukannya.
Seperti biasa dengan:

linesall="$(wc -l < "$thefile" )"

Beberapa waktu yang diukur adalah:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Perhatikan bahwa waktu berubah secara drastis jika garis yang dipilih mendekati awal atau dekat akhir. Perintah yang tampaknya berfungsi dengan baik di satu sisi file, mungkin sangat lambat di sisi lain file.


sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
terdon
@ Binzebra - jauh lebih baik.
mikeserv
0

Saya cukup sering melakukan ini dan menulis skrip ini. Saya tidak perlu menemukan nomor baris, skrip melakukan semuanya.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4
Doolan
sumber
2
Anda menjawab pertanyaan yang tidak diajukan. Jawaban Anda adalah 10% tail|head, yang telah dibahas secara luas dalam pertanyaan dan jawaban lainnya, dan 90% menentukan nomor baris tempat string / pola tertentu muncul, yang bukan bagian dari pertanyaan . PS Anda harus selalu mengutip parameter dan variabel shell Anda; mis., "$ 3" dan "$ 4".
G-Man