CodeGym /Các khóa học /JAVA 25 SELF /Xử lý tệp bị hỏng, khôi phục dữ liệu

Xử lý tệp bị hỏng, khôi phục dữ liệu

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Dấu hiệu của tệp bị hỏng

Trong một thế giới lý tưởng, tệp luôn được đọc và ghi không lỗi, và dữ liệu trong đó — như những chiếc bánh mới nướng: mềm, thơm, đều, nguyên vẹn. Nhưng trong thực tế, tệp có thể “bị hỏng”, “lệch”, “không đầy đủ” hoặc “sai định dạng”. Nguyên nhân có thể rất đa dạng. Ví dụ: lỗi khi ghi ra đĩa (do mất điện đột ngột), lỗi mạng khi truyền tệp, hỏng phương tiện lưu trữ (bad sector). Hoặc việc chỉnh sửa thủ công tệp bằng chương trình không phù hợp có thể làm hỏng tệp, cũng như việc định dạng dữ liệu mong đợi và thực tế không trùng khớp.

Trong Java, những tình huống như vậy thường biểu hiện qua ngoại lệ khi đọc/ghi, và đôi khi — qua hành vi lạ của chương trình (ví dụ, dữ liệu đột ngột kết thúc hoặc xuất hiện ký tự rác).

Ngoại lệ khi đọc

Dấu hiệu rõ ràng nhất — các ngoại lệ bất ngờ. Một số ngoại lệ thường gặp:

  • EOFException — kết thúc tệp bất ngờ (End Of File). Bạn kỳ vọng trong tệp còn dữ liệu, nhưng thực tế không có.
  • MalformedInputException (hoặc, trong API cũ, MalformedInputException từ NIO) — tệp không khớp với mã hóa hoặc cấu trúc mong đợi.
  • ZipException — nếu bạn cố đọc một tệp nén như tệp thường.
  • StreamCorruptedException — khi đọc các đối tượng đã tuần tự hóa nếu tệp bị hỏng.

Không khớp định dạng dữ liệu

Đôi khi tệp được đọc mà không phát sinh ngoại lệ, nhưng nội dung không khớp với định dạng mong đợi:

  • Mong đợi một chuỗi, nhưng nhận được một tập ký tự khó hiểu.
  • Mong đợi một số lượng số nhất định, nhưng lại ít hơn.
  • Mong tệp ở định dạng CSV, nhưng thực tế là JSON (hoặc ngược lại).

Ví dụ thực tế

Giả sử bạn viết một ứng dụng lưu danh sách công việc trong tệp văn bản. Chương trình mong đợi mỗi dòng — là một công việc. Nhưng người dùng mở tệp bằng Excel, sửa đổi, lưu ở định dạng khác... và giờ chương trình của bạn không thể đọc tệp được nữa.

2. Chiến lược xử lý tệp bị hỏng

Ghi log và thông báo cho người dùng

Quy tắc đầu tiên: đừng hoảng! (và cũng đừng để người dùng hoảng). Luôn ghi log lỗi và thông báo cho người dùng khi có sự cố. Nhưng không nhất thiết phải mô tả cho họ mọi thứ khủng khiếp của Java stack trace.

try {
    // đọc tệp
} catch (EOFException e) {
    System.err.println("Tệp kết thúc đột ngột. Có thể nó đã bị hỏng.");
    // ghi log chi tiết
    e.printStackTrace();
}

Thử khôi phục một phần

Đôi khi có thể “cứu” được ít nhất một phần dữ liệu. Ví dụ, nếu tệp được đọc theo từng dòng, có thể xử lý tất cả các dòng cho đến lỗi đầu tiên.

Sử dụng bản sao lưu (backup)

Những chương trình “nghiêm túc” thường tạo bản sao lưu các tệp quan trọng trước khi ghi. Nếu tệp chính bị hỏng, có thể thử khôi phục dữ liệu từ bản sao lưu.

3. Thực hành: Đọc tệp kết thúc bất ngờ (EOF)

Tình huống kinh điển

Giả sử chúng ta có một tệp nhị phân trong đó các số kiểu int được ghi nối tiếp nhau. Chương trình mong đợi có đúng 5 số, nhưng tệp bị hỏng và chỉ ghi 3 số.

import java.io.*;

public class DamagedFileExample {
    public static void main(String[] args) {
        String filename = "numbers.bin";

        // Ví dụ: tạo tệp với 3 số (thay vì 5)
        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(filename))) {
            out.writeInt(42);
            out.writeInt(7);
            out.writeInt(2024);
            // out.writeInt(1); out.writeInt(2); // cố tình không ghi!
        } catch (IOException e) {
            System.err.println("Lỗi khi tạo tệp: " + e.getMessage());
        }

        // Bây giờ thử đọc 5 số
        try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
            for (int i = 0; i < 5; i++) {
                int number = in.readInt();
                System.out.println("Đã đọc số: " + number);
            }
        } catch (EOFException e) {
            System.err.println("Tệp kết thúc đột ngột! Có thể nó đã bị hỏng.");
        } catch (IOException e) {
            System.err.println("Lỗi đọc: " + e.getMessage());
        }
    }
}

