ترميز المحارف ويونيكود

يونيكود (Unicode) هو فهرس محارف العالم: لكل حرف ورقم وعلامة ورمز تعبيري رقم يسمى نقطة الترميز (Code Point)، يُكتب مثل U+0639 للحرف العربي ع. أما ترميز المحارف (Character Encoding) فهو القاعدة التي تحوّل هذه الأرقام إلى بايتات للتخزين والنقل، ثم تعيدها محارف. وUTF-8 هو ترميز الويب: يكتب كل نقطة ترميز في بايت واحد إلى أربعة بايتات، فالحرف A بايت واحد، وع بايتان، وन ثلاثة، و😀 أربعة.

ولعيوب النص ثلاثة أسباب شائعة:تختلف طبقتان على الترميز، أو يبدو تسلسلان من نقاط الترميز متطابقين لكنهما يُقارَنان مختلفين، أو يُحسب الطول بالبايتات في مكوّن وبالمحارف في مكوّن آخر. يوضح هذا الدرس كيف تبدو كل مشكلة في البايتات، وكيف تختبر الثلاث معاً بمجموعة بيانات واحدة ثابتة متعددة اللغات.

لماذا يهم

يتوقف نجاح المنتج المستخدم في السعودية أو الهند على معالجته للنص: اسم عميل يُحفظ على هيئة علي، وفاتورة PDF فيها علامات استفهام مكان العنوان، وبحث لا يجد مستخدمة لأن اسمها كُتب بصيغة مختلفة من الحركات، وحدّ 30 حرفاً يرفض اسماً عربياً من 30 حرفاً لأنه قيس بالبايتات. وكل واحد من هذه عيب قد يصادفه المستخدم في يومه الأول، وكثيراً ما يكون في الحقل الذي يحمل اسمه.

بالنسبة إلى المختبِر، عيوب الترميز رخيصة الاكتشاف، لكن تفويتها مكلف. فهي تختبئ خلف بيانات اختبار إنجليزية، ولا تظهر إلا عند نقطة التقاء بين نظامين (من المتصفح إلى الواجهة البرمجية، ومنها إلى قاعدة البيانات، ومنها إلى التصدير)، وما إن يُحفظ النص المشوّه مجدداً حتى يُخزَّن كأنه صحيح، فتكرّر كل قراءة لاحقة الضرر نفسه. ومجموعة بيانات صغيرة مع صورة واضحة لمواضع تحوّل البايتات إلى محارف تكشفها قبل الإصدار.

نقاط الترميز وUTF-8 والترميزات الأخرى

يعرّف يونيكود نقاط ترميز من U+0000 إلى U+10FFFF، والنص الذي يعالجه التطبيق تسلسل منها. والترميز يربط كل نقطة ترميز ببايتات تُكتب بالنظام الست عشري (Hexadecimal)، برقمين لكل بايت من 00 إلى FF. يستخدم UTF-8، المعرَّف في RFC 3629، بايتاً واحداً لمحارف ASCII المئة والثمانية والعشرين، فأي ملف لا يحوي إلا محارف ASCII هو UTF-8 صالح أصلاً؛ وبايتين لمعظم الحروف اللاتينية ذات علامات النبر وللإغريقية والسيريلية والعبرية والعربية؛ وثلاثة للديفاناغارية والصينية واليابانية ومعظم أنظمة الكتابة الأخرى الشائعة الاستخدام؛ وأربعة لمعظم الرموز التعبيرية وكل ما يقع فوق U+FFFF.

يخزّن UTF-16 معظم المحارف في وحدة واحدة من 16 بت، والمحارف التي فوق U+FFFF في وحدتين تسمّيان زوجاً بديلاً (Surrogate Pair). وتعدّ سلاسل JavaScript وJava وواجهات Windows بهذه الوحدات، ولهذا تكون قيمة '😀'.length هي 2. أما الترميزات القديمة أحادية البايت، مثل Windows-1252 للغات أوروبا الغربية وWindows-1256 أو ISO ⁦8859-6⁩ للعربية، فتتسع لـ 256 محرفاً على الأكثر، فلا تستطيع مثلاً أن تجمع العربية والديفاناغارية في نص واحد؛ وهي باقية في قواعد البيانات القديمة وملفات CSV المصدَّرة وأنظمة البريد، ومنها يبدأ معظم النص المشوّه.

