Làm cách nào để lấy nhiều dòng ra khỏi tệp bằng biểu thức chính quy?


10

Làm cách nào để lấy nhiều dòng ra khỏi tệp bằng biểu thức chính quy?

Tôi thường muốn nhận được nhiều dòng / sửa đổi nhiều dòng bằng regex. Một trường hợp ví dụ:

Tôi đang cố đọc một phần của tệp XML / SGML (chúng không nhất thiết phải được tạo tốt hoặc theo cú pháp có thể dự đoán được, vì vậy một regex sẽ an toàn hơn một trình phân tích cú pháp thích hợp. Ngoài ra, tôi muốn có thể làm điều này cũng hoàn toàn trắng các tệp không có cấu trúc trong đó chỉ có một số từ khóa được biết đến.) trong tập lệnh shell (chạy trên Solaris và Linux).

Ví dụ XML:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

Từ đây tôi muốn đọc <tag1>nếu nó chứa foomột nơi nào đó bên trong nó.

Một regex like (<tag1>.*?foo.*?</tag1>)nên cung cấp đúng phần nhưng các công cụ thích grepsedchỉ hoạt động với tôi với các dòng đơn. Làm thế nào tôi có thể nhận được

<tag1>
 <tag2>foo</tag2>
</tag1>

trong ví dụ này?



@evilsoup Đúng vậy, nhưng câu hỏi của tôi không cụ thể về các tệp XML / SGML, chỉ là về bất kỳ tệp văn bản nào.
Den

Câu trả lời:


7

Nếu bạn đã cài đặt GNU grep, bạn có thể thực hiện tìm kiếm nhiều dòng bằng cách chuyển qua -Pcờ (perl-regex) và kích hoạt PCRE_DOTALLbằng(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

Nếu cách trên không hoạt động trên nền tảng của bạn, hãy thử chuyển -zcờ ngoài ra, điều này buộc grep phải coi NUL là dấu phân cách dòng, khiến toàn bộ tệp trông giống như một dòng.

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt

Điều này không cung cấp đầu ra trên hệ thống của tôi khi chạy trên tệp ví dụ của OP.
terdon

Làm việc cho tôi. +1. Cảm ơn vì tiền (?s)boa
Nathan Wallace

@terdon, bạn đang chạy phiên bản GNU grep nào?
iruvar

@ 1_CR (GNU grep) 2.14trên Debian. Tôi đã sao chép ví dụ OP như (chỉ thêm dòng mới) và chạy greptrên đó nhưng không có kết quả.
terdon

1
@slm, tôi đang dùng pcre 6.6, GNU grep 2.5.1 trên RHEL. Bạn có phiền khi thử grep -ozPthay vì grep -oPtrên nền tảng của bạn?
iruvar

3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

Nếu bạn làm như trên, với dữ liệu bạn hiển thị, trước dòng dọn dẹp cuối cùng ở đó, bạn sẽ làm việc với một sedkhông gian mẫu trông giống như:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

Bạn có thể in ra không gian mẫu của bạn bất cứ khi nào bạn muốn với look. Sau đó bạn có thể giải quyết các \nký tự.

sed l <file

Sẽ cho bạn thấy mỗi dòng sedxử lý nó ở giai đoạn lđược gọi.

Vì vậy, tôi vừa thử nghiệm nó và nó cần thêm một \backslashsau ,commatrong dòng đầu tiên, nhưng nếu không làm việc như là. Ở đây tôi đặt nó vào _sed_functionđể tôi có thể dễ dàng gọi nó cho mục đích trình diễn trong suốt câu trả lời này: (hoạt động với các bình luận được đưa vào, nhưng ở đây bị xóa vì lý do ngắn gọn)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

Bây giờ chúng ta sẽ chuyển pcho một lvì vậy chúng tôi có thể xem những gì chúng tôi đang làm việc với khi chúng ta phát triển kịch bản của chúng tôi và loại bỏ các bản demo không-op s?nên dòng cuối cùng của chúng tôi sed 3<<\SCRIPTchỉ trông giống như:

l;s/.*//;h;b}}

Sau đó, tôi sẽ chạy lại:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Đồng ý! Vì vậy, tôi đã đúng - đó là một cảm giác tốt. Bây giờ, hãy xáo trộn look của chúng tôi để xem các dòng nó kéo vào nhưng xóa. Chúng tôi sẽ xóa hiện tại của chúng tôi lvà thêm một cái !{block}để nó trông giống như:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

Đó là những gì nó trông giống như trước khi chúng tôi xóa sạch nó.

Một điều cuối cùng tôi muốn cho bạn thấy là Hkhông gian cũ khi chúng tôi xây dựng nó. Có một vài khái niệm chính tôi hy vọng tôi có thể chứng minh. Vì vậy, tôi loại bỏ look cuối cùng một lần nữa và thay đổi dòng đầu tiên để thêm một cái nhìn vào Hkhông gian cũ ở cuối:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

Hkhông gian cũ tồn tại chu kỳ dòng - do đó tên. Vì vậy, những gì mọi người thường gặp phải - ok, những gì tôi thường gặp phải - là nó cần xóa sau khi bạn sử dụng nó. Trong trường hợp này tôi chỉ xthay đổi một lần, vì vậy không gian giữ trở thành không gian mẫu và ngược lại và thay đổi này cũng tồn tại theo chu kỳ dòng.

Hiệu quả là tôi cần xóa không gian giữ của mình, nơi từng là không gian mẫu của tôi. Tôi làm điều này bằng cách trước tiên xóa không gian mẫu hiện tại với:

s/.*//

Mà chỉ cần chọn mỗi ký tự và loại bỏ nó. Tôi không thể sử dụng dbởi vì điều này sẽ kết thúc chu kỳ dòng hiện tại của tôi và lệnh tiếp theo sẽ không hoàn thành, điều này sẽ làm hỏng khá nhiều kịch bản của tôi.

h

Điều này hoạt động theo cách tương tự Hnhưng nó ghi đè lên không gian, vì vậy tôi vừa sao chép không gian mẫu trống của mình lên trên không gian giữ của tôi, xóa nó một cách hiệu quả. Bây giờ tôi chỉ có thể:

b

ngoài.

Và đó là cách tôi viết sedkịch bản.


Cảm ơn @slm! Bạn là một chàng trai thực sự ok, bạn biết điều đó?
mikeerv

Cảm ơn, công việc tốt, tăng rất nhanh lên 3k, tiếp theo 5k 8-)
slm

