Computer >> Máy Tính >  >> Lập trình >> Ruby

Sử dụng các điều kiện bên trong các biểu thức chính quy Ruby

Trong số nhiều tính năng mới mà Ruby 2.0 xuất xưởng vào năm 2013, tính năng mà tôi ít chú ý nhất là công cụ biểu thức chính quy mới, Onigmo. Xét cho cùng, biểu thức chính quy là biểu thức chính quy - tại sao tôi nên quan tâm đến cách Ruby triển khai chúng?

Hóa ra, công cụ regex của Onigmo có một vài thủ thuật gọn gàng bao gồm khả năng sử dụng các điều kiện bên trong các biểu thức chính quy của bạn.

Trong bài đăng này, chúng ta sẽ đi sâu vào các điều kiện regex, tìm hiểu về những điều kỳ quặc khi Ruby triển khai chúng và thảo luận một vài thủ thuật để khắc phục những hạn chế của Ruby. Hãy bắt đầu!

Nhóm &Chụp

Để hiểu điều kiện trong biểu thức chính quy, trước tiên bạn cần hiểu nhóm và bắt.

Hãy tưởng tượng rằng bạn có một danh sách các trích dẫn của Hoa Kỳ:

Fayetteville, AR
Seattle, WA

Bạn muốn tách tên thành phố khỏi tên viết tắt của tiểu bang. Một cách để làm điều này là thực hiện nhiều đối sánh:

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">

Điều này hiệu quả, nhưng nó quá dài dòng. Bằng cách sử dụng nhóm, bạn có thể chụp cả thành phố và tiểu bang chỉ với một biểu thức chính quy.

Vì vậy, hãy kết hợp các biểu thức chính quy ở trên và bao quanh mỗi phần bằng dấu ngoặc đơn. Parens là cách bạn nhóm mọi thứ trong biểu thức chính quy.

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

Như bạn có thể thấy, biểu thức trên chụp cả thành phố và tiểu bang. Bạn truy cập chúng bằng cách xử lý MatchData như một mảng:

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

Vấn đề với việc phân nhóm, như đã làm ở trên, là dữ liệu đã thu thập được đưa vào một mảng. Nếu vị trí của nó trong mảng thay đổi, bạn phải cập nhật mã của mình hoặc bạn vừa giới thiệu một lỗi.

Ví dụ:chúng tôi có thể quyết định rằng thật ngớ ngẩn khi nắm bắt ", " nhân vật. Vì vậy, chúng tôi loại bỏ các parens xung quanh phần đó của biểu thức chính quy:

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 nhà nước - thành phố lỗi.

Các nhóm được đặt tên

Bạn có thể làm cho các nhóm biểu thức chính quy có nhiều ngữ nghĩa hơn bằng cách đặt tên cho chúng. Cú pháp khá giống với những gì chúng tôi vừa sử dụng. Chúng tôi bao quanh regex trong các parens và chỉ định tên như vậy:

/(?<groupname>regex)/

Nếu chúng tôi áp dụng điều này cho biểu thức chính quy thành phố / tiểu bang của mình, chúng tôi nhận được:

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

Và chúng tôi có thể truy cập dữ liệu đã thu thập bằng cách xử lý MatchData giống như một hàm băm:

m[:city] 
# => "Fayetteville"

Điều kiện

Các điều kiện trong biểu thức chính quy có dạng /(?(A)X|Y)/ . Dưới đây là một số cách hợp lệ để sử dụng chúng:

# 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)/

Hai trong số các tùy chọn phổ biến nhất cho tình trạng của bạn, A là:

  • Một nhóm được đặt tên hoặc được đánh số đã được nắm bắt chưa?
  • Việc xem qua có đánh giá đúng không?

Hãy xem cách sử dụng chúng:

Một nhóm đã bị bắt chưa?

Để kiểm tra sự hiện diện của một nhóm, hãy sử dụng ?(n) cú pháp, trong đó n là số nguyên hoặc tên nhóm được bao quanh bởi <> hoặc '' .

# Has group number 1 been captured?
/(?(1)foo|bar)/