Kết quả:

Đã đọc số: 42
Đã đọc số: 7
Đã đọc số: 2024
Tệp kết thúc đột ngột! Có thể nó đã bị hỏng.

Đọc cho đến lỗi đầu tiên

Thường hợp lý là đọc dữ liệu trong vòng lặp cho đến khi phát sinh ngoại lệ. Như vậy có thể lấy được ít nhất một phần thông tin.

try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
    while (true) {
        try {
            int number = in.readInt();
            System.out.println("Đã đọc số: " + number);
        } catch (EOFException e) {
            System.out.println("Dữ liệu đã hết (hoặc tệp bị hỏng).");
            break;
        }
    }
} catch (IOException e) {
    System.err.println("Lỗi đọc: " + e.getMessage());
}

4. Làm việc với tệp văn bản và mã hóa

Vấn đề về mã hóa

Nếu tệp được ghi bằng một mã hóa, còn đọc bằng mã hóa khác, có thể xảy ra lỗi giải mã:

import java.nio.charset.*;

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(new FileInputStream("tasks.txt"), "UTF-8"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (MalformedInputException e) {
    System.err.println("Lỗi mã hóa! Tệp bị hỏng hoặc không được ghi bằng UTF-8.");
} catch (IOException e) {
    System.err.println("Lỗi đọc: " + e.getMessage());
}

Quan trọng: Đôi khi thay vì ngoại lệ, bạn sẽ nhận được “ký tự rác” — đây cũng là dấu hiệu của tệp bị hỏng hoặc mã hóa không đúng.

Xử lý thế nào?

  • Thông báo cho người dùng về vấn đề.
  • Thử mở tệp bằng một mã hóa khác.
  • Nếu dữ liệu quan trọng — đề nghị khôi phục từ bản sao lưu.

5. Khôi phục dữ liệu: chiến lược

Đọc phần dữ liệu còn hợp lệ

Nếu cấu trúc tệp cho phép, có thể “trích” ít nhất những dữ liệu đọc được cho đến khi gặp lỗi. Ví dụ, nếu tệp là danh sách dòng (mỗi công việc — một dòng), có thể xử lý tất cả các dòng cho đến khi xảy ra sự cố.

try (BufferedReader reader = new BufferedReader(new FileReader("tasks.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // xử lý dòng
    }
} catch (IOException e) {
    System.err.println("Lỗi đọc: " + e.getMessage());
    // Có thể lưu lại dữ liệu đã đọc hoặc đề nghị người dùng khôi phục
}

Sử dụng tệp sao lưu (backup)

Nếu bạn tạo sẵn một bản sao của tệp (ví dụ, tasks.txt.bak), có thể khôi phục dữ liệu từ đó:

File original = new File("tasks.txt");
File backup = new File("tasks.txt.bak");

if (!original.exists() && backup.exists()) {
    // Sao chép bản sao lưu vào vị trí của bản gốc
    Files.copy(backup.toPath(), original.toPath(), StandardCopyOption.REPLACE_EXISTING);
    System.out.println("Khôi phục từ bản sao lưu đã hoàn tất.");
}

Kiểm tra tổng và xác thực

Với các tệp quan trọng, bạn có thể lưu kiểm tra tổng (ví dụ, MD5 hoặc SHA-256) và mỗi lần mở tệp thì so sánh với giá trị hiện tại. Nếu không khớp — tệp đã bị hỏng.

// Sơ đồ minh họa (bỏ qua phần hiện thực hóa băm để đơn giản)
String expectedHash = "..."; // giá trị đã lưu trước đó
String actualHash = calculateFileHash("tasks.txt");
if (!expectedHash.equals(actualHash)) {
    System.out.println("Tệp tasks.txt bị hỏng! Hãy thử khôi phục từ bản sao lưu.");
}

6. Những lỗi thường gặp khi xử lý tệp bị hỏng

Lỗi số 1: Không kiểm tra định dạng tệp. Nếu bạn mong đợi mỗi dòng — ví dụ — là một số, nhưng thực tế là văn bản — sẽ phát sinh NumberFormatException. Tốt hơn nên xác thực dữ liệu ngay trong lúc đọc.

Lỗi số 2: Không dùng try-with-resources. Nếu không dùng try-with-resources, tệp có thể bị “treo” (không được đóng) ngay cả khi có lỗi, gây khó khăn cho việc khôi phục hoặc xóa.

Lỗi số 3: Ghi đè tệp bị hỏng mà không tạo bản sao lưu. Nếu khi gặp lỗi bạn lập tức ghi đè tệp, cơ hội khôi phục sẽ giảm. Tốt hơn hãy lưu bản sao lưu trước.

Lỗi số 4: Khôi phục nhưng không thông tin đầy đủ. Người dùng cần biết rằng tệp đã bị hỏng và được khôi phục từ bản sao lưu — nếu không họ có thể không hiểu vì sao một phần dữ liệu biến mất.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION