สเปรดชีตหนึ่งเก็บคอลัมน์รายชื่อลูกค้า บางชื่อเป็นภาษาจีน บางชื่อเป็นอักษรซีริลลิก และบางชื่อมีเครื่องหมายการออกเสียงภาษาเยอรมันหรือฝรั่งเศส คุณส่งออกข้อมูลนั้นเป็นไฟล์ CSV และเปิดผลลัพธ์ดู และพบว่าทุกตัวอักษรยังคงความสมบูรณ์ แต่เมื่อคุณส่งออกสมุดงานเดียวกันเป็นไฟล์ RTF สำหรับเป็นเทมเพลตจดหมายเวียน และเปิดดูในโปรแกรมประมวลผลคำ รายชื่อที่ไม่ใช่อักษร ASCII กลับกลายเป็นแถวของเครื่องหมายคำถามไปทั้งหมด ข้อมูลไม่ได้เปลี่ยนแปลง สิ่งที่เปลี่ยนแปลงคือข้อตกลงในการเข้ารหัสของรูปแบบไฟล์ที่คุณเขียน และแต่ละเส้นทางการส่งออกก็มีเกณฑ์การเข้ารหัสที่แตกต่างกันออกไป
นี่คือกัปดักที่คอยดักเล่นงานไลบรารีที่ดูเหมือนว่าจะรองรับยูนิโค้ดอย่างครบถ้วนที่ลักษณะภายนอก ข้อความเซลล์จะถูกเก็บไว้ภายในเป็นประเภท WideString ดังนั้นโมเดลการประมวลผลจึงไม่เคยทำตัวอักษรใดๆ หล่นหาย ความสูญเสียจะเกิดขึ้นที่บริเวณขอบเขต ในขั้นตอนของตัวเขียนซึ่งทำหน้าที่แปลงข้อความเหล่านั้นให้อยู่ในรูปแบบที่มีกฎระเบียบของตัวเองว่าไบต์ใดบ้างที่ถูกต้อง และองค์ประกอบใดๆ ที่อยู่นอกเกณฑ์จะต้องถูกเข้ารหัสอย่างไร พัฒนาตัวเขียนตัวหนึ่งให้ถูกต้อง แต่คุณก็ยังอาจจัดส่งตัวเขียนอีกตัวที่ทำให้ข้อความเดียวกันเกิดความเสียหายได้ วิธีแก้ไขไม่ใช่การปรับเปลี่ยนสวิตช์ระดับสากล แต่มันคือการตัดสินใจแบบแยกส่วนและถูกต้องในทุกๆ เส้นทางการทำงาน
RTF เป็นรูปแบบไฟล์ที่ปลอดภัยสำหรับ 7 บิตโดยการออกแบบ
Rich Text Format (RTF) ถูกคิดค้นขึ้นมาก่อนยุคของยูนิโค้ด และได้รับการกำหนดขึ้นเพื่อให้สามารถรอดพ้นระบบรับส่งข้อมูลที่อนุญาตผ่านเฉพาะตัวอักษร ASCII ที่พิมพ์ได้เท่านั้น เอกสาร RTF จะประกาศรหัสหน้า (code page) ไว้ในส่วนหัว และอักขระใดๆ ที่ตัวเขียนไม่สามารถนำเสนอในรหัสหน้านั้นได้ จะต้องถูกส่งออกมาในรูปแบบของรหัสควบคุม (escape) แทนที่จะเป็นไบต์ดิบ รหัสควบคุมที่เกี่ยวข้องคือ \u ซึ่งจะทำหน้าที่เก็บหน่วยรหัสขนาด 16 บิตแบบมีเครื่องหมาย และตามด้วยตัวอักษร ASCII สำรองสำหรับโปรแกรมอ่านที่มีอายุมากเกินกว่าจะทำความเข้าใจรหัสควบคุมนี้ได้
HotXLS เขียนไฟล์ RTF ด้วยวิธีนี้ ส่วนหัวของเอกสารจะเปิดใช้งานโดยการประกาศรหัสหน้าในรูป \ansi\ansicpg1252\uc1 และตัวเขียนในหน่วยการทำงาน lxRTF จะดำเนินการตรวจสอบทุกสตริงและส่งออกอักขระใดๆ ที่สูงกว่า ASCII ปกติให้อยู่ในรูปของรหัสควบคุม \u เพื่อให้กระแสข้อมูลไบต์อยู่ในรูป 7 บิตอย่างสะอาดเรียบร้อยโดยไม่ต้องคำนึงถึงข้อมูลที่รหัสหน้าที่ประกาศสามารถบันทึกได้ จุดรหัสเช่น U+4E2D จะกลายเป็นลำดับอักษร \u20013? แทนที่จะเป็นไบต์ดิบซึ่งโปรแกรมแสดงผลอาจตีความผ่านรหัสหน้าใดๆ ที่มันคาดเดาขึ้นมาเอง หากไม่มีข้อกำหนดนี้ ข้อมูลใดๆ ที่อยู่นอกรหัสหน้าที่ประกาศจะไม่มีรูปแบบไบต์ที่ถูกต้องตามกฎหมาย และตัวเขียนที่ส่งออกค่าดิบจะผลิตเครื่องหมายคำถามซึ่งเป็นประเด็นหลักของบทความนี้
รายละเอียดที่ต้องระลึกไว้เสมอคือรหัสหน้าที่ประกาศและรหัสควบคุมคือสองซีกของข้อตกลงเดียวกัน การประกาศรหัสหน้าเพียงอย่างเดียวจะไม่สามารถช่วยเหลือข้อความที่อยู่นอกเหนือขอบเขตของมันได้ และการส่งออกรหัสควบคุมโดยไม่มีรหัสหน้าที่ประกาศจะทำให้ตัวอักษรสำรองเกิดความไม่ชัดเจน ทั้งสองส่วนจะต้องมีความถูกต้องร่วมกัน ซึ่งเป็นเหตุผลที่ตัวเขียนที่จัดการเพียงส่วนใดส่วนหนึ่งยังคงล้มเหลวในการทำงานกับสมุดงานแบบหลายภาษา
HTML escaping is about more than angle brackets
การส่งออก HTML จะผลิตเอกสารแบบหลายแผ่นงานซึ่งเฟรมนำทางจะมีชื่อแผ่นงานเป็นข้อความที่แสดงผล ชื่อเหล่านั้นเป็นสตริงที่ผู้ใช้กำหนดซึ่งสามารถเก็บตัวอักษรใดๆ ได้ รวมถึงตัวอักษรที่มีความหมายสำคัญต่อการเขียนโค้ดมาร์กอัปด้วย แผ่นงานที่ตั้งชื่อในรูป Q1 & Q2 <draft> จะต้องถูกแปลงให้เป็นเอนทิตีหลีกเลี่ยง (escaped entities) เมื่อส่งมาที่หน้ากระดาษ มิฉะนั้นวงเล็บมุมจะเปิดแท็กจำลองและเครื่องหมายแอมเพอร์แซนด์จะเริ่มอ้างอิงเอนทิตีโดยไม่ได้ตั้งใจ นี่คือการหลีกเลี่ยงอักขระ HTML ตามปกติ และการละเลยขั้นตอนนี้บนป้ายกำกับเฟรมก็มักจะเป็นจุดตกหล่นที่รอดพ้นการทดสอบผ่านเฉพาะชื่อแผ่นงานที่เป็นภาษาอังกฤษ ASCII
ประเด็นเรื่องการเข้ารหัสจะอยู่ลึกลงไปอีกหนึ่งชั้น เมื่ออักขระที่ไม่ใช่ ASCII ตกอยู่ในบริบทที่ไม่มีการรับประกันว่าจะถูกจัดส่งในรูปแบบ UTF-8 รูปแบบการแสดงผลที่ปลอดภัยคือการใช้อ้างอิงอักขระตัวเลข (numeric character reference) ดังนั้น U+00E9 จึงควรเขียนเป็น é แทนที่จะเป็นไบต์ดิบซึ่งความหมายจะขึ้นอยู่กับชุดอักขระตอบกลับ (response charset) ที่คาดเดา ภาพสะท้อนของกฎข้อนี้จะนำมาใช้ในฝั่งขาเข้า สมุดงานที่อ่านกลับมาจาก XLSX จะเก็บสตริงร่วมกันซึ่งตัวอักษรบางตัวอาจถูกจัดเก็บเป็นเอนทิตี XML แบบตัวเลขไปแล้ว และเอนทิตีนั้นจะต้องได้รับการถอดรหัสให้เป็นตัวอักษรที่สมบูรณ์หนึ่งตัวก่อนที่จะเข้าสู่โมเดลของเซลล์ หากถอดรหัสอย่างไร้ความระมัดระวังโดยการแยกจุดรหัสออกเป็นไบต์ย่อย ตัวอักษรเดี่ยวจะกลายเป็นอักษรขยะ (mojibake) สองชิ้นที่การส่งออกในภายหลังไม่สามารถกู้คืนกลับมาได้อีกเลย
คอนเทนเนอร์ XLSX คือไฟล์ ZIP และ ZIP ก็มีการเข้ารหัสชื่อของตัวเอง
ไฟล์ XLSX คือคลังเก็บ ZIP และคลังเก็บจะบันทึกชื่อสำหรับสมาชิกทุกตัวที่มันเก็บไว้ รูปแบบ ZIP นั้นเก่าแก่มากพอที่ข้อกำหนดดั้งเดิมไม่ได้ระบุสิ่งใดเกี่ยวกับการเข้ารหัสชื่อเหล่านั้นไว้ ดังนั้นโปรแกรมอ่านที่ไม่พบสัญญาณจะสมมติใช้รหัสหน้าท้องถิ่นของคลังเก็บนั้น การคาดเดานั้นจะผิดพลาดทันทีที่ชื่อสมาชิกมีตัวอักษรที่ไม่ใช่ ASCII ซึ่งเกิดขึ้นกับชื่อส่วนแผ่นงานที่ผ่านการแปลท้องถิ่นและกับสื่อบันทึกที่ฝังไว้ซึ่งชื่อไฟล์มีเครื่องหมายการออกเสียงหรือตัวอักษรที่ไม่ใช่อักษรละติน
วิธีแก้ไขคือการใช้บิตเดี่ยว บิตวัตถุประสงค์ทั่วไปตำแหน่งที่ 11 (General-purpose bit 11) ในส่วนหัวไฟล์ท้องถิ่นแต่ละตัวจะประกาศว่าชื่อสมาชิกได้รับการเข้ารหัสเป็น UTF-8 ฟังก์ชันของ HotXLS จะตรวจสอบบิตนี้อย่างแม่นยำเมื่ออ่านคลังเก็บ โดยทดสอบแฟล็กวัตถุประสงค์ทั่วไปกับมาสก์ $0800 และโปรแกรมอ่านหรือโปรแกรมเขียนที่ละเลยข้อกำหนดนี้จะอ่านชื่อที่โปรแกรมเวอร์ชันถูกต้องจัดเก็บเป็น UTF-8 ผิดพลาดไป บิตนี้มีต้นทุนการตั้งค่าต่ำและประมวลผลได้ง่าย และเป็นส่วนแบ่งระหว่างชื่อสมาชิกที่รอดพ้นการเดินทางไปกลับกับชื่อสมาชิกที่เสียหายตั้งแต่ก่อนที่เนื้อหาของสเปรดชีตจะได้รับการวิเคราะห์ไวยากรณ์ด้วยซ้ำ
การเปลี่ยนรูปอักษร (Case folding) และการสแกนตัวเลขก็ซ่อนความเสี่ยงแบบเดียวกันไว้
การประเมินผลสูตรคือจุดที่ความปลอดภัยสำหรับยูนิโค้ดเปลี่ยนจากการแปลงรูปแบบข้อมูลไปเป็นเรื่องของการเปรียบเทียบ ฟังก์ชัน SEARCH จะไม่ไวต่ออักษรพิมพ์เล็กพิมพ์ใหญ่ ซึ่งหมายความว่ามันจะต้องแปลงรูปอักษรก่อนที่จะค้นหาข้อความย่อย วิธีการแปลงที่ผิดคือการประมวลผลผ่านรหัสหน้า ANSI เนื่องจากตัวอักษรพิมพ์ใหญ่ของข้อความที่ไม่ใช่ ASCII ด้วยวิธีนั้นจะส่งอักขระผ่านรหัสหน้าที่มีขนาดแคบและทำความเสียหายแก่สิ่งใดก็ตามที่อยู่นอกเกณฑ์ วิธีการที่ถูกต้องคือการทำให้อักษรตัวพิมพ์ใหญ่ของสตริงแบบกว้าง (wide-string) ซึ่งจะคงรูปช่วง UTF-16 ทั้งหมดไว้ได้อย่างครบถ้วน HotXLS แปลงสไตล์ด้วย WideUpperCase ด้วยเหตุผลนี้ ดังนั้นการค้นหาข้อความที่มีเครื่องหมายออกเสียงหรืออักษรที่ไม่ใช่ละตินจะยังคงตรงกับตัวอักษรเดิมที่ส่งเข้าไป แทนที่จะเป็นค่าที่รหัสหน้าบิดเบือนไป
ตัวแบ่งโทเค็นของสูตร (formula tokenizer) มีหน้าที่เกี่ยวข้องที่ไม่ได้เกี่ยวข้องกับตัวอักษรเลย แต่เกี่ยวข้องกับตำแหน่งสิ้นสุดของโทเค็น รูปแบบทางวิทยาศาสตร์เช่น 1E3 หรือ 2.5E-3 เป็นค่าคงที่ตัวเลขเดียว และตัวสแกนจะต้องจดจำอักษร 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) และทั้งคู่ต่างใช้ข้อความเดียวกันในหน่วยความจำ ไม่มีส่วนใดในโค้ดที่เรียกใช้งานที่ประกาศรหัสหน้าหรือทำรหัสควบคุมอักขระ เนื่องจากหน้าที่รับผิดชอบอยู่ที่ตัวเขียนซึ่งเข้าใจรูปแบบไฟล์ของมันเอง ฟังก์ชันระดับสมุดงาน SaveAsCSV ก็ใช้รูปแบบเดียวกันหากคุณต้องการส่งออกข้อมูลแบบมีตัวคั่นจากแหล่งข้อมูลเดียวกัน
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
ความปลอดภัยของยูนิโค้ดขึ้นอยู่กับเส้นทางการส่งออก ไม่ใช่ตัวไลบรารีทั้งหมด
บทความนี้มีคีย์เทรลเลอร์จากพจนานุกรมกระแสเป็นข้อความธรรมดา ขยายทุก /ObjStm ด้วยตัวถอดรหัส Flate ก่อนที่จะตรวจสอบเอกสาร และจัดการการถอดรหัสรายการอ้างอิงไขว้ไบนารีเป็นงานที่ใหญ่ขึ้นซึ่งเลือกทำภายหลังได้
บทเรียนที่ควรจำไว้คือไม่มีจุดใดจุดหนึ่งที่เป็นจุดทำให้ปลอดภัยสำหรับยูนิโค้ดได้ทั้งหมด RTF ต้องการรหัสหน้าที่ประกาศรวมถึงรหัสควบคุม \u HTML ต้องการการหลีกเลี่ยงเอนทิตีสำหรับอักขระที่มีความหมายสำคัญทางมาร์กอัปและการอ้างอิงตัวเลขเมื่อไม่มีการรับประกันชุดอักขระ ตลอดจนการถอดรหัสเอนทิตีที่ส่งมาในรูปของสตริงร่วมกันอย่างถูกต้อง ส่วน ZIP ต้องการการตั้งค่าบิตวัตถุประสงค์ทั่วไปตำแหน่งที่ 11 เพื่อให้อ่านชื่อสมาชิกที่เป็น UTF-8 ได้อย่างถูกต้อง การประเมินผลสูตรต้องการการจัดสไตล์ตัวอักษรแบบสตริงกว้างและตัวแบ่งโทเค็นที่ช่วยรักษาโครงสร้างวิทยาศาสตร์ไว้เป็นชิ้นเดียว แต่ละประเด็นเหล่านี้เป็นข้อตกลงที่แตกต่างกัน และไลบรารีอาจรองรับความต้องการหนึ่งในขณะที่ฝ่าฝืนอีกข้อตกลงหนึ่งอย่างเงียบๆ ได้ นั่นคือสาเหตุที่เครื่องมือที่ประมวลผลไฟล์ CSV ได้อย่างถูกต้องยังสามารถส่งมอบไฟล์ RTF ที่เต็มไปด้วยเครื่องหมายคำถามให้แก่คุณได้
หากการส่งออกของคุณพึ่งพารูปแบบแบบมีตัวคั่น ข้อแลกเปลี่ยนระหว่างกันจะมีอธิบายไว้ในคู่มือการส่งออก CSV, TSV และ HTML ของเรา และเมื่อแหล่งข้อมูลเป็นชุดผลลัพธ์แทนที่จะเป็นแผ่นงานที่สร้างขึ้นด้วยมือ รูปแบบในการส่งออกฐานข้อมูลสำหรับรายงาน Delphi จะทำงานร่วมกับกฎการเข้ารหัสที่อธิบายไว้ในที่นี้ได้อย่างลงตัว ทั้งหมดนี้จัดส่งมาในตัว HotXLS Component สำหรับ Delphi และ C++Builder พร้อมด้วย API การอ่าน สูตร และการจัดรูปแบบที่มีอธิบายในส่วนอื่นของบล็อกนี้