التعبيرات العادية في بايثون

تعد التعبيرات العادية لغة قوية لمطابقة أنماط النص. تقدم هذه الصفحة مقدمة أساسية عن التعبيرات العادية، وهي كافية لممارسة تمارين بايثون وتوضح كيفية عمل التعبيرات العادية في بايثون. تتيح الوحدة "re" في Python إمكانية التعبير العادي.

في بايثون، عادةً ما تتم كتابة البحث بالتعبير العادي على النحو التالي:

match = re.search(pat, str)

تأخذ الطريقة re.search() نمط تعبير عادي وسلسلة وتبحث عن هذا النمط داخل السلسلة. إذا كان البحث ناجحًا، ستعرض search() كائن مطابقة أو None بخلاف ذلك. ولذلك، عادةً ما يتبع البحث على الفور عبارة if لاختبار ما إذا كان البحث قد نجح، كما هو موضح في المثال التالي الذي يبحث عن النمط 'word:' متبوعة بكلمة مكونة من 3 أحرف (التفاصيل أدناه):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

يُخزن الرمز match = re.search(pat, str) نتيجة البحث في متغير يسمى "match". ثم تختبر العبارة الشرطية المطابقة -- إذا نجح البحث بنجاح وكانت match.group() هي النص المطابق (على سبيل المثال، 'word:cat'). وإلّا إذا كانت المطابقة خاطئة (بدون تحديد أكثر)، فلن ينجح البحث، ولا يوجد نص مطابق.

يعيّن الحرف "r" في بداية سلسلة النمط سلسلة python "raw" تمر عبر الشرطات المائلة للخلف بدون تغيير، وهو أمر مفيد جدًا في التعبيرات العادية (تحتاج Java إلى هذه الميزة بشكل سيئ!). أوصي بأن تكتب دائمًا سلاسل نمطية باستخدام "r" كالعادة.

الأنماط الأساسية

تكمن قوة التعبيرات العادية في أنه يمكنها تحديد أنماط، وليس فقط أحرف ثابتة. إليك أكثر الأنماط الأساسية التي تتطابق مع الأحرف المفردة:

  • a وX و9 و< -- تتطابق الأحرف العادية تمامًا مع نفسها. فالأحرف الوصفية التي لا تتطابق لأن لها معانٍ خاصة هي: . ^ $ * + ? { [ ] \ | ( ) (يُرجى الاطّلاع على التفاصيل أدناه)
  • . (نقطة) -- تتطابق مع أي حرف مفرد باستثناء السطر الجديد '\n'
  • \w -- (الحرف الصغير w) يتطابق مع حرف "الكلمة": حرف أو رقم أو شريط سفلي [a-zA-Z0-9_]. لاحظ أنه على الرغم من أن كلمة "word" هي ذاكرتي لهذا الغرض، إلا أنها تتطابق مع كلمة واحدة حرف فقط، وليس كلمة كاملة. \W (للأحرف الكبيرة W) يتطابق مع أي حرف غير أحرف.
  • \b -- الحدود بين كلمة وغير كلمة
  • \s -- (الأحرف الصغيرة) تتطابق مع حرف مسافة بيضاء واحد -- مسافة، سطر جديد، رجوع، علامة جدولة، نموذج [ \n\r\t\f]. \S (الأحرف الكبيرة S) يتطابق مع أي حرف بدون مسافات بيضاء.
  • \t، \n،\r -- علامة تبويب، سطر جديد، رجوع
  • \d -- الرقم العشري [0-9] (بعض أدوات التعبيرات العادية القديمة لا تتيح استخدام \d، ولكن جميعها تتوافق مع \w و\s)
  • ^ = start, $ = end -- تتطابق مع بداية السلسلة أو نهايتها
  • \ -- تحجب "خاصية" الشخصية. لذلك، على سبيل المثال، يمكنك استخدام \. لمطابقة نقطة أو \\ لمطابقة شرطة مائلة. إذا لم تكن متأكدًا مما إذا كان للحرف معنى خاص، مثل "@"، يمكنك تجربة وضع شرطة مائلة أمامه، \@. وإذا لم يكن تسلسل إلغاء صالحًا، مثل \c، فإن برنامج بايثون سيتوقف مع حدوث خطأ.

أمثلة أساسية

نكتة: ماذا نسمي الخنزير صاحب ثلاثة عيون؟

القواعد الأساسية للبحث باستخدام التعبير العادي عن نمط داخل سلسلة هي:

  • تتم مواصلة البحث خلال السلسلة من البداية إلى النهاية، ويتوقف عند أول تطابق يتم العثور عليه.
  • يجب أن تتم مطابقة كل النمط، ولكن ليس كل السلسلة
  • إذا كانت المطابقة match = re.search(pat, str) ناجحة، فلن تكون المطابقة "None" ("بدون")، ويكون على وجه التحديد match.group() هو النص المطابق
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

التكرار

تصبح الأمور أكثر إثارة عند استخدام + و * لتحديد التكرار في النمط

  • + -- موضع ورود واحد أو أكثر للنمط على يساره، على سبيل المثال. 'i+' = واحد أو أكثر من i
  • * -- 0 أو أكثر من تكرارات النمط على يساره
  • ? -- تطابق 0 أو مرة ورود واحدة للنمط على يساره

أقصى اليسار والأكبر

أولاً، يعثر البحث على أقصى تطابق للنمط، وثانيًا يحاول استخدام أكبر قدر ممكن من السلسلة -- أي يذهب + و * إلى أبعد ما يكون ممكنًا (يُقال إن + و* "جشع").

أمثلة على التكرار

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

مثال على الرسائل الإلكترونية

لنفترض أنك تريد العثور على عنوان البريد الإلكتروني داخل السلسلة "xyz alice-b@google.com قرد أرجواني". سنستخدم هذا المثال كمثال لعرض المزيد من ميزات التعبير العادي. إليك محاولة باستخدام النمط r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

لم يحصل البحث على عنوان البريد الإلكتروني بالكامل في هذه الحالة لأن \w لا يتطابق مع "-" أو "." في العنوان. سنحل هذه المشكلة باستخدام ميزات التعبير العادي أدناه.

الأقواس المربّعة

يمكن استخدام الأقواس المربعة للإشارة إلى مجموعة من الأحرف، ولذلك تتطابق [abc] مع "a" أو "b" أو "c". تعمل الرموز \w و\s وما إلى ذلك داخل أقواس مربعة أيضًا مع الاستثناء الوحيد الذي يشير إلى أن النقطة (.) تعني فقط النقطة الحرفية. بالنسبة لمشكلة رسائل البريد الإلكتروني، تعد الأقواس المربعة طريقة سهلة لإضافة '.' و '-' إلى مجموعة الأحرف التي يمكن أن تظهر حول @ بالنمط r'[\w.-]+@[\w.-]+' للحصول على عنوان البريد الإلكتروني بالكامل:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(المزيد من ميزات الأقواس المربّعة) يمكنك أيضًا استخدام شرطة للإشارة إلى نطاق، وبالتالي تتطابق [a-z] مع جميع الأحرف الصغيرة. لاستخدام شرطة بدون الإشارة إلى نطاق، ضع الشرطة في النهاية، على سبيل المثال [abc-]. الرمز الأعلى (^) في بداية مجموعة القوس المربّع يؤدي إلى عكسها، أي أنّ [^ab] تعني أي حرف باستثناء "a" أو "b".

استخراج المجموعة

تتيح لك ميزة "group" في التعبير العادي اختيار أجزاء من النص المطابق. لنفترض أننا نريد استخراج اسم المستخدم والمضيف بشكل منفصل في ما يتعلق بمشكلة رسائل البريد الإلكتروني. للقيام بذلك، أضف أقواس ( ) حول اسم المستخدم والمضيف في النمط، كالتالي: r'([\w.-]+)@([\w.-]+)'. في هذه الحالة، لا تغير الأقواس ما سيطابقه النمط، وبدلاً من ذلك تنشئ "مجموعات" منطقية داخل نص المطابقة. في عملية بحث ناجحة، يكون match.group(1) هو نص المطابقة المقابل للقوسين الأيسرين الأولين، وmatch.group(2) هو النص المقابل للقوسين الأيسرين الثانيين. لا يزال ملف match.group() العادي نص المطابقة بالكامل كالمعتاد.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

يتمثل سير العمل الشائع مع التعبيرات العادية في كتابة نمط للشيء الذي تبحث عنه، وإضافة مجموعات أقواس لاستخراج الأجزاء التي تريدها.

العثور على

من المحتمل أن تكون findall() هي الدالة الوحيدة الأقوى في الوحدة re. أعلاه استخدمنا re.search() للعثور على أول تطابق لنمط. findall() العثور على *جميع* التطابقات وتعيدها كقائمة من السلاسل، مع كل سلسلة تمثل تطابقًا واحدًا.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

العثور على جميع الملفات باستخدام Files

بالنسبة للملفات، قد تكون معتادًا على كتابة تكرار حلقي للتكرار على سطور الملف، ويمكنك بعد ذلك استدعاء findall() على كل سطر. بدلاً من ذلك، دع findall() تقوم بالتكرار نيابة عنك - أفضل بكثير! ما عليك سوى إدخال نص الملف بأكمله في findall() والسماح له بعرض قائمة بجميع التطابقات في خطوة واحدة (تذكر أن f.read() يعرض النص الكامل للملف في سلسلة واحدة):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

"العثور على" و"مجموعات Google"

يمكن دمج آلية المجموعة الأقواس ( ) مع findall(). إذا كان النمط يتضمن مجموعتين أو أكثر من الأقواس، فبدلاً من عرض قائمة من السلاسل، تعرض findall() قائمة *tuples*. يمثل كل صف تطابقًا واحدًا للنمط، وداخل الصف توجد بيانات المجموعة(1)، المجموعة(2) ... لذلك إذا تمت إضافة مجموعتين من قوسين إلى نمط البريد الإلكتروني، فإن findall() تعرض قائمة بالصفوف، يحتوي كل طول 2 على اسم المستخدم والمضيف، على سبيل المثال ('alice'، 'google.com').

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

بعد الحصول على قائمة الصفوف، يمكنك إجراء تكرار حلقي فوقها لإجراء بعض العمليات الحسابية لكل صف. إذا كان النمط لا يتضمن أقواسًا، فإن findall() تعرض قائمة بالسلاسل التي تم العثور عليها كما في الأمثلة السابقة. إذا كان النمط يتضمن مجموعة واحدة من الأقواس، فإن findall() تعرض قائمة من السلاسل المقابلة لتلك المجموعة الفردية. (ميزة اختيارية غير واضحة: في بعض الأحيان يكون لديك مجموعات أقواس ( ) في النمط، ولكنك لا تريد استخراجها. في هذه الحالة، اكتب الأقواس مع علامة ?: في البداية، على سبيل المثال (?: ) ولن يتم احتساب الأقواس اليسرى تلك كنتيجة جماعية.)

إعادة سير العمل وتصحيح الأخطاء

تحمل أنماط التعبير العادي معنى كبير في عدد قليل من الأحرف فقط ، ولكنها كثيفة للغاية، بحيث يمكنك قضاء الكثير من الوقت في تصحيح أخطاء أنماطك. يمكنك إعداد وقت التشغيل حتى تتمكن من تشغيل نمط وطباعة ما يتطابق معه بسهولة، على سبيل المثال عن طريق تشغيله على نص اختبار صغير وطباعة نتيجة findall(). وإذا لم يتطابق النمط مع أي شيء، فجرب إضعاف النمط، وإزالة أجزاء منه حتى تحصل على عدد كبير جدًا من التطابقات. وعندما تكون القيمة متطابقة مع أي شيء، لا يمكنك إحراز أي تقدم لأنّه لا يوجد شيء ملموس يمكن النظر إليه. وعندما يصبح العدد مطابقًا كثيرًا، يمكنك العمل على زيادة الروابط تدريجيًا للوصول إلى ما تريد.

الخيارات

تستخدم الدوال re خيارات لتعديل سلوك مطابقة النمط. تتم إضافة علامة الخيار كوسيطة إضافية إلى search() أو findall() وما إلى ذلك، على سبيل المثال. re.search(pat, str, re.IGNORECASE).

  • تجاهل -- تجاهل الاختلافات الكبيرة/الصغيرة للمطابقة، لذا فإن "a" تطابق كلاً من "a" و"A".
  • DOTALL -- السماح للنقطة (.) بمطابقة السطر الجديد -- عادةً ما تتطابق مع أي نص غير السطر الجديد. قد يخطئك هذا؛ فقد تعتقد أن .* يتطابق مع كل شيء، لكنه لا يتخطى نهاية السطر افتراضيًا. تجدر الإشارة إلى أنّ \s (المسافة البيضاء) تتضمّن أسطرًا جديدة، وبالتالي إذا أردت مطابقة سلسلة من المسافات البيضاء التي قد تتضمّن سطرًا جديدًا، ما عليك سوى استخدام \s*.
  • متعدّد -- داخل سلسلة مكونة من عدة أسطر، اسمح لـ ^ و $ بمطابقة بداية ونهاية كل سطر. عادةً ما يتطابق ^/$ مع بداية ونهاية السلسلة بأكملها.

طموح مقابل غير طمع (اختياري)

هذا قسم اختياري يعرض أسلوبًا أكثر تقدمًا للتعبير العادي غير اللازم للتمارين.

لنفترض أن لديك نصًّا يحتوي على علامات: <b>foo</b> و <i>وما إلى ذلك</i>

لنفترض أنك تحاول مطابقة كل علامة بالنمط '(<.*>)' -- فما الذي تطابقه أولاً؟

والنتيجة غير مفاجئة بعض الشيء، إلا أن الجانب الجشع في نطاق .* يجعله يتطابق مع "<b>foo</b>" بالكامل و<i>وما إلى ذلك</i>" باعتباره مطابقة كبيرة. المشكلة هي أنّ .* يصل إلى أبعد ما يمكن، بدلاً من التوقّف عند أول حرف > (يُعرف أيضًا باسم "جشع").

هناك إضافة إلى التعبير العادي حيث تضيف علامة ؟ في النهاية، مثل .*? أو .+?، وتغييرها لتصبح غير طماعة. الآن تتوقف في أقرب وقت ممكن. وبالتالي فإن النمط '(<.*?>)' سيحصل على '<b>' كأول مطابقة، و '</b>' كمطابقة ثانية، وهكذا الحصول على كل زوج <..> بالترتيب. عادةً ما يكون النمط هو أنك تستخدم .*? متبوعة على الفور ببعض العلامات الملموسة (> في هذه الحالة) التي يتم فرض تمديد تشغيل .*? عليها.

نشأت الإضافة *? باللغة Perl، وتُعرف التعبيرات العادية التي تشمل إضافات Perl باسم التعبيرات العادية المتوافقة مع Perl؛ pcre. تتضمن بايثون دعم pcre. تحتوي العديد من أدوات سطر الأوامر وما إلى ذلك على علامة تقبل أنماط pcre.

هناك أسلوب قديم ولكنه يُستخدم على نطاق واسع لترميز هذه الفكرة حول "كل هذه الأحرف باستثناء التوقُّف عند X"، وهو يستخدم نمط القوس المربع. بالنسبة لما سبق، يمكنك كتابة النمط، ولكن بدلاً من .* للحصول على جميع الأحرف، استخدم [^>]* التي تتخطى جميع الأحرف غير > (يؤدي ^ البادئة إلى "عكس" مجموعة القوس المربع، لذا فهو يطابق أي حرف ليس بين القوسين).

الاستبدال (اختياري)

تبحث الدالة re.sub(pat, alternate, str) عن جميع مثيلات النمط في السلسلة المحددة، وتستبدلها. يمكن أن تحتوي سلسلة الاستبدال على "\1" و'\2' التي تشير إلى النص من group(1) وgroup(2) وما إلى ذلك من النص المطابق الأصلي.

وفي ما يلي مثال يبحث عن جميع عناوين البريد الإلكتروني ويغيرها للاحتفاظ بالمستخدم (\1) ولكن مع استخدام yo-yo-dyne.com كمضيف.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

التمارين الرياضية

لممارسة التعبيرات العادية، اطلع على تمرين أسماء الأطفال.