Cấu trúc dưới lớp của Microsoft Excel (.xlsx)
Định dạng file Excel hiện đại (từ phiên bản 2007 trở lên) tuân theo tiêu chuẩn Office Open XML (OOXML), quy định bởi ECMA-376 và ISO/IEC 29500. Khác với các định dạng nhị phân cũ (.xls), các file .xlsx thực chất là một kho lưu trữ ZIP đóng gói nhiều tệp XML riêng biệt. Điều này có nghĩa là bất kỳ thao tác nào trên nội dung Excel đều có thể được thực hiện bằng cách trích xuất, sửa đổi các thành phần XML và tái nén lại.
1. Phân tích cấu trúc thư mục
Khi giải nén một file .xlsx, ta thường thấy cấu trúc như sau:
MySpreadsheet.xlsx (zip)
├── _rels/
│ └── .rels # Định nghĩa mối quan hệ gốc của gói
├── xl/
│ ├── workbook.xml # Dữ liệu chính của sổ làm việc
│ ├── worksheets/ # Các trang tính cụ thể
│ │ ├── sheet1.xml
│ │ └── sheet1.xml.rels # Mối quan hệ riêng cho trang tính
│ ├── embeddings/ # Thư mục chứa các đối tượng đã nhúng
│ ├── drawings/ # Đồ họa hoặc vị trí đặt đối tượng
│ └── _rels/workbook.xml.rels
├── docProps/ # Metadata (tác giả, ngày tạo...)
└── [Content_Types].xml # Khai báo kiểu dữ liệu cho từng file trong zip
Việc chèn một tệp phụ kiện (attachment) thực chất là quá trình tạo ra một đối tượng OLE (Object Linking and Embedding) và thiết lập các liên kết giữa các tệp XML để dẫn hướng từ giao diện người dùng đến tệp đính kèm thực tế trong thư mục `embeddings`.
2. Cơ chế OLE Object trong OOXML
Để Excel nhận diện và hiển thị một biểu tượng tệp đính kèm, cần phải thực hiện 3 bước chỉnh sửa đồng bộ:
2.1. Chứa tệp đính kèm
Tệp đích cần được sao chép vào đường dẫn tương đối `xl/embeddings/`. Ví dụ: nếu muốn nhúng `data.pdf`, nó sẽ nằm tại `xl/embeddings/data.pdf`.
2.2. Khai báo mối quan hệ (Relationships)
Cần khai báo trong file `xl/worksheets/_rels/sheet1.xml.rels` rằng có một mối liên kết mới. Mối liên kết này chỉ tới tệp ở `embeddings`.
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rIdNewID" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="../embeddings/data.pdf"/>
</Relationships>
2.3. Định vị và hiển thị trong WorkSheet
Trong file trang tính (`xl/worksheets/sheet1.xml`), cần chèn thẻ `
<drawing r:id="rIdDrawing1"/>
<oleObjects xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<oleObject progId="Package" shapeId="1025" r:id="rIdNewID"/>
</oleObjects>
Cùng với đó, `
<Default Extension="pdf" ContentType="application/pdf"/>
3. Quy trình thủ công chi tiết
Ngoài ra, có thể tham khảo các bước sau để thực hiện việc này trên môi trường dòng lệnh.
- Bảo vệ dữ liệu: Sao lưu bản gốc trước khi thao tác.
- Giải nén: Đổi đuôi file từ `.xlsx` sang `.zip` và sử dụng công cụ nén như `unzip` để giải nén toàn bộ nội dung ra một thư mục tạm.
- Xử lý tệp nhúng: Copy tệp cần nhúng vào thư mục `xl/embeddings`.
- Sửa XML: Sử dụng trình soạn thảo văn bản để chèn nội dung OLE vào các file XML đúng chỗ đã phân tích ở mục 2.
- Tái nén: Gom lại tất cả các file vào thư mục gốc và lưu thành file `.xlsx` mới. Lưu ý phải đảm bảo không thêm thư mục con vào bên trong zip nếu tên gốc đã thay đổi.
4. Tự động hóa bằng Java API
Thay vì thao tác thủ công, các nhà phát triển thường sử dụng Java để tự động hóa quy trình đọc/ghi ZIP và xử lý XML. Dưới đây là ví dụ về một hàm tiện ích giúp nhúng tệp mà không cần cài đặt các thư viện nặng nề như POI, chỉ dựa vào luồng IO cơ bản.
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;
public class XlsxEmbedder {
private static final String TARGET_DIR = "xl/embeddings";
private static final String SHEETS_REL_DIR = "xl/worksheets/_rels";
public void attachFile(String inputPath, String filePathToAdd) throws Exception {
Path tempRoot = Files.createTempDirectory("xlsx_proc");
try {
// Giải nén file nguồn
unzipFile(inputPath, tempRoot);
// Lấy tên file để đặt vào thư mục embeddings
String fileName = Paths.get(filePathToAdd).getFileName().toString();
File embeddedFile = tempRoot.resolve(TARGET_DIR).resolve(fileName).toFile();
// Sao chép file cần nhúng
Files.copy(Paths.get(filePathToAdd), embeddedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// Cập nhật Content Types
updateContentTypes(tempRoot, fileName);
// Cập nhật Mối quan hệ và Worksheet
injectOleObject(tempRoot, fileName);
// Tái nén thành file mới
String outputPath = inputPath.replace(".xlsx", "_embedded.xlsx");
zipFilesIntoFile(tempRoot, outputPath);
System.out.println("Hoàn tất: " + outputPath);
} finally {
// Vệ sinh thư mục tạm
deleteDirRecursive(tempRoot);
}
}
private void extractContent(ZipInputStream zis, Path root) throws IOException {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path target = root.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING);
}
zis.closeEntry();
}
}
private void updateContentTypes(Path root, String extension) throws Exception {
Path contentTypesFile = root.resolve("[Content_Types].xml");
String xmlContent = new String(Files.readAllBytes(contentTypesFile));
if (!xmlContent.contains(<Extension="" + extension)) {
String typeEntry = String.format("<Default Extension="%s" ContentType="application/octet-stream"/>\n", extension);
xmlContent = xmlContent.replace("</Types>", typeEntry + "</Types>");
Files.write(contentTypesFile, xmlContent.getBytes());
}
}
private void injectOleObject(Path root, String fileName) throws Exception {
// Giả lập tìm và chỉnh sửa sheet1.xml và các rels file
// Trong thực tế cần dùng XPath hoặc DOM Parser để sửa chuẩn hơn
// Tại đây minh họa logic đơn giản
Path relFile = root.resolve(SHEETS_REL_DIR).resolve("sheet1.xml.rels");
if (Files.exists(relFile)) {
String relXml = new String(Files.readAllBytes(relFile));
int idCounter = countRelIds(relXml);
String newRId = "rId" + (idCounter + 1);
String relationshipTag = String.format(
"<Relationship Id="%s" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="../embeddings/%s"/>\n",
newRId, fileName
);
if (!relXml.contains(newRId)) {
relXml = relXml.replace("</Relationships>", relationshipTag + "</Relationships>");
Files.write(relFile, relXml.getBytes());
}
}
}
private int countRelIds(String content) {
int max = 0;
java.util.regex.Pattern p = java.util.regex.Pattern.compile("rId(\\d+)");
java.util.regex.Matcher m = p.matcher(content);
while (m.find()) {
int val = Integer.parseInt(m.group(1));
if (val > max) max = val;
}
return max;
}
private void deleteDirRecursive(Path path) throws IOException {
if (Files.exists(path)) {
Files.walk(path).sorted((a, b) -> -a.compareTo(b)).forEach(p -> {
try { Files.delete(p); } catch (IOException e) {}
});
}
}
}
5. Những điểm kỹ thuật cần lưu ý
- ID duy nhất: Mỗi mối quan hệ trong hệ thống XML cần một ID duy nhất (rId...). Nếu xung đột, Excel sẽ báo lỗi cấu hình file hỏng.
- Thuộc tính Drawing: Đối tượng nhúng thường cần một thẻ `
` tồn tại trong Sheet, nhưng thực tế XML của drawing (vị trí icon) đôi khi nằm riêng. Để đơn giản hóa, việc thiếu drawing reference có thể gây lỗi hiển thị mà không mất dữ liệu. - Mã hóa: Tất cả các file XML trong bộ zip bắt buộc phải sử dụng UTF-8. Sử dụng mã khác có thể gây lỗi đọc.
- Kiểm thử: Sau khi sửa đổi, luôn kiểm tra bằng cách mở lại Excel xem có cảnh báo "Repair Mode" hay không.