คุณลักษณะใหม่มากมายที่ Ruby 2.0 จัดส่งกลับมาในปี 2013 คุณลักษณะที่ฉันให้ความสนใจน้อยที่สุดคือ Onigmo เอ็นจิ้นนิพจน์ทั่วไปใหม่ ท้ายที่สุด นิพจน์ทั่วไปก็คือนิพจน์ทั่วไป ทำไมฉันจึงควรสนใจว่า Ruby นำไปใช้อย่างไร
ตามที่ปรากฎ เอ็นจิ้น Onigmo regex มีเทคนิคเล็กๆ น้อยๆ บางอย่างรวมถึงความสามารถในการใช้เงื่อนไขภายในนิพจน์ทั่วไปของคุณ
ในโพสต์นี้ เราจะเจาะลึกถึงเงื่อนไข regex เรียนรู้เกี่ยวกับความไม่ชอบมาพากลของการนำ Ruby ไปใช้ และหารือเกี่ยวกับกลเม็ดเล็กๆ น้อยๆ เพื่อแก้ไขข้อจำกัดของ Ruby เริ่มกันเลย!
จับกลุ่ม &จับ
ในการทำความเข้าใจเงื่อนไขในนิพจน์ทั่วไป คุณต้องเข้าใจการจัดกลุ่มและการจับภาพก่อน
ลองนึกภาพว่าคุณมีรายการอ้างอิงในสหรัฐอเมริกา:
Fayetteville, AR
Seattle, WA
คุณต้องการแยกชื่อเมืองออกจากตัวย่อของรัฐ วิธีหนึ่งในการทำเช่นนี้คือทำการแข่งขันหลายรายการ:
PLACE = "Fayetteville, AR"
# City: Match any char that's not a comma
PLACE.match(/[^,]+/)
# => #<MatchData "Fayetteville">
# Separator: Match a comma and optional spaces
PLACE.match(/, */)
# => #<MatchData ", ">
# State: Match a 2-letter code at the end of the string.
PLACE.match(/[A-Z]{2}$/)
# => #<MatchData "AR">
ใช้งานได้ แต่มันละเอียดเกินไป ด้วยการใช้กลุ่ม คุณสามารถจับภาพทั้งเมืองและรัฐด้วยนิพจน์ทั่วไปเพียงนิพจน์เดียว
เรามารวมนิพจน์ทั่วไปด้านบนเข้าด้วยกัน แล้วล้อมแต่ละส่วนด้วยวงเล็บ Parens คือวิธีที่คุณจัดกลุ่มสิ่งต่าง ๆ ในนิพจน์ทั่วไป
PLACE = "Fayetteville, AR"
m = PLACE.match(/([^,]+)(, *)([A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:", " 3:"AR">
อย่างที่คุณเห็น นิพจน์ด้านบนครอบคลุมทั้งเมืองและรัฐ คุณเข้าถึงได้โดยปฏิบัติกับ MatchData
เช่นอาร์เรย์:
m[1]
# => "Fayetteville"
m[3]
# => "AR"
ปัญหาเกี่ยวกับการจัดกลุ่มดังที่ทำไว้ข้างต้นคือข้อมูลที่ดักจับถูกใส่ลงในอาร์เรย์ หากตำแหน่งของมันในอาร์เรย์เปลี่ยนไป คุณต้องอัปเดตโค้ดของคุณ ไม่เช่นนั้นคุณเพิ่งแนะนำจุดบกพร่อง
ตัวอย่างเช่น เราอาจตัดสินใจว่าการจับภาพ ", "
. เป็นเรื่องโง่ ตัวอักษร ดังนั้นเราจึงลบ paren รอบส่วนนั้นของนิพจน์ทั่วไป:
m = PLACE.match(/([^,]+), *([A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:"AR">
m[3]
# => nil
แต่ตอนนี้ m[3]
ไม่มีสถานะอีกต่อไป - เมืองบั๊ก
ตั้งชื่อกลุ่ม
คุณสามารถสร้างกลุ่มนิพจน์ทั่วไปให้มีความหมายมากขึ้นได้โดยการตั้งชื่อ ไวยากรณ์ค่อนข้างคล้ายกับที่เราเพิ่งใช้ เราล้อม regex ด้วย parens และระบุชื่อดังนี้:
/(?<groupname>regex)/
หากเราใช้สิ่งนี้กับนิพจน์ทั่วไปของเมือง/รัฐ เราจะได้:
m = PLACE.match(/(?<city>[^,]+), *(?<state>[A-Z]{2})/)
# => #<MatchData "Fayetteville, AR" city:"Fayetteville" state:"AR">
และเราสามารถเข้าถึงข้อมูลที่บันทึกไว้โดยจัดการ MatchData
ชอบแฮช:
m[:city]
# => "Fayetteville"
เงื่อนไข
เงื่อนไขในนิพจน์ทั่วไปอยู่ในรูปแบบ /(?(A)X|Y)/
. ต่อไปนี้คือวิธีที่ถูกต้องบางประการ:
# If A is true, then evaluate the expression X, else evaluate Y
/(?(A)X|Y)/
# If A is true, then X
/(?(A)X)/
# If A is false, then Y
/(?(A)|Y)/
สองตัวเลือกที่พบบ่อยที่สุดสำหรับอาการของคุณ A
คือ:
- มีการจับกลุ่มที่มีชื่อหรือหมายเลขหรือไม่
- การมองไปรอบๆ ประเมินว่าเป็นจริงหรือไม่
มาดูวิธีใช้กัน:
มีการจับกลุ่มไหม
หากต้องการตรวจสอบการมีอยู่ของกลุ่ม ให้ใช้ ?(n)
ไวยากรณ์ โดยที่ n เป็นจำนวนเต็ม หรือชื่อกลุ่มล้อมรอบด้วย <>
หรือ ''
.
# Has group number 1 been captured?
/(?(1)foo|bar)/
# Has a group named "mygroup" been captured?
/(?(<mygroup>)foo|bar)/
ตัวอย่าง
ลองนึกภาพว่าคุณกำลังแยกวิเคราะห์หมายเลขโทรศัพท์ในสหรัฐอเมริกา ตัวเลขเหล่านี้มีรหัสพื้นที่สามหลักซึ่งเป็นทางเลือก เว้นแต่ว่าตัวเลขจะขึ้นต้นด้วยหนึ่ง
1-800-555-1212 # Valid
800-555-1212 # Valid
555-1212 # Valid
1-555-1212 # INVALID!!
เราสามารถใช้เงื่อนไขเพื่อทำให้รหัสพื้นที่เป็นข้อกำหนดได้ก็ต่อเมื่อตัวเลขเริ่มต้นด้วย 1
# This regular expression looks complex, but it's made of simple pieces
# `^(1-)?` Does the string start with "1-"? If so, capture it as group 1
# `(?(1)` Was anything captured in group one?
# `\d{3}-` if so, do a required match of three digits and a dash (the area code)
# `|(\d{3}-)?` if not, do an optional match of three digits and a dash (area code)
# `\d{3}-\d{4}` match the rest of the phone number, which is always required.
re = /^(1-)?(?(1)\d{3}-|(\d{3}-)?)\d{3}-\d{4}/
"1-800-555-1212".match(re)
#=> #<MatchData "1-800-555-1212" 1:"1-" 2:nil>
"800-555-1212".match(re)
#=> #<MatchData "800-555-1212" 1:nil 2:"800-">
"555-1212".match(re)
#=> #<MatchData "555-1212" 1:nil 2:nil>
"1-555-1212".match(re)
=> nil
ข้อจำกัด
ปัญหาหนึ่งในการใช้เงื่อนไขแบบกลุ่มคือการจับคู่กลุ่ม "ใช้" อักขระเหล่านั้นในสตริง อักขระเหล่านั้นไม่สามารถใช้โดยเงื่อนไขได้
ตัวอย่างเช่น รหัสต่อไปนี้พยายามและไม่จับคู่ 100 หากมีข้อความ "USD":
"100USD".match(/(USD)(?(1)\d+)/) # nil
ใน Perl และภาษาอื่นๆ คุณสามารถเพิ่มคำสั่ง look-ahead ให้กับเงื่อนไขของคุณได้ ซึ่งช่วยให้คุณทริกเกอร์เงื่อนไขตามข้อความที่ใดก็ได้ในสตริง แต่รูบี้ไม่มีสิ่งนี้ เราจึงต้องสร้างสรรค์สักหน่อย
มองไปรอบๆ
โชคดีที่เราสามารถหลีกเลี่ยงข้อจำกัดในเงื่อนไข regex ของ Ruby ได้โดยใช้นิพจน์ look-around ในทางที่ผิด
มองไปรอบๆ คืออะไร
โดยปกติ parser นิพจน์ทั่วไปจะผ่านสตริงของคุณตั้งแต่ต้นจนจบเพื่อค้นหารายการที่ตรงกัน เหมือนกับการเลื่อนเคอร์เซอร์จากซ้ายไปขวาในโปรแกรมประมวลผลคำ
นิพจน์มองไปข้างหน้าและมองข้างหลังทำงานแตกต่างกันเล็กน้อย ช่วยให้คุณตรวจสอบสตริงโดยไม่ต้องใช้อักขระใดๆ เมื่อเสร็จแล้ว เคอร์เซอร์จะอยู่ที่จุดเดิมที่จุดเริ่มต้น
สำหรับข้อมูลเบื้องต้นที่ดีเกี่ยวกับการมองไปรอบๆ โปรดดูคำแนะนำของ Reexegg ในการมองไปข้างหน้าและมองไปข้างหลัง
ไวยากรณ์มีลักษณะดังนี้:
ประเภท | ไวยากรณ์ | ตัวอย่าง |
---|---|---|
มองไปข้างหน้า | (?=query) | \d+(?= dollars) จับคู่ 100 ใน "100 ดอลลาร์" |
มองไปข้างหน้าในแง่ลบ | (?!query) | \d+(?! dollars) จับคู่ 100 หากไม่ตามด้วยคำว่า "ดอลลาร์" |
ดูย้อนหลัง | (?<=query) | (?<=lucky )\d ตรงกับ 7 ใน "โชคดี 7" |
มองในแง่ลบ | (?<!query) | (?<!furious )\d ตรงกับ 7 ใน "โชคดี 7" |
การใช้ look-around ในทางที่ผิดเพื่อปรับปรุงเงื่อนไข
ในเงื่อนไขของเรา เราสามารถสอบถามการมีอยู่ของกลุ่มที่ตั้งค่าไว้แล้วเท่านั้น โดยปกติ นี่หมายความว่าเนื้อหาของกลุ่มถูกใช้ไปแล้วและไม่สามารถใช้ได้กับแบบมีเงื่อนไข
แต่คุณสามารถใช้การมองไปข้างหน้าเพื่อตั้งกลุ่มโดยไม่ต้องใช้อักขระใด ๆ ! สติแตกรึยัง?
จำรหัสนี้ที่ใช้ไม่ได้หรือไม่
"100USD".match(/(USD)(?(1)\d+)/) # nil
หากเราแก้ไขเพื่อจับภาพกลุ่มล่วงหน้า มันก็ใช้งานได้ดี:
"100USD".match(/(?=.*(USD))(?(1)\d+)/)
=> #<MatchData "100" 1:"USD">
มาแยกย่อยคำถามนั้นและดูว่าเกิดอะไรขึ้น:
(?=.*(USD))
ใช้การมองไปข้างหน้า สแกนข้อความเพื่อหา "USD" และจับกลุ่มที่ 1(?(1)
หากมีกลุ่มที่ 1\d+
จากนั้นจับคู่ตัวเลขอย่างน้อยหนึ่งตัว
ค่อนข้างเรียบร้อยใช่มั้ย