Làm thế nào để grep-inverse-match và loại trừ các dòng trước trước và các dòng sau


26

Xem xét một tệp văn bản với các mục sau:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Đưa ra một mẫu (ví dụ fff), tôi muốn grep tệp ở trên để nhận đầu ra:

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

Ví dụ: nếu B = 2A = 1, đầu ra có mẫu = fffnên là:

aaa
bbb
ccc
hhh
iii

Làm thế nào tôi có thể làm điều này với grep hoặc các công cụ dòng lệnh khác?


Lưu ý, khi tôi thử:

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

Tôi không nhận được những gì tôi muốn. Tôi thay vào đó nhận được:

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

Câu trả lời:


9

don có thể tốt hơn trong hầu hết các trường hợp, nhưng chỉ trong trường hợp tệp thực sự lớn và bạn không thể sedxử lý tệp tập lệnh lớn (có thể xảy ra ở khoảng 5000+ dòng script) , ở đây rõ ràng là 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'

Đây là một ví dụ về cái được gọi là cửa sổ trượt trên đầu vào. Nó hoạt động bằng cách xây dựng bộ đệm nhìn về phía trước của các $Bdòng -count trước khi cố gắng in bất cứ thứ gì.

Và trên thực tế, có lẽ tôi nên làm rõ quan điểm trước đây của mình: bộ giới hạn hiệu suất chính cho cả giải pháp này và don sẽ liên quan trực tiếp đến khoảng thời gian. Giải pháp này sẽ chậm với kích thước khoảng lớn hơn , trong khi don sẽ chậm với tần số khoảng lớn hơn . Nói cách khác, ngay cả khi tệp đầu vào rất lớn, nếu khoảng thời gian thực tế xảy ra vẫn không thường xuyên thì giải pháp của anh ta có lẽ là cách để đi. Tuy nhiên, nếu kích thước khoảng tương đối dễ quản lý và có khả năng xảy ra thường xuyên, thì đây là giải pháp bạn nên chọn.

Vì vậy, đây là quy trình công việc:

  • Nếu $matchđược tìm thấy trong không gian mẫu có trước một \newline, sedsẽ bỏ qua đệ quy Dmọi \newline trước nó.
    • Tôi đã xóa $matchhoàn toàn không gian mô hình trước đó - nhưng để dễ dàng xử lý sự chồng chéo, để lại một dấu mốc dường như hoạt động tốt hơn nhiều.
    • Tôi cũng đã cố s/.*\n.*\($match\)/\1/gắng để có được nó trong một lần và tránh vòng lặp, nhưng khi $A/$Blớn, Dvòng lặp elete chứng tỏ nhanh hơn đáng kể.
  • Sau đó, chúng tôi kéo vào Ndòng mở rộng của đầu vào trước \ndấu phân cách ewline và thử lại một lần nữa để xóa Dmột /\n.*$match/lần nữa bằng cách tham khảo biểu thức chính quy được sử dụng gần đây nhất của chúng tôi w / //.
  • Nếu không gian mẫu khớp với nhau $matchthì nó chỉ có thể làm như vậy $matchở đầu dòng - tất cả các $Bdòng eFor đã bị xóa.
    • Vì vậy, chúng tôi bắt đầu vòng lặp qua $After.
    • Mỗi lần chạy của vòng lặp này, chúng tôi sẽ cố gắng s///ubstitute cho &bản thân những $Athứ \nvật ewline trong không gian mô hình, và nếu thành công, test sẽ chi nhánh chúng tôi - và cả chúng tôi $Ađệm fter - ra của kịch bản hoàn toàn để bắt đầu kịch bản lại từ đầu với dòng đầu vào tiếp theo nếu có.
    • Nếu test không thành công, chúng tôi sẽ bquay lại :tnhãn op và lặp lại cho một dòng đầu vào khác - có thể bắt đầu vòng lặp nếu $matchxảy ra trong khi thu thập $After.
  • Nếu chúng ta vượt qua một $matchvòng lặp chức năng, sau đó chúng tôi sẽ cố gắng print những $dòng cuối cùng nếu điều này là nó, và nếu !không cố gắng để s///ubstitute cho &bản thân những $Bthứ \nvật ewline trong không gian mô hình.
    • Chúng tôi cũng sẽ đánh giá tđiều này và nếu thành công, chúng tôi sẽ phân nhánh :Pnhãn rint.
    • Nếu không, chúng tôi sẽ phân nhánh trở lại :top và nhận một dòng đầu vào khác được thêm vào bộ đệm.
  • Nếu chúng ta làm cho nó để :Print chúng tôi sẽ Print sau đó Delete lên đến đầu tiên \newline trong không gian mô hình và chạy lại kịch bản từ đầu với những gì còn lại.