السلسلة Aعन😀 أربع نقاط ترميز لكنها عشرة بايتات UTF-8: يأخذ A بايتاً واحداً، وع بايتين، وन ثلاثة، والرمز التعبيري أربعة.

السلسلة على هيئة محارف: أربع نقاط ترميز

الجزء 1:Aالجزء 2:عالجزء 3:नالجزء 4:😀

السلسلة نفسها بايتات UTF-8 بالست عشري: عشرة بايتات

الجزء 1:41 الجزء 2:D8 B9 الجزء 3:E0 A4 A8 الجزء 4:F0 9F 98 80

  • 1–44 نقاط ترميز، 10 بايتات UTF-8، 5 وحدات UTF-16
  1. 1الحرف اللاتيني AU+0041: بايت واحد، مطابق لقيمته في ASCIIبايت واحد
  2. 2الحرف العربي عينU+0639: بايتان، كالحروف الإغريقية والسيريلية والعبريةبايتان
  3. 3الحرف الديفاناغاري ناU+0928: ثلاثة بايتات، كمعظم أنظمة الكتابة الأخرىثلاثة بايتات
  4. 4الرمز التعبيري وجه مبتسمU+1F600: أربعة بايتات؛ وهو فوق U+FFFF، فيشغل أيضاً وحدتَي UTF-16أربعة بايتات
النسخة النصية
String:  A ع न 😀                     4 code points

Char   Code point   UTF-8 bytes      UTF-16 units
A      U+0041       41               1
ع      U+0639       D8 B9            1
न      U+0928       E0 A4 A8         1
😀     U+1F600      F0 9F 98 80      2 (surrogate pair)

Total: 4 characters, 10 UTF-8 bytes, 5 UTF-16 units

جرّبها: مرمّز Hex جرّبها: مرمّز Unicode

النص المشوّه (Mojibake): بايتات صحيحة ومفكّك ترميز خاطئ

النص المشوّه (Mojibake) هو النص المبعثر الذي يظهر عندما تُقرأ بايتات كُتبت بترميز ما على أنها بترميز آخر. البايتات غالباً سليمة، لكن القارئ خمّن خطأً. فكلمة café المخزّنة بـ UTF-8 هي 63 61 66 C3 A9؛ والمكوّن الذي يقرأ هذه البايتات على أنها Windows-1252 يعرض café، لأن C3 في ذلك الجدول هو à وA9 هو ©. ويتفاقم الضرر عندما يُحفظ النص المشوّه مجدداً: عندها يُرمَّز café بوصفه UTF-8 صالحاً، فيعرضه كل قارئ لاحق كما هو، ولا يُستعاد الأصل إلا بعكس التحويل الخاطئ سجلاً سجلاً، وهذا لا ينجح دائماً.

كل طبقة تعلن ترميزاً أو تفترضه: الترويسة Content-Type: text/html; charset=utf-8 والوسم <meta charset="utf-8"> في HTML؛ وJSON الذي يشترط RFC 8259 أن يكون UTF-8 عند تبادله خارج نظام مغلق، ولا يعرّف نوع وسائطه application/json أي معامل charset؛ وعناوين URL التي تُرمَّز فيها بايتات UTF-8 بترميز النسبة المئوية (فالحرف ع يُنقَل على هيئة %D8%B9)؛ وأعمدة قاعدة البيانات واتصالاتها؛ وملفات CSV بعلامة ترتيب البايتات (EF BB BF) أو من دونها؛ وقوالب البريد وملفات السجلات. ويظهر النص المشوّه عند نقطة الالتقاء التي تختلف فيها طبقتان.

