Biểu thức chính quy HOWTO

tác giả:

A.M. Kuchling <amk@amk.ca>

Giới thiệu

Biểu thức chính quy (được gọi là RE, hoặc biểu thức chính quy hoặc mẫu biểu thức chính quy) về cơ bản là một ngôn ngữ lập trình nhỏ, có tính chuyên môn cao được nhúng bên trong Python và được cung cấp thông qua mô-đun re. Bằng cách sử dụng ngôn ngữ nhỏ này, bạn chỉ định các quy tắc cho tập hợp các chuỗi có thể có mà bạn muốn khớp; bộ này có thể chứa các câu tiếng Anh, địa chỉ e-mail hoặc lệnh TeX hoặc bất cứ thứ gì bạn thích. Sau đó, bạn có thể đặt các câu hỏi như "Chuỗi này có khớp với mẫu không?" hoặc "Có khớp mẫu nào ở bất kỳ đâu trong chuỗi này không?". Bạn cũng có thể sử dụng RE để sửa đổi một chuỗi hoặc tách nó ra theo nhiều cách khác nhau.

Các mẫu biểu thức chính quy được biên dịch thành một chuỗi mã byte, sau đó được thực thi bởi một công cụ phù hợp được viết bằng C. Để sử dụng nâng cao, có thể cần phải chú ý cẩn thận đến cách công cụ sẽ thực thi một RE nhất định và viết RE theo một cách nhất định để tạo ra mã byte chạy nhanh hơn. Tối ưu hóa không được đề cập trong tài liệu này vì nó yêu cầu bạn phải hiểu rõ về nội bộ của công cụ khớp.

Ngôn ngữ biểu thức chính quy tương đối nhỏ và bị hạn chế, vì vậy không phải tất cả các tác vụ xử lý chuỗi có thể đều có thể được thực hiện bằng cách sử dụng biểu thức chính quy. Ngoài ra còn có các nhiệm vụ can được thực hiện với các biểu thức thông thường, nhưng các biểu thức này hóa ra rất phức tạp. Trong những trường hợp này, tốt hơn hết bạn nên viết mã Python để xử lý; mặc dù mã Python sẽ chậm hơn một biểu thức chính quy phức tạp nhưng nó cũng có thể dễ hiểu hơn.

hoa văn đơn giản

Chúng ta sẽ bắt đầu bằng việc tìm hiểu về các biểu thức chính quy đơn giản nhất có thể. Vì các biểu thức chính quy được sử dụng để hoạt động trên các chuỗi nên chúng ta sẽ bắt đầu với nhiệm vụ phổ biến nhất: khớp các ký tự.

Để có giải thích chi tiết về khoa học máy tính dựa trên các biểu thức chính quy (automata hữu hạn xác định và không xác định), bạn có thể tham khảo hầu hết mọi sách giáo khoa về viết trình biên dịch.

Ký tự phù hợp

Hầu hết các chữ cái và ký tự sẽ tự khớp với nhau. Ví dụ: biểu thức chính quy test sẽ khớp chính xác với chuỗi test. (Bạn có thể bật chế độ không phân biệt chữ hoa chữ thường để cho phép RE này khớp với Test hoặc TEST; thông tin thêm về điều này sau.)

Có những ngoại lệ cho quy tắc này; một số ký tự là metacharacters đặc biệt và không khớp với chính chúng. Thay vào đó, chúng báo hiệu rằng một số điều không bình thường cần phải khớp với nhau hoặc chúng ảnh hưởng đến các phần khác của RE bằng cách lặp lại chúng hoặc thay đổi ý nghĩa của chúng. Phần lớn tài liệu này được dành để thảo luận về các siêu ký tự khác nhau và chức năng của chúng.

Đây là danh sách đầy đủ các siêu ký tự; ý nghĩa của chúng sẽ được thảo luận trong phần còn lại của HOWTO này.

. ^ $ * + ? { } [ ] \ | ( )

Các siêu ký tự đầu tiên chúng ta sẽ xem xét là []. Chúng được sử dụng để chỉ định một lớp ký tự, là một tập hợp các ký tự mà bạn muốn so khớp. Các ký tự có thể được liệt kê riêng lẻ hoặc có thể biểu thị một phạm vi ký tự bằng cách đưa ra hai ký tự và phân tách chúng bằng '-'. Ví dụ: [abc] sẽ khớp với bất kỳ ký tự nào trong số các ký tự a, b hoặc c; điều này giống với [a-c], sử dụng một phạm vi để thể hiện cùng một bộ ký tự. Nếu bạn chỉ muốn khớp các chữ cái viết thường, RE của bạn sẽ là [a-z].

Siêu ký tự (ngoại trừ \) không hoạt động bên trong các lớp. Ví dụ: [akm$] sẽ khớp với bất kỳ ký tự nào trong số các ký tự 'a', 'k', 'm' hoặc '$'; '$' thường là một siêu ký tự, nhưng bên trong một lớp nhân vật, nó bị mất đi tính chất đặc biệt.

Bạn có thể khớp các ký tự không được liệt kê trong lớp theo bộ complementing. Điều này được biểu thị bằng cách bao gồm '^' làm ký tự đầu tiên của lớp. Ví dụ: [^5] sẽ khớp với bất kỳ ký tự nào ngoại trừ '5'. Nếu dấu mũ xuất hiện ở nơi khác trong lớp ký tự thì nó không có ý nghĩa đặc biệt. Ví dụ: [5^] sẽ khớp với '5' hoặc '^'.

