cat line X ถึง line Y ในไฟล์ขนาดใหญ่


132

ว่าฉันมีแฟ้มข้อความขนาดใหญ่ (> 2GB) และฉันเพียงต้องการที่จะcatสายXไปY(เช่น 57890000-57890010)

จากสิ่งที่ฉันเข้าใจฉันสามารถทำสิ่งนี้ได้โดยการไพพ์headไปยังtailหรือ viceversa เช่น

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

หรืออีกวิธีหนึ่ง

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

ที่A, B, CและDสามารถคำนวณจากจำนวนบรรทัดในไฟล์และXY

แต่มีสองปัญหาด้วยวิธีนี้:

  1. คุณต้องคำนวณA, B, และCD
  2. คำสั่งสามารถpipeต่อบรรทัดอื่น ๆ ได้มากกว่าที่ฉันสนใจในการอ่าน (เช่นถ้าฉันอ่านเพียงไม่กี่บรรทัดที่อยู่ตรงกลางไฟล์ขนาดใหญ่)

มีวิธีที่จะให้เชลล์ใช้งานและส่งออกบรรทัดที่ฉันต้องการหรือไม่? (ในขณะที่ให้เท่านั้นXและY)?


1
FYI การเปรียบเทียบการทดสอบความเร็วจริงของ 6 วิธีที่เพิ่มลงในคำตอบของฉัน
เควิน

คำตอบ:


119

ฉันแนะนำsedวิธีแก้ปัญหา แต่เพื่อความสมบูรณ์

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

หากต้องการตัดออกหลังจากบรรทัดสุดท้าย:

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

ทดสอบความเร็ว:

  • สร้างไฟล์ 100,000,000 บรรทัด seq 100000000 > test.in
  • อ่านค่าบรรทัดได้ 50,000,000-50,000,010
  • ทดสอบในลำดับไม่เฉพาะ
  • realเวลาตามที่รายงานโดย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

สิ่งเหล่านี้ไม่ได้เป็นมาตรฐานที่แม่นยำ แต่ความแตกต่างนั้นชัดเจนและทำซ้ำได้มากพอที่จะให้ความรู้สึกที่ดีเกี่ยวกับความเร็วสัมพัทธ์ของคำสั่งแต่ละคำสั่งเหล่านี้

*: ยกเว้นระหว่างสองคนแรกsed -n p;qและhead|tailซึ่งดูเหมือนจะเป็นหลัก


11
จากความอยากรู้: คุณล้างแคชดิสก์ระหว่างการทดสอบอย่างไร
Paweł Rumian

2
สิ่งที่เกี่ยวกับtail -n +50000000 test.in | head -n10ซึ่งต่างจากtail -n-50000000 test.in | head -n10จะให้ผลลัพธ์ที่ถูกต้อง?
Gilles

4
ตกลงฉันไปแล้วก็ทำการวัดประสิทธิภาพ tail | head เร็วกว่า sed วิธีต่างกันมากเกินกว่าที่ฉันคาดไว้
Gilles

3
@Gilles คุณถูกต้องฉันไม่ดี tail+|headเร็วกว่า 10-15% เร็วกว่าฉันเพิ่มมาตรฐานแล้ว
เควิน

1
ฉันรู้ว่าคำถามนั้นถามถึงบรรทัด แต่ถ้าคุณใช้-cตัวละครเพื่อข้ามตัวอักษรนั้นtail+|headจะเกิดขึ้นทันที แน่นอนคุณไม่สามารถพูดว่า "50000000" และอาจต้องค้นหาด้วยตนเองเริ่มต้นของส่วนที่คุณกำลังมองหา
Danny Kirchmeier

51

หากคุณต้องการรวมบรรทัด X ถึง Y (เริ่มต้นที่หมายเลข 1) ให้ใช้

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

tailจะอ่านและยกเลิกบรรทัด X-1 บรรทัดแรก (ไม่มีวิธีแก้ไข) จากนั้นอ่านและพิมพ์บรรทัดต่อไปนี้ headจะอ่านและพิมพ์จำนวนบรรทัดที่ต้องการจากนั้นออก เมื่อheadออกจากtailรับสัญญาณSIGPIPEและตายดังนั้นมันจะไม่อ่านมากกว่าค่าของขนาดบัฟเฟอร์ (โดยทั่วไปไม่กี่กิโลไบต์) ของบรรทัดจากไฟล์อินพุต

หรืออีกวิธีหนึ่งตามที่แนะนำgorkyplใช้ sed:

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