# Has a group named "mygroup" been captured?
/(?(<mygroup>)foo|bar)/

Ví dụ

Hãy tưởng tượng bạn đang phân tích cú pháp các số điện thoại của Hoa Kỳ. Các số này có mã vùng gồm ba chữ số là tùy chọn trừ khi số bắt đầu bằng một.

1-800-555-1212 # Valid
800-555-1212 # Valid
555-1212 # Valid

1-555-1212 # INVALID!!

Chúng ta có thể sử dụng một điều kiện để đặt mã vùng trở thành yêu cầu chỉ khi số bắt đầu bằng 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

Hạn chế

Một vấn đề với việc sử dụng các điều kiện dựa trên nhóm là việc so khớp một nhóm sẽ "tiêu thụ" các ký tự đó trong chuỗi. Vì vậy, các ký tự đó không thể được sử dụng bởi điều kiện.

Ví dụ:mã sau cố gắng và không khớp với 100 nếu có văn bản "USD":

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

Trong Perl và một số ngôn ngữ khác, bạn có thể thêm câu lệnh nhìn trước vào điều kiện của mình. Điều này cho phép bạn kích hoạt điều kiện dựa trên văn bản ở bất kỳ đâu trong chuỗi. Nhưng Ruby không có điều này, vì vậy chúng ta phải sáng tạo một chút.

​​Xem xung quanh

May mắn thay, chúng ta có thể khắc phục những hạn chế trong điều kiện regex của Ruby bằng cách lạm dụng các biểu thức nhìn xung quanh.

​​Xem xét xung quanh là gì?

Thông thường, trình phân tích cú pháp biểu thức chính quy đi qua chuỗi của bạn từ đầu đến cuối để tìm kiếm các kết quả phù hợp. Nó giống như di chuyển con trỏ từ trái sang phải trong trình xử lý văn bản.

Biểu thức nhìn về phía trước và nhìn lại phía sau hoạt động hơi khác một chút. Họ cho phép bạn kiểm tra chuỗi mà không sử dụng bất kỳ ký tự nào. Khi chúng hoàn tất, con trỏ sẽ được để ở đúng vị trí ban đầu.

Để có phần giới thiệu tuyệt vời về cách nhìn tổng thể, hãy xem hướng dẫn của Rexegg để làm chủ cái nhìn trước và nhìn sau

Cú pháp có dạng như sau:

Loại Cú pháp Ví dụ
Nhìn về phía trước (?=query) \d+(?= dollars) khớp với 100 trong "100 đô la"
Cái nhìn tiêu cực về phía trước (?!query) \d+(?! dollars) khớp với 100 nếu nó KHÔNG được theo sau bởi từ "đô la"
Nhìn lại phía sau (?<=query) (?<=lucky )\d khớp với 7 trong "số 7 may mắn"
Cái nhìn tiêu cực về phía sau (?<!query) (?<!furious )\d khớp với 7 trong "số 7 may mắn"

Lạm dụng look-around để nâng cao điều kiện

Trong điều kiện của chúng tôi, chúng tôi chỉ có thể truy vấn sự tồn tại của các nhóm đã được thiết lập. Thông thường, điều này có nghĩa là nội dung của nhóm đã được sử dụng và không có sẵn cho người có điều kiện.

Nhưng bạn có thể sử dụng tính năng nhìn trước để thiết lập một nhóm mà không cần sử dụng bất kỳ ký tự nào! Tâm trí của bạn vẫn chưa?

Nhớ mã này không hoạt động?

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

Nếu chúng tôi sửa đổi nó để nắm bắt nhóm trong một cái nhìn về phía trước, nó đột nhiên hoạt động tốt:

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

Hãy chia nhỏ truy vấn đó và xem điều gì đang xảy ra:

  • (?=.*(USD)) Sử dụng cái nhìn trước, quét văn bản để tìm "USD" và chụp nó trong nhóm 1
  • (?(1) Nếu nhóm 1 tồn tại
  • \d+ Sau đó, khớp một hoặc nhiều số

Khá gọn gàng, phải không?