Có lẽ siêu ký tự quan trọng nhất là dấu gạch chéo ngược, \. Giống như trong chuỗi ký tự Python, dấu gạch chéo ngược có thể được theo sau bởi nhiều ký tự khác nhau để báo hiệu các chuỗi đặc biệt khác nhau. Nó cũng được sử dụng để thoát khỏi tất cả các siêu ký tự để bạn vẫn có thể khớp chúng theo mẫu; ví dụ: nếu bạn cần khớp [ hoặc \, bạn có thể đặt trước chúng bằng dấu gạch chéo ngược để loại bỏ ý nghĩa đặc biệt của chúng: \[ hoặc \\.

Một số chuỗi đặc biệt bắt đầu bằng '\' biểu thị các tập hợp ký tự được xác định trước thường hữu ích, chẳng hạn như tập hợp các chữ số, tập hợp các chữ cái hoặc tập hợp bất kỳ thứ gì không phải là khoảng trắng.

Hãy lấy một ví dụ: \w khớp với bất kỳ ký tự chữ và số nào. Nếu mẫu biểu thức chính quy được biểu thị bằng byte thì mẫu này tương đương với lớp [a-zA-Z0-9_]. Nếu mẫu biểu thức chính quy là một chuỗi, \w sẽ khớp với tất cả các ký tự được đánh dấu là các chữ cái trong cơ sở dữ liệu Unicode do mô-đun unicodedata cung cấp. Bạn có thể sử dụng định nghĩa hạn chế hơn về \w trong mẫu chuỗi bằng cách cung cấp cờ re.ASCII khi biên dịch biểu thức chính quy.

Danh sách các trình tự đặc biệt sau đây chưa đầy đủ. Để biết danh sách đầy đủ các chuỗi và định nghĩa lớp mở rộng cho các mẫu chuỗi Unicode, hãy xem phần cuối của Regular Expression Syntax trong tài liệu tham khảo Thư viện chuẩn. Nói chung, các phiên bản Unicode khớp với bất kỳ ký tự nào thuộc danh mục thích hợp trong cơ sở dữ liệu Unicode.

\d

Khớp với bất kỳ chữ số thập phân nào; điều này tương đương với lớp [0-9].

\D

Khớp với bất kỳ ký tự không có chữ số nào; điều này tương đương với lớp [^0-9].

\s

Khớp với bất kỳ ký tự khoảng trắng nào; điều này tương đương với lớp [ \t\n\r\f\v].

\S

Khớp với bất kỳ ký tự không phải khoảng trắng nào; điều này tương đương với lớp [^ \t\n\r\f\v].

\w

Khớp với bất kỳ ký tự chữ và số nào; điều này tương đương với lớp [a-zA-Z0-9_].

\W

Khớp với bất kỳ ký tự không phải chữ và số nào; điều này tương đương với lớp [^a-zA-Z0-9_].

Những chuỗi này có thể được bao gồm trong một lớp ký tự. Ví dụ: [\s,.] là lớp ký tự sẽ khớp với bất kỳ ký tự khoảng trắng nào hoặc ',' hoặc '.'.

Siêu ký tự cuối cùng trong phần này là .. Nó khớp với mọi thứ ngoại trừ ký tự dòng mới và có một chế độ thay thế (re.DOTALL) trong đó nó sẽ khớp ngay cả với một dòng mới. . thường được sử dụng khi bạn muốn khớp "bất kỳ ký tự nào".

Lặp đi lặp lại những điều

Khả năng khớp các tập hợp ký tự khác nhau là điều đầu tiên mà các biểu thức chính quy có thể thực hiện, điều này vốn không thể thực hiện được bằng các phương thức có sẵn trên chuỗi. Tuy nhiên, nếu đó là khả năng bổ sung duy nhất của biểu thức chính quy thì chúng sẽ không tiến bộ nhiều. Một khả năng khác là bạn có thể chỉ định các phần của RE phải được lặp lại một số lần nhất định.

Siêu ký tự đầu tiên để lặp lại những thứ mà chúng ta sẽ xem xét là *. * không khớp với ký tự chữ '*'; thay vào đó, nó chỉ định rằng ký tự trước đó có thể được khớp 0 hoặc nhiều lần, thay vì chính xác một lần.

Ví dụ: ca*t sẽ khớp với 'ct' (0 ký tự 'a'), 'cat' (1 'a'), 'caaat' (3 ký tự 'a'), v.v.

Sự lặp lại như *greedy; khi lặp lại RE, công cụ so khớp sẽ cố gắng lặp lại nó nhiều lần nhất có thể. Nếu các phần sau của mẫu không khớp thì công cụ khớp sẽ sao lưu và thử lại với ít lần lặp lại hơn.

Một ví dụ từng bước sẽ làm cho điều này rõ ràng hơn. Hãy xem xét biểu thức a[bcd]*b. Điều này khớp với chữ cái 'a', không hoặc nhiều chữ cái từ lớp [bcd] và cuối cùng kết thúc bằng 'b'. Bây giờ hãy tưởng tượng kết hợp RE này với chuỗi 'abcbd'.

Bước

Đã khớp

Giải thích

1

a

Zz000zz trong các trận đấu RE.

2

abcbd

Công cụ khớp với [bcd]*, đi xa nhất có thể, đến cuối chuỗi.

3

Failure

Công cụ cố gắng khớp b nhưng vị trí hiện tại nằm ở cuối chuỗi nên không thành công.

4

abcb

Sao lưu để [bcd]* khớp với ít ký tự hơn.

5

Failure

Hãy thử lại b nhưng vị trí hiện tại là ký tự cuối cùng, đó là 'd'.

6

abc

Sao lưu lại một lần nữa để [bcd]* chỉ khớp với bc.

7

abcb

Hãy thử lại b. Lần này nhân vật ở vị trí hiện tại là 'b' nên thành công.

Hiện tại đã đạt đến điểm cuối của RE và nó đã khớp với 'abcb'. Điều này thể hiện cách công cụ so khớp đi xa nhất có thể lúc đầu và nếu không tìm thấy kết quả khớp nào thì nó sẽ dần dần sao lưu và thử đi thử lại phần còn lại của RE. Nó sẽ sao lưu cho đến khi không thử kết quả nào cho [bcd]* và nếu sau đó không thành công, công cụ sẽ kết luận rằng chuỗi hoàn toàn không khớp với RE.

Một siêu ký tự lặp lại khác là +, khớp với một hoặc nhiều lần. Hãy chú ý cẩn thận đến sự khác biệt giữa *+; * khớp với zero hoặc nhiều lần hơn, do đó, bất cứ điều gì được lặp lại có thể không xuất hiện, trong khi + yêu cầu ít nhất one xuất hiện. Để sử dụng ví dụ tương tự, ca+t sẽ khớp với 'cat' (1 'a'), 'caaat' (3 'a's), nhưng sẽ không khớp với 'ct'.

Có thêm hai toán tử hoặc định lượng lặp lại. Ký tự dấu chấm hỏi, ?, khớp một lần hoặc 0 lần; bạn có thể coi nó như việc đánh dấu một cái gì đó là tùy chọn. Ví dụ: home-?brew khớp với 'homebrew' hoặc 'home-brew'.

Bộ định lượng phức tạp nhất là {m,n}, trong đó mn là số nguyên thập phân. Bộ định lượng này có nghĩa là phải có ít nhất sự lặp lại m và nhiều nhất là n. Ví dụ: a/{1,3}b sẽ khớp với 'a/b', 'a//b''a///b'. Nó sẽ không khớp với 'ab', không có dấu gạch chéo hoặc 'a////b', có bốn dấu gạch chéo.

Bạn có thể bỏ qua m hoặc n; trong trường hợp đó, một giá trị hợp lý được giả định cho giá trị còn thiếu. Việc bỏ qua m được hiểu là giới hạn dưới của 0, trong khi việc bỏ qua n sẽ dẫn đến giới hạn trên của vô cực.

Trường hợp đơn giản nhất {m} khớp với mục trước chính xác m lần. Ví dụ: a/{2}b sẽ chỉ khớp với 'a//b'.

Những độc giả theo khuynh hướng giản lược có thể nhận thấy rằng ba bộ định lượng khác đều có thể được biểu diễn bằng cách sử dụng ký hiệu này. {0,} giống với *, {1,} tương đương với +, và {0,1} giống với ?. Tốt hơn nên sử dụng *, + hoặc ? khi có thể, đơn giản vì chúng ngắn hơn và dễ đọc hơn.

Sử dụng biểu thức chính quy

Bây giờ chúng ta đã xem xét một số biểu thức chính quy đơn giản, làm cách nào để sử dụng chúng trong Python? Mô-đun re cung cấp giao diện cho công cụ biểu thức chính quy, cho phép bạn biên dịch RE thành các đối tượng và sau đó thực hiện so khớp với chúng.

Biên dịch biểu thức chính quy

Biểu thức chính quy được biên dịch thành các đối tượng mẫu, có các phương thức cho nhiều thao tác khác nhau như tìm kiếm kết quả khớp mẫu hoặc thực hiện thay thế chuỗi.

>>> nhập lại
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() cũng chấp nhận một đối số flags tùy chọn, được sử dụng để kích hoạt các tính năng đặc biệt và các biến thể cú pháp khác nhau. Chúng ta sẽ xem xét các cài đặt có sẵn sau, nhưng bây giờ chỉ có một ví dụ sẽ thực hiện:

>>> p = re.compile('ab*', re.IGNORECASE)

RE được chuyển tới re.compile() dưới dạng một chuỗi. RE được xử lý dưới dạng chuỗi vì các biểu thức chính quy không phải là một phần của ngôn ngữ Python cốt lõi và không có cú pháp đặc biệt nào được tạo để biểu thị chúng. (Có những ứng dụng hoàn toàn không cần RE, do đó không cần phải tăng cường đặc tả ngôn ngữ bằng cách đưa chúng vào.) Thay vào đó, mô-đun re chỉ đơn giản là một mô-đun mở rộng C đi kèm với Python, giống như các mô-đun socket hoặc zlib.

Việc đặt RE vào chuỗi giúp ngôn ngữ Python đơn giản hơn nhưng có một nhược điểm đó là chủ đề của phần tiếp theo.

Bệnh dịch hạch ngược

Như đã nêu trước đó, biểu thức chính quy sử dụng ký tự dấu gạch chéo ngược ('\') để biểu thị các dạng đặc biệt hoặc cho phép sử dụng các ký tự đặc biệt mà không cần gọi ý nghĩa đặc biệt của chúng. Điều này mâu thuẫn với việc Python sử dụng cùng một ký tự cho cùng một mục đích trong chuỗi ký tự.

Giả sử bạn muốn viết một RE khớp với chuỗi \section, chuỗi này có thể tìm thấy trong tệp LaTeX. Để tìm ra những gì cần viết trong mã chương trình, hãy bắt đầu với chuỗi mong muốn được khớp. Tiếp theo, bạn phải thoát mọi dấu gạch chéo ngược và các siêu ký tự khác bằng cách đặt trước chúng một dấu gạch chéo ngược, dẫn đến chuỗi \\section. Chuỗi kết quả phải được chuyển tới re.compile() phải là \\section. Tuy nhiên, để thể hiện điều này dưới dạng chuỗi ký tự Python, cả hai dấu gạch chéo ngược phải được thoát again.

nhân vật

Sân khấu

\section

Chuỗi văn bản cần khớp

\\section

Đã thoát dấu gạch chéo ngược cho re.compile()

"\\\\section"

Đã thoát dấu gạch chéo ngược cho một chuỗi ký tự

Nói tóm lại, để khớp với dấu gạch chéo ngược theo nghĩa đen, người ta phải viết '\\\\' dưới dạng chuỗi RE, vì biểu thức chính quy phải là \\ và mỗi dấu gạch chéo ngược phải được biểu thị dưới dạng \\ bên trong một chuỗi ký tự Python thông thường. Trong RE có dấu gạch chéo ngược lặp đi lặp lại, điều này dẫn đến nhiều dấu gạch chéo ngược lặp lại và làm cho chuỗi kết quả khó hiểu.

Giải pháp là sử dụng ký hiệu chuỗi thô của Python cho các biểu thức chính quy; dấu gạch chéo ngược không được xử lý theo bất kỳ cách đặc biệt nào trong chuỗi ký tự có tiền tố 'r', vì vậy r"\n" là chuỗi hai ký tự chứa '\''n', trong khi "\n" là chuỗi một ký tự chứa dòng mới. Biểu thức chính quy thường sẽ được viết bằng mã Python bằng cách sử dụng ký hiệu chuỗi thô này.

Ngoài ra, các chuỗi thoát đặc biệt hợp lệ trong các biểu thức thông thường, nhưng không hợp lệ ở dạng chuỗi ký tự trong Python, hiện dẫn đến SyntaxWarning và cuối cùng sẽ trở thành SyntaxError, có nghĩa là các chuỗi sẽ không hợp lệ nếu ký hiệu chuỗi thô hoặc thoát khỏi dấu gạch chéo ngược không được sử dụng.

Chuỗi thông thường

Chuỗi thô

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

Thực hiện các trận đấu

Khi bạn có một đối tượng biểu thị một biểu thức chính quy được biên dịch, bạn sẽ làm gì với nó? Các đối tượng mẫu có một số phương thức và thuộc tính. Chỉ những cái quan trọng nhất mới được đề cập ở đây; tham khảo tài liệu re để có danh sách đầy đủ.

Phương thức/Thuộc tính

Mục đích

match()

Xác định xem RE có khớp ở đầu chuỗi không.

search()

Quét qua một chuỗi, tìm kiếm bất kỳ vị trí nào có RE khớp.

findall()

Tìm tất cả các chuỗi con có RE khớp và trả về chúng dưới dạng danh sách.

finditer()

Tìm tất cả các chuỗi con có RE khớp và trả về chúng dưới dạng iterator.

match()search() trả về None nếu không tìm thấy kết quả phù hợp. Nếu thành công, một phiên bản match object sẽ được trả về, chứa thông tin về kết quả khớp: nơi nó bắt đầu và kết thúc, chuỗi con mà nó khớp, v.v.

Bạn có thể tìm hiểu về điều này bằng cách thử nghiệm tương tác với mô-đun re.

Zz001zz này sử dụng trình thông dịch Python tiêu chuẩn cho các ví dụ của nó. Đầu tiên, chạy trình thông dịch Python, nhập mô-đun re và biên dịch RE:

>>> nhập lại
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

Bây giờ, bạn có thể thử kết hợp nhiều chuỗi khác nhau với RE [a-z]+. Một chuỗi trống hoàn toàn không khớp vì + có nghĩa là 'một hoặc nhiều lần lặp lại'. match() sẽ trả về None trong trường hợp này, điều này sẽ khiến trình thông dịch không in được kết quả. Bạn có thể in rõ ràng kết quả của match() để làm rõ điều này.

>>> p.match("")
>>> print(p.match(""))
không có

Bây giờ, hãy thử trên một chuỗi phù hợp, chẳng hạn như tempo. Trong trường hợp này, match() sẽ trả về match object, vì vậy bạn nên lưu kết quả vào một biến để sử dụng sau.

>>> m = p.match('tempo')
>>> tôi
<re.Match đối tượng; span=(0, 5), match='nhịp độ'>

Bây giờ bạn có thể truy vấn match object để biết thông tin về chuỗi phù hợp. Các phiên bản đối tượng khớp cũng có một số phương thức và thuộc tính; những cái quan trọng nhất là:

Phương thức/Thuộc tính

Mục đích

group()

Trả về chuỗi khớp với RE

start()

Trả lại vị trí bắt đầu của trận đấu

end()

Trả về vị trí kết thúc trận đấu

span()

Trả về một bộ dữ liệu chứa vị trí (bắt đầu, kết thúc) của trận đấu

Việc thử những phương pháp này sẽ sớm làm rõ ý nghĩa của chúng:

>>> m.group()
'nhịp độ'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() trả về chuỗi con khớp với RE. start()end() trả về chỉ số bắt đầu và kết thúc của trận đấu. span() trả về cả chỉ mục bắt đầu và kết thúc trong một bộ dữ liệu. Vì phương thức match() chỉ kiểm tra xem RE có khớp ở đầu chuỗi hay không nên start() sẽ luôn bằng 0. Tuy nhiên, phương pháp mẫu search() quét qua chuỗi, do đó, kết quả khớp có thể không bắt đầu từ 0 trong trường hợp đó.

>>> print(p.match('::: message'))
không có
>>> m = p.search('::: message'); in(m)
<re.Match đối tượng; span=(4, 11), match='message'>
>>> m.group()
'tin nhắn'
>>> m.span()
(4, 11)

Trong các chương trình thực tế, kiểu phổ biến nhất là lưu trữ match object trong một biến, sau đó kiểm tra xem đó có phải là None hay không. Điều này thường trông giống như:

p = biên dịch lại( ... )
m = p.match( 'chuỗi ở đây' )
nếu tôi:
    print('Đã tìm thấy kết quả phù hợp: ', m.group())
khác:
    print('Không khớp')

Hai phương thức mẫu trả về tất cả các kết quả khớp cho một mẫu. findall() trả về danh sách các chuỗi phù hợp

>>> p = re.compile(r'\d+')
>>> p.findall('12 tay trống đánh trống, 11 người thổi sáo, 10 chúa tể nhảy lên')
['12', '11', '10']

Tiền tố r, làm cho chữ này trở thành chữ chuỗi thô, là cần thiết trong ví dụ này vì các chuỗi thoát trong một chữ chuỗi "nấu" thông thường không được Python nhận ra, trái ngược với các biểu thức thông thường, giờ đây dẫn đến SyntaxWarning và cuối cùng sẽ trở thành SyntaxError. Xem Bệnh dịch hạch ngược.

findall() phải tạo toàn bộ danh sách trước khi có thể trả về kết quả. Phương thức finditer() trả về một chuỗi các phiên bản match object dưới dạng iterator:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

Các hàm cấp mô-đun

Bạn không cần phải tạo một đối tượng mẫu và gọi các phương thức của nó; mô-đun re cũng cung cấp các chức năng cấp cao nhất được gọi là match(), search(), findall(), sub(), v.v. Các hàm này nhận các đối số giống như phương thức mẫu tương ứng với chuỗi RE được thêm làm đối số đầu tiên và vẫn trả về một phiên bản None hoặc match object.

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<re.Match object; span=(0, 5), match='From '>

Về cơ bản, các hàm này chỉ đơn giản là tạo một đối tượng mẫu cho bạn và gọi phương thức thích hợp trên đó. Họ cũng lưu trữ đối tượng đã biên dịch trong bộ đệm, do đó, các cuộc gọi trong tương lai sử dụng cùng một RE sẽ không cần phải phân tích cú pháp mẫu nhiều lần.

Bạn nên sử dụng các hàm cấp mô-đun này hay bạn nên lấy mẫu và tự gọi các phương thức của nó? Nếu bạn đang truy cập biểu thức chính quy trong vòng lặp, việc biên dịch trước nó sẽ lưu một số lệnh gọi hàm. Ngoài các vòng lặp, không có nhiều khác biệt nhờ bộ đệm trong.

Cờ biên soạn

Cờ biên dịch cho phép bạn sửa đổi một số khía cạnh về cách hoạt động của biểu thức chính quy. Cờ có sẵn trong mô-đun re dưới hai tên, một tên dài như IGNORECASE và một dạng ngắn, một chữ cái như I. (Nếu bạn quen thuộc với các công cụ sửa đổi mẫu của Perl, thì dạng một chữ cái sử dụng các chữ cái giống nhau; ví dụ: dạng rút gọn của re.VERBOSEre.X.) Nhiều cờ có thể được chỉ định bằng cách OR-ing chúng theo bit; Ví dụ: re.I | re.M đặt cả cờ IM.

Đây là bảng các cờ có sẵn, theo sau là phần giải thích chi tiết hơn về từng cờ.

Cờ

Ý nghĩa

ASCII, A

Làm cho một số ký tự thoát như \w, \b, \s\d chỉ khớp với các ký tự ASCII có thuộc tính tương ứng.

DOTALL, S

Làm cho . khớp với bất kỳ ký tự nào, kể cả dòng mới.

IGNORECASE, I

Thực hiện các trận đấu không phân biệt chữ hoa chữ thường.

LOCALE, L

Thực hiện đối sánh nhận biết địa phương.

MULTILINE, M

Khớp nhiều dòng, ảnh hưởng đến ^$.

VERBOSE, X (dành cho 'mở rộng')

Kích hoạt RE chi tiết để có thể sắp xếp rõ ràng và dễ hiểu hơn.

re.I
re.IGNORECASE

Thực hiện khớp không phân biệt chữ hoa chữ thường; lớp ký tự và chuỗi ký tự sẽ khớp với các chữ cái bằng cách bỏ qua chữ hoa chữ thường. Ví dụ: [A-Z] cũng sẽ khớp với các chữ cái viết thường. Đối sánh Unicode đầy đủ cũng hoạt động trừ khi cờ ASCII được sử dụng để vô hiệu hóa các đối sánh không phải ASCII. Khi sử dụng mẫu Unicode [a-z] hoặc [A-Z] kết hợp với cờ IGNORECASE, chúng sẽ khớp với 52 chữ cái ASCII và 4 chữ cái không phải ASCII bổ sung: 'İ' (U+0130, chữ in hoa Latinh I có dấu chấm ở trên), 'ı' (U+0131, chữ nhỏ Latinh không có dấu chấm i), 'ſ' (U+017F, chữ nhỏ Latinh dài s) và 'K' (U+212A, ký hiệu Kelvin). Spam sẽ khớp với 'Spam', 'spam', 'spAM' hoặc 'ſpam' (cái sau chỉ khớp ở chế độ Unicode). Việc viết thường này không tính đến ngôn ngữ hiện tại; điều đó sẽ xảy ra nếu bạn cũng đặt cờ LOCALE.

re.L
re.LOCALE

Tạo \w, \W, \b, \B và đối sánh không phân biệt chữ hoa chữ thường phụ thuộc vào ngôn ngữ hiện tại thay vì cơ sở dữ liệu Unicode.

Ngôn ngữ là một tính năng của thư viện C nhằm giúp viết các chương trình có tính đến sự khác biệt về ngôn ngữ. Ví dụ: nếu bạn đang xử lý văn bản tiếng Pháp được mã hóa, bạn muốn có thể viết \w+ để khớp các từ, nhưng \w chỉ khớp với lớp ký tự [A-Za-z] theo mẫu byte; nó sẽ không khớp với byte tương ứng với é hoặc ç. Nếu hệ thống của bạn được cấu hình đúng cách và ngôn ngữ tiếng Pháp được chọn, một số hàm C nhất định sẽ cho chương trình biết rằng byte tương ứng với é cũng phải được coi là một chữ cái. Đặt cờ LOCALE khi biên dịch biểu thức chính quy sẽ khiến đối tượng được biên dịch thu được sử dụng các hàm C này cho \w; việc này chậm hơn nhưng cũng cho phép \w+ khớp với các từ tiếng Pháp như bạn mong đợi. Việc sử dụng cờ này không được khuyến khích trong Python 3 vì cơ chế ngôn ngữ rất không đáng tin cậy, nó chỉ xử lý một "nền văn hóa" tại một thời điểm và nó chỉ hoạt động với các ngôn ngữ 8 bit. Tính năng khớp Unicode đã được bật theo mặc định trong Python 3 cho các mẫu Unicode (str) và nó có thể xử lý các ngôn ngữ/ngôn ngữ khác nhau.

re.M
re.MULTILINE

(^$ chưa được giải thích; chúng sẽ được giới thiệu trong phần Nhiều siêu ký tự hơn.)

Thông thường ^ chỉ khớp ở đầu chuỗi và $ chỉ khớp ở cuối chuỗi và ngay trước dòng mới (nếu có) ở cuối chuỗi. Khi cờ này được chỉ định, ^ khớp ở đầu chuỗi và ở đầu mỗi dòng trong chuỗi, ngay sau mỗi dòng mới. Tương tự, siêu ký tự $ khớp ở cuối chuỗi và ở cuối mỗi dòng (ngay trước mỗi dòng mới).

re.S
re.DOTALL

Làm cho ký tự đặc biệt '.' khớp với bất kỳ ký tự nào, kể cả dòng mới; không có cờ này, '.' sẽ khớp với bất kỳ dòng nào của except.

re.A
re.ASCII

Làm cho \w, \W, \b, \B, \s\S thực hiện đối sánh chỉ ASCII thay vì đối sánh Unicode đầy đủ. Điều này chỉ có ý nghĩa đối với các mẫu Unicode và bị bỏ qua đối với các mẫu byte.

re.X
re.VERBOSE

Cờ này cho phép bạn viết các biểu thức chính quy dễ đọc hơn bằng cách cho phép bạn linh hoạt hơn trong cách định dạng chúng. Khi cờ này đã được chỉ định, khoảng trắng trong chuỗi RE sẽ bị bỏ qua, ngoại trừ khi khoảng trắng nằm trong một lớp ký tự hoặc đứng trước dấu gạch chéo ngược không thoát; điều này cho phép bạn sắp xếp và thụt lề RE rõ ràng hơn. Cờ này cũng cho phép bạn đặt các nhận xét trong RE mà công cụ sẽ bỏ qua; các nhận xét được đánh dấu bằng '#' không thuộc lớp ký tự cũng như không có dấu gạch chéo ngược không thoát trước.

Ví dụ: đây là RE sử dụng re.VERBOSE; xem nó dễ đọc hơn bao nhiêu?

charref = re.compile(r"""
 &[#] # Start của tham chiếu thực thể số
 (
     0[0-7]+ dạng # Octal
   | [0-9]+ dạng # Decimal
   | dạng x[0-9a-fA-F]+ # Hexadecimal
 )
 ;                   dấu chấm phẩy # Trailing
""", re.VERBOSE)

Nếu không có cài đặt chi tiết, RE sẽ trông như thế này:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

Trong ví dụ trên, tính năng ghép chuỗi tự động của Python đã được sử dụng để chia RE thành các phần nhỏ hơn, nhưng nó vẫn khó hiểu hơn phiên bản sử dụng re.VERBOSE.

Thêm sức mạnh mẫu

Cho đến nay chúng ta mới chỉ đề cập đến một phần các tính năng của biểu thức chính quy. Trong phần này, chúng tôi sẽ đề cập đến một số siêu ký tự mới và cách sử dụng các nhóm để truy xuất các phần văn bản khớp.

Nhiều siêu ký tự hơn

Có một số siêu ký tự mà chúng tôi chưa đề cập đến. Hầu hết chúng sẽ được đề cập trong phần này.

Một số siêu ký tự còn lại sẽ được thảo luận là zero-width assertions. Chúng không làm cho động cơ chuyển động qua dây đàn; thay vào đó, họ không sử dụng ký tự nào cả và chỉ đơn giản là thành công hay thất bại. Ví dụ: \b là xác nhận rằng vị trí hiện tại nằm ở ranh giới từ; vị trí không hề bị thay đổi bởi \b. Điều này có nghĩa là các xác nhận có độ rộng bằng 0 không bao giờ được lặp lại, bởi vì nếu chúng khớp một lần tại một vị trí nhất định, rõ ràng chúng có thể được khớp vô số lần.

|

Luân phiên, hoặc toán tử "hoặc". Nếu AB là biểu thức chính quy, A|B sẽ khớp với bất kỳ chuỗi nào khớp với A hoặc B. | có mức độ ưu tiên rất thấp để làm cho nó hoạt động hợp lý khi bạn xen kẽ các chuỗi nhiều ký tự. Crow|Servo sẽ khớp với 'Crow' hoặc 'Servo', không phải 'Cro', 'w' hoặc 'S''ervo'.

Để khớp với '|' theo nghĩa đen, hãy sử dụng \| hoặc đặt nó bên trong một lớp ký tự, như trong [|].

^

Trận đấu ở đầu dòng. Trừ khi cờ MULTILINE được đặt, cờ này sẽ chỉ khớp ở đầu chuỗi. Trong chế độ MULTILINE, điều này cũng khớp ngay sau mỗi dòng mới trong chuỗi.

Ví dụ: nếu bạn chỉ muốn khớp từ From ở đầu dòng thì RE cần sử dụng là ^From.

>>> print(re.search('^From', 'From Here to Eternity'))
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

Để khớp với '^' theo nghĩa đen, hãy sử dụng \^.

$

Khớp ở cuối dòng, được xác định là cuối chuỗi hoặc bất kỳ vị trí nào theo sau là ký tự dòng mới.

>>> print(re.search('}$', '{block}'))
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))
<re.Match object; span=(6, 7), match='}'>

Để khớp với '$' theo nghĩa đen, hãy sử dụng \$ hoặc đặt nó bên trong một lớp ký tự, như trong [$].

\A

Chỉ khớp ở đầu chuỗi. Khi không ở chế độ MULTILINE, \A^ thực sự giống nhau. Trong chế độ MULTILINE, chúng khác nhau: \A vẫn chỉ khớp ở đầu chuỗi, nhưng ^ có thể khớp ở bất kỳ vị trí nào trong chuỗi theo sau ký tự dòng mới.

\z

Chỉ khớp ở cuối chuỗi.

\Z

Tương tự với \z Để tương thích với các phiên bản Python cũ.

\b

Ranh giới từ. Đây là xác nhận có độ rộng bằng 0 chỉ khớp ở đầu hoặc cuối của từ. Một từ được định nghĩa là một chuỗi các ký tự chữ và số, do đó phần cuối của từ được biểu thị bằng khoảng trắng hoặc ký tự không phải chữ và số.

Ví dụ sau chỉ khớp với class khi đó là một từ hoàn chỉnh; nó sẽ không khớp khi nó được chứa trong một từ khác.

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('không có lớp nào cả'))
<re.Match đối tượng; span=(3, 8), match='class'>
>>> print(p.search('thuật toán được giải mật'))
không có
>>> print(p.search('một lớp con là'))
không có

Có hai điểm tinh tế bạn nên nhớ khi sử dụng trình tự đặc biệt này. Đầu tiên, đây là sự xung đột tồi tệ nhất giữa chuỗi ký tự của Python và chuỗi biểu thức chính quy. Trong chuỗi ký tự của Python, \b là ký tự backspace, giá trị ASCII 8. Nếu bạn không sử dụng chuỗi thô thì Python sẽ chuyển \b thành backspace và RE của bạn sẽ không khớp như bạn mong đợi. Ví dụ sau trông giống như RE trước của chúng tôi, nhưng bỏ qua 'r' ở phía trước chuỗi RE.

>>> p = re.compile('\bclass\b')
>>> print(p.search('không có lớp nào cả'))
không có
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match đối tượng; span=(0, 7), match='\x08class\x08'>

Thứ hai, bên trong một lớp ký tự, nơi xác nhận này không được sử dụng, \b biểu thị ký tự xóa lùi, để tương thích với các chuỗi ký tự của Python.

\B

Một xác nhận có độ rộng bằng 0 khác, điều này trái ngược với \b, chỉ khớp khi vị trí hiện tại không nằm ở ranh giới từ.

Nhóm

Thông thường, bạn cần thu thập nhiều thông tin hơn là liệu RE có khớp hay không. Biểu thức chính quy thường được sử dụng để phân tích các chuỗi bằng cách viết RE được chia thành nhiều nhóm con phù hợp với các thành phần quan tâm khác nhau. Ví dụ: dòng tiêu đề RFC-822 được chia thành tên tiêu đề và giá trị, được phân tách bằng ':', như sau:

Từ: tác giả@example.com
Tác nhân người dùng: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Phiên bản: 1.0
Tới: editor@example.com

Điều này có thể được xử lý bằng cách viết một biểu thức chính quy khớp với toàn bộ dòng tiêu đề và có một nhóm khớp với tên tiêu đề và một nhóm khác khớp với giá trị của tiêu đề.

Các nhóm được đánh dấu bằng siêu ký tự '(', ')'. '('')' có ý nghĩa tương tự như trong các biểu thức toán học; chúng nhóm các biểu thức chứa bên trong chúng lại với nhau và bạn có thể lặp lại nội dung của một nhóm bằng bộ định lượng, chẳng hạn như *, +, ? hoặc {m,n}. Ví dụ: (ab)* sẽ khớp với 0 hoặc nhiều lần lặp lại của ab.

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

Các nhóm được biểu thị bằng '(', ')' cũng ghi lại chỉ mục bắt đầu và kết thúc của văn bản mà chúng khớp; điều này có thể được truy xuất bằng cách chuyển một đối số tới group(), start(), end()span(). Các nhóm được đánh số bắt đầu bằng 0. Nhóm 0 luôn có mặt; đó là toàn bộ RE, vì vậy các phương thức match object đều có nhóm 0 làm đối số mặc định. Sau này chúng ta sẽ xem cách thể hiện các nhóm không nắm bắt được khoảng văn bản phù hợp với chúng.

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

Các nhóm con được đánh số từ trái sang phải, từ 1 trở lên. Các nhóm có thể được lồng vào nhau; để xác định số, chỉ cần đếm các ký tự trong ngoặc đơn mở đầu, đi từ trái sang phải.

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() có thể được truyền nhiều số nhóm cùng một lúc, trong trường hợp đó, nó sẽ trả về một bộ chứa các giá trị tương ứng cho các nhóm đó.

>>> m.group(2,1,2)
('b', 'abc', 'b')

Phương thức groups() trả về một bộ chứa các chuỗi cho tất cả các nhóm con, từ 1 cho đến số lượng có nhiều.

>>> m.groups()
('abc', 'b')

Tham chiếu ngược trong mẫu cho phép bạn chỉ định rằng nội dung của nhóm thu thập trước đó cũng phải được tìm thấy tại vị trí hiện tại trong chuỗi. Ví dụ: \1 sẽ thành công nếu có thể tìm thấy nội dung chính xác của nhóm 1 ở vị trí hiện tại và nếu không thì sẽ thất bại. Hãy nhớ rằng các chuỗi ký tự của Python cũng sử dụng dấu gạch chéo ngược theo sau là các số để cho phép bao gồm các ký tự tùy ý trong một chuỗi, vì vậy hãy đảm bảo sử dụng chuỗi thô khi kết hợp các tham chiếu ngược trong RE.

Ví dụ: RE sau đây phát hiện các từ được nhân đôi trong một chuỗi.

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris mùa xuân').group()
'cái'

Các tham chiếu ngược như thế này thường không hữu ích khi chỉ tìm kiếm trong một chuỗi --- có một số định dạng văn bản lặp lại dữ liệu theo cách này --- nhưng bạn sẽ sớm nhận ra rằng chúng rất hữu ích khi thực hiện thay thế chuỗi.

Các nhóm không bắt giữ và được đặt tên

Các RE phức tạp có thể sử dụng nhiều nhóm, vừa để nắm bắt các chuỗi con quan tâm, vừa để nhóm và cấu trúc chính RE. Trong các RE phức tạp, việc theo dõi số nhóm trở nên khó khăn. Có hai tính năng giúp giải quyết vấn đề này. Cả hai đều sử dụng cú pháp chung cho phần mở rộng biểu thức chính quy, vì vậy chúng ta sẽ xem xét điều đó trước tiên.