وشكل التشويه يدلّك على فئة العيب الذي أمامك:

  • é و€ في النص اللاتيني، والعربية على هيئة سلاسل من Ø وÙ (فتصبح سلام هي سلام)، والديفاناغارية على هيئة سلاسل من ठوà¥: بايتات UTF-8 قُرئت على أنها ترميز غربي أحادي البايت.
  • ? مكان الحروف: حُوّل النص إلى ترميز لا خانة فيه لتلك المحارف، فضاعت نهائياً.
  • �، محرف الاستبدال U+FFFD: صادف مفكّك الترميز تسلسل بايتات غير صالح في UTF-8، وغالباً ما يكون نصاً بترميز Windows-1256 قُرئ على أنه UTF-8، أو سلسلة قُطعت في منتصف محرف متعدد البايتات.
  • مربعات فارغة، أو علامة صوتية مرسومة على دائرة منقّطة: فُكّ ترميز البايتات صحيحاً، لكن الخط لا يحوي رسماً (Glyph) لهذا المحرف، أو أن محرك التشكيل (Shaping Engine)، أي جزء العرض الذي يصل الحروف ويضع عليها العلامات، صادف علامة لا يسبقها حرف، كأن يُقطع النص بين الحرف وحركته. ابحث عندئذٍ في العرض والاقتطاع لا في الترميز.

جرّبها: مرمّز Hex جرّبها: مقارنة النصوص (Diff)

التطبيع وعدّ المحارف

يمكن تخزين النص المرئي نفسه بنقاط ترميز مختلفة. فالحرف é نقطة ترميز واحدة (U+00E9) أو نقطتان (e تتبعها علامة النبر المركِّبة U+0301)؛ والحرف العربي مع حركة قصيرة كالفتحة نقطتان دائماً، وأ نقطة واحدة (U+0623) أو نقطتان (ا تتبعها الهمزة العلوية U+0654). وتطبيع يونيكود (Normalization)، المعرَّف في UAX #15، يحوّل النص إلى صيغة واحدة متفق عليها: NFC يركّب هذه التسلسلات وNFD يفكّكها. أما صيغتا التوافق NFKC وNFKD فتذهبان أبعد، فتطويان أيضاً الحرف المركّب (Ligature) fi إلى fi، وA كامل العرض إلى A، وصيغة العرض العربية ﻻ إلى ل ثم ا؛ وهذا صحيح للبحث وخاطئ لتخزين اسم كما كُتب. ولا يحوّل التطبيع نظام أرقام إلى نظام آخر: فالرقم العربي الهندي ٣ يبقى ٣ حتى مع NFKC، وهذه مسألة إعدادات محلية للدرس التالي.

ما لم يطبّع المنتج النص عند حدوده، يستطيع مستخدمان تسجيل José مرتين، ويفوّت البحث عن اسم كُتب بلوحة مفاتيح الاسمَ نفسه مكتوباً بلوحة أخرى، وتفشل كلمة مرور ضُبطت بصيغة في الصيغة الأخرى. اسأل أي صيغة يستخدمها المنتج وأين يطبّقها؛ وتوصي إرشادات التدويل في W3C بصيغة NFC لنصوص الويب.

وللطول أربع إجابات. المستخدم يعدّ العناقيد الرسومية (Grapheme Clusters)، أي المحارف كما يدركها المستخدم وفق UAX #29؛ وعمود قاعدة البيانات قد يعدّ البايتات؛ وlength في JavaScript تعدّ وحدات UTF-16؛ وPython تعدّ نقاط الترميز، بينما تعدّ Go وRust البايتات. ولا تتفق الأربع إلا في نص ASCII البسيط:

String        Graphemes  Code points  UTF-16 units  UTF-8 bytes
Ali               3           3            3             3
علي               3           3            3             6
مُحَمَّد              4           8            8            16
José (NFC)        4           4            4             5
José (NFD)        4           5            5             6
हिंदी              2           5            5            15
👍🏽                1           2            4             8

جرّبها: عدّاد طول السلاسل النصية جرّبها: مولّد مجموعات بيانات Unicode

مثال تطبيقي: اختبار حقل الاسم من طرف إلى طرف

