Xử lý chuỗi và văn bản tiếng Việt trong Python

Xử lý văn bản là việc gần như chương trình nào cũng phải làm. Python có bộ công cụ chuỗi rất phong phú — bài này đi qua những thứ dùng nhiều nhất.

f-string: cách định dạng nên dùng

ten = "Lan"
diem = 8.567

print(f"{ten} được {diem} điểm")
print(f"{ten} được {diem:.1f} điểm")      # làm tròn 1 chữ số
print(f"{diem:>10.2f}")                    # căn phải, rộng 10
print(f"{1234567:,}")                      # 1,234,567
print(f"{0.856:.1%}")                      # 85.6%

Từ Python 3.8, thêm dấu = để in cả tên biến — rất tiện khi gỡ lỗi:

print(f"{diem=}")        # diem=8.567
print(f"{diem*2=:.2f}")  # diem*2=17.13

Căn chỉnh khi in bảng

hang_hoa = [("Bàn phím", 450_000), ("Chuột", 250_000), ("Màn hình", 3_200_000)]

for ten, gia in hang_hoa:
    print(f"{ten:<15} {gia:>12,}đ")
Bàn phím             450,000đ
Chuột                250,000đ
Màn hình           3,200,000đ

Các thao tác thường dùng

s = "  Xin chào Việt Nam  "

s.strip()          # bỏ khoảng trắng hai đầu
s.lstrip()         # bỏ bên trái
s.lower()          # chữ thường
s.upper()          # CHỮ HOA
s.title()          # Viết Hoa Đầu Từ
s.replace("chào", "gửi")
s.split()          # ['Xin', 'chào', 'Việt', 'Nam']
"-".join(["a", "b", "c"])   # 'a-b-c'

Kiểm tra nội dung

ten_file = "bao_cao.pdf"

ten_file.endswith(".pdf")           # True
ten_file.startswith("bao")          # True
"cao" in ten_file                   # True
ten_file.count("a")                 # 3
ten_file.find("cao")                # 4, trả -1 nếu không thấy

Muốn kiểm tra nhiều đuôi file cùng lúc, truyền tuple:

if ten_file.endswith((".jpg", ".png", ".webp")):
    print("Đây là ảnh")

Chuỗi là bất biến

Mọi thao tác đều tạo chuỗi mới, không sửa chuỗi cũ:

s = "python"
s.upper()          # trả về 'PYTHON'
print(s)           # vẫn là 'python'

s = s.upper()      # phải gán lại

Điều này có hệ quả về hiệu năng. Nối chuỗi trong vòng lặp rất chậm vì mỗi lần đều tạo chuỗi mới:

# Chậm với dữ liệu lớn
ket_qua = ""
for dong in danh_sach:
    ket_qua += dong + "\n"

# Nhanh hơn nhiều
ket_qua = "\n".join(danh_sach)

Xử lý tiếng Việt

Python 3 làm việc với Unicode nên tiếng Việt hoạt động bình thường. Nhưng có vài điểm cần biết.

Chuẩn hóa dạng Unicode

Chữ “ế” có thể được lưu bằng một ký tự, hoặc bằng “e” cộng hai dấu rời. Nhìn giống hệt nhau nhưng máy coi là khác:

import unicodedata

a = "tiếng"                 # dạng dựng sẵn
b = "tiếng"                 # dạng tổ hợp (nhìn y hệt)

print(a == b)                                    # có thể False
print(unicodedata.normalize("NFC", a) ==
      unicodedata.normalize("NFC", b))           # True

Khi so sánh hoặc lưu văn bản tiếng Việt từ nhiều nguồn, hãy chuẩn hóa về NFC trước. Đây là nguyên nhân của nhiều lỗi “tìm không ra dù nhìn giống nhau”.

Bỏ dấu tiếng Việt

Hữu ích khi tạo đường dẫn thân thiện hoặc tìm kiếm không dấu:

import unicodedata, re

def bo_dau(s):
    s = unicodedata.normalize("NFD", s)
    s = "".join(c for c in s if unicodedata.category(c) != "Mn")
    return s.replace("đ", "d").replace("Đ", "D")

def tao_slug(s):
    s = bo_dau(s).lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    return s.strip("-")

print(tao_slug("Lập trình Python cơ bản"))
# lap-trinh-python-co-ban

Chú ý phải xử lý riêng chữ “đ” — nó không phải là “d” kèm dấu nên bước chuẩn hóa không tách ra được.

Biểu thức chính quy

Khi thao tác chuỗi thông thường không đủ:

import re

van_ban = "Liên hệ: an@example.com hoặc binh@company.vn"

emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", van_ban)
print(emails)

# Thay thế
sach = re.sub(r"\d{4,}", "[đã ẩn]", "Số thẻ 1234567890")

# Kiểm tra định dạng
if re.fullmatch(r"0\d{9}", so_dien_thoai):
    print("Số điện thoại hợp lệ")

Khi dùng cùng một mẫu nhiều lần, hãy biên dịch trước:

MAU_EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")

for dong in hang_trieu_dong:
    if MAU_EMAIL.search(dong):
        ...

Lời khuyên cuối: đừng dùng biểu thức chính quy để phân tích HTML hay JSON. Có thư viện chuyên dụng cho việc đó, và chúng xử lý đúng những trường hợp mà regex sẽ sai một cách âm thầm.