วิธี grep-inverse-match และแยกบรรทัด“ before” และ“ after”


26

พิจารณาไฟล์ข้อความด้วยรายการต่อไปนี้:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

ด้วยรูปแบบ (เช่นfff) ฉันต้องการ grep ไฟล์ด้านบนเพื่อให้ได้ผลลัพธ์:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

ตัวอย่างเช่นถ้าB = 2และA = 1เอาต์พุตที่มี pattern = fffควรเป็น:

aaa
bbb
ccc
hhh
iii

ฉันจะทำสิ่งนี้กับ grep หรือเครื่องมือบรรทัดคำสั่งอื่น ๆ ได้อย่างไร


หมายเหตุเมื่อฉันลอง:

grep -v 'fff'  -A1 -B2 file.txt

ฉันไม่ได้สิ่งที่ฉันต้องการ ฉันได้รับ:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii

คำตอบ:


9

ดอนอาจจะดีกว่าในกรณีส่วนใหญ่ แต่เพียงในกรณีที่ไฟล์นั้นจริงๆขนาดใหญ่และคุณไม่ได้รับsedการจัดการไฟล์สคริปต์ที่มีขนาดใหญ่(ซึ่งสามารถเกิดขึ้นได้ที่ประมาณ 5000 + สายของสคริปต์) , ที่นี่มันเป็นธรรมดาsed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

นี่คือตัวอย่างของสิ่งที่เรียกว่าหน้าต่างบานเลื่อนบนอินพุต มันทำงานได้โดยการสร้างบัฟเฟอร์การค้นหาล่วงหน้าของ$B-count บรรทัดก่อนที่จะพยายามพิมพ์อะไร

และที่จริงแล้วฉันควรจะชี้แจงประเด็นก่อนหน้านี้: ตัว จำกัด ประสิทธิภาพหลักสำหรับทั้งโซลูชันนี้และของ don จะเกี่ยวข้องโดยตรงกับช่วงเวลา การแก้ปัญหานี้จะชะลอตัวกับช่วงเวลาที่มีขนาดใหญ่ขนาดในขณะที่ดอนจะชะลอตัวที่มีขนาดใหญ่ช่วงความถี่ กล่าวอีกนัยหนึ่งถึงแม้ว่าไฟล์อินพุตมีขนาดใหญ่มากหากช่วงเวลาที่เกิดขึ้นจริงยังคงไม่บ่อยนักโซลูชันของเขาอาจเป็นวิธีที่จะไป อย่างไรก็ตามหากขนาดช่วงเวลานั้นค่อนข้างจัดการได้และมีแนวโน้มที่จะเกิดขึ้นบ่อยนี่เป็นวิธีแก้ปัญหาที่คุณควรเลือก

ดังนั้นนี่คือขั้นตอนการทำงาน:

  • หาก$matchพบในพื้นที่รูปแบบนำหน้าด้วย\newline sedจะลบซ้ำDทุก\newline ที่นำหน้า
    • ฉันล้าง$matchพื้นที่รูปแบบออกหมดก่อน - แต่เพื่อจัดการกับการทับซ้อนกันได้ง่ายการทิ้งแลนด์มาร์กไว้ดูเหมือนจะดีกว่ามาก
    • ฉันยังพยายามที่s/.*\n.*\($match\)/\1/จะลองมันในครั้งเดียวและหลบห่วง แต่เมื่อ$A/$Bมีขนาดใหญ่Dวง elete พิสูจน์ได้เร็วขึ้นมาก
  • แล้วเราดึงในNสายต่อของการป้อนข้อมูลนำหน้าด้วย\nตัวคั่น ewline และลองอีกครั้งDหนึ่งคำ/\n.*$match/อีกครั้งโดยอ้างถึงใช้มากที่สุดเมื่อเร็ว ๆ นี้การแสดงออกของเราอย่างสม่ำเสมอ w ///
  • หากการเว้นวรรครูปแบบตรงกัน$matchก็สามารถทำได้$matchที่ส่วนหัวของบรรทัด - ทุก$Bบรรทัด efore ได้รับการล้าง
    • ดังนั้นเราจึงเริ่มวนรอบ$After
    • การทำงานของวงนี้แต่ละคนเราจะพยายามs///ubstitute สำหรับ&ตัวเอง$ATH \nตัวอักษร ewline ในพื้นที่รูปแบบและหากประสบความสำเร็จtคือจะสาขาเรา - และเราทั้ง$Aกันชนแจก - ออกของสคริปต์ทั้งหมดจะเริ่มต้นสคริปต์ที่มาจากด้านบน ด้วยอินพุตบรรทัดถัดไปหากมี
    • หากtEST ไม่ประสบความสำเร็จเราจะbกลับไปที่:tฉลาก op และเรียกเก็บเงินจากอินพุตอื่น - อาจเริ่มวนซ้ำหาก$matchเกิดขึ้นขณะรวบรวม$After
  • หากเราได้รับที่ผ่านมา$matchวงฟังก์ชั่นแล้วเราจะพยายามที่จะprint $บรรทัดสุดท้ายว่านี้มันเป็นและถ้า!ไม่พยายามที่จะs///ubstitute สำหรับ&ตัวเอง$BTH \nตัวอักษร ewline ในพื้นที่รูปแบบ
    • เราจะทำtสิ่งนี้เช่นกันและหากประสบความสำเร็จเราจะแยกสาขาไปที่:Pป้ายกำกับ
    • ถ้าไม่ใช่เราจะแยกสาขากลับไปที่:top และรับสายอินพุตอื่นต่อท้ายบัฟเฟอร์
  • ถ้าเราทำให้มันไป:Print เราจะPrint แล้วDหนึ่งคำขึ้นไปครั้งแรก\newline ในพื้นที่รูปแบบและรันสคริปต์จากด้านบนกับสิ่งที่ยังคง

ดังนั้นเวลานี้ถ้าเราทำ A=2 B=2 match=5; seq 5 | sed...

พื้นที่รูปแบบสำหรับการทำซ้ำครั้งแรกที่:Print จะมีลักษณะดังนี้:

^1\n2\n3$

และนั่นคือวิธีsedรวบรวม$Bบัฟเฟอร์ efore และsedพิมพ์ไปยัง$Bบรรทัดเอาต์พุต - นับบรรทัดด้านหลังอินพุตที่รวบรวมไว้ ซึ่งหมายความว่าตามตัวอย่างก่อนหน้าของเราsedจะPrint 1to output แล้วลบออกDและส่งกลับไปด้านบนสุดของสคริปต์เพื่อกำหนดพื้นที่รูปแบบซึ่งมีลักษณะดังนี้:

^2\n3$

... และที่ด้านบนสุดของสคริปต์Nบรรทัดอินพุตอินพุทจะถูกดึงออกมาและการทำซ้ำครั้งถัดไปจะมีลักษณะดังนี้:

^2\n3\n4$

ดังนั้นเมื่อเราพบสิ่งแรกที่เกิดขึ้น5ในอินพุทพื้นที่รูปแบบจะมีลักษณะดังนี้:

^3\n4\n5$

จากนั้นDวง elete ก็จะเตะเข้าและเมื่อผ่านไปมันจะดูเหมือน:

^5$

และเมื่อNดึงสายสัญญาณเข้า ext จะsedกระทบ EOF และออก เมื่อถึงเวลานั้นมันจะมีเพียงPสาย rinted 1 และ 2 เท่านั้น

นี่คือตัวอย่างการเรียกใช้:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

ภาพพิมพ์นั้น:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100

ฉันทำงานกับไฟล์ขนาดใหญ่จริง ๆ และคำตอบของดอนก็ช้ากว่าโซลูชันนี้อย่างเห็นได้ชัด ตอนแรกฉันลังเลที่จะเปลี่ยนคำตอบที่ฉันยอมรับ แต่ความแตกต่างความเร็วค่อนข้างชัดเจน
Amelio Vazquez-Reina

4
@Amelio - สิ่งนี้จะใช้ได้กับสตรีมทุกขนาดและไม่จำเป็นต้องอ่านไฟล์เพื่อใช้งาน ปัจจัยที่มีผลการดำเนินงานที่ใหญ่ที่สุดคือขนาดของและ$A / หรือ $Bยิ่งคุณสร้างตัวเลขเหล่านั้นมากเท่าไหร่ก็จะยิ่งช้าลงเท่านั้น แต่คุณสามารถทำให้ตัวเลขมีขนาดใหญ่ขึ้นอย่างสมเหตุสมผล
mikeserv

1
@ AmelioVazquez-Reina - ถ้าคุณใช้อันที่เก่ากว่านี้ดีกว่าฉันคิดว่า
mikeserv