المتطلب: «اسم العرض، من ⁦2⁩ إلى ⁦30⁩ حرفاً، يظهر في الملف الشخصي وفي رسائل البريد وفي تصدير CSV لقائمة المستخدمين». يُختبر الترميز بوصفه رحلة ذهاب وإياب: أدخل مجموعة بيانات ثابتة، واحفظ، وأعد التحميل، واقرأها عبر الواجهة البرمجية، وصدّرها، وقارن كل محطة بالأصل، بالبايتات لا بالعين.

  • ابنِ مجموعة البيانات مرة واحدة واحتفظ بها: لاتينية بعلامات نبر، وعربية بحركات ومن دونها، وديفاناغارية بعلامات صوتية، ورمز تعبيري بلون بشرة، والاسم نفسه بصيغتي NFC وNFD. يولّد مولّد بيانات يونيكود سلاسل من كل نوع مع أعدادها، ويعطيك عدّاد طول السلسلة الأعداد الأربعة لأي سلسلة تضيفها بنفسك.
  • أدخل كل سلسلة، واحفظ، وأعد تحميل الملف الشخصي، وقارن النص المعروض بالمدخل في أداة مقارنة النصوص؛ ثم اجلب المستخدم نفسه من الواجهة البرمجية، وقارن بايتات قيمة JSON مكتوبةً بالست عشري ببايتات الأصل.
  • اختبر الحدّ: علي مكررة عشر مرات هي 30 حرفاً و60 بايتاً. فإن رُفضت بينما يُقبل 30 حرفاً لاتينياً فالحدّ يعدّ البايتات؛ وإن عُدّ 👍🏽 أربعة فهو يعدّ وحدات UTF-16. سجّل أيهما، واسأل هل قصد المتطلب المحارف كما يدركها المستخدم.
  • ابحث عن José بصيغة NFD بعد حفظها بصيغة NFC، والعكس؛ ثم افتح تصدير CSV في برنامج جداول وفي محرر نصوص بسيط وابحث عن Ã وØ و? و�.
  • ولّد رسالة بريد واحدة وملف PDF واحداً يتضمنان الاسم، واقرأ سجل الخادم حيث تظهر العربية غالباً على هيئة تسلسلات هروب مثل \u0639: وهذا صحيح ما دام عارض السجلات يفكّها، ومفكّك ترميز يونيكود يعيدها نصاً مقروءاً.
POST /api/v1/profile HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"displayName":"علي"}

GET /api/v1/profile HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{"displayName":"علي"}         OK: value bytes D8 B9 D9 84 D9 8A
{"displayName":"علي"}      mojibake: read once as Windows-1252, saved again
{"displayName":"???"}         lost: converted to an encoding without Arabic

جرّبها: مولّد مجموعات بيانات Unicode جرّبها: عدّاد طول السلاسل النصية جرّبها: مقارنة النصوص (Diff) جرّبها: مفكّك ترميز Unicode

أخطاء شائعة

  • الاختبار ببيانات إنجليزية أو ASCII فقط، وهي بيانات يعالجها كل ترميز معالجة صحيحة.
  • معاملة المحارف والبايتات شيئاً واحداً، فيُسمّى متطلب 30 حرفاً وعمود 30 بايتاً وفحص JavaScript من 30 وحدة كلها «30».
  • لوم البيانات أو الخط حين يكون مفكّك الترميز هو الخاطئ: علي تعني أن البايتات كانت سليمة حتى قرأتها طبقة ما على أنها Windows-1252.
  • نسيان المحارف رباعية البايتات: فالعمود الذي يخزّن العربية والهندية قد يرفض الرموز التعبيرية (utf8 في MySQL يتسع لثلاثة بايتات على الأكثر، وutf8mb4 يتسع ليونيكود كله).
  • مقارنة السلاسل أو البحث فيها من دون صيغة تطبيع متفق عليها، ثم الإبلاغ بأن «البحث معطّل» من دون نقاط الترميز التي تثبت ذلك.
  • الوثوق بالشاشة: قد تعرض صفحة الملف الشخصي الاسم صحيحاً بينما يشوّهه البريد وملف PDF وتصدير CSV كلٌّ بطريقته.

من منظور ضمان الجودة

يُحسم الترميز في مراجعة التصميم لا بعد ظهور العيب. فعند ظهور حقل أو استيراد أو تصدير أو تكامل جديد، اسأل أي ترميز تستخدمه كل محطة، وأي صيغة تطبيع تُطبَّق، وكيف يُحسب حدّ الطول. اسأل المطور عن مجموعات محارف الأعمدة واتصال قاعدة البيانات، ومهندس DevOps عن الإعدادات المحلية للخوادم والحاويات، لأن إعداداً افتراضياً للخادم ليس UTF-8 مصدر شائع للنص المشوّه في السجلات والملفات المولَّدة.

في تقرير العيب، اذكر المدخل بالضبط بنقاط الترميز والبايتات (U+0639 U+0644 U+064A وD8 B9 D9 84 D9 8A)، والطبقة التي انكسر عندها، وما الذي عاد، والترميز الذي تظن أن البايتات قُرئت به. واحتفظ بمجموعة البيانات في حزمة اختبارات الانحدار: فعيوب الترميز تعود كلما تغيّرت مكتبة أو برنامج تشغيل (Driver) أو قالب تصدير.

تمرين عملي

قِس مجموعة بيانات وتوقّع أين ينكسر حقل الاسم

المطلوب

يقول نموذج الملف الشخصي: «اسم العرض: من ⁦2⁩ إلى ⁦30⁩ حرفاً». لم يحدد الفريق كيف يعدّ، والاسم يُطبع أيضاً في تصدير CSV. باستخدام مجموعة البيانات أدناه، اكتب الأعداد الأربعة لكل سلسلة، ثم توقّع أي السلاسل يقبلها حدّ يعدّ البايتات، وحدّ يعدّ وحدات UTF-16، وحدّ يعدّ العناقيد الرسومية.

وأخيراً اكتب كيف ستبدو نسخة مشوّهة من الاسم العربي في تصدير CSV، لتتعرّف عليها حين تراها.

المادة
1  Ali
2  علي
3  مُحَمَّد          (with diacritics)
4  José            (é as U+00E9)
5  José            (e + U+0301)
6  हिंदी
7  👍🏽              (thumbs up + skin tone)
8  علي x 10        (repeated, no spaces)
الخطوات
  1. سجّل لكل سلسلة العناقيد الرسومية ونقاط الترميز ووحدات UTF-16 وبايتات UTF-8 (يعرض عدّاد طول السلسلة الأربعة كلها).
  2. حدّد أي السلاسل يقبلها كل نوع من الحدود للنطاق من ⁦2⁩ إلى ⁦30⁩.
  3. اذكر ما سيراه المستخدم لو قُرئ ملف CSV على أنه Windows-1252.
إظهار الإجابة النموذجية
الإجابة النموذجية

الأعداد (عناقيد / نقاط ترميز / UTF-16 / بايتات): Ali ⁦3/3/3/3⁩؛ علي ⁦3/3/3/6⁩؛ مُحَمَّد ⁦4/8/8/16⁩؛ José بصيغة NFC ⁦4/4/4/5⁩؛ José بصيغة NFD ⁦4/5/5/6⁩؛ हिंदी ⁦2/5/5/15⁩؛ 👍🏽 ⁦1/2/4/8⁩؛ علي × 10 هي ⁦30/30/30/60⁩.

حدّ يعدّ البايتات بقيمة 30 يرفض السلسلة 8 (60 بايتاً) مع أنها 30 حرفاً، ويقبل البقية. وحدّ وحدات UTF-16 يقبل الثماني كلها لكنه يعدّ الرمز التعبيري أربعة والاسم المشكول ثمانية، فلا يستطيع المستخدم إدخال أكثر من سبعة رموز تعبيرية كهذا. وحدّ العناقيد الرسومية يطابق توقع المستخدم ويقبل كل السلاسل عدا السابعة: فالرمز 👍🏽 عنده محرف واحد فيخالف الحدّ الأدنى 2، بينما يقبله عدّ نقاط الترميز أو وحدات UTF-16، وتقع हिंदी على الحدّ الأدنى تماماً. اسأل مالك المنتج أي عدّ هو المقصود.

السلسلتان 4 و5 تبدوان متطابقتين؛ وما لم يطبّع المنتج فهما اسمان مختلفان في البحث وفحص التفرّد، وعدّ العناقيد يخفي الفرق.

ملف CSV المقروء على أنه Windows-1252 سيعرض علي بدلاً من علي: بايتات UTF-8 وهي D8 B9 D9 84 D9 8A معروضة بايتاً بايتاً عبر الجدول الغربي. والبايتات في الملف ما زالت صحيحة، فإعادة فتحه بترميز UTF-8 تعرض علي من جديد؛ ولا ينتشر الضرر إلا إذا حفظ أحدهم العرض المشوّه بترميز UTF-8.

أسئلة شائعة

ما الفرق بين يونيكود وUTF-8؟

يونيكود هو مجموعة المحارف: يعطي كل محرف نقطة ترميز مثل U+0639. وUTF-8 ترميز واحد لهذه المجموعة: قاعدة تكتب كل نقطة ترميز في بايت واحد إلى أربعة بايتات. ويرمّز UTF-16 وUTF-32 نقاط الترميز نفسها بطريقة مختلفة؛ أما Windows-1256 فمجموعة محارف مختلفة وأصغر بكثير ولها قيم بايتات خاصة بها.

لماذا يبدو النص صحيحاً على الشاشة وخاطئاً في التصدير أو البريد؟

لأن كل مسار يعلن ترميزه الخاص. فصفحة الويب تحمل charset=utf-8 في ترويساتها، أما ملف CSV أو مولّد PDF أو قالب البريد فقد يفترض ترميزاً آخر أو يغفل علامة ترتيب البايتات التي تحتاجها بعض برامج الجداول. اختبر كل قناة إخراج على حدة بمجموعة البيانات نفسها.

هل ينبغي للتطبيق أن يرفض المدخلات غير المعتادة أم أن يطبّعها؟

طبّع المعرّفات مثل أسماء المستخدمين والبريد وعبارات البحث إلى صيغة واحدة (NFC عادةً) عند الحدّ، ثم خزّن النص المطبَّع وقارنه؛ ولا تغيّر النص الحر مثل نص الرسالة بصمت أبداً. ورفض الحركات العربية أو الرموز التعبيرية لأن عموداً لا يستطيع تخزينها عيب في العمود لا في المدخل.

هل علامة ترتيب البايتات ضرورية في ملفات UTF-8؟

لا. يسمح معيار يونيكود بعلامة ترتيب البايتات في UTF-8 لكنه لا يشترطها ولا يوصي بها؛ وهي البايتات EF BB BF في بداية الملف. تستخدمها بعض برامج الجداول للتعرف على ملفات CSV بترميز UTF-8، بينما تعاملها بعض المحلّلات جزءاً من الحقل الأول. اختبر الملفات بالطريقتين وأبلغ عمّا يفعله بها البرنامج الذي يقرؤها.

المراجع

  • The Unicode StandardUnicode Consortiumمجموعة المحارف العالمية وأشكال ترميزها (UTF-8 وUTF-16 وUTF-32) والتطبيع وخصائص كل نقطة ترميز.
  • RFC 3629: UTF-8, a transformation format of ISO 10646IETF · 2003كيف تُرمَّز نقاط الترميز إلى بايت واحد حتى أربعة بايتات في UTF-8، وأي تسلسلات بايت غير صالحة.
  • UAX #15: Unicode Normalization FormsUnicode Consortiumصيغ التطبيع NFC وNFD وNFKC وNFKD — سبب اختلاف سلسلتين متطابقتين بصرياً عند المقارنة قبل التطبيع.
  • UAX #29: Unicode Text SegmentationUnicode Consortiumكيفية تقسيم النص إلى عناقيد رسومية وكلمات وجمل — ولماذا قد يختلف عدد «الأحرف» عن عدد نقاط الترميز أو البايتات.

تدرّب بأدوات متقن

تحويل النص إلى بايتات سداسية عشرية (UTF-8) مع فواصل مخصصة وحالة أحرف قابلة للتحديد.

الترميز

تحويل النص إلى تسلسلات تهريب Unicode (\u0041) أو نقاط الترميز (U+0041) أو كيانات HTML الرقمية.

الترميز

فكّ ترميز تهريبات \uXXXX ونقاط الترميز U+ والكيانات الرقمية إلى نص مقروء.

الترميز

ولّد سلاسل نصية اختبارية بترميز Unicode عبر الخطوط (العربية والديفاناغارية وCJK والسيريلية) والرموز التعبيرية وعلامات الدمج وصيغ التطبيع.

بيانات الاختبار

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة