با Regular Expression رفیق شویم!

یکشنبه، 23 تیر 1398

Regular Expression
Regex
C#
Javascript
Aaron Burden - Unsplash
Aaron Burden - Unsplash

مقدمه

تاریخ آشنایی من با Regular Expression (که زین پس RegEx صداش می کنم) بر می گرده به سال ها پیش. کجا و چطورش درست خاطرم نیست. بهش تسلطی هم نداشتم! فقط می دونستم چیزی به اسم RegEx وجود داره که با تعریف الگوها (Patterns) می تونه رشته ها رو جست و جو و بررسی کنه. توی همه ی این سال ها هر وقت بهش نیاز پیدا می کردم می رفتم سراغش. کمی توی سایتا چرخ می زدم، StackOverflow رو بالا پایین می کردم و بالاخره با هزار مصیبت یک چیزی می نوشتم تا کارم راه بیفته. 

چند بارم تصمیمِ کبری گرفتم که اصولی یاد بگیرمش، که از تمام اون تصمیما فقط یک بارش به عمل ناقص رسید! چند فصل از کتابی رو خوندم اما بعدش نمی دونم چی شد که ولش کردم. به هر حال. چیزی که جالبه و البته بدیهی؛ اینه که همون چند فصل هم بعد از مدتی فراموش کردم! یعنی هر وقت گیر می کردم دوباره روز از نو...! باید دوره می افتادم بین سایتا تا یک خط RegEx بنویسم.

اصولاً روشِ "اول گوگل کن بعد کد بزن" چیز جالبی نیست. حداقل برای پروژه های جدی و بزرگ. یک برنامه نویسِ حرفه ای تکنولوژی و ابزارش رو خوب می شناسه و جوری ازش استفاده می کنه که آدم حظ کنه. انقدر به خودم تلقینِ "حرفه ای بودن" کردم تا بالاخره مجبور شدم "هم بکشم" و یک بار بشینم این لعنتی رو درست یاد بگیرم. این اتفاق پنج-شیش ماهِ پیش افتاد که سعی کردم حاصلش رو توی این پست بیارم.

یکی از مهم ترین تکنیکا واسه یاد گیری تکراره. برنامه نویسِ بیچاره باید همش یاد بگیره و برای اینکه یادش نره باید همش تمرین کنه. RegEx هم مثل باقی چیزاست. اگه می خواهید درست و حسابی یاد بگیریدش باید درست و حسابی هم تمرین کنید. حتما به سایتِ HackerRank سری بزنید. کلی تمرینِ RegEx و چیزای دیگه داره. حتی هر روز براتون یک تمرین ایمیل می کنه! 😎 عالیه!

RegEx چیست و از کجا پیداش شد؟

RegEx (که به فارسی "عبارت باقاعده" ترجمه شده 👎) زبون برنامه نویسی نیست، تکنولوژی هم نیست، نرم افزار هم نیست، بیش تر شبیهِ یه استاندارده. RegEx مجموعه ای از نشانه هاست که باهاش می شه یک رشته رو پردازش کرد. مثلا تمام کلماتی که با حروف بزرگ شروع می شن رو استخراج کرد، تمام تاریخ های یک متن رو پیدا کرد، یک رشته رو بررسی کرد که آیا ایمیل هست یا نه و از این جور کارا.

RegEx اصلا چیز جدیدی نیست. از دهه ی 1950 تا الان سن داره و یه ریاضی دان به اسمِ Stephen Kleene ابداعش کرده. توی خیلی از ابزارا و زبون های برنامه نویسی این استاندارد پیاده سازی شده و مثل تمام استانداردهای دیگه بدیهیه که هر پیاده سازی به طور کامل تعاریف رو پشتیبانی نمی کنه. به هر کدوم از این پیاده سازی ها اصطلاحا یک Flavor می گن. 

چرا برنامه نویسا بهتره RegEx بلد باشن؟

به همون دلیلی که بهتره C، HTML، Web، OOP و خیلی چیزای مهمِ دیگه رو بلد باشن! همه ی ما هر روز با رشته ها سر و کار داریم. داشتن یه ابزارِ قوی می تونه باعث بشه زمان و انرژی خیلی کمتری صرف کنیم.

تمام مثال های این پست از سایت regexr.com تهیه شده. بد نیست قبل از شروع این سایت رو معرفی کنم تا اگه تازه کارید زیاد گیج نشید. سایتِ regexr از چند قسمت مهم تشکیل شده:

محیط سایت regexr
محیط سایت regexr

String Literal

ساده ترین حالت برای نوشتنِ RegEx وارد کردنِ عینه متنیه که می خواهیم جست و جو کنیم. یعنی بدون هیچ نشانه ای. به این رشته های ثابت در الگو می گن: String Literal.

Character Class

اول به این تصویر دقت کنید:

در این مثال علاوه بر String Literal از نشانه های [ و ] هم استفاده شده. به این ساختار می گن Character Class یا Character Set. همون طور که از نامش پیداست ازش برای تعریف یک مجموعه از کاراکترها استفاده می شه. مثال بالا به فارسی این طور بیان می شه: انتخاب تمام رشته هایی که با G یا S یا N شروع بشه و با ame ادامه پیدا کنه. اما قدرتِ Character Class به اینجا محدود نمی شه:

در این مثال با استفاده از نشانه ی یک بازه ایجاد کردیم. به عبارت دیگه: انتخاب تمام رشته هایی که با یکی از حروفِ A تا G شروع می شن و با ame ادامه پیدا می کنن. با نشونه ی ^ (بخوانید Caret) هم می شه محتویات داخلِ Character Class رو نفی کرد.

مثال زیر نفی مثال بالاست. یعنی: انتخاب تمام رشته هایی که با حروف A تا G شروع نمی شن و با ame ادامه پیدا می کنن. در تصویر به زمانی که هنوز ame نوشته نشده دقت کنید. در اون مرحله تمام حروف به غیر از A تا G انتخاب می شن.

تمام کاراکترهای الگو به حروف بزرگ و کوچک حساس اند = Case Sensitive

در Character Classها می شه بیش تر از یک بازه نوشت. علاوه بر حروف از اعداد هم می شه استفاده کرد:

اگر قصد استفاده از کاراکترهای ویژه رو دارید حتما یادتون باشه که باید اونا رو Escape کرد. یعنی کنارشون \ بنویسید.

Character Shorthand

خیلی وقتا نوشتن Character Class کاره سختیه و باعث شلوغ شدنِ الگو می شه. واسه همین RegEx برای ترکیب های پر استفاده میانبرهای خاصی تعریف کرده. به این میانبرها می گن Shorthand. تمام Shorthandها در RegEx به شکلِ x\ تعریف می شن. Shorthandهای مهم عبارتند از:

w\ : شامل حروف (بزرگ و کوچک)، اعداد و کاراکترِ _. معادل: [_A-Za-z0-9]

W\ : شامل تمام کاراکترها به جز حروف، اعداد و _. معادل: [A-Za-z0-9_^]

d\ : شامل اعداد. معادل: [9-0]

D\ : شامل تمام کاراکترها به جز اعداد. معادل: [9-0^]

s\ : شامل تمام فواصل (Space، Tab و New Line). معادل: [ t\s\n\]

S\ : شامل تمام کاراکترها به جز فواصل. معادل: [ t\s\n\^]

به حروف بزرگ و کوچک در نامِ Shorthandها دقت کنید. حروف بزرگ نفی حروف کوچک هستند.
منظور از New Line کاراکتریه که باعث ایجادِ خط بعدی می شه. مثل زمانی که کلید Enter رو می زنیم. این کاراکتر با n\ نشون داده می شه.

s\ کار کردن با فواصل رو خیلی راحت می کنه. برای درک بهتر از قسمت پایین سایت تبِ Replace رو انتخاب کنید و تمام فاصله ها رو با کاراکترِ عوض کنید و به نتیجه دقت کنید.

یک shorthand ِ مهم دیگه می مونه که در قسمتِ Boundary بهش می رسیم.

Quantifiers

با Character Class و Shorthandها موفق شدیم بیان کنیم که قصد انتخاب چه چیز رو داریم. حالا نوبتِ تعداده. یعنی بتونیم بگیم از فلان کاراکتر به چه تعدادی انتخاب بشه. برای سنجشِ تعداد از Quantifierها استفاده می شه. که به فارسی می شه گفت "شمارنده" یا "کمیت سنج"👍. به طور کلی پنج نوع شمارنده وجود داره:

؟

این شمارنده حضورِ رشته رو اختیاری می کنه. یعنی امکانِ حضورِ صفر یا یک مرتبه.

در مثال بالا کاراکترِ با ؟ علامت گذاری شده، بنابراین حضورش توی متن اختیاریه. به همین دلیل، هم عبارتِ javascript معتبره و هم عبارتِ java-script.

+

رشته ای که این شمارنده بهش اشاره می کنه می تونه یک یا بیشتر از یک مرتبه حضور داشته باشه. 

در این مثال تمام عبارتایی که با abc شروع می شن و دنبالشون یک یا بیشتر از یک عدد دارن، انتخاب می شن. دقت کنید برای بیان اعداد از d\ استفاده کردم.

*

به این شمارنده Star یا Kleene Star (به افتخار Stephen Kleene) هم گفته می شه. رشته ای که این شمارنده بهش اشاره می کنه می تونه صفر یا بیش تر از صفر مرتبه حضور داشته باشه. در مثال قبلی اگر به جای + از * استفاده می شد، تمام عبارتا انتخاب می شدن:

. هر کاراکتری رو به غیر از New Line شامل می شه. بنابراین معنی این الگو به فارسی می شه: انتخابِ تمام رشته هایی که با " شروع می شن، بعد می رسن به هر تعداد از هر کاراکتری (به غیر از New Line)، بعد هم با " تموم می شن. مثال بالا رو یک بار با علامت + به جای * اجرا کنید و نتیجه را ببینید.

{}

به وسیله ی این شمارنده می تونیم تعدادِ دفعات رو به صورت دقیق مشخص کنیم. 

مثال بالا رشته هایی رو انتخاب می کنه که بین xها دقیقا چهار مرتبه عدد داشته باشه. 

با استفاده از , (Comma) می شه برای تعداد کاراکترها بازه تعین کرد:

اگر عددِ بعد از , نوشته نشه انتخاب تا حد ممکن جلو می ره. معنیِ مثال بالا بدون نوشتنِ عددِ 5 یعنی انتخاب تمام رشته هایی که بین xها سه یا بیش تر از سه مرتبه عدد داره. 

Greedy & Lazy

نحوه ی عملکرد تمام شمارنده های RegEx حریصانه (Greedy) است. یعنی به طور پیش فرض انتخاب تا جای ممکن ادامه پیدا می کنه. 

در این مثال حدِ پایینِ انتخاب، سه کاراکتر و حدِ بالای انتخاب بی نهایته. و همون طور که گفتم به دلیل حریص بودنِ شمارنده ها تمام اعداد انتخاب شدن. در حالی که این رفتار قابل تغییره. به راحتی با استفاده از نشانه ی ؟ می شه عملکردِ شمارنده رو Lazy کرد:

Boundary 

معنی Boundary به فارسی می شه مرز یا کران 👍. فرض کنید می خواهیم تمام اعدادِ مستقل رو انتخاب کنیم: 

در این مثال همه ی اعداد انتخاب شدن. درحالی که همشون مستقل نیستن. بدیهیه که ما قصدِ انتخابِ 1st و x1 رو نداریم. با کمک b\ که بهش Word Boundary هم گفته می شه می تونیم این مشکل رو به راحتی حل کنیم. با استفاده از این Shorthand می شه بیان کرد که اعداد حتما باید کلماتی مستقل باشند. این همان Shorthand ِ دیگری است که قرار بود بررسی کنیم. 

توجه کنید هر دو طرفِ الگویی که می خواهیم مرز کلمه رو داشته باشه باید با نشانه ی b\ علامت گذاری بشه. b\ هم مثل سایر Shorthandها یه نفی داره: B\.

در مثال بالا تمام eهای آخر کلمات انتخاب شدن! به همین راحتی! سمت چپ eها باید بدون مرز و سمت راست دارای مرز باشه.

Anchors

با استفاده از Anchorها می شه ابتدا و انتهای رشته ها یا خطوط رو مشخص کرد. از علامت ^ برای مشخص کردنِ شروع و از $ برای مشخص کردنِ پایان رشته یا خط استفاده می شه. در مثال زیر فقط رشته ای انتخاب شده که همه ی خط را اشغال کرده و چیزی قبل و بعدش نیست.

دقت کنید برای اجرای این الگو حتما گزینه ی Multiline از منوی Flag در بالا سمتِ راست روشن شده باشد.
Flagها قسمتی از قواعد RegEx هستند. 

Groups

یکی دیگه از قابلیت های RegEx گروه ها هستن و هدف از ایجادشون می تونه هر کدام از دلایل زیر باشه:

گروه ها رو می شه به دو دسته تقسیم کرد: Capturing Groups و Non-Capturing Groups. گروه های Capturing امکان انتخاب قسمتی از رشته رو فراهم می کنند و علاوه بر اون قسمتِ انتخاب شده رو به حافظه می سپرن. برای درک بهتر مثال زیر را اجرا کنید. از قسمت پایین سایت گزینه ی List را انتخاب کنید. در اینجا می شه با متغیرهای 1$، 2$ و ... مقدار گروه ها را دید:

متغیرِ 1$ به مقدار اولین گروه اشاره می کنه، 2$ به مقدار دومین گروه و الی آخر. همون طور که از این مثال هم پیداست گروه های Capturing مقدار الگوهای پیدا شده در رشته را در حافظه ذخیره می کنن تا به وقت نیاز بشه اونا رو بازیابی کرد. بنابراین بدیهیه که اگه قصد بازیابی رو نداشته باشیم استفاده از گروه های Capturing کار درستی نیست، چون باعث افتِ کارایی می شن. به خصوص اگه مقدار رشته زیاد و الگوها پیچیده باشه. 

گروه های Non Capturing درست مثل دسته ی Capturing با پرانتز تعریف می شن، با این تفاوت که علامت :? هم درون پرانتز قرار می گیره. بیشترین کاربردِ این گروه ها دسته بندی الگوها (برای مرتب شدن) و اعمال شمارنده هاست. برای روشن تر شدن کاربردِ شمارنده ها در گروه ها به مثال زیر دقت کنید. در این مثال نامِ Domainها و Sub Domainها رو انتخاب می کنیم. 

استفاده از گروه های Non Capturing برای اعمال شمارنده ها الزامی نیست. می شه از گروه های Capturing هم استفاده کرد اما اگر قصد استفاده از مقادیر ذخیره شده توسط گروه رو ندارید بهتره از Non Capturingها استفاده کنید.

معنی گروه بندی مثال بالا اینه: کل الگوی .\+w\ می تونه یک یا بیش تر از یک مرتبه تکرار بشه. به عبارتی: باید ترکیبِ چند کاراکترِ حرفی و در نهایت یک نقطه، یک یا بیش از یک بار در کل رشته موجود باشه.

Backreference

در این مثال هدف اینه که جداکننده ی - یا بین تمام کاراکترها حضور داشته باشه و یا اصلا نباشه. همان طور که در مثال هم نشان داده شده این امکان هست که با تکرارِ (?-) حضورِ بینِ a و b و c را اختیاری کرد. اما در این صورت ورودی های a-bc و ab-c هم انتخاب می شن. بنابراین نتیجه غلط خواهد بود. اما هدف این الگو اینه که اگر بین a و b کاراکترِ قرار گرفت، بین b و c هم قرار حتما بگیره. در غیر این صورت بین b و c هم هیچ کاراکتری نباشه. با Backreference این مشکل قابل حله. با این قابلیت می شه به حاصل یک گروه برای تکرار شدن اشاره کرد. گروه موردِ اشاره هر مقداری که داشته باشه باید در محلِ اشاره شده هم شامل همان مقدار باشه. اگر قصد اشاره به گروه اول را داشتیم از 1\ استفاده می کنیم. اگر قصد اشاره به گروه دوم، از 2\ و الی آخر.

امکان نام دهی گروه ها هم وجود داره. در اون صورت می شه با استفاده از نام بهشون اشاره کرد.

یک مثال بهتر برای Backreference فرمت شماره موبایل هاست. فرض کنید کاربر برای وارد کردن شماره ی موبایل یا باید از کاراکترِ استفاده کنه یا فاصله. ترکیب هر دو ممکن نیست:

Alteration

در مثال قبلی داخل پرانتز از نشانه ی | (بخوانید Pipe) استفاده شد. عملکرد این نشانه درست مثلِ OR در خیلی از زبان هاست. استفاده از این نشانه بیانگرِ اینه که هر کدام از کاراکترهای - یا فاصله قابل انتخاب هستن. 

از بزرگی الگو وحشت نکنید. به داخل گروه دقت کنید. قصد الگوهای داخل گروه انتخاب یکی از دو فرمت زیر است:

وجود هر کدام از این دو فرمت در رشته قابل قبوله.

Lookarounds

خیلی از اوقات اتفاق می افته که بخواهیم قسمتی از یک رشته را بر اساس آنچه قبل یا بعد از آن آمده انتخاب کنیم. بر همین اساس Lookaroundها به دو دسته تقسیم می شن: 

قالب کلی Lookbehind به شکل (=>?) است:

در مثال بالا نام خانوادگیِ تمامِ افرادی که دارای اسمِ Ali هستن انتخاب می شه.

قالب کلی Lookahead به شکل (=?) است:

در این مثال هم نام تمام کسانی که دارای فامیلِ Ebtehaj هستن انتخاب می شه. 

با تغیر = به ! می شه نفی عبارت های بالا رو ساخت. امتحان کنید.

Regular Expression در #C

تمام کلاس های مربوط به RegEx در System.Text.RegularExpressions قرار دارن. مهم ترین کلاس این مجموعه Regex است. متدِ ()Matches در این کلاس جست و جوی رشته ها رو به عهده داره. کد زیر تمام شماره موبایل های موجود در یک متن رو استخراج می کنه:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15static void Main() { var text = @" I think his number was '09121231234'. I asked him to write it down in nice formats. So once he wrote '0912 123 1234' and the other time '0912-123-1234'. But wait! One more thing! Do not stop them wieh '0's. Something like '912 123 1234'"; foreach (var phone in GetPhoneNumbers(text)) Console.WriteLine(phone); } private static IEnumerable<string> GetPhoneNumbers(string text) => Regex.Matches(text, @"\b0?9\d{2}((?:-| )?)\d{3}\1\d{4}\b") .Select(x => x.Value);
091212231234
0912 123 1234
0912-123-1234
912 123 1234

Regular Expression در Javascript

Javascript با کد خیلی کمتری مسئله ی بالا رو حل می کنه 😎👌

1 2 3 4 5 6 7 8 9 10const text = ` I think his number was '09121231234'. I asked him to write it down in nice formats. So once he wrote '0912 123 1234' and the other time '0912-123-1234'. But wait! One more thing! Do not stop them wieh '0's. Something like '912 123 1234'`; const getPhoneNumbers = text => text.match(/\b0?9\d{2}((?:-| )?)\d{3}\1\d{4}\b/g); console.log(getPhoneNumbers(text));

در JS تابعِ ()match متعلق به نوعِ string است بنابراین در اکثر موارد نیازی به استفاده از کلاس سومی نیست. همچنین به g/ دقت کنید. این نشانه که بهش Global Flag می گن بیان کننده ی جست و جوی کل رشته است. در صورتی که نوشته نشود تنها اولین برخورد بازگشت داده خواهد شد. این Flag در سایت regexr.com هم موجوده برای آشنایی بیشتر مثال های این پست رو بدون این Flag هم امتحان کنید.

توجه! دقت کنید که Flag‏ ِ Global امکان Javascript نیست بلکه یکی از ویژگی های RegEx است. در پیاده سازی NET. این ویژگی در نام متدها کپسوله شده است. متدِ ()Match و ()Matches را مقایسه کنید.

کلام آخر

در این پست قواعد کلی و نشانه های کاربردی در Regular Expressions بیان شد و در نهایت یک مثال در دو زبانِ #C و Javascript هم نوشته شد. پرداختن به قابلیت زبان ها در این زمینه دغدغه ی این پست نبود، بنابراین توضیحات زیادی هم داده نشد. برای یادگیری بیشترِ RegEx به سایت regular-expressions.info مراجعه کنید، کلی کتاب خوب هم در این زمینه موجوده.