รูปแบบ Regex ที่จะจับคู่ไม่รวมเมื่อ… / ยกเว้นระหว่าง


108

- แก้ไข - คำตอบปัจจุบันมีแนวคิดที่เป็นประโยชน์ แต่ฉันต้องการบางสิ่งที่สมบูรณ์กว่านี้ซึ่งฉันสามารถเข้าใจและนำกลับมาใช้ใหม่ได้ 100% นั่นคือเหตุผลที่ฉันตั้งค่าหัว นอกจากนี้แนวคิดที่ใช้งานได้ทุกที่ยังดีกว่าสำหรับฉันมากกว่าไวยากรณ์มาตรฐานเช่น\K

คำถามนี้เกี่ยวกับวิธีจับคู่รูปแบบยกเว้นบางสถานการณ์ s1 s2 s3 ฉันยกตัวอย่างเฉพาะเพื่อแสดงความหมายของฉัน แต่ต้องการคำตอบทั่วไปที่ฉันเข้าใจได้ 100% เพื่อที่ฉันจะได้ใช้ซ้ำในสถานการณ์อื่น ๆ

ตัวอย่าง

ฉันต้องการจับคู่ตัวเลขห้าหลักโดยใช้\b\d{5}\bแต่ไม่ใช่ในสามสถานการณ์ s1 s2 s3:

s1:ไม่อยู่ในบรรทัดที่ลงท้ายด้วยจุดเหมือนประโยคนี้

s2:ไม่มีที่ใดในห้องเก็บของ

