Bài 3.25. Bài tập Python xử lý văn bản: thống kê từ

Chào mừng bạn đến với bài thực hành về xử lý văn bản bằng Python! Trong thế giới dữ liệu ngày nay, văn bản là một nguồn thông tin khổng lồ, từ các bài báo, mạng xã hội, email, đến mã nguồn. Khả năng xử lý và trích xuất thông tin có ý nghĩa từ văn bản là một kỹ năng vô cùng giá trị.

Một trong những tác vụ cơ bản nhất nhưng lại là nền tảng cho nhiều kỹ thuật phức tạp hơn (như phân tích tình cảm, mô hình chủ đề, v.v.) chính là thống kê tần suất xuất hiện của các từ (word count). Việc này giúp chúng ta hiểu được những từ nào được sử dụng thường xuyên nhất trong một tài liệu, qua đó có thể nắm bắt được chủ đề chính hoặc những điểm nhấn quan trọng.

Trong bài này, chúng ta sẽ cùng nhau xây dựng một chương trình Python để thực hiện công việc này. Chúng ta sẽ đi từ những bước đơn giản đến việc xử lý các vấn đề thường gặp như dấu câu và chữ hoa/thường.

Mục tiêu

Mục tiêu của chúng ta là viết một chương trình Python có khả năng:

  1. Nhận đầu vào là một chuỗi văn bản (hoặc đọc từ một file).
  2. Thực hiện các bước tiền xử lý cần thiết (làm sạch dữ liệu).
  3. Đếm số lần xuất hiện của mỗi từ duy nhất trong văn bản đó.
  4. Trình bày kết quả một cách rõ ràng.

Hãy bắt đầu nào!

Bước 1: Cách tiếp cận đơn giản ban đầu

Giả sử chúng ta có một đoạn văn bản đơn giản. Cách dễ nhất để tách các từ là sử dụng phương thức split() của chuỗi, phương thức này mặc định sẽ tách chuỗi dựa trên khoảng trắng.

# Ví dụ 1: Cách tiếp cận đơn giản ban đầu
van_ban = "Đây là một câu ví dụ. Ví dụ này đơn giản."

# Tách chuỗi thành danh sách các 'từ' dựa trên khoảng trắng
tu_ngu = van_ban.split()

print("Danh sách các 'từ' ban đầu:")
print(tu_ngu)

Chạy đoạn code trên, bạn sẽ nhận được kết quả:

Danh sách các 'từ' ban đầu:
['Đây', 'là', 'một', 'câu', 'ví', 'dụ.', 'Ví', 'dụ', 'này', 'đơn', 'giản.']

Phân tích: Ngay lập tức, chúng ta thấy một vài vấn đề:

  • Dấu câu: Các từ như "dụ.""giản." bị dính liền với dấu chấm. Nếu chúng ta đếm ngay bây giờ, "dụ." sẽ được coi là một từ khác với "dụ".
  • Chữ hoa/thường: Từ "Đây""Ví" được viết hoa ở đầu câu, trong khi "ví" ở giữa câu lại viết thường. Về mặt ngữ nghĩa, chúng có thể là cùng một từ, nhưng nếu đếm trực tiếp, chúng sẽ bị coi là hai từ khác nhau (case-sensitive).

Để có kết quả thống kê chính xác hơn, chúng ta cần tiền xử lý văn bản.

Bước 2: Tiền xử lý văn bản - Làm sạch dữ liệu

2.1 Chuẩn hóa chữ hoa/thường

Cách đơn giản nhất để giải quyết vấn đề chữ hoa/thường là chuyển toàn bộ văn bản về một dạng duy nhất, thường là chữ thường (lowercase). Chúng ta sử dụng phương thức lower() của chuỗi.

# Ví dụ 2: Chuẩn hóa về chữ thường
van_ban = "Đây là một câu ví dụ. Ví dụ này đơn giản."
van_ban_thuong = van_ban.lower()

print("Văn bản sau khi chuyển thành chữ thường:")
print(van_ban_thuong)
# Kết quả: đây là một câu ví dụ. ví dụ này đơn giản.

Bây giờ, tất cả các từ đều là chữ thường, giải quyết được vấn đề phân biệt hoa/thường.

2.2 Loại bỏ dấu câu

Dấu câu (., ,, !, ?, ;, :, v.v.) thường không mang nhiều ý nghĩa khi chúng ta chỉ muốn đếm từ. Chúng ta cần loại bỏ chúng. Có nhiều cách để làm điều này, một cách hiệu quả là sử dụng phương thức translate() kết hợp với str.maketrans() và hằng số string.punctuation.