Tôi không biết, @slm. Tôi bắt đầu thấy tôi học ngày càng ít ở đây - có lẽ tôi đã vượt xa sự hữu ích của nó. Tôi phải suy nghĩ về nó. ive hầu như không đến trang web trong vài tuần qua.
mikeerv

Ít nhất là nhận được đến 10k. Tất cả mọi thứ có giá trị mở khóa là ở mức đó. Tiếp tục sứt mẻ, 5k sẽ đến khá nhanh bây giờ.
slm

1
Chà, @slm - dù sao bạn cũng là một giống chó quý hiếm. Tôi đồng ý về nhiều câu trả lời mặc dù. Đó là lý do tại sao nó lỗi tôi khi một số qs bị đóng cửa. Nhưng điều đó hiếm khi xảy ra, thực sự. Cảm ơn một lần nữa, slm.
mikeerv

2

Câu trả lời của @ jamespfinn sẽ hoạt động hoàn hảo nếu tệp của bạn đơn giản như ví dụ của bạn. Nếu bạn có một tình huống phức tạp hơn khi <tag1>có thể kéo dài hơn 2 dòng, bạn sẽ cần một thủ thuật phức tạp hơn một chút. Ví dụ:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

Tập lệnh perl sẽ xử lý từng dòng của tệp đầu vào của bạn và

  • if(/<tag1>/){$a=1;}: biến $ađược đặt thành 1nếu <tag1>tìm thấy thẻ mở ( ).

  • if($a==1){push @l,$_}: cho mỗi dòng, nếu $a1, thêm dòng đó vào mảng @l.

  • if(/<\/tag1>/) : nếu dòng hiện tại khớp với thẻ đóng:

    • if(grep {/foo/} @l){print "@l"}: nếu bất kỳ dòng nào được lưu trong mảng @l(đây là các dòng giữa <tag1></tag1>) khớp với chuỗi foo, hãy in nội dung của @l.
    • $a=0; @l=(): làm trống danh sách ( @l=()) và đặt $avề 0.