s3:ไม่อยู่ในบล็อกที่ขึ้นต้นด้วยif(และลงท้ายด้วย//endif

ฉันรู้วิธีแก้ปัญหาใด ๆ ของ s1 s2 s3 ด้วย lookahead และ lookbehind โดยเฉพาะใน C # lookbehind หรือ\Kใน PHP

ตัวอย่างเช่น

s1 (?m)(?!\d+.*?\.$)\d+

s3 กับ C # lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

s3 กับ PHP \ K (?:(?:if\(.*?//endif)\D*)*\K\d+

แต่การผสมผสานของเงื่อนไขเข้าด้วยกันทำให้หัวของฉันระเบิด ข่าวร้ายยิ่งกว่านั้นคือฉันอาจต้องเพิ่มเงื่อนไขอื่น ๆ s4 s5 ในเวลาอื่น

ข่าวดีก็คือฉันไม่สนใจว่าฉันจะประมวลผลไฟล์โดยใช้ภาษาทั่วไปเช่น PHP, C #, Python หรือเครื่องซักผ้าของเพื่อนบ้านหรือไม่ :) ฉันค่อนข้างเป็นมือใหม่ใน Python & Java แต่สนใจที่จะเรียนรู้ว่ามีวิธีแก้ไขหรือไม่

ฉันจึงมาที่นี่เพื่อดูว่ามีใครคิดสูตรอาหารที่ยืดหยุ่นได้บ้าง

คำแนะนำก็โอเคคุณไม่จำเป็นต้องให้รหัสเต็ม :)

ขอบคุณ.


1
\Kไม่ใช่ไวยากรณ์ php พิเศษ กรุณาอธิบายและชี้แจงสิ่งที่คุณต้องการจะพูด หากคุณตั้งเป้าหมายที่จะบอกเราว่าคุณไม่ต้องการวิธีแก้ปัญหาที่ "ซับซ้อน" คุณต้องบอกว่าอะไรซับซ้อนสำหรับคุณและทำไม
hakre

@hakre คุณหมายถึงเพราะตอนนี้ทับทิมใช้มันและเริ่มต้นใน perl?
Hans Schindler

1
ไม่เพราะเป็น PCRE ที่ไม่ใช่ PHP (หรือ Ruby) Perl นั้นแตกต่างกันอย่างไรก็ตาม PCRE มุ่งหวังให้เข้ากันได้กับ Perl Regex
hakre

ข้อกำหนด s2 และ s3 ของคุณดูเหมือนจะขัดแย้งกัน s2 หมายความว่าวงเล็บจะตรงกันเสมอและอาจซ้อนกันได้ แต่ s3 ต้องการให้: "if("open paren ถูกปิดไม่ใช่ด้วย a ")"แต่ควรใช้ a "//endif":? และถ้าสำหรับ s3 คุณหมายความว่า if clause ควรปิดด้วย: "//endif)"ดังนั้นข้อกำหนด s3 จะเป็นเซตย่อยของ s2
ridgerunner

@hakre ใช่ฉันรู้จัก PCRE แต่เพื่ออธิบายคำถามเกี่ยวกับภาษาโปรแกรม ... มันบอกว่าespecially in C# lookbehind or \K in PHP... แต่ C # lookbehind ไม่ใช่แค่ C # เท่านั้นที่เป็น. NET ดังนั้นคุณสามารถบ่นได้เช่นกันฉันพูดว่า C # ไม่ใช่ .NET :) และในการตอบกลับฉันพูด Ruby ไม่ใช่ Onigurama ก็แย่เหมือนกัน ... มีภาษาอื่นที่ใช้ PCRE หรือไม่? ไม่ได้พูดถึง Notepad ++ หรือเครื่องมือเซิร์ฟเวอร์นี่เป็นคำถามเกี่ยวกับการใช้คุณสมบัติในภาษาฉันหวังว่าคำอธิบายและขออภัยหากดูผิด
Hans Schindler

คำตอบ:


205

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

ก่อนอื่นนี่เป็นคำถามที่ยอดเยี่ยม มักจะมีคำถามเกี่ยวกับการจับคู่รูปแบบบางอย่างยกเว้นในบางบริบท (เช่นภายในบล็อกโค้ดหรือในวงเล็บ) คำถามเหล่านี้มักก่อให้เกิดการแก้ปัญหาที่ค่อนข้างน่าอึดอัดใจ ดังนั้นคำถามของคุณเกี่ยวกับหลายบริบทจึงเป็นความท้าทายพิเศษ

เซอร์ไพรส์

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

น่าเศร้าที่เทคนิคนี้ไม่เป็นที่รู้จักกันดี: ฉันประเมินว่าในคำถาม SO ยี่สิบข้อที่สามารถใช้ได้มีเพียงคำตอบเดียวที่กล่าวถึงมันซึ่งหมายความว่าอาจจะหนึ่งในห้าสิบหรือหกสิบคำตอบ ดูการแลกเปลี่ยนของฉันกับ Kobi ในความคิดเห็น เทคนิคนี้ได้อธิบายไว้ในเชิงลึกในบทความนี้ซึ่งเรียกมันว่า (ในแง่ดี) "เคล็ดลับ regex ที่ดีที่สุดเท่าที่เคยมีมา" โดยไม่ต้องลงรายละเอียดมากนักฉันจะพยายามให้คุณเข้าใจอย่างถ่องแท้ว่าเทคนิคนี้ทำงานอย่างไร สำหรับรายละเอียดเพิ่มเติมและตัวอย่างโค้ดในภาษาต่างๆเราขอแนะนำให้คุณศึกษาแหล่งข้อมูลนั้น

รูปแบบที่รู้จักกันดีกว่า

มีรูปแบบที่ใช้ไวยากรณ์เฉพาะสำหรับ Perl และ PHP ที่ทำได้เหมือนกัน คุณจะได้เห็นมันในดังนั้นในมือของโท regex เช่นCasimiretHippolyteและHamza ฉันจะบอกคุณเพิ่มเติมเกี่ยวกับเรื่องนี้ด้านล่าง แต่จุดสนใจของฉันที่นี่คือโซลูชันทั่วไปที่ใช้ได้กับ regex ทุกรสชาติ (ตราบใดที่คุณสามารถตรวจสอบกลุ่มการจับภาพในโค้ดของคุณได้)

ขอบคุณสำหรับความเป็นมา zx81 ... แต่สูตรคืออะไร?

ข้อมูลสำคัญ

วิธีนี้จะส่งคืนการจับคู่ในการจับภาพกลุ่ม 1 ไม่สนใจเลยเกี่ยวกับการแข่งขันโดยรวม

ในความเป็นจริงเคล็ดลับคือการจับคู่บริบทต่างๆที่เราไม่ต้องการ (การผูกโยงบริบทเหล่านี้โดยใช้|OR / alternation) เพื่อ "ทำให้เป็นกลาง" หลังจากที่ตรงกันทั้งหมดบริบทที่ไม่พึงประสงค์ส่วนสุดท้ายของการสลับตรงกับสิ่งที่เราไม่ต้องการและจับไปยังกลุ่มที่ 1

สูตรทั่วไปคือ

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

สิ่งนี้จะตรงกันNot_this_contextแต่ในแง่หนึ่งการจับคู่จะกลายเป็นถังขยะเพราะเราจะไม่ดูการแข่งขันโดยรวม: เราดูเฉพาะการจับภาพของกลุ่ม 1

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

s1|s2|s3|(\b\d+\b)

โปรดทราบว่าเนื่องจากจริงๆแล้วเราจับคู่ s1, s2 และ s3 แทนที่จะพยายามหลีกเลี่ยงด้วยการค้นหาแบบวนรอบนิพจน์แต่ละรายการสำหรับ s1, s2 และ s3 จึงยังคงชัดเจนเหมือนวัน (เป็นนิพจน์ย่อยในแต่ละด้านของ a |)

นิพจน์ทั้งหมดสามารถเขียนได้ดังนี้:

(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)

ดูการสาธิตนี้(แต่เน้นที่กลุ่มการจับภาพในบานหน้าต่างด้านขวาล่าง)

หากคุณพยายามแยกนิพจน์ทั่วไปนี้ที่|ตัวคั่นแต่ละตัวในใจจริงๆแล้วมันเป็นเพียงชุดของนิพจน์ธรรมดาสี่ชุดเท่านั้น

สำหรับรสชาติที่รองรับการเว้นระยะห่างจะอ่านได้ดีเป็นพิเศษ

(?mx)
      ### s1: Match line that ends with a period ###
^.*\.$  
|     ### OR s2: Match anything between parentheses ###
\([^\)]*\)  
|     ### OR s3: Match any if(...//endif block ###
if\(.*?//endif  
|     ### OR capture digits to Group 1 ###
(\b\d+\b)

อ่านและบำรุงรักษาง่ายเป็นพิเศษ

การขยายนิพจน์ทั่วไป

เมื่อคุณต้องการละเว้นสถานการณ์เพิ่มเติม s4 และ s5 คุณเพิ่มในทางเลือกเพิ่มเติมทางด้านซ้าย:

s4|s5|s1|s2|s3|(\b\d+\b)

วิธีนี้ทำงานอย่างไร?

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

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

ฉันไม่ได้เป็นแฟนของการแสดงภาพเสมอไป แต่วิธีนี้ทำได้ดีในการแสดงให้เห็นว่าวิธีการนั้นง่ายเพียงใด "บรรทัด" แต่ละรายการสอดคล้องกับการจับคู่ที่เป็นไปได้ แต่จะจับเฉพาะบรรทัดล่างสุดในกลุ่ม 1

การแสดงภาพนิพจน์ทั่วไป

Debuggex Demo

การเปลี่ยนแปลง Perl / PCRE

ตรงกันข้ามกับโซลูชันทั่วไปข้างต้นมีรูปแบบสำหรับ Perl และ PCRE ที่มักเห็นใน SO อย่างน้อยก็อยู่ในมือของ regex Gods เช่น @CasimiretHippolyte และ @HamZa มันคือ:

(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant

ในกรณีของคุณ:

(?m)(?:^.*\.$|\([^()]*\)|if\(.*?//endif)(*SKIP)(*F)|\b\d+\b

รูปแบบนี้ใช้งานได้ง่ายกว่าเล็กน้อยเนื่องจากเนื้อหาที่จับคู่ในบริบท s1, s2 และ s3 นั้นถูกข้ามไปดังนั้นคุณจึงไม่จำเป็นต้องตรวจสอบการจับภาพกลุ่ม 1 (สังเกตว่าวงเล็บจะหายไป) การแข่งขันมีเพียงwhatYouWant

โปรดทราบว่า(*F), (*FAIL)และ(?!)ทุกคนในสิ่งเดียวกัน หากคุณต้องการปกปิดมากขึ้นคุณสามารถใช้ไฟล์(*SKIP)(?!)

การสาธิตสำหรับเวอร์ชันนี้

การใช้งาน

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

  1. ฉันจะตรงกับ foo ยกเว้นใดก็ได้ในแท็กเหมือน<a stuff...>...</a>?
  2. ฉันจะจับคู่ foo ได้อย่างไรยกเว้นใน<i>แท็กหรือข้อมูลโค้ดจาวาสคริปต์ (เงื่อนไขเพิ่มเติม)
  3. ฉันจะจับคู่คำทั้งหมดที่ไม่อยู่ในบัญชีดำนี้ได้อย่างไร?
  4. ฉันจะเพิกเฉยต่อสิ่งที่อยู่ในบล็อก SUB ... END SUB ได้อย่างไร?
  5. ฉันจะจับคู่ทุกอย่างยกเว้น ... s1 s2 s3 ได้อย่างไร

วิธีการตั้งโปรแกรมจับภาพกลุ่ม 1

คุณไม่ได้เป็นรหัส แต่เพื่อให้เสร็จสมบูรณ์ ... รหัสในการตรวจสอบกลุ่ม 1 จะขึ้นอยู่กับภาษาที่คุณเลือกอย่างชัดเจน ไม่ว่าในกรณีใดก็ตามไม่ควรเพิ่มเกินสองสามบรรทัดในโค้ดที่คุณจะใช้ตรวจสอบการจับคู่

หากมีข้อสงสัยฉันขอแนะนำให้คุณดูส่วนตัวอย่างโค้ดของบทความที่กล่าวถึงก่อนหน้านี้ซึ่งนำเสนอโค้ดสำหรับภาษาไม่กี่ภาษา

ทางเลือก

ขึ้นอยู่กับความซับซ้อนของคำถามและในเอนจิ้น regex ที่ใช้มีหลายทางเลือก นี่คือสองข้อที่สามารถใช้ได้กับสถานการณ์ส่วนใหญ่รวมถึงเงื่อนไขต่างๆ ในมุมมองของฉันไม่มีอะไรน่าสนใจเท่าs1|s2|s3|(whatYouWant)สูตรอาหารถ้าเพียงเพราะความชัดเจนมักจะชนะ

1. แทนที่แล้วจับคู่

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

2. Lookarounds

โพสต์ต้นฉบับของคุณแสดงให้เห็นว่าคุณเข้าใจวิธีการยกเว้นเงื่อนไขเดียวโดยใช้การค้นหา คุณบอกว่า C # ดีมากสำหรับสิ่งนี้และคุณพูดถูก แต่ไม่ใช่ตัวเลือกเดียว NET regex รสชาติที่พบใน C #, VB.NET และ Visual C ++ เช่นเดียวกับregexโมดูลที่ยังคงทดลองเพื่อแทนที่reใน Python เป็นเพียงสองเอ็นจิ้นที่ฉันรู้ว่ารองรับการมองแบบไม่มีที่สิ้นสุด ด้วยเครื่องมือเหล่านี้เงื่อนไขเดียวในรูปลักษณ์เดียวเบื้องหลังสามารถดูแลไม่เพียง แต่มองข้างหลัง แต่ยังรวมถึงการแข่งขันและนอกเหนือจากการแข่งขันโดยไม่ต้องประสานงานกับผู้มอง เงื่อนไขเพิ่มเติม? การค้นหาเพิ่มเติม

การรีไซเคิล regex ที่คุณมีสำหรับ s3 ใน C # รูปแบบทั้งหมดจะเป็นแบบนี้

(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

แต่ตอนนี้คุณก็รู้ว่าฉันไม่แนะนำสิ่งนี้ใช่ไหม?

การลบ

@HamZa และ @Jerry WhatYouWantได้แนะนำให้ผมพูดถึงเคล็ดลับเพิ่มเติมสำหรับกรณีเมื่อคุณพยายามที่จะเพียงแค่ลบ คุณจำได้ว่าสูตรที่จะจับคู่WhatYouWant(จับเป็นกลุ่ม 1) คือs1|s2|s3|(WhatYouWant)ใช่ไหม? ในการลบอินสแตนซ์ทั้งหมดWhatYouWantคุณเปลี่ยน regex เป็น

(s1|s2|s3)|WhatYouWant

$1สำหรับสตริงทดแทนคุณใช้ สิ่งที่เกิดขึ้นที่นี่คือสำหรับแต่ละอินสแตนซ์s1|s2|s3ที่ตรงกันการแทนที่จะ$1แทนที่อินสแตนซ์นั้นด้วยตัวมันเอง (อ้างอิงโดย$1) ในทางกลับกันเมื่อWhatYouWantมีการจับคู่มันจะถูกแทนที่ด้วยกลุ่มว่างเปล่าและไม่มีอะไรอื่น - ดังนั้นจึงถูกลบ ดูการสาธิตนี้ขอบคุณ @HamZa และ @Jerry ที่แนะนำเพิ่มเติมที่ยอดเยี่ยมนี้

การเปลี่ยน

สิ่งนี้นำเราไปสู่การเปลี่ยนซึ่งฉันจะพูดสั้น ๆ

  1. เมื่อแทนที่โดยไม่มีอะไรให้ดูเคล็ดลับ "การลบ" ด้านบน
  2. เมื่อเปลี่ยนหากใช้ Perl หรือ PCRE ให้ใช้(*SKIP)(*F)รูปแบบที่กล่าวถึงข้างต้นเพื่อให้ตรงกับสิ่งที่คุณต้องการและทำการแทนที่แบบตรง
  3. ในรสชาติอื่น ๆ ภายในการเรียกฟังก์ชันการแทนที่ให้ตรวจสอบการจับคู่โดยใช้การเรียกกลับหรือแลมบ์ดาและแทนที่หากมีการตั้งค่ากลุ่ม 1 หากคุณต้องการความช่วยเหลือบทความที่อ้างถึงแล้วจะให้รหัสแก่คุณในภาษาต่างๆ

มีความสุข!

ไม่รอยังมีอีก!

อาไม่ฉันจะเก็บบันทึกไว้เป็นความทรงจำของฉันในยี่สิบเล่มเพื่อออกในฤดูใบไม้ผลิปีหน้า


2
@ โกบีสองส่วนตอบกลับ. ใช่เมื่อคืนได้ดำเนินการเขียนไปแล้วและเขียนไว้ที่ด้านล่างว่าฉันจะนอนบนนั้นและจะทำให้เป็นระเบียบในภายหลัง :) ใช่เคล็ดลับนั้นง่าย แต่ฉันไม่ได้บอกคุณว่ามันเป็น "พื้นฐาน" เพราะดูเหมือนจะไม่ได้เป็นส่วนหนึ่งของเครื่องมือทั่วไปที่คนทั่วไปใช้ในการแก้ปัญหาการกีดกัน เมื่อฉัน googled สำหรับปัญหา "ยกเว้น" หรือ "เว้นแต่" หรือ "ไม่อยู่ใน" ใน SO มีเพียงคำตอบเดียว (ที่ไม่มีการโหวต) ที่แนะนำไม่มีข้อใดทำ ฉันไม่เห็นคำตอบของคุณเลยซึ่งยอดเยี่ยมมาก :)
zx81

2
ขออภัย "เคล็ดลับที่ดีที่สุด" ของ Rex ไม่ได้ผล ( อย่างน่าเชื่อถือ ) สมมติว่าคุณต้องการจับคู่Tarzanแต่ไม่ใช่เมื่อใดก็ตามภายในเครื่องหมายคำพูดคู่ นี้/no|no|(yes)/regex เคล็ดลับจะเป็นสิ่งที่ชอบ/"[^"]*"|Tarzan/(ละเลยตัวอักษรหนี) นี้จะทำงานหลายกรณี แต่ล้มเหลวอย่างสมบูรณ์เมื่อนำไปใช้กับข้อความ JavaScript var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";ที่ถูกต้องต่อไปนี้: เคล็ดลับของ Rex ใช้งานได้ก็ต่อเมื่อโครงสร้างที่เป็นไปได้ทั้งหมดตรงกันกล่าวคือคุณต้องแยกวิเคราะห์ข้อความทั้งหมดเพื่อรับประกันความถูกต้อง 100%
ridgerunner

1
ขออภัยหากฉันฟังดูรุนแรงนั่นไม่ใช่เจตนาของฉันอย่างแน่นอน ประเด็นของฉัน (เช่นเดียวกับในความคิดเห็นที่สองของฉันต่อคำถามเดิมด้านบน) คือวิธีแก้ปัญหาที่ถูกต้องขึ้นอยู่กับข้อความเป้าหมายที่กำลังค้นหา ตัวอย่างของฉันมีซอร์สโค้ด JavaScript เป็นข้อความเป้าหมายซึ่งมีเครื่องหมายอัญประกาศคู่หนึ่งอยู่ภายในสตริงที่มีเครื่องหมายอัญประกาศ มันอาจเป็น RegExp ตามตัวอักษรได้อย่างง่ายดายเช่น: var bug1 = /"[^"]*"|(Tarzan)/gi;และมีผลเช่นเดียวกัน (และตัวอย่างที่สองนี้ไม่ใช่กรณีขอบ) มีตัวอย่างอีกมากมายที่ฉันสามารถอ้างอิงได้ว่าเทคนิคนี้ไม่สามารถทำงานได้อย่างน่าเชื่อถือ
ridgerunner

1
@ridgerunner ฉันชอบที่จะได้ยินจากคุณเสมอมันฟังดูรุนแรงสำหรับฉันอย่างไม่มีเหตุผล เมื่อเรารู้ว่าสตริงของเราสามารถมี "การแจ้งเตือนที่ผิดพลาด" เราทุกคนก็ปรับเปลี่ยนรูปแบบของเรา ตัวอย่างเช่นในการจับคู่สตริงที่อาจมีเครื่องหมายคำพูดที่ใช้ Escape ซึ่งอาจทำให้ตัวจับคู่สตริงหลุดคุณอาจใช้(?<!\\)"(?:\\"|[^"\r\n])*+" You don't pull the big gun เว้นแต่คุณจะมีเหตุผล หลักการของการแก้ปัญหายังคงใช้ได้ หากเราไม่สามารถแสดงรูปแบบที่จะวางทางด้านซ้ายนั่นเป็นเรื่องที่แตกต่างออกไปเราจำเป็นต้องมีวิธีแก้ปัญหาที่แตกต่างออกไป แต่การแก้ปัญหาไม่ได้เป็นสิ่งที่โฆษณา
zx81

1
คำตอบนี้ได้ถูกเพิ่มลงในคำถามที่พบบ่อยเกี่ยวกับนิพจน์ทั่วไปของStack Overflowโดยผู้ใช้ @funkwurm
aliteralmind

11

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

แก้ไข: ขอขยายความหน่อยเพราะคำถามน่าสนใจมากขึ้น :-)

แนวคิดทั่วไปที่คุณพยายามจับภาพที่นี่คือจับคู่กับรูปแบบ regex บางอย่าง แต่ไม่ใช่เมื่อมีรูปแบบอื่น ๆ (อาจเป็นตัวเลขใดก็ได้) อยู่ในสตริงการทดสอบ โชคดีที่คุณสามารถใช้ประโยชน์จากภาษาการเขียนโปรแกรมของคุณ: ทำให้ regexes เป็นเรื่องง่ายและใช้เงื่อนไขแบบผสม แนวทางปฏิบัติที่ดีที่สุดคือการจับความคิดนี้ในองค์ประกอบที่ใช้ซ้ำได้ดังนั้นเรามาสร้างคลาสและวิธีการที่นำไปใช้:

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public class MatcherWithExceptions {
  private string m_searchStr;
  private Regex m_searchRegex;
  private IEnumerable<Regex> m_exceptionRegexes;

  public string SearchString {
    get { return m_searchStr; }
    set {
      m_searchStr = value;
      m_searchRegex = new Regex(value);
    }
  }

  public string[] ExceptionStrings {
    set { m_exceptionRegexes = from es in value select new Regex(es); }
  }

  public bool IsMatch(string testStr) {
    return (
      m_searchRegex.IsMatch(testStr)
      && !m_exceptionRegexes.Any(er => er.IsMatch(testStr))
    );
  }
}

public class App {
  public static void Main() {
    var mwe = new MatcherWithExceptions();

    // Set up the matcher object.
    mwe.SearchString = @"\b\d{5}\b";
    mwe.ExceptionStrings = new string[] {
      @"\.$"
    , @"\(.*" + mwe.SearchString + @".*\)"
    , @"if\(.*" + mwe.SearchString + @".*//endif"
    };

    var testStrs = new string[] {
      "1." // False
    , "11111." // False
    , "(11111)" // False
    , "if(11111//endif" // False
    , "if(11111" // True
    , "11111" // True
    };

    // Perform the tests.
    foreach (var ts in testStrs) {
      System.Console.WriteLine(mwe.IsMatch(ts));
    }
  }
}

ข้างต้นเราตั้งค่าสตริงการค้นหา (ตัวเลขห้าหลัก) สตริงข้อยกเว้นหลายรายการ ( s1 , s2และs3 ของคุณ ) จากนั้นพยายามจับคู่กับสตริงทดสอบหลาย ๆ ผลลัพธ์ที่พิมพ์ควรเป็นดังที่แสดงในความคิดเห็นถัดจากแต่ละสตริงการทดสอบ


2
คุณหมายถึงอาจจะชอบจับคู่ regex สามตัวในแถว? Regex 1 กำจัดสถานการณ์ 1 (อาจจะแค่ลบตัวเลขที่ไม่ดี), r2 ลบ s2, r3 ลบ s3 และจับคู่ตัวเลขทางซ้าย? นั่นเป็นความคิดที่น่าสนใจ
Hans Schindler

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

2

ข้อกำหนดของคุณที่ว่ามันไม่ได้อยู่ในห้องเก็บของและไม่สามารถทำให้อิ่มได้ในทุกกรณี กล่าวคือถ้าคุณสามารถหา(ทางซ้ายและ)ทางขวาได้ก็ไม่ได้หมายความว่าคุณจะอยู่ในห้องเก็บของเสมอไป เช่น.

(....) + 55555 + (.....)- ไม่ได้อยู่ใน parens แต่มี(และ)ไปทางซ้ายและขวา

ตอนนี้คุณอาจคิดว่าตัวเองฉลาดและมองหา(ทางซ้ายเฉพาะในกรณีที่คุณไม่เจอ)มาก่อนและกลับกันทางขวา สิ่งนี้ใช้ไม่ได้กับกรณีนี้:

((.....) + 55555 + (.....))- ภายใน parens แม้ว่าจะมีการปิด)และ(ไปทางซ้ายและทางขวา

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

พิจารณางานนี้ง่ายขึ้น: การใช้ regex พบว่าทั้งหมด (อาจจะซ้อนกัน) parens ในสตริงจะปิดที่มีไว้สำหรับทุกคนที่คุณต้องการค้นหา( )คุณจะพบว่ามันเป็นไปไม่ได้ที่จะแก้และถ้าคุณไม่สามารถแก้มันด้วย regex คุณจะไม่สามารถรู้ได้ว่าคำนั้นอยู่ใน parens สำหรับทุกกรณีหรือไม่เนื่องจากคุณไม่สามารถหาตำแหน่งใดตำแหน่งหนึ่งในสตริงได้หาก ทั้งหมดก่อนหน้า(นี้มี).


2
ไม่มีใครพูดอะไรเกี่ยวกับวงเล็บที่ซ้อนกันและกรณี # 1 ของคุณได้รับการจัดการอย่างดีโดยคำตอบของ zx81
Dan Bechard

ขอบคุณสำหรับความคิดที่ดี :) แต่วงเล็บที่ซ้อนกันไม่ทำให้ฉันกังวลสำหรับคำถามนี้มันเกี่ยวกับความคิดของสถานการณ์ที่ไม่ดี s1 s2 s3
Hans Schindler

แน่นอนว่าเป็นไปไม่ได้! นี่คือเหตุผลที่คุณต้องติดตามระดับของ parens ที่คุณกำลังแยกวิเคราะห์อยู่
MrWonderful

ถ้าคุณกำลังแยกวิเคราะห์ CFG บางประเภทเช่น OP ดูเหมือนว่าคุณจะทำหน้าที่ได้ดีกว่าโดยการสร้าง LALR หรือตัวแยกวิเคราะห์ที่คล้ายกันซึ่งไม่มีปัญหากับสิ่งนี้
RokL

2

ฮันส์ถ้าคุณไม่รังเกียจฉันใช้เครื่องซักผ้าของเพื่อนบ้านชื่อ perl :)

แก้ไข: ด้านล่างรหัสหลอก:

  loop through input
  if line contains 'if(' set skip=true
        if skip= true do nothing
        else
           if line match '\b\d{5}\b' set s0=true
           if line does not match s1 condition  set s1=true
           if line does not match s2 condition  set s2=true
           if s0,s1,s2 are true print line 
  if line contains '//endif' set skip=false

ระบุไฟล์ input.txt:

tiago@dell:~$ cat input.txt 
this is a text
it should match 12345
if(
it should not match 12345
//endif 
it should match 12345
it should not match 12345.
it should not match ( blabla 12345  blablabla )
it should not match ( 12345 )
it should match 12345

และสคริปต์ validator.pl:

tiago@dell:~$ cat validator.pl 
#! /usr/bin/perl
use warnings;
use strict;
use Data::Dumper;

sub validate_s0 {
    my $line = $_[0];
    if ( $line =~ \d{5/ ){
        return "true";
    }
    return "false";
}

sub validate_s1 {
    my $line = $_[0];
    if ( $line =~ /\.$/ ){
        return "false";
    }
    return "true";
}

sub validate_s2 {
    my $line = $_[0];
    if ( $line =~ /.*?\(.*\d{5.*?\).*/ ){
        return "false";
    }
    return "true";
}

my $skip = "false";
while (<>){
    my $line = $_; 

    if( $line =~ /if\(/ ){
       $skip = "true";  
    }

    if ( $skip eq "false" ) {
        my $s0_status = validate_s0 "$line"; 
        my $s1_status = validate_s1 "$line";
        my $s2_status = validate_s2 "$line";

        if ( $s0_status eq "true"){
            if ( $s1_status eq "true"){
                if ( $s2_status eq "true"){
                    print "$line";
                }
            }
        }
    } 

    if ( $line =~ /\/\/endif/) {
        $skip="false";
    }
}

การดำเนินการ:

tiago @ dell: ~ $ cat input.txt | perl validator.pl
ควรตรงกับ 12345
ควรตรงกับ 12345
ควรตรงกับ 12345

2

ไม่แน่ใจว่าสิ่งนี้จะช่วยคุณได้หรือไม่ แต่ฉันกำลังเสนอวิธีแก้ปัญหาโดยพิจารณาจากสมมติฐานต่อไปนี้ -

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

อย่างไรก็ตามฉันพิจารณาสิ่งต่อไปนี้ด้วย -

  1. ไฟล์ที่ให้มามีข้อผิดพลาดน้อยที่สุด หากเป็นเช่นนั้นรหัสของฉันอาจต้องมีการปรับเปลี่ยนเพื่อรับมือกับสิ่งนั้น
  2. ฉันใช้ Stack เพื่อติดตามif(บล็อก

ตกลงนี่คือทางออก -

ฉันใช้ C # และใช้ MEF (Microsoft Extensibility Framework) เพื่อใช้ตัวแยกวิเคราะห์ที่กำหนดค่าได้ แนวคิดคือใช้ตัวแยกวิเคราะห์เดียวเพื่อแยกวิเคราะห์และรายการของคลาสตัวตรวจสอบที่กำหนดค่าได้เพื่อตรวจสอบความถูกต้องของบรรทัดและส่งคืนจริงหรือเท็จตามการตรวจสอบความถูกต้อง จากนั้นคุณสามารถเพิ่มหรือลบตัวตรวจสอบความถูกต้องได้ตลอดเวลาหรือเพิ่มตัวตรวจสอบใหม่ก็ได้หากต้องการ จนถึงตอนนี้ฉันได้ติดตั้ง S1, S2 และ S3 ที่คุณพูดถึงแล้วตรวจสอบคลาสที่จุด 3 คุณต้องเพิ่มคลาสสำหรับ s4, s5 หากคุณต้องการในอนาคต

  1. ขั้นแรกสร้างอินเทอร์เฟซ -

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace FileParserDemo.Contracts
    {
        public interface IParser
        {
            String[] GetMatchedLines(String filename);
        }
    
        public interface IPatternMatcher
        {
            Boolean IsMatched(String line, Stack<string> stack);
        }
    }
  2. จากนั้นโปรแกรมอ่านและตัวตรวจสอบไฟล์ -

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using FileParserDemo.Contracts;
    using System.ComponentModel.Composition.Hosting;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Collections;
    
    namespace FileParserDemo.Parsers
    {
        public class Parser : IParser
        {
            [ImportMany]
            IEnumerable<Lazy<IPatternMatcher>> parsers;
            private CompositionContainer _container;
    
            public void ComposeParts()
            {
                var catalog = new AggregateCatalog();
                catalog.Catalogs.Add(new AssemblyCatalog(typeof(IParser).Assembly));
                _container = new CompositionContainer(catalog);
                try
                {
                    this._container.ComposeParts(this);
                }
                catch
                {
    
                }
            }
    
            public String[] GetMatchedLines(String filename)
            {
                var matched = new List<String>();
                var stack = new Stack<string>();
                using (StreamReader sr = File.OpenText(filename))
                {
                    String line = "";
                    while (!sr.EndOfStream)
                    {
                        line = sr.ReadLine();
                        var m = true;
                        foreach(var matcher in this.parsers){
                            m = m && matcher.Value.IsMatched(line, stack);
                        }
                        if (m)
                        {
                            matched.Add(line);
                        }
                     }
                }
                return matched.ToArray();
            }
        }
    }
  3. จากนั้นก็มีการใช้งานหมากฮอสแต่ละตัวชื่อชั้นเรียนเป็นตัวอธิบายดังนั้นฉันไม่คิดว่าพวกเขาต้องการคำอธิบายเพิ่มเติม

    using FileParserDemo.Contracts;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading.Tasks;
    
    namespace FileParserDemo.PatternMatchers
    {
        [Export(typeof(IPatternMatcher))]
        public class MatchAllNumbers : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("\\d+");
                return regex.IsMatch(line);
            }
        }
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveIfBlock : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("if\\(");
                if (regex.IsMatch(line))
                {
                    foreach (var m in regex.Matches(line))
                    {
                        //push the if
                        stack.Push(m.ToString());
                    }
                    //ignore current line, and will validate on next line with stack
                    return true;
                }
                regex = new Regex("//endif");
                if (regex.IsMatch(line))
                {
                    foreach (var m in regex.Matches(line))
                    {
                        stack.Pop();
                    }
                }
                return stack.Count == 0; //if stack has an item then ignoring this block
            }
        }
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveWithEndPeriod : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("(?m)(?!\\d+.*?\\.$)\\d+");
                return regex.IsMatch(line);
            }
        }
    
    
        [Export(typeof(IPatternMatcher))]
        public class RemoveWithInParenthesis : IPatternMatcher
        {
            public Boolean IsMatched(String line, Stack<string> stack)
            {
                var regex = new Regex("\\(.*\\d+.*\\)");
                return !regex.IsMatch(line);
            }
        }
    }
  4. โปรแกรม -

    using FileParserDemo.Contracts;
    using FileParserDemo.Parsers;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace FileParserDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                var parser = new Parser();
                parser.ComposeParts();
                var matches = parser.GetMatchedLines(Path.GetFullPath("test.txt"));
                foreach (var s in matches)
                {
                    Console.WriteLine(s);
                }
                Console.ReadLine();
            }
        }
    }

สำหรับการทดสอบฉันใช้ไฟล์ตัวอย่างของ @ Tiago Test.txtซึ่งมีบรรทัดต่อไปนี้ -

this is a text
it should match 12345
if(
it should not match 12345
//endif 
it should match 12345
it should not match 12345.
it should not match ( blabla 12345  blablabla )
it should not match ( 12345 )
it should match 12345

ให้ผลลัพธ์ -

it should match 12345
it should match 12345
it should match 12345

ไม่รู้ว่านี่จะช่วยคุณได้หรือไม่ฉันสนุกกับการเล่นกับมัน .... :)

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


2

เช่นเดียวกับ @ zx81 (*SKIP)(*F)แต่ใช้การยืนยันผู้มองเชิงลบ

(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)

การสาธิต

ใน python ฉันจะทำได้ง่ายๆแบบนี้

import re
string = """cat 123 sat.
I like 000 not (456) though 111 is fine
222 if(  //endif if(cat==789 stuff  //endif   333"""
for line in string.split('\n'):                                  # Split the input according to the `\n` character and then iterate over the parts.
    if not line.endswith('.'):                                   # Don't consider the part which ends with a dot.
        for i in re.split(r'\([^()]*\)|if\(.*?//endif', line):   # Again split the part by brackets or if condition which endswith `//endif` and then iterate over the inner parts.
            for j in re.findall(r'\b\d+\b', i):                  # Then find all the numbers which are present inside the inner parts and then loop through the fetched numbers.
                print(j)                                         # Prints the number one ny one.

เอาท์พุต:

000
111
222
333
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.