Bắt Đầu Với Regex Trong 5 Phút
Regex — viết tắt của regular expressions — là cách mô tả các mẫu (pattern) trong văn bản. Dù chỉ hiểu ở mức cơ bản, bạn đã có thể tìm kiếm, kiểm tra và biến đổi chuỗi theo những cách mà nếu làm thủ công có thể tốn đến 20 dòng code.
Tôi còn nhớ lần đầu phải trích xuất tất cả địa chỉ email từ một file log lộn xộn. Tôi mất hai tiếng đồng hồ viết logic xử lý chuỗi trước khi một senior dev chỉ cho tôi một regex duy nhất giải quyết xong trong một dòng. Khoảnh khắc đó thay đổi hoàn toàn cách tôi nghĩ về xử lý văn bản. Regex là một trong những kỹ năng hiếm hoi mà chỉ cần luyện tập một cuối tuần là bạn hái quả suốt cả sự nghiệp.
Hãy bắt đầu với ví dụ đơn giản nhất có thể. Nếu bạn mới làm quen với Python, hãy xem thêm những kiến thức Python cơ bản dành cho quản trị viên hệ thống trước khi tiếp tục. Mở Python và thử đoạn code này:
import re
text = "Contact us at [email protected] or [email protected]"
matches = re.findall(r'\w+@\w+\.\w+', text)
print(matches)
# ['[email protected]', '[email protected]']
Chuỗi \w+@\w+\.\w+ đó chính là pattern regex đầu tiên của bạn. Nó tìm ra hai địa chỉ email mà không cần một vòng lặp nào. Đó chính là sức mạnh của regex.
Đào Sâu: Các Thành Phần Cơ Bản
Regex được xây dựng từ một tập hợp nhỏ các ký hiệu cốt lõi. Nắm vững khoảng 15 cấu trúc này là bạn có thể viết được hầu hết mọi pattern gặp phải trong công việc thực tế.
Character Classes (Lớp Ký Tự)
\w— bất kỳ ký tự từ (chữ cái, chữ số, dấu gạch dưới)\d— bất kỳ chữ số nào (0–9)\s— bất kỳ khoảng trắng nào (space, tab, xuống dòng)\W,\D,\S— ngược lại với các ký hiệu trên (viết hoa = phủ định)[abc]— một trong các ký tự: a, b hoặc c[^abc]— bất kỳ ký tự nào ngoại trừ a, b, c[a-z]— bất kỳ chữ cái thường nào
Quantifiers (Bộ Định Lượng)
+— một hoặc nhiều lần*— không hoặc nhiều lần?— không hoặc một lần (tùy chọn){3}— đúng 3 lần{2,5}— từ 2 đến 5 lần
Anchors và Boundaries (Mỏ Neo và Ranh Giới)
^— đầu chuỗi$— cuối chuỗi\b— ranh giới từ
Kết hợp chúng trong Python như sau:
import re
# Khớp ngày tháng theo định dạng YYYY-MM-DD
pattern = r'\d{4}-\d{2}-\d{2}'
text = "Deployment date: 2025-03-15, rollback date: 2025-03-16"
dates = re.findall(pattern, text)
print(dates)
# ['2025-03-15', '2025-03-16']
# Chỉ khớp từ "error" đứng độc lập (không phải "errors" hay "error_code")
pattern = r'\berror\b'
log = "found 3 errors but only one error was critical"
print(re.findall(pattern, log))
# ['error']
Groups và Capturing (Nhóm và Bắt Giữ)
Dấu ngoặc tròn () tạo ra capture group — cho phép bạn trích xuất các phần cụ thể từ kết quả khớp thay vì lấy toàn bộ chuỗi.
import re
log_line = "2025-03-15 14:23:01 ERROR Database connection failed"
pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
match = re.match(pattern, log_line)
if match:
date, time, level, message = match.groups()
print(f"Ngày: {date}, Mức độ: {level}")
print(f"Thông báo: {message}")
# Ngày: 2025-03-15, Mức độ: ERROR
# Thông báo: Database connection failed
Sử Dụng Nâng Cao
Non-capturing Groups và Alternation
Đôi khi bạn cần nhóm cho logic xử lý nhưng không muốn lưu kết quả. Đó chính là mục đích của (?:...) — nhóm mà không làm ô nhiễm kết quả trả về.
import re
# Khớp "color" hoặc "colour" (chính tả Mỹ/Anh)
pattern = r'colo(?:u)?r'
text = "The color and colour are both valid spellings"
print(re.findall(pattern, text))
# ['color', 'colour']
# Alternation với |
pattern = r'\b(cat|dog|bird)\b'
text = "I have a cat and a dog, but no bird"
print(re.findall(pattern, text))
# ['cat', 'dog', 'bird']
Lookahead và Lookbehind
Zero-width assertion là một trong những kỹ thuật mạnh mẽ nhất của regex. Chúng kiểm tra ngữ cảnh xung quanh một vị trí mà không tiêu thụ ký tự nào trong kết quả khớp.
import re
# Lookahead: chỉ khớp số tiền khi theo sau là " USD"
pattern = r'\$\d+(?:\.\d{2})?(?= USD)'
text = "Total: $49.99 USD, Tax: $3.50 USD"
print(re.findall(pattern, text))
# ['$49.99', '$3.50']
# Lookbehind: chỉ khớp số phiên bản khi đứng sau "version "
pattern = r'(?<=version )\d+\.\d+'
text = "Running version 3.11, upgrading to version 3.12"
print(re.findall(pattern, text))
# ['3.11', '3.12']
Dùng Regex Trong Bash
Regex không chỉ dùng trong Python. Trong shell scripting, grep mặc định dùng basic regex. Thêm cờ -E (hoặc dùng egrep) để chuyển sang extended regex — khi đó bạn có thể dùng +, ? và |. Nếu bạn thường xuyên làm việc với log và shell, các công cụ CLI tích hợp AI cho quy trình DevOps & SysAdmin có thể giúp bạn tăng tốc đáng kể.
# Tìm tất cả các dòng chứa địa chỉ IP trong file log
grep -E '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' /var/log/nginx/access.log
# Đếm các HTTP status code (200, 404, 500...)
grep -oE ' [0-9]{3} ' access.log | sort | uniq -c
# Tìm các dòng KHÔNG chứa "GET"
grep -v 'GET' access.log
# Tìm không phân biệt hoa/thường cho các mức độ nghiêm trọng
grep -iE 'error|warning|critical' app.log
Regex Để Thay Thế Chuỗi
Tìm và thay thế có lẽ là trường hợp sử dụng phổ biến nhất trong thực tế. Đây là ví dụ thực hành: chuẩn hóa số điện thoại đến dưới ba định dạng khác nhau.
import re
# Chuẩn hóa tất cả các dạng số điện thoại về định dạng XXX-XXX-XXXX
def normalize_phone(text):
return re.sub(
r'\(?([0-9]{3})\)?[.\s-]?([0-9]{3})[.\s-]?([0-9]{4})',
r'\1-\2-\3',
text
)
print(normalize_phone("Call (123) 456-7890 or 123.456.7890 or 123-456-7890"))
# Call 123-456-7890 or 123-456-7890 or 123-456-7890
Mẹo Thực Tế Để Viết Regex Tốt Hơn
Mẹo 1: Dùng Raw String Trong Python
Luôn thêm tiền tố r trước pattern. Nếu không, Python sẽ xử lý dấu backslash trước khi regex nhìn thấy chúng — \n thành ký tự xuống dòng, \b thành ký tự backspace. Raw string ngăn điều đó xảy ra.
# Sai — \b bị hiểu là backspace (ASCII 8)
pattern = "\bword\b"
# Đúng — raw string giữ nguyên dấu backslash
pattern = r'\bword\b'
Mẹo 2: Hãy Cụ Thể, Đừng Tham Lam
Mặc định, .* là greedy (tham lam) — nó khớp càng nhiều văn bản càng tốt. Thêm ? để chuyển sang chế độ lazy (lười biếng), dừng lại ở kết quả khớp sớm nhất có thể.
import re
html = "<b>bold</b> and <b>more bold</b>"
# Greedy — khớp từ <b> đầu tiên đến </b> cuối cùng
print(re.findall(r'<b>.*</b>', html))
# ['<b>bold</b> and <b>more bold</b>']
# Lazy — dừng ở thẻ đóng đầu tiên gặp
print(re.findall(r'<b>.*?</b>', html))
# ['<b>bold</b>', '<b>more bold</b>']
Mẹo 3: Compile Pattern Dùng Nhiều Lần
Chạy cùng một pattern trong vòng lặp chặt chẽ? Hãy compile nó trước. Qua các benchmark với 100.000 vòng lặp, pattern đã compile chạy nhanh hơn khoảng 2–3 lần so với việc truyền chuỗi thô vào re.findall() mỗi lần.
import re
# Compile một lần
email_pattern = re.compile(r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}')
# Tái sử dụng nhiều lần
for line in open('emails.txt'):
matches = email_pattern.findall(line)
if matches:
print(matches)
Mẹo 4: Test Pattern Theo Cách Tương Tác
Hãy dán pattern của bạn vào regex101.com trước khi đưa vào code. Công cụ này tô sáng chính xác ký tự nào mỗi group khớp, giải thích từng token bằng ngôn ngữ dễ hiểu, và cảnh báo các lỗi thường gặp. Bên cạnh đó, hãy xem qua bộ công cụ thiết yếu cho nhà phát triển như JSON Formatter, Regex Tester và Base64 Encoder — rất hữu ích để kiểm tra nhanh trong quá trình phát triển. Tôi đã debug được pattern trong 30 giây ở đó trong khi nếu làm trong Python REPL có thể mất đến 20 phút.
Mẹo 5: Biết Khi Nào KHÔNG Nên Dùng Regex
Regex là công cụ sai khi xử lý các định dạng có cấu trúc phức tạp. Parse HTML? Dùng BeautifulSoup hoặc lxml. Validate JSON? Dùng schema validator như jsonschema. Nguyên tắc chung: nếu regex của bạn cần hơn hai cấp lồng nhau hoặc logic có trạng thái, hãy dùng một parser thực thụ thay thế.
Dự Án Nhỏ Trong Thực Tế
Đây là script phân tích access log của nginx và tổng hợp số lượng request theo HTTP status code — đúng kiểu việc bạn sẽ viết thực sự trong công việc:
import re
from collections import Counter
log_pattern = re.compile(
r'(?P<ip>[\d.]+) .+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" '
r'(?P<status>\d{3}) (?P<size>\d+)'
)
status_counts = Counter()
with open('/var/log/nginx/access.log') as f:
for line in f:
match = log_pattern.match(line)
if match:
status_counts[match.group('status')] += 1
for status, count in sorted(status_counts.items()):
print(f"HTTP {status}: {count} requests")
Named group ((?P<name>...)) giúp code tự giải thích ý nghĩa của nó. Bạn đọc match.group('status') là biết ngay nó chứa gì — không cần đếm dấu ngoặc để biết mình đang ở group số mấy. Nếu bạn cần phân tích log nginx ở quy mô lớn hơn, hãy xem xét ghi nhật ký tập trung với Grafana Loki để giám sát toàn diện hơn.
Regex có vẻ khó hiểu trong vài giờ đầu. Rồi bỗng dưng có gì đó bật sáng trong đầu. Bạn bắt đầu nhận ra các mẫu trong dữ liệu mà trước đây bạn phải xử lý thủ công bằng vòng lặp — và code của bạn ngắn đi một nửa.