Và vì vậy lần này, nếu chúng ta đang làm A=2 B=2 match=5; seq 5 | sed...

Không gian mẫu cho lần lặp đầu tiên tại :Print sẽ trông như sau:

^1\n2\n3$

Và đó là cách sedtập hợp $Bbộ đệm e ấp của nó . Và do đó, sedin ra $Bcác dòng -count đằng sau đầu vào mà nó đã thu thập. Điều này có nghĩa rằng, cho ví dụ trước của chúng tôi, sedsẽ Print 1đến đầu ra, và sau đó Delete đó và gửi lại để phía trên cùng của kịch bản một không gian mô hình mà trông giống như:

^2\n3$

... và ở đầu tập lệnh, Ndòng đầu vào ext được lấy ra và do đó lần lặp tiếp theo trông như sau:

^2\n3\n4$

Và do đó, khi chúng ta tìm thấy sự xuất hiện đầu tiên của 5đầu vào, không gian mẫu thực sự trông như sau:

^3\n4\n5$

Sau đó, Dvòng lặp elete khởi động và khi nó đi qua, nó trông giống như:

^5$

Và khi Ndòng đầu vào ext được kéo, sedchạm EOF và thoát. Vào thời điểm đó, nó chỉ có Pcác dòng 1 và 2.

Đây là một ví dụ chạy:

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'

Đó là bản in:

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

Tôi thực sự đang làm việc với các tệp lớn và câu trả lời của don chậm hơn đáng kể so với giải pháp này. Ban đầu tôi rất do dự khi thay đổi câu trả lời được chấp nhận của mình, nhưng sự khác biệt về tốc độ là khá rõ ràng.
Amelio Vazquez-Reina

4
@Amelio - điều này sẽ hoạt động với một luồng có kích thước bất kỳ và nó không cần phải đọc tệp để làm việc. Yếu tố hiệu suất lớn nhất là kích thước $Avà / hoặc $B. Bạn càng tạo ra những con số đó càng lớn, nó sẽ càng chậm - nhưng bạn có thể làm cho chúng lớn hơn một cách hợp lý.
mikeerv

1
@ AmelioVazquez-Reina - nếu bạn đang sử dụng cái cũ hơn, điều này tốt hơn, tôi nghĩ vậy.
mikeerv

11

Bạn có thể sử dụng gnu grepvới -A-Bđể in chính xác các phần của tệp bạn muốn loại trừ nhưng thêm công -ntắc để in số dòng và sau đó định dạng đầu ra và chuyển nó dưới dạng tập lệnh sedđể xóa các dòng đó:

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

Điều này cũng sẽ làm việc với các tệp của các mẫu được truyền grepqua, -fví dụ:

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

Tôi nghĩ rằng điều này có thể được tối ưu hóa một chút nếu nó thu gọn bất kỳ ba hoặc nhiều số dòng liên tiếp thành các phạm vi để có ví dụ 2,6dthay vì 2d;3d;4d;5d;6d... mặc dù nếu đầu vào chỉ có một vài kết quả thì không đáng để thực hiện.


Các cách khác không bảo toàn thứ tự dòng và rất có thể chậm hơn:
với comm:

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

commyêu cầu đầu vào được sắp xếp có nghĩa là thứ tự dòng sẽ không được giữ ở đầu ra cuối cùng (trừ khi tệp của bạn đã được sắp xếp) vì vậy nlđược sử dụng để đánh số các dòng trước khi sắp xếp, comm -13chỉ in các dòng duy nhất cho FILE thứ 2 và sau đó cutloại bỏ phần được thêm vào bởi nl(đó là trường đầu tiên và dấu phân cách :)
với join:

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

Cảm ơn Don! Câu hỏi nhanh, bạn có mong đợi giải pháp commnhanh hơn giải pháp ban đầu với sedgrepkhông?
Amelio Vazquez-Reina

1
@ AmelioVazquez-Reina - Tôi không nghĩ như vậy vì nó vẫn đọc tệp đầu vào hai lần (cộng với việc sắp xếp một số thứ) trái ngược với giải pháp của Mike chỉ xử lý tệp một lần.
don_crissti

9

