บทความเทคนิค

การแปลงข้อมูล XLSX แบบไม่สูญเสียข้อมูลใน Delphi: ธีม, extLst, calcChain

HotXLS ซึ่งเป็นไลบรารี Excel แบบเนทีฟสำหรับ Delphi และ C++Builder ได้รับการสร้างขึ้นสำหรับการแปลงข้อมูล XLSX แบบไม่สูญเสียข้อมูล (lossless round-trip): เปิดสมุดงาน แก้ไขเซลล์เดียว สั่งบันทึก และผลลัพธ์ที่ได้คือธีมแบบกำหนดเองของลูกค้า บล็อกส่วนต่อขยาย extLst ของระบบภายนอก และห่วงโซ่การคำนวณจะยังคงอยู่ทั้งหมด มีกลไกสามส่วนที่ช่วยให้บรรลุผลลัพธ์ดังกล่าว — ได้แก่ การแคชข้อมูลไบต์ดั้งเดิมของ xl/theme/theme1.xml, การสร้างโครงสร้างบล็อก <ext> ที่ไม่รู้จักขึ้นใหม่ตามเหตุการณ์ และการสร้างไฟล์ xl/calcChain.xml ใหม่ที่ถูกต้องตามข้อกำหนดในทุกครั้งที่สั่งบันทึกสมุดงานที่มีสูตรการคำนวณ

สถานการณ์ที่เป็นตัวกระตุ้นกลไกทั้งสามนี้เป็นเรื่องที่พบเจอได้บ่อยมาก บริการออกใบแจ้งหนี้โหลดเทมเพลตที่ลูกค้าออกแบบใน Excel — ธีมสีขององค์กร, เส้นแผนภูมิขนาดเล็ก (sparkline) ในคอลัมน์ KPI, กฎการจัดรูปแบบตามเงื่อนไขที่เพิ่มเข้ามาโดย Excel รุ่นที่ใหม่กว่า — จากนั้นเขียนยอดรวมใบแจ้งหนี้ลงในเซลล์ B3 และบันทึก เมื่อลูกค้าเปิดไฟล์ผลลัพธ์ ปรากฏว่าสีของแบรนด์เปลี่ยนกลับเป็นสีน้ำเงินเริ่มต้นของ Office, เส้นแผนภูมิขนาดเล็กหายไป และ Excel แจ้งเตือนขอ "ซ่อมแซม" ไฟล์ ทั้งที่ไม่มีส่วนใดในรหัสโปรแกรมของคุณไปแตะต้องฟีเจอร์เหล่านั้นเลย แต่ตัวไลบรารีทำลายมันไปเพียงเพราะกระบวนการสั่งบันทึกธรรมดา

ทำไมไฟล์ Excel จึงสูญเสียการจัดรูปแบบหลังการแก้ไขด้วยไลบรารี?

ไฟล์ Excel สูญเสียการจัดรูปแบบหลังการแก้ไขด้วยไลบรารีเนื่องจากไลบรารีส่วนใหญ่ไม่ได้แก้ไขไฟล์จริง — แต่มันใช้วิธีสร้างไฟล์ขึ้นใหม่ทั้งหมด แพ็กเกจ .xlsx คือไฟล์ ZIP ของชิ้นส่วน XML: เช่น xl/workbook.xml, ไฟล์ xl/worksheets/sheetN.xml หนึ่งไฟล์ต่อหนึ่งแผ่นงาน, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml และอื่นๆ ไลบรารีทั่วไปจะวิเคราะห์ชิ้นส่วนเหล่านั้นให้กลายเป็นแบบจำลองออบเจกต์เมื่อเปิดไฟล์ และสร้างทุกชิ้นส่วนขึ้นใหม่จากแบบจำลองนั้นเมื่อบันทึก ฟีเจอร์ใดๆ ที่แบบจำลองไม่มีตัวแปรแทน — เช่น ธีมที่ไม่เคยถูกวิเคราะห์ หรือบล็อกส่วนต่อขยายจากโปรแกรม Excel เวอร์ชันที่ใหม่กว่า — จะไม่มีที่อยู่การจัดเก็บในหน่วยความจำ ส่งผลให้ชิ้นส่วนที่สร้างขึ้นใหม่ละทิ้งข้อมูลเหล่านั้นไปอย่างเงียบๆ

