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

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

Trong Python, một cụm từ tìm kiếm theo biểu thức chính quy thường được viết là:

match = re.search(pat, str)

Phương thức re.search() nhận một mẫu biểu thức chính quy và một chuỗi, đồng thời tìm kiếm mẫu đó trong chuỗi. Nếu tìm kiếm thành công, search() sẽ trả về một đối tượng khớp hoặc Không có đối tượng nào. Do đó, tìm kiếm thường được đặt ngay sau câu lệnh if để kiểm tra xem việc tìm kiếm có thành công hay không, như trong ví dụ sau, tìm kiếm mẫu 'word:' theo sau là từ gồm 3 chữ cái (chi tiết 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ị là true, thì việc tìm kiếm đã thành công và match.group() là văn bản trùng khớp (ví dụ: 'word:cat'). Ngược lại, nếu kết quả khớp là sai (Không có nội dung cụ thể nào hơn), thì lượt tìm kiếm sẽ không thành công và không có văn bản nào phù hợp.

Dấu 'r' ở đầu chuỗi mẫu chỉ định chuỗi "thô" của python đi qua dấu gạch chéo ngược mà không thay đổi, rất hữu ích cho biểu thức chính quy (Java rất cần tính năng này!). Bạn nên luôn viết các chuỗi mẫu có ký tự "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 mẫu, chứ không chỉ là các ký tự cố định. Dưới đây là các mẫu cơ bản nhất phù hợp với ký tự đơn:

  • a, X, 9, < -- các ký tự thông thường khớp chính xác với chính chúng. Các siêu ký tự không 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 bất kỳ ký tự đơn nào ngoại trừ dòng mới '\n'
  • \w – (chữ thường w) khớp với ký tự "từ": một chữ cái hoặc chữ số hoặc thanh gạch dưới [a-zA-Z0-9_]. Lưu ý rằng mặc dù "từ" là ký tự ghi nhớ cho việc này, nhưng nó chỉ khớp với một từ duy nhất char chứ không phải một từ nguyên vẹn. \W (chữ hoa W) khớp với bất kỳ ký tự nào không phải 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 – dấu cách, dòng mới, giá trị trả về, thẻ, biểu mẫu [ \n\r\t\f]. \S (chữ hoa S) khớp với mọi ký tự không phải là khoảng trắng.
  • \n, \n, \r -- tab, dòng mới, trả về
  • \d - chữ số thập phân [0-9] (một số tiện ích biểu thức chính quy cũ không hỗ trợ \d, nhưng tất cả đề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ụ: sử dụng \. để khớp dấu chấm hoặc \\ để khớp với dấu gạch chéo. Nếu bạn không chắc một ký tự có ý nghĩa đặc biệt (chẳng hạn như "@") hay không, bạn có thể thử đặt dấu gạch chéo lên phía trước ký tự đó, \@. Nếu ký tự đó không phải là chuỗi thoát hợp lệ, chẳng hạn như \c, thì chương trình python của bạn sẽ dừng lại do lỗi.

Ví dụ cơ bản

Chuyện cười: bạn gọi một con lợn có ba mắt là gì? trời ơi!

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

  • Tìm kiếm sẽ tiếp tục qua chuỗi từ đầu đến cuối, dừng lại ở kết quả phù hợp đầu tiên được tìm thấy
  • Tất cả mẫu phải khớp, nhưng không phải toàn bộ chuỗi
  • Nếu match = re.search(pat, str) thành công, thì kết quả trùng khớp sẽ không phải là Không có (None) mà cụ thể là match.group() sẽ 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ứ sẽ trở nên thú vị hơn khi bạn sử dụng + và * để chỉ định sự 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 i's
  • * -- 0 hoặc nhiều lần xuất hiện của mẫu ở bên trái
  • ? -- kết hợp 0 hoặc 1 lần xuất hiện của mẫu với bên trái

Ngoài cùng bên trái và lớn nhất

Đầu tiên, chức năng tìm kiếm sẽ tìm kết quả phù hợp nhất bên trái cho mẫu đó, và thứ hai là cố gắng sử dụng nhiều chuỗi nhất có thể – tức là + và * càng nhiều càng tốt (dấu + và * được cho 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ỉ tím'. Chúng tôi sẽ sử dụng ví dụ này làm ví dụ về chạy để minh hoạ thêm các tính năng của biểu thức chính quy. Hãy thử 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 lấy được toàn bộ địa chỉ email do \w không khớp với '-' hoặc '.' trong địa chỉ. Chúng ta sẽ khắc phục vấn đề này bằng cách sử dụng các tính năng biểu thức chính quy dưới đây.

Chân đế hình 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 vấn đề về email, dấu ngoặc vuông là một cách dễ dàng để thêm '.' và '-' vào bộ 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 dấu ngoặc vuông khác) Bạn cũng có thể sử dụng dấu gạch ngang để biểu thị dải ô, vì vậy [a-z] khớp với tất cả chữ cái viết thường. Để sử dụng dấu gạch ngang mà không biểu thị dải ô, hãy đặt dấu gạch ngang cuối cùng, ví dụ: [abc-]. Mũi tên hướng lên (^) ở đầu cặp 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

Tính năng "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 mà chúng ta muốn trích xuất riêng tên người dùng và máy chủ lưu trữ. Để làm điều 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 không thay đổi mẫu sẽ khớp, thay vào đó chúng thiết lập "nhóm" logic bên trong văn bản khớp. Trên một lượt 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 đầu tiên 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 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)

Quy trình làm việc phổ biến với biểu thức chính quy là bạn viết một mẫu cho nội dung bạn đ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 mạnh nhất trong mô-đun lại. Ở trên, chúng tôi đã sử dụng re.search() để tìm kết quả phù hợp đầu tiên cho một mẫu. findall() sẽ 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)

findall có tệp

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

findall và Groups

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 nhóm dấu ngoặc đơn trở lên, thì thay vì trả về một danh sách chuỗi, findall() sẽ trả về danh sách *bộ dữ liệu*. Mỗi bộ dữ liệu đại diện cho một kiểu khớp của mẫu và bên trong bộ dữ liệu là nhóm(1), group(2) .. dữ liệu. 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ộ giá trị, mỗi độ dài 2 chứa tên người dùng và máy chủ, 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 phép 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 đã 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 nhóm đơn đó. (Tính năng tuỳ 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 các dấu ngoặc đơn có ?: ở đầu, ví dụ: (?: ) và dấu ngoặc đơn bên trái sẽ không được tính là kết quả theo nhóm.)

Gỡ lỗi và quy trình làm việc RE

Mẫu biểu thức chính quy đóng 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, nên bạn có thể dành nhiều thời gian để gỡ lỗi cho mẫu của mình. Thiết lập thời gian chạy để có thể chạy một mẫu và in những 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 thử nghiệm nhỏ và in kết quả của findall(). Nếu mẫu không khớp với gì, hãy thử làm suy yếu mẫu đó, xoá các phần của mẫu đó để bạn nhận được quá nhiều kết quả trùng khớp. Khi nó không khớp với gì, bạn không thể tiến bộ vì không có gì cụ thể để xem xét. Khi nó khớp quá nhiều, bạn có thể tăng dần số lượng để đạt được thứ mình muốn.

Tùy chọn

Hàm re có 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 vào dưới dạng một đối số bổ sung vào hàm search() hoặc findall(), v.v., ví dụ: re.search(pat, str, re.IgnoreCASE).

  • BỎ QUA – bỏ qua những điểm khác biệt chữ hoa/chữ thường khi 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, dấu chấm này khớp với mọi dòng trừ dòng mới. Việc này có thể khiến bạn gặp khó khăn – bạn cho rằng .* khớp với mọi trường hợp, nhưng theo mặc định, ký tự 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 dòng mới, vì vậy 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 sử dụng \s*
  • MULTILINE -- Trong một chuỗi gồm nhiều dòng, hãy cho phép ^ và $ khớp với phần đầu và cuối 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 người không tham gia (không bắt buộc)

Đây là phần không bắt buộc, trong đó giới thiệu một kỹ thuật biểu thức chính quy nâng cao hơn và không cần thiết cho bài tập.

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

Giả sử bạn đang cố gắng kết hợp mỗi thẻ với mẫu '(<.*>)' -- thẻ nào sẽ khớp trước tiên?

Kết quả này hơi đáng 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>vừa</i>' như một kết quả trùng khớp lớn. Vấn đề là .* đi xa nhất có thể, thay vì dừng ở đầu > (còn gọi là "tham lam").

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

Phần mở rộng *? có nguồn gốc từ Perl và các biểu thức chính quy bao gồm 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 ứng dụng dòng lệnh, v.v. có cờ để chấp nhận các mẫu pcre.

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

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 từ nhóm(1), nhóm(2), v.v. từ văn bản khớp gốc.

Dưới đây là ví dụ tìm kiếm tất cả địa chỉ email và thay đổi các địa chỉ đó để giữ người dùng (\1) nhưng lại chọn yo-yo-dyne.com làm 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

Để thực hành biểu thức chính quy, hãy xem Bài tập về tên em bé.