Perl 5 nổi tiếng với những bổ sung mạnh mẽ cho các biểu thức chính quy tiêu chuẩn. Đối với những tính năng mới này, các nhà phát triển Perl không thể chọn các siêu ký tự nhấn phím đơn mới hoặc các chuỗi đặc biệt mới bắt đầu bằng \ mà không làm cho các biểu thức chính quy của Perl khác biệt một cách khó hiểu với các RE tiêu chuẩn. Ví dụ: nếu họ chọn & làm siêu ký tự mới thì các biểu thức cũ sẽ cho rằng & là một ký tự thông thường và sẽ không thoát khỏi nó bằng cách viết \& hoặc [&].

Giải pháp được các nhà phát triển Perl lựa chọn là sử dụng (?...) làm cú pháp mở rộng. ? ngay sau dấu ngoặc đơn là lỗi cú pháp vì ? sẽ không có gì để lặp lại, vì vậy điều này không gây ra bất kỳ vấn đề tương thích nào. Các ký tự ngay sau ? cho biết tiện ích mở rộng nào đang được sử dụng, vì vậy (?=foo) là một thứ (xác nhận nhìn về phía trước tích cực) và (?:foo) là một thứ khác (một nhóm không bắt giữ có chứa biểu thức con foo).

Python hỗ trợ một số phần mở rộng của Perl và thêm cú pháp phần mở rộng vào cú pháp phần mở rộng của Perl. Nếu ký tự đầu tiên sau dấu chấm hỏi là P thì bạn biết rằng đó là tiện ích mở rộng dành riêng cho Python.

Bây giờ chúng ta đã xem xét cú pháp mở rộng chung, chúng ta có thể quay lại các tính năng giúp đơn giản hóa việc làm việc với các nhóm trong RE phức tạp.

Đôi khi bạn muốn sử dụng một nhóm để biểu thị một phần của biểu thức chính quy nhưng không quan tâm đến việc truy xuất nội dung của nhóm. Bạn có thể làm rõ điều này bằng cách sử dụng nhóm không thu thập: (?:...), trong đó bạn có thể thay thế ... bằng bất kỳ biểu thức chính quy nào khác.

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

Ngoại trừ thực tế là bạn không thể truy xuất nội dung của những gì nhóm đã khớp, nhóm không bắt giữ hoạt động giống hệt như nhóm bắt giữ; bạn có thể đặt bất cứ thứ gì vào bên trong nó, lặp lại nó với một siêu ký tự lặp lại chẳng hạn như * và lồng nó vào các nhóm khác (chụp hoặc không bắt). (?:...) đặc biệt hữu ích khi sửa đổi mẫu hiện có, vì bạn có thể thêm nhóm mới mà không thay đổi cách đánh số tất cả các nhóm khác. Cần đề cập rằng không có sự khác biệt về hiệu suất trong việc tìm kiếm giữa các nhóm thu thập và không thu thập; không có hình thức nào nhanh hơn hình thức kia.

Một tính năng quan trọng hơn là các nhóm được đặt tên: thay vì đề cập đến chúng bằng số, các nhóm có thể được tham chiếu bằng tên.

Cú pháp cho một nhóm được đặt tên là một trong những phần mở rộng dành riêng cho Python: (?P<name>...). name rõ ràng là tên của nhóm. Các nhóm được đặt tên hoạt động giống hệt như việc thu thập các nhóm và liên kết thêm tên với một nhóm. Tất cả các phương thức match object xử lý các nhóm bắt giữ đều chấp nhận số nguyên tham chiếu đến nhóm theo số hoặc chuỗi chứa tên nhóm mong muốn. Các nhóm được đặt tên vẫn được cung cấp số nên bạn có thể truy xuất thông tin về nhóm theo hai cách:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '((((Nhiều dấu câu )))' )
>>> m.group('word')
'Rất nhiều'
>>> m.group(1)
'Rất nhiều'

Ngoài ra, bạn có thể truy xuất các nhóm được đặt tên dưới dạng từ điển với groupdict():

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'đầu tiên': 'Jane', 'cuối cùng': 'Doe'}

Các nhóm được đặt tên rất tiện lợi vì chúng cho phép bạn sử dụng những cái tên dễ nhớ thay vì phải nhớ số. Đây là ví dụ RE từ mô-đun imaplib:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<năm>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

Rõ ràng việc truy xuất m.group('zonem') dễ dàng hơn nhiều thay vì phải nhớ truy xuất nhóm 9.

Cú pháp cho các tham chiếu ngược trong một biểu thức chẳng hạn như (...)\1 đề cập đến số lượng của nhóm. Đương nhiên có một biến thể sử dụng tên nhóm thay vì số. Đây là một tiện ích mở rộng Python khác: (?P=name) chỉ ra rằng nội dung của nhóm có tên name sẽ lại được khớp tại thời điểm hiện tại. Biểu thức chính quy để tìm các từ nhân đôi, \b(\w+)\s+\1\b cũng có thể được viết là \b(?P<word>\w+)\s+(?P=word)\b:

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris mùa xuân').group()
'cái'

Những khẳng định về phía trước

Một xác nhận có độ rộng bằng 0 khác là xác nhận nhìn về phía trước. Các xác nhận trước mắt có sẵn ở cả dạng tích cực và tiêu cực và có dạng như sau:

(?=...)

Khẳng định cái nhìn tích cực. Điều này thành công nếu biểu thức chính quy chứa trong đó, được biểu thị ở đây bằng ..., khớp thành công tại vị trí hiện tại và ngược lại thì không thành công. Tuy nhiên, khi biểu thức chứa trong đó đã được thử, công cụ so khớp sẽ không hoạt động nữa; phần còn lại của mẫu được thử ngay tại nơi xác nhận bắt đầu.

(?!...)

Khẳng định cái nhìn tiêu cực. Điều này trái ngược với khẳng định tích cực; nó thành công nếu biểu thức chứa doesn't khớp với vị trí hiện tại trong chuỗi.

Để làm rõ điều này, chúng ta hãy xem xét một trường hợp trong đó việc xem trước rất hữu ích. Hãy xem xét một mẫu đơn giản để khớp với tên tệp và tách nó thành tên cơ sở và phần mở rộng, được phân tách bằng .. Ví dụ: trong news.rc, news là tên cơ sở và rc là phần mở rộng của tên tệp.

Mẫu để phù hợp với điều này khá đơn giản:

.*[.].*$

Lưu ý rằng . cần được xử lý đặc biệt vì nó là siêu ký tự, do đó, nó nằm trong một lớp ký tự để chỉ khớp với ký tự cụ thể đó. Ngoài ra, hãy chú ý đến dấu $; điều này được thêm vào để đảm bảo rằng tất cả phần còn lại của chuỗi phải được đưa vào phần mở rộng. Biểu thức chính quy này khớp với foo.barautoexec.batsendmail.cfprinters.conf.

Bây giờ, hãy xem xét việc phức tạp hóa vấn đề một chút; Điều gì sẽ xảy ra nếu bạn muốn khớp tên tệp có phần mở rộng không phải là bat? Một số lần thử không chính xác:

.*[.][^b].*$

Lần thử đầu tiên ở trên cố gắng loại trừ bat bằng cách yêu cầu ký tự đầu tiên của tiện ích mở rộng không phải là b. Điều này là sai vì mẫu cũng không khớp với foo.bar.

.*[.]([^b]..|.[^a].|..[^t])$

Biểu thức trở nên lộn xộn hơn khi bạn cố gắng khắc phục giải pháp đầu tiên bằng cách yêu cầu một trong các trường hợp sau khớp: ký tự đầu tiên của tiện ích mở rộng không phải là b; ký tự thứ hai không phải là a; hoặc ký tự thứ ba không phải là t. Điều này chấp nhận foo.bar và từ chối autoexec.bat, nhưng nó yêu cầu phần mở rộng gồm ba chữ cái và sẽ không chấp nhận tên tệp có phần mở rộng hai chữ cái, chẳng hạn như sendmail.cf. Chúng tôi sẽ làm phức tạp lại mẫu này trong nỗ lực sửa nó.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

Trong lần thử thứ ba, tất cả các chữ cái thứ hai và thứ ba đều được đặt làm tùy chọn để cho phép khớp các phần mở rộng ngắn hơn ba ký tự, chẳng hạn như sendmail.cf.

Mẫu này hiện đang trở nên thực sự phức tạp, khiến nó khó đọc và khó hiểu. Tệ hơn nữa, nếu sự cố thay đổi và bạn muốn loại trừ cả batexe làm tiện ích mở rộng, mẫu sẽ càng trở nên phức tạp và khó hiểu hơn.

Một cái nhìn tiêu cực sẽ cắt đứt tất cả sự nhầm lẫn này:

.*[.](?!bat$)[^.]*$

Giao diện phủ định có nghĩa là: nếu biểu thức bat không khớp tại thời điểm này, hãy thử phần còn lại của mẫu; nếu bat$ không khớp thì toàn bộ mẫu sẽ thất bại. Cần có $ ở cuối để đảm bảo rằng những thứ như sample.batch, trong đó tiện ích mở rộng chỉ bắt đầu bằng bat, sẽ được cho phép. Zz005zz đảm bảo rằng mẫu hoạt động khi có nhiều dấu chấm trong tên tệp.

Việc loại trừ phần mở rộng tên tệp khác giờ đây thật dễ dàng; chỉ cần thêm nó như một sự thay thế bên trong xác nhận. Mẫu sau loại trừ tên tệp kết thúc bằng bat hoặc exe:

.*[.](?!bat$|exe$)[^.]*$

Sửa đổi chuỗi

Cho đến thời điểm này, chúng tôi chỉ thực hiện tìm kiếm dựa trên một chuỗi tĩnh. Biểu thức chính quy cũng thường được sử dụng để sửa đổi chuỗi theo nhiều cách khác nhau, sử dụng các phương thức mẫu sau:

Phương thức/Thuộc tính

Mục đích

split()

Chia chuỗi thành một danh sách, chia chuỗi đó ở bất cứ nơi nào RE khớp

sub()

Tìm tất cả các chuỗi con có RE khớp và thay thế chúng bằng một chuỗi khác

subn()

Thực hiện tương tự như sub(), nhưng trả về chuỗi mới và số lần thay thế

Tách chuỗi

Phương thức split() của một mẫu sẽ tách một chuỗi ra bất cứ nơi nào RE khớp, trả về danh sách các phần. Nó tương tự như phương thức chuỗi split() nhưng cung cấp tính tổng quát hơn nhiều trong các dấu phân cách mà bạn có thể phân chia theo; chuỗi split() chỉ hỗ trợ phân tách theo khoảng trắng hoặc theo chuỗi cố định. Như bạn mong đợi, cũng có chức năng re.split() cấp mô-đun.

.split(string[, maxsplit=0])

Chia string cho các kết quả khớp của biểu thức chính quy. Nếu sử dụng dấu ngoặc đơn trong RE thì nội dung của chúng cũng sẽ được trả về như một phần của danh sách kết quả. Nếu maxsplit khác 0 thì tối đa việc phân chia maxsplit được thực hiện.

Bạn có thể giới hạn số lần phân tách được thực hiện bằng cách chuyển giá trị cho maxsplit. Khi maxsplit khác 0, tối đa việc phân chia maxsplit sẽ được thực hiện và phần còn lại của chuỗi được trả về làm phần tử cuối cùng của danh sách. Trong ví dụ sau, dấu phân cách là bất kỳ chuỗi ký tự không phải chữ và số nào.

>>> p = re.compile(r'\W+')
>>> p.split('Đây là một bài kiểm tra ngắn gọn và thú vị về phương pháp chia tách().')
['Đây', 'là', 'a', 'kiểm tra', 'ngắn', 'và', 'ngọt ngào', 'của', 'tách', '']
>>> p.split('Đây là một bài kiểm tra ngắn gọn và hấp dẫn của Split().', 3)
['Đây', 'là', 'a', 'kiểm tra, ngắn gọn và ngọt ngào, của Split().']

Đôi khi bạn không chỉ quan tâm đến văn bản giữa các dấu phân cách là gì mà còn cần biết dấu phân cách là gì. Nếu sử dụng dấu ngoặc đơn trong RE thì giá trị của chúng cũng được trả về như một phần của danh sách. So sánh các cuộc gọi sau:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('Đây... là một bài kiểm tra.')
['Cái này', 'là', 'a', 'kiểm tra', '']
>>> p2.split('Đây... là một bài kiểm tra.')
['Cái này', '... ', 'là', ' ', 'a', ' ', 'kiểm tra', '.', '']

Hàm cấp mô-đun re.split() thêm RE được sử dụng làm đối số đầu tiên, nhưng mặt khác thì giống nhau.

>>> re.split(r'[\W]+', 'Từ, từ, từ.')
['Từ', 'từ', 'từ', '']
>>> re.split(r'([\W]+)', 'Từ, từ, từ.')
['Từ', ', ', 'từ', ', ', 'từ', '.', '']
>>> re.split(r'[\W]+', 'Từ, từ, từ.', 1)
['Từ', 'từ, từ.']

Tìm kiếm và thay thế

Một nhiệm vụ phổ biến khác là tìm tất cả các kết quả khớp cho một mẫu và thay thế chúng bằng một chuỗi khác. Phương thức sub() nhận một giá trị thay thế, có thể là chuỗi hoặc hàm và chuỗi cần xử lý.

.sub(replacement, string[, count=0])

Trả về chuỗi thu được bằng cách thay thế các lần xuất hiện không chồng chéo ngoài cùng bên trái của RE trong string bằng replacement thay thế. Nếu không tìm thấy mẫu, string sẽ được trả về không thay đổi.

Đối số tùy chọn count là số lần xuất hiện mẫu tối đa được thay thế; count phải là số nguyên không âm. Giá trị mặc định là 0 có nghĩa là thay thế tất cả các lần xuất hiện.

Đây là một ví dụ đơn giản về cách sử dụng phương pháp sub(). Nó thay thế tên màu bằng từ colour:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('color', 'tất xanh và giày đỏ')
'tất màu và giày màu'
>>> p.sub('màu', 'tất xanh và giày đỏ', count=1)
'tất màu và giày đỏ'

Phương thức subn() thực hiện công việc tương tự nhưng trả về 2 bộ chứa giá trị chuỗi mới và số lần thay thế đã được thực hiện:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('color', 'tất xanh và giày đỏ')
('tất màu và giày màu', 2)
>>> p.subn('color', 'không có màu nào cả')
('không có màu sắc nào cả', 0)

Các kết quả phù hợp trống chỉ được thay thế khi chúng không liền kề với kết quả trống trước đó.

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

Nếu replacement là một chuỗi, mọi dấu gạch chéo ngược thoát ra trong đó đều được xử lý. Nghĩa là, \n được chuyển đổi thành một ký tự dòng mới, \r được chuyển đổi thành ký tự đầu dòng, v.v. Những lối thoát không xác định như \& bị bỏ lại một mình. Các tham chiếu ngược, chẳng hạn như \6, được thay thế bằng chuỗi con khớp với nhóm tương ứng trong RE. Điều này cho phép bạn kết hợp các phần của văn bản gốc vào chuỗi thay thế thu được.

Ví dụ này khớp với từ section, theo sau là một chuỗi được bao trong {, } và thay đổi section thành subsection:

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{Đầu tiên} phần{giây}')
'tiểu mục{Đầu tiên} tiểu mục{giây}'

Ngoài ra còn có một cú pháp để tham chiếu đến các nhóm được đặt tên như được xác định bởi cú pháp (?P<name>...). \g<name> sẽ sử dụng chuỗi con khớp với nhóm có tên name\g<number> sử dụng số nhóm tương ứng. Do đó, \g<2> tương đương với \2, nhưng không mơ hồ trong chuỗi thay thế, chẳng hạn như \g<2>0. (\20 sẽ được hiểu là tham chiếu đến nhóm 20, không phải tham chiếu đến nhóm 2 theo sau là ký tự chữ '0'.) Các thay thế sau đây đều tương đương, nhưng sử dụng cả ba biến thể của chuỗi thay thế.

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'tiểu mục{Đầu tiên}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'tiểu mục{Đầu tiên}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'tiểu mục{Đầu tiên}'

replacement cũng có thể là một chức năng giúp bạn có nhiều quyền kiểm soát hơn. Nếu replacement là một hàm thì hàm này sẽ được gọi cho mỗi lần xuất hiện không chồng lấp của pattern. Trong mỗi lệnh gọi, hàm này được truyền một đối số match object cho kết quả khớp và có thể sử dụng thông tin này để tính toán chuỗi thay thế mong muốn và trả về chuỗi đó.

Trong ví dụ sau, hàm thay thế chuyển số thập phân thành thập lục phân:

>>> def hexrepl(match):
... "Trả về chuỗi hex cho số thập phân"
... giá trị = int(match.group())
... trả về hex(giá trị)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Gọi 65490 để in, 49152 để lấy mã người dùng.')
'Gọi 0xffd2 để in, 0xc000 để lấy mã người dùng.'

Khi sử dụng hàm re.sub() cấp mô-đun, mẫu sẽ được chuyển làm đối số đầu tiên. Mẫu có thể được cung cấp dưới dạng một đối tượng hoặc dưới dạng một chuỗi; nếu bạn cần chỉ định cờ biểu thức chính quy, bạn phải sử dụng đối tượng mẫu làm tham số đầu tiên hoặc sử dụng công cụ sửa đổi được nhúng trong chuỗi mẫu, ví dụ: sub("(?i)b+", "x", "bbbb BBBB") trả về 'x x'.

Các vấn đề thường gặp

Biểu thức chính quy là một công cụ mạnh mẽ cho một số ứng dụng, nhưng theo một số cách, hành vi của chúng không trực quan và đôi khi chúng không hoạt động theo cách bạn mong đợi. Phần này sẽ chỉ ra một số cạm bẫy phổ biến nhất.

Sử dụng các phương thức chuỗi

Đôi khi sử dụng mô-đun re là một sai lầm. Nếu bạn đang khớp một chuỗi cố định hoặc một lớp ký tự đơn và bạn không sử dụng bất kỳ tính năng re nào chẳng hạn như cờ IGNORECASE thì có thể không cần toàn bộ sức mạnh của biểu thức chính quy. Chuỗi có một số phương thức để thực hiện các thao tác với chuỗi cố định và chúng thường nhanh hơn nhiều vì việc triển khai là một vòng lặp C nhỏ duy nhất được tối ưu hóa cho mục đích thay vì công cụ biểu thức chính quy lớn, tổng quát hơn.

Một ví dụ có thể là thay thế một chuỗi cố định bằng một chuỗi khác; ví dụ: bạn có thể thay thế word bằng deed. re.sub() có vẻ giống như hàm được sử dụng cho việc này, nhưng hãy xem xét phương pháp replace(). Lưu ý rằng replace() cũng sẽ thay thế word bên trong các từ, biến swordfish thành sdeedfish, nhưng RE word ngây thơ cũng sẽ làm điều đó. (Để tránh thực hiện việc thay thế các phần của từ, mẫu sẽ phải là \bword\b, để yêu cầu word có ranh giới từ ở hai bên. Điều này khiến công việc vượt quá khả năng của replace().)

Một tác vụ phổ biến khác là xóa mọi lần xuất hiện của một ký tự trong chuỗi hoặc thay thế nó bằng một ký tự đơn khác. Bạn có thể thực hiện việc này với thứ gì đó như re.sub('\n', ' ', S), nhưng translate() có khả năng thực hiện cả hai tác vụ và sẽ nhanh hơn bất kỳ thao tác biểu thức chính quy nào có thể thực hiện được.

Nói tóm lại, trước khi chuyển sang mô-đun re, hãy xem xét liệu vấn đề của bạn có thể được giải quyết bằng phương pháp chuỗi nhanh hơn và đơn giản hơn hay không.

Tham lam và không tham lam

Khi lặp lại một biểu thức chính quy, chẳng hạn như trong a*, hành động thu được là sử dụng càng nhiều mẫu càng tốt. Thực tế này thường làm bạn khó chịu khi bạn đang cố gắng so khớp một cặp dấu phân cách cân bằng, chẳng hạn như dấu ngoặc nhọn bao quanh thẻ HTML. Mẫu đơn giản để khớp một thẻ HTML không hoạt động do tính chất tham lam của .*.

>>> s = '<html><head><title>Tiêu đề</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Tiêu đề</title>

RE khớp với '<' trong '<html>'.* tiêu thụ phần còn lại của chuỗi. Tuy nhiên, vẫn còn nhiều hơn trong RE và > không thể khớp ở cuối chuỗi, vì vậy công cụ biểu thức chính quy phải dò ngược từng ký tự cho đến khi tìm thấy kết quả khớp với >. Trận đấu cuối cùng kéo dài từ '<' trong '<html>' đến '>' trong '</title>', đây không phải là điều bạn muốn.

Trong trường hợp này, giải pháp là sử dụng các bộ định lượng không tham lam *?, +?, ?? hoặc {m,n}?, khớp với văn bản little nhất có thể. Trong ví dụ trên, '>' được thử ngay sau lần khớp '<' đầu tiên và khi thất bại, công cụ sẽ tiến lên một nhân vật tại một thời điểm, thử lại '>' ở mỗi bước. Điều này tạo ra kết quả đúng

>>> print(re.match('<.*?>', s).group())
<html>

(Lưu ý rằng việc phân tích cú pháp HTML hoặc XML bằng các biểu thức chính quy là điều khó khăn. Các mẫu nhanh và bẩn sẽ xử lý các trường hợp phổ biến, nhưng HTML và XML có các trường hợp đặc biệt sẽ phá vỡ biểu thức chính quy rõ ràng; vào thời điểm bạn viết một biểu thức chính quy xử lý tất cả các trường hợp có thể xảy ra, các mẫu sẽ phức tạp very. Hãy sử dụng mô-đun phân tích cú pháp HTML hoặc XML cho các tác vụ như vậy.)

Sử dụng re.VERBOSE

Đến đây có lẽ bạn đã nhận thấy rằng biểu thức chính quy là một ký hiệu rất nhỏ gọn nhưng chúng rất khó đọc. RE có độ phức tạp vừa phải có thể trở thành tập hợp dài các dấu gạch chéo ngược, dấu ngoặc đơn và siêu ký tự, khiến chúng khó đọc và khó hiểu.

Đối với các RE như vậy, việc chỉ định cờ re.VERBOSE khi biên dịch biểu thức chính quy có thể hữu ích vì nó cho phép bạn định dạng biểu thức chính quy rõ ràng hơn.

Cờ re.VERBOSE có một số hiệu ứng. Khoảng trắng trong biểu thức chính quy mà isn't bên trong một lớp ký tự bị bỏ qua. Điều này có nghĩa là một biểu thức như dog | cat tương đương với dog|cat khó đọc hơn, nhưng [a b] vẫn sẽ khớp với các ký tự 'a', 'b' hoặc khoảng trắng. Ngoài ra, bạn cũng có thể đặt các nhận xét bên trong RE; nhận xét mở rộng từ ký tự # sang dòng mới tiếp theo. Khi được sử dụng với các chuỗi có dấu ngoặc kép, điều này cho phép RE được định dạng gọn gàng hơn:

pat = re.compile(r"""
 \s* # Skip khoảng trắng ở đầu
 (?P<header>[^:]+) tên # Header
 \s* : # Whitespace và dấu hai chấm
 (?P<value>.*?)      # The header's value -- *? đã từng
                     # lose khoảng trắng ở cuối sau
 \s*$ # Trailing khoảng trắng ở cuối dòng
""", re.VERBOSE)

Điều này dễ đọc hơn nhiều so với:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

Phản hồi

Biểu thức chính quy là một chủ đề phức tạp. Tài liệu này có giúp bạn hiểu chúng không? Có phần nào chưa rõ ràng hoặc vấn đề bạn gặp phải chưa được đề cập ở đây không? Nếu vậy, vui lòng gửi đề xuất cải tiến cho issue tracker.

Cuốn sách đầy đủ nhất về biểu thức chính quy gần như chắc chắn là Mastering Regular Expressions của Jeffrey Friedl, do O'Reilly xuất bản. Thật không may, nó chỉ tập trung vào các dạng biểu thức chính quy của Perl và Java và hoàn toàn không chứa bất kỳ tài liệu Python nào, vì vậy nó sẽ không hữu ích khi làm tài liệu tham khảo cho việc lập trình bằng Python. (Phiên bản đầu tiên bao gồm mô-đun regex hiện đã bị loại bỏ của Python, điều này sẽ không giúp ích gì nhiều cho bạn.) Hãy cân nhắc việc kiểm tra nó từ thư viện của bạn.