มาตรฐาน ECMA-376 คาดการณ์ปัญหานี้ไว้แล้วครึ่งหนึ่ง โดย SpreadsheetML กำหนดให้ extLst (ECMA-376 Part 1 "Future Feature Data Storage Area" ตามหัวข้อ §18.2.10 สำหรับอิลีเมนต์ขอบเขตสมุดงาน) เป็นจุดต่อขยายที่ระบุเฉพาะ: โปรแกรมสร้างไฟล์รุ่นใหม่กว่าจะฝากฟีเจอร์ไว้ที่นั่น โดยแต่ละตัวจะถูกห่อหุ้มในอิลีเมนต์ <ext> ที่มีคุณลักษณะ uri ระบุฟีเจอร์นั้น และโปรแกรมอ่านรุ่นเก่าได้รับการคาดหวังให้คงรักษาข้อมูลที่พวกมันไม่เข้าใจไว้ เส้นแผนภูมิขนาดเล็ก ตัวแบ่งข้อมูล (slicer) และประเภทการจัดรูปแบบตามเงื่อนไขใหม่ๆ ล้วนส่งผ่านข้อมูลด้วยวิธีนี้ ไลบรารีที่ละทิ้งบล็อก <ext> ที่ไม่รู้จักจึงไม่ใช่เพียงแค่การสูญเสียข้อมูลธรรมดา — แต่มันละเมิดข้อตกลงความเข้ากันได้ย้อนหลัง (forward-compatibility) ที่รูปแบบไฟล์นี้ถูกออกแบบมารองรับ คำถามสำหรับสเปรดชีตไลบรารีใดๆ ที่คุณกำลังประเมินจึงเป็นเรื่องตรงไปตรงมา: หากฉันแก้ไขข้อมูลเซลล์เดียว มีสิ่งอื่นใดเปลี่ยนแปลงอีกหรือไม่

HotXLS รักษาธีมแบบกำหนดเองแบบไบต์ต่อไบต์ได้อย่างไร?

HotXLS รักษาธีมของสมุดงานโดยการแคชข้อมูลไบต์ดั้งเดิมของ xl/theme/theme1.xml ณ เวลาเปิดไฟล์ และเขียนบันทึกข้อมูลนั้นกลับไปตามเดิมทุกประการเมื่อสั่งบันทึก ชิ้นส่วนธีม (ECMA-376 Part 1, §14.2.7) คือโครงสร้าง DrawingML ไม่ใช่ SpreadsheetML — เช่น รูปแบบสี รูปแบบฟอนต์ รูปแบบการจัดวาง — และเอนจินสเปรดชีตไม่มีความจำเป็นต้องสร้างแบบจำลองการเข้าถึงเชิงลึก HotXLS เวอร์ชันก่อนหน้าจะสร้างธีม Office ที่กำหนดค่าตายตัวขึ้นใหม่ในทุกการบันทึก ซึ่งทำให้เกิดข้อผิดพลาดสีของแบรนด์เปลี่ยนกลับตามที่กล่าวข้างต้น แต่ตั้งแต่เวอร์ชัน v2.89.46 เป็นต้นมา ธีมของแพ็กเกจที่เปิดจะถูกเก็บในรูปแบบดิบและส่งออกใหม่โดยไม่มีการแตะต้อง ส่วนธีม Office ในตัวจะถูกสร้างขึ้นเฉพาะสำหรับสมุดงานที่สร้างขึ้นใหม่ตั้งแต่ต้นเท่านั้น ข้อมูลไบต์ดิบเป็นการรับประกันความถูกต้องที่ดีที่สุด: ไม่มีการวิเคราะห์ ไม่มีกระบวนการแปลงอนุกรมใหม่ และไม่มีโอกาสเกิดความผิดเพี้ยนของข้อมูล

