مقال تقني

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

يحتوي جدول بيانات على عمود بأسماء العملاء. بعضها باللغة الصينية، وبعضها باللغة السيريلية (Cyrillic)، والقليل منها يحمل إمالات ألمانية (German umlauts) أو لكنة فرنسية (French accent). تقوم بتصديره إلى CSV وفتح النتيجة، ويكون كل حرف سليمًا. تقوم بتصدير نفس المصنف إلى RTF لقالب دمج المراسلات (mail-merge)، وتفتحه في معالج نصوص، وانهارت الأسماء غير ASCII إلى صفوف من علامات الاستفهام. البيانات لم تتغير أبدًا. ما تغير هو عقد الترميز (encoding contract) الخاص بالتنسيق الذي كتبته، وكل مسار تصدير يحمل مسارًا مختلفًا

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

RTF هو تنسيق آمن لـ 7 بت (7-bit-safe) حسب التصميم

يسبق تنسيق Rich Text Format (RTF) معيار Unicode وتم تحديده للنجاة من عمليات النقل التي لا تمرر سوى أحرف ASCII القابلة للطباعة. يعلن مستند RTF عن صفحة رموز (code page) في رأسه (header)، وأي حرف لا يستطيع الكاتب تمثيله في صفحة الرموز تلك يجب إصداره كهروب (escape) بدلاً من بايت خام (raw byte). الهروب ذو الصلة هو \u، والذي يحمل وحدة كود 16 بت موقعة (signed 16-bit code unit) متبوعة بحرف احتياطي (fallback character) ASCII للقراء القدامى جدًا بحيث لا يمكنهم فهم الهروب على الإطلاق

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

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

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

ينتج تصدير HTML مستندًا متعدد الأوراق تحمل إطارات التنقل الخاصة به أسماء الأوراق كنص مرئي. هذه الأسماء هي سلاسل يتحكم فيها المؤلف ويمكن أن تحتوي على أي حرف، بما في ذلك الأحرف الهامة في الترميز (markup-significant). الورقة المسماة حرفيًا Q1 & Q2 <draft> يجب أن تصل إلى الصفحة ككيانات هربت (escaped entities)، أو تفتح الأقواس الزاوية علامة فانتوم (phantom tag) وتبدأ علامة العطف (ampersand) مرجع كيان (entity reference) لم يكن مقصودًا أبدًا. هذا هو الهروب العادي لـ HTML، وتخطيه على تسمية إطار هو نوع الحذف الذي يجتاز كل اختبار مبني من أسماء الأوراق التي تحتوي على ASCII فقط

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

حاوية XLSX عبارة عن ملف ZIP، وZIP له ترميز الاسم الخاص به

ملف XLSX عبارة عن أرشيف ZIP، ويخزن الأرشيف اسمًا لكل عضو يحتفظ به. ZIP قديم بما يكفي لدرجة أن مواصفاته الأصلية لم تذكر شيئًا عن ترميز تلك الأسماء، لذلك يفترض القارئ الذي لا يجد أي إشارة (signal) صفحة الرموز المحلية للأرشيف (archive's local code page). هذا الافتراض خاطئ في اللحظة التي يحتوي فيها اسم العضو على حرف غير ASCII، وهو ما يحدث مع أسماء أجزاء ورقة العمل المترجمة والوسائط المضمنة (embedded media) التي تحمل أسماء ملفاتها لهجات (accents) أو نص غير لاتيني (non-Latin script)

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

يطوي طي حالة الأحرف (Case folding) ومسح الأرقام نفس الخطر

تقييم الصيغة هو المكان الذي تتوقف فيه سلامة Unicode عن التمسك بالتسلسل (serialisation) وتصبح متعلقة بالمقارنة. وظيفة SEARCH غير حساسة لحالة الأحرف (case-insensitive)، مما يعني أنه يتعين عليها طي الحالة (fold case) قبل أن تبحث عن سلسلة فرعية (substring). الطريقة الخاطئة للطي هي من خلال صفحة رموز ANSI، لأن تكبير النص غير ASCII بهذه الطريقة يوجه الأحرف عبر صفحة رموز ضيقة ويفسد أي شيء خارجها. الطريقة الصحيحة هي تكبير السلسلة العريضة (wide-string uppercasing)، مما يحافظ على نطاق UTF-16 الكامل. تطوي HotXLS باستخدام WideUpperCase لهذا السبب بالضبط، لذا فإن البحث عن نص ذي تشكيل (accented) أو نص غير لاتيني يطابق نفس الأحرف التي تم إعطاؤها إياه بدلاً من تقريب مشوه بصفحة الرموز (code-page-mangled approximation)

يحمل مميز الصيغ (formula tokenizer) التزامًا ذا صلة لا علاقة له بالحروف وكل ما له علاقة بالمكان الذي ينتهي فيه الرمز المميز (token). التدوين العلمي مثل 1E3 أو 2.5E-3 عبارة عن حرف رقمي واحد (single numeric literal)، ويتعين على الماسح الضوئي (scanner) التعرف على E، وعلامة اختيارية، والأرقام التالية كجزء من الرقم بدلاً من تقسيم الإدخال إلى اسم يتبعه رقم منفصل. الماسح الضوئي الذي يسيء التعامل مع هذا يحول الثابت الصالح تمامًا إلى خطأ تحليل (parse error) أو، الأسوأ من ذلك، إلى تعبير خاطئ صامت. إنه ينتمي إلى نفس المناقشة لأن كلتا الحالتين تتعلقان باتخاذ القارئ قرارًا صحيحًا على مستوى الحرف (character-level decision): إحداهما حول كيفية طي حرف للمقارنة، والأخرى حول ما إذا كان الحرف يستمر في الرمز المميز الحالي

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

واجهة برمجة التطبيقات العامة (public API) لا تطلب منك التفكير في أي من هذا. يمكنك بناء المصنف من قيم خلايا WideString واستدعاء نقطة إدخال (entry point) التصدير التي تريدها. تحدث قرارات الترميز داخل كل كاتب. يزرع المثال أدناه ورقة بنصوص مكتوبة بنصوص (scripts) متعددة، ثم يكتب كلاً من ملف 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، وكلاهما يستهلك نفس النص الموجود في الذاكرة. لا يعلن أي شيء في التعليمات البرمجية المستدعية (calling code) عن صفحة رموز أو يهرب حرفًا، لأن المسؤولية تقع على عاتق الكاتب الذي يعرف تنسيقه الخاص. تتبع SaveAsCSV على مستوى المصنف نفس الشكل إذا كنت بحاجة إلى تصدير محدد (delimited export) من المصدر المطابق

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

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

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

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