رجکس (Regex)!… همون ابزار قدرتمند و مرموزی که یه روز حس میکنی باهاش داری جادو میکنی و فردای همون روز، ساعتها به یه خط کد زل زدی و نمیفهمی چرا کار نمیکنه!
این حس کلافگی رو همهی ما که با متن و داده سر و کار داریم، تجربه کردیم. حس اینکه «چرا این الگوی به این سادگی جواب نمیده؟!»
واقعیت اینه که رجکس یه سری تلههای پنهان داره. یه سری دامهای کوچیک که اگه حواست بهشون نباشه، کلی از وقت و انرژیت رو میگیرن. من توی این مسیر بارها و بارها توی این تلهها افتادم و خب، درسهای زیادی گرفتم.
توی این مقاله، نمیخوام یه کلاس تئوری خشک و خستهکننده راه بندازم. میخوام دستت رو بگیرم و با هم قدم به قدم بریم سراغ همون «محدودیتها و خطاهای رایج در رجکس» که خودم تجربه کردم. از اون ستاره (*) و بعلاوه (+) معروف گرفته تا تلهی ترسناک «حریص بودن» (Greedy) که میتونه کل دیتای تو رو ببلعه!
آمادهای که این غول رو با هم رام کنیم و یک بار برای همیشه بفهمیم مشکل از کجا آب میخوره؟
جدول کاربردی: چکلیست فرار ازخطاهای رجکس
| تله (اشتباه رایج) | راهحل سریع (چیکار کنیم؟) |
|---|---|
| ۱. فراموش کردن Escape کردن (مثل . یا *) | همیشه با بک اسلش () خنثیشون کن (مثلاً .). |
| ۲. تفاوت * و + (تکرارکنندهها) | * یعنی «صفر یا بیشتر» (اختیاری) / + یعنی «یکی یا بیشتر» (اجباری). |
| ۳. تطبیق «حریصانه» (بلعیدن همهچیز با .*) | «تنبلش» کن! از علامت سوال ? استفاده کن (مثلاً .*?). |
| ۴. فراموش کردن لنگرها (^ و $) | برای اعتبارسنجی کل رشته، حتماً با ^ شروع و با $ تموم کن. |
| ۵. محدوده اشتباه در کلاس (مثل [A-z]) | همیشه دقیق و جدا بنویس: [A-Za-z]. |
| ۶. پرانتزهای بیدلیل (Capturing الکی) | اگه به حافظهی گروه نیاز نداری، از (?:…) استفاده کن تا سریعتر بشه. |
| ۷. درک نکردن Lookaround | یادت باشه اینا «چک میکنن» ولی جزو «نتیجه» (متن مَچ شده) نیستن. |
تله (اشتباه رایج)
راهحل سریع (چیکار کنیم؟)
۱. فراموش کردن Escape کردن (مثل . یا *)
همیشه با بک اسلش () خنثیشون کن (مثلاً .).
۲. تفاوت * و + (تکرارکنندهها)
* یعنی «صفر یا بیشتر» (اختیاری) / + یعنی «یکی یا بیشتر» (اجباری).
۳. تطبیق «حریصانه» (بلعیدن همهچیز با .*)
«تنبلش» کن! از علامت سوال ? استفاده کن (مثلاً .*?).
۴. فراموش کردن لنگرها (^ و $)
برای اعتبارسنجی کل رشته، حتماً با ^ شروع و با $ تموم کن.
۵. محدوده اشتباه در کلاس (مثل [A-z])
همیشه دقیق و جدا بنویس: [A-Za-z].
۶. پرانتزهای بیدلیل (Capturing الکی)
اگه به حافظهی گروه نیاز نداری، از (?:…) استفاده کن تا سریعتر بشه.
۷. درک نکردن Lookaround
یادت باشه اینا «چک میکنن» ولی جزو «نتیجه» (متن مَچ شده) نیستن.
خطای شماره ۱: فراموش کردن Escape کردن متاکاراکترها (Metacharacters)
آخ! این یکی دقیقاً همون جاییه که خیلی از ماها، از جمله خودِ من، اولین باری که با رِجِکس (Regex) کار کردیم، به بنبست خوردیم. حس کسی رو داری که میخواد یه پیچ ساده رو سفت کنه، اما آچاری که دستشه، هی پیچ رو هرز میکنه و کار رو خرابتر میکنه. اون آچار اشتباهی، همین «متاکاراکترها» هستن.
متاکاراکتر چیست؟ (لیست کاراکترهای خاص مانند . * + ? ( ))
ببین، متاکاراکترها یه سری حروف و علامت خاص توی دنیای رجکس هستن که معنی «تحتاللفظی» خودشون رو نمیدن. اونا در واقع «دستور» هستن.
انگار توی یه دستور آشپزی، بهجای اینکه بگی «نمک»، بگی «هر ادویهای که دم دستته». این کاراکترها به رجکس میگن که چطور جستجو کنه، نه اینکه دنبال چی بگرده.
معروفترینهاشون اینان:
. (نقطه): یعنی «هر کاراکتری» (به جز خط جدید، معمولاً)
* (ستاره): یعنی «صفر یا بیشتر» از کاراکتر قبلی
+ (بعلاوه): یعنی «یک یا بیشتر» از کاراکتر قبلی
? (علامت سوال): یعنی «صفر یا یکی» از کاراکتر قبلی
( ) (پرانتز): برای گروهبندی کردن
[ ] (براکت): برای تعریف یه «کلاس کاراکتر» (مثلاً [a-z])
(بک اسلش): این خودِ راهحله که الان بهش میرسیم!
راه حل: استفاده از بک اسلش () برای خنثی کردن معنای خاص
خب، حالا اگه واقعاً بخوای دنبال خودِ «نقطه» بگردی چی؟ یا بخوای عبارتی رو پیدا کنی که توش علامت «+» داره؟
اینجاست که «بک اسلش» ( ) مثل یه قهرمان وارد میشه.
کار بک اسلش اینه که به کاراکترِ بعد از خودش میگه: «هی رفیق! میدونم که تو معمولاً یه دستور خاصی، ولی این یه بار رو بیخیال شو و فقط خودت باش. میخوام خودِ خودت رو پیدا کنم.» به این کار میگن ‘Escape کردن’.
مثال عملی (تجربه): تفاوت جستجوی . (هر کاراکتر) و . (کاراکتر نقطه)
بذار یه خاطره واقعی برات تعریف کنم. اوایل کارم، یه لیست بلندبالا از URLها داشتم و میخواستم تمام دامنههایی که به .ir ختم میشن رو توی یه فایل تکست پیدا کنم.
منم خیلی ساده توی ابزار سرچم که از رجکس پشتیبانی میکرد، نوشتم: .ir
نتیجه یه فاجعه بود! میدونی چی پیدا کرد؟
چیزایی مثل site.ir رو پیدا کرد (که درست بود)، اما site-ir و sitebir و site9ir رو هم پیدا کرد!
چرا؟ چون اون نقطه (.) داشت مثل یه «کارت جوکر» عمل میکرد و میگفت: «هر کاراکتری قبل از ir قبوله!»
اونجا بود که فهمیدم اشتباه کردم و باید دنبال این بگردم: .ir
این بک اسلش کوچولو، جادوی کار بود. به رجکس گفت: «من دنبال هر کاراکتری نیستم، من دقیقاً دنبال خودِ کاراکترِ نقطه میگردم.»
پس یادت باشه:
. (نقطه تنها): یعنی «هر چیزی» (مثل a, b, 1, !, … )
. (نقطه با بک اسلش): یعنی دقیقاً خودِ «.»
درک نادرست Quantifiers (تعیینکنندههای تکرار)
آخ! این همونجاییه که رجکس (Regex) از یه ابزار ساده تبدیل میشه به یه ابزار قدرتمند… یا یه کابوس! Quantifiers دقیقاً به موتور رجکس میگن که از اون الگوی قبلی، «چند تا» میخوای؟
یادمه اوایل فکر میکردم اینا دیگه زیادی پیچیدهان، ولی بذار ساده بگم: مثل اینه که بری رستوران و بگی «من کباب میخوام». خب، گارسون میپرسه: «چند سیخ؟» اون * و + و ? همون جوابیان که تو به گارسون میدی.
اشتباه رایج: تفاوت کلیدی * (صفر یا بیشتر) و + (یک یا بیشتر)
این دو تا شبیهان، اما تفاوتشون مرگ و زندگیه!
ستاره * (صفر یا بیشتر): این یکی خیلی «بیخیال» و «آسودهخاطر»ه. میگه: «ببین، اگه اون کاراکتر قبلی بود، دمش گرم، هر چند تا بود بیار. اگه هم نبود، بازم فدای سرت! من بازم الگو رو مَچ (Match) میکنم.»
مثال: الگوی go*l رو در نظر بگیر. این الگو هم gl رو پیدا میکنه (چون ‘o’ صفر بار اومده) و هم gol (یک بار) و هم goool (چند بار).
بعلاوه + (یک یا بیشتر): این یکی «سختگیر» و «جدی»ـه. میگه: «من حداقل یکی از اون کاراکتر قبلی رو میخوام. اگه نباشه، اصلاً حرفشم نزن! مَچی در کار نیست.»
مثال: الگوی go+l رو در نظر بگیر. این الگو gol (یک بار) و goool (چند بار) رو پیدا میکنه، ولی هرگزgl رو پیدا نمیکنه (چون ‘o’ حداقل یک بار باید باشه).
این اشتباه ساده میتونه کل نتایج فیلتر کردن لاگها یا پیدا کردن URL ها رو به هم بریزه.
چه زمانی از ? (صفر یا یکی) استفاده کنیم؟
علامت سوال ? دوستداشتنیترین تعیینکنندهی منه. بهش میگم «کاراکترِ اختیاری».
? میگه: «اون کاراکتر قبلی، یا نباشه (صفر بار) یا فقط یکی باشه (یک بار). بیشتر شد دیگه نمیخوامش.»
این عالیه برای وقتهایی که یه چیزی «اختیاری» (Optional) هست.
مثال کلاسیک: فرض کن میخوای هم http رو پیدا کنی و هم https. اگه بنویسی https، اونایی که http هستن جا میمونن.
راه حل: مینویسی https?.
اینجا s? یعنی اون ‘s’ میتونه باشه (که میشه https) یا میتونه نباشه (که میشه http). تو هر دو حالت، الگو مَچ میشه.
چرا .* میتواند خطرناک باشد؟ (مشکل تطبیق بیش از حد یا Over-matching)
خب، رسیدیم به خطرناکترین ترکیب رجکس! .* (نقطه ستاره).
بذار اینو برات کالبدشکافی کنم:
. (نقطه) یعنی: «هر کاراکتری»
* (ستاره) یعنی: «صفر یا بیشتر»
پس .* یعنی: «هر کاراکتری رو، هر چند تا که بود، بگیر و برو جلو تا جایی که میتونی!»
مشکل کجاست؟ این ترکیب «حریص» (Greedy) هست. مثل جاروبرقیای که روشنش میکنی و نهتنها آشغالها، بلکه فرش و هرچیزی که روشه رو هم میبلعه!
تجربه ترسناک من (Over-matching):
یه بار میخواستم متن بین دو تا تگ <b> رو توی یه فایل HTML دربیارم. متن این بود:
لطفا <b>این متن</b> را بخوانید و به <b>این نکته</b> توجه کنید.
منم سادهلوحانه نوشتم: <b>.*</b>
انتظار داشتم دو تا نتیجه بگیرم: ۱.این متن۲.این نکته
اما میدونی چی گرفتم؟ یه نتیجهی غولآسا:
این متن</b> را بخوانید و به <b>این نکته
چرا؟! چون اون .* حریص، از اولین <b> شروع کرد و تا آخرین</b> که توی متن دید، همهچیز رو بلعید! اصلاً براش مهم نبود که وسط راه یه </b> دیگه هم بود.
این مشکل «تطبیق بیش از حد» (Over-matching) میتونه دادههای شما رو کاملاً نابود کنه. (راه حلش معمولاً استفاده از نسخهی «تنبل» یا Non-Greedy یعنی .*? هست، که اون یه داستان دیگهاس!)
تلهی Greedy (حریصانه) در برابر Lazy (تنبل) Matching
این دقیقاً ادامهی اون داستان ترسناک .* هست که برات تعریف کردم! این تفاوت بین «حریص» و «تنبل» بودن، یکی از اون نکتههاییه که وقتی یاد میگیری، حس میکنی یه قفل بزرگ توی مغزت باز شده. بیشتر خطاهایی که توی رجکس (Regex) میبینیم، دقیقاً از همین تلهی «حریص بودن» میاد.
Greedy Matching (تطبیق حریصانه) چگونه کار میکند؟
به طور پیشفرض، همهی اون تعیینکنندههای تکرار (Quantifiers) که گفتیم (*, +) حریص (Greedy) هستن.
«حریص» یعنی چی؟ یعنی تا جایی که جا داره، میبلعه!
وقتی به رجکس میگی .*، اون مثل یه کش عمل میکنه که تا جایی که میتونه کش میاد. از نقطهی شروع، اونقدر کاراکترها رو میخوره و جلو میره تا به آخرین نقطهی ممکن توی متن برسه که هنوز کل الگو (مثلاً وجود یه </b> در انتها) صادق باشه. اصلاً کاری به تطبیقهای کوتاهترِ وسط راه نداره.
راه حل: چگونه با افزودن ? (مانند *?) تطبیق را Lazy کنیم؟
حالا، راه نجات چیه؟ یه علامت سوال (?) کوچولو!
اگه یه ? رو بلافاصله بعد از یه تعیینکننده تکرار (* یا +) بذاری، کل رفتار اون رو عوض میکنی. اون رو از «حریص» (Greedy) تبدیل میکنی به «تنبل» (Lazy).
*? (تنبل): یعنی «صفر یا بیشتر، اما تا جایی که ممکنه کم! (کمترین حالت ممکن)»
+? (تنبل): یعنی «یک یا بیشتر، اما تا جایی که ممکنه کم! (کمترین حالت ممکن)»
وقتی از .*? استفاده میکنی، به موتور رجکس میگی: «ببین، هر کاراکتری رو مَچ کن، ولی تنبل باش! به محض اینکه اولین چیزی رو پیدا کردی که با ادامهی الگوی من جور درمیاد، همونجا وایسا! لازم نیست تا ته دنیا بری.»
سناریوی واقعی: استخراج تگهای HTML (نمایش تجربه عملی)
برگردیم سر همون تجربهی واقعی من با تگهای <b>.
متن این بود:
لطفا <b>این متن</b> را بخوانید و به <b>این نکته</b> توجه کنید.
۱. تلاش حریصانه (اشتباه):
الگو:<b>.*</b>
تفکر موتور رجکس: «خب، <b> رو پیدا کردم (اول متن). حالا .* شروع میکنه به بلعیدن: ‘ا’, ‘ی’, ‘ن’, ‘ ‘, ‘م’, ‘ت’, ‘ن’, ‘<‘, ‘/’, ‘b’, ‘>’, ‘ ‘, ‘ر’, ‘ا’, … همینطور میرم جلو… اوه، اینجا یه </b> هست (بعد از ‘این متن’)، ولی صبر کن… بذار ببینم بازم میتونم جلوتر برم و تهش یه </b> دیگه پیدا کنم؟… آره! ایناهاش! یکی هم آخر جمله (بعد از ‘این نکته’) هست! پس تا همونجا هممممه رو میگیرم!»
نتیجه:این متن</b> را بخوانید و به <b>این نکته (فاجعه!)
۲. تلاش تنبل (راه حل):
الگو:<b>.*?</b> (اون ? جادویی رو ببین!)
تفکر موتور رجکس: «خب، <b> رو پیدا کردم (اول متن). حالا .*? شروع میکنه به مَچ کردن، ولی تنبله. دائماً از خودش میپرسه: ‘الان </b> رو پیدا کردم؟’ … ‘ا’… نه. ‘ی’… نه. … ‘ن’… نه. حالا <… ‘/’… ‘b’… ‘>’… آها! خودشه! همینجا وایمیسم!»
نتیجه اول:این متن
ادامه کار موتور: «خب، کارم اینجا تموم شد. حالا از همینجا دوباره دنبال الگو میگردم… جلو… جلو… آها! یه <b> دیگه پیدا کردم (قبل از ‘این نکته’). حالا دوباره .*? تنبل شروع میکنه… ‘ا’… ‘ی’… ‘ن’… تا برسه به <… ‘/’… ‘b’… ‘>’… عالیه! دوباره پیدا کردم و وایمیسم.»
نتیجه دوم:این نکته
این ? کوچولو، تفاوت بین یه ابزار دقیق و یه ماشین خرابکار رو مشخص میکنه. از وقتی اینو یاد گرفتم، تقریباً همیشهموقع کار با متنهای پیچیده مثل HTML یا XML، از نسخهی Lazy یعنی .*? استفاده میکنم.
استفاده اشتباه از Anchors (لنگرها: ^ و $)
اوه، این یکی از اون تلههای کلاسیکه! لنگرها (^ و $) مثل اون نگهبانهای دم در هستن. اونا کاری به خودِ کاراکترها ندارن، بلکه به «موقعیت» کاراکترها کار دارن. یعنی میگن الگو کجا باید شروع بشه و کجا باید تموم بشه.
فراموش کردنشون مثل اینه که یه شماره تلفن رو بدون اینکه چک کنی اول و آخرش چیز اضافهای (مثل حروف یا کاراکترهای عجیب) نداشته باشه، توی فرم قبول کنی. نتیجه؟ دادهی کثیف!
چرا الگوی شما کل رشته را مطابقت نمیدهد؟ (نقش ^ و $)
این سوال کلیدیترین نکتهی اعتبارسنجی (Validation) با رجکسه.
ببین، وقتی تو یه الگو مینویسی، مثلاً d+ (یعنی یک یا چند عدد)، موتور رجکس میگرده تا هر جایی توی متن که این الگو پیدا بشه، بهت خبر بده.
متن:اسم من 123 است.
الگو:d+
نتیجه:123 رو پیدا میکنه و خوشحاله! چون تکهای از متن با الگوش مَچ شده.
حالا فرض کن میخوای چک کنی که یه ورودی فقط شامل عدده یا نه. یعنی میخوای مطمئن بشی کل رشته، از اول تا آخر، فقط عدده.
اینجاست که لنگرها وارد میشن:
^(هشت یا کلاه): میگه «مَچ باید دقیقاً از ابتدای رشته شروع بشه.»
$(دلار): میگه «مَچ باید دقیقاً در انتهای رشته تموم بشه.»
حالا الگوی قبلی رو اینطوری بازنویسی میکنیم:
متن:اسم من 123 است.
الگو:^d+$
نتیجه:هیچی! چون رشته با «ا» شروع شده، نه با عدد (d). پس شرط ^ همون اول کار رَد میشه.
حالا اگه این متن رو بهش بدی:
متن:123
الگو:^d+$
نتیجه:123 (موفقیت کامل!)
پس اگه میخوای کل رشته رو اعتبارسنجی کنی (مثل فرمت ایمیل، شماره موبایل، کد پستی)، همیشه باید از ^ در اول و $ در آخر الگوت استفاده کنی.
تاثیر حالت چندخطی (Multiline Mode / m flag) بر عملکرد لنگرها
اینم یه پیچ خطرناک! ^ و $ یه رفتار دوگانه دارن.
رفتار پیشفرض:^ یعنی شروع کل متن و $ یعنی پایان کل متن.
اما اگه تو یه متن چند خطی داشته باشی (مثلاً یه فایل لاگ یا یه پاراگراف)، و بخوای خط به خط پردازش کنی چی؟
اینجا «حالت چندخطی» یا Multiline Mode (که معمولاً با یه فلگ m فعال میشه) وارد عمل میشه.
وقتی این حالت روشنه، رفتار لنگرها عوض میشه:
^(در حالت /m): یعنی شروع کل متنیا شروع هر خط جدید (بعد از n).
$(در حالت /m): یعنی پایان کل متنیا پایان هر خط (قبل از n).
مثال: فرض کن میخوای تمام خطهایی که با کلمهی ERROR: شروع میشن رو پیدا کنی.
متن:
INFO: System boot…
ERROR: Disk space low.
INFO: Services loading…
ERROR: DB connection failed.
الگوی بدون /m:^ERROR:
نتیجه:هیچی! چون کل متن با INFO: شروع شده، نه ERROR:.
الگوی با /m:^ERROR: (و فلگ m فعال است)
نتیجه ۱:ERROR: Disk space low.
نتیجه ۲:ERROR: DB connection failed.
میبینی؟ ^ حالا به شروع خط دوم و چهارم هم واکنش نشون داد!
تفاوت A و Z (شروع و پایان مطلق رشته) با ^ و $
خب، حالا که دیدیم ^ و $ با حالت چندخطی رفتارشون عوض میشه، یه سوال پیش میاد:
«اگه من همیشه و تحت هر شرایطی، فقط و فقط دنبال شروع و پایان مطلقِ کلِ متن باشم، حتی اگه حالت /m روشن بود، چیکار کنم؟»
جواب اینجاست: A و Z (و گاهی z).
A(Absolute Start): این لنگر همیشه فقط و فقط به شروع کل رشته مَچ میشه. حالت m روش هیچ تاثیری نداره. این همون نگهبان سرسختِ ورودی اصلیه!
Z(Absolute End): این لنگر همیشه به پایان کل رشته مَچ میشه. (تفاوت خیلی ظریفی با z داره که معمولاً Z اجازه میده یه خط جدید (n) در انتهای فایل باشه، ولی z حتی اونم اجازه نمیده. اما در ۹۹٪ موارد، کار Z همون پایان مطلقه).
خلاصه:
میخوای کل یه ورودی رو اعتبارسنجی کنی؟ ^…$ (و حواست به فلگ m باشه).
میخوای خط به خط توی یه متن چندخطی جستجو کنی؟ ^… یا …$ (و فلگ m رو روشن کن).
میخوای تحت هر شرایطی مطمئن بشی که دقیقاً اول یا آخر کل دیتا هستی؟ از A… یا …Z استفاده کن.
سردرگمی در کلاسهای کاراکتری (Character Classes [])
آها! «کلاسهای کاراکتری» یا همون براکتها []! من همیشه به اینا میگم «دنیای موازی» یا «اتاق امنِ» رجکس (Regex).
قضیه اینه که وقتی کاراکترها وارد این براکتها میشن، قوانین دنیای بیرون (یعنی رجکس معمولی) تا حد زیادی عوض میشه. خیلی از اون کاراکترهای ویژه که کلی در موردشون حرف زدیم، اینجا تبدیل به کاراکترهای کاملاً معمولی میشن. اما خودِ این اتاق امن هم یه سری قانون جدید داره که اگه ندونیم، بدجوری غافلگیر میشیم.
آیا متاکاراکترها (مانند .) در داخل [] هنوز خاص هستند؟
این اولین و کلیدیترین قانون این «اتاق امن» هست: نه!
وقتی متاکاراکترها پاشون رو میذارن داخل []، اون قدرت جادوییشون رو از دست میدن و تبدیل به یه شهروند عادی میشن.
بیرون براکت:. (نقطه) یعنی «هر کاراکتری».
داخل براکت:[.] یعنی «فقط و فقط خودِ کاراکترِ نقطه».
همین داستان برای بقیه هم هست:
اگه تو بنویسی [?*+]، تو دیگه دنبال «صفر یا بیشتر» یا «یک یا بیشتر» نیستی. تو دقیقاً دنبال خودِ کاراکترِ «علامت سوال» یا «ستاره» یا «بعلاوه» میگردی. اونا دیگه دستور نیستن، فقط خودِ کاراکترن.
البته چندتا استثنای مهم وجود داره:
(بک اسلش) هنوزم قدرت Escape کردن رو داره. (مثلاً [d] هنوز یعنی عدد).
– (هایفن) اگه وسط بیاد، برای تعریف «محدوده» (Range) استفاده میشه.
^ (کلاه) اگه دقیقاً اول بیاد، معنی «نفی» میده (که الان بهش میرسیم).
اشتباه در تعریف محدوده (Range): چرا [A-z] اشتباه است؟ (راه حل: [A-Za-z])
وای، این اشتباهیه که شاید بشه گفت همهی ما حداقل یه بار انجامش دادیم!
آدم با خودش میگه خب، «از A تا z»! اینطوری همهی حروف بزرگ و کوچیک رو میگیرم دیگه. منطقی هم به نظر میاد، نه؟ اما کاملاً اشتباهه!
مشکل اینجاست که کامپیوتر و رجکس، حروف الفبا رو بر اساس ترتیب ما نمیشناسن، بر اساس جدول کدهای اسکی (ASCII) میشناسن. توی این جدول:
حروف بزرگ A تا Z پشت سر هم هستن.
حروف کوچیک a تا z هم پشت سر هم هستن.
اما بین Z و a یه عالمه کاراکتر مزاحم دیگه وجود داره! (چیزایی مثل [, , ], ^, _, `)
نتیجه فاجعهبار:
وقتی تو مینویسی [A-z]، در واقع داری به رجکس میگی: «هر کاراکتری بین A تا Z، بعلاوهی همهی اون کاراکترهای مزاحم وسط، بعلاوهی همهی کاراکترهای بین a تا z.»
این فیلتر شما رو پر از کاراکترهای ناخواسته میکنه.
راه حل درست و تمیز:
اگه فقط و فقط حروف الفبای انگلیسی (بزرگ و کوچیک) رو میخوای، باید صریح بهش بگی: [A-Za-z].
این یعنی: «هر کاراکتری که یا توی محدودهی A تا Z هست یا توی محدودهی a تا z».
استفاده صحیح از نفی (Negation) با ^ در ابتدای کلاس
یادت میاد گفتیم لنگر ^ (هشت یا کلاه) یعنی «شروع رشته»؟ خب، این قانون مال بیرون براکت بود.
همین کاراکتر ^ اگه بیاد داخل براکت []، و دقیقاًاولین کاراکتر باشه، معنیش ۳۶۰ درجه عوض میشه!
[^…] یعنی «نفی» (Negation) یا «هر چیزی به جز…».
این به رجکس میگه: «من دنبال هر کاراکتری میگردم، به جز اونایی که تو جلوی من لیست کردی.»
مثال تفاوتشون:
[aeiou]
معنی: «یکی از حروف صدادار» (a یا e یا i یا o یا u).
[^aeiou]
معنی: «هر کاراکتری به جز حروف صدادار» (مثلاً b, c, 1, $ و… همگی مَچ میشن).
تله کوچیک: این قانون فقط وقتیه که ^اولین کاراکتر باشه. اگه وسط بیاد، همون کاراکتر «کلاه» معمولیه.
[a^b] یعنی «a یا ^ یا b». (دیگه معنی نفی نمیده).
اشتباهات رایج در گروهبندی (Grouping) و استخراج (Capturing)
آخ، این پرانتزها! ()… اونا قلب تپندهی رجکس (Regex) هستن. راستش رو بخوای، اوایل کارم فکر میکردم پرانتز فقط برای اینه که بگم «این چند تا کاراکتر با هم هستن». مثل اینکه یه سری وسیله رو میریزی توی یه کیسه پلاستیکی که گم نشن.
غافل از اینکه رجکس یه قابلیت پنهانی و خیلی مهم هم به این «کیسهها» میده: «حافظه»!
اینجاست که همهچی پیچیده (و البته فوقالعاده کاربردی) میشه و اکثر اشتباهات هم دقیقاً از همین نقطه شروع میشن.
تفاوت گروه Capturing (…) و گروه Non-Capturing (?:…)
این کلیدیترین مفهومییه که باید در مورد پرانتزها بدونی.
گروه Capturing (پرانتز معمولی (…)):
این همون «کیسهی شفافِ شمارهدار» خودمونه. وقتی از () معمولی استفاده میکنی، به موتور رجکس دو تا دستور میدی:
۱. «این الگوها رو با هم در نظر بگیر.»
۲. «هرچیزی که داخل این پرانتز پیدا کردی رو یادداشت کن و توی حافظه نگه دار.» (مثلاً بهعنوان گروه شماره ۱، گروه شماره ۲ و الی آخر).
گروه Non-Capturing (پرانتز با ?: یعنی (?:…)):
این مثل یه «کیسهی پلاستیکی معمولی» عمل میکنه. وقتی از (?:…) استفاده میکنی، فقط یه دستور میدی:
۱. «این الگوها رو با هم در نظر بگیر.»
همین! موتور رجکس اصلاً به خودش زحمت نمیده که محتویات داخل این پرانتز رو به حافظه بسپره. فقط کار گروهبندی رو انجام میده و رد میشه.
اشتباه رایج کجاست؟
ما همهجا، حتی وقتی اصلاً به محتویات اون گروه نیازی نداریم، از () معمولی استفاده میکنیم. این کار بیخودی حافظهی رجکس رو پر میکنه، میتونه الگو رو کُند کنه و بدتر از همه، شمارهبندی گروههایی که واقعاً لازم داریم رو به هم میریزه!
چرا Backreference (مانند 1) شما کار نمیکند؟
این یکی از اون باگهاییه که آدم رو حسابی کلافه میکنه و ریشهاش دقیقاً توی همون تفاوت بالاییه.
Backreference (مثل 1 یا 2) یعنی چی؟ یعنی به رجکس بگی: «برو ببین توی گروه شماره ۱ چی پیدا کرده بودی؟ دقیقاً همون رو دوباره اینجا پیدا کن.»
این عالیه برای پیدا کردن کلمات تکراری. مثلاً الگوی (w+) 1:
۱. (w+): یه کلمه پیدا کن (مثلاً hello) و بریزش توی حافظهی گروه ۱.
۲. : یه فاصله پیدا کن.
۳. 1: حالا دقیقاً دنبال همون چیزی که تو گروه ۱ هست (یعنی hello) بگرد.
نتیجه:hello hello رو پیدا میکنه.
حالا اشتباه کجاست؟
فرض کن تو به اشتباه (مثلاً برای تمیزکاری!) الگوت رو اینطوری بنویسی: (?:w+) 1
این الگو هیچوقت کار نمیکنه!
چرا؟ چون 1 میگرده دنبال «حافظهی گروه شماره ۱»، ولی تو با استفاده از (?:…) صراحتاً به رجکس گفتی: «این گروه رو لازم ندارم، یادداشتش نکن!» در نتیجه حافظهی گروه ۱ خالیه و 1 به هیچی اشاره نمیکنه و الگو شکست میخوره.
استفاده از گروههای نامگذاری شده (Named Groups) برای خوانایی
خب، حالا فرض کن یه الگوی رجکس نوشتی که ۱۰ تا پرانتز تو در تو داره تا یه تاریخ یا URL پیچیده رو تجزیه کنه. مثل یه کلاف کاموای گره خورده!
^(d{4})-(d{2})-(d{2})$
بعداً که میخوای توی کدِت ازش استفاده کنی، باید یادت باشه که 1 (یا گروه ۱) یعنی سال، 2 یعنی ماه، و 3 یعنی روز. حالا اگه یه پرانتز دیگه وسطش اضافه کنی چی؟ کل شمارهبندی به هم میریزه! این یه کابوسه برای نگهداری کد.
راه حل شیک و حرفهای:
استفاده از «گروههای نامگذاری شده» (Named Groups).
تو میتونی برای اون «کیسههای شفاف» اسم بذاری!
الگوی بالا رو اینطوری بازنویسی میکنیم:
^(?<year>d{4})-(?<month>d{2})-(?<day>d{2})$
حالا ببین چقدر خوانا شد!
بهجای اینکه بعداً بگی «گروه شماره ۲ رو بهم بده»، توی کدِت (مثلاً پایتون، C#، جاوااسکریپت و…) مستقیماً میگی «گروه month رو بهم بده».
این کار خوانایی الگوی تو رو وحشتناک بالا میبره و دیگه لازم نیست هفتهی بعد که کدت رو میبینی، از خودت بپرسی «این 2 چی بود اصلاً؟!»
راهنمای عملی دیباگ کردن Regex (افزایش اعتماد و تخصص)
خب، رسیدیم به جایی که عیار متخصص از مبتدی معلوم میشه! نوشتن رجکس (Regex) یه بخشه، دیباگ کردنش یه بخش دیگه. راستشو بخوای، همهی ما لحظههایی رو داشتیم که به یه الگوی رجکس زل زدیم و حس کردیم داریم خط میخی میخونیم! «چرا کار نمیکنه؟!»
این همونجاییه که اعتماد به نفست میریزه و حس میکنی کل ماجرا زیادی پیچیدهاس. ولی بذار چند تا ابزار و تکنیک بهت بگم که نه تنها مشکلت رو حل میکنه، بلکه باعث میشه به رجکسی که مینویسی اعتماد کامل داشته باشی. این همون حس تخصص واقعیه!
معرفی بهترین ابزارهای آنلاین (مانند Regex101 و Regexr)
اگه بخوام فقط یه توصیه بهت بکنم، اینه: هیچوقت رجکس رو مستقیم توی کدِت یا توی دیتابیس دیباگ نکن! این مثل اینه که بخوای موتور هواپیما رو وسط پرواز تعمیر کنی. ابزارهای آنلاین برای همین ساخته شدن، اونا «زمین بازی» امن ما هستن.
Regex101.com:
این رفیق همیشگی منه. اصلاً بدون این ابزار کد رجکس نمیزنم. یه جورایی مثل یه دستیار متخصصه که بغل دستت نشسته. چرا اینقدر خوبه؟
توضیح الگو (Explanation): مثل یه مترجم همزمان، قدم به قدم به زبان آدمیزاد بهت میگه هر تیکه از الگوی تو ( d ، + ، [] ) دقیقاً داره چیکار میکنه.
تست زنده (Live Test): متن نمونهات رو میذاری اون پایین، و همونلحظه که الگوت رو تایپ میکنی، بهت نشون میده چی مَچ شد و چی نشد.
حافظهی گروهها (Capture Groups): قشنگ بهت نشون میده توی هر پرانتز (گروه) دقیقاً چی گیر افتاده.
Regexr.com:
اینم عالیه، یه کم ظاهرش دوستانهتر و شاید سادهتره. برای شروعهای سریع و الGOهای سادهتر خیلی خوبه و یه «تقلبنامه» (Cheatsheet) خیلی دمدستی و عالی هم کنارش داره.
تکنیک تست واحد (Unit Testing) برای الگوهای پیچیده
این تکنیک رو من از بچههای برنامهنویس یاد گرفتم و زندگیم رو عوض کرد. وقتی یه الگوی پیچیده داری (مثلاً برای اعتبارسنجی یه URL خاص، یه فرمت پیچیدهی لاگ، یا پیدا کردن کدهای ملی)، فقط به یه متن نمونه که کار میکنه اکتفا نکن.
تو باید یه «تست واحد» (Unit Test) کامل براش بنویسی. یعنی چی؟
لیست موفقیت (Must Match): یه لیست از ۱۰ تا متن نمونه آماده کن که الگوی تو باید اونا رو پیدا کنه (مثلاً فرمتهای مختلف ایمیل درست).
لیست شکست (Must NOT Match): مهمتر از قبلی! یه لیست از ۱۰ تا متن دیگه آماده کن که الگوی تو نباید اونا رو پیدا کنه (مثلاً ایمیلهای ناقص، اشتباه، یا بدون @).
حالا الگوت رو با این ۲۰ تا تست میسنجی (توی همون Regex101 میتونی همهی اینا رو تست کنی). اگه الگوی تو از همهی این تستها سربلند بیرون اومد، یعنی «ضدگلوله» (bulletproof) شده و میتونی با اعتماد کامل ازش توی محصول نهایی استفاده کنی.
اهمیت استفاده از حالت Verbose (Verbose Mode / x flag) و کامنتگذاری
و اما، زیباترین و حرفهایترین بخش ماجرا! حالت Verbose (که معمولاً با فلگ x فعال میشه) به رجکس تو «نَفَس» میده.
یادته گفتیم رجکس شبیه خط میخییه؟
مثلاً این الگو برای پیدا کردن تاریخ: ^(?<year>d{4})-(?<month>d{2})-(?<day>d{2})$
اگه شش ماه دیگه به این نگاه کنی، باید کلی فسفر بسوزونی تا بفهمی چی به چی بود.
حالا اینو ببین (وقتی فلگ x روشنه):
Code snippet
^ # شروع رشته
(?<year>d{4}) # گروه ‘year’: 4 تا عدد برای سال
– # جداکننده هایفن
(?<month>d{2}) # گروه ‘month’: 2 تا عدد برای ماه
– # جداکننده هایفن
(?<day>d{2}) # گروه ‘day’: 2 تا عدد برای روز
$ # پایان رشته
متوجه شدی چی شد؟ حالت Verbose به تو دو تا قدرت شگفتانگیز میده:
نادیده گرفتن فاصلهها (Whitespace): میتونی الگوت رو بشکنی و توی چند خط بنویسی تا خوانا بشه.
کامنتگذاری (Comments): میتونی با # (درست مثل کد پایتون) برای خودت و همتیمیهات توضیح بنویسی که هر بخش الگو دقیقاً چیکار میکنه.
این کار رجکس تو رو از یه معمای غیرقابل فهم، به یه سند خوانا و قابل نگهداری (Maintainable) تبدیل میکنه. این یعنی اوج تخصص و احترام به کسی که قراره بعد از تو اون کد رو ببینه (که اون کس، خیلی وقتها خودِ آیندهی توئه!).
اشتباهات پیشرفته: درک نادرست از Lookarounds
اوه! رسیدیم به بخش مورد علاقهی من! Lookarounds! اینا یه جورایی مثل «جادوی نامرئی» توی رجکس (Regex) میمونن.
بذار یه تصویرسازی برات بکنم: فرض کن داری توی یه راهرو راه میری. Lookarounds مثل اینه که سرت رو از یه در «نگاه کنی» تو، ببینی چی داخله، و بعد سرت رو بیاری بیرون و بدون اینکه وارد اتاق بشی، به راهت ادامه بدی.
اشتباه بزرگ اینجاست که خیلیا فکر میکنن اینا هم مثل پرانتز معمولی، بخشی از متن رو «میگیرن» یا «مصرف میکنن». نمیکنن! اونا فقط «چک میکنن». به همین خاطر بهشون میگن “Zero-width assertions“ یعنی ادعاهایی که هیچ «طولی» یا کاراکتری رو اشغال نمیکنن.
تفاوت Positive Lookahead (?=…) و Negative Lookahead (?!…)
این دو تا، اون جاسوسهایی هستن که «جلو» رو نگاه میکنن.
Positive Lookahead(?=…) (نگاه به جلوی مثبت):
این میگه: «من این الگو رو مَچ میکنم، به شرطی که فلان چیز بلافاصله بعدش بیاد.»
سناریوی واقعی من: یه بار میخواستم تمام قیمتهایی رو توی متن پیدا کنم که واحدشون «تومان» هست.
متن:قیمت 15000 تومان است و 25 دلار.
الگوی اشتباه:d+ تومان (این 15000 تومان رو برمیگردوند، ولی من فقط خود عدد رو میخواستم).
الگوی درست:d+(?= تومان)
تحلیل: این الگو میره جلو، 15000 رو پیدا میکنه. بعدش وایمیسته و «نگاه میکنه جلو». آیا « تومان» بلافاصله بعدش هست؟ بله! پس 15000 رو بهعنوان مَچ برمیگردونه. اون « تومان» فقط چک شد، ولی جزو نتیجهی نهایی نبود.
Negative Lookahead (?!…) (نگاه به جلوی منفی):
این برعکسه. میگه: «من این الگو رو مَچ میکنم، به شرطی که فلان چیز بلافاصله بعدش نیاد.»
سناریوی واقعی من: میخواستم تمام کلمات q رو پیدا کنم که بعدشون uنیومده باشه (مثل کلمات عربی یا اسامی خاص).
متن:This is a queen from Iraq.
الگو:q(?!u)
تحلیل: موتور رجکس میره جلو. به q در queen میرسه. «نگاه میکنه جلو». آیا u بعدش هست؟ بله! پس این مَچ نمیشه.
ادامه میده… به q در Iraq میرسه. «نگاه میکنه جلو». آیا u بعدش هست؟ نه! (پایان رشته است). پس این q مَچ میشه!
نتیجه: فقط q در Iraq.
چرا Lookbehindها ((?<=…)) در همه موتورهای Regex پشتیبانی نمیشوند؟
آخ! این همون جاییه که خیلی از ماها، از جمله خود من، کلی حرص خوردیم. مخصوصاً ماهایی که با جاوا اسکریپت زیاد سر و کار داشتیم.
Lookbehindها ((?<=…) و (?<!…)) برعکس Lookaheadها، «پشت سر» رو نگاه میکنن. «من این الGO رو مَچ میکنم به شرطی که قبلش فلان چیز اومده باشه.»
حالا چرا پشتیبانی نمیشن؟
دلیل اصلی و تاریخیش، پیچیدگی پیادهسازی و عملکرد (Performance) هست.
ببین، موتور رجکس به صورت طبیعی داره از «چپ به راست» توی متن حرکت میکنه.
Lookahead (نگاه به جلو) آسونه: موتور توی موقعیت فعلیشه، فقط کافیه یه «پیک» به جلو بزنه، ببینه چی هست، و برگرده سر جاش و تصمیم بگیره.
Lookbehind (نگاه به عقب) سخته: موتور توی موقعیت فعلیشه. حالا باید «برگرده عقب»! این جریان کاریِ طبیعیش رو به هم میزنه.
اما مشکل اصلی این نبود. مشکل اصلی وقتی بود که میخواستی «پشت سر» رو برای یه الگوی با طول متغیر (Variable-length) چک کنی.
طول ثابت (Fixed-length): مثلاً (?<=USD)d+. این یعنی «عددی رو پیدا کن که دقیقاً۳ کاراکتر قبلش USD باشه». این آسونه. موتور میدونه باید ۳ تا بره عقب و چک کنه. خیلی از موتورها (مثل پایتون) فقط این حالت رو پشتیبانی میکردن.
طول متغیر (Variable-length): حالا اینو تصور کن: (?<=w+)d+. این یعنی «عددی رو پیدا کن که قبلش یک یا چند حرف (w+) اومده باشه.»
این برای موتور یه کابوسه! چقدر باید بره عقب؟ یه کاراکتر؟ ده تا؟ یا تا اولِ کل متن رو بگرده؟! این عدم قطعیت، پیادهسازیش رو وحشتناک سخت و کُند میکرد.
به همین دلیل، خیلی از موتورها (مخصوصاً جاوا اسکریپت تا قبل از ES2018) قیدش رو زده بودن. خوشبختانه، الان موتور V8 جاوا اسکریپت (توی کروم و نود) و اکثر موتورهای مدرن دیگه، Lookbehind رو (حتی با طول متغیر) پشتیبانی میکنن، ولی این سابقهی بد باعث شده هنوزم بگیم «پشتیبانیشون همهگیر نیست».
جمعبندی (نتیجهگیری)
خب، اینم از سفرمون به دنیای مینگذاریشدهی رجکس!
دیدی؟ اونقدرها هم ترسناک نبود. رجکس واقعاً مثل یه زبان برنامهنویسی مینیاتوری و فوقالعاده قدرتمنده. اولش شاید ظاهرش گیجکننده باشه، ولی وقتی منطق پشت این تلهها و خطاها رو درک میکنی، تازه میفهمی چه ابزار دقیقی برای جراحی متن دستته.
یادت نره، نکته کلیدی اینه که نترسی و تمرین کنی. از اون ابزارهای آنلاینی که معرفی کردم (مثل Regex101) استفاده کن و با الگوهای مختلف بازی کن. این خطاها رو نبین بهعنوان بنبست؛ ببینشون بهعنوان پلههایی که دارن بهت منطق عمیقتر رجکس رو یاد میدن.
حالا تو برام بنویس؛ کدوم یکی از این اشتباهات بیشتر از همه وقتت رو گرفته بود؟ یا شاید تلهی جدیدی سراغ داری که من توی این لیست جا انداختم؟ مشتاق خوندن تجربههات هستم!