Việc đổi tên một tham chiếu worksheet hardcode trên một nghìn mẫu báo cáo có bật macro loại trừ khả năng mở từng file trong VBA editor bằng tay. HotXLS, thành phần Excel Delphi và C++Builder gốc, xử lý trường hợp đó bằng cách phơi bày mã nguồn của một module VBA như một thuộc tính SourceCode có thể chỉnh sửa và nén lại mỗi lần chỉnh sửa bằng thuật toán nén MS-OVBA mà Microsoft định nghĩa cho việc lưu trữ VBA, ghi kết quả trở lại vào bộ lưu trữ VBA XLS cổ điển, một file dự án VBA độc lập, hoặc một workbook XLSM có bật macro. Không có instance Excel nào, không VBA editor nào, và không có macro recorder nào liên quan ở bất cứ đâu trong đường xử lý đó
Vì sao một luồng module VBA không phải một file văn bản
Một module VBA bên trong một workbook XLS hay một file dự án VBA độc lập không phải văn bản nguồn nằm trong một luồng chờ được đọc — nó là một container nhị phân nhỏ. Một cache hiệu năng đã biên dịch đến trước, các byte mà Office dùng để bỏ qua việc biên dịch lại module lúc nạp khi cache vẫn khớp với phiên bản host, và văn bản nguồn thực sự theo sau, chạy qua một lược đồ nén độc quyền mà MS-OVBA định nghĩa riêng cho lưu trữ VBA. Lược đồ đó không phải zip, không phải deflate, và không phải bất cứ thứ gì các API nén của Windows tạo ra một cách gốc, đó chính xác là lý do vì sao hầu hết các thư viện Excel bên thứ ba có thể đọc mã nguồn của một module — giải nén là nửa dễ hơn của vấn đề — trong khi dừng lại trước việc ghi nó trở lại, vì nén lại là nơi một bit sai một cách tinh vi tạo ra một file mà Excel từ chối mở. Các bài viết công khai về phía đọc thì có sẵn; các triển khai phía ghi thực sự thực hiện việc nén lại, chứ không chỉ giải nén một module hiện có để kiểm tra, thì hiếm đến mức đây vẫn còn là một trong những góc ít được ghi chép nhất của các định dạng file Excel
Thuộc tính SourceCode của HotXLS thực sự thay đổi điều gì?
HotXLS biểu diễn mỗi module VBA như một đối tượng TXLSVBAModule với một thuộc tính SourceCode: WideString thuần túy, và việc gán cho nó một giá trị mới đơn giản đúng như vẻ ngoài của nó: module được đánh dấu bẩn trong bộ nhớ, và không có gì đụng đến luồng OLE bên dưới cho đến khi dự án được lưu. Bản thân dự án đến từ IXLSWorkbook.VBAProject trên engine XLS cổ điển hoặc TXLSXWorkbook.ParsedVBAProject trên engine OOXML có bật macro, cả hai đều trả về một TXLSVBAProject mà các module của nó nằm sau một chỉ mục Item[] bắt đầu từ 1 và một thuộc tính Count, nên một lượt chỉnh sửa hàng loạt qua mọi module trong một workbook chỉ là một vòng lặp trên một khoảng số nguyên
var
Wb: TXLSWorkbook;
Project: TXLSVBAProject;
I: Integer;
Updated: WideString;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('MonthlyReport.xls');
if Wb.HasVBAProject then
begin
Project := Wb.VBAProject;
for I := 1 to Project.Count do
begin
Updated := StringReplace(Project[I].SourceCode,
'ReportSheet2025', 'ReportSheet2026', [rfReplaceAll]);
if Updated <> Project[I].SourceCode then
Project[I].SourceCode := Updated; // marks the module dirty
end;
Wb.SaveAs('MonthlyReport.xls'); // recompresses on write
end;
finally
Wb.Free;
end;
end;
Vòng lặp đó cũng là hình dạng của một lượt kiểm toán. Trước khi một nghìn mẫu bị đụng đến, hầu hết các đội trước tiên muốn biết có bao nhiêu trong số đó thực sự mang macro và các macro đó tham chiếu đến gì, đó là kịch bản đứng sau xưởng kiểm toán và chuyển đổi workbook — cùng Project.Count điều khiển một vòng lặp viết lại ở đây trở thành một bảng tổng hợp macro theo từng file ở đó
Bên trong container nén MS-OVBA
Định dạng nén của MS-OVBA đóng gói các byte nguồn vào thứ mà spec gọi là CompressedContainer: một byte chữ ký duy nhất, bắt buộc bằng 0x01, theo sau bởi một chuỗi các khối CompressedChunk, mỗi khối bao phủ tối đa 4096 byte dữ liệu đã giải nén. Một header chunk 16-bit mang ba trường — một chữ ký 3-bit phải bằng 3, một trường kích thước 12-bit, và một bit CompressedChunkFlag đánh dấu liệu payload của chunk là byte chữ nghĩa hay một chuỗi đã nén theo token. Khi cờ được đặt, payload là một chuỗi các nhóm tám token có tiền tố byte cờ, và mỗi token hoặc là một byte chữ nghĩa đơn lẻ hoặc một CopyToken: một tham chiếu ngược offset/độ dài vào các byte đã được giải nén trước đó trong cùng chunk, với chiều rộng bit chia giữa offset và độ dài thay đổi tùy theo bộ giải nén hiện đang ở vị trí nào trong chunk. Phần này của MS-OVBA (§2.4.1, Compression and Decompression) là nơi một triển khai viết tay thường mất một ngày trời cho một lỗi lệch-một-đơn-vị trong phép tính chiều rộng bit đó
Vì sao HotXLS ghi các chunk thô thay vì so khớp token
Đường ghi của HotXLS né hoàn toàn nửa so-khớp-token của thuật toán đó. Khi nó nén lại một module đã chỉnh sửa, mọi chunk đi ra với CompressedChunkFlag bị xóa, nghĩa là chunk chứa byte chữ nghĩa thay vì token tham chiếu ngược — hợp lệ theo MS-OVBA, vì một container nén được phép hoàn toàn gồm các chunk chưa nén, và điều đó loại bỏ chính xác phần khó làm đúng nhất bằng tay của thuật toán: tìm các tham chiếu ngược hợp lệ và đóng gói một cặp offset/độ dài vào một chiều rộng bit phụ thuộc vào vị trí hiện tại bên trong chunk. Sự đánh đổi thể hiện ở kích thước file, không phải tính đúng đắn — một luồng module được viết lại có kích thước gần bằng văn bản nguồn của nó cộng thêm một header hai byte cho mỗi khối 4096 byte, không nhỏ hơn theo cách một chunk nén token đầy đủ sẽ có. Mọi trình đọc triển khai phía giải nén của spec, kể cả Excel, vẫn mở kết quả đúng, vì một chunk thô cũng hợp lệ như một CompressedChunk như một chunk nén token
HotXLS để nguyên những gì khi nó viết lại một module
Việc nén lại chỉ bao giờ thay thế một phần của luồng module. Mỗi luồng module lưu trữ cache hiệu năng của nó trước và mã nguồn đã nén thứ hai, và luồng dir của dự án ghi lại chính xác điểm chia đó rơi ở đâu cho mỗi module trong một mục MODULEOFFSET; HotXLS đọc offset đó, giữ nguyên mọi byte trước nó đúng như tìm thấy, và chỉ dựng lại container đã nén từ offset đó trở đi
Bản thân văn bản nguồn khứ hồi qua trang mã riêng của dự án VBA thay vì UTF-8 — cùng trang mã cũ mà Office đã viết dự án đó ngay từ đầu. Một chỉnh sửa SourceCode đưa vào các ký tự nằm ngoài kho ký tự của trang mã đó bị âm thầm thay thế bằng các ký tự thay thế tốt-nhất-có-thể khi HotXLS mã hóa lại chuỗi trở thành byte, không bị từ chối, nên một ký tự khu vực khác thường thả vào một comment hay một chuỗi literal là nơi dễ nhận thấy sự mất mát nhất. Các tham chiếu ngoài và binding thư viện bên trong cùng dự án đi theo một đường bảo toàn liên quan nhưng riêng biệt, được nói đến trong bài viết đồng hành về bảo toàn liên kết ngoài VBA, và đáng đọc trước khi một lượt viết lại đụng đến một dự án liên kết ra các workbook hay thư viện kiểu khác
Làm sao để đưa các macro đã viết lại trở lại vào một workbook?
Không có gì gọi bước nén lại một cách tường minh — nó chạy tự động ngay khi một workbook hay một dự án VBA độc lập được lưu. TXLSVBAProject.ApplyChanges duyệt qua mọi module, nén lại những module có SourceCode đã thay đổi kể từ lần lưu cuối, và chỉ viết lại luồng của module đó; TXLSWorkbook.SaveAs cổ điển, khi đích lưu giữ nguyên định dạng gốc của file, và TXLSXWorkbook.SaveAs OOXML cho một gói XLSM có bật macro cả hai đều gọi nó nội bộ trước khi bất cứ thứ gì được ghi ra đĩa, và SaveVBAProjectToFile gọi cùng phương thức đó khi đích là một file dự án VBA tách rời thay vì một workbook đầy đủ
var
Wb: TXLSWorkbook;
begin
Wb := TXLSWorkbook.Create;
try
if Wb.LoadVBAProjectFromFile('LegacyMacros.ole') = 1 then
begin
Wb.VBAProject[1].SourceCode :=
StringReplace(Wb.VBAProject[1].SourceCode, 'OldServer', 'NewServer', [rfReplaceAll]);
Wb.SaveVBAProjectToFile('LegacyMacros_Patched.ole'); // ApplyChanges runs internally
end;
finally
Wb.Free;
end;
end;
var
Xlsx: TXLSXWorkbook;
Project: TXLSVBAProject;
begin
Xlsx := TXLSXWorkbook.Create;
try
Xlsx.Open('Dashboard.xlsm');
Project := Xlsx.ParsedVBAProject;
if Assigned(Project) then
begin
Project[1].SourceCode := StringReplace(Project[1].SourceCode,
'ConnStringV1', 'ConnStringV2', [rfReplaceAll]);
Xlsx.SaveAs('Dashboard.xlsm'); // SyncParsedVBAProject recompresses before the part is written
end;
finally
Xlsx.Free;
end;
end;
Cả ba đích đến đều chia sẻ cùng cơ chế SourceCode và ApplyChanges bên dưới; khác biệt thực sự duy nhất giữa chúng là lệnh gọi lưu nào cuối cùng kích hoạt việc nén lại
Nơi điều này vẫn hỏng
Hai chế độ lỗi đủ phổ biến để lên kế hoạch trước khi một lượt viết lại chạy trên các file sản xuất. Một dự án VBA đã ký số ngừng hợp lệ về mặt chữ ký ngay khi mã nguồn của nó thay đổi, vì chữ ký bao phủ nội dung của dự án; HotXLS không có cách nào để ký lại một dự án thay bạn, và Excel loại bỏ hoặc đánh dấu chữ ký lần tiếp theo file mở, nên một dự án macro đã ký cần một bước ký lại ở hạ nguồn nếu chữ ký đó là thứ mà luồng công việc của bạn thực sự kiểm tra. Chế độ lỗi thứ hai thuộc về bất kỳ ai bị cám dỗ tự triển khai lại định dạng nén này từ đầu thay vì dùng một thư viện đã xử lý sẵn nó: một bit sai duy nhất trong một header chunk, trong nibble chữ ký, trường kích thước, hay cờ nén, tạo ra một file mà Excel từ chối mở, thường đằng sau một cảnh báo hỏng chung chung không cho manh mối nào về byte nào sai — chính xác là loại lỗi mà chiến lược ghi chunk thô được mô tả ở trên tồn tại để tránh
Không điều nào trong số này đòi hỏi phải reverse-engineer định dạng để sử dụng. Lập trình viên Delphi và C++Builder nhận được quyền đọc và ghi SourceCode, việc nén lại tuân thủ MS-OVBA, và cả ba đích ghi lại được mô tả ở đây như một phần của HotXLS Component tiêu chuẩn, cùng với phần còn lại của API workbook XLS cổ điển và OOXML của nó