Module string cung cấp một chuỗi chứa tất cả các dấu câu phổ biến: string.punctuation.

import string # Đừng quên import module string

# Ví dụ 3: Loại bỏ dấu câu
van_ban = "Đây là một câu ví dụ. Ví dụ này đơn giản, rất đơn giản!"

# 1. Chuyển thành chữ thường
van_ban_thuong = van_ban.lower()
print(f"Sau khi lower(): '{van_ban_thuong}'")

# 2. Tạo bảng dịch để loại bỏ dấu câu
# str.maketrans('', '', string.punctuation) tạo ra một bảng dịch
# thay thế mọi ký tự trong string.punctuation bằng None (tức là loại bỏ)
bang_dich = str.maketrans('', '', string.punctuation)
print(f"Các dấu câu cần loại bỏ: {string.punctuation}")

# 3. Áp dụng bảng dịch vào văn bản
van_ban_sach = van_ban_thuong.translate(bang_dich)
print(f"Sau khi loại bỏ dấu câu: '{van_ban_sach}'")

# 4. Bây giờ mới tách thành từ
tu_ngu_sach = van_ban_sach.split()
print("\nDanh sách các từ đã được làm sạch:")
print(tu_ngu_sach)

Kết quả sẽ là:

Sau khi lower(): 'đây là một câu ví dụ. ví dụ này đơn giản, rất đơn giản!'
Các dấu câu cần loại bỏ: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
Sau khi loại bỏ dấu câu: 'đây là một câu ví dụ ví dụ này đơn giản rất đơn giản'

Danh sách các từ đã được làm sạch:
['đây', 'là', 'một', 'câu', 'ví', 'dụ', 'ví', 'dụ', 'này', 'đơn', 'giản', 'rất', 'đơn', 'giản']

Giải thích:

  • string.punctuation chứa các ký tự dấu câu !"#$%&'()*+,-./:;<=>?@[\]^_{|}~`.
  • str.maketrans('', '', string.punctuation) tạo ra một "bản đồ" ánh xạ, yêu cầu xóa bỏ tất cả các ký tự có trong string.punctuation.
  • van_ban_thuong.translate(bang_dich) áp dụng bản đồ này lên chuỗi, trả về một chuỗi mới đã loại bỏ các dấu câu.
  • Cuối cùng, split() trên chuỗi sạch sẽ này cho chúng ta danh sách các từ mong muốn.

Lưu ý: Phương pháp này loại bỏ tất cả các ký tự trong string.punctuation. Trong một số trường hợp đặc biệt (ví dụ: muốn giữ lại dấu gạch ngang trong từ ghép như "state-of-the-art"), bạn có thể cần tùy chỉnh lại chuỗi ký tự cần loại bỏ.

Bước 3: Đếm tần suất từ

Sau khi có danh sách các từ đã được làm sạch (tu_ngu_sach), chúng ta cần đếm xem mỗi từ xuất hiện bao nhiêu lần. Có hai cách phổ biến:

3.1 Sử dụng Dictionary thông thường

Chúng ta có thể dùng một dictionary để lưu trữ kết quả. Key của dictionary là từ, và value là số lần xuất hiện của từ đó.

# (Tiếp tục từ danh sách tu_ngu_sach ở trên)
tu_ngu_sach = ['đây', 'là', 'một', 'câu', 'ví', 'dụ', 'ví', 'dụ', 'này', 'đơn', 'giản', 'rất', 'đơn', 'giản']

# Ví dụ 4: Đếm từ sử dụng Dictionary
dem_tu_dict = {} # Khởi tạo dictionary rỗng

for tu in tu_ngu_sach:
    if tu in dem_tu_dict:
        # Nếu từ đã có trong dict, tăng bộ đếm lên 1
        dem_tu_dict[tu] += 1
    else:
        # Nếu từ chưa có, thêm vào dict với bộ đếm là 1
        dem_tu_dict[tu] = 1

print("\nKết quả đếm từ (sử dụng Dictionary):")
print(dem_tu_dict)

Kết quả:

Kết quả đếm từ (sử dụng Dictionary):
{'đây': 1, 'là': 1, 'một': 1, 'câu': 1, 'ví': 2, 'dụ': 2, 'này': 1, 'đơn': 2, 'giản': 2, 'rất': 1}

Cách này hoạt động tốt và dễ hiểu. Tuy nhiên, Python cung cấp một công cụ chuyên dụng và hiệu quả hơn cho việc này: collections.Counter.

