Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การใช้เงื่อนไขภายในนิพจน์ทั่วไปของ Ruby

คุณลักษณะใหม่มากมายที่ 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+ จากนั้นจับคู่ตัวเลขอย่างน้อยหนึ่งตัว

ค่อนข้างเรียบร้อยใช่มั้ย