مدوّنة رؤيةأنظمة الأعمال١٣ أغسطس ٢٠٢٦ · 10 د قراءة

لماذا تفشل حدود الكلمات مع النصوص العربية؟ 4 مصائد برمجية

حدود الكلمات في جافاسكريبت وPHP معرّفة على الحروف اللاتينية وحدها، فـ لا يطابق العربية أبدًا. أربع مصائد ضربتنا فعليًا، وحلولها بالكود.

شبقلم شركة رؤيةاقرأ المقال ↓
لماذا تفشل حدود الكلمات مع النصوص العربية؟ 4 مصائد برمجية
٠١المقال

كتبنا تعبيرًا نمطيًا يلتقط الأسئلة من مقالاتنا العربية. اشتغل. استخرج 142 سؤالًا من 39 مقالًا.

وهذه أول مصيدة من أربع تعلّمناها بالطريق الصعب في التعبيرات النمطية العربية.

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

رسم يوضح أن حدود الكلمات تعمل مع الحروف اللاتينية وتفشل تمامًا مع الحروف العربية

الحرف الذي لا يعمل مع العربية

في التعبيرات النمطية، \b يعني «حدّ كلمة». وحدود الكلمات هي الموضع: الموضع بين حرف كلمة وحرف ليس كلمة. تستعمله لتضمن أن «قط» تطابق كلمة «قط» وحدها لا «فقط» ولا «قطار».

ومصدر المشكلة أن حدود الكلمات العربية لا تُحسب أصلًا، لأن التعريف يبدأ من «حرف كلمة». في جافاسكريبت وPHP، \w يساوي [A-Za-z0-9_]. هذه هي القائمة كاملة.

الحروف العربية ليست فيها.

وبما أن \b معرّف على أنه الحدّ بين \w و\W، فالنتيجة أن كل حرف عربي يُصنَّف «ليس حرف كلمة». فلا يوجد حدّ بين حرفين عربيين، ولا بين حرف عربي ومسافة.

// جافاسكريبت
/\bقط\b/.test("قط")        // false
/\bقط\b/.test("فقط")       // false
/\bقط\b/.test("هذا قط")    // false

// PHP
preg_match('/\bقط\b/u', 'قط');   // 0

لاحظ السطر الأول: النمط لا يطابق الكلمة نفسها معزولة. هذا ليس فشلًا في حالة طرفية، بل فشل كامل.

لماذا لم نكتشفها فورًا

الخطأ نجا أسابيع لأن نتيجته لم تكن انهيارًا. كان التعبير يلتقط الأسئلة فعلًا، فبدا سليمًا.

شكل التعبير كان قريبًا من هذا:

/(?:\bما|\bكيف|\bهل|\bلماذا)[^؟]{5,120}؟/gu

القراءة الظاهرة: التقط جملة تبدأ بأداة استفهام وتنتهي بعلامة سؤال. القراءة الفعلية: التقط أي شيء ينتهي بعلامة سؤال، لأن كل بدائل البادئة ميتة.

وبما أن أغلب الجمل المنتهية بـ«؟» في المقال أسئلة حقيقية، جاءت النتائج معقولة. فمرّت المراجعة.

الفرق ظهر في الحالات التي كنا نظن أننا نستبعدها: عبارة تنتهي بعلامة استفهام داخل اقتباس، أو سطر تعليق ساخر، أو جملة استنكارية في متن المقال. كلها كانت تدخل الاستخراج على أنها أسئلة شائعة.

البديل: احسب حدود الكلمات بنفسك

لا توجد صيغة سحرية تجعل حدود الكلمات تعمل مع العربية. الحل أن تعرّف «حرف الكلمة» بنفسك ليشمل العربية، ثم تتحقق من الحرف السابق واللاحق.

// نطاقات الحروف العربية + اللاتينية + الأرقام
const LETTER = /[ء-يٱ-ۓA-Za-z0-9]/;

function isStandaloneWord(text, word, index) {
  const before = text[index - 1];
  const after  = text[index + word.length];
  const beforeIsLetter = before ? LETTER.test(before) : false;
  const afterIsLetter  = after  ? LETTER.test(after)  : false;
  return !beforeIsLetter && !afterIsLetter;
}

وفي PHP:

$letter = '~[\x{0621}-\x{064A}\x{0671}-\x{06D3}A-Za-z0-9]~u';