3.2 Sử dụng collections.Counter - Cách Pythonic

collections.Counter là một lớp con của dict, được thiết kế đặc biệt để đếm các đối tượng hashable (như chuỗi). Nó làm cho việc đếm trở nên cực kỳ đơn giản.

from collections import Counter # Import Counter từ module collections

# (Tiếp tục từ danh sách tu_ngu_sach ở trên)
tu_ngu_sach = ['đây', 'là', 'một', 'câu', 'ví', 'dụ', 'ví', 'dụ', 'này', 'đơn', 'giản', 'rất', 'đơn', 'giản']

# Ví dụ 5: Đếm từ sử dụng collections.Counter
dem_tu_counter = Counter(tu_ngu_sach)

print("\nKết quả đếm từ (sử dụng collections.Counter):")
print(dem_tu_counter)

Kết quả:

Kết quả đếm từ (sử dụng collections.Counter):
Counter({'ví': 2, 'dụ': 2, 'đơn': 2, 'giản': 2, 'đây': 1, 'là': 1, 'một': 1, 'câu': 1, 'này': 1, 'rất': 1})

Ưu điểm của Counter:

  • Ngắn gọn: Chỉ cần một dòng để thực hiện việc đếm.
  • Tiện lợi: Cung cấp các phương thức hữu ích như most_common(n) để lấy n phần tử phổ biến nhất.
# Ví dụ 6: Lấy 3 từ phổ biến nhất
top_3 = dem_tu_counter.most_common(3)
print("\n3 từ phổ biến nhất:")
print(top_3)
# Kết quả có thể là: [('ví', 2), ('dụ', 2), ('đơn', 2)] (Thứ tự các từ có cùng số đếm có thể thay đổi)

Bước 4: Tổng hợp thành hàm hoàn chỉnh và đọc từ file

Bây giờ, hãy kết hợp tất cả các bước lại thành một hàm có thể đọc nội dung từ một file văn bản và trả về kết quả đếm từ.

import string
from collections import Counter

def dem_tu_trong_file(ten_file, encoding='utf-8'):
    """
    Đọc nội dung từ file văn bản, tiền xử lý và đếm tần suất từ.

    Args:
        ten_file (str): Đường dẫn đến file văn bản.
        encoding (str, optional): Bảng mã của file. Mặc định là 'utf-8'.

    Returns:
        collections.Counter or None: Đối tượng Counter chứa kết quả đếm,
                                     hoặc None nếu có lỗi đọc file.
    """
    try:
        # Sử dụng 'with open' đảm bảo file được đóng đúng cách
        with open(ten_file, 'r', encoding=encoding) as f:
            van_ban = f.read()
            print(f"--- Đã đọc thành công file '{ten_file}' ---")
    except FileNotFoundError:
        print(f"*** Lỗi: Không tìm thấy file '{ten_file}' ***")
        return None
    except Exception as e:
        print(f"*** Lỗi không xác định khi đọc file: {e} ***")
        return None

    # 1. Chuẩn hóa chữ thường
    van_ban_thuong = van_ban.lower()

    # 2. Loại bỏ dấu câu
    bang_dich = str.maketrans('', '', string.punctuation)
    van_ban_sach = van_ban_thuong.translate(bang_dich)

    # 3. Tách thành từ (split sẽ tự động xử lý các khoảng trắng thừa)
    tu_ngu = van_ban_sach.split()

    # 4. Kiểm tra xem có từ nào không sau khi xử lý
    if not tu_ngu:
        print("--- File rỗng hoặc chỉ chứa dấu câu/khoảng trắng sau khi xử lý. ---")
        return Counter() # Trả về Counter rỗng

    # 5. Đếm từ sử dụng Counter
    dem_tu_counter = Counter(tu_ngu)

    return dem_tu_counter

# ---- Sử dụng hàm ----

# Giả sử bạn có một file tên là 'sample.txt' với nội dung sau:
# """
# This is the first line.
# This line is the second line.
# The third one is a bit different, line!
# """

# --- !!! Quan trọng: Tạo file sample.txt trước khi chạy !!! ---
file_content = """This is the first line.
This line is the second line.
The third one is a bit different, line!
"""
file_name = 'sample.txt'
try:
    with open(file_name, 'w', encoding='utf-8') as f_out:
        f_out.write(file_content)
    print(f"--- Đã tạo file mẫu '{file_name}' ---")
except IOError as e:
    print(f"*** Không thể tạo file mẫu '{file_name}': {e} ***")
