الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

يونيكود وUTF-8 للمبتدئ: ليه طول النص بيطلع غلط مع الإيموجي والعربي

مبتدئ24 يوليو 20265 دقائق قراءة
يونيكود وUTF-8 للمبتدئ: ليه طول النص بيطلع غلط مع الإيموجي والعربي

هذا المقال لمستوى: مبتدئ

يونيكود وUTF-8 للمبتدئ: ليه طول النص بيطلع غلط مع الإيموجي والعربي

لو بتحدّد طول اسم المستخدم بـ name.length، فيه احتمال كبير إن الكود بتاعك بيقص أسماء عربية غلط وبيحسب الإيموجي خانتين. المقال ده هيوريك بالظبط ليه بيحصل ده، وإزاي تعدّه في سطر واحد. اللي هتكسبه: تعرف الفرق بين ثلاث حاجات الناس بتخلط بينها، البايت، ووحدة الترميز، ونقطة الرمز.

المشكلة باختصار

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

مثال بسيط الأول

تخيّل إنك بتشحن طرود في شاحنة. القاعدة عندك: كل صنف بياخد صندوق واحد. ده صح طول ما الأصناف كلها صغيرة. لكن أول ما ييجي صنف كبير بياخد أربع صناديق، عدّة الصناديق بتاعتك بتبقى غير عدّة الأصناف. الحروف زي كده بالظبط. الحرف الإنجليزي A بياخد صندوق واحد، الحرف العربي بياخد صندوقين، والإيموجي ممكن ياخد أربعة. لو انت بتعدّ الصناديق وفاكرها أصناف، الرقم هيطلع غلط.

الشرح العلمي: ثلاث كلمات لازم تفرّق بينهم

النص عند الكمبيوتر بيمر بثلاث طبقات، وكل طبقة ليها طول مختلف:

  • نقطة الرمز (Code Point): الرقم اللي يونيكود بيدّيه لكل حرف. مثلًا 🌍 رقمها U+1F30D. دي أقرب حاجة للحرف اللي انت شايفه.
  • وحدة الترميز (Code Unit): الطريقة اللي الذاكرة بتخزّن بيها نقطة الرمز. جافاسكريبت بتستخدم UTF-16 داخليًا، فأي رمز فوق U+FFFF بيتخزّن في وحدتين (اسمهم surrogate pair). وده اللي length بيعدّه فعلًا.
  • البايت (Byte): لمّا تحفظ النص في ملف أو قاعدة بيانات بترميز UTF-8، كل نقطة رمز بتتحوّل لبايت لحد أربعة بايتات.

يعني كلمة "طول النص" لوحدها من غير ما تحدد أي طول، جملة ناقصة.

الكود اللي بيحصل فعلاً

JavaScript
// جافاسكريبت بتعدّ وحدات UTF-16، مش الحروف
"🌍".length            // 2  ← وحدتان (surrogate pair)
[..."🌍"].length       // 1  ← نعدّ نقاط الرمز الصح

// الاسم العائلي للإيموجي أسوأ
"👨‍👩‍👧".length         // 5  ← تلات وجوه + محرفَي ربط خفيين
[..."👨‍👩‍👧"].length    // 5  برضه، لأنها نقاط رمز متعددة

// عدد البايتات في UTF-8 (Node.js)
Buffer.byteLength("🌍", "utf8")   // 4
Buffer.byteLength("م", "utf8")    // 2
Buffer.byteLength("A", "utf8")    // 1

وبايثون كمان

Python
# بايثون بتعدّ نقاط الرمز، فأحسن شوية من JavaScript
len("🌍")                  # 1
len("🌍".encode("utf-8"))  # 4  ← البايتات
len("A".encode("utf-8"))   # 1
len("م".encode("utf-8"))   # 2

الخلاصة من الكودين: length في جافاسكريبت بيعدّ وحدات UTF-16، وبايثون بتعدّ نقاط الرمز، ولا واحد فيهم بيعدّ "الحروف المرئية" (الـ grapheme clusters) اللي المستخدم بيشوفها.

سيناريو واقعي بالأرقام

