Biểu thức chính quy trong Python

Biểu thức chính quy là một ngôn ngữ hiệu quả để so khớp các mẫu văn bản. Trang này giới thiệu cơ bản về biểu thức chính quy đủ để làm các bài tập Python và cho thấy cách hoạt động của biểu thức chính quy trong Python. Chữ "re" của Python cung cấp hỗ trợ biểu thức chính quy.

Trong Python, phép tìm kiếm biểu thức chính quy thường được viết là:

match = re.search(pat, str)

Phương thức re.search() lấy một mẫu biểu thức chính quy cùng một chuỗi rồi tìm kiếm mẫu đó trong chuỗi. Nếu tìm kiếm thành công, search() sẽ trả về đối tượng khớp hoặc Không có nếu không. Do đó, tìm kiếm thường đứng ngay trước câu lệnh if để kiểm tra xem tìm kiếm có thành công hay không, như được minh hoạ trong ví dụ sau, khi tìm kiếm mẫu 'từ': theo sau là một từ gồm 3 chữ cái (chi tiết như bên dưới):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

match = re.search(pat, str) lưu trữ kết quả tìm kiếm trong một biến có tên là "match". Sau đó, câu lệnh if sẽ kiểm tra kết quả trùng khớp -- nếu giá trị true thì tìm kiếm thành công và match.group() là văn bản khớp (ví dụ: 'word:cat'). Ngược lại, nếu kết quả trùng khớp là false (Không có gì cụ thể hơn), thì tìm kiếm không thành công và không có văn bản nào phù hợp.

Chữ 'r' ở đầu chuỗi mẫu chỉ định một python "thô" chuỗi vượt qua dấu gạch chéo ngược mà không thay đổi, rất thuận tiện với biểu thức chính quy (Java cần tính năng này rất kém!). Tôi khuyên bạn nên luôn viết chuỗi mẫu có 'r' như một thói quen.

Mẫu cơ bản