วิธีการแก้ปัญหาช้าลงอย่างมีนัยสำคัญแม้ว่า (อย่างน้อยสำหรับยูทิลิตี้ GNU และยูทิลิตี้ Busybox; sed อาจจะมีการแข่งขันมากขึ้นถ้าคุณแตกไฟล์ส่วนใหญ่บนระบบปฏิบัติการที่ท่อช้าและช้าเร็ว) นี่คือมาตรฐานอย่างรวดเร็วภายใต้ Linux; ข้อมูลถูกสร้างขึ้นโดยseq 100000000 >/tmp/aสภาพแวดล้อมคือ Linux / amd64 /tmpเป็น tmpfs และเครื่องไม่ได้ใช้งานและไม่ได้ทำการแลกเปลี่ยน

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

หากคุณทราบช่วงไบต์ที่คุณต้องการคุณสามารถแยกมันได้เร็วขึ้นโดยการข้ามไปยังตำแหน่งเริ่มต้นโดยตรง แต่สำหรับบรรทัดคุณต้องอ่านตั้งแต่ต้นและนับจำนวนบรรทัดใหม่ หากต้องการแยกบล็อกจาก x ที่รวมอยู่ใน y แต่เพียงผู้เดียวเริ่มต้นที่ 0 ด้วยขนาดบล็อกเป็น b:

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

1
คุณแน่ใจหรือไม่ว่าไม่มีแคชในตัว? ความแตกต่างระหว่างหาง | หัวและ sed ดูใหญ่เกินไปสำหรับฉัน
Paweł Rumian

@gorkypl ฉันทำหลายมาตรการและเวลาก็เทียบเคียงได้ ตามที่ฉันเขียนสิ่งนี้เกิดขึ้นใน RAM (ทุกอย่างอยู่ในแคช)
Gilles

1
@Gilles tail will read and discard the first X-1 lineดูเหมือนว่าจะหลีกเลี่ยงเมื่อกำหนดจำนวนบรรทัดจากจุดสิ้นสุดในกรณีเช่นนี้ดูเหมือนว่าหางจะอ่านย้อนหลังจากปลายตามเวลาการดำเนินการ โปรดอ่าน: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra ใช่ถ้าอินพุตเป็นไฟล์ปกติการใช้งานบางอย่างของtail(รวมถึงหาง GNU) มีฮิวริสติกให้อ่านจากตอนท้าย ที่ปรับปรุงtail | headวิธีการแก้ปัญหาเมื่อเทียบกับวิธีอื่น ๆ
Gilles

22

head | tailวิธีการเป็นหนึ่งในดีที่สุดและวิธีการที่ "สำนวน" การทำเช่นนี้:

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

ตามที่ Gilles ชี้ให้เห็นในความคิดเห็นวิธีที่เร็วกว่าคือ

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

สาเหตุที่เร็วกว่านี้คือX-1บรรทัดแรกไม่จำเป็นต้องผ่านท่อเมื่อเปรียบเทียบกับhead | tailวิธีการ

คำถามของคุณที่ใช้ถ้อยคำเป็นสิ่งที่ทำให้เข้าใจผิดเล็กน้อยและอาจอธิบายถึงความวิตกกังวลที่ไม่มีมูลของคุณต่อแนวทางนี้

  • คุณบอกว่าคุณต้องคำนวณA, B, C, Dแต่เป็นคุณสามารถดูการนับสายของไฟล์ไม่จำเป็นและที่มากที่สุด 1 คำนวณเป็นสิ่งที่จำเป็นซึ่งเปลือกสามารถทำเพื่อคุณนะ

  • คุณกังวลว่าท่อจะอ่านบรรทัดมากกว่าที่จำเป็น อันที่จริงเรื่องนี้ไม่เป็นความจริง: tail | headเกี่ยวกับประสิทธิภาพเท่าที่คุณจะได้รับในแง่ของไฟล์ I / O ก่อนอื่นให้พิจารณาจำนวนงานขั้นต่ำที่จำเป็น: เพื่อหาบรรทัดที่Xในไฟล์วิธีเดียวที่จะทำคืออ่านทุกไบท์และหยุดเมื่อคุณนับสัญลักษณ์X ขึ้นบรรทัดใหม่เนื่องจากไม่มีวิธีที่จะทำนายไฟล์ ชดเชยของเส้นX ' เมื่อคุณไปถึงบรรทัด * X * th คุณจะต้องอ่านทุกบรรทัดเพื่อที่จะพิมพ์พวกเขาหยุดที่บรรทัดY ดังนั้นการเข้าใกล้จึงไม่สามารถอ่านได้น้อยกว่าเส้นY ตอนนี้head -n $Yอ่านไม่เกินYบรรทัด (ปัดเศษเป็นหน่วยบัฟเฟอร์ที่ใกล้ที่สุด แต่บัฟเฟอร์ถ้าใช้อย่างถูกต้องปรับปรุงประสิทธิภาพดังนั้นไม่จำเป็นต้องกังวลเกี่ยวกับค่าใช้จ่ายนั้น) นอกจากนี้tailจะไม่อ่านมากไปกว่าheadดังนั้นเราจึงแสดงให้เห็นว่าhead | tailอ่านจำนวนบรรทัดน้อยที่สุดเท่าที่จะเป็นไปได้ (อีกครั้งบวกกับการบัฟเฟอร์เล็กน้อยที่เราไม่สนใจ) ข้อได้เปรียบเชิงประสิทธิภาพเพียงอย่างเดียวของวิธีการใช้เครื่องมือแบบเดี่ยวที่ไม่ได้ใช้ไพพ์คือกระบวนการที่น้อยลง


1
ไม่เคยเห็นการเปลี่ยนเส้นทางไปก่อนในบรรทัดก่อน เท่มันทำให้การไหลของท่อชัดเจนขึ้น
2768 clacke

14

วิธีดั้งเดิมมากที่สุด ( แต่ไม่เร็วที่สุดเท่าที่สังเกตGillesด้านบน) sedจะใช้

ในกรณีของคุณ:

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

-nตัวเลือกหมายความว่าเพียงเส้นที่เกี่ยวข้องจะมีการพิมพ์ที่ stdout

พีในตอนท้ายของการตกแต่งหมายเลขบรรทัดหมายถึงสายการพิมพ์ในช่วงที่กำหนด Qในส่วนที่สองของสคริปต์ที่ช่วยประหยัดเวลาบางส่วนโดยการกระโดดข้ามที่เหลือของไฟล์


1
ฉันคาดหวังsedและtail | headจะใกล้เคียงกัน แต่กลับกลายtail | headเป็นว่าเร็วขึ้นมาก (ดูคำตอบของฉัน )
Gilles

1
ฉันไม่รู้จากสิ่งที่ฉันได้อ่านtail/ headถือเป็น "ออร์โธดอกซ์" มากกว่าเนื่องจากการตัดปลายทั้งสองด้านของไฟล์เป็นสิ่งที่พวกเขาทำขึ้นมาอย่างแม่นยำ ในวัสดุเหล่านั้นsedดูเหมือนว่าจะป้อนรูปภาพเมื่อจำเป็นต้องใช้การแทนที่เท่านั้นและจะถูกผลักออกจากภาพอย่างรวดเร็วเมื่อมีสิ่งใดที่ซับซ้อนกว่าเริ่มเกิดขึ้นเนื่องจากวากยสัมพันธ์สำหรับงานที่ซับซ้อนนั้นแย่กว่า AWK มากซึ่งจะเข้ามาแทนที่ .
underscore_d

7

หากเรารู้ช่วงที่จะเลือกจากบรรทัดแรก: lStartถึงบรรทัดสุดท้าย: lEndเราสามารถคำนวณ:

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

หากเราทราบจำนวนบรรทัดทั้งหมด: lAllเราสามารถคำนวณระยะทางจนถึงจุดสิ้นสุดของไฟล์:

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

จากนั้นเราจะได้รู้ว่า:

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

เลือกขนาดที่เล็กที่สุดของสิ่งเหล่าtailnumberนี้:

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

ช่วยให้เราใช้คำสั่งการดำเนินการที่เร็วที่สุดอย่างต่อเนื่อง:

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

โปรดสังเกตเครื่องหมายบวก ("+") เพิ่มเติมเมื่อ$linestartเลือก

ข้อแม้เดียวคือเราต้องนับจำนวนบรรทัดทั้งหมดและอาจต้องใช้เวลาเพิ่มเติมในการค้นหา
ตามปกติด้วย:

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

บางครั้งการวัดคือ:

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

โปรดทราบว่าเวลาเปลี่ยนไปอย่างมากหากบรรทัดที่เลือกอยู่ใกล้กับจุดเริ่มต้นหรือใกล้ถึงจุดสิ้นสุด คำสั่งที่ดูเหมือนว่าจะทำงานได้ดีที่ด้านหนึ่งของไฟล์อาจช้ามากที่ด้านอื่น ๆ ของไฟล์


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
terdon

@BinaryZebra - วิธีที่ดีกว่า
mikeserv

0

ฉันทำสิ่งนี้บ่อยพอและเขียนสคริปต์นี้ ฉันไม่ต้องการค้นหาหมายเลขบรรทัดสคริปต์ทำทุกอย่าง

#!/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
คุณกำลังตอบคำถามที่ไม่ได้ถาม คำตอบของคุณคือ 10% tail|headซึ่งได้รับการกล่าวถึงอย่างกว้างขวางในคำถามและคำตอบอื่น ๆ และการกำหนดหมายเลขบรรทัดที่ระบุสตริง / รูปแบบการปรากฏซึ่ง 90% ไม่ได้เป็นส่วนหนึ่งของคำถาม PS คุณควรอ้างอิงพารามิเตอร์และตัวแปรเชลล์ของคุณเสมอ เช่น "$ 3" และ "$ 4"
G-Man
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.