معالجة مشاكل الترميز
character set and encoding issues
1 تعريف المشكلة:
عموما تحدث عملية الخطأ في عرض البيانات، التي يشار إليها باسم مشاكل الترميز، عندما يخطئ تطبيق ما في معرفة الترميز الصحيح للبيانات التي يتعامل معها.
فيما يتعلق بمشاكل عرض اللغة العربية مثلا، هناك نوعين متكررين من الأخطاء. فأحيانا يقوم متصفح الوب بعرض البيانات بشكل رموز غريبة لا يمكن قراءتها، وهذا يحدث بسبب عدم تعرفه على الترميز الأصلي واستخدامه ترميزا (افتراضيا) مختلفا. ويمكن في هذه الحالة تصحيح العرض يدويا عن طريق اختيار الترميز الصحيح من قوائم المتصفح، غالبا خاصية مثل: View->Encoding->X
النوع الثاني يتعلق بالخطأ في استخدام الترميز الصحيح عند حفظ البيانات في قاعدة البيانات. في هذه الحالة، عند الدخول على الجدول عبر واجهة لقاعدة البيانات (phpmyadmin مثلا، أو أي واجهة يوفرها مدير السيرفر للمستخدمين ضمن لوحة التحكم database manager interface) فإما أن تظهر البيانات على شكل رموز غريبة، أو تظهر على شكل علامات استفهام.
مع أنه في حالة ظهور الرموز الغريبة، غالبا ما يظل بالإمكان تصحيح الترميز (على الطائر) بحيث يعرضها المتصفح بصورة صحيحة في آخر المطاف، إلا أن هذه الحالة تظل جزءا من تعريفي للمشكلة. فالهدف الأصلي عند معالجة مشاكل الترميز هو حفظ البيانات بصورة صحيحة من البداية.
أما البيانات التي تظهر بصورة علامات استفهام فهي بيانات مفقودة. أخطأ برنامج إدارة قاعدة البيانات في معالجة ترميزها ووضع بدلا منها تلك العلامات.
2 شرح أسباب المشكلة:
هناك خمسة أطراف داخلة في عملية استعراض ملفات الوب: مبرمج الوب أو مؤلف الملف web developer، المتصفح web browser، سيرفر الوب web server، اللغة المضمّنة المستخدمة scripting language settings ، سيرفر قاعدة البيانات database server.
بصورة عامة جميع هذه التطبيقات المذكورة تمتلك ملفات خاصة بالإعدادات ini. or .conf configuration files، وكل واحد منها يحفظ في هذه الملفات ترميزاته الافتراضية. هذا باستثناء المتصفح، الذي يعتمد على القيمة المرسلة إليه من سيرفر الوب، أو على الترميز الذي عرَّفه مبرمج الوب داخل الملف بواسطة الـ html meta tag. وفي حالة عدم وجود ما سبق، قد يحاول المتصفح تخمين الترميز بواسطة خوارزمية معيّنة، أو سيختار القيمة الافتراضية للمستخدم.
وهناك ثلاثة أنواع من ملفات الوب: ملف وب بسيط static file، ملف وب ديناميكي (يحتوي لغة برمجة مضمنة) web file containing some scripting language، وملف وب ديناميكي يتصل أيضا بقاعدة بيانات dynamic file connecting to a database. سيتم الحديث عن كل حالة من هذه الحالات واحدة تلو الأخرى.
2ـ1 أولا الملف البسيط static web document:
في هذه الحالة يحدث التواصل بين طرفين فقط هما المتصفح وسيرفر الوب. ما يحدث هو أن المتصفح يرسل في طلب ملف وب من السيرفر، وهو يحتاج من السيرفر أيضا إلى معرفة الترميز الخاص بالملف المطلوب ليقوم بعرضه بصورة صحيحة.
هذه المعلومة يحصل عليها المتصفح بواحدة من الطرق الآتية، مرتبة حسب الأولوية:
1) يقوم بقراءتها من الـ HTTP Content-Type header التي يفترض أن يرسلها السيرفر مع كل ملف. وهي تأخذ هذه الصورة: Content-Type: text/html; charset=utf-8 ـ (utf-8 يستخدم للتمثيل ليس إلا).
في هذه الحالة يعتمد السيرفر في إرسال الترميز الصحيح على أحد مصدرين (مرتبين حسب الأولوية):
أ) إعدادات ملف htaccess.، فيبحث عن واحدة من هذه الأوامر:
AddCharset utf-8 .html
AddDefaultCharset utf-8
AddDefaultCharset off
ب) ملف الإعدادات الخاص بالسيرفر httpd.conf:
AddDefaultCharset utf-8
مع العلم أن عدم وجود أي من هذه الإعدادات يعني أن السيرفر سيستخدم القيمة الافتراضية الأصلية، وهي Latin1 (ISO-8859-1)o
2) من خلال الترميز الذي حفظ به الملف على النظام:
خاص بالملفات المحتوية لعلامة (Byte-Order Mark (BOM. مبرمج الوب هو المسؤول عن وجود هذه العلامة عند حفظه للملف بواسطة أي برنامج تحرير. ينصح بعدم اختيار ترميز يقوم بتضمينها في حالة التعامل مع ملفات ديناميكية.
3) XML declaration:
وهذا خاص بملفات XML أو XHTML، ويأخذ هذه الصورة:
<? "xml version="1.0" encoding="utf-8 ?>
4) HTML meta tag :
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />o
2ـ2 ثانيا استخدام لغة مضمنة dynamic file with a scripting language:
استخدام لغة مضمنة لا يغير من آلية التواصل بين المتصفح والسيرفر، ويؤثر فقط على اختيار سيرفر الوب للترميز الذي سيرفقه كـ HTTP Content-Type header.
ففي حالة الملف البسيط static file يبحث السيرفر عن الترميز في ملف htaccess.، وملف httpd.conf كما ذكر سابقا. أما عند استخدام لغة مضمنة scripting language، فالسيرفر يبحث أولا في إعدادات اللغة المستخدمة، فهو يعطيها الأولوية قبل إعداداته الخاصة. فيصبح الترتيب هكذا، حسب الأولوية:
أ) PHP header instruction:
header('Content-Type:text/html; charset=utf-8')o
ب) PHP .ini configuration file:
default_charset = "utf-8"o
مع العلم أن القيمة الافتراضية الأصلية وهي (Latin1 (ISO-8859-1
ج) htaccess.
د) httpd.conf
2ـ3 ثالثا الاتصال بقاعدة بيانات connecting to a database:
تحتوي قاعدة بيانات MySQL نوعين من إعدادات الترميز: ترميزات خاصة بتخزين البيانات، وترميزات أخرى خاصة باستقبال الاتصال الذي ينشئه العميل.
2ـ3ـ1 المتغيرات الخاصة بتخزين البيانات:
توجد أربعة متغيرات: ترميز قاعدة البيانات، ترميز سيرفر MySQL، ترميز الجدول، ترميز الحقل. هي كالتالي، من غير أهمية للترتيب:
- character_set_database + collation_database
هذا هو الترميز الافتراضي لإنشاء قاعدة البيانات. ويعيّن غالبا من قبل مدير السيرفر:
CREATE DATABASE mydb
DEFAULT CHARACTER SET utf8
DEFAULT COLLATE utf8_general_ci;
- Character_set_server + collation_server
وتستخدم هذه القيمة عند إنشاء قاعدة بيانات جديدة عندما لا يكون الترميز الافتراضي لقاعدة البيانات (character_set_database) معروفا. ويتم تغيير هذه القيمة في ملف my.ini أو my.cnf (حسب نوع النظام: ويندوز، أم أي نظام آخر):
[mysqld]ـ
character-set-server=utf8
collation-server=utf8_general_ci
- Table character set and collation
- Column character set and collation
ويمكن تعيين ترميزات كل من الجداول والحقول من خلال الأمر CREATE TABLE.
2ـ3ـ2 متغيرات الترميز الخاصة بمعالجة بيانات الاتصال:
وهي كالتالي من غير أهمية للترتيب:
- ترميز البيانات القادمة من العميل الذي أنشأ الاتصال مع سيرفر قاعدة البيانات:
character_set_client
- ترميز البيانات أثناء معالجتها على السيرفر:
character_set_connection + collation_connection
مع العلم أنه عموما يكفي تعيين إحدى القيمتين (إما character-set أو collation) ليتم تعيين الأخرى تلقائيا بالقيمة الافتراضية المخصصة لها: فتعيين character-set=latin1 مثلا، يعني أن تأخذ collation قيمة افتراضية هي latin1_swedish_ci تلقائيا.
والسيرفر يقوم فعليا بتحويل البيانات القادمة من العميل، من الترميز المعرّف في character_set_client.. إلى الترميز المعيّن في متغير character_set_connection، أثناء معالجة البيانات (هي استعلامات SQL) على السيرفر.
- ترميز النتائج المعادة إلى العميل:
character_set_results
وهذا هو الترميز الذي ترسل به البيانات المقروءة من جداول قاعدة البيانات عندما تعاد إلى العميل.
ترميزات الاتصال هذه مهمة جدا، والواقع أن كل ملف ديناميكي يتصل بقاعدة البيانات ينبغي عليه تعيين هذه القيم الثلاثة والتأكد من توافقها مع الترميزات المستخدمة في جداول قاعدة البيانات (الحقول بصورة أخص).
ويمكن تعيين هذه القيم الثلاثة دفعة واحدة بتنفيذ الأمر: 'SET NAMES 'utf8 (أو SET CHARACTER SET 'utf8' مع وجود فارق بسيط بين الأمرين)، بواسطة لغة البرمجة المستخدمة من قبل العميل (عن طريق دالة ()mysql_query مثلا). وهذا الأمر يعادل تنفيذ الأسطر الثلاثة التالية:
SET character_set_client = utf8;
SET character_set_results = utf8;
SET character_set_connection = utf8
وكما ذكر من قبل، فتعيين قيمة character_set_connection يؤدي تلقائيا إلى تعيين الخاصية collation_connection بالقيمة الافتراضية لها (في هذه الحالة: utf8_general_ci).
يذكر أنه إذا تم تغيير الترميز الافتراضي لسيرفر MySQL في ملف my.cnf باستعمال هذين السطرين:
[mysqld]ـ
default-character-set=utf8
skip-character-set-client-handshake
فإن التغييرات ستكون عامة على مستوى جميع الاتصالات التي تتم مع السيرفر، وسيُعتمد ترميز السيرفر بغض النظر عن أي ترميز يحدده العميل بواسطة SET NAMES. أي أنه لن تعود هناك لا ضرورة، ولا فائدة، من استخدام هذه الدالة من قبل مبرمج الوب مع كل اتصال جديد.
3 البحث عن السيناريو الأمثل:
في كلمة موجزة: ما هو الحل إذن؟ ما الذي ينبغي عمله؟
الحل ينبغي أن يتخذ بناء على مفاضلة بين الخيارات. ففي بعض الحالات قد لا يكون فرض ترميز موحد على جميع المستويات خيارا متاحا.
بداية لا مناص من توزيع المسؤوليات بين الأطراف المختلفة. مثلا، أي مشكلة في الترميز سببها حفظ الملف مع علامة BOM، لا بد أن يتحمل تبعته المستخدم مالك ومحرّر الملف الأصلي، فمشكلة مثل هذه لا يمكن تأمينها override it من قبل مدير السيرفر، بسبب أولوية BOM العالية في التأثير على المتصفح في اختيار ترميز العرض (تأتي بعد HTTP header مباشرة).
المشكلة الثانية هي HTTP header. فمعلومات الـ header لا تتوافر دائما للمتصفح. السيرفر عادة يرفق HTTP header مع كل ملف يرسله إلى المتصفح، ولكن في بعض الأحيان تفقد هذه المعلومات في الطريق. فالملفات التي تحفظ مؤقتا في Cache خاص بسيرفر ما (Proxy server مثلا) تفقد معلومات HTTP header الأصلية الخاصة بها. فلا يعود أمام المتصفحات إلا الاعتماد على علامة BOM إذا كانت موجودة (وإدراج هذه العلامة غير محبذ لهذا السبب) أو على HTML meta tag. ولكن للأسف لا توجد طريقة لإجبار المستخدم على كتابة meta tags دائما داخل ملفات الوب الخاصة به.
المشكلة الثالثة هي أنه لا توجد طريقة أيضا تضمن أن يستخدم مبرمج الوب دالة SET NAMES عند كل اتصال بقاعدة البيانات، من أجل تصحيح الترميزات المستخدمة على سيرفر قاعدة البيانات. هذه المشكلة تمثل واحدة من المشاكل المتكررة المسؤولة عن أخطاء الحفظ وأخطاء العرض. وأخيرا، سيظل المبرمج أيضا هو من يتحمل مسؤولية اختيار الترميز الصحيح لجداوله، وحقول تلك الجداول، عند إنشائها برمجيا من قبله.
هذا يعني أن بعض الأخطاء ستستمر في الظهور بسبب بعض الممارسات البرمجية للمطوّرين. أما ما يهمنا في هذا السياق فهي الأخطاء التي يتحمل مسؤوليتها مدير السيرفر.
عموما فإن الخطوات التي يلزم مدير السيرفر تنفيذها خطوات مباشرة وواضحة. هذه هي المتغيرات التي ينبغي على مدير السيرفر إعدادها بصورة صحيحة:
PHP default character set value
Apache default character set value
MySQL connection-related character set values
MySQL database character set value
MySQL server character set value
بالنسبة للـApache و PHP، فذلك يتم في ملفات httpd.conf و php.ini، على الترتيب (بالكيفية التي ذكرت سابقا). في هذه الحالة لا حاجة لتوحيد الترميز على مستوى جميع المستخدمين للسيرفر، بل يمكن منح المستخدمين حرية اختيار ترميزاتهم الخاصة عن طريق استخدام ملف htaccess مع Apache (وربما يمكن استخدام htaccess مع PHP إذا كان يعمل في صورة Module تحت سيرفر الـ Apache، هذه معلومة غير موثقة). بالنسبة لمدير السيرفر، فإما أن يتيح إمكانية تحرير ملف htaccess للمستخدم، أو يطلب منه الترميز الذي يريده مع بداية استخدامه لسيرفر الوب.
هذا يحل نصف المشكلة، ويبقى النصف الآخر الخاص بمتغيرات قاعدة البيانات.
يمكن تعيين قيمة موحدة لسيرفر MySQL، وفي هذه الحالة فإن هذه الإعدادات ستكون سارية على مستوى النظام بأكمله، وليس على مستوى كل مستخدم (اتصال) على حدى. وقد يمكن إعداد سيرفر MySQL برمجيا لتنفيذ جملة SET NAMES بصورة آلية على مستوى كل اتصال جديد (عميل جديد) يصل إليه، ولكن هذه الخاصية تخضع لبعض الاستثناءات التي تحتاج إلى مراجعة لها، وينبغي كذلك التأكد من طبيعة عمل هذه الميزة.
ينبغي التنبيه إلى أن أي دخول على قاعدة البيانات من خلال واجهة لقاعدة البيانات database manager (مثل phpmyadmin) وإدخال البيانات (السجلات) يدويا، قد يعرض تلك البيانات للتلف وظهور علامات الاستفهام القاتلة إذا لم يكن ترميز collation_connection هو نفس ترميز حقول الجداول المعدلة. هذه المسألة حيوية لأن المستخدم لا بد له من الدخول على قاعدة البيانات لسبب أو لآخر، ويبقى الـ database manager هو الواجهة الرسمية للتعديل في قاعدة البيانات، فلا ينبغي أن يحتوي مشاكل في الترميز قد تؤدي إلى فقد البيانات بالكامل من قاعدة البيانات.
ملاحظة أخيرة: المعلومات المذكورة في هذا الموضوع لم يتم تجربتها في نظام حقيقي من قبل الكاتب. وينبغي الإشارة إلى أن الحديث يعود على سيرفر الوب Apache ولغة PHP وقاعدة بيانات MySQL، سواء ذكر ذلك في السياق أم لم يذكر.