Điểm mạnh của biểu thức chính quy là chúng có thể chỉ định các mẫu, không chỉ các ký tự cố định. Dưới đây là các mẫu cơ bản nhất khớp với một ký tự đơn:

  • a, X, 9, < -- nhân vật thông thường chỉ khớp chính xác với chính họ. Các siêu ký tự không tự khớp với chính chúng vì chúng có ý nghĩa đặc biệt là: . ^ $ * + ? { [ ] \ | ( ) (chi tiết bên dưới)
  • . (dấu chấm) -- khớp với mọi ký tự đơn ngoại trừ dòng mới '\n'
  • \w -- (chữ thường w) khớp với một "từ" ký tự: một chữ cái hoặc chữ số hoặc gạch dưới [a-zA-Z0-9_]. Lưu ý rằng mặc dù "từ" là cách ghi nhớ cho việc này, nó chỉ khớp với một từ duy nhất, chứ không phải cả một từ. \W (chữ hoa W) khớp với mọi ký tự không phải là từ.
  • \b -- ranh giới giữa từ và không phải từ
  • \s -- (chữ thường s) khớp với một ký tự khoảng trắng duy nhất -- dấu cách, dòng mới, trả về, thẻ, biểu mẫu [ \n\t\t\f]. \S (S chữ hoa) khớp với mọi ký tự không phải khoảng trắng.
  • \n, \n, \r -- tab, dòng mới, trở lại
  • \d -- chữ số thập phân [0-9] (một số tiện ích biểu thức chính quy cũ hơn không hỗ trợ \d, nhưng tất cả chúng đều hỗ trợ \w và \s)
  • ^ = start, $ = end -- khớp với phần đầu hoặc phần cuối của chuỗi
  • \ -- hạn chế "đặc biệt" của một ký tự. Ví dụ: hãy sử dụng \. để khớp với dấu chấm hoặc \\ để khớp với dấu gạch chéo. Nếu bạn không chắc chắn liệu một ký tự có ý nghĩa đặc biệt hay không, chẳng hạn như "@", bạn có thể thử đặt một dấu gạch chéo lên trước, \@. Nếu đó không phải là một chuỗi thoát hợp lệ, chẳng hạn như \c, thì chương trình python của bạn sẽ tạm dừng kèm theo thông báo lỗi.

Ví dụ cơ bản

Chuyện cười: bạn gọi lợn có 3 mắt là gì? ái chà!

Quy tắc cơ bản của việc tìm kiếm biểu thức chính quy cho một mẫu trong một chuỗi là:

  • Quá trình tìm kiếm tiếp tục qua chuỗi từ đầu đến cuối, dừng ở kết quả trùng khớp đầu tiên tìm được
  • Phải khớp tất cả mẫu, nhưng không phải toàn bộ chuỗi
  • Nếu match = re.search(pat, str) thành công, kết quả trùng khớp sẽ không phải là Không có và cụ thể là match.group() là văn bản trùng khớp
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Lặp lại

Mọi thứ trở nên thú vị hơn khi bạn sử dụng + và * để chỉ định lặp lại trong mẫu

  • + -- 1 hoặc nhiều lần xuất hiện của mẫu ở bên trái, ví dụ: "i+" = một hoặc nhiều tôi
  • * -- 0 lần xuất hiện trở lên của mẫu ở bên trái
  • ? -- khớp 0 hoặc 1 lần xuất hiện của mẫu này với bên trái

Ngoài cùng bên trái & Lớn nhất

Đầu tiên, tìm kiếm tìm kết quả khớp ngoài cùng bên trái cho mẫu và thứ hai là tìm kiếm sử dụng hết nhiều chuỗi nhất có thể -- tức là + và * đi xa nhất có thể (dấu + và * được gọi là "tham lam").

Ví dụ về việc lặp lại

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Ví dụ về email

Giả sử bạn muốn tìm địa chỉ email bên trong chuỗi 'xyz alice-b@google.com khỉ màu tím'. Chúng ta sẽ dùng ví dụ này làm ví dụ chạy để minh hoạ thêm các tính năng của biểu thức chính quy. Hãy thử sử dụng mẫu r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

Trong trường hợp này, tìm kiếm không nhận được toàn bộ địa chỉ email vì \w không khớp với '-' hoặc '.' trong địa chỉ. Chúng tôi sẽ khắc phục vấn đề này bằng cách sử dụng các tính năng của biểu thức chính quy bên dưới.

Dấu ngoặc vuông

Bạn có thể dùng dấu ngoặc vuông để biểu thị một tập hợp ký tự, vì vậy [abc] khớp với "a" hoặc 'b' hoặc 'c'. Các mã \w, \s, v.v. cũng hoạt động bên trong dấu ngoặc vuông với một ngoại lệ là dấu chấm (.) chỉ có nghĩa là dấu chấm theo nghĩa đen. Đối với các vấn đề về email, dấu ngoặc vuông là cách dễ dàng để thêm '.' và '-' đối với tập hợp các ký tự có thể xuất hiện xung quanh @ với mẫu r'[\w.-]+@[\w.-]+' để lấy toàn bộ địa chỉ email:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(Các tính năng khác trong dấu ngoặc vuông) Bạn cũng có thể sử dụng dấu gạch ngang để biểu thị một dải ô, vì vậy [a-z] khớp với tất cả các chữ cái viết thường. Để sử dụng dấu gạch ngang mà không chỉ phạm vi, hãy đặt dấu gạch ngang cuối cùng, ví dụ: [abc-]. Mũ đội lên (^) ở đầu bộ dấu ngoặc vuông đảo ngược nó, vì vậy [^ab] có nghĩa là bất kỳ ký tự nào ngoại trừ 'a' hoặc 'b'.

Trích xuất nhóm

"Nhóm" của biểu thức chính quy cho phép bạn chọn các phần của văn bản phù hợp. Giả sử đối với sự cố email, chúng ta muốn trích xuất riêng tên người dùng và máy chủ lưu trữ. Để thực hiện việc này, hãy thêm dấu ngoặc đơn ( ) xung quanh tên người dùng và máy chủ lưu trữ trong mẫu, như sau: r'([\w.-]+)@([\w.-]+)'. Trong trường hợp này, dấu ngoặc đơn sẽ không thay đổi nội dung mà mẫu sẽ khớp, thay vào đó chúng thiết lập "nhóm" logic bên trong văn bản đối sánh. Khi tìm kiếm thành công, match.group(1) là văn bản khớp tương ứng với dấu ngoặc đơn bên trái thứ 1 và match.group(2) là văn bản tương ứng với dấu ngoặc đơn bên trái thứ 2. Match.group() thuần tuý vẫn là toàn bộ văn bản trùng khớp như thường lệ.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

Một quy trình làm việc chung với biểu thức chính quy là bạn viết mẫu cho nội dung mình đang tìm kiếm, thêm các nhóm dấu ngoặc đơn để trích xuất các phần bạn muốn.

tìm tất cả

findall() có thể là hàm đơn lẻ mạnh nhất trong mô-đun re. Ở trên, chúng ta đã dùng hàm re.search() để tìm kết quả phù hợp đầu tiên cho một mẫu. findall() tìm *tất cả* kết quả trùng khớp và trả về chúng dưới dạng danh sách chuỗi, với mỗi chuỗi đại diện cho một kết quả trùng khớp.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

tìm tất cả có tệp

Đối với tệp, bạn có thể có thói quen viết vòng lặp để lặp lại các dòng của tệp, sau đó bạn có thể gọi findall() trên mỗi dòng. Thay vào đó, hãy để findall() thực hiện việc lặp lại cho bạn -- tốt hơn nhiều! Chỉ cần đưa toàn bộ văn bản tệp vào findall() và để công cụ này trả về danh sách tất cả kết quả trùng khớp trong một bước duy nhất (hãy nhớ rằng f.read() trả về toàn bộ văn bản của một tệp trong một chuỗi):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

tìm tất cả và Nhóm

Cơ chế nhóm dấu ngoặc đơn ( ) có thể được kết hợp với findall(). Nếu mẫu bao gồm 2 hoặc nhiều nhóm dấu ngoặc đơn, thì thay vì trả về một danh sách chuỗi, findall() sẽ trả về một danh sách *tuples*. Mỗi bộ dữ liệu đại diện cho một kết quả trùng khớp của mẫu và bên trong bộ dữ liệu đó là dữ liệu group(1), group(2) .. Vì vậy, nếu 2 nhóm dấu ngoặc đơn được thêm vào mẫu email, thì findall() sẽ trả về một danh sách các bộ dữ liệu, mỗi độ dài 2 chứa tên người dùng và máy chủ lưu trữ, ví dụ: ('alice', 'google.com').

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Sau khi có danh sách các bộ dữ liệu, bạn có thể lặp lại danh sách đó để thực hiện một số tính toán cho mỗi bộ dữ liệu. Nếu mẫu không có dấu ngoặc đơn thì findall() sẽ trả về danh sách các chuỗi được tìm thấy như trong các ví dụ trước. Nếu mẫu bao gồm một cặp dấu ngoặc đơn thì findall() sẽ trả về danh sách các chuỗi tương ứng với một nhóm đó. (Tính năng tùy chọn bị che khuất: Đôi khi bạn có các nhóm dấu ngoặc đơn ( ) trong mẫu, nhưng bạn không muốn trích xuất. Trong trường hợp đó, hãy viết dấu ngoặc đơn có ?: ở đầu, ví dụ: (?: ) và dấu ngoặc đơn trái đó sẽ không được tính là kết quả nhóm.)

Quy trình công việc và gỡ lỗi RE

Mẫu biểu thức chính quy gói rất nhiều ý nghĩa chỉ trong một vài ký tự , nhưng chúng rất dày đặc, bạn có thể dành nhiều thời gian để gỡ lỗi các mẫu của mình. Thiết lập thời gian chạy để bạn có thể chạy mẫu và in mẫu phù hợp một cách dễ dàng, chẳng hạn như bằng cách chạy mẫu trên một văn bản kiểm thử nhỏ rồi in kết quả của lệnh findall(). Nếu mẫu không khớp, hãy thử làm yếu mẫu, xoá các phần của mẫu để bạn có quá nhiều kết quả trùng khớp. Khi dữ liệu không khớp với nhau, bạn không thể có tiến triển vì không có gì cụ thể để xem xét. Khi khớp quá nhiều, bạn có thể tìm cách thắt chặt dần để đạt được chỉ số mong muốn.

Tùy chọn

Hàm re nhận các tuỳ chọn để sửa đổi hành vi của việc so khớp mẫu. Cờ tuỳ chọn được thêm làm đối số bổ sung cho search() hoặc findall(), v.v., ví dụ: re.search(pat, str, re.quá trình đồng ý).

  • BỎ QUA -- bỏ qua sự khác biệt viết hoa/chữ thường để so khớp, vì vậy 'a' khớp với cả "a" và 'A'.
  • DOTALL - cho phép dấu chấm (.) để khớp với dòng mới - thông thường nó khớp với bất kỳ dòng nào trừ dòng mới. Việc này có thể làm bạn vướng mắc -- bạn cho rằng .* khớp với mọi giá trị, nhưng theo mặc định, giá trị này không vượt quá cuối dòng. Lưu ý rằng \s (khoảng trắng) bao gồm cả các dòng mới, nên nếu muốn khớp một loạt khoảng trắng có thể bao gồm một dòng mới, bạn chỉ cần dùng \s*
  • MULTILINE -- Trong một chuỗi gồm nhiều dòng, cho phép ^ và $ khớp với điểm bắt đầu và kết thúc của mỗi dòng. Thông thường, ^/$ sẽ chỉ khớp với phần đầu và phần cuối của toàn bộ chuỗi.

Tham lam so với Không tham lam (không bắt buộc)

Đây là phần không bắt buộc, hiển thị kỹ thuật biểu thức chính quy nâng cao hơn không cần thiết cho các bài tập.

Giả sử bạn có văn bản chứa thẻ: <b>foo</b> và <i>v.v.</i>

Giả sử bạn đang cố kết hợp từng thẻ với mẫu '(<.*>)' -- điều kiện nào khớp đầu tiên?

Kết quả hơi ngạc nhiên, nhưng khía cạnh tham lam của .* khiến nó khớp với toàn bộ '<b>foo</b> và <i>tiếp tục</i>' như một trận đấu lớn. Vấn đề là .* đi xa nhất có thể, thay vì dừng ở đầu tiên > (hay còn gọi là "tham lam").

Có phần mở rộng cho biểu thức chính quy mà bạn thêm ? ở cuối, chẳng hạn như .*? hoặc .+?, thay đổi chúng để không tham lam. Giờ thì chúng dừng lại càng sớm càng tốt. Vì vậy, mẫu '(<.*?>)' sẽ chỉ nhận được '<b>' là kết quả phù hợp đầu tiên và '</b>' làm kết quả trùng khớp thứ hai và cứ tiếp tục như thế để nhận được mỗi <..> ghép nối lần lượt. Thường thì bạn sử dụng kiểu .*? ngay trước đó là một số điểm đánh dấu cụ thể (> trong trường hợp này) mà .*? lượt chạy buộc phải kéo dài.

Dấu *? phần mở rộng bắt nguồn từ Perl và biểu thức chính quy bao gồm các phần mở rộng của Perl được gọi là Biểu thức chính quy tương thích với Perl -- pcre. Python có hỗ trợ pcre. Nhiều tiện ích dòng lệnh, v.v. có cờ chấp nhận các mẫu pcre.

Một kỹ thuật cũ hơn nhưng được sử dụng rộng rãi để viết mã cho ý tưởng "tất cả các ký tự này ngoại trừ các ký tự dừng ở X" sử dụng kiểu dấu ngoặc vuông. Ở trên, bạn có thể viết mẫu, nhưng thay vì .* để nhận tất cả các ký tự, hãy sử dụng [^>]* để bỏ qua tất cả các ký tự không > (dấu ngoặc vuông ^ "đảo ngược" ở đầu sẽ khớp với bất kỳ ký tự nào không nằm trong dấu ngoặc vuông).

Thay thế (không bắt buộc)

Hàm re.sub(pat, replace, str) tìm kiếm tất cả các phiên bản của mẫu trong chuỗi đã cho và thay thế các phiên bản đó. Chuỗi thay thế có thể bao gồm "\1", "\2" tham chiếu đến văn bản trong nhóm(1), nhóm(2), v.v. từ văn bản trùng khớp ban đầu.

Sau đây là ví dụ về cách tìm tất cả địa chỉ email và thay đổi các địa chỉ đó để giữ lại người dùng (\1) nhưng có yo-yo-dyne.com là máy chủ lưu trữ.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Bài tập

Để luyện tập biểu thức chính quy, hãy xem Bài tập tên trẻ.