Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm chủ Onigmo của Ruby 2.0: Sử dụng Conditional Expressions trong Regular Expressions

Trong nhiều tính năng mới Ruby 2.0 ship năm 2013, cái tôi ít chú ý nhất là regex engine mới, Onigmo.畢竟, regular expressions là regular expressions - tại sao tôi phải quan tâm Ruby implement chúng như thế nào?

Như kết quả, Onigmo regex engine có vài trick neat trong tay áo bao gồm khả năng dùng conditionals bên trong regular expressions.

Trong bài này, chúng ta sẽ đào sâu vào regex conditionals, học về quirks của implementation Ruby, và thảo luận vài trick workaround hạn chế của Ruby. Hãy bắt đầu!

Groups & Capturing

Để hiểu conditionals trong regular expressions, bạn trước hết cần hiểu grouping và capturing.

Tưởng tượng bạn có danh sách US cities:

Fayetteville, AR Seattle, WA 

Bạn muốn tách city name từ state abbreviation. Cách là multiple matches:

PLACE = "Fayetteville, AR" # City: Match bất kỳ char không phải comma PLACE.match(/[^,]+/) # => #<MatchData "Fayetteville"> # Separator: Match comma và optional spaces PLACE.match(/, */) # => #<MatchData ", "> # State: Match 2-letter code ở cuối string. PLACE.match(/[A-Z]{2}$/) # => #<MatchData "AR"> 

Điều này chạy, nhưng quá verbose. Bằng cách dùng groups, bạn capture cả city và state chỉ với một regex.

Hãy combine regex trên, và bao quanh mỗi section bằng parentheses. Parens là cách group trong regex.

PLACE = "Fayetteville, AR" m = PLACE.match(/([^,]+)(, *)([A-Z]{2})/) # => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:", " 3:"AR"> 

Như bạn thấy, expression trên capture cả city và state. Bạn truy cập bằng cách treat MatchData như array:

m[1] # => "Fayetteville" m[3] # => "AR" 

Vấn đề với grouping như trên là captured data được đưa vào array. Nếu vị trí trong array thay đổi, bạn phải update code hoặc bạn vừa tạo bug.

Ví dụ, chúng ta quyết định capture ", " characters là vô lý. Vì vậy bỏ parens quanh phần đó của regex:

m = PLACE.match(/([^,]+), *([A-Z]{2})/) # => #<MatchData "Fayetteville, AR" 1:"Fayetteville" 2:"AR"> m[3] # => nil 

Nhưng bây giờ m[3] không còn chứa state - bug city.

Named groups

Bạn có thể làm regex groups semantic hơn bằng cách đặt tên. Cú pháp tương tự cái vừa dùng. Bao quanh regex bằng parens, chỉ định tên như sau:

/(?<groupname>regex)/ 

Áp dụng cho city/state regex:

m = PLACE.match(/(?<city>[^,]+), *(?<state>[A-Z]{2})/) # => #<MatchData "Fayetteville, AR" city:"Fayetteville" state:"AR"> 

Và truy cập captured data bằng cách treat MatchData như hash:

m[:city] # => "Fayetteville" 

Conditionals

Conditionals trong regular expressions có dạng /(?(A)X|Y)/. Dưới đây vài cách hợp lệ dùng chúng:

# Nếu A true, evaluate expression X, else evaluate Y /(?(A)X|Y)/ # Nếu A true, thì X /(?(A)X)/ # Nếu A false, thì Y /(?(A)|Y)/ 

Hai option phổ biến nhất cho condition A là:

  • Named hoặc numbered group đã được captured chưa?
  • Look-around evaluate true không?

Hãy xem cách dùng:

Group đã được captured?

Để check sự hiện diện của group, dùng cú pháp ?(n), với n là integer, hoặc group name bao quanh bởi <> hoặc ''.

# Group số 1 đã captured? /(?(1)foo|bar)/ # Group tên "mygroup" đã captured? /(?(<mygroup>)foo|bar)/ 

Ví dụ

Tưởng tượng bạn parse US telephone numbers. Các số có area code 3 chữ số optional trừ khi số bắt đầu bằng 1.

1-800-555-1212 # Valid 800-555-1212 # Valid 555-1212 # Valid 1-555-1212 # INVALID!! 

Chúng ta có thể dùng conditional làm area code bắt buộc chỉ khi số bắt đầu bằng 1.

# Regex này trông phức tạp, nhưng làm từ mảnh đơn giản # `^(1-)?` String có bắt đầu "1-"? Nếu có, capture làm group 1 # `(?(1)` Có gì captured trong group one? # `\d{3}-` nếu có, required match 3 chữ số và dash (area code) # `|(\d{3}-)?` nếu không, optional match 3 chữ số và dash (area code) # `\d{3}-\d{4}` match phần còn lại của phone number, luôn 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 

Hạn chế

Một vấn đề với group-based conditionals là matching group "consume" characters trong string. Những character đó không thể dùng bởi conditional.

Ví dụ, code sau thử và fail match 100 nếu text "USD" hiện diện:

"100USD".match(/(USD)(?(1)\d+)/) # nil 

Trong Perl và ngôn ngữ khác, bạn có thể thêm look-ahead statement vào conditional. Điều này cho trigger conditional dựa trên text bất kỳ đâu trong string. Nhưng Ruby không có, nên chúng ta phải creative.

Look-around

May mắn thay, chúng ta có thể workaround hạn chế regex conditionals của Ruby bằng cách lạm dụng look-around expressions.

Look-around là gì?

Thông thường, regex parser step qua string từ đầu đến cuối tìm match. Giống như di chuyển cursor trái sang phải trong word processor.

Look-ahead và look-behind expressions hoạt động khác. Chúng cho inspect string mà không consume characters. Khi xong, cursor để nguyên chỗ.

Cho intro tuyệt vời về look-arounds, xem guide của Rexegg về mastering look ahead and look behind.

Cú pháp trông như sau:

TypeSyntaxExample
Look Ahead(?=query)\d+(?= dollars) match 100 trong "100 dollars"
Negative Look Ahead(?!query)\d+(?! dollars) match 100 nếu KHÔNG theo sau "dollars"
Look Behind(?<=query)(?<=lucky )\d match 7 trong "lucky 7"
Negative Look Behind(?<!query)(?<!furious )\d match 7 trong "lucky 7"

Lạm dụng look-arounds để nâng cao conditionals

Trong conditional, chúng ta chỉ query sự tồn tại của groups đã set. Thông thường, nghĩa là content của group đã consumed và không available cho conditional.

Nhưng bạn có thể dùng look-ahead để set group mà KHÔNG consume characters! Mind blown chưa?

Nhớ code không chạy này?

"100USD".match(/(USD)(?(1)\d+)/) # nil 

Nếu sửa để capture group trong look-ahead, nó đột ngột chạy tốt:

"100USD".match(/(?=.*(USD))(?(1)\d+)/) => #<MatchData "100" 1:"USD"> 

Hãy phân tích query đó:

  • (?=.*(USD)) Dùng look-ahead, scan text tìm "USD" và capture vào group 1
  • (?(1) Nếu group 1 tồn tại
  • \d+ Thì match một hoặc nhiều số

Khá neat, phải không?