การจับภาพกลุ่มจาก Grep RegEx


380

ฉันมีสคริปต์เล็ก ๆ นี้ในsh(Mac OSX 10.6) เพื่อดูไฟล์ต่างๆ Google หยุดให้ความช่วยเหลือในตอนนี้:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

จนถึงตอนนี้ (เห็นได้ชัดว่าคุณปรมาจารย์เปลือกหอย) $nameถือเพียง 0, 1 หรือ 2 ขึ้นอยู่กับว่าgrepพบว่าชื่อไฟล์ตรงกับเรื่องที่ให้ สิ่งที่ผมต้องการคือการจับสิ่งที่อยู่ภายใน parens([a-z]+)และร้านค้าที่ให้กับตัวแปร

ฉันต้องการที่จะใช้grepเพียง แต่ถ้าเป็นไปได้ ถ้าไม่โปรดอย่าใช้ Python หรือ Perl เป็นต้นsedหรืออะไรทำนองนี้ - ฉันใหม่กับเชลล์และต้องการโจมตีมันจากมุม * * * * คนเจ้าระเบียบ

นอกจากนี้ในฐานะที่เป็นbonu สุดยอดฉันอยากรู้อยากเห็นว่าฉันสามารถเชื่อมสตริงในเปลือก? กลุ่มที่ฉันจับเป็นสตริง "somename" ที่เก็บไว้ในชื่อ $ และฉันต้องการเพิ่มสตริง ".jpg" ที่ส่วนท้ายของมันcat $name '.jpg'ใช่ไหม

โปรดอธิบายสิ่งที่เกิดขึ้นหากคุณมีเวลา


30
grep นั้นบริสุทธิ์กว่ายูนิกซ์จริงๆหรือ?
มาร์ตินเคลย์ตัน

3
อาไม่ได้หมายความว่าจะแนะนำอย่างนั้น ฉันแค่หวังว่าจะพบวิธีแก้ปัญหาโดยใช้เครื่องมือที่ฉันพยายามเรียนรู้ที่นี่โดยเฉพาะ ถ้ามันเป็นไปไม่ได้ที่จะแก้ปัญหาโดยใช้grepแล้วจะดีถ้ามันเป็นไปได้ที่จะแก้ปัญหาโดยใช้sed sed
Isaac

2
ฉันควรจะได้ใส่ :) บนว่าครับ ...
มาร์ตินเคลย์ตัน

วันนี้ Psh สมองของฉันทอดเกินไปฮ่าฮ่า
Isaac

