یونیکد و عبارت منظم؛ پردازش درست متن فارسی
در این درس ۵ بخش
در دورهٔ قبل، متن فارسی را جستوجو و رقمها را قالببندی کردیم. این بار مسئله ظریفتر است: چرا دو نوشتهٔ ظاهراً یکسان برابر نیستند، چرا یک شکل چند واحد طول دارد و چرا بریدن پیام میتواند نشانهای را خراب کند؟ این دانستهها برای فهرست پیام، پیشنمایش و بررسی ورودی کاربرد دارند. مثالهای این درس مستقل از پروژهٔ چتاند؛ آنها را در فایلهای معرفیشده با Node.js ۲۴ یا مرورگر جدید اجرا کنید. هدف، عوضکردن بیاعلام متن کاربر نیست؛ باید قرارداد شمارش و مقایسه را بشناسید و متن اصلی را از نسخهٔ مخصوص جستوجو جدا نگه دارید.
یونیکد، واحد رشته و خوشهٔ نوشتاری یکی نیستند
یونیکد برای نشانهها و قواعد پردازش متن یک چارچوب مشترک فراهم میکند. یک code point، مقدار مشخصی در این چارچوب است؛ اما رشتهٔ جاوااسکریپت با واحدهای UTF-16 نگهداری میشود. بعضی نشانهها با دو واحد نمایش داده میشوند و بعضی شکلهای قابل مشاهده، ترکیب چند code point هستند. بنابراین length همیشه شمار آنچه کاربر «حرف» میبیند نیست. مدخل #string-index-length را بهعنوان مبنا مرور کنید؛ مسئلهٔ جدید ما انتخاب واحد درست است، نه حفظکردن یک روش دیگر برای خواندن طول رشته. این تفاوت در شمارندهٔ پیام و کوتاهکردن پیشنمایش آشکار میشود.
خوشهٔ نوشتاری یا grapheme cluster تقریب مناسبی از یک واحد قابل مشاهده برای کاربر است. مثلاً «ن» همراه کسره، دو code point دارد، ولی معمولاً یک خوشه دیده میشود. شکل گربه نیز خارج از محدودهٔ سادهٔ یکواحدی UTF-16 است. Intl.Segmenter مرزهای مناسبِ بخشبندی را فراهم میکند. نتیجهٔ آن قانون ثابت اندازهٔ شبکه نیست؛ بایتهای UTF-8 موضوع درس بعدِ دادهٔ باینریاند. اگر سرور سقف بایت یا واحد رشته دارد، شمارش خوشه در رابط نباید آن قرارداد را مخفیانه عوض کند. برای هر محدودیت، واحد را نزدیک پیام راهنما بنویسید.
فایل unicode-length.mjs را بسازید و بلوک زیر را اجرا کنید. رشته عمداً شامل کسره و ایموجی است. پیش از اجرا سه عدد را حدس بزنید؛ سپس علت تفاوت را توضیح دهید. spread روی رشته، code pointها را پیمایش میکند؛ نتیجهٔ segmenter مرز خوشهها را دنبال میکند. هیچکدام صرفاً با نام «تعداد کاراکتر» بدون توضیح واحد، قرارداد روشنی به کاربر نمیدهند. در کنسول میتوانید آرایهٔ بخشها را هم ببینید تا متوجه شوید فقط با یک فرمول عددی روبهرو نیستید و هر روش واقعاً چه واحدی را جدا میکند.
const sample = 'ن\u0650🐱';
const segmenter = new Intl.Segmenter('fa', { granularity: 'grapheme' });
console.log(sample.length); // 4 UTF-16 units
console.log([...sample].length); // 3 code points
console.log([...segmenter.segment(sample)].length); // 2 grapheme clusters
console.log([...segmenter.segment(sample)].map(item => item.segment));
// ['نِ', '🐱']عادیسازی، برابری قراردادی میسازد؛ همهٔ شباهتها را حل نمیکند
دو رشته ممکن است از نظر نمایش نزدیک باشند، ولی ترکیب داخلی متفاوتی داشته باشند. حرف لاتین é میتواند آماده یا از e و نشانهٔ ترکیبی ساخته شود. normalize با فرم NFC، شکلهای دارای همارزی کانونی را به نمایش سازگار نزدیک میکند. این قابلیت را با اصلاح املای فارسی یکی ندانید: ک عربی و ک فارسی، یا ی عربی و ی فارسی، صرفاً با NFC یک نشانه نمیشوند. نمونهٔ درس قبلی نگاشت حروف برای جستوجو را بررسی کرده بود؛ اینجا مرز استاندارد عادیسازی و تصمیم محصول را جدا میکنیم.
const composed = '\u00E9';
const decomposed = 'e\u0301';
console.log(composed === decomposed); // false
console.log(composed.normalize('NFC') === decomposed.normalize('NFC')); // true
console.log('كي'.normalize('NFC') === 'کی'.normalize('NFC')); // false
console.log(decomposed.length, decomposed.normalize('NFC').length); // 2 1این بلوک را مستقل در normalize-lab.mjs اجرا کنید. متن اصلی را نگه دارید و اگر برای جستوجو کلید دیگری میسازید، آن را با نام روشن مانند searchKey نگه دارید. حذف بیقید نیمفاصله، اعراب یا نشانههای جهت ممکن است معنی یا شکل نمایش را عوض کند. NFKC نیز با NFC یک تصمیم یکسان نیست و برخی تمایزهای سازگاری را از بین میبرد. انتخاب آن فقط بهخاطر «نتیجههای برابر بیشتر» کافی نیست. برای نام نمایشی، جستوجو و شناسهٔ امنیتی، سیاست یکسان و بیتوضیح نسازید؛ شباهت ظاهری اثبات هویت کاربر نیست.
عبارت منظم را با متن چندزبانه آزمایش کنید
عبارت منظم، الگوی یافتن یا سنجیدن بخشی از متن است. مدخل #regexp-basics نحو پایه را توضیح میدهد؛ اینجا به ویژگیهای یونیکد و حالت نگهداریشده میپردازیم. کلاسهای ساده مانند \w و \d نمایندهٔ همهٔ حروف و رقمهای دنیا نیستند. با پرچم u میتوان از ویژگیهایی مانند Letter و Number استفاده کرد. الگوی زیر حروف، نشانههای ترکیبی، عدد و نیمفاصله را در یک قطعه جمع میکند؛ سیاست واژهیابی همین تمرین است، نه تعریف کامل زبان فارسی یا ابزار تشخیص معنای پیام. محدودیت آن را قبل از استفاده در جستوجوی واقعی بشناسید.
const text = 'میروم ۱۲۳، سلام 👋';
const tokens = text.match(/[\p{L}\p{M}\p{N}\u200C]+/gu) ?? [];
console.log(tokens); // ['میروم', '۱۲۳', 'سلام']
const stateful = /سلام/g;
console.log(stateful.test('سلام')); // true
console.log(stateful.test('سلام')); // false
const containsHello = /سلام/u;
console.log(containsHello.test('سلام'), containsHello.test('سلام')); // true trueبلوک را در regex-unicode.mjs اجرا کنید. پرچم g برای جستوجوی چند تطبیق است و lastIndex را تغییر میدهد؛ همان شیء الگو در فراخوانی بعدی از وضعیت قبلی استفاده میکند. در بررسی بولیِ مستقلِ یک فیلد، الگوی غیرسراسری انتخاب روشنتری است. اگر نتیجهٔ اعتبارسنجی یک بار درست و بار دیگر غلط میشود، علاوه بر داده، هویت شیء RegExp و lastIndex را ببینید. صفرکردن تصادفی متغیرها بدون شناخت این رفتار، علت را توضیح نمیدهد. اگر الگو از بیرون دریافت شود، خطای ساخت و هزینهٔ الگو نیز باید بررسی شود.
الگوی ثابتِ این درس را با ساختن الگو از متن کاربر اشتباه نگیرید. گذاشتن رشتهٔ تایپشده مستقیم داخل RegExp ممکن است نشانههایی مثل پرانتز را به دستور الگو تبدیل کند. برای جستوجوی سادهٔ متن، includes روی کلید جستوجوی تعریفشده معمولاً روشنتر است. حذف همهٔ نویسههای ناشناخته نیز #validation-contracts را جایگزین نمیکند؛ گاهی باید ورودی را رد کرد یا خطا را نشان داد. در رابط، متن نامعتبر را برای اصلاح حفظ کنید و روی سرور همان محدودیت ضروری را مستقل بسنجید. نامحدودبودن طول ورودی را هم با کوتاهبودن کد الگو توجیه نکنید.
پیشنمایش را بر اساس واحد نمایش کوتاه کنید
برای پیشنمایش ششواحدی، بریدن با slice روی خود رشته ممکن است یک نشانهٔ چندواحدی را بشکند. فایل preview-graphemes.mjs زیر را اجرا کنید. تابع، بخشهای قابل مشاهده را جدا و نخستین تعداد را به هم وصل میکند؛ متن اصلی را تغییر نمیدهد. این نسخه برای متنهای کوچک تمرینی است و قبل از بخشبندی کامل، محدودیت ورودی لازم را باید در مرز دریافت اعمال کرد. راه نمایش کوتاهتر، مجوز حذف بخشی از پیام ذخیرهشده نیست. سه چیز مستقلاند: متن اصلی، نسخهٔ نمایشی کوتاه و سقف مجاز پیام در قرارداد ارتباط.
function previewGraphemes(text, limit) {
if (typeof text !== 'string') throw new TypeError('Expected text');
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new RangeError('Limit must be 1..100');
}
const parts = new Intl.Segmenter('fa', { granularity: 'grapheme' }).segment(text);
return Array.from(parts, part => part.segment).slice(0, limit).join('');
}
console.log(previewGraphemes('سلام 🐱 دوست من', 6)); // سلام 🐱
console.log(previewGraphemes('ن\u0650🐱', 1)); // نِتمرین، پاسخ و پیوند با آموختههای قبلی
تمرین: برای «ن با کسره»، گربه، é ترکیبی و یک عبارت دارای نیمفاصله، جدولی از طول UTF-16، code point و خوشه بسازید. پاسخ راهنما این است که ابتدا دادهٔ واقعی را با escape یا آرایهٔ بخشها روشن کنید، نه اینکه از شکل روی صفحه حدس قطعی بزنید. سپس یک پیشنمایش بسازید که متن ورودی را دستنخورده نگه دارد و حد نامعتبر را رد کند. معیار تحویل، تفاوت مستند سه واحد و آزمون یک ورودی خالی و یک نشانهٔ ترکیبی است؛ اجرای نمونه با حروف ساده بهتنهایی مسئلهٔ این درس را نمیسنجد.
برای نحو رشته و تبدیل نمایشی عدد، به درس پایه مراجعه کنید: https://www.darsavand.com/courses/javascript-from-zero-to-project?lesson=0227d50a-a871-4b9b-af0f-7d4b04b10635
برای سیاست نگاشت حروف در جستوجوی کاتالوگ، درس متوسط را بخوانید: https://www.darsavand.com/courses/javascript-project-lab?lesson=70875d30-98aa-4b81-8fe3-ef7349b1c555
در اینجا #string-transform باید با هدف مشخص استفاده شود؛ نسخهٔ جستوجو را از متن اصلی جدا کنید. در پروژهٔ چت، واحد محدودیت را از قرارداد همان پروژه بخوانید و در رابط با توضیح یکسان نشان دهید. از مثال مستقل خوشهبندی نتیجه نگیرید که سرور چت نیز الزاماً همین معیار را برای رد پیام انتخاب کرده است.
منابع رسمی انگلیسی:
MDN — Intl.Segmenter: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
MDN — String.normalize: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
MDN — Unicode property escapes: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape
MDN — RegExp.lastIndex: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex
تألیف و مثالهای مستقل: تحریریه درسآوند. منابع رسمی برای بررسی رفتار زبان و مطالعهٔ بیشتر معرفی شدهاند.
تألیف و مثالهای اختصاصی: تحریریه درسآوند. فایلهای کامل پروژه و راهنمای اجرا و دیباگ:
https://www.darsavand.com/resources/simple-websocket-chat-project
متن درسهای عمومی را بدون ورود بخوانید.