การแคชข้อมูลแบบดิบจะอยู่เหนือการแก้ไขธีมผ่านโปรแกรม TXLSXWorkbook จะอนุญาตให้เข้าถึง ThemeMajorFont และ ThemeMinorFont เพื่อกำหนดรูปแบบฟอนต์ของสมุดงานใหม่ได้ แต่เมื่อมีการแคชข้อมูลธีมจากไฟล์ที่เปิดอยู่ ฟังก์ชันเหล่านั้นจะไม่มีผลต่อการบันทึก — เนื่องจากระบบจัดลำดับความสำคัญของกระบวนการแปลงย้อนกลับก่อนเป็นสำคัญ หากคุณต้องการปรับแต่งธีมของสมุดงาน แนะนำให้ทำการแก้ไขที่ไฟล์เทมเพลตบนโปรแกรม Excel โดยตรงแทนการแก้ไขผ่าน API ข้อมูล ในการทำงานทั่วไปจึงไม่จำเป็นต้องใช้ API ส่วนนี้เลย:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // การแก้ไขเพียงจุดเดียว
    Book.SaveAs('branded-invoice-out.xlsx');
    // ไฟล์ theme1.xml ในเอาต์พุตมีค่าไบต์ตรงกันทุกประการกับไฟล์อินพุต
  finally
    Book.Free;
  end;
end;

เกิดอะไรขึ้นกับบล็อก extLst ที่ไม่รู้จักเมื่อสั่งบันทึก?

HotXLS จะจับข้อมูลบล็อก <ext> ระดับแผ่นงานที่มันไม่มีแบบจำลองรองรับและนำมาเขียนบันทึกซ้ำในส่วน extLst ของแผ่นงานที่บันทึก ทำให้ฟีเจอร์ที่สร้างโดย Excel เวอร์ชันใหม่สามารถผ่านกระบวนการแปลงข้อมูลไปกลับได้ตามปกติ ตั้งแต่เวอร์ชัน v2.131.0 ชิ้นส่วนที่จับข้อมูลได้จะแสดงผลผ่านคุณสมบัติอ่านอย่างเดียว RawWorksheetExts ซึ่งเป็น TStringList ของแต่ละแผ่นงาน ซึ่งช่วยให้การตรวจสอบความถูกต้องทำงานได้ในขั้นตอนทดสอบโค้ดจริง:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d บล็อกส่วนต่อขยายระบบภายนอกที่จับข้อมูลได้',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // ตรวจสอบค่า uriแต่ละรายการ
  finally
    Book.Free;
  end;
end;

รายละเอียดการติดตั้งที่สำคัญคือ การจับข้อมูลนี้จะเป็นการแปลงอนุกรมใหม่ในระดับเหตุการณ์ (event-level re-serialization) ไม่ใช่การคัดลอกไบต์ดิบ ตัวอ่านสตรีมมิ่ง XML ของ HotXLS ไม่แสดงค่าระยะออฟเซ็ตต้นทาง โครงสร้างย่อยที่ไม่รู้จักจึงถูกสร้างขึ้นใหม่จากเหตุการณ์ Element, Text และ EndElement ที่ไหลผ่านเข้ามา วิธีนี้มีกับดักอย่างหนึ่งซ่อนอยู่: อิลีเมนต์ที่ปิดในตัว เช่น <a/> จะทริกเกอร์เฉพาะเหตุการณ์ Element ที่ระบุสถานะว่างเปล่า และจะไม่ส่งเหตุการณ์ EndElement เลย ดังนั้นตัวนับความลึกที่ลดค่าเฉพาะเมื่อพบ EndElement จะไม่มีวันรับรู้จุดสิ้นสุดของโครงสร้างย่อยได้ เมื่อจัดการปัญหานี้ได้ ชิ้นส่วนข้อมูลที่สร้างใหม่จะมีความสมมูลเชิงตรรกะกับข้อมูลดั้งเดิม — การกำหนดอัญประกาศของคุณลักษณะและการปิดตัวเองจะถูกปรับปรุงให้เป็นมาตรฐาน จึงอาจไม่ได้ค่าไบต์ตรงกันทุกประการ แต่โปรแกรม Excel จะอ่านความหมายไม่ใช่ไบต์ดิบ คุณสมบัติสองประการของเอาต์พุตของ Excel ช่วยให้กระบวนการส่งออกนี้ปลอดภัย: Excel จะประกาศแอตทริบิวต์ xmlns ที่จำเป็นไว้บนอิลีเมนต์ <ext> หรือภายในนั้น ทำให้แต่ละชิ้นส่วนที่จับข้อมูลได้มีเนมสเปซในตัวเอง และการมีเนมสเปซในตัวเองนี้เป็นเหตุผลว่าทำไมการทำซ้ำแผ่นงานภายในหรือข้ามสมุดงาน จึงสามารถนำบล็อกภายนอกติดไปได้ด้วยผ่านการมอบหมายรายการสตริงธรรมดา

