Technical Article

تصدير جداول البيانات الآمن لـ Unicode في Delphi: RTF وHTML

تحتوي ورقة العمل على عمود لأسماء العملاء. بعضها بالصينية، وبعضها بالسيريلية، وبعضها يحمل نبرات ألمانية أو لهجة فرنسية. تقوم بتصديرها إلى CSV وفتح النتيجة، لتجد كل حرف سليماً. بينما تصدر نفس كتاب العمل إلى RTF لقالب دمج المراسلات، وتفتحه في معالج نصوص، لتجد الأسماء غير المترجمة لـ ASCII قد انهارت إلى صفوف من علامات الاستفهام. البيانات لم تتغير أبداً. ما تغير هو عقد الترميز للتنسيق الذي كتبته، ويحمل كل مسار تصدير عقداً مختلفاً.

هذا هو الفخ الذي تقع فيه مكتبة تبدو واعية تماماً بـ Unicode على السطح. يتم الاحتفاظ بنص الخلية داخلياً كـ WideString، لذا لا يفقد النموذج محرفاً أبداً. يحدث الفقد عند الحدود، في الكاتب الذي يتعين عليه تسلسل هذا النص في تنسيق له قواعده الخاصة حول البايتات القانونية وكيف يجب ترميز أي شيء خارج النطاق القانوني. اضبط كاتباً واحداً بشكل صحيح وظل بإمكانك شحن كاتب آخر يشوه نفس النص. الحل ليس مفتاحاً عاماً. بل هو قرار منفصل وصحيح على كل مسار.

تنسيق RTF آمن لـ 7 بتات بالتصميم

يسبق تنسيق النص المنسق (RTF) ظهور Unicode وتم تحديده للنجاة في عمليات النقل التي تمرر فقط أحرف ASCII القابلة للطباعة. يعلن مستند RTF عن صفحة رموز في رأسه، وأي حرف لا يمكن للكاتب تمثيله في صفحة الرموز تلك يجب إرساله كرمز هروب بدلاً من بايت خام. رمز الهروب ذو الصلة هو \u، والذي يحمل وحدة كود 16 بت موقعة متبوعة بحرف تراجع ASCII للقراء القدامى الذين لا يفهمون رمز الهروب على الإطلاق.

يكتب HotXLS ملفات RTF بهذه الطريقة. يفتح رأس المستند بالإعلان عن صفحة الرموز، بصيغة \ansi\ansicpg1252\uc1، ويمشي الكاتب في وحدة lxRTF عبر كل سلسلة مرسلاً أي حرف أعلى من ASCII المجرد كرمز هروب \u بحيث يظل تدفق البايتات نظيفاً بـ 7 بتات بغض النظر عما يمكن لصفحة الرموز المعلنة الاحتفاظ به. وتصبح نقطة كود مثل U+4E2D هي التسلسل الحرفي  3?، وليست بايتاً خاماً سيحاول برنامج العرض تفسيره من خلال أي صفحة رموز يفترضها. وبدون هذا الالتزام، فإن أي شيء خارج صفحة الرموز المعلنة ليس له تمثيل بايتات قانوني، والكاتب الذي يرسل القيمة الخام ينتج علامات الاستفهام التي بدأ بها هذا المقال.

التفصيل الذي يجب تذكره هو أن صفحة الرموز المعلنة ورموز الهروب هما نصفان لعقد واحد. الإعلان عن صفحة الرموز بمفرده لا يساعد النص الذي يقع خارجها. وإرسال رموز الهروب بدون صفحة رموز معلنة يترك الأحرف البديلة غامضة. يجب أن يكون كلاهما صحيحاً معاً، وهذا هو السبب في أن الكاتب الذي يتعامل مع أحدهما فقط لا يزال يفشل في أول كتاب عمل متعدد اللغات.

الهروب في HTML يتعلق بأكثر من مجرد الأقواس الزاوية

ينتج تصدير HTML مستنداً متعدد الأوراق تحمل إطارات التنقل الخاصة به أسماء الأوراق كنص مرئي. تلك الأسماء هي سلاسل نصية يتحكم بها الكاتب ويمكن أن تحتوي على أي حرف، بما في ذلك الأحرف الهامة في الترميز. ورقة تسمى حرفياً Q1 & Q2 <draft> يجب أن تصل إلى الصفحة ككيانات مهروبة، وإلا فإن الأقواس الزاوية تفتح وسماً وهمياً وتبدأ علامة وساطة الكيانات المرجعية التي لم تكن مقصودة أبداً. هذا هو الهروب العادي في HTML، وتخطيه على تسمية إطار هو نوع السهو الذي يجتاز كل اختبار مبني على أسماء أوراق تحتوي على ASCII فقط.

سؤال الترميز يقع في طبقة أدنى من ذلك. عندما تهبط الأحرف غير المصنفة كـ ASCII في سياق غير مضمون تقديمه كـ UTF-8، فإن التمثيل الآمن هو مرجع حرف رقمي، لذا يكتب U+00E9 كـ é بدلاً من بايت خام يعتمد معناه على ترميز الاستجابة. وتنطبق الصورة المعاكسة لهذه القاعدة على طريق الإدخال. كتاب العمل المقروء مجدداً من XLSX يحمل سلاسل مشتركة قد يكون الحرف فيها مخزناً بالفعل ككيان XML رقمي، ويجب فك ترميز هذا الكيان إلى حرف كامل واحد قبل أن يدخل نموذج الخلية. فك ترميزه بإهمال، بتقسيم نقطة الكود إلى بايتات منفصلة، ويظهر الحرف الواحد كقطعتين من النصوص المشوهة (mojibake) التي لا يمكن لأي تصدير لاحق إصلاحها.

حاوية XLSX هي ملف ZIP، ولـ ZIP ترميز أسماء خاص به

