في جملة واحدة
يونيكود هو المعيار العالمي للنصوص الذي يجعل إنترنتنا العالمي ممكنًا، لكن اتساعه الهائل يضم حروفًا غير مرئية، وأخرى متشابهة، وغرائب أخرى يمكن أن تحول نصًا بسيطًا ظاهريًا إلى حقل ألغام من الأخطاء البرمجية.
المشكلة التي يحلها
في حساء الحوسبة البدائي، كانت الحياة بسيطة. كان لدينا ASCII. لقد أعطانا 127 حرفًا: حروف كبيرة، حروف صغيرة، أرقام، علامات ترقيم، وبعض رموز التحكم. كان الأمر مرتبًا ومنظمًا ويتناسب تمامًا مع بايت واحد. ولكنه كان أيضًا ذا توجه إنجليزي بحت بشكل يائس. إذا أردت كتابة ¿Qué pasa? أو 你好 أو спасибо، فلم يكن لديك حظ.
أدى هذا إلى حقبة فوضوية تُعرف بـ "جحيم الـ codepages". كانت أجهزة الكمبيوتر في مناطق مختلفة تستخدم مجموعات حروف مختلفة بحجم 8 بت توسع معيار ASCII. أي مستند مكتوب بترميز Windows-1252 (لأوروبا الغربية) كان سيتحول إلى فوضى مشوهة (ما نحب أن نسميه بمودة mojibake) عند فتحه على نظام يستخدم KOI8-R (للغة الروسية). كانت مشاركة النصوص دوليًا أشبه بلعبة التلفون الخربان.
ثم، جاء اتحاد يونيكود (Unicode Consortium) على حصان أبيض لإنقاذ الموقف. كانت مهمتهم: مجموعة حروف واحدة وموحدة لجميع أنظمة الكتابة الحديثة والتاريخية. معيار واحد ليسود على الجميع. كل حرف—من 'A' إلى '€' إلى إيموجي 'كومة البراز' (💩)—سيحصل على رقمه الفريد الخاص، أو ما يسمى بـ "code point".
لقد كان إنجازًا هائلاً يدعم عالمنا الحديث. لكن هذا التوحيد العظيم خلق مجموعته الخاصة من المشاكل "النيردية" الرائعة. لاستيعاب تعقيدات اللغات البشرية، كان على يونيكود أن يضم أكثر من مجرد حروف مرئية. لقد احتاج إلى:
- الحروف المدمجة (Combining characters): علامة نبرة (
´) هي حرف منفصل، مصممة لتوضع فوق حرف آخر (e). - حروف عديمة العرض (Zero-width characters): علامات غير مرئية يمكن أن تقترح فاصل أسطر (
U+200B Zero-Width Space) أو تلصق الإيموجيز معًا (U+200D Zero-Width Joiner). - الحروف الملتبسة: العشرات من أنواع المسافات والشرطات وعلامات الاقتباس المختلفة.
- الحروف المتشابهة (Homoglyphs): الحرف اللاتيني
aوالحرف الكيريليаيبدوان متطابقين في العديد من الخطوط، ولكن بالنسبة للكمبيوتر، هما مختلفان تمامًا مثلaوb.
فجأة، ما تراه ليس هو ما تحصل عليه. نص يبدو مثل "cat" قد يحتوي على حرف غير مرئي، مما يجعل طوله 4 وليس 3. اسم متغير مثل safeString قد يخفي حرف 'α' يونانيًا بدلاً من 'a' لاتيني. هذه هي المشكلة التي يحلها مفتش النصوص: يرتدي نظارات الأشعة السينية ليُريك الحقيقة الخام وغير المفلترة لما يتكون منه النص فعليًا، كاشفًا عن الأشباح الخفية في الآلة.
كيف يعمل في الكواليس
لتشريح نص ما، نحتاج إلى فهم طبقاته الأساسية الثلاث: الحرف المجرد، وتمثيله بالبايتات، والأشياء الغريبة التي تختبئ بينهما.
نقاط الترميز (Code Points): عنوان الحرف
في جوهره، يونيكود هو مجرد قائمة ضخمة. كل حرف يتم تعيين رقم فريد له يسمى نقطة ترميز (code point). هذا هو العنوان الدائم للحرف في عالم يونيكود. نكتبها باستخدام الصيغة U+XXXX، حيث XXXX هو رقم بالنظام الست عشري.
U+0041هوA(Latin Capital Letter A)U+00E9هوé(Latin Small Letter E with Acute)U+20ACهو€(Euro Sign)U+1F4A9هو💩(Pile of Poo)
الـ code point فكرة مجردة. إنه ليس بايتًا أو خطًا. إنه مجرد رقم مرتبط بحرف. كيف نخزن هذا الرقم هي قصة أخرى.
التكويد (Encodings): تخزين نقاط الترميز في بايتات
لا يمكنك حفظ "code point" في ملف. عليك أن تحفظ بايتات. التكويد (encoding) هو مجموعة من القواعد لتحويل سلسلة من الـ code points إلى سلسلة من البايتات.
ملك التكويد اليوم هو UTF-8. تكمن عبقريته في تصميمه متغير الطول.
- لأي حرف موجود أيضًا في مجموعة ASCII الأصلية (مثل
A،U+0041)، يستخدم UTF-8 بايتًا واحدًا—نفس البايت الذي استخدمه ASCII بالضبط. هذا جعله متوافقًا مع الأنظمة القديمة وسهل التبني. - بالنسبة للحروف الأخرى، يستخدم سلسلة من 2 أو 3 أو 4 بايتات. تعمل البتات القليلة الأولى من كل بايت كإشارات، تخبر الكمبيوتر بعدد البايتات التي تشكل جزءًا من الحرف الحالي.
لنلقِ نظرة على ¡Hola!:
| الحرف | Code Point | بايتات UTF-8 (بالنظام الست عشري) |
|---|---|---|
¡ |
U+00A1 |
C2 A1 |
H |
U+0048 |
48 |
o |
U+006F |
6F |
l |
U+006C |
6C |
a |
U+0061 |
61 |
! |
U+0021 |
21 |
يقوم مفتش النصوص بهذه العملية بشكل عكسي. يقرأ البايتات الخام للنص، يفسرها وفقًا لتكويد معين (عادةً UTF-8)، ويعرض لك سلسلة الـ code points التي يتكون منها.
المشاغبون غير المرئيين
هنا تبدأ المتعة. تتمثل المهمة الرئيسية لمفتش النصوص في تسليط الضوء على الحروف التي لا تبدو كشيء.
| الفئة | مثال للحرف ونقطة الترميز | الغرض الخبيث |
|---|---|---|
| مسافة عديمة العرض | U+200B |
تبدو كلاشيء. حرف غير مرئي يقترح مكانًا جيدًا لكسر السطر في كلمة طويلة أو URL. |
| رابط عديم العرض | U+200D |
الصمغ السحري للإيموجيز. 👨 + ZWJ + 👩 + ZWJ + 👧 = 👨👩👧. يربط حروفًا لا تتصل عادةً. |
| مسافة غير قابلة للكسر | U+00A0 |
تبدو كمسافة عادية، لكنها تمنع كسر السطر. مفيدة لأشياء مثل 100 كم أو Dr. Strange. |
| علامة دمج | U+0301 (Combining Acute Accent) |
نبرة (´) هي حرف بحد ذاتها. يتم رسمها فوق الحرف الذي يسبقها. |
| حرف متشابه (Homoglyph) | U+0430 (Cyrillic Small Letter A) |
يبدو مطابقًا للحرف اللاتيني a (U+0061) في معظم الخطوط، لكنه code point مختلف تمامًا. |
هذا يقودنا إلى مفهوم التوحيد القياسي (normalization). يمكن تمثيل الحرف é بطريقتين:
- الشكل المدمج (NFC): نقطة ترميز واحدة،
U+00E9. - الشكل المفكك (NFD): نقطتا ترميز،
e(U+0065) متبوعة بعلامة النبرة المدمجة´(U+0301).
بصريًا، هما متطابقان. ولكن بالنسبة لجهاز كمبيوتر يقوم بمقارنة بسيطة للبايتات، "\u00E9" لا يساوي "e\u0301". يمكن لمفتش النصوص أن يكشف لك أي شكل لديك ويساعدك على التحويل بينهما.
قصص من الواقع
كارثة النسخ واللصق
مطور مبتدئ يعمل في وقت متأخر، محاولاً إصلاح خطأ برمجي. يجد حلاً على مدونة، سطر واحد من جافاسكريبت: const timeout = 100;. ينسخه، يلصقه في محرر الكود، ويضغط على حفظ. يفشل بناء التطبيق بالكامل مع خطأ غامض SyntaxError: Invalid or unexpected token.
يحدق في السطر. إنه مثالي. يعيد كتابته يدويًا. يعمل. يلصق السطر المنسوخ مرة أخرى. يتعطل. هل هو على وشك أن يجن؟ بعد ساعة من شد شعره، يمعن مطور أقدم النظر في السطر ويقول: "الصق ذلك في مفتش نصوص".
النتيجة: const[U+00A0]timeout[U+00A0]=[U+00A0]100;. كان الـ CSS الخاص بالمدونة قد جمّل الكود، واستبدل المسافات القياسية (U+0020) بمسافات غير قابلة للكسر (U+00A0). تبدوان متطابقتين، لكن محرك جافاسكريبت ليس لديه أي فكرة عن ماهية "المسافة غير القابلة للكسر" في هذا السياق.
العبرة: النص المنسوخ من الويب (أو ملفات PDF، أو مستندات Word) متهم حتى تثبت براءته. غالبًا ما يكون ملوثًا بعلامات اقتباس "ذكية"، ومسافات غير قياسية، وعفاريت خفية أخرى.
المستخدم الشبح الذي لم يستطع تسجيل الدخول
يسجل مستخدم جديد في خدمة بالاسم François. يقوم النظام بإنشاء الحساب بسعادة. في اليوم التالي، يحاول فرانسوا تسجيل الدخول. يكتب اسمه، يضغط على إدخال... "اسم المستخدم أو كلمة المرور غير صالحة". يحاول مرة أخرى، بحذر. نفس النتيجة. لقد تم إقفال حسابه.
في قاعدة البيانات، تم تخزين اسمه باستخدام حروف مفككة: F، r، a، n، c، o، i، s و U+0327 (Combining Cedilla). لكن نموذج تسجيل الدخول، كان يرسل الحرف المدمج ç (U+00E7). بصريًا، c + ¸ هو نفسه ç. لكن الخادم كان يقوم بمقارنة نصوص بسيطة: François (مفكك) لا يساوي François (مدمج). فشل استعلام WHERE username = '...'.
العبرة: قم دائمًا بتوحيد مدخلات المستخدم إلى شكل ثابت (NFC هو الخيار الأكثر شيوعًا) قبل تخزينها في قاعدة بيانات أو إجراء مقارنات.
النطاق الخادع
يتلقى موظف بريدًا إلكترونيًا يبدو أنه من قسم تكنولوجيا المعلومات في شركته. "تحديث أمني مطلوب: يرجى تسجيل الدخول إلى microsоft.com/update لتأمين حسابك." الرابط يبدو شرعيًا. اسم النطاق واضح. ينقر عليه، يدخل بيانات اعتماده في صفحة تبدو تمامًا مثل الصفحة الحقيقية، ويمضي في يومه.
لقد وقع للتو ضحية للتصيد الاحتيالي (phishing). لم يكن النطاق microsoft.com. بل كان microsоft.com. الحرف 'o' الثاني لم يكن الحرف اللاتيني 'o' (U+006F) بل الحرف الكيريلي 'о' (U+043E). هذا هجوم تماثل الرسوم البيانية لأسماء النطاقات الدولية (IDN Homograph Attack). للعين البشرية، إنه تزوير مثالي. لنظام أسماء النطاقات (DNS)، إنه عنوان مختلف تمامًا، يؤدي إلى خادم المحتال.
العبرة: كن شديد الشك في المعرّفات التي تخلط بين مجموعات الحروف. على الرغم من أن المتصفحات الحديثة لديها بعض الحماية، فإن مبدأ هجمات تماثل الرسوم البيانية يمثل تهديدًا مستمرًا في أسماء المستخدمين، وقواعد التحقق من الصحة، وفي أي مكان آخر تستخدم فيه النصوص للأمان.
أخطاء وفخاخ شائعة
- افتراض أن
string.lengthيحسب الحروف. في العديد من اللغات (مثل جافاسكريبت)، فإنه يحسب وحدات الترميز، وليس الحروف المدركة. على سبيل المثال،"👍🏽".lengthهو 4 في JS، لأنه يتكون من إيموجي "إعجاب" (👍، وحدتان) و "معدّل لون البشرة المتوسط" (🏽، وحدتان). - التعامل مع كل المسافات البيضاء على أنها متساوية. تشغيل
trim()على نص لن يزيلU+200B Zero-Width Spaceالمختبئ في المنتصف. قد لا يلتقط تعبير نمطي (regex) مثل\s+المسافةU+00A0 Non-Breaking Space. عليك أن تعرف ما الذي تبحث عنه. - تجاهل التوحيد القياسي (normalization). كما رأينا مع فرانسوا، مقارنة النصوص التي تبدو متشابهة ولكن لها تمثيلات بايت أساسية مختلفة هو خطأ كلاسيكي ومحبط.
string1.normalize() === string2.normalize()هو صديقك. - الثقة بعينيك. لا يمكنك تصحيح هذه المشكلات بالنظر إلى النص المعروض. مفتش النصوص الذي يعرض نقاط الترميز الفردية وأسماءها هو الطريقة الوحيدة للتأكد مما هو موجود حقًا.
- محاولة كتابة 'منظف' خاص بك للحروف الغريبة. محاولة كتابة تعبير نمطي (regex) لإزالة كل الحروف "الغريبة" هي مهمة عبثية. إما أنك ستفوت بعضها، أو الأسوأ من ذلك، ستزيل حروفًا شرعية مطلوبة للغات أخرى، مما يؤدي إلى تشويه أسماء المستخدمين ونصوصهم.
لماذا يجب أن تضعه على رادارك
يجب أن تلجأ إلى مفتش نصوص يونيكود كلما تصرف نص ما بشكل غير متوقع. إنها أداة لا غنى عنها لتصحيح الأخطاء (debugging). فكر في استخدامه عندما:
- تفشل مقارنة نصوص بينما "من الواضح" أنها يجب أن تنجح.
- تحصل على خطأ في بناء الجملة (syntax error) في سطر من الكود يبدو صالحًا تمامًا.
- تقوم بالتحقق من صحة المدخلات التي يقدمها المستخدم مثل أسماء المستخدمين أو رسائل البريد الإلكتروني أو عناوين URL.
- تعمل مع بيانات من أنظمة متعددة، خاصة إذا كانت تتضمن لغات مختلفة.
- تحتاج إلى فهم سبب إعطاء
string.lengthلك رقمًا "خاطئًا". - تقوم ببناء أي نظام يحتاج إلى أن يكون قويًا وآمنًا ويعمل لجمهور عالمي.
باختصار، في أي وقت يختلف فيه الحاسوب والإنسان حول ما يقوله نص ما، فمن المرجح أن الحاسوب على حق بشأن البايتات، ومفتش النصوص هو مترجمك.
تعمق أكثر
- The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) — المقالة الأسطورية لجويل سبولسكي. لا بد من قراءتها.
- Unicode (Wikipedia) — نظرة عامة معمقة وشاملة لتاريخ المعيار وتفاصيله التقنية.
- UTF-8 (RFC 3629) — المواصفات التقنية لتكويد الحروف المهيمن على الإنترنت. مكثف، لكنه المصدر الموثوق.
- String.prototype.normalize() (MDN Web Docs) — إرشادات عملية للتعامل مع الـ normalization في جافاسكريبت.
- The Unicode Consortium — المصدر الرسمي. ينشرون المعايير، ومخططات الرموز، والتقارير التقنية.