สเปรดชีตมีคอลัมน์ชื่อลูกค้า บางรายเป็นภาษาจีน บางรายเป็นซีริลลิก และมีบางรายที่ใช้เครื่องหมายอุมเลาต์ของภาษาเยอรมันหรือเครื่องหมายเน้นเสียงของภาษาฝรั่งเศส คุณส่งออกเป็น CSV และเปิดดูผลลัพธ์ อักขระทุกตัวจะครบถ้วนสมบูรณ์ คุณส่งออกเวิร์กบุ๊กเดียวกันไปยัง RTF เพื่อเป็นเทมเพลตจดหมายเวียน เปิดมันในโปรแกรมประมวลผลคำ แล้วพบว่าชื่อที่ไม่ใช่ ASCII ได้พังทลายลงกลายเป็นแถวของเครื่องหมายคำถาม ข้อมูลนั้นไม่เคยเปลี่ยน สิ่งที่เปลี่ยนไปคือสัญญาในการเข้ารหัสของรูปแบบที่คุณเขียน และแต่ละเส้นทางการส่งออกก็มีรูปแบบที่แตกต่างกันไป
นี่คือกับดักที่ดักจับไลบรารีที่ดูเผินๆ เหมือนว่ารองรับ Unicode อย่างสมบูรณ์ ข้อความในเซลล์ถูกจัดเก็บไว้ภายในเป็น WideString ดังนั้นโมเดลจึงไม่มีทางสูญเสียอักขระใดๆ ไป การสูญเสียเกิดขึ้นที่บริเวณรอยต่อ ในตัวเขียนที่ต้องจัดลำดับข้อความนั้นให้อยู่ในรูปแบบที่มีกฎของตัวเองเกี่ยวกับไบต์ที่ถูกต้อง และสิ่งใดก็ตามที่อยู่นอกเหนือช่วงที่ถูกต้องจะต้องถูกเข้ารหัสอย่างไร ทำให้ตัวเขียนตัวหนึ่งทำงานได้ถูกต้องและคุณก็ยังคงสามารถส่งออกอีกตัวที่ทำให้ข้อความเดียวกันผิดเพี้ยนไปได้ วิธีแก้ไขไม่ใช่การใช้สวิตช์แบบโกลบอล แต่มันคือการตัดสินใจที่ถูกต้องและแยกต่างหากในทุกๆ เส้นทาง
RTF เป็นรูปแบบ 7-bit-safe โดยการออกแบบ
Rich Text Format มีมาก่อน Unicode และถูกระบุให้อยู่รอดจากการขนส่งที่ผ่านได้เฉพาะ ASCII ที่พิมพ์ได้เท่านั้น เอกสาร RTF ประกาศโค้ดเพจในส่วนหัวของมัน และอักขระใดๆ ที่ตัวเขียนไม่สามารถแสดงได้ในโค้ดเพจนั้นจะต้องถูกส่งออกมาเป็น escape แทนที่จะเป็นไบต์ดิบ escape ที่เกี่ยวข้องคือ \u ซึ่งจะมีหน่วยรหัส 16 บิตที่มีเครื่องหมายตามด้วยตัวอักษรทางเลือกสำรอง (fallback character) ในรูปแบบ ASCII สำหรับโปรแกรมอ่านที่เก่าเกินกว่าจะเข้าใจ escape ได้ทั้งหมด
HotXLS เขียน RTF ด้วยวิธีนี้ ส่วนหัวของเอกสารเริ่มต้นด้วยการประกาศโค้ดเพจในรูปแบบ \ansi\ansicpg1252\uc1 และตัวเขียนในยูนิต lxRTF จะเดินตามทุกสตริงที่ปล่อยอักขระใดๆ ที่อยู่เหนือ plain ASCII เป็น escape \u เพื่อให้สตรีมไบต์ยังคงสะอาดที่ระดับ 7 บิต ไม่ว่าโค้ดเพจที่ประกาศไว้จะสามารถเก็บอะไรได้บ้างก็ตาม โค้ดพอยต์ (code point) อย่างเช่น U+4E2D จะกลายเป็นลำดับที่แท้จริงคือ \u20013? ไม่ใช่ไบต์ดิบที่โปรแกรมดูจะพยายามตีความผ่านโค้ดเพจใดๆ ที่มันทึกทักเอาเอง หากไม่มีระเบียบวินัยนี้ สิ่งใดก็ตามที่อยู่นอกเหนือโค้ดเพจที่ประกาศจะไม่มีการนำเสนอไบต์ที่ถูกต้อง และตัวเขียนที่ปล่อยค่าดิบออกมาจะสร้างเครื่องหมายคำถามตามที่ได้กล่าวไว้ในตอนต้นของบทความนี้
รายละเอียดที่ต้องจำไว้ก็คือ โค้ดเพจที่ประกาศและ escape เป็นสองส่วนของสัญญาฉบับเดียวกัน การประกาศเพียงโค้ดเพจอย่างเดียวไม่สามารถช่วยข้อความที่อยู่นอกเหนือโค้ดเพจนั้นได้ การส่งออก escape โดยไม่มีการประกาศโค้ดเพจ จะทำให้อักขระสำรองมีความคลุมเครือ ทั้งสองส่วนต้องถูกต้องไปพร้อมกัน ซึ่งเป็นเหตุผลที่ว่าทำไมตัวเขียนที่จัดการเพียงส่วนใดส่วนหนึ่งก็ยังคงล้มเหลวในการจัดการกับเวิร์กบุ๊กหลายภาษาตัวแรก
การหลีกเลี่ยง (escaping) ใน HTML มีอะไรมากกว่าแค่วงเล็บมุม
การส่งออกเป็น HTML จะสร้างเอกสารแบบหลายชีตซึ่งเฟรมการนำทางจะแสดงชื่อชีตเป็นข้อความที่มองเห็นได้ ชื่อเหล่านั้นคือสตริงที่ผู้แต่งเป็นผู้ควบคุม ซึ่งสามารถมีอักขระใดๆ ก็ได้ รวมไปถึงตัวที่มีความสำคัญต่อมาร์กอัปด้วย ชีตที่ตั้งชื่อตรงๆ ว่า Q1 & Q2 <draft> จะต้องไปถึงหน้าเว็บในรูปแบบเอ็นทิตีที่ถูกจัดการการหลีกเลี่ยง (escaped entities) มิฉะนั้นวงเล็บมุมก็จะเปิดแท็กผี และเครื่องหมายและ (ampersand) ก็จะเริ่มต้นการอ้างอิงเอ็นทิตีที่ไม่ได้ตั้งใจให้เป็น นี่คือการทำ escaping ใน HTML แบบธรรมดา และการข้ามขั้นตอนนี้บนเลเบลของเฟรม ถือเป็นความล้มเหลวแบบหนึ่งที่สามารถผ่านได้ทุกการทดสอบที่สร้างจากชื่อชีตที่มีแต่ตัวอักษร ASCII เท่านั้น
คำถามเกี่ยวกับการเข้ารหัสอยู่ลึกลงไปอีกหนึ่งชั้น เมื่ออักขระที่ไม่ใช่ ASCII ตกอยู่ในบริบทที่ไม่สามารถรับประกันได้ว่าจะให้บริการในรูปแบบ UTF-8 ตัวแทนที่ปลอดภัยคือการอ้างอิงอักขระตัวเลข ดังนั้น U+00E9 จึงเขียนเป็น é แทนที่จะเป็นไบต์ดิบซึ่งความหมายขึ้นอยู่กับ charset ของการตอบสนอง ภาพสะท้อนของกฎนี้ใช้กับทางเข้าเช่นกัน เวิร์กบุ๊กที่อ่านกลับจาก XLSX มี shared strings ซึ่งอักขระอาจถูกจัดเก็บไว้แล้วในฐานะเอ็นทิตี XML ที่เป็นตัวเลข และเอ็นทิตีนั้นจะต้องถูกถอดรหัสเป็นหนึ่งอักขระที่สมบูรณ์ก่อนเข้าสู่โมเดลเซลล์ การถอดรหัสอย่างไม่ระมัดระวัง การแยกโค้ดพอยต์ออกเป็นไบต์ที่แยกจากกัน จะทำให้อักขระเดี่ยวปรากฏขึ้นใหม่เป็น mojibake สองชิ้น ซึ่งไม่มีการส่งออกใดๆ ในภายหลังที่สามารถซ่อมแซมได้
คอนเทนเนอร์ XLSX คือ ZIP และ ZIP มีการเข้ารหัสชื่อเป็นของตัวเอง
ไฟล์ XLSX คือคลังเก็บข้อมูลแบบ ZIP และคลังนี้จะจัดเก็บชื่อของทุกสมาชิกที่มีอยู่ภายใน ZIP เก่าพอที่ข้อกำหนดดั้งเดิมของมันไม่ได้กล่าวถึงการเข้ารหัสของชื่อเหล่านั้น ดังนั้นตัวอ่านที่ไม่พบสัญญาณใดๆ จะถือเอาว่าใช้โค้ดเพจภายในของคลังข้อมูลนั้น สมมติฐานนั้นจะผิดทันทีเมื่อชื่อของสมาชิกประกอบด้วยอักขระที่ไม่ใช่ ASCII ซึ่งมักจะเกิดขึ้นกับชื่อส่วนประกอบของเวิร์กชีตที่ถูกจัดทำเป็นภาษาท้องถิ่น และกับสื่อที่ฝังไว้ซึ่งมีชื่อไฟล์ที่มีเครื่องหมายเน้นเสียงหรือเป็นสคริปต์ที่ไม่ใช่ภาษาละติน
วิธีแก้ไขมีเพียงบิตเดียว บิตวัตถุประสงค์ทั่วไปที่ 11 ในส่วนหัวของไฟล์โลคัลแต่ละตัว จะประกาศว่าชื่อของสมาชิกถูกเข้ารหัสเป็น UTF-8 HotXLS จะตรวจสอบบิตนั้นอย่างแน่ชัดเมื่ออ่านคลังข้อมูล โดยทำการทดสอบธงวัตถุประสงค์ทั่วไปเทียบกับมาสก์ $0800 และตัวอ่านหรือตัวเขียนที่ละเว้นบิตนี้จะอ่านชื่อที่ถูกจัดการและจัดเก็บเป็น UTF-8 อย่างถูกต้องผิดเพี้ยนไป บิตนี้ง่ายต่อการตั้งค่าและง่ายต่อการปฏิบัติตาม และนี่คือความแตกต่างทั้งหมดระหว่างชื่อสมาชิกที่รอดพ้นจากการเดินทางไปกลับ กับชื่อที่ได้รับความเสียหายก่อนที่เนื้อหาของสเปรดชีตจะถูกแยกวิเคราะห์เสียอีก
การพับตัวพิมพ์ (case folding) และการสแกนตัวเลขซ่อนอันตรายแบบเดียวกัน
การประเมินสูตรคือจุดที่ความปลอดภัยของ Unicode สิ้นสุดการเป็นเรื่องของการจัดลำดับความสำคัญและกลายเป็นเรื่องของการเปรียบเทียบ ฟังก์ชัน SEARCH ไม่แยกความแตกต่างระหว่างตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ ซึ่งหมายความว่ามันจะต้องพับตัวพิมพ์ก่อนที่จะค้นหาสตริงย่อย วิธีการพับที่ผิดคือการทำผ่าน ANSI code page เนื่องจากกระบวนการแปลงข้อความที่ไม่ใช่ ASCII เป็นตัวพิมพ์ใหญ่ด้วยวิธีนี้ จะนำอักขระผ่านโค้ดเพจที่แคบและทำให้สิ่งใดก็ตามที่อยู่นอกเหนือจากนั้นได้รับความเสียหาย วิธีที่ถูกต้องคือการแปลงสตริงแบบกว้าง (wide-string) ให้เป็นตัวพิมพ์ใหญ่ ซึ่งจะรักษาระยะของ UTF-16 ไว้ได้ทั้งหมด HotXLS จะพับตัวพิมพ์ด้วย WideUpperCase ด้วยเหตุผลนี้โดยเฉพาะ เพื่อให้การค้นหาข้อความที่มีเครื่องหมายเน้นเสียงหรือสคริปต์ที่ไม่ใช่ภาษาละติน สามารถจับคู่อักขระเดียวกันกับที่ถูกป้อนเข้ามา แทนที่จะเป็นการประมาณค่าที่ถูกทำให้ยุ่งเหยิงโดยโค้ดเพจ
ตัวสร้างโทเค็นของสูตร (formula tokenizer) มีหน้าที่ที่เกี่ยวข้องซึ่งไม่เกี่ยวอะไรกับตัวอักษรเลย แต่เกี่ยวพันกับจุดที่โทเค็นสิ้นสุดลง สัญกรณ์วิทยาศาสตร์ (scientific notation) เช่น 1E3 หรือ 2.5E-3 เป็นค่าที่เป็นตัวเลขเดี่ยวๆ (single numeric literal) และเครื่องสแกนจะต้องจดจำ E เครื่องหมายตัวเลือก และตัวเลขที่ตามมาให้เป็นส่วนหนึ่งของตัวเลข แทนที่จะแบ่งอินพุตออกเป็นชื่อแล้วตามด้วยตัวเลขที่แยกจากกัน เครื่องสแกนที่จัดการเรื่องนี้ผิดพลาดจะเปลี่ยนค่าคงที่ที่ถูกต้องสมบูรณ์ให้กลายเป็นข้อผิดพลาดในการแยกวิเคราะห์ หรือแย่กว่านั้นคือนิพจน์ที่ผิดพลาดแบบเงียบๆ สิ่งนี้ถูกจัดอยู่ในหัวข้อสนทนาเดียวกัน เนื่องจากทั้งสองกรณีเป็นเรื่องของตัวอ่านที่ต้องตัดสินใจในระดับอักขระได้อย่างถูกต้อง: เรื่องแรกคือวิธีพับตัวพิมพ์เพื่อเปรียบเทียบ ส่วนเรื่องที่สองคือการที่อักขระใดๆ จะสืบต่อโทเค็นในปัจจุบันหรือไม่
การสร้างและการส่งออกเวิร์กบุ๊กแบบหลายภาษา
API สาธารณะไม่ได้เรียกร้องให้คุณต้องมาคิดเกี่ยวกับสิ่งเหล่านี้เลย คุณสามารถสร้างเวิร์กบุ๊กจากค่าในเซลล์ที่เป็น WideString และเรียกใช้จุดเข้าใช้งานสำหรับการส่งออกตามที่คุณต้องการ การตัดสินใจเกี่ยวกับการเข้ารหัสเกิดขึ้นภายในตัวเขียนแต่ละตัว ตัวอย่างด้านล่างนี้เป็นการกำหนดชีตด้วยข้อความในสคริปต์หลายภาษา จากนั้นจึงเขียนไฟล์ RTF และไฟล์ HTML จากเวิร์กบุ๊กเดียวกัน ดังนั้นทั้งสองเส้นทางนี้จึงทำงานกับอินพุตที่เหมือนกันทุกประการ
uses
lxHandle;
procedure ExportMultilingualWorkbook;
var
Book: IXLSWorkbook;
Sheet: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add('Customers');
Sheet.Cells[1, 1].Value := 'Name';
Sheet.Cells[1, 2].Value := 'City';
// Cell text is held as WideString, so every script survives the model.
Sheet.Cells[2, 1].Value := '王伟'; // Chinese
Sheet.Cells[2, 2].Value := '北京';
Sheet.Cells[3, 1].Value := 'Müller'; // German umlaut
Sheet.Cells[3, 2].Value := 'Köln';
Sheet.Cells[4, 1].Value := 'Иванов'; // Cyrillic
Sheet.Cells[4, 2].Value := 'Москва';
Sheet.Cells[5, 1].Value := 'Désirée'; // French accents
Sheet.Cells[5, 2].Value := 'Montréal';
// RTF: the lxRTF writer declares the code page and emits every
// non-ASCII character as a \u escape, keeping the file 7-bit clean.
Book.SaveAsRTF('Customers.rtf');
// HTML: sheet names are HTML-escaped and non-ASCII text is written
// so it does not depend on a guessed response charset.
Book.SaveAsHTML('Customers.html');
finally
Book := nil;
end;
end;
การเรียกใช้ทั้งสองแบบจะคืนค่าสถานะเป็น Integer และทั้งคู่ใช้ข้อความในหน่วยความจำ (in-memory text) ชุดเดียวกัน ไม่มีตรงไหนในโค้ดเรียกใช้งานที่จะประกาศโค้ดเพจหรือการหลีกเลี่ยง (escape) อักขระ เพราะความรับผิดชอบนี้ตกอยู่กับตัวเขียนซึ่งรู้จักรูปแบบของตนเองดี SaveAsCSV ในระดับเวิร์กบุ๊กก็จะทำตามรูปแบบเดียวกันนี้ หากคุณต้องการส่งออกข้อมูลแบบมีตัวคั่นจากแหล่งที่มาที่เหมือนกัน
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
ความปลอดภัยของ Unicode เป็นแบบต่อเส้นทาง ไม่ใช่ต่อไลบรารี
บทเรียนที่คุ้มค่าแก่การนำกลับไปใช้ก็คือ ไม่มีที่ใดเพียงแห่งเดียวที่จะปลอดภัยสำหรับ Unicode RTF ต้องการการประกาศโค้ดเพจควบคู่ไปกับ escape \u HTML ต้องการการจัดการกับเอนทิตีที่หลบเลี่ยงสำหรับอักขระที่มีความสำคัญต่อมาร์กอัป และการอ้างอิงตัวเลขในกรณีที่ไม่มีการรับประกันของ charset รวมถึงการถอดรหัสเอ็นทิตีที่มาพร้อมกับ shared strings ได้อย่างถูกต้อง คอนเทนเนอร์ ZIP ต้องการการตั้งค่าบิตวัตถุประสงค์ทั่วไปที่ 11 เพื่อให้ชื่อของสมาชิก UTF-8 ถูกอ่านเป็น UTF-8 การประเมินสูตรต้องการการพับตัวพิมพ์ของสตริงแบบกว้าง (wide-string case folding) และตัวสร้างโทเค็นที่ช่วยให้สัญกรณ์วิทยาศาสตร์คงความเป็นหนึ่งเดียวกัน สิ่งเหล่านี้ล้วนเป็นข้อสัญญาที่แตกต่างกัน และไลบรารีก็สามารถปฏิบัติตามสัญญาข้อหนึ่งในขณะที่ฝ่าฝืนอีกข้อหนึ่งอย่างเงียบๆ นั่นคือเหตุผลว่าทำไมเครื่องมือที่ใช้งาน CSV ได้อย่างถูกต้อง จึงยังคงส่งมอบ RTF ที่เต็มไปด้วยเครื่องหมายคำถามให้กับคุณได้
หากการส่งออกของคุณพึ่งพารูปแบบที่มีตัวคั่น ข้อดีข้อเสียระหว่างกันจะถูกครอบคลุมอยู่ใน คำแนะนำทีละขั้นตอนของเราสำหรับการส่งออก CSV, TSV และ HTML และเมื่อแหล่งที่มาคือชุดผลลัพธ์ (result set) แทนที่จะเป็นชีตที่สร้างขึ้นด้วยมือ รูปแบบใน การส่งออกฐานข้อมูลสำหรับรายงานของ Delphi ก็จับคู่ได้อย่างลงตัวกับกฎการเข้ารหัสที่อธิบายไว้ที่นี่ ทั้งหมดนี้รวมอยู่ใน คอมโพเนนต์ HotXLS สำหรับ Delphi และ C++Builder ควบคู่ไปกับ API ด้านการอ่าน, สูตร และการจัดรูปแบบที่ครอบคลุมอยู่ในที่อื่นในบล็อกนี้