ملف XLSX هو أرشيف ZIP، ويخزن الأرشيف اسماً لكل عضو يحتويه. تنسيق ZIP قديم بما يكفي لدرجة أن مواصفاته الأصلية لم تذكر شيئاً عن ترميز تلك الأسماء، لذا فإن القارئ الذي لا يجد إشارة يفترض صفحة الرموز المحلية للأرشيف. هذا الافتراض خاطئ بمجرد أن يحتوي اسم العضو على حرف غير مصنف كـ ASCII، وهو ما يحدث مع أسماء أجزاء ورقة العمل المحلية ومع الوسائط المضمنة التي تحمل أسماؤها علامات نبر أو نصاً غير لاتيني.

الحل هو بت واحد. يعلن البت 11 ذو الأغراض العامة في كل رأس ملف محلي أن اسم العضو مرموز كـ UTF-8. يفحص HotXLS هذا البت بالضبط عندما يقرأ الأرشيف، مختبراً أعلام الأغراض العامة مقابل القناع $0800، والقارئ أو الكاتب الذي يتجاهله سيخطئ في قراءة الاسم الذي خزنته النسخة الصحيحة كـ UTF-8. هذا البت رخيص الضبط ورخيص الاحترام، وهو الفرق بأكمله بين اسم عضو ينجو من رحلة الذهاب والإياب واسم يصل تالفاً قبل أن يتم تحليل محتوى جدول البيانات.

طي الحالة ومسح الأرقام يخفيان نفس الخطر

تقييم الصيغ هو المكان الذي تتوقف فيه سلامة Unicode عن كونها متعلقة بالتسلسل وتصبح متعلقة بالمقارنة. دالة SEARCH غير حساسة لحالة الأحرف، مما يعني أنه يتعين عليها طي الحالة قبل أن تبحث عن سلسلة فرعية. الطريقة الخاطئة للطي هي من خلال صفحة رموز ANSI، لأن تحويل النص غير المصنف كـ ASCII للأحرف الكبيرة بهذه الطريقة يوجه الأحرف عبر صفحة رموز ضيقة ويفسد أي شيء خارجها. الطريقة الصحيحة هي تحويل السلاسل العريضة للأحرف الكبيرة، مما يحافظ على نطاق UTF-16 الكامل. يطوي HotXLS الحالة باستخدام WideUpperCase لهذا السبب بالضبط، بحيث يطابق البحث عن النص الذي يحمل علامات نبر أو غير اللاتيني نفس الأحرف التي أُعطي إياها بدلاً من تقريب مشوه بصفحة الرموز.

يحمل محلل الصيغ التزاماً ذا صلة لا علاقة له بالحروف وكل شيء يتعلق بمكان انتهاء الرمز المميز. التدوين العلمي مثل 1E3 أو 2.5E-3 هو قيمة حرفية رقمية واحدة، ويتعين على الماسح الضوئي التعرف على حرف E، والعلامة الاختيارية، والأرقام التالية كجزء من الرقم بدلاً من تقسيم الإدخال إلى اسم متبوع برقم منفصل. الماسح الضوئي الذي يسيء معالجة هذا يحول ثابتاً صالحاً تماماً إلى خطأ في التحليل، أو الأسوأ من ذلك، تعبير خاطئ بصمت. إنه ينتمي إلى نفس المناقشة لأن كلتا الحالتين تتعلقان باتخاذ القارئ قراراً صحيحاً على مستوى الحروف: أحدهما يتعلق بكيفية طي حرف للمقارنة، والآخر يتعلق بما إذا كان الحرف يكمل الرمز المميز الحالي.

بناء وتصدير كتاب عمل متعدد اللغات

لا تطلب منك واجهة برمجة التطبيقات العامة التفكير في أي من هذا. تبني كتاب العمل من قيم خلايا 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');

أمان Unicode يكون لكل مسار، وليس لكل مكتبة

الدرس الذي يجدر بنا استخلاصه هو أنه لا يوجد مكان واحد لتكون فيه آمناً لـ Unicode. يحتاج تنسيق RTF إلى صفحة رموز معلنة بالإضافة إلى رموز هروب \u. ويحتاج تنسيق HTML إلى هروب الكيانات للأحرف الهامة في الترميز ومراجع رقمية حيث لا يكون ترميز الحروف مضموناً، بالإضافة إلى فك ترميز الكيانات التي تصل في سلاسل مشتركة بشكل صحيح. وتحتاج حاوية ZIP إلى ضبط البت 11 ذي الأغراض العامة بحيث يُقرأ اسم العضو المكتوب بـ UTF-8 كـ UTF-8. ويحتاج تقييم الصيغ إلى طي حالة السلاسل العريضة ومحلل رموز يحافظ على التدوين العلمي قطعة واحدة. كل عقد من هذه العقود هو عقد مختلف، ويمكن للمكتبة تلبية أحدها بينما تنتهك آخر بصمت. هذا هو السبب في أن الأداة التي تنجح في CSV يمكنها أن تسلمك ملف RTF مليئاً بعلامات الاستفهام.

إذا كانت صادراتك تعتمد على التنسيقات المحددة، فإن المقايضات بينها مغطاة في دليلنا لتصدير CSV وTSV وHTML، وعندما يكون المصدر عبارة عن مجموعة نتائج بدلاً من ورقة مبنية يدوياً، فإن الأنماط في تصدير قاعدة البيانات لتقارير Delphi تقترن بشكل طبيعي بقواعد الترميز الموصوفة هنا. كل هذا يشحن كجزء من مكون جداول البيانات HotXLS لـ Delphi وC++Builder، إلى جانب واجهات برمجة تطبيقات القراءة والصيغ والتنسيق المغطاة في مكان آخر من هذه المدونة.