วิธีการรับหลายบรรทัดออกจากไฟล์โดย regex?


10

วิธีการรับหลายบรรทัดออกจากไฟล์โดย regex?

ฉันมักจะต้องการรับหลายบรรทัด / แก้ไขหลายบรรทัดโดย regex กรณีตัวอย่าง:

ฉันกำลังพยายามอ่านส่วนหนึ่งของไฟล์ XML / SGML (ไม่จำเป็นต้องมีรูปแบบที่ดีหรืออยู่ในรูปแบบที่สามารถคาดเดาได้ดังนั้น regex จะปลอดภัยกว่าตัวแยกวิเคราะห์ที่เหมาะสมนอกจากนี้ฉันอยากจะทำเช่นนี้ได้เล็กน้อย ไฟล์ที่ไม่มีโครงสร้างซึ่งรู้จักคำสำคัญเพียงบางคำ) ในเชลล์สคริปต์ (ทำงานบน Solaris และ Linux)

ตัวอย่าง XML:

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

จากนี้ฉันอยากจะอ่าน<tag1>มันถ้ามันมีfooบางแห่งในนั้น

regex ที่ชอบ(<tag1>.*?foo.*?</tag1>)ควรให้ส่วนที่ถูกต้อง แต่เครื่องมือที่ชอบgrepและsedใช้ได้กับฉันในบรรทัดเดียวเท่านั้น ฉันจะได้รับ

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

ในตัวอย่างนี้?



@evilsoup นั้นจริง แต่คำถามของฉันไม่ได้เกี่ยวกับไฟล์ XML / SGML โดยเฉพาะเกี่ยวกับไฟล์ข้อความใด ๆ
Den

คำตอบ:


7

หากคุณมี GNU grep ติดตั้งอยู่คุณสามารถทำการค้นหาหลายบรรทัดโดยผ่านการ-Pตั้งค่าสถานะ (perl-regex) และเปิดใช้งานPCRE_DOTALLด้วย(?s)

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

หากข้อมูลด้านบนใช้ไม่ได้กับแพลตฟอร์มของคุณให้ลองผ่านการ-zตั้งค่าสถานะนี้บังคับให้ grep ปฏิบัติต่อ NUL เป็นตัวคั่นบรรทัดทำให้ไฟล์ทั้งหมดดูเหมือนเป็นบรรทัดเดียว

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

สิ่งนี้ไม่ให้ผลลัพธ์ในระบบของฉันเมื่อทำงานกับไฟล์ตัวอย่างของ OP
terdon

ได้ผลสำหรับฉัน +1 ขอบคุณสำหรับ(?s)เคล็ดลับ
นาธานวอลเลซ

@terdon grep GNU รุ่นใดที่คุณใช้อยู่
iruvar

@ 1_CR (GNU grep) 2.14บน Debian ฉันคัดลอกตัวอย่าง OPs ตามที่เป็นอยู่ (เพิ่มเฉพาะการขึ้นบรรทัดใหม่สุดท้าย) และเรียกgrepใช้งานมัน แต่ไม่มีผลลัพธ์
terdon

1
@slm ฉันอยู่บน pcre 6.6, GNU grep 2.5.1 บน RHEL คุณลองgrep -ozPเปลี่ยนเป็นgrep -oPแพลตฟอร์มหรือไม่?
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

หากคุณทำตามข้างต้นให้ข้อมูลที่คุณแสดงก่อนบรรทัดการล้างข้อมูลสุดท้ายที่นั่นคุณควรทำงานกับsedพื้นที่รูปแบบที่มีลักษณะดังนี้:

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

คุณสามารถพิมพ์รูปแบบพื้นที่ของคุณเมื่อใดก็ได้ที่คุณต้องการด้วยlook จากนั้นคุณสามารถระบุที่อยู่เป็น\nตัวอักษร

sed l <file

จะแสดงให้คุณเห็นแต่ละบรรทัดsedประมวลผลในระยะที่lเรียกว่า

ดังนั้นฉันเพิ่งทดสอบและต้องการอีกหนึ่ง\backslashหลังจาก,commaในบรรทัดแรก แต่ทำงานได้ ที่นี่ฉันใส่ไว้ใน_sed_functionเพื่อให้ฉันสามารถเรียกมันได้อย่างง่ายดายเพื่อวัตถุประสงค์ในการสาธิตตลอดคำตอบนี้: (ทำงานร่วมกับความคิดเห็นรวม แต่จะถูกลบออกที่นี่เพื่อประโยชน์ของความกะทัดรัด)

_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>

ตอนนี้เราจะเปลี่ยนpเพื่อlให้เราสามารถเห็นสิ่งที่เรากำลังทำงานกับในขณะที่เราพัฒนาสคริปต์ของเราและลบการสาธิตที่ไม่ใช่ op s?ดังนั้นบรรทัดสุดท้ายของเราsed 3<<\SCRIPTเพียงแค่มีลักษณะ:

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

จากนั้นฉันจะเรียกใช้อีกครั้ง:

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

ตกลง! ดังนั้นฉันพูดถูก - นั่นเป็นความรู้สึกที่ดี ทีนี้เราลองสลับlook รอบ ๆ เพื่อดูบรรทัดที่ดึงเข้าไป แต่ลบออก เราจะลบปัจจุบันของเราlและเพิ่มหนึ่งรายการ!{block}เพื่อให้ดูเหมือนว่า:

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

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

นั่นคือสิ่งที่ดูเหมือนก่อนที่เราจะล้างมันออกไป

สิ่งสุดท้ายที่ฉันต้องการแสดงให้คุณเห็นคือHพื้นที่เก่าที่เราสร้างมันขึ้นมา มีแนวคิดหลักสองสามข้อที่ฉันหวังว่าฉันจะสามารถสาธิตได้ ดังนั้นฉันจะลบlook สุดท้ายอีกครั้งและแก้ไขบรรทัดแรกเพื่อเพิ่มการค้นหาในHพื้นที่เก่าในตอนท้าย:

{ 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>$

Hพื้นที่เก่ามีชีวิตรอดรอบเส้น - จึงชื่อ ดังนั้นสิ่งที่ผู้คนมักจะเดินทางไป - โอเคสิ่งที่ฉันมักจะเดินทางไป - คือมันต้องการลบหลังจากที่คุณใช้มัน ในกรณีนี้ฉันจะxเปลี่ยนเพียงครั้งเดียวดังนั้นพื้นที่พักกลายเป็นพื้นที่รูปแบบและในทางกลับกันและการเปลี่ยนแปลงนี้ยังมีชีวิตอยู่ในรอบบรรทัด

ผลที่ได้คือฉันต้องลบพื้นที่พักซึ่งเคยเป็นพื้นที่รูปแบบของฉัน ฉันทำสิ่งนี้โดยการล้างพื้นที่รูปแบบปัจจุบันด้วย:

s/.*//

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

h

วิธีนี้ใช้ได้ในลักษณะคล้ายกันHแต่เขียนทับพื้นที่ว่างดังนั้นฉันเพิ่งคัดลอกพื้นที่รูปแบบเปล่าของฉันไปไว้ด้านบนของพื้นที่พักของฉันเพื่อลบอย่างมีประสิทธิภาพ ตอนนี้ฉันสามารถ:

b

ออก.

และนั่นคือวิธีที่ฉันเขียนsedสคริปต์


ขอบคุณ @slm! คุณเป็นคนดีจริง ๆ คุณรู้หรือไม่
mikeserv

ขอบคุณงานที่ดีมากปีนขึ้นไปอย่างรวดเร็วถึง 3k ต่อไป 5k 8-)
slm

ฉันไม่ชอบ @slm ฉันเริ่มเห็นว่าฉันเรียนรู้น้อยลงเรื่อย ๆ ที่นี่ - บางทีฉันอาจมีประโยชน์มากกว่า ฉันต้องคิดเกี่ยวกับมัน ive เพิ่งมาที่ไซต์เมื่อสองสามสัปดาห์ที่ผ่านมา
mikeserv

อย่างน้อยก็สูงถึง 10k ทุกสิ่งที่คุ้มค่าการปลดล็อคอยู่ในระดับนั้น ทิ้งไว้ให้ห่างออกไป 5k จะมาค่อนข้างเร็วในตอนนี้
slm

1
@slm - คุณเป็นสายพันธุ์ที่หายากอยู่แล้ว ฉันเห็นด้วยกับคำตอบหลาย ๆ อย่าง นั่นเป็นเหตุผลว่าทำไมมันทำให้ฉันรำคาญเมื่อบางคนถาม แต่นั่นไม่ค่อยเกิดขึ้นจริง ขอขอบคุณอีกครั้ง slm
mikeserv

2

คำตอบของ @ jamespfinn จะทำงานได้อย่างสมบูรณ์แบบหากไฟล์ของคุณนั้นเรียบง่ายเหมือนตัวอย่างของคุณ หากคุณมีสถานการณ์ที่ซับซ้อนมากขึ้นซึ่ง<tag1>สามารถขยายได้มากกว่า 2 บรรทัดคุณจะต้องใช้กลอุบายที่ซับซ้อนกว่านี้เล็กน้อย ตัวอย่างเช่น:

$ 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>

สคริปต์ Perl จะประมวลผลแต่ละบรรทัดของไฟล์อินพุตของคุณและ

  • if(/<tag1>/){$a=1;}: ตัวแปร$aถูกตั้งค่าเป็น1หากพบแท็กเปิด ( <tag1>)

  • if($a==1){push @l,$_}เพราะแต่ละบรรทัดถ้า$aเป็นเพิ่มบรรทัดที่อาร์เรย์1@l

  • if(/<\/tag1>/) : หากบรรทัดปัจจุบันตรงกับแท็กปิด:

    • if(grep {/foo/} @l){print "@l"}ถ้าใด ๆ ของเส้นที่บันทึกไว้ในอาร์เรย์@l(เหล่านี้จะเป็นเส้นระหว่าง<tag1>และ</tag1>) ตรงกับสตริงพิมพ์เนื้อหาของfoo@l
    • $a=0; @l=(): ลบรายการ ( @l=()) และตั้งค่า$aกลับเป็น 0

วิธีนี้ใช้งานได้ดียกเว้นในกรณีที่มี <tag1> มากกว่าหนึ่งรายการที่มี "foo" ในกรณีที่จะพิมพ์ทุกสิ่งจากจุดเริ่มต้นของครั้งแรก <tag1> ไปยังจุดสิ้นสุดของสุดท้าย </ tag1> ที่ ...
Den

@den ฉันทดสอบด้วยตัวอย่างที่แสดงในคำตอบของฉันซึ่งมี 3 <tag1>ด้วยfooและทำงานได้ดี คุณล้มเหลวเมื่อไหร่?
terdon

มันให้ความรู้สึกการแยกวิเคราะห์ XML ที่ไม่ถูกต้องดังนั้นการใช้ regex :)
Braiam

1

นี่คือsedทางเลือก:

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

คำอธิบาย

  • -n หมายถึงไม่พิมพ์บรรทัดเว้นแต่จะได้รับคำแนะนำ
  • /<tag1/ แรกตรงกับแท็กเปิด
  • :x เป็นป้ายกำกับที่เปิดใช้งานการกระโดดไปยังจุดนี้ในภายหลัง
  • N เพิ่มบรรทัดถัดไปในพื้นที่รูปแบบ (บัฟเฟอร์ที่ใช้งาน)
  • /<\/tag1/!b xหมายความว่าหากพื้นที่รูปแบบปัจจุบันไม่มีแท็กปิดให้แยกไปยังxป้ายกำกับที่สร้างไว้ก่อนหน้า ดังนั้นเราจึงเพิ่มบรรทัดลงในพื้นที่รูปแบบจนกว่าเราจะพบแท็กปิดของเรา
  • /foo/pหมายความว่าถ้าพื้นที่รูปแบบปัจจุบันตรงกันfooควรจะพิมพ์

1

คุณสามารถทำได้ด้วย GNU awk ฉันคิดว่าโดยใช้แท็กปิดท้ายเป็นตัวคั่นเรคคอร์ดเช่นสำหรับแท็กปิดท้ายที่รู้จัก</tag1>:

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

หรือมากกว่าโดยทั่วไป (ด้วย regex สำหรับแท็กปิดท้าย)

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

ทดสอบกับ @ 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

หากไฟล์ของคุณมีโครงสร้างตามที่แสดงไว้ด้านบนคุณสามารถใช้แฟล็ก -A (บรรทัดหลัง) & -B (บรรทัดก่อนหน้า) สำหรับ grep ... ตัวอย่างเช่น:

$ 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>

หากเวอร์ชันของคุณgrepรองรับคุณยังสามารถใช้-Cตัวเลือกที่ง่ายกว่า(สำหรับบริบท) ซึ่งพิมพ์บรรทัด N รอบ:

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

ขอบคุณ แต่ไม่ใช่ นี้เป็นเพียงตัวอย่างและสิ่งที่จริงมีลักษณะที่คาดเดาไม่ได้สวย ;-)
Den

1
นั่นไม่ใช่การค้นหาแท็กที่มี foo อยู่ในนั้นมันเป็นเพียงการค้นหา foo และแสดงบรรทัดของบริบท
Nathan Wallace

@NathanWallace ใช่ซึ่งเป็นสิ่งที่ OP ขอมาคำตอบนี้ใช้ได้ดีอย่างสมบูรณ์ในกรณีที่ให้ไว้ในคำถาม
terdon

@terdon ไม่ได้เป็นอย่างที่ถาม ข้อความอ้างอิง: "ฉันต้องการอ่าน <tag1> หากมี foo อยู่ในนั้น" วิธีการแก้ปัญหานี้เป็นเหมือน "ฉันต้องการอ่าน 'foo' และบริบท 1 บรรทัดโดยไม่คำนึงถึงที่ 'foo' ปรากฏขึ้น" ทำตามตรรกะของคุณคำตอบที่ถูกต้องเท่าเทียมกันสำหรับคำถามนี้ก็tail -3 input_file.xmlคือ ใช่ใช้ได้กับตัวอย่างเฉพาะนี้ แต่ไม่ใช่คำตอบที่เป็นประโยชน์สำหรับคำถาม
Nathan Wallace

@ NathanWallace จุดของฉันคือ OP ระบุไว้โดยเฉพาะนี่ไม่ใช่รูปแบบ XML ที่ถูกต้องในกรณีนั้นมันอาจจะเพียงพอที่จะพิมพ์บรรทัด N รอบสตริงที่ OP กำลังค้นหาอยู่ ด้วยข้อมูลที่มีอยู่คำตอบนี้ก็เพียงพอแล้ว
terdon
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.