Python حرفه‌ای: عبارات با قاعده

Professional Python: Regular Expression

24 اسفند 1399
درسنامه درس 23 از سری پایتون حرفه‌ای
Python حرفه ای: عبارات با قاعده (قسمت 23)

regular expression چیست؟

عبارات با قاعده یا regular expressions یکی از مباحث مهم در تمام زبان های برنامه نویسی هستند. اگر بخواهیم به زبان فنی توضیح بدهیم، عبارات با قاعده، توالی خاصی از کاراکترها هستند که یک الگوی جستجوی خاص را مشخص می کنند. در این تعریف باید تمام حواس خود را روی کلمه «الگو» بگذارید چرا که ما می توانیم کل این تعریف را در همین کلمه خلاصه کنیم. ما با استفاده از عبارات با قاعده الگوهایی را تعریف می کنیم که بر اساس این الگوها جستجو انجام می شود. مثلا «دو کاراکتر حرفی و یک کاراکتر عددی پشت سر هم» یک الگوی جستجو است و برای تمام رشته های زیر صدق می کند:

AB2

ow3

Ub7

چرا؟ به دلیل اینکه تمام این رشته ها الگوی مشخص شده توسط ما را دارند: دو کاراکتر حرفی (حروف الفبا) و سپس یک عدد. به مثال زیر توجه کنید:

text = "You can learn programming and web development at roxo.ir/plus for free!"


print('roxo' in text)

من در این کد یک رشته ساده به نام text را دارم. حالا می خواهیم ببینیم آیا رشته roxo درون رشته text وجود دارد یا خیر. برای این کار از اپراتور in استفاده کرده ام و زمانی که این کد را اجرا کنید true را دریافت خواهید کرد. چرا؟ به دلیل اینکه رشته roxo درون text وجود دارد اما اگر بخواهیم بدانیم دقیقا در چه قسمتی از آن وجود دارد چطور؟ در این حالت می توانیم از تابع search استفاده کنیم:

text = "You can learn programming and web development at roxo.ir/plus for free!"


print(text.find('roxo'))

تابع find دقیقا index رشته roxo در text را برمی گرداند. با اجرای کد بالا عدد ۴۹ را دریافت خواهید کرد که یعنی roxo از ایندکس ۴۹ در رشته text شروع می شود (ایندکس حرف r). در صورتی که این جستجو نتیجه ای نداشته باشد عدد ۱- را دریافت خواهید کرد. تمام این قابلیت های جستجو در پایتون کاربردی هستند اما در برخی از اوقات کافی نیستند! ما بعضا با رشته های پیچیده روبرو هستیم و می خواهیم جستجوهای پیشرفته تری را انجام بدهیم. در اینجاست که عبارات باقاعده وارد کار می شوند.

ماژول RE در پایتون

همانطور که قبلا توضیح دادم عبارات با قاعده یک الگو هستند (مثال خودمان «دو کاراکتر حرفی و یک کاراکتر عددی پشت سر هم») اما به روش خاصی نوشته می شوند. این الگوها در ساده ترین حالت یک رشته ساده هستند! برای اینکه متوجه این مفهوم بشوید باید ماژول RE (مخفف regular expression) را وارد فایل خود کنیم:

import re

 

text = "You can learn programming and web development at roxo.ir/plus for free!"

 

result = re.search('roxo', text)

 

print(result)

ماژول re متدی به نام search دارد که سه آرگومان می گیرد:

  • الگو یا pattern: این الگو در ساده ترین حالت مانند مثال بالا (roxo) یک رشته است.
  • رشته ای که باید در آن جستجو انجام شود (متغیر text).
  • Flag های مربوط به جستجو که بعدا در موردشان صحبت می کنیم.

من در مثال بالا ابتدا ماژول re را import کرده ام، سپس الگوی خودمان را به صورت رشته ساده roxo نوشته ام و نتیجه را در متغیری به نام result ذخیره کرده ام. با اجرای کد بالا نتیجه زیر را دریافت می کنیم:

<re.Match object; span=(49, 53), match='roxo'>

به عبارتی یک شیء جستجو برایمان برگردانده شده است که به Match object معروف است. این مسئله بدین معنی است که جستجوی ما دارای نتیجه بوده است. در صورتی که نتیجه ای نداشته باشیم، مقدار None برایمان برگردانده می شود. اگر به دقت به این شیء برگردانده شده نگاه کنید، متوجه span در آن می شوید که به شما می گوید رشته مورد نظر ما از ایندکس ۴۹ تا ۵۳ ادامه دارد (ایندکس شروع و پایان roxo در رشته text). ما به شکل زیر به این خصوصیت دسترسی داریم:

import re

 

text = "You can learn programming and web development at roxo.ir/plus for free!"

 

result = re.search('roxo', text)

 

print(result.span())

 

با اجرای کد بالا tuple زیر را دریافت می کنید:

(49, 53)

همانطور که گفتم مقادیر درون این tuple ایندکس های آغازین و پایانی نتیجه ما است. در صورتی که بخواهیم فقط ایندکس آغازین یا فقط ایندکس پایانی را داشته باشیم از متدهای start و end استفاده می کنیم:

import re

 

text = "You can learn programming and web development at roxo.ir/plus for free!"

 

result = re.search('roxo', text)

 

print(result.start(), result.end())

 

با اجرای کد بالا نتیجه 49 و 53 را به صورت جداگانه دریافت می کنیم. همچنین اگر بخواهیم دقیقا نتیجه جستجو را برگردانیم باید از متد group استفاده کنیم:

import re

 

text = "You can learn programming and web development at roxo.ir/plus for free!"

 

result = re.search('roxo', text)

 

print(result.group())

با اجرای دستور بالا رشته roxo را دریافت می کنیم که نتیجه جستجوی ما بوده است. شاید با خودتان بگویید چنین متدی بی کاربرد است، ما که می دانیم roxo را جستجو کرده ایم! اگر کمی صبر کنید تا با الگوهای REGEX آشنا شویم، جوابتان را خواهید گرفت.

در مرحله بعدی از شما سوالی دارم. اگر بخواهیم تمام رشته را جستجو کرده و تمام نتایج را برگردانیم باید چه کار کنیم؟ به کد زیر توجه کنید:

import re

 

pattern = re.compile('roxo')

text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result1 = pattern.search(text)

 

result2 = pattern.findall(text)

 

print(result1.group())

print(result2)

من در ابتدا برای اینکه کد های خودم را تکرار نکنم از متد compile در ماژول re استفاده کرده ام تا الگوی خودم را تعریف کنم. الگو فعلا همان رشته ساده roxo است. در ضمن در نظر داشته باشید که متن رشته text را کمی تغییر داده ام تا ۲ بار کلمه roxo را در خود داشته باشد. در مرحله بعدی از متد search استفاده کرده ام که اولین نتیجه جستجو را برایمان برمی گرداند. توجه کنید که این بار به جای صدا زدن search به صورت مستقیم از ماژول re، آن را روی pattern صدا زده ایم بنابراین نیازی به پاس دادن الگو (رشته roxo) نیست و فقط باید رشته مورد نظر برای جستجو را پاس بدهیم. به غیر از search متد دیگری به نام findall وجود دارد که تمام رشته را جستجو کرده و تمام نتایج را برایمان برمی گرداند. با اجرای کد بالا نتیجه زیر را دریافت می کنید:

roxo

['roxo', 'roxo']

نتیجه اول متعلق به متد search و نتیجه دوم متعلق به findall است که تمام نتایج (هر دو رشته roxo) را برگردانده است. متد دیگری به نام fullmatch نیز وجود دارد که واحد جستجو را کل رشته در نظر می گیرد:

import re

 

pattern = re.compile('roxo')

 

text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result = pattern.fullmatch(text)

 

print(result)

با اجرای کد بالا none را دریافت می کنیم. چرا؟ به دلیل اینکه fullmatch تمام جمله را در نظر می گیرد و از آنجایی که رشته roxo برابر با کل رشته text نیست، جستجو حاصلی ندارد. همچنین متد دیگری به نام match وجود دارد که تنها شروع رشته را جستجو می کند. به طور مثال:

import re

 

pattern1 = re.compile('roxo')

 

pattern2 = re.compile('You')

 

pattern3 = re.compile('plus')


text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result1 = pattern1.match(text)

 

result2 = pattern2.match(text)

 

result3 = pattern3.match(text)

 

print(result1)

print(result2)

print(result3)

با اجرای این کد نتیجه زیر را می گیریم:

None

<re.Match object; span=(0, 3), match='You'>

None

همانطور که می بینید تنها الگوی You دارای نتیجه بوده است و جستجو های دیگر ما None شده اند. چرا؟ به دلیل اینکه match جستجو را الزاما از ابتدای رشته شروع می کند و اگر الگوی شما را در ابتدای رشته پیدا نکند دیگر ادامه نخواهد داد. این متدها کاربردی هستند اما هنوز هم به قدرت اصلی عبارات باقاعده، یعنی الگوها، نرسیده ایم. تا این بخش الگوی ما یک رشته ساده بوده است که در عمل مانند عدم استفاده از عبارات با قاعده و استفاده از متدهایی مانند find است. اگر الگونویسی در REGEX را یاد بگیریم قدرت بسیار بیشتری پیدا می کنیم.

الگو (pattern) نویسی در Regex

شاید بتوان گفت که عبارات باقاعده یک زبان جداگانه محسوب بشوند! چرا که پیچیدگی های خودشان را دارند و من از شما انتظار ندارم که در یک جلسه روی آن ها تسلط پیدا کنید اما در این بخش سعی خواهیم کرد تا شما را با بیشتر مباحث آن آشنا کنیم. من با یک مثال بسیار ساده شروع می کنم:

import re

 

pattern = re.compile(r't.ro')

 

text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result = pattern.search(text)

 

print(result)