function roeia_is_standalone(string $text, string $word, int $pos, string $letter): bool {
    $before = $pos > 0 ? mb_substr($text, $pos - 1, 1) : '';
    $after  = mb_substr($text, $pos + mb_strlen($word), 1);
    $b = $before !== '' && preg_match($letter, $before);
    $a = $after  !== '' && preg_match($letter, $after);
    return ! $b && ! $a;
}

لاحظ mb_substr لا substr. الحرف العربي يشغل بايتين في UTF-8، ودوال البايت تقطعه نصفين فتنتج محارف تالفة.

ولاحظ أن النطاق يشمل \x{0671}-\x{06D3}. هذا يغطي الألف الوصلية والحروف الفارسية والأردية التي تظهر في النصوص العربية المنقولة. لو اكتفيت بالنطاق الأساسي، ستعامل هذه الحروف كفواصل فتقطع كلمات سليمة.

ماذا يحدث حين تتجاهلها

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

الكلمة المستهدفة طابقت داخل
زد يزداد · الازدهار · زدياد
مدى المدى · على مدى الطويل
سلة السلة · سلة التسوق

النتيجة كانت روابط داخل كلمات لا علاقة لها بالموضوع. كلمة «الازدهار» في فقرة عن نمو السوق تتحول إلى رابط لمنصة تجارة إلكترونية.

المصيدة الثانية: حدود الكلمات الصحيحة لا تكفي

بعد ضبط حدود الكلمات بقيت أخطاء. السبب أن بعض أسماء العلامات التجارية كلمات عربية شائعة.

«سلة» منصة متاجر، و«سلة التسوق» عبارة يستعملها كل مقال عن التجارة الإلكترونية. الحدود الصحيحة تطابق الاثنتين، لأن كلتيهما كلمة مستقلة فعلًا.

فأضفنا طبقتين:

شرط سياق: لا تربط إلا إذا وردت كلمة من مجال المنصة ضمن نحو 70 حرفًا حول الموضع (اسم منافس، أو مصطلح تقني، أو ذكر للمنصة كمنتج).

استثناء بما يلي الكلمة: إن تلا الكلمةَ «التسوق» أو «المشتريات» فهي عربة شراء لا منصة.

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

بالطبقتين معًا نزل عدد المقالات المرشحة من 39 (فيها أخطاء) إلى 22 مقالًا صحيحًا، و36 رابطًا، دون تغيير حرف واحد من النص المرئي.

المصيدة الثالثة: الإملاء نفسه ليس ثابتًا

العربية تُكتب بأشكال متعددة للحرف الواحد:

  • ة وه في آخر الكلمة: «تقنية» و«تقنيه»
  • ى وي: «على» و«علي»
  • أ إ آ وا: «إلكتروني» و«الكتروني»

‏Google يطبّع هذه الفروق في البحث، فيعامل الشكلين شيئًا واحدًا. وأدوات تحليل المحتوى لا تفعل. فقد يكون العنوان مكتوبًا بشكل والكلمة المفتاحية بشكل آخر، فتظهر النتيجة صفرًا رغم أن الكلمة موجودة.

أصاب هذا 40 صفحة عندنا وأسقط نتائجها.

الحل: طبّع للمقارنة، واستخرج الأصل من المصدر

الخطأ الشائع أن تطبّع النص ثم تستعمل النسخة المطبّعة في الكتابة، فتفسد الإملاء الصحيح. الصواب أن تستعمل التطبيع للمقارنة فقط، ثم تأخذ الصيغة الحقيقية من النص الأصلي بالموضع.

const fold = s => s
  .replace(/[أإآ]/g, 'ا')
  .replace(/ى/g, 'ي')
  .replace(/ة/g, 'ه');

const i = fold(title).indexOf(fold(keyword));
const correct = title.slice(i, i + keyword.length);   // الإملاء كما كتبه صاحبه

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

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

مصيدة رابعة: التشكيل والتطويل

حتى بعد ضبط الحدود والإملاء، تبقى محارف تكسر المطابقة الحرفية وهي غير مرئية تقريبًا في النص.

التشكيل (الفتحة والضمة والكسرة والشدة والسكون) محارف مستقلة تُضاف فوق الحرف أو تحته. كلمة «مُحَمَّد» المشكّلة لا تساوي «محمد» في المقارنة الحرفية، رغم أنهما الكلمة نفسها للقارئ.

التطويل (الكشيدة) محرف يمدّ الخط بين الحروف: «مـحـمـد». يستعمله المصممون لضبط المحاذاة، ويظل جزءًا من النص.

const stripMarks = s => s
  .replace(/[ً-ْٰ]/g, '')   // التشكيل
  .replace(/ـ/g, '');                   // التطويل

انتبه: هذا التنظيف يغيّر الطول. فإن كنت تعتمد على تطابق المواضع بين الأصل والمنظَّف (كما في حيلة slice أعلاه) فسينكسر الضمان. طبّق حذف التشكيل على مسار منفصل، أو ابنِ خريطة مواضع صريحة تربط كل حرف في النسخة المنظَّفة بموضعه في الأصل.

وهناك محرف ثالث أقل شيوعًا: الفاصل الصفري (\u200C و\u200F). يظهر في نصوص منسوخة من ملفات ووثائق، ولا يُرى إطلاقًا، ويكسر المطابقة كما يكسرها أي حرف آخر. أضفه إلى قائمة التنظيف.

والأرقام أيضًا

العربية تُكتب بمجموعتي أرقام: العربية الغربية (0-9) والعربية الشرقية (٠-٩). المستعمل نفسه قد يخلط بينهما في المقال الواحد.

const foldDigits = s => s.replace(/[٠-٩]/g, d =>
  String(d.charCodeAt(0) - 0x0660)
);

هذا يهمّك تحديدًا إن كنت تستخرج أسعارًا أو تواريخ أو أرقام هواتف. بحث عن «2026» لن يجد «٢٠٢٦».

كيف نختبر هذا النوع من الأخطاء الآن

الدرس الذي غيّر طريقتنا: الاختبار الذي يثبت أن النمط لا يعطي نتائج خاطئة لا يثبت شيئًا. نمط ميت يجتازه دائمًا.

صرنا نكتب لكل نمط عربي ثلاث حالات على الأقل:

  1. حالة موجبة معزولة: الكلمة وحدها. إن لم تطابق فالنمط معطّل، وينتهي الفحص هنا.
  2. حالة موجبة في سياق: الكلمة داخل جملة كاملة بمسافات وعلامات ترقيم.
  3. حالة سالبة: الكلمة كجزء من كلمة أطول. يجب ألا تطابق.
// مثال: نمط يلتقط «قط» ككلمة مستقلة
assert(matches('قط'), 'يجب أن يطابق الكلمة معزولة');
assert(matches('عندنا قط في البيت'), 'يجب أن يطابق داخل جملة');
assert(!matches('فقط'), 'يجب ألا يطابق داخل كلمة أطول');

الحالة الأولى هي التي كانت ستكشف خطأنا في دقيقة. لم نكتبها لأنها بدت بديهية إلى حد السخافة: من يشك في أن نمطًا يطابق الكلمة التي كُتب من أجلها؟

ونضيف فحصًا بشريًا: بعد أي تشغيل بالجملة، اقرأ عشرين نتيجة عشوائية بعينك. الأرقام الإجمالية تكذب بلطف. «استخرجنا 142 عنصرًا» رقم يبعث على الاطمئنان، ولا يقول لك إن نصفها لا يخص الموضوع.

لماذا تخذلنا التعبيرات النمطية العربية تحديدًا

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

  • الحروف خارج [A-Za-z] فتسقط من \w ومن معها
  • الحرف يشغل أكثر من بايت فتقطعه دوال البايت
  • الحرف الواحد له أشكال إملائية متعددة
  • محارف غير مرئية (تشكيل، تطويل، فواصل صفرية) تدخل النص بلا أثر بصري
  • مجموعتان للأرقام
  • اتجاه الكتابة من اليمين يضيف محارف تحكم في بعض المصادر

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

الأثر على أدوات السيو تحديدًا

هذه المصائد تصيب أدوات تحليل المحتوى قبل أن تصيب كودك، وأثرها هناك أوضح لأنك ترى نتيجة رقمية تنخفض بلا سبب ظاهر.

أداة التحليل تبحث عن كلمتك المفتاحية في العنوان والوصف والفقرة الأولى والعناوين الفرعية. وهي تبحث حرفيًا. فإن كتبتَ في المحرر «تصميم موقع إلكتروني» بالهمزة، وكتبتَ الكلمة المفتاحية «تصميم موقع الكتروني» بلا همزة، فالأداة تخبرك أن الكلمة غائبة من العنوان.

وأنت ترى الكلمة أمامك في العنوان، فتظن الأداة معطلة. وهي ليست معطلة، بل تقارن حرفين مختلفين في اليونيكود: إ وا.

عندنا أصاب هذا أربعين صفحة. وكان الفارق في أغلبها حرفًا واحدًا.

الفحص السريع: انسخ الكلمة المفتاحية من حقلها، والصقها في مربع بحث المتصفح داخل صفحة المقال. إن لم يجدها المتصفح فالمشكلة إملائية لا تحليلية. وقد شرحنا حالة مشابهة أفسدت قراءة Google لخريطة موقعنا.

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

ولماذا لا يهمّ Google؟

‏Google يطبّق تطبيعًا واسعًا على استعلامات البحث والمحتوى معًا، فيعامل «إلكتروني» و«الكتروني» شيئًا واحدًا في الترتيب. الباحث لن يفقدك بسبب همزة.

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

قائمة فحص قبل أي مطابقة نصوص عربية

  1. ابحث عن كل موضع تعتمد فيه على حدود الكلمات (\b و\w). إن وجدتها في نمط يمس نصًا عربيًا فهي معطّلة. استبدلها بفحص الجار.
  2. استعمل الراية u في جافاسكريبت و/u في PHP، وإلا فُسّرت المحارف بايتات لا أحرفًا.
  3. استعمل دوال mb_* في PHP لكل قصّ وقياس.
  4. اختبر بالكلمة معزولة أولًا. إن لم يطابق النمط الكلمة وحدها فالخلل في النمط لا في البيانات. هذا الاختبار وحده كان سيكشف خطأنا في دقيقة.
  5. اختبر الحالة السالبة. نمط لا يطابق شيئًا يجتاز اختبار «لم يعطِ نتائج خاطئة» بامتياز. اطلب منه أن يطابق شيئًا تعرف أنه يجب أن يطابقه.
  6. افحص قائمة النتائج بعينك. عدّ النتائج لا يكفي، فالرقم المعقول يخفي أخطاء منهجية. اقرأ عشرين نتيجة عشوائية.

أسئلة شائعة

هل توجد لغة تدعم \b مع العربية؟

‏Python مع re.UNICODE يوسّع \w ليشمل حروف اليونيكود، فيعمل \b مع العربية. وكذلك محرك regex الخارجي في Python. أما جافاسكريبت وPHP فلا، حتى مع الراية u: هي تصحّح تفسير المحارف لكنها لا توسّع تعريف \w.

ماذا عن \p{L}؟

خاصية اليونيكود \p{L} تطابق أي حرف بأي لغة، وتعمل في PHP مع /u وفي جافاسكريبت مع الراية u. تصلح بديلًا مختصرًا لنطاقات الحروف:

const LETTER = /[\p{L}\p{N}]/u;

لكنها تظل تحل نصف المشكلة: تعطيك تعريفًا صحيحًا لحرف الكلمة، ولا تعطيك \b يعمل. تحتاج فحص الجار في الحالتين.

هل التطبيع يفسد النص المنشور؟

لا، ما دمت تستعمله للمقارنة فقط. القاعدة: طبّع نسخة في الذاكرة للبحث، واكتب دائمًا من النص الأصلي. أي كتابة من نسخة مطبّعة تحوّل «تقنية» إلى «تقنيه» في محتوى منشور.

كيف أعرف أن الإملاء يكسر تحليلي؟

افحص كلمتك المفتاحية والعنوان بعد التطبيع. إن تطابقا مطبّعَين واختلفا أصلَين، فهذا هو السبب. عندنا كان الفرق حرفًا واحدًا في 40 صفحة.

الخلاصة

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

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

والخطأ الصامت أخطر من الذي يرفع استثناءً. الاستثناء يوقفك، والنتيجة المعقولة تمر من المراجعة.

نبني المواقع والأنظمة العربية منذ 2014، ونجمع ما نتعلّمه في مدونتنا وفي أدواتنا المجانية.

لماذا تفشل حدود الكلمات مع النصوص العربية؟ 4 مصائد برمجية | رؤية