11

คุณสามารถใช้gnu grepกับ-Aและ-Bเพื่อพิมพ์ส่วนต่าง ๆ ของไฟล์ที่คุณต้องการยกเว้น แต่เพิ่ม-nสวิตช์เพื่อพิมพ์หมายเลขบรรทัดแล้วจัดรูปแบบเอาต์พุตและส่งเป็นสคริปต์คำสั่งsedเพื่อลบบรรทัดเหล่านั้น:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

สิ่งนี้ควรทำงานกับไฟล์รูปแบบที่ส่งไปยังgrepผ่าน-fเช่น:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

ฉันคิดว่าสิ่งนี้อาจปรับให้เหมาะสมเล็กน้อยหากมันยุบหมายเลขบรรทัดที่ต่อเนื่องกันสามหมายเลขขึ้นไปในช่วงเพื่อให้มีเช่น2,6dแทนแทนที่จะเป็น2d;3d;4d;5d;6d... แม้ว่าอินพุตจะมีคู่ที่ตรงกันเพียงเล็กน้อย แต่ก็ไม่คุ้มที่จะทำ


วิธีอื่น ๆ ที่ไม่รักษาลำดับคำสั่งซื้อและน่าจะช้ากว่า:
ด้วยcomm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

commต้องมีการป้อนข้อมูลที่เรียงลำดับซึ่งหมายความว่าคำสั่งซื้อจะไม่ได้รับการเก็บรักษาไว้ในผลลัพธ์สุดท้าย (ยกเว้นกรณีที่ไฟล์ของคุณจะถูกจัดเรียงแล้ว) จึงnlถูกนำมาใช้กับจำนวนเส้นก่อนที่จะเรียงลำดับการcomm -13พิมพ์เพียงเส้นที่ไม่ซ้ำกับไฟล์ที่ 2แล้วcutเอาส่วนที่ถูกเพิ่มเข้ามาโดยnl(นั่นคือฟิลด์แรกและตัวคั่น:)
ด้วยjoin:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-

ขอบคุณดอน! คำถามด่วนคุณคาดหวังว่าโซลูชันcommจะเร็วขึ้นกว่าเดิมด้วยsedและgrep?
Amelio Vazquez-Reina

1
@ AmelioVazquez-Reina - ฉันไม่คิดอย่างนั้นเพราะมันยังคงอ่านไฟล์อินพุตสองครั้ง (รวมถึงการเรียงลำดับบางอย่าง) ซึ่งตรงข้ามกับโซลูชันของ Mike ซึ่งประมวลผลไฟล์เพียงครั้งเดียวเท่านั้น
don_crissti

9

หากคุณไม่รังเกียจการใช้vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesเปิดโหมด ex ex ที่ไม่รองรับการทำงาน มีประโยชน์สำหรับการเขียนสคริปต์
  • +{command}บอกเป็นกลุ่มเพื่อเรียกใช้{command}บนไฟล์
  • g/${PAT}/- /fff/ในทุกสายการจับคู่ สิ่งนี้จะยุ่งยากหากรูปแบบมีอักขระพิเศษนิพจน์ทั่วไปที่คุณไม่ต้องการให้ทำเช่นนั้น
  • .-${B} - จาก 1 บรรทัดด้านบนอันนี้
  • .+${A}- ถึง 2 บรรทัดด้านล่างบรรทัดนี้ (ดู:he cmdline-rangesสองบรรทัดนี้)
  • d - ลบบรรทัด
  • +w !tee จากนั้นเขียนไปยังเอาต์พุตมาตรฐาน
  • +q! ออกโดยไม่บันทึกการเปลี่ยนแปลง

คุณสามารถข้ามตัวแปรและใช้รูปแบบและหมายเลขได้โดยตรง ฉันใช้มันเพื่อจุดประสงค์ที่ชัดเจน


3

