mèo dòng X đến dòng Y trên một tập tin lớn


132

Giả sử tôi có một tệp văn bản khổng lồ (> 2GB) và tôi chỉ muốn catcác dòng Xđến Y(ví dụ: 57890000 đến 57890010).

Từ những gì tôi hiểu, tôi có thể làm điều này bằng cách chuyển headvào tailhoặc ngược lại, tức là

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

Hay cách khác

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

nơi A, B, CDcó thể được tính từ số lượng dòng trong tập tin, XY.

Nhưng có hai vấn đề với cách tiếp cận này:

  1. Bạn cần phải tính toán A, B, CD.
  2. Các lệnh có thể pipecho nhau nhiều dòng hơn tôi muốn đọc (ví dụ: nếu tôi chỉ đọc một vài dòng ở giữa một tệp lớn)

Có cách nào để shell chỉ hoạt động và xuất ra các dòng tôi muốn không? (trong khi chỉ cung cấp XY)?


1
FYI, so sánh thử nghiệm tốc độ thực tế của 6 phương pháp được thêm vào câu trả lời của tôi.
Kevin

Câu trả lời:


119

Tôi đề nghị sedgiải pháp, nhưng để hoàn thiện,

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

Để cắt ra sau dòng cuối cùng:

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

Kiểm tra tốc độ:

  • Tập tin 100.000.000 được tạo bởi seq 100000000 > test.in
  • Đọc các dòng 50.000.000-50.000.010
  • Các xét nghiệm không theo thứ tự cụ thể
  • realthời gian như báo cáo bashcủa nội dungtime
 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

Đây không phải là điểm chuẩn chính xác, nhưng sự khác biệt là rõ ràng và có thể lặp lại đủ * để mang lại cảm giác tốt về tốc độ tương đối của mỗi lệnh này.

*: Ngoại trừ giữa hai cái đầu tiên, sed -n p;qhead|tail, về cơ bản là giống nhau.


11
Vì tò mò: làm thế nào bạn xóa bộ đệm đĩa giữa các lần kiểm tra?
Paweł Rumian

2
Điều gì về tail -n +50000000 test.in | head -n10, không giống như tail -n-50000000 test.in | head -n10sẽ cho kết quả chính xác?
Gilles

4
Ok, tôi đã đi và làm một số điểm chuẩn. đuôi | đầu nhanh hơn sed, sự khác biệt nhiều hơn tôi tưởng.
Gilles

3
@Gilles bạn nói đúng, xấu của tôi. tail+|headnhanh hơn 10-15% so với sed, tôi đã thêm điểm chuẩn đó.
Kevin

1
Tôi nhận ra rằng câu hỏi yêu cầu các dòng, nhưng nếu bạn sử dụng -cđể bỏ qua các ký tự, tail+|headlà tức thời. Tất nhiên, bạn không thể nói "50000000" và có thể phải tự tìm kiếm phần đầu của phần bạn đang tìm kiếm.
Daniel Kirchmeier

51

Nếu bạn muốn bao gồm các dòng X đến Y (bắt đầu đánh số từ 1), hãy sử dụng

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

tailsẽ đọc và loại bỏ các dòng X-1 đầu tiên (không có cách nào khác), sau đó đọc và in các dòng sau. headsẽ đọc và in số dòng yêu cầu, sau đó thoát. Khi headthoát, tailnhận tín hiệu SIGPIPE và chết, do đó, nó sẽ không đọc nhiều hơn giá trị của kích thước bộ đệm (thường là vài kilobyte) từ các tệp đầu vào.

Ngoài ra, như gorkypl đề xuất, hãy sử dụng sed:

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

Tuy nhiên, giải pháp sed chậm hơn đáng kể (ít nhất là đối với các tiện ích GNU và tiện ích Busybox; sed có thể cạnh tranh hơn nếu bạn trích xuất một phần lớn tệp trên HĐH nơi đường ống chậm và sed nhanh). Dưới đây là điểm chuẩn nhanh trong Linux; dữ liệu được tạo bởi seq 100000000 >/tmp/a, môi trường là Linux / amd64, /tmplà tmpfs và máy thì không hoạt động và không trao đổi.

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

Nếu bạn biết phạm vi byte bạn muốn làm việc, bạn có thể trích xuất nó nhanh hơn bằng cách bỏ qua trực tiếp đến vị trí bắt đầu. Nhưng đối với các dòng, bạn phải đọc từ đầu và đếm dòng mới. Để trích xuất các khối từ x bao gồm đến y độc quyền bắt đầu từ 0, với kích thước khối là b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
Bạn có chắc chắn rằng không có bộ nhớ đệm giữa? Sự khác biệt giữa đuôi | đầu và sed dường như quá lớn đối với tôi.
Paweł Rumian

@gorkypl Tôi đã làm một số biện pháp và thời gian là tương đương nhau. Như tôi đã viết, tất cả điều này xảy ra trong RAM (mọi thứ đều nằm trong bộ đệm).
Gilles

1
@Gilles tail will read and discard the first X-1 linedường như tránh được khi số dòng được đưa ra từ cuối, Trong trường hợp như vậy, đuôi dường như đọc ngược từ cuối theo thời gian thực hiện. Xin vui lòng đọc : http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Có, nếu đầu vào là một tệp thông thường, một số triển khai của tail(bao gồm cả đuôi GNU) có các heuristic để đọc từ cuối. Điều đó cải thiện tail | headgiải pháp so với các phương pháp khác.
Gilles

22