Nếu bạn không phiền khi sử dụng vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesbật chế độ ex không tương thích, im lặng. Hữu ích cho kịch bản.
  • +{command}nói với vim để chạy {command}trên tập tin.
  • g/${PAT}/- trên tất cả các dòng khớp /fff/. Điều này trở nên khó khăn nếu mẫu chứa các ký tự đặc biệt biểu thức chính quy mà bạn không có ý định xử lý theo cách đó.
  • .-${B} - từ 1 dòng trên dòng này
  • .+${A}- đến 2 dòng bên dưới dòng này (xem :he cmdline-rangeshai dòng này)
  • d - xóa các dòng.
  • +w !tee sau đó ghi vào đầu ra tiêu chuẩn.
  • +q! thoát mà không lưu thay đổi.

Bạn có thể bỏ qua các biến và sử dụng mẫu và số trực tiếp. Tôi đã sử dụng chúng chỉ cho mục đích rõ ràng.


3

Còn về (sử dụng GNU grepbash):

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

Ở đây chúng tôi đang tìm các dòng bị loại bỏ grep -B2 -A1 'fff' file.txt, sau đó sử dụng dòng này làm tệp đầu vào để tìm các dòng mong muốn loại bỏ các dòng này.


Hmm, điều này không xuất ra bất cứ thứ gì trên máy của tôi (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina xin lỗi về điều đó..tôi không biết hệ điều hành của bạn trước đây..tất cả tôi đã thử nghiệm điều này trên Ubuntu ..
heemayl

2
Điều này sẽ có cùng một vấn đề như kosgiải pháp (hiện đã bị xóa) như thể có các dòng trùng lặp trong tệp đầu vào và một số trong số chúng nằm ngoài phạm vi và một số khác nằm trong phạm vi này sẽ xóa tất cả. Ngoài ra, với nhiều lần xuất hiện của mẫu , nếu có các dòng như --trong tệp đầu vào (nằm ngoài phạm vi) thì điều này sẽ xóa chúng vì dấu phân cách --xuất hiện trong grepđầu ra khi có nhiều dòng khớp với mẫu (dòng sau rất khó xảy ra nhưng có giá trị đề cập đến tôi đoán).
don_crissti 01/07/2015

@don_crissti Cảm ơn..bạn nói đúng .. mặc dù tôi đã lấy ví dụ của OP theo nghĩa đen..tôi sẽ rời khỏi nó trong trường hợp ai đó thấy nó hữu ích sau này ..
heemayl

1

Bạn có thể đạt được kết quả đủ tốt bằng cách sử dụng các tệp tạm thời:

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

Kết quả là đủ tốt vì bạn có thể mất một số vết lõm trong quá trình, nhưng nếu đó là tệp không nhạy cảm xml hoặc thụt lề thì đó không phải là vấn đề. Vì tập lệnh này sử dụng ổ đĩa ram, việc ghi và đọc các tệp tạm thời đó nhanh như làm việc trong bộ nhớ.


1

Ngoài ra, nếu bạn chỉ muốn loại trừ một số dòng trước một điểm đánh dấu nhất định, bạn có thể sử dụng:

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

(glenn jackman tại /programming//a/1492538 )

Bằng cách dẫn một số lệnh, bạn có thể nhận được trước / sau 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
Rực rỡ, sử dụng awktrên một tệp đảo ngược để xử lý các dòng sau khi bạn có nghĩa là ảnh hưởng đến các dòng trước và đảo ngược kết quả.
karmakaze

0

Một cách để thực hiện điều này, có lẽ cách dễ nhất là tạo một biến và làm như sau:

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

Bằng cách này bạn vẫn có cấu trúc của bạn. Và bạn có thể dễ dàng nhìn thấy từ một lớp lót mà bạn đang cố gắng loại bỏ.

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

cùng một giải pháp như heemayl và cùng một vấn đề như được mô tả bởi don_crissti: Điều này sẽ có cùng một vấn đề như giải pháp của kos (hiện đã bị xóa) như thể có các dòng trùng lặp trong tệp đầu vào và một số trong số chúng nằm ngoài phạm vi đó và một số khác nằm trong phạm vi đó Điều này sẽ xóa tất cả. Ngoài ra, với nhiều lần xuất hiện của mẫu, nếu có các dòng như - trong tệp đầu vào (nằm ngoài phạm vi), điều này sẽ xóa chúng vì dấu phân cách - xuất hiện trong đầu ra của grep khi có nhiều dòng khớp với mẫu (dòng sau rất cao Tôi đoán là không thể nhưng đáng nói).
Bodo Thiesen

0

Nếu chỉ có 1 trận đấu:

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

Mặt khác (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)
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.