Hướng Dẫn Toàn Diện Về Biểu Thức Chính Quy Trong Python

1. Hệ Thống Ký Hiệu và Cú Pháp Regex Cốt Lõi

Biểu thức chính quy (Regular Expression - Regex) cung cấp một ngôn ngữ mẫu mạnh mẽ để tìm kiếm, khớp và thao tác chuỗi. Dưới đây là bảng tổng hợp các toán tử và ký tự đặc biệt thường dùng.

1.1. Các Toán Tử và Ký Tự Cơ Bản

Biểu ThứcMô TảVí Dụ
literalKhớp chính xác chuỗi văn bản literalapple
exp1|exp2Khớp biểu thức exp1 hoặc exp2cat|dog
.Khớp bất kỳ ký tự đơn nào (trừ ký tự xuống dòng \n)a.c
^Khớp vị trí bắt đầu của chuỗi^Hello
$Khớp vị trí kết thúc của chuỗiend$
*Khớp 0 hoặc nhiều lần ký tự/mẫu đứng trướcab*c
+Khớp 1 hoặc nhiều lần ký tự/mẫu đứng trướcab+c
?Khớp 0 hoặc 1 lần ký tự/mẫu đứng trướccolou?r
{N}Khớp chính xác N lần\d{3}
{M,N}Khớp từ M đến N lần\d{2,4}
[...]Khớp bất kỳ ký tự nào nằm trong tập hợp[aeiou]
[^...]Khớp bất kỳ ký tự nào KHÔNG nằm trong tập hợp[^0-9]
(...)Nhóm các mẫu lại và lưu vào bộ nhớ đệm để trích xuất(\d+)-(\d+)

1.2. Các Lớp Ký Tự Viết Tắt (Shorthand Classes)

Ký HiệuMô TảTương Đương
\dKhớp bất kỳ chữ số thập phân nào[0-9]
\DKhớp bất kỳ ký tự nào KHÔNG phải là số[^0-9]
\wKhớp ký tự chữ cái, số hoặc gạch dưới[a-zA-Z0-9_]
\WKhớp ký tự KHÔNG thuộc nhóm chữ cái, số, gạch dưới[^a-zA-Z0-9_]
\sKhớp bất kỳ ký tự khoảng trắng nào (space, tab, newline)[ \t\n\r\f\v]
\SKhớp bất kỳ ký tự nào KHÔNG phải là khoảng trắng[^ \t\n\r\f\v]
\bKhớp ranh giới của một từ (word boundary)\bword\b

1.3. Cú Pháp Mở Rộng (Lookaround và Nhóm Nâng Cao)

Cú PhápMô Tả
(?:...)Nhóm không lưu trữ (non-capturing group), không tốn bộ nhớ đệm
(?P<name>...)Nhóm được đặt tên (named group), truy xuất qua tên thay vì chỉ số
(?=...)Positive lookahead: Khớp nếu theo sau là mẫu ...
(?!...)Negative lookahead: Khớp nếu theo sau KHÔNG phải là mẫu ...
(?<=...)Positive lookbehind: Khớp nếu đứng trước là mẫu ...
(?<!...)Negative lookbehind: Khớp nếu đứng trước KHÔNG phải là mẫu ...

2. Tích Hợp Regex Trong Python Qua Module re

Python cung cấp module re tích hợp sẵn để làm việc với biểu thức chính quy. Các hàm trong module này có thể được gọi trực tiếp hoặc thông qua đối tượng Regex đã được biên dịch.

Hàm / Phương ThứcChức Năng
re.compile(pattern, flags)Biên dịch mẫu regex thành một đối tượng để tái sử dụng, tăng hiệu suất.
re.match(pattern, string)Chỉ kiểm tra sự khớp nhau bắt đầu từ vị trí đầu tiên của chuỗi.
re.search(pattern, string)Quét toàn bộ chuỗi để tìm vị trí khớp đầu tiên.
re.findall(pattern, string)Trả về một danh sách chứa tất cả các chuỗi khớp (không chồng lấp).
re.finditer(pattern, string)Trả về một iterator chứa các match object, tiết kiệm bộ nhớ.
re.split(pattern, string)Cắt chuỗi thành một danh sách dựa trên mẫu phân cách.
re.sub(pattern, repl, string)Thay thế tất cả các đoạn khớp bằng chuỗi repl.
re.subn(pattern, repl, string)Tương tự sub nhưng trả về tuple gồm chuỗi mới và số lần thay thế.

2.1. Các Cờ (Flags) Điều Chỉnh Hành Vi

  • re.IGNORECASE (re.I): Không phân biệt hoa thường.
  • re.MULTILINE (re.M): Ký tự ^ và $ khớp với đầu/cuối mỗi dòng thay vì toàn bộ chuỗi.
  • re.DOTALL (re.S): Ký tự . khớp với mọi ký tự, bao gồm cả dấu xuống dòng \n.
  • re.VERBOSE (re.X): Cho phép thêm khoảng trắng và comment vào mẫu regex để tăng tính dễ đọc.

3. Thực Hành Các Phương Thức Tìm Kiếm và Thao Tác Chuỗi

3.1. Biên Dịch Mẫu và Trích Xuất Nhóm

Sử dụng re.compile() giúp tối ưu hiệu suất khi một mẫu regex được sử dụng nhiều lần. Kết hợp với group() và groups() để lấy các đoạn chuỗi con.

import re

# Biên dịch mẫu URL để trích xuất domain và extension
url_regex = re.compile(r'https?://(?:www\.)?([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})')
match_obj = url_regex.match('https://my-domain.org')

if match_obj:
    print(match_obj.group(0))  # Toàn bộ chuỗi khớp: 'https://my-domain.org'
    print(match_obj.group(1))  # Nhóm 1: 'my-domain'
    print(match_obj.group(2))  # Nhóm 2: 'org'
    print(match_obj.groups())  # Tuple chứa tất cả nhóm: ('my-domain', 'org')

3.2. Phân Biệt match() và search()

match() chỉ kiểm tra sự khớp nhau bắt đầu từ vị trí đầu tiên của chuỗi, trong khi search() quét toàn bộ chuỗi để tìm vị trí khớp đầu tiên.

import re

text_data = "welcome to python programming"

# match() thất bại vì chuỗi không bắt đầu bằng 'python'
match_result = re.match(r'python', text_data)
print("match:", match_result)  # None

# search() thành công vì nó tìm kiếm trên toàn bộ chuỗi
search_result = re.search(r'python', text_data)
if search_result:
    print("search:", search_result.group())  # 'python'

3.3. Tìm Kiếm Nhiều Kết Quả với findall() và finditer()

findall() trả về một danh sách các chuỗi khớp, trong khi finditer() trả về một iterator chứa các match object, giúp tiết kiệm bộ nhớ khi xử lý dữ liệu lớn.

import re

log_text = "ERROR: Disk full. INFO: Retry. ERROR: Timeout. INFO: Success."

# findall trả về danh sách các chuỗi khớp
errors = re.findall(r'ERROR: (\w+)', log_text)
print(errors)  # ['Disk', 'Timeout']

# finditer trả về iterator chứa các match object
for match in re.finditer(r'INFO: (\w+)', log_text):
    print(f"Found info: {match.group(1)} at position {match.span()}")

3.4. Thay Thế Chuỗi và Tách Dữ Liệu

sub() và subn() dùng để thay thế các đoạn khớp, còn split() dùng để cắt chuỗi dựa trên mẫu phân cách.

import re

raw_text = "Liên hệ: 0901234567 hoặc 0987654321 để được hỗ trợ."

# Thay thế số điện thoại bằng chuỗi ẩn danh
masked_text = re.sub(r'\d{10}', '[PHONE_HIDDEN]', raw_text)
print(masked_text)

# subn trả về cả số lần thay thế
masked_text_v2, replace_count = re.subn(r'\d{10}', '[PHONE_HIDDEN]', raw_text)
print(f"Đã thay thế {replace_count} lần.")

# Tách chuỗi CSV có dấu phân cách không đồng nhất
csv_data = "apple, banana;orange|grape"
fruits = re.split(r'[,;|]\s*', csv_data)
print(fruits)  # ['apple', 'banana', 'orange', 'grape']

4. Xử Lý Dữ Liệu Phức Tạp: Lookaround và Tính Tham Lam

4.1. Trích Xuất Dữ Liệu Có Cấu Trúc với Named Groups

Khi phân tích log hoặc dữ liệu có cấu trúc, việc sử dụng named groups (?P<name>...) kết hợp với groupdict() giúp mã nguồn dễ đọc và bảo trì hơn.

import re

log_entries = [
    "2023-10-25 08:14:22 INFO [MainThread] User 'admin' logged in",
    "2023-10-25 08:15:01 ERROR [Worker-2] Failed to connect to DB",
]

# Mẫu regex sử dụng named groups
log_pattern = re.compile(
    r'^(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) '
    r'(?P<level>\w+) '
    r'\[(?P<thread>.+?)\] '
    r'(?P<message>.+)$'
)

for entry in log_entries:
    match = log_pattern.match(entry)
    if match:
        data = match.groupdict()
        print(f"[{data['level']}] Lúc {data['time']}: {data['message']}")

4.2. Ứng Dụng Lookahead và Lookbehind

Các cú pháp lookaround cho phép kiểm tra điều kiện xung quanh vị trí khớp mà không tiêu thụ ký tự trong chuỗi.

import re

data = "Guido van Rossum, Tim Peters, Alex Martelli, Just van Rossum"

# Positive lookahead: Tìm tên đệm/đầu nếu theo sau là " van Rossum"
names = re.findall(r'\w+(?= van Rossum)', data)
print(names)  # ['Guido', 'Just']

# Negative lookbehind: Tìm các từ không đứng ngay sau dấu phẩy và khoảng trắng
# (Lưu ý: Python yêu cầu lookbehind phải có độ dài cố định)
words = re.findall(r'(?<!, )\b\w+\b', "apple, banana, cherry")
print(words)

4.3. Xử Lý Tính Tham Lam (Greedy) và Lười (Lazy)

Theo mặc định, các toán tử lặp như *, +, và {} hoạt động theo cơ chế "tham lam" (greedy), tức là chúng sẽ khớp càng nhiều ký tự càng tốt. Để chuyển sang chế độ "lười" (lazy/non-greedy), ta thêm dấu ? vào sau toán tử.

import re

html_snippet = "<div class='content'>Hello <b>World</b></div>"

# Greedy: Khớp từ thẻ mở đầu tiên đến thẻ đóng cuối cùng
greedy_match = re.search(r'<.*>', html_snippet)
print("Greedy:", greedy_match.group()) 
# Output: <div class='content'>Hello <b>World</b></div>

# Lazy: Khớp thẻ ngắn nhất có thể
lazy_match = re.search(r'<.*?>', html_snippet)
print("Lazy:", lazy_match.group()) 
# Output: <div class='content'>

Thẻ: python regex regular-expressions re-module string-manipulation

Đăng vào ngày 14 tháng 10 lúc 08:08