Cách head | tailtiếp cận là một trong những cách tốt nhất và "thành ngữ" nhất để làm điều này:

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

Như Gilles đã chỉ ra trong các bình luận, một cách nhanh hơn là

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

Lý do nhanh hơn là các dòng X - 1 đầu tiên không cần phải đi qua đường ống so với head | tailcách tiếp cận.

Câu hỏi của bạn như phrased là một chút sai lệch và có thể giải thích một số sai lầm vô căn cứ của bạn đối với phương pháp này.

  • Bạn nói rằng bạn phải tính toán A, B, C, Dnhưng khi bạn có thể thấy, số lượng dòng của tập tin không cần thiết và tối đa là 1 tính toán là cần thiết, mà vỏ có thể làm cho bạn anyways.

  • Bạn lo lắng rằng đường ống sẽ đọc nhiều dòng hơn mức cần thiết. Trong thực tế, điều này không đúng: tail | headhiệu quả như bạn có thể nhận được về mặt tệp I / O. Trước tiên, hãy xem xét số lượng công việc tối thiểu cần thiết: để tìm dòng thứ X trong một tệp, cách chung duy nhất để làm là đọc từng byte và dừng khi bạn đếm các ký hiệu dòng mới X vì không có cách nào để phân chia tệp bù của dòng X '. Khi bạn đạt đến dòng * X *, bạn phải đọc tất cả các dòng để in chúng, dừng lại ở dòng Y '. Do đó, không có cách tiếp cận nào có thể thoát khỏi việc đọc ít hơn dòng Y. Bây giờ, head -n $Yđọc không quá Ycác dòng (được làm tròn đến đơn vị bộ đệm gần nhất, nhưng bộ đệm nếu được sử dụng chính xác sẽ cải thiện hiệu suất, do đó không cần phải lo lắng về chi phí đó). Ngoài ra, tailsẽ không đọc nhiều hơn head, do đó chúng tôi đã chỉ ra rằng head | tailđọc số dòng ít nhất có thể (một lần nữa, cộng với một số bộ đệm không đáng kể mà chúng tôi đang bỏ qua). Lợi thế hiệu quả duy nhất của cách tiếp cận công cụ duy nhất không sử dụng đường ống là ít quy trình hơn (và do đó ít chi phí hơn).


1
Chưa bao giờ thấy chuyển hướng đi đầu tiên trên dòng trước. Mát mẻ, nó làm cho đường ống rõ ràng hơn.
clacke

14

Cách chính thống nhất (nhưng không phải là nhanh nhất, như Gilles đã lưu ý ở trên) sẽ là sử dụng sed.

Trong trường hợp của bạn:

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

Các -ntùy chọn có nghĩa là chỉ có các dòng có liên quan được in để stdout.

Các p vào cuối kết thúc số dòng có nghĩa là để in dòng trong phạm vi nhất định. Các q trong phần thứ hai của kịch bản tiết kiệm thời gian bằng cách bỏ qua phần còn lại của tập tin.


1
Tôi đã mong đợi sedtail | headsẽ ngang tầm, nhưng hóa ra tail | headlà nhanh hơn đáng kể (xem câu trả lời của tôi ).
Gilles

1
Tôi không biết, từ những gì tôi đã đọc, tail/ headđược coi là "chính thống" hơn, vì việc cắt xén một trong hai tập tin chính xác là những gì họ làm. Trong các tài liệu đó, seddường như chỉ nhập vào hình ảnh khi cần thay thế - và nhanh chóng bị đẩy ra khỏi hình ảnh khi bất kỳ điều gì phức tạp hơn bắt đầu xảy ra, vì cú pháp của nó cho các nhiệm vụ phức tạp tồi tệ hơn nhiều so với AWK, sau đó tiếp quản .
gạch dưới

7

Nếu chúng ta biết phạm vi cần chọn, từ dòng đầu tiên: lStartđến dòng cuối cùng: lEndchúng ta có thể tính toán:

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

Nếu chúng ta biết tổng số lượng dòng: lAllchúng ta cũng có thể tính khoảng cách đến cuối tệp:

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

Sau đó, chúng ta sẽ biết cả hai:

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

Chọn nhỏ nhất trong số đó: tailnumbernhư thế này:

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

Cho phép chúng tôi sử dụng lệnh thực thi nhanh nhất:

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

Vui lòng lưu ý dấu cộng ("+") bổ sung khi $linestartđược chọn.

Nhắc nhở duy nhất là chúng ta cần tổng số dòng và có thể mất thêm thời gian để tìm.
Như thường lệ với:

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

Một số lần đo là:

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

Lưu ý rằng thời gian thay đổi mạnh mẽ nếu các dòng được chọn ở gần đầu hoặc gần cuối. Một lệnh dường như hoạt động độc đáo ở một bên của tệp, có thể cực kỳ chậm ở phía bên kia của tệp.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
terdon

@BinaryZebra - cách tốt hơn.
mikeerv

0

Tôi làm điều này thường xuyên đủ và vì vậy đã viết kịch bản này. Tôi không cần tìm số dòng, kịch bản làm tất cả.

#!/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

2
Bạn đang trả lời một câu hỏi không được hỏi. Câu trả lời của bạn là 10% tail|head, đã được thảo luận rộng rãi trong câu hỏi và các câu trả lời khác, và 90% xác định số dòng nơi chuỗi / mẫu được chỉ định xuất hiện, không phải là một phần của câu hỏi . PS bạn nên luôn luôn trích dẫn các tham số và biến shell của bạn; ví dụ: "$ 3" và "$ 4".
G-Man
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.