วิธีการเกี่ยวกับ (ใช้ GNU grepและbash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

ที่นี่เรากำลังค้นหาบรรทัดที่จะถูกละทิ้งโดยgrep -B2 -A1 'fff' file.txtใช้สิ่งนี้เป็นไฟล์อินพุตเพื่อค้นหาบรรทัดที่ต้องการทิ้งเหล่านี้


อืมสิ่งนี้จะไม่ส่งผลอะไรต่อเครื่องของฉัน (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina ขออภัยเกี่ยวกับเรื่องนั้น .. ฉันไม่รู้จักระบบปฏิบัติการของคุณมาก่อน .. อย่างไรก็ตามฉันได้ทดสอบสิ่งนี้บน Ubuntu ..
heemayl

2
สิ่งนี้จะมีปัญหาเช่นเดียวกับkosวิธีการแก้ปัญหา (ตอนนี้ถูกลบ) ราวกับว่ามีบรรทัดที่ซ้ำกันในไฟล์อินพุตและบางส่วนอยู่นอกช่วงและอื่น ๆ อยู่ในช่วงนั้นซึ่งจะเป็นการลบทั้งหมด นอกจากนี้ด้วยรูปแบบที่เกิดขึ้นหลายครั้งหากมีบรรทัดเหมือน--ในไฟล์อินพุต (อยู่นอกช่วง) สิ่งนี้จะลบออกเพราะตัวคั่น--จะปรากฏในgrepเอาต์พุตของเมื่อมากกว่าหนึ่งบรรทัดเป็นรูปแบบการจับคู่(หลังไม่น่าเป็นไปได้สูง แต่คุ้มค่า พูดถึงฉันเดา)
don_crissti

@don_crissti ขอบคุณ.. คุณพูดถูก .. ถึงแม้ฉันจะยกตัวอย่างของ OP อย่างแท้จริง .. ฉันจะทิ้งมันไว้ในกรณีที่มีคนพบว่ามันมีประโยชน์ในภายหลัง ..
heemayl

1

คุณสามารถเข้าถึงผลลัพธ์ที่ดีพอโดยใช้ไฟล์ชั่วคราว:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

ผลลัพธ์นั้นดีพอเพราะคุณสามารถลดการเยื้องในกระบวนการได้ แต่ถ้าเป็นไฟล์ xml หรือการเยื้องที่ไม่มีการเยื้องก็ไม่น่าจะมีปัญหา เนื่องจากสคริปต์นี้ใช้ RAM ไดรฟ์การเขียนและอ่านไฟล์ temp เหล่านั้นจึงเร็วเท่ากับการทำงานในหน่วยความจำ


1

นอกจากนี้หากคุณต้องการยกเว้นบางบรรทัดก่อนเครื่องหมายที่ระบุคุณสามารถใช้:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(เกล็นแจ็กแมนที่https://stackoverflow.com/a/1492538 )

โดยการไพพ์คำสั่งบางคำสั่งคุณจะได้รับก่อน / หลัง behaivour:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac

1
ยอดเยี่ยมใช้awkไฟล์ที่กลับด้านเพื่อจัดการบรรทัดต่อไปนี้เมื่อคุณต้องการส่งผลกระทบต่อบรรทัดก่อนและย้อนกลับผลลัพธ์
karmakaze

0

วิธีหนึ่งในการทำสิ่งนี้ให้สำเร็จอาจเป็นวิธีที่ง่ายที่สุดในการสร้างตัวแปรและทำสิ่งต่อไปนี้:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

วิธีนี้คุณยังคงมีโครงสร้างของคุณ และคุณสามารถดูได้ง่าย ๆ จากสิ่งที่คุณพยายามจะเอาออก

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii

โซลูชันเดียวกับ heemayl และปัญหาเดียวกับที่อธิบายโดย don_crissti: สิ่งนี้จะมีปัญหาเช่นเดียวกับวิธีแก้ปัญหาของ kos (ตอนนี้ถูกลบ) ราวกับว่ามีบรรทัดที่ซ้ำกันในไฟล์อินพุตและบางส่วนอยู่นอกช่วงและอื่น ๆ อยู่ในช่วงนั้น จะเป็นการลบทั้งหมด นอกจากนี้เมื่อมีลวดลายหลายรายการหากมีบรรทัดเช่น - ในไฟล์อินพุต (นอกช่วง) สิ่งนี้จะลบออกเพราะตัวคั่น - ปรากฏในเอาต์พุตของ grep เมื่อมากกว่าหนึ่งบรรทัดคือรูปแบบการจับคู่ (หลังจะสูงมาก ไม่น่าเชื่อ แต่ก็คุ้มค่าที่จะกล่าวถึง)
Bodo Thiesen

0

หากมีการแข่งขันเพียง 1 รายการ:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

มิฉะนั้น (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.