การเขียน calcChain.xml เพื่อให้ Excel เชื่อถือสูตรของคุณ

HotXLS จะเขียนไฟล์ xl/calcChain.xml ( Calculation Chain ตามมาตรฐาน ECMA-376 Part 1, §12.3.1) ทุกครั้งเมื่อสมุดงานที่บันทึกมีสูตรคำนวณ โดยจะเลือกโครงสร้างลำดับสองแบบ หากแผนภูมิลำดับการขึ้นต่อกันของสูตรถูกสร้างขึ้นและทำงานอยู่ — เช่น คุณเรียกใช้ Recalculate หลังแก้ไขข้อมูลล่าสุด — ห่วงโซ่จะถูกส่งออกตามลำดับทอพอโลยี โครงสร้างอ้างอิงก่อนหน้าจะอยู่ก่อนตัวที่ขึ้นต่อกัน โดยมีสมาชิกของวงจรอ้างอิงแนบต่อท้ายในส่วนท้ายสุด นอกเหนือจากนั้น เซลล์จะแสดงตามลำดับที่พบในเอกสาร ซึ่งทั้งสองรูปแบบมีความถูกต้อง: เอกสารการออกแบบของไมโครซอฟท์ [MS-XLSX] กำหนดให้ห่วงโซ่เป็นคำแนะนำที่ Excel จะตรวจสอบและจัดลำดับใหม่เมื่อเปิดไฟล์ ดังนั้นการแสดงรายการสูตรที่สมบูรณ์จึงถูกต้องตามไวยากรณ์ และ HotXLS จะไม่บังคับให้รันสร้างแผนภูมิขณะเรียกใช้ SaveAs — เนื่องจากความเร็วในการดึงเส้นขอบมีอัตรากำลังสองเทียบกับจำนวนเซลล์ ซึ่งเป็นภาระที่แฝงมาสูงเกินไปสำหรับการบันทึกเซลล์ระดับล้านเซลล์

Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// บันทึกตอนนี้ calcChain.xml จะแสดงเซลล์สูตรเรียงตามลำดับเอกสาร
// หลัง Recalculate จะมีแผนภูมิลำดับการขึ้นต่อกันอยู่ การบันทึกเดิม
// จะส่งออกลำดับทอพอโลยีฉบับเต็มแทน:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

ทำไมต้องใส่ใจชิ้นส่วนข้อมูลที่ Excel ถือว่าเป็นเพียงข้อมูลแนะนำ? คำตอบคือเพราะการขาดหายไปของมันเป็นสัญญาณบ่งชี้ โปรแกรมอ่านบางราย — เช่น ระบบซ่อมแซมเชิงประเมิน ตัวอ่านของระบบภายนอก หรือเครื่องมือเปรียบเทียบผลต่าง — คาดหวังจะพบห่วงโซ่การคำนวณในสมุดงานที่มีสูตร และไลบรารีที่ละทิ้งชิ้นส่วนนี้ไปขณะสั่งบันทึกจะสร้างไฟล์ที่มีพฤติกรรมบางอย่างไม่เหมือนกับไฟล์ที่ Excel เขียนขึ้น การส่งออกห่วงโซ่ที่ถูกต้องจะช่วยรักษาผลลัพธ์ให้อยู่ภายใต้กรอบการทำงานที่ระบบนิเวศอื่นๆ ได้ทำการทดสอบรองรับ ซึ่งนี่คืองานพื้นฐานสำคัญของกระบวนการวิศวกรรมการแปลงข้อมูลไปกลับ (round-trip engineering)

จุดสิ้นสุดความถูกต้องของการแปลงข้อมูลย้อนกลับ

ความตรงไปตรงมามีความสำคัญอย่างมากในหัวข้อนี้ ขอบเขตการทำงานจึงเป็นสิ่งที่คุณควรระบุไว้ HotXLS ไม่ได้ทำการคัดลอกไฟล์ทั้งแพ็กเกจแบบไบต์ต่อไบต์: ชิ้นส่วนแผ่นงาน XML, รูปแบบสไตล์, สตริงข้อมูลร่วม และชิ้นส่วนสมุดงานจะถูกสร้างขึ้นใหม่จากแบบจำลองที่วิเคราะห์ได้ ดังนั้นผลลัพธ์เอาต์พุตจะมีความสมมูลเชิงตรรกะแต่ไม่ใช่ความเหมือนกันทุกบิตของไฟล์ฐานสอง — เฉพาะตัวส่วนควบคุม ZIP ท้องถิ่นจะเก็บค่าประทับเวลาของ DOS ใหม่ ชิ้นส่วน <ext> ที่จับข้อมูลได้จะส่งกลับมาแบบปรับเป็นมาตรฐานตามที่กล่าวข้างต้น การกำหนดรูปแบบฟอนต์ธีมผ่านโปรแกรมจะถูกละเว้นไปเมื่อมีโครงสร้างธีมตามแบบดั้งเดิมค้างอยู่ และขอบเขตการเก็บรักษามีการกำหนดไว้อย่างชัดเจน: ได้แก่ ฟีเจอร์ที่ HotXLS ออกแบบโมเดลไว้ในตัว (เช่น เส้นแผนภูมิขนาดเล็ก จะถูกวิเคราะห์และเขียนใหม่แทนการคัดลอกตรงๆ) บวกกับเนื้อหา extLst ของระบบภายนอก และชิ้นส่วนที่แคชข้อมูลดั้งเดิมไว้ ชิ้นส่วนใดๆ ที่ไม่มีแบบจำลองรองรับและไม่ได้อยู่ภายใต้จุดต่อขยาย — เช่น ชิ้นส่วนเฉพาะของโปรแกรมเสริมอื่นๆ — จะอยู่นอกเหนือกลไกทั้งสามตัวนี้ ดังนั้นโปรดทำการทดสอบกับไฟล์จริงของคุณแทนการทึกทักเอาเอง

งานรักษาสภาพแวดล้อมข้างเคียงอื่นๆ ช่วยให้ภาพรวมมีความสมบูรณ์ โปรเจกต์ VBA และการอ้างอิงสมุดงานภายนอกจะผ่านกระบวนการบันทึกได้ภายใต้ปรัชญา "เก็บสิ่งที่คุณไม่มีตัวแบบรองรับ" เช่นเดียวกัน ซึ่งจะอธิบายไว้ในบทความคู่ขนานเกี่ยวกับการรักษาโปรเจกต์ VBA และการเชื่อมโยงภายนอก และคุณสมบัติของเอกสารภายใต้ docProps จะมี API สำหรับการเขียนอ่านของตัวเอง แทนการถูกละทิ้งไปอย่างเงียบๆ เมื่อคุณประเมินคุณภาพของสเปรดชีตไลบรารีใดๆ ให้ทำการทดสอบการแก้ไขเซลล์เดียว: โดยเปิดสมุดงานใช้งานจริงที่มีฟีเจอร์ซับซ้อน แก้ไขค่าข้อมูลเพียงเซลล์เดียว สั่งบันทึก และเปรียบเทียบไฟล์ XML ที่แตกซิปออกมาเทียบกับไฟล์ดั้งเดิม ข้อมูลจุดอื่นที่เปลี่ยนแปลงไปนอกเหนือจากแผ่นงานที่คุณแก้ไขจะบอกรายละเอียดของไลบรารีนั้นได้ดีกว่าตารางสรุปฟีเจอร์การทำงาน

กลไกการแปลงข้อมูลไปกลับที่อธิบายไว้ในบทความนี้ — ได้แก่ การแคชรักษาธีมดั้งเดิมตั้งแต่เวอร์ชัน v2.89.46, การจับข้อมูล extLst ภายนอกและการส่งออก calcChain.xml ตั้งแต่เวอร์ชัน v2.131.0 — จัดส่งมาในชุดของ HotXLS Delphi Excel Component ซึ่งบนหน้าผลิตภัณฑ์จะรวบรวมข้อมูลฟีเจอร์การอ่านเขียนไฟล์ XLSX ไว้อย่างครบถ้วนสำหรับ Delphi และ C++Builder