به الگویی که به compile داده ام دقت کنید. در ابتدا حرف r را قبل از رشته گذاشته ایم که مخفف raw است؛ یعنی رشته ای که می نویسیم یک رشته خالص است. یعنی چه؟ اگر یادتان باشد ما می توانستیم کاراکترهای خاصی مانند n\ را در رشته خود قرار بدهیم و این کاراکترها معنی خاصی داشتند. زمانی که از r استفاده کنیم این کاراکترها معنی خاصی نخواهند داشت و n\ واقعا رشته n\ خواهد بود نه یک line break. اما در مورد الگویی که ما درون این رشته نوشته ایم باید بگویم که t همان حرف t است اما علامت نقطه به معنی یک کاراکتر از هر نوعی است (مثلا A یا j یا 5 یا % یا #) البته به غیر از کاراکتر new line و سپس ro نیز به معنی حروف ro است. با این ترجمه این الگو بدین شکل است: رشته ای که «حرف t سپس یک کاراکتر از هر نوع و سپس حروف ro را داشته باشد». با اجرای این کد نتیجه زیر را می گیریم:

<re.Match object; span=(47, 51), match='t ro'>

همانطور که می بینید رشته پیدا شده توسط ما t ro است که قسمتی از at roxo در رشته text می باشد. حالا الگو را کامل تر می کنم:

import re

 

pattern = re.compile(r't.ro.{2}')

 

text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result = pattern.search(text)

 

print(result)

اضافه کردن {}‌ و قرار دادن یک عدد در آن به معنی تکرار کاراکتر قبلی به آن تعداد است. در این رشته کاراکتر قبلی علامت نقطه می باشد که به معنی هر کاراکتری است و با ترکیب {۲} به معنی دو بار تکرار هر کاراکتری خواهد بود. با اجرای کد بالا نتیجه زیر را می گیریم:

<re.Match object; span=(47, 53), match='t roxo'>

قبلا t ro را دریافت کرده بودیم اما حالا نتیجه جستجو t roxo است. دو حرفی که تکرار شده اند xo هستند. من این مثال ساده را برای شما زدم تا بدانید منظور از الگو نویسی چیست و متوجه بشوید که الگو نویسی قوانین خاص خودش را دارد. من برخی از این قوانین مهم را برایتان توضیح می دهم:

  • علامت [] به معنی بازه ای از کاراکترها است.
  • علامت . به معنی هر کاراکتری به جز کاراکتر new line است.
  • علامت ^ به معنی شروع رشته با مقدار خاصی است.
  • علامت \ به معنی کاراکتری با معنی خاص در regex یا برای escape کردن کاراکترهای خاص است.
  • علامت $ به معنی پایان رشته است.
  • علامت * به معنی تکرار صفر بار یا بیشتر است (از صفر تا بی نهایت بار).
  • علامت + به معنی تکرار یک بار یا بیشتر است (از یک تا بی نهایت بار).
  • علامت {} به معنی تکرار به تعداد دفعات خاصی است.
  • علامت | به معنی «یا» است (برقراری یکی از دو حالت).
  • علامت () به معنی گروه بندی مقادیر است.

به کاراکترهای بالا Metacharacters می گویند و وظیفه هر کدام را نیز توضیح داده ام (در ادامه از آن ها مثال خواهید دید) اما به کاراکترهای زیر Special Characters می گویند و وظیفه دیگری دارند. این کاراکترها نسبت به بزرگی و کوچکی حروف حساس هستند بنابراین نهایت دقت را به خرج بدهید:

  • کاراکتر A\: رشته مورد نظر باید در ابتدای رشته اصلی باشد (مانند ^).
  • کاراکتر b\: رشته مورد نظر باید باید در ابتدا یا انتهای یک کلمه باشد.
  • کاراکتر B\: دقیقا برعکس b\ عمل می کند.
  • کاراکتر d\: نماینده یک عدد است و با حروف کاری ندارد.
  • کاراکتر D\: نماینده هر چیزی (حروف، نماد ها و غیره) است که عدد نباشد (دقیقا برعکس d\).
  • کاراکتر s\: نماینده کاراکتر space است.
  • کاراکتر S\: نماینده هر کاراکتری به غیر از space است.
  • کاراکتر w\: نماینده حروف انگلیسی از a تا z و همچنین اعداد از 0 تا 9 و کاراکتر آندراسکور (علامت _) می باشد.
  • کاراکتر W\: دقیقا برعکس w\ عمل می کند.
  • کاراکترZ\: نماینده کاراکترهای انتهای خط است.

بیایید چند مثال از این کاراکترها را با هم حل کنیم تا بیشتر با این دستورات آشنا شوید:

import re

 

pattern = re.compile(r'[a-h]')


text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result = pattern.search(text)

 

print(result)

من در اینجا گفته ام که بین حروف الفبای a تا h در زبان انگلیسی، کاراکتر را برایمان پیدا کند. نتیجه به شکل زیر خواهد بود:

<re.Match object; span=(4, 5), match='c'>

طبیعتا حرف c بین a تا h قرار دارد. مثال بعدی:

import re

 

pattern = re.compile(r'^roxo')

 

text = "You can learn programming and web development at roxo for free! Go to roxo.ir/plus"

 

result = pattern.search(text)

 

print(result)

علامت ^ بدین معنی است که جستجو باید در ابتدای این رشته باشد بنابراین نتیجه None خواهد بود. در صورتی که الگو را به شکل $roxo^ می نوشتیم یعنی تمام رشته باید roxo باشد و هیچ چیز کمتر یا بیشتری نداشته باشد. من چند الگو و نتیجه اش را برایتان قرار می دهم:

pattern --> \brox

result --> rox

دلیل و توضیحات: به دلیل اینکه b\ به معنی ابتدا یا انتهای کلمه است و brox\ یعنی کلمه ای که ابتدایش rox داشته باشد.

pattern --> \brox\b

result --> None

دلیل و توضیحات: الگوی بالا به دنبال کلمه rox می گردد (قبل و بعدش اسپیس باشد) و چنین کلمه ای در رشته ما وجود ندارد.

pattern --> \w{5}

result --> learn

دلیل و توضیحات: الگوی بالا می گوید علامت _ یا حرف یا عددی که ۵ بار حضور داشته باشد. l و e و a و r و n همگی جزء این دسته حساب می شوند بنابراین ۵ بار تکرار برای w\ برقرار می شود.

pattern --> \W

result --> " "

دلیل و توضیحات: این الگو به دنبال هر کاراکتری به غیر از حروف، اعداد و علامت آندراسکور می گردد. یک اسپیس دقیقا چنین کاراکتری است.

pattern --> (\w){17}|(us\Z)

result --> us

دلیل و توضیحات: این الگو می گوید یا حروف و اعداد و _ که ۱۷ بار تکرار شده باشد را پیدا کن یا قسمتی که آخرش us باشد. ما هیچ کاراکتر حرفی یا عددی یا _ را نداریم که پشت سر هم ۱۷ بار تکرار شده باشد (اسپیس آن ها را قطع می کند و هیچ گاه به ۱۷ تکرار نمی رسند) بنابراین حالت اول برقرار نیست و علامت | (به معنی «یا») ما را به قسمت دوم می برد که می گوید آیا رشته با us تمام می شود یا خیر؟ رشته ما با plus تمام می شود که آخرش us دارد بنابراین شرط برقرار بوده و us برگردانده می شود.

یک مثال بسیار پیشرفته تر، مثال پیدا کردن ایمیل است:

import re

 

pattern = re.compile(r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+')

 

text1 = "info@roxo.ir"

 

text2 = "email@example.com"

 

text3 = "akwudhgku342liahd2@company.org"

 

 

result1 = pattern.search(text1)

result2 = pattern.search(text2)

result3 = pattern.search(text3)

 

print(result1)

print(result2)

print(result3)

با اجرای این کد نتیجه زیر را می گیریم:

<re.Match object; span=(0, 12), match='info@roxo.ir'>

<re.Match object; span=(0, 17), match='email@example.com'>

<re.Match object; span=(0, 30), match='akwudhgku342liahd2@company.org'>

به عبارتی تمام ایمیل هایی که وارد کردیم بر اساس الگوی پاس داده شده صحیح هستند بنابراین پیدا شده اند. اگر به الگو دقت کنید، متوجه پیچیدگی آن خواهید شد. ما در ابتدا یک بازه را مشخص کرده ایم که شامل حروف از a تا z و اعداد 0 تا 9 و علامت های _ و + و . و - نیز می شود. چرا چنین کاری را کرده ایم؟ به دلیل اینکه این علامت ها در ایمیل مجاز است. در قسمت بعد به کل این بازه علامت + را اضافه کرده ایم که یعنی باید یک یا بی نهایت بار تکرار شود. سپس علامت @ را داریم و بعد از آن دامنه سایت می آید که طبیعتا می تواند شامل اعداد، حروف و علامت - (خط فاصله) باشد (مثال web-site.com). در نهایت باید علامت نقطه را داشته باشیم که دامنه سایت را به پسوند نهایی متصل کند اما از آنجایی که نقطه به معنی هر کاراکتری بود باید از علامت \ استفاده کنیم تا نقطه را escape کنیم (یعنی به پایتون بگوییم که منظورمان واقعا نقطه است نه هر کاراکتری).

عبارات باقاعده نیاز به دوره خودشان را دارند و ما نمی توانیم در این دوره آن ها را به طور کامل بررسی کنیم. من دوره ای برای Regex در جاوا اسکریپت را نوشته ام و می توانید آن را در roxo.ir/plus مشاهده کنید. با اینکه این دوره برای زبان جاوا اسکریپت نوشته شده است اما مفاهیم Regex در تمام زبان های برنامه نویسی تقریبا یکی است و می توانید به راحتی آن را یاد بگیرید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.