2
@martinclayton นั่นเป็นข้อโต้แย้งที่น่าสนใจ ฉันคิดว่า sed จริง ๆ (หรือ ed เพื่อความแม่นยำ) จะแก่กว่า (และดังนั้น purer?
ffledgling

คำตอบ:


499

หากคุณกำลังใช้ Bash คุณไม่จำเป็นต้องใช้grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

จะเป็นการดีกว่าถ้าใส่ regex ลงในตัวแปร รูปแบบบางอย่างจะไม่ทำงานถ้ารวมตัวอักษร

วิธีนี้ใช้ =~ซึ่งเป็นโอเปอเรเตอร์การแข่งขันของ Bash $BASH_REMATCHผลที่ได้จากการแข่งขันจะถูกบันทึกไปยังอาร์เรย์ที่เรียกว่า กลุ่มการจับครั้งแรกจะถูกเก็บไว้ในดัชนี 1, ที่สอง (ถ้ามี) ในดัชนี 2, ฯลฯ ดัชนีศูนย์คือการแข่งขันแบบเต็ม

คุณควรทราบว่าไม่มีจุดยึด regex นี้ (และอันที่ใช้grep) จะตรงกับตัวอย่างใด ๆ ต่อไปนี้และอื่น ๆ ซึ่งอาจไม่ใช่สิ่งที่คุณกำลังมองหา:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

หากต้องการกำจัดตัวอย่างที่สองและสี่ให้ทำ regex ของคุณดังนี้:

^[0-9]+_([a-z]+)_[0-9a-z]*

ซึ่งระบุว่าสตริงจะต้องเริ่มต้นด้วยตัวเลขหนึ่งหลักหรือมากกว่า กะรัตหมายถึงจุดเริ่มต้นของสตริง หากคุณเพิ่มเครื่องหมายดอลลาร์ในตอนท้ายของ regex เช่นนี้:

^[0-9]+_([a-z]+)_[0-9a-z]*$

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

หากคุณมี GNU grep(ประมาณ 2.5 หรือใหม่กว่าฉันคิดว่าเมื่อมี\Kการเพิ่มโอเปอเรเตอร์):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

\Kประกอบการ (ความยาวตัวแปรมองหลัง) ทำให้เกิดรูปแบบก่อนหน้านี้จะมีการแข่งขัน แต่ไม่รวมถึงการแข่งขันในผลลัพธ์ ความยาวคงที่เท่ากับคือ(?<=)- รูปแบบจะถูกรวมไว้ในวงเล็บปิด คุณต้องใช้\Kถ้าปริมาณอาจตรงกับสตริงของความยาวที่แตกต่างกัน (เช่น+, *, {2,4})

ตัว(?=)ดำเนินการจับคู่รูปแบบคงที่หรือความยาวผันแปรและเรียกว่า "ดูล่วงหน้า" นอกจากนี้ยังไม่รวมสตริงที่ตรงกันในผลลัพธ์

เพื่อให้ตรงกับตัวพิมพ์เล็กและตัวพิมพ์ใหญ่(?i)จะใช้ตัวดำเนินการ มันส่งผลต่อรูปแบบที่ตามมาดังนั้นตำแหน่งจึงมีความสำคัญ

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


48
ในคำตอบนี้ฉันต้องการ upvote บรรทัดที่ระบุว่า "เป็นการดีกว่าที่จะใส่ regex ไว้ในตัวแปร
Brandin

5
@FrancescoFrassinelli: ตัวอย่างคือรูปแบบที่มีช่องว่าง มันน่าอึดอัดใจที่จะหลบหนีและคุณไม่สามารถใช้คำพูดได้เพราะมันบังคับให้มันจาก regex ไปยังสตริงธรรมดา วิธีที่ถูกต้องที่จะทำคือใช้ตัวแปร สามารถใช้เครื่องหมายคำพูดระหว่างการมอบหมายทำให้สิ่งต่าง ๆ ง่ายขึ้นมาก
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

5
/Kผู้ประกอบการหิน
razz

2
@Brandon: มันทำงานได้ คุณกำลังใช้ Bash เวอร์ชันใด แสดงให้ฉันดูว่าคุณกำลังทำอะไรที่ไม่ได้ผลและบางทีฉันสามารถบอกคุณได้ว่าทำไม
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

2
@mdelolmo: grepคำตอบของฉันรวมถึงข้อมูลเกี่ยวกับ มันได้รับการยอมรับจาก OP และยังเพิ่มขึ้นค่อนข้างมาก ขอบคุณสำหรับ downvote
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

145

สิ่งนี้เป็นไปไม่ได้จริง ๆ กับบริสุทธิ์grepอย่างน้อยก็ไม่ใช่

แต่ถ้ารูปแบบของคุณเหมาะสมคุณอาจจะสามารถใช้grepหลาย ๆ ครั้งภายในไปป์ไลน์เพื่อลดบรรทัดของคุณลงในรูปแบบที่รู้จักก่อนแล้วจึงแยกเพียงบิตที่คุณต้องการ (แม้ว่าเครื่องมือจะชอบcutและsedดีกว่านี้)

สมมติว่าเป็นเพราะการโต้แย้งว่ารูปแบบของคุณนั้นเรียบง่ายกว่า: [0-9]+_([a-z]+)_คุณสามารถแยกแบบนี้ได้:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

ก่อนgrepจะลบบรรทัดใด ๆ ที่ไม่ตรงกับบิดาโดยรวมของคุณที่สองgrep(ซึ่งได้--only-matchingระบุไว้) จะแสดงส่วนอัลฟาของชื่อ ใช้งานได้เนื่องจากรูปแบบเหมาะสม: "ส่วนอัลฟา" มีความเฉพาะเจาะจงมากพอที่จะดึงสิ่งที่คุณต้องการ

(นอกเหนือจาก: โดยส่วนตัวแล้วฉันจะใช้grep+ cutเพื่อให้ได้สิ่งที่คุณเป็นหลังจากนั้น: echo $name | grep {pattern} | cut -d _ -f 2. สิ่งนี้cutจะแยกบรรทัดลงในฟิลด์โดยแยกบนตัวคั่น_และส่งคืนเฉพาะฟิลด์ 2 (หมายเลขฟิลด์เริ่มต้นที่ 1))

ปรัชญา Unix คือการมีเครื่องมือที่ทำสิ่งใดสิ่งหนึ่งและทำได้ดีและรวมเข้าด้วยกันเพื่อให้ได้งานที่ไม่สำคัญดังนั้นฉันจึงยืนยันว่าgrep+ sedฯลฯ เป็นวิธี Unixy มากขึ้นในการทำสิ่งต่าง ๆ :-)


