كيف أقسم النص إلى كلمات في JavaScript؟
استخدم Intl.Segmenter لاستخراج الكلمات من النص بأي لغة، بما في ذلك اللغات التي لا تحتوي على مسافات بين الكلمات.
مقدمة
عندما تحتاج إلى استخراج الكلمات من النص، فإن الطريقة الشائعة هي التقسيم على المسافات باستخدام split(" "). هذا يعمل مع اللغة الإنجليزية، لكنه يفشل تمامًا مع اللغات التي لا تستخدم مسافات بين الكلمات. اللغات الصينية واليابانية والتايلاندية وغيرها من اللغات تكتب النص بشكل متواصل دون فواصل بين الكلمات، ومع ذلك يدرك المستخدمون كلمات متميزة في ذلك النص.
تحل واجهة برمجة التطبيقات Intl.Segmenter هذه المشكلة. فهي تحدد حدود الكلمات وفقًا لمعايير Unicode والقواعد اللغوية لكل لغة. يمكنك استخراج الكلمات من النص بغض النظر عما إذا كانت اللغة تستخدم مسافات، ويتعامل المقسم مع تعقيد تحديد أين تبدأ الكلمات وأين تنتهي.
يشرح هذا المقال لماذا يفشل التقسيم الأساسي للنصوص مع النصوص الدولية، وكيف تعمل حدود الكلمات عبر أنظمة الكتابة المختلفة، وكيفية استخدام Intl.Segmenter لتقسيم النص إلى كلمات بشكل صحيح لجميع اللغات.
لماذا يفشل التقسيم على المسافات
تقوم طريقة split() بتقسيم النص عند كل ظهور للفاصل. بالنسبة للنص الإنجليزي، يستخرج التقسيم على المسافات الكلمات.
const text = "Hello world";
const words = text.split(" ");
console.log(words);
// ["Hello", "world"]
يفترض هذا النهج أن الكلمات مفصولة بمسافات. العديد من اللغات لا تتبع هذا النمط.
النص الصيني لا يتضمن مسافات بين الكلمات.
const text = "你好世界";
const words = text.split(" ");
console.log(words);
// ["你好世界"]
يرى المستخدم كلمتين متميزتين، لكن split() يُرجع النص بالكامل كعنصر واحد لأنه لا توجد مسافات للتقسيم عليها.
النص الياباني يمزج بين عدة أنظمة كتابة ولا يستخدم مسافات بين الكلمات.
const text = "今日は良い天気です";
const words = text.split(" ");
console.log(words);
// ["今日は良い天気です"]
تحتوي هذه الجملة على كلمات متعددة، لكن التقسيم على المسافات ينتج عنصراً واحداً.
النص التايلاندي أيضاً يكتب الكلمات بشكل متواصل دون مسافات.
const text = "สวัสดีครับ";
const words = text.split(" ");
console.log(words);
// ["สวัสดีครับ"]
يحتوي النص على كلمتين، لكن split() يُرجع عنصراً واحداً.
بالنسبة لهذه اللغات، تحتاج إلى نهج مختلف لتحديد حدود الكلمات.
لماذا تفشل التعبيرات النمطية في تحديد حدود الكلمات
تستخدم حدود الكلمات في التعبيرات النمطية نمط \b لمطابقة المواضع بين أحرف الكلمات والأحرف غير الكلمات. يعمل هذا مع اللغة الإنجليزية.
const text = "Hello world!";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["Hello", "world"]
يفشل هذا النمط مع اللغات التي لا تحتوي على مسافات لأن محرك التعبيرات النمطية لا يتعرف على حدود الكلمات في الكتابات مثل الصينية أو اليابانية أو التايلاندية.
const text = "你好世界";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["你好世界"]
يتعامل التعبير النمطي مع النص بالكامل ككلمة واحدة لأنه لا يفهم حدود الكلمات الصينية.
حتى بالنسبة للغة الإنجليزية، يمكن أن تنتج أنماط التعبيرات النمطية نتائج غير صحيحة مع علامات الترقيم أو الاختصارات أو الأحرف الخاصة. لم يتم تصميم التعبيرات النمطية للتعامل مع تقسيم الكلمات اللغوي عبر جميع أنظمة الكتابة.
ما هي حدود الكلمات عبر اللغات
حد الكلمة هو موضع في النص حيث تنتهي كلمة وتبدأ أخرى. تستخدم أنظمة الكتابة المختلفة اصطلاحات مختلفة لحدود الكلمات.
اللغات المفصولة بمسافات مثل الإنجليزية والإسبانية والفرنسية والألمانية تستخدم المسافات لتحديد حدود الكلمات. يتم فصل كلمة "hello" عن "world" بمسافة.
لغات الكتابة المتصلة مثل الصينية واليابانية والتايلاندية لا تستخدم مسافات بين الكلمات. توجد حدود الكلمات بناءً على قواعد دلالية وصرفية، لكن هذه الحدود غير محددة بصرياً في النص. يتعرف القارئ الصيني على مكان انتهاء كلمة وبداية أخرى من خلال الإلمام باللغة، وليس من خلال فواصل بصرية.
تستخدم بعض اللغات اصطلاحات مختلطة. تجمع اليابانية بين أحرف الكانجي والهيراغانا والكاتاكانا، وتحدث حدود الكلمات عند الانتقالات بين أنواع الأحرف أو بناءً على البنية النحوية.
يحدد معيار Unicode قواعد حدود الكلمات في UAX 29. تحدد هذه القواعد كيفية تحديد حدود الكلمات لجميع الأنظمة الكتابية. تأخذ القواعد في الاعتبار خصائص الأحرف وأنواع الأنظمة الكتابية والأنماط اللغوية لتحديد أين تبدأ الكلمات وتنتهي.
استخدام Intl.Segmenter لتقسيم النص إلى كلمات
ينشئ المُنشئ Intl.Segmenter كائن مُقسِّم يقسم النص وفقًا لقواعد Unicode. يمكنك تحديد لغة ومستوى دقة.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = segmenter.segment(text);
الوسيطة الأولى هي معرّف اللغة. الوسيطة الثانية هي كائن خيارات حيث يخبر granularity: "word" المُقسِّم بالتقسيم عند حدود الكلمات.
تُرجع الطريقة segment() كائنًا قابلاً للتكرار يحتوي على مقاطع. يمكنك التكرار عبر المقاطع باستخدام for...of.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
for (const segment of segmenter.segment(text)) {
console.log(segment);
}
// { segment: "Hello", index: 0, input: "Hello world!", isWordLike: true }
// { segment: " ", index: 5, input: "Hello world!", isWordLike: false }
// { segment: "world", index: 6, input: "Hello world!", isWordLike: true }
// { segment: "!", index: 11, input: "Hello world!", isWordLike: false }
يحتوي كل كائن مقطع على خصائص:
segment: نص هذا المقطعindex: الموضع في السلسلة الأصلية حيث يبدأ هذا المقطعinput: السلسلة الأصلية التي يتم تقسيمهاisWordLike: ما إذا كان هذا المقطع كلمة أو محتوى غير كلمة
فهم خاصية isWordLike
عند تقسيم النص حسب الكلمات، يُرجع المُقسِّم كلاً من مقاطع الكلمات ومقاطع غير الكلمات. تتضمن الكلمات الحروف والأرقام والأحرف الأيديوغرافية. تتضمن المقاطع غير الكلمات المسافات وعلامات الترقيم والفواصل الأخرى.
تشير الخاصية isWordLike إلى ما إذا كان المقطع كلمة. هذه الخاصية true للمقاطع التي تحتوي على أحرف كلمات، وfalse للمقاطع التي تحتوي فقط على مسافات أو علامات ترقيم أو أحرف أخرى غير كلمات.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "Hello" true
// "," false
// " " false
// "world" true
// "!" false
استخدم خاصية isWordLike لتصفية أجزاء الكلمات من علامات الترقيم والمسافات البيضاء. يمنحك هذا الكلمات فقط بدون فواصل.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(words);
// ["Hello", "world"]
يعمل هذا النمط مع أي لغة، بما في ذلك اللغات التي لا تحتوي على مسافات.
استخراج الكلمات من نص بدون مسافات
يحدد المقسّم بشكل صحيح حدود الكلمات في اللغات التي لا تستخدم المسافات. بالنسبة للنص الصيني، يقوم المقسّم بالتقسيم عند حدود الكلمات بناءً على قواعد Unicode والأنماط اللغوية.
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "你好" true
// "世界" true
يحدد المقسّم كلمتين في هذا النص. لا توجد مسافات، لكن المقسّم يفهم حدود الكلمات الصينية ويقسم النص بشكل مناسب.
بالنسبة للنص الياباني، يتعامل المقسّم مع تعقيد النصوص المختلطة ويحدد حدود الكلمات.
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
const text = "今日は良い天気です";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "今日" true
// "は" true
// "良い" true
// "天気" true
// "です" true
يقسم المقسّم هذه الجملة إلى خمسة أجزاء من الكلمات. يتعرف على أن الجسيمات مثل "は" هي كلمات منفصلة وأن الكلمات المركبة مثل "天気" تشكل وحدات مفردة.
بالنسبة للنص التايلاندي، يحدد المقسّم حدود الكلمات بدون مسافات.
const segmenter = new Intl.Segmenter("th", { granularity: "word" });
const text = "สวัสดีครับ";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "สวัสดี" true
// "ครับ" true
يحدد المقسّم بشكل صحيح كلمتين في هذه التحية.
بناء دالة لاستخراج الكلمات
أنشئ دالة تستخرج الكلمات من النص بأي لغة.
function getWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
}
تعمل هذه الدالة مع اللغات المفصولة بمسافات وغير المفصولة بمسافات.
getWords("Hello, world!", "en");
// ["Hello", "world"]
getWords("你好世界", "zh");
// ["你好", "世界"]
getWords("今日は良い天気です", "ja");
// ["今日", "は", "良い", "天気", "です"]
getWords("Bonjour le monde!", "fr");
// ["Bonjour", "le", "monde"]
getWords("สวัสดีครับ", "th");
// ["สวัสดี", "ครับ"]
تُرجع الدالة مصفوفة من الكلمات بغض النظر عن اللغة أو نظام الكتابة.
عد الكلمات بدقة عبر اللغات
أنشئ عداد كلمات يعمل مع جميع اللغات من خلال عد الأجزاء الشبيهة بالكلمات.
function countWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments).filter(s => s.isWordLike).length;
}
تنتج هذه الدالة عدد كلمات دقيق للنص بأي لغة.
countWords("Hello world", "en");
// 2
countWords("你好世界", "zh");
// 2
countWords("今日は良い天気です", "ja");
// 5
countWords("Bonjour le monde", "fr");
// 3
countWords("สวัสดีครับ", "th");
// 2
تتطابق الأعداد مع إدراك المستخدم لحدود الكلمات في كل لغة.
إيجاد الكلمة التي تحتوي على موضع معين
تجد الدالة containing() المقطع الذي يتضمن فهرساً محدداً في النص. هذا مفيد لتحديد الكلمة التي يقع فيها المؤشر أو الكلمة التي تم النقر عليها.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);
const segment = segments.containing(7);
console.log(segment);
// { segment: "world", index: 6, input: "Hello world", isWordLike: true }
يقع الفهرس 7 ضمن الكلمة "world"، والتي تبدأ عند الفهرس 6. تُرجع الدالة كائن المقطع لتلك الكلمة.
إذا وقع الفهرس ضمن مسافة بيضاء أو علامة ترقيم، تُرجع الدالة ذلك المقطع مع isWordLike: false.
const segment = segments.containing(5);
console.log(segment);
// { segment: " ", index: 5, input: "Hello world", isWordLike: false }
استخدم هذا لميزات محرر النصوص مثل تحديد الكلمة بالنقر المزدوج، أو القوائم السياقية بناءً على موضع المؤشر، أو تمييز الكلمة الحالية.
التعامل مع علامات الترقيم والاختصارات
يتعامل المقسّم مع علامات الترقيم كمقاطع منفصلة. عادةً ما يتم تقسيم الاختصارات في اللغة الإنجليزية إلى مقاطع متعددة.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "I can't do it.";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "I" true
// " " false
// "can" true
// "'" false
// "t" true
// " " false
// "do" true
// " " false
// "it" true
// "." false
يتم تقسيم الاختصار "can't" إلى "can" و "'" و "t". إذا كنت بحاجة إلى الاحتفاظ بالاختصارات ككلمات مفردة، فأنت بحاجة إلى منطق إضافي لدمج المقاطع بناءً على الفواصل العليا.
بالنسبة لمعظم حالات الاستخدام، فإن عد المقاطع الشبيهة بالكلمات يمنحك أعداد كلمات ذات معنى حتى عندما يتم تقسيم الاختصارات.
كيف تؤثر اللغة المحلية على تقسيم الكلمات
تؤثر اللغة المحلية التي تمررها إلى المقسّم على كيفية تحديد حدود الكلمات. قد يكون للغات محلية مختلفة قواعد مختلفة لنفس النص.
بالنسبة للغات ذات قواعد حدود كلمات محددة جيداً، تضمن اللغة المحلية تطبيق القواعد الصحيحة.
const segmenterEn = new Intl.Segmenter("en", { granularity: "word" });
const segmenterZh = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const wordsEn = Array.from(segmenterEn.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
const wordsZh = Array.from(segmenterZh.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(wordsEn);
// ["你好世界"]
console.log(wordsZh);
// ["你好", "世界"]
لا تتعرف اللغة المحلية الإنجليزية على حدود الكلمات الصينية وتتعامل مع النص بأكمله ككلمة واحدة. تطبق اللغة المحلية الصينية قواعد حدود الكلمات الصينية وتحدد بشكل صحيح كلمتين.
استخدم دائمًا اللغة المحلية المناسبة للغة النص الذي يتم تقسيمه.
إنشاء مقسمات قابلة لإعادة الاستخدام لتحسين الأداء
إنشاء مقسم ليس مكلفًا، ولكن يمكنك إعادة استخدام المقسمات عبر سلاسل نصية متعددة لتحسين الأداء.
const enSegmenter = new Intl.Segmenter("en", { granularity: "word" });
const zhSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
const jaSegmenter = new Intl.Segmenter("ja", { granularity: "word" });
function getWords(text, locale) {
const segmenter = locale === "zh" ? zhSegmenter
: locale === "ja" ? jaSegmenter
: enSegmenter;
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
ينشئ هذا النهج المقسمات مرة واحدة ويعيد استخدامها لجميع استدعاءات getWords(). يخزن المقسم بيانات اللغة المحلية مؤقتًا، لذا فإن إعادة استخدام النسخ يتجنب التهيئة المتكررة.
مثال عملي: بناء محلل تكرار الكلمات
ادمج تقسيم الكلمات مع العد لتحليل تكرار الكلمات في النص.
function getWordFrequency(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment.toLowerCase());
const frequency = {};
for (const word of words) {
frequency[word] = (frequency[word] || 0) + 1;
}
return frequency;
}
const text = "Hello world! Hello everyone in this world.";
const frequency = getWordFrequency(text, "en");
console.log(frequency);
// { hello: 2, world: 2, everyone: 1, in: 1, this: 1 }
تقسم هذه الدالة النص إلى كلمات، وتوحد الأحرف إلى أحرف صغيرة، وتحسب التكرارات. تعمل مع أي لغة.
const textZh = "你好世界!你好大家!";
const frequencyZh = getWordFrequency(textZh, "zh");
console.log(frequencyZh);
// { "你好": 2, "世界": 1, "大家": 1 }
يتعامل نفس المنطق مع النص الصيني دون تعديل.
التحقق من دعم المتصفح
وصلت واجهة برمجة التطبيقات Intl.Segmenter إلى حالة Baseline في أبريل 2024. تعمل في الإصدارات الحالية من Chrome وFirefox وSafari وEdge. المتصفحات القديمة لا تدعمها.
تحقق من الدعم قبل استخدام واجهة برمجة التطبيقات.
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
// Use segmenter
} else {
// Fallback for older browsers
}
بالنسبة لتطبيقات الإنتاج التي تستهدف المتصفحات القديمة، قدم تطبيقًا احتياطيًا. يستخدم الحل الاحتياطي البسيط split() للنص الإنجليزي ويعيد السلسلة النصية بأكملها للغات الأخرى.
function getWords(text, locale) {
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
// Fallback: only works for space-separated languages
return text.split(/\s+/).filter(word => word.length > 0);
}
يضمن هذا تشغيل الكود الخاص بك في المتصفحات القديمة، وإن كان بوظائف محدودة للغات التي لا تفصل بمسافات.
الأخطاء الشائعة التي يجب تجنبها
لا تقسم على المسافات أو أنماط regex للنص متعدد اللغات. تعمل هذه الأساليب فقط مع مجموعة فرعية من اللغات وتفشل مع الصينية واليابانية والتايلاندية واللغات الأخرى التي لا تحتوي على مسافات.
لا تنس التصفية حسب isWordLike عند استخراج الكلمات. بدون هذا الفلتر، ستحصل على مسافات وعلامات ترقيم ومقاطع أخرى غير كلمات في نتائجك.
لا تستخدم اللغة المحلية الخاطئة عند تقسيم النص. تحدد اللغة المحلية قواعد حدود الكلمات المطبقة. استخدام لغة محلية إنجليزية لنص صيني ينتج نتائج غير صحيحة.
لا تفترض أن جميع اللغات تعرّف الكلمات بنفس الطريقة. تختلف حدود الكلمات حسب نظام الكتابة والاصطلاحات اللغوية. استخدم التقسيم الواعي باللغة المحلية للتعامل مع هذه الاختلافات.
لا تحسب الكلمات باستخدام split(" ").length للنصوص الدولية. هذا يعمل فقط مع اللغات المفصولة بمسافات وينتج أعدادًا خاطئة للغات الأخرى.
متى تستخدم تقسيم الكلمات
استخدم تقسيم الكلمات عندما تحتاج إلى:
- حساب الكلمات في المحتوى الذي ينشئه المستخدم عبر لغات متعددة
- تنفيذ ميزات البحث والتمييز التي تعمل مع أي نظام كتابة
- بناء أدوات تحليل النصوص التي تعالج النصوص الدولية
- إنشاء ميزات التنقل أو التحرير المستندة إلى الكلمات في محررات النصوص
- استخراج الكلمات المفتاحية أو المصطلحات من المستندات متعددة اللغات
- التحقق من حدود عدد الكلمات في النماذج التي تقبل أي لغة
لا تستخدم تقسيم الكلمات عندما تحتاج فقط إلى عدد الأحرف. استخدم تقسيم الجرافيم للعمليات على مستوى الأحرف.
لا تستخدم تقسيم الكلمات لتقسيم الجمل. استخدم دقة الجملة لهذا الغرض.
كيف يتناسب تقسيم الكلمات مع التدويل
واجهة برمجة التطبيقات Intl.Segmenter هي جزء من واجهة برمجة تطبيقات التدويل في ECMAScript. تتعامل واجهات برمجة التطبيقات الأخرى في هذه المجموعة مع جوانب مختلفة من التدويل:
Intl.DateTimeFormat: تنسيق التواريخ والأوقات وفقًا للغة المحليةIntl.NumberFormat: تنسيق الأرقام والعملات والوحدات وفقًا للغة المحليةIntl.Collator: فرز ومقارنة السلاسل النصية وفقًا للغة المحليةIntl.PluralRules: تحديد صيغ الجمع للأرقام في اللغات المختلفة
معًا، توفر واجهات برمجة التطبيقات هذه الأدوات اللازمة لبناء تطبيقات تعمل بشكل صحيح للمستخدمين في جميع أنحاء العالم. استخدم Intl.Segmenter مع دقة الكلمات عندما تحتاج إلى تحديد حدود الكلمات، واستخدم واجهات برمجة التطبيقات الأخرى Intl للتنسيق والمقارنة.