# ---------------------------------------------------------------


# Gọi hàm để đếm từ
ket_qua = dem_tu_trong_file(file_name)

if ket_qua is not None:
    print("\n--- Kết quả đếm từ ---")
    # In ra 5 từ phổ biến nhất
    print("5 từ phổ biến nhất:")
    for tu, dem in ket_qua.most_common(5):
        print(f"- '{tu}': {dem} lần")

    # Bạn cũng có thể truy cập số đếm của một từ cụ thể
    # print(f"\nSố lần xuất hiện của từ 'line': {ket_qua['line']}")

    # In toàn bộ kết quả nếu muốn (có thể rất dài với file lớn)
    # print("\nToàn bộ kết quả:")
    # print(ket_qua)

# Thử với file không tồn tại
print("\n--- Thử đọc file không tồn tại ---")
ket_qua_loi = dem_tu_trong_file('non_existent_file.txt')
# Hàm sẽ in ra lỗi và trả về None

Giải thích Code:

  • def dem_tu_trong_file(...): Định nghĩa hàm nhận tên file và encoding.
  • try...except: Bắt các lỗi có thể xảy ra khi đọc file (không tìm thấy file, lỗi quyền truy cập, lỗi encoding...). FileNotFoundError là lỗi phổ biến nhất.
  • with open(...) as f:: Đây là cách tốt nhất để làm việc với file trong Python. Nó đảm bảo file sẽ được đóng lại tự động ngay cả khi có lỗi xảy ra. 'r' nghĩa là đọc file (read mode). encoding='utf-8' rất quan trọng để xử lý đúng các ký tự tiếng Việt hoặc các ngôn ngữ khác.
  • f.read(): Đọc toàn bộ nội dung của file vào biến van_ban. Đối với các file rất lớn, bạn có thể cần đọc từng dòng để tránh hết bộ nhớ.
  • Các bước tiền xử lý (lower, translate, split) được áp dụng như đã giải thích.
  • if not tu_ngu:: Kiểm tra trường hợp file trống hoặc chỉ chứa các ký tự đã bị loại bỏ.
  • Counter(tu_ngu): Tạo đối tượng Counter trực tiếp từ danh sách từ.
  • Phần sử dụng hàm cho thấy cách gọi hàm, cách tạo file mẫu để ví dụ chạy được, và cách xử lý kết quả trả về (bao gồm cả trường hợp lỗi None).

Mở rộng và Lưu ý

  • Stop Words: Trong nhiều ứng dụng thực tế (như tìm kiếm, phân tích chủ đề), người ta thường loại bỏ các stop words - những từ xuất hiện rất phổ biến nhưng ít mang ý nghĩa riêng biệt (ví dụ: "là", "thì", "mà", "và", "the", "a", "is" trong tiếng Anh). Bạn có thể tạo một danh sách các stop words và lọc chúng ra khỏi tu_ngu trước khi đếm.
  • Stemming và Lemmatization: Để nhóm các dạng khác nhau của cùng một từ (ví dụ: "run", "running", "ran" -> "run"), người ta sử dụng các kỹ thuật như stemming (cắt bỏ hậu tố) hoặc lemmatization (đưa về dạng gốc dựa trên từ điển). Đây là các bước nâng cao hơn trong xử lý ngôn ngữ tự nhiên (NLP).
  • Xử lý các trường hợp đặc biệt: Văn bản thực tế có thể chứa số, từ ghép, tên riêng, URL, email, v.v. Tùy thuộc vào yêu cầu cụ thể, bạn có thể cần các quy tắc tiền xử lý phức tạp hơn để xử lý những trường hợp này.
  • Hiệu năng với file lớn: Với các file văn bản cực lớn (gigabytes), đọc toàn bộ file vào bộ nhớ (f.read()) có thể không khả thi. Bạn nên đọc file theo từng dòng hoặc từng khối dữ liệu nhỏ và cập nhật Counter dần dần.

Qua bài tập này, bạn đã học được cách thực hiện một tác vụ xử lý văn bản cơ bản nhưng quan trọng: thống kê tần suất từ. Bạn đã thấy tầm quan trọng của việc tiền xử lý dữ liệu và cách sử dụng các công cụ hiệu quả của Python như string.punctuation, str.translate, và đặc biệt là collections.Counter. Đây là bước đệm vững chắc để bạn tiếp tục khám phá các lĩnh vực thú vị khác của xử lý văn bản và khoa học dữ liệu!

Comments

There are no comments at the moment.