Điều này hoạt động tốt trừ trường hợp có nhiều hơn một <tag1> chứa "foo". Trong trường hợp đó nó in tất cả mọi thứ từ đầu là người đầu tiên <TAG1> để kết thúc trước </ TAG1> ...
Den

@den Tôi đã thử nghiệm nó với ví dụ thể hiện trong câu trả lời của tôi, trong đó có 3 <tag1>với foovà nó hoạt động tốt. Khi nào nó thất bại cho bạn?
terdon

cảm thấy rất sai khi phân tích xml bằng regex :)
Braiam

1

Đây là một sedthay thế:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

Giải trình

  • -n có nghĩa là không in dòng trừ khi được hướng dẫn.
  • /<tag1/ đầu tiên phù hợp với thẻ mở
  • :x là một nhãn để cho phép nhảy đến thời điểm này sau
  • N thêm dòng tiếp theo vào không gian mẫu (bộ đệm hoạt động).
  • /<\/tag1/!b xcó nghĩa là nếu không gian mẫu hiện tại không chứa thẻ đóng, nhánh tới xnhãn được tạo trước đó. Do đó, chúng tôi tiếp tục thêm các dòng vào không gian mẫu cho đến khi chúng tôi tìm thấy thẻ đóng của mình.
  • /foo/pcó nghĩa là nếu không gian mẫu hiện tại khớp foo, nó sẽ được in.

1

Tôi nghĩ bạn có thể làm điều đó với GNU awk, bằng cách coi thẻ kết thúc là dấu tách bản ghi, ví dụ như đối với thẻ kết thúc đã biết </tag1>:

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

hoặc nói chung hơn (với biểu thức chính cho thẻ kết thúc)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

Kiểm tra nó trên @ terdon's foo.xml:

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>

0

Nếu tệp của bạn được cấu trúc chính xác như bạn đã trình bày ở trên, bạn có thể sử dụng các cờ -A (dòng sau) & -B (dòng trước) cho grep ... ví dụ:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

Nếu phiên bản grephỗ trợ của bạn, bạn cũng có thể sử dụng -Ctùy chọn đơn giản hơn (cho ngữ cảnh) in N dòng xung quanh:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>

Cảm ơn, nhưng không có. Đây chỉ là một ví dụ và những thứ thực sự có vẻ khá khó đoán ;-)
Den

1
Đó không phải là tìm kiếm một thẻ với foo trong đó, đó chỉ là tìm kiếm foo và hiển thị các dòng bối cảnh
Nathan Wallace

@NathanWallace có, đó chính xác là những gì OP đã yêu cầu, câu trả lời này hoạt động hoàn toàn tốt trong trường hợp được đưa ra trong câu hỏi.
terdon

@terdon đó không phải là tất cả những gì câu hỏi yêu cầu. Trích dẫn: "Tôi muốn đọc <tag1> nếu nó chứa foo ở đâu đó trong đó." Giải pháp này giống như "Tôi muốn đọc 'foo' và 1 dòng ngữ cảnh bất kể 'foo' xuất hiện ở đâu". Theo logic của bạn, một câu trả lời hợp lệ cho câu hỏi này sẽ là tail -3 input_file.xml. Vâng, nó hoạt động cho ví dụ cụ thể này, nhưng nó không phải là một câu trả lời hữu ích cho câu hỏi.
Nathan Wallace

@NathanWallace quan điểm của tôi là OP đặc biệt tuyên bố đây không phải là định dạng XML hợp lệ, trong trường hợp đó, nó có thể đủ để in các dòng N xung quanh chuỗi mà OP đang tìm kiếm. Với thông tin có sẵn, câu trả lời này là đủ tốt.
terdon
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.