บางครั้งคำถามเดียวที่รูทีนรับข้อมูลเข้าต้องการคำตอบก็เป็นแค่เรื่องโครงสร้างล้วน ๆ: workbook นี้มี sheet ชื่อ "Mapping" หรือเปล่า หรือมันมีกี่แท็บ การตอบคำถามนี้ด้วยการเรียก Open คือวิธีที่แพงเกินจำเป็น การเปิดแบบเต็มจะทำให้ shared string table พองตัว ถอดรหัส style record ทุกตัว และไล่ผ่านเซลล์ของทุก worksheet เพราะมันไม่มีทางรู้เลยว่าคุณต้องการแค่สารบัญ ในไฟล์ขนาดใหญ่นั่นคือการจัดสรรหน่วยความจำหลายร้อยเมกะไบต์และ CPU อีกหลายวินาที เพื่ออ่านรายการที่กินพื้นที่แค่ไม่กี่กิโลไบต์ HotXLS ไลบรารีสเปรดชีตแบบ native ของ Delphi จาก losLab ให้รายการนั้นแก่คุณได้เลยโดยตรง: GetSheetNames คืนชื่อ worksheet กลับมาตามลำดับใน workbook โดยไม่ต้อง materialize เซลล์แม้แต่เซลล์เดียว
เหตุใดแคตตาล็อกจึงอ่านได้ถูกจนแทบไม่มีต้นทุน
ฟอร์แมตสเปรดชีตทั้งสองแบบวางสารบัญไว้ใกล้จุดเริ่มต้นของไฟล์ นี่คือสิ่งที่ทำให้การเรียกดูรายการเร็ว ไม่ใช่เพราะฉลาดเป็นพิเศษ แพ็กเกจ OOXML เก็บแคตตาล็อกของ sheet ไว้ใน xl/workbook.xml ซึ่งเป็นส่วนที่มีขนาดเล็กเสมอไม่ว่า workbook จะมีสิบแถวหรือสิบล้านแถว ไฟล์ BIFF8 .xls เก็บ BoundSheet record ไว้ที่จุดเริ่มต้นของ workbook globals stream ก่อนข้อมูลเซลล์ใด ๆ ทั้งสิ้น ดังนั้นงานที่การเรียกดูรายการช่วยหลีกเลี่ยงไม่ใช่แค่ตัวเลขปัดเศษเมื่อเทียบกับการเปิดแบบเต็ม แต่มันคืองานส่วนใหญ่ของไฟล์เลยทีเดียว การอ่านแคตตาล็อกมีต้นทุนแค่ไม่กี่กิโลไบต์เท่ากันเสมอไม่ว่าจำนวนแถวจะเป็นเท่าไร ในขณะที่การเปิดแบบเต็มจะขยายตามข้อมูล และในไฟล์ workbook ขนาดหลายเมกะไบต์ ช่องว่างนี้กว้างถึงหลายเท่าตัวทั้งในแง่ไบต์ที่ถูกแตะและหน่วยความจำที่ถูกจัดสรร
ต้นทุนคงที่แบบนี้แหละคือคุณสมบัติที่คุ้มค่าจะออกแบบให้เป็นแกนกลาง gate รับข้อมูลเข้าที่สร้างจาก GetSheetNames จะทำงานเหมือนกันทั้งกับไฟล์ 200 แถวและไฟล์ 200 MB ดังนั้นไฟล์ที่ช้าที่สุดในชุดงานหนึ่ง ๆ จะไม่เป็นตัวกำหนดจังหวะของการตัดสินใจอีกต่อไปว่าไฟล์นั้นคุ้มค่าที่จะประมวลผลหรือไม่
เรียกครั้งเดียวครอบคลุมทั้ง .xls, .xlsx และฟอร์แมตเทมเพลต
บน facade ของ XLS นั้น TXLSWorkbook.GetSheetNames อ่านได้มากกว่าแค่ .xls มันรับไฟล์แบบ zip อย่าง .xlsx, .xlsm, .xltx และ .xltm ด้วย โดยดึงแค่ workbook.xml ออกมาจาก archive สำหรับ input .xls แท้ ๆ มันจะสแกน BoundSheet record แล้วหยุดที่ EOF record ตัวแรกของ globals substream ดังนั้นไฟล์ไบนารีขนาดใหญ่ก็ยังมีต้นทุนแค่ไม่กี่กิโลไบต์แรกของไฟล์เท่านั้น facade ของ XLSX มีการรับประกันอย่างหนึ่งที่สำคัญกว่าที่มองแวบแรกสำหรับโค้ดบริการที่รันต่อเนื่องเป็นเวลานาน: TXLSXWorkbook.GetSheetNames จะไม่ reset และไม่เติมข้อมูลลงในอินสแตนซ์ของ workbook เลย ดังนั้นอินสแตนซ์ที่กำลังถือเอกสารที่เปิดอยู่แล้วก็ยังสำรวจไฟล์อื่นได้โดยไม่รบกวนไฟล์ที่ถืออยู่ในมือ GetODSSheetNames ใช้วิธีเดียวกันนี้กับแพ็กเกจ OpenDocument และทุกเมธอดในกลุ่มนี้มี overload แบบ stream ให้ด้วย ซึ่งทำให้คุณตรวจสอบไฟล์อัปโหลดที่ไม่เคยแตะดิสก์เลยได้
var
Book: TXLSXWorkbook;
Names: TStringList;
I: Integer;
begin
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
raise Exception.Create('unreadable workbook package');
if Names.IndexOf('Mapping') < 0 then
raise Exception.Create('required Mapping sheet is missing');
for I := 0 to Names.Count - 1 do
Writeln(Format('sheet %d: %s', [I, Names[I]]));
finally
Book.Free;
Names.Free;
end;
end;
เรียกแบบเดียวกันนี้ยังใช้ทำ import dialog บนเดสก์ท็อปได้ดีด้วย แสดงรายชื่อ sheet ให้ผู้ใช้เลือกก่อน แล้วค่อยจ่ายต้นทุนของการเปิดแบบเต็มก็ต่อเมื่อเลือกแล้วเท่านั้น กับ workbook ที่มีห้าสิบ sheet ความต่างจะเห็นชัดเจน: หน้าต่างเลือกที่ปรากฏขึ้นทันที เทียบกับหน้าต่างที่ค้างอยู่ขณะที่ไฟล์ทั้งหมดกำลังโหลดอยู่ข้างหลัง
ไฟล์ .xlsm ที่เปิดใช้ macro และฟอร์แมตเทมเพลตต่าง ๆ ก็แสดงรายชื่อ sheet ได้เหมือนกับ .xlsx ธรรมดาเป๊ะ เพราะแคตตาล็อกอยู่ใน workbook.xml เดียวกันไม่ว่าจะมี vbaProject.bin ติดมาในแพ็กเกจด้วยหรือไม่ pipeline รับข้อมูลเข้าจึงสามารถแจกแจง sheet ของ macro workbook เพื่อการ routing ได้เลย โดยไม่แตะ payload ของ macro และไม่ทำอะไรที่จะทำให้มันรันขึ้นมาเลย แล้วปล่อยการตัดสินใจเรื่องนโยบาย macro ไว้ให้กับขั้นตอนที่เปิดไฟล์จริง ๆ
อ่านค่าที่คืนมาโดยไม่หลอกตัวเอง
ธรรมเนียมค่าที่คืนกลับมาไม่ได้เหมือนกันทั้งหมดใน HotXLS บางเมธอดคืน 1 เมื่อสำเร็จ บางตัวคืนจำนวนนับ ดังนั้นสำหรับฟังก์ชันกลุ่มแสดงรายการ วิธีตรวจที่ใช้ได้เสมอมีแค่แบบเดียวคือถือว่าค่าใด ๆ ที่เป็นศูนย์หรือต่ำกว่าคือความล้มเหลว พร้อมกับที่ string list ถูกล้างไปด้วย อย่าหลงไปตีความรายการว่างว่าหมายถึง "workbook ที่ไม่มี sheet เลย" ทั้ง ECMA-376 และข้อกำหนด BIFF8 บังคับให้ workbook ที่ถูกต้องต้องมีอย่างน้อยหนึ่ง sheet เสมอ ดังนั้นจำนวนชื่อเป็นศูนย์จึงหมายถึงการอ่านล้มเหลวเท่านั้น ไม่เคยหมายถึงไฟล์ว่างเปล่าโดยถูกต้องตามกฎ
การแสดงรายการที่ล้มเหลวเองก็เป็นสัญญาณที่ควรเก็บไว้ ไฟล์ .xlsx ที่เรียกแล้วล้มเหลวมักเป็นหนึ่งในไม่กี่กรณีเฉพาะ: ไฟล์ถูกตัดทอน ไม่ใช่แพ็กเกจ OOXML จริง ๆ เลย (ไฟล์ CSV ที่ถูกติดป้ายผิดจากระบบอื่นโผล่มาแบบนี้บ่อยมาก) หรือเป็น container ที่ถูกเข้ารหัส การแยกแยะว่าเป็นกรณีไหนคือหน้าที่ของการตรวจสอบขั้นถัดไป การบันทึกไบต์แรก ๆ ของไฟล์ที่ถูกปฏิเสธไว้คู่กับความล้มเหลวมักทำให้เธรดของ support จบลงด้วยข้อความเดียว
ตรวจจับ container ที่เข้ารหัสก่อนจะ route
.xlsx ที่ถูกเข้ารหัสไม่ใช่ zip มันคือ OLE compound file ที่ห่อ stream ชื่อ EncryptionInfo และ EncryptedPackage ไว้ ดังนั้น GetSheetNames จึงมองไม่เห็นข้างในและคืนค่าความล้มเหลวเหมือนไฟล์ที่อ่านไม่ได้ทั่วไป CanReadEncrypted ตรวจสอบรูปทรงของ container แบบนั้นโดยเฉพาะ ซึ่งทำให้การรับข้อมูลเข้าสามารถ route ไฟล์ที่เข้ารหัสได้อย่างตั้งใจ แทนที่จะกลืน error การอ่านทั่ว ๆ ไปที่มาจากที่ไหนสักแห่งลึก ๆ ใน worker:
type
TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);
function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
// OOXML ที่เข้ารหัสคือ OLE container ไม่ใช่ zip ต้องตรวจสอบก่อน
// เพราะเมธอดแสดงรายการมองเข้าไปข้างในไม่ได้
if Book.CanReadEncrypted(FileName) then
Exit(irNeedsPassword);
if SameText(ExtractFileExt(FileName), '.ods') then
begin
if Book.GetODSSheetNames(FileName, Names) <= 0 then
Exit(irUnreadable);
end
else if Book.GetSheetNames(FileName, Names) <= 0 then
Exit(irUnreadable);
Result := irNormal;
finally
Book.Free;
end;
end;
เรื่องการเข้ารหัสคือจุดที่ HotXLS จงใจทำให้ไม่สมมาตร ดังนั้นการ routing ก็ต้องเคารพความจริงข้อนี้ด้วย การเข้ารหัส .xls แบบดั้งเดิม (RC4, RC4 CryptoAPI, XOR) อ่านได้: TXLSWorkbook.Open(FileName, Password) ถอดรหัสด้วยรหัสผ่านที่เก็บไว้ และไฟล์เหล่านั้นก็อยู่บนเส้นทางอัตโนมัติต่อไปได้ แพ็กเกจ OOXML ที่เข้ารหัสไปในทางตรงข้าม HotXLS เขียนมันได้ด้วย SaveAsEncrypted แต่อ่านกลับไม่ได้ OpenEncrypted จะโยน EXlsxEncryptionNotImplemented เมื่อเจอแพ็กเกจที่เข้ารหัส นี่คือเหตุผลที่การออกแบบระบบรับข้อมูลเข้าอย่างซื่อตรงจะส่ง .xlsx ที่เข้ารหัสไปให้คนที่มี Excel และเก็บ .xls ที่มีรหัสผ่านไว้ในโค้ดต่อไป
สำหรับงานแบบ batch ตัวจำแนกประเภทนี้คุ้มค่าที่จะรันผ่านทั้งไดเรกทอรีที่รับเข้ามาก่อนที่ worker ตัวไหนจะเริ่มประมวลผลจริง เพราะแต่ละการตรวจมีต้นทุนแค่การเปิดไฟล์หนึ่งครั้งกับการอ่านไม่กี่กิโลไบต์ การทำสิ่งนี้ไว้ล่วงหน้าเปลี่ยนรูปแบบความล้มเหลวที่ทีมปฏิบัติการใส่ใจจริง ๆ แทนที่จะเป็นงานตอนตีสามที่ตายตรงไฟล์ที่ 412 จาก 600 คุณจะได้ไฟล์ 412 ไฟล์เข้าคิวและ 5 ไฟล์ถูกปฏิเสธตั้งแต่ขั้นรับเข้า พร้อมเหตุผลติดอยู่กับแต่ละไฟล์ เรียกไลบรารีแบบเดียวกัน แต่เรื่องราวด้านปฏิบัติการดีกว่ากันมาก
คำถามที่การเรียกดูรายการตอบให้ไม่ได้
ชื่อกับลำดับคือทั้งหมดที่คุณจะได้รับ การเรียกดูรายการไม่บอกอะไรเลยเรื่องการมองเห็น ดังนั้น sheet ที่ถูกซ่อนและซ่อนลึก (very-hidden) ก็จะโผล่มาในรายการเหมือน sheet ทั่วไปทุกประการ มันไม่รายงานขนาดของ used-range ไม่รายงานจำนวนเซลล์ และไม่รายงาน document properties ส่วน docProps/core.xml เองก็มีขนาดเล็กเช่นกัน แต่ทุกวันนี้ยังไม่มีการตรวจแบบ properties-only ดังนั้น metadata อย่างผู้เขียนและชื่อเรื่องยังคงมีต้นทุนเท่ากับ Open แบบเต็มอยู่ดี วิธีที่สะอาดในการอยู่กับเรื่องนี้คือปล่อยให้ข้อเท็จจริงราคาถูก route ทุกไฟล์ก่อน แล้วเก็บงานที่แพงไว้สำหรับไฟล์ที่รอดจากการ route เท่านั้น สำหรับไฟล์ที่ต้องเข้าสู่การอ่านแบบลึก การสแกนไฟล์ .xls ขนาดใหญ่แบบ read-only จะเร็วขึ้นอย่างเห็นได้ชัดด้วย _DisableGraphics := True ซึ่งข้ามการแปลง OfficeArt ไป แค่อย่า save จากอินสแตนซ์นั้นเด็ดขาด: เลเยอร์รูปวาดที่ถูกข้ามไปจะหายไปจากโมเดล และการ save ก็จะทำให้มันหายไปจากไฟล์ด้วย
ไฟล์ที่ผ่านการคัดกรองมักไปต่อสู่การวิเคราะห์เชิงลึก workbench สำหรับการตรวจสอบและแปลง workbook ครอบคลุมตัวนับรายชีทที่ควรเก็บเมื่อการเปิดแบบเต็มคุ้มค่าแล้ว และ คู่มือประสิทธิภาพของ workbook ขนาดใหญ่ ครอบคลุมวิธีทำให้การเปิดแบบเต็มนั้นยังคงเร็วอยู่
HotXLS คือไลบรารีสเปรดชีตแบบ native ที่เขียนด้วย Object Pascal สำหรับ Delphi และ C++Builder ส่วน API ทั้งหมด รวมถึงเมธอดตรวจสอบที่แสดงไว้ในบทความนี้ มีเอกสารอยู่ที่ หน้าผลิตภัณฑ์ HotXLS Delphi Component