3
for f in $files; do name=echo $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | ตัด -d _ -f 2 ;Aha!
Isaac

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

7
@ ghostdog74: ไม่มีข้อโต้แย้งที่นี่ว่าการผูกมัดการดำเนินการเล็ก ๆ จำนวนมากเข้าด้วยกันโดยทั่วไปแล้วจะมีประสิทธิภาพน้อยกว่าการทำทั้งหมดในที่เดียว ตัวอย่างเช่น tar เพียงแค่เก็บไฟล์มันไม่บีบอัดไฟล์และเนื่องจากมันส่งออกไปยัง STDOUT ตามค่าเริ่มต้นคุณสามารถไพพ์ข้ามเครือข่ายด้วย netcat หรือบีบอัดด้วย bzip2 เป็นต้นซึ่งความคิดของฉันจะเสริมการประชุมและทั่วไป ร๊อคที่เครื่องมือ Unix ควรสามารถทำงานร่วมกันในท่อได้
RobM

ตัดน่ากลัว - ขอบคุณสำหรับเคล็ดลับ! สำหรับเครื่องมือเทียบกับการโต้แย้งประสิทธิภาพฉันชอบความเรียบง่ายของเครื่องมือการผูกมัด
ether_joe

อุปกรณ์ประกอบฉากสำหรับตัวเลือกของ grep ซึ่งมีประโยชน์มาก
chiliNUT

96

ฉันรู้ว่าคำตอบได้รับการยอมรับแล้วสำหรับเรื่องนี้ แต่จาก "มุมพิถีพิถันอย่างเคร่งครัด *" ดูเหมือนว่าเครื่องมือที่เหมาะสมสำหรับงานคือpcregrepซึ่งยังไม่ได้กล่าวถึง ลองเปลี่ยนเส้น:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

เพื่อต่อไปนี้:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

เพื่อรับเนื้อหาของกลุ่มการดักจับ 1 เท่านั้น

pcregrepเครื่องมือที่ใช้ทั้งหมดของไวยากรณ์เดียวกับที่คุณเคยใช้แล้วกับgrepแต่ใช้ฟังก์ชันการทำงานที่คุณต้องการ

พารามิเตอร์ใช้-oงานได้เหมือนกับgrepเวอร์ชันหากไม่แสดง แต่ก็ยอมรับพารามิเตอร์ตัวเลขpcregrepด้วยซึ่งจะระบุกลุ่มการจับที่คุณต้องการแสดง

ด้วยวิธีนี้มีการเปลี่ยนแปลงขั้นต่ำเปลือยในสคริปต์ คุณเพียงแค่แทนที่ยูทิลิตี้แบบแยกส่วนหนึ่งด้วยอีกอันหนึ่งแล้วปรับแต่งพารามิเตอร์

หมายเหตุที่น่าสนใจ:คุณสามารถใช้อาร์กิวเมนต์ -o หลายอันเพื่อส่งคืนกลุ่มการดักจับหลายกลุ่มตามลำดับที่ปรากฏในบรรทัด


3
pcregrepไม่สามารถใช้งานได้ตามค่าเริ่มต้นMac OS Xซึ่งเป็นสิ่งที่ OP ใช้
grebneke

4
ฉันpcregrepดูเหมือนจะไม่เข้าใจหลักหลังจาก-o: "ตัวเลือกที่ไม่รู้จักตัวอักษร '1' ใน" -o1 "นอกจากนี้ยังไม่มีการพูดถึง functionaliy เมื่อดูที่pcregrep --help
Peter Herdenborg

1
@WAF ขออภัยฉันควรรวมข้อมูลนั้นไว้ในความคิดเห็นของฉันด้วย ฉันอยู่บน Centos 6.5 และรุ่น pcregrep 7.8 2008-09-05คือเห็นได้ชัดว่าเก่ามาก:
Peter Herdenborg

2
ใช่ช่วยได้มากเช่นecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei

5
pcregrep8.41 (ติดตั้งพร้อมapt-get install pcregrepเปิดUbuntu 16.03) ไม่รู้จัก-Eiสวิตช์ มันใช้งานได้อย่างสมบูรณ์แบบหากปราศจากมัน สำหรับ macOS ที่pcregrepติดตั้งผ่านhomebrew(เช่น 8.41) ตามที่ @anishpatel กล่าวถึงข้างต้นอย่างน้อยใน High Sierra -Eสวิตช์ก็ไม่รู้จักเช่นกัน
วิลล์

27

เป็นไปไม่ได้ใน grep ที่ฉันเชื่อ

สำหรับ sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

ฉันจะแทงด้วยโบนัสแม้ว่า:

echo "$name.jpg"

2
น่าเสียดายที่sedโซลูชันนั้นใช้ไม่ได้ มันพิมพ์ทุกอย่างในไดเรกทอรีของฉัน
Isaac

อัปเดตแล้วจะส่งออกบรรทัดว่างหากไม่มีการจับคู่ดังนั้นโปรดตรวจสอบให้แน่ใจก่อนอื่น
cobbal

ตอนนี้มันจะแสดงบรรทัดว่างเปล่าเท่านั้น!
Isaac

sed นี้มีปัญหา กลุ่มแรกของการจับภาพวงเล็บรวมทุกอย่าง แน่นอน \ 2 จะไม่มีอะไร
ghostdog74

มันใช้งานได้กับกรณีทดสอบง่ายๆ ... \ 2 ได้รับกลุ่มภายใน
cobbal

16

นี่คือทางออกที่ใช้เพ่งพิศ มันเป็นสิ่งที่ฉันพบว่าฉันจำเป็นต้องใช้บ่อย ๆ ดังนั้นฉันจึงสร้างฟังก์ชั่นสำหรับมัน

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

ที่จะใช้เพียงแค่ทำ

$ echo 'hello world' | regex1 'hello\s(.*)'
world

คิดที่ดี แต่ดูเหมือนจะไม่ทำงานร่วมกับช่องว่างใน regexp - \sพวกเขาจะต้องถูกแทนที่ด้วย คุณรู้วิธีการแก้ไขหรือไม่
Adam Ryczkowski

4

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

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

จากนั้นจะมีค่าnameabc

ดูเอกสารสำหรับนักพัฒนาของ Apple ค้นหาคำว่า 'การขยายพารามิเตอร์'


สิ่งนี้จะไม่ตรวจสอบ ([az] +)
ghostdog74

@levislevis - จริง แต่ตามความเห็นของ OP แล้วมันจะทำสิ่งที่ต้องการ
มาร์ตินเคลย์ตัน

2

หากคุณมีทุบตีคุณสามารถใช้การขยายแบบวงกลม

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

หรือ

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

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