الافتراض: عندك جدول في MySQL فيه عمود VARCHAR(255) بترميز utf8mb4، وبتخزّن فيه تعليقات فيها إيموجي. لو عندك مليون تعليق، متوسط طول كل واحد 100 حرف مرئي، ومنهم 10 إيموجي:

  • 90 حرف لاتيني تقريبًا = 90 بايت.
  • 10 إيموجي × 4 بايت = 40 بايت.
  • المجموع 130 بايت للتعليق، مش 100.

يعني حساباتك للمساحة هتقل 30% عن الحقيقة لو افترضت إن كل حرف بايت. على مليون صف، ده فرق حوالي 30 ميجابايت مش محسوبين، من غير الفهرسة. ولو كنت حاطط VARCHAR(10) ظانًّا إنها 10 حروف، الاسم "سلمى👩‍💻" ممكن يتقص لأنه بياخد وحدات ترميز أكتر من 10.

الحل العملي

لو عايز تعدّ الحروف اللي المستخدم بيشوفها فعلًا، استخدم Intl.Segmenter، ده الطريقة الرسمية والمدعومة في كل المتصفحات الحديثة وNode 16 فما فوق:

JavaScript
function countGraphemes(str) {
  const seg = new Intl.Segmenter("ar", { granularity: "grapheme" });
  return [...seg.segment(str)].length;
}

countGraphemes("👨‍👩‍👧")   // 1  ← أخيرًا الرقم الصح
countGraphemes("سلمى👩‍💻")  // 5  ← 4 حروف + إيموجي واحد

القاعدة العملية: لو بتقيس طول للعرض للمستخدم (حد أقصى للاسم أو التغريدة) استخدم Intl.Segmenter. لو بتحسب مساحة تخزين استخدم عدد البايتات في UTF-8. لو بتقارن نصوص أو تعمل مفاتيح، اعمل normalize("NFC") الأول عشان الحرف الواحد ما يتخزّنش بأكتر من شكل.

الـ trade-offs

كل اختيار وله ثمنه. Intl.Segmenter بيدّيك العدّ الصح، مقابل إنه أبطأ من length بمراحل، لأنه بيمر على النص ويطبّق قواعد يونيكود لتقطيع الحروف. على نص قصير الفرق مش محسوس، لكن لو بتعدّ ملايين النصوص في حلقة، هتحس بيه. الـ UTF-8 نفسه trade-off: بيوفّر مساحة كبيرة مع النصوص اللاتينية (بايت لكل حرف)، بس النص العربي والصيني بياخد فيه بايتين لتلاتة، أكتر من UTF-16 في بعض الحالات.

متى متشغلش بالك

لو التطبيق بتاعك بيتعامل مع ASCII بس (أكواد، معرّفات، أرقام)، length تمام وما تعقّدش حياتك. كمان لو بتعدّ عشان تحفظ في عمود بيقيس بالبايت، متستخدمش عدّ الحروف أصلًا، استخدم عدد البايتات مباشرة. المبالغة في عدّ الـ graphemes لكل حاجة بتضيف تعقيد وبطء من غير فايدة لو مفيش مدخلات من المستخدم فيها إيموجي أو حروف مركّبة.

الخطوة التالية

افتح كود التحقق من المدخلات عندك دلوقتي، ودوّر على أي .length بتطبّقه على نص جاي من المستخدم. بدّله بـ [...str].length كحد أدنى، أو Intl.Segmenter لو عايز الدقة الكاملة. جرّبه على المدخل 👨‍👩‍👧، لو رجّع 1 يبقى شغّال صح، ولو رجّع 5 أو أكتر يبقى لسه بيعدّ غلط.

المصادر

  • معيار يونيكود الرسمي، الإصدار 15: unicode.org/versions/latest
  • تقطيع الحروف (Grapheme Clusters) — Unicode UAX #29: unicode.org/reports/tr29
  • تعريف UTF-8 الرسمي — RFC 3629: rfc-editor.org/rfc/rfc3629
  • MDN — String.prototype.length ووحدات UTF-16: MDN String length
  • MDN — Intl.Segmenter: MDN Intl.Segmenter
  • توثيق MySQL — ترميز utf8mb4 وطول البايت: dev.mysql.com/doc/refman utf8mb4

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة