التعبيرات العادية هي لغة فعّالة لمطابقة أنماط النص. تقدم هذه الصفحة مقدمة أساسية للتعبيرات العادية نفسها بما يكفي لتمارين بايثون وتوضح كيفية عمل التعبيرات العادية في بايثون. لغة بايثون "re" دعم التعبير العادي.
في بايثون، تتم كتابة البحث بالتعبير العادي عادةً على النحو التالي:
match = re.search(pat, str)
تستخدم الطريقة re.search() نمط تعبير عادي وسلسلة وتبحث عن هذا النمط داخل السلسلة. إذا كان البحث ناجحًا، فإن search() تعرض كائن مطابقة أو لا شيء بخلاف ذلك. ولذلك، عادةً ما يتبع البحث على الفور عبارة 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". بعد ذلك تختبر العبارة if المطابقة -- إذا نجح البحث، نجح البحث وmatch.group() هو النص المطابق (على سبيل المثال 'word:cat'). وبخلاف ذلك، إذا كانت المطابقة خاطئة (لا يوجد شيء أكثر تحديدًا)، يعني ذلك أن البحث لم ينجح، وليس هناك نص مطابق.
الحرف 'r' في بداية سلسلة النمط يعين بايثون "raw" سلسلة تمر عبر شرطات مائلة للخلف دون تغيير، وهو أمر مفيد للغاية للتعبيرات العادية (تحتاج Java إلى هذه الميزة بشكل سيئ!). أوصي بأن تكتب دائمًا سلاسل أنماط باستخدام "r" تمامًا.
أنماط أساسية
تكمن قوة التعبيرات العادية في أنها يمكنها تحديد أنماط، وليس مجرد أحرف ثابتة. في ما يلي الأنماط الأساسية التي تتطابق مع أحرف مفردة:
- a، X، 9، < -- تتطابق الأحرف العادية تمامًا مع نفسها. لا تطابق الأحرف الوصفية نفسها نظرًا لأن لها معانٍ خاصة: . ^ $ * + ? { [ ] \ | ( ) (يُرجى الاطّلاع على التفاصيل أدناه)
- . (نقطة) -- يتطابق مع أي حرف مفرد ما عدا السطر الجديد '\n'
- \w -- (حرف w صغير) يتطابق مع "كلمة" الحرف: حرف أو رقم أو شريط سفلي [a-zA-Z0-9_]. لاحظ أنه على الرغم من أن "word" هو ذاكرة للذاكرة، فهو يتطابق فقط مع كلمة واحدة، وليس كلمة كاملة. \W (حرف كبير W) يتطابق مع أي حرف ليس من الكلمات.
- \b -- حدود بين الكلمة والأخرى
- \s -- (الأحرف الصغيرة s) يتطابق مع حرف مسافة بيضاء مفرد -- مسافة، سطر جديد، رجوع، علامة تبويب، نموذج [ \n\r\t\f]. \S (حرف S كبير) يطابق أي حرف بدون مسافات بيضاء.
- \t، \n، \r -- علامة تبويب، سطر جديد، رجوع
- \d -- الرقم العشري [0-9] (لا تتوافق بعض أدوات التعبير العادي القديمة مع \d، ولكن جميعها تتيح \w و\s)
- ^ = start، $ = end -- تطابق بداية السلسلة أو نهايتها
- \ -- يمنع "التخصص" إحدى الشخصيات. لذلك، على سبيل المثال، استخدم \. لمطابقة نقطة أو \\ لمطابقة شرطة مائلة. إذا لم تكن متأكدًا مما إذا كان الحرف له معنى خاص، مثل "@"، فيمكنك تجربة وضع شرطة مائلة أمامه، مثل "@". إذا لم يكن تسلسل إلغاء صالح، مثل \c، سيتوقف برنامج python مع ظهور خطأ.
أمثلة أساسية
نكتة: ماذا تسمّي "الخنزير" بثلاثة عيون؟ خنزير
القواعد الأساسية للبحث باستخدام التعبير العادي عن نمط داخل سلسلة هي:
- يستمر البحث عبر السلسلة من البداية إلى النهاية، ويتوقف عند أول تطابق يتم العثور عليه
- يجب مطابقة كل الأنماط، ولكن لا يجب مطابقة السلسلة
- إذا نجحت
match = re.search(pat, str)
، لا تكون المطابقة "بلا"، ولا سيما 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 أو 1 من مرات ظهور النمط على يساره
في أقصى اليسار الأكبر
أولاً، يعثر البحث على أقصى تطابق للنمط، وثانيًا يحاول استخدام أكبر قدر ممكن من السلسلة -- بمعنى أن + و * أبعد ما يمكن (تسمى + و * أنهما "طبيعي").
أمثلة على التكرار
## 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 chocolate monkey". سنستخدم هذا كمثال قيد التشغيل لتوضيح المزيد من ميزات التعبير العادي. إليك محاولة استخدام النمط 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'
استخراج المجموعة
"المجموعة" خاصية التعبير العادي بانتقاء أجزاء من النص المطابق. لنفترض بالنسبة إلى مشكلة الرسائل الإلكترونية التي نريد استخراج اسم المستخدم والمضيف بشكل منفصل. للقيام بذلك، أضف أقواسًا ( ) حول اسم المستخدم والمضيف في النمط، كما يلي: 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.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())
العثور على الكل والمجموعات
يمكن دمج آلية مجموعة الأقواس ( ) مع findall(). إذا كان النمط يشتمل على مجموعتين أو أكثر من الأقواس، فبدلاً من عرض قائمة من السلاسل، تعرض findall() قائمة *الصفوف*. يمثل كل صف تطابقًا واحدًا للنمط، وداخل الصف توجد بيانات المجموعة(1)، المجموعة(2) ... لذلك إذا تمت إضافة مجموعتين مع قوسين إلى نمط البريد الإلكتروني، فإن findall() تعرض قائمة بالصفوف، ويحتوي كل طول منها 2 على اسم المستخدم والمضيف، على سبيل المثال. ("أليس"، "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(). إذا لم يتطابق النمط مع أي شيء، فحاول إضعاف النمط، وإزالة أجزاء منه حتى تحصل على عدد كبير جدًا من التطابقات. وعندما لا يتطابق النص مع أي شيء، لا يمكنك إحراز أي تقدم بسبب عدم وجود شيء ملموس للنظر فيه. وبعد إضافة تطابق كبير، يمكنك تضييقه بشكل تدريجي لتحقيق ما تريد.
الخيارات
تستخدم الدوال إعادة خيارات لتعديل سلوك مطابقة النمط. تتم إضافة علامة الخيار كوسيطة إضافية إلى search() أو findall() وما إلى ذلك، على سبيل المثال re.search(pat, str, re.IGNORECASE).
- IGNORECASE -- تجاهل الاختلافات الكبيرة/الصغيرة للمطابقة، بحيث يكون "a" تتطابق مع كل من 'a' و"A".
- DOTALL -- اسمح للنقطة (.) بأن تتطابق مع السطر الجديد -- وعادةً ما تتطابق مع أي شيء ما عدا السطر الجديد. وقد يؤدي هذا إلى توقفك -- تعتقد أن ** يطابق كل شيء، ولكن افتراضيًا لا يتجاوز نهاية السطر. لاحظ أن \s (المسافة البيضاء) يتضمن أسطرًا جديدة، وبالتالي إذا أردت مطابقة مجموعة مسافات بيضاء قد تتضمن سطرًا جديدًا، يمكنك استخدام \s* فقط
- MULTILINE -- ضمن سلسلة مكونة من عدة أسطر، اسمح للرمزين ^ و $ بمطابقة بداية ونهاية كل سطر. عادةً ما تطابق ^/$ فقط بداية ونهاية السلسلة بأكملها.
طمع مقابل غير طمع (اختياري)
هذا القسم اختياري يعرض أسلوب تعبير عادي أكثر تقدّمًا غير ضروري للتمارين.
لنفرض أنّ لديك نصًا يحتوي على علامات: <b>foo</b> و<i>وما إلى ذلك</i>
لنفترض أنك تحاول مطابقة كل علامة مع النمط '(<.*>)' - ما الذي يتطابق معه أولاً؟
قد تبدو النتيجة مفاجئة بعض الشيء، لكن الجانب الجشع لـ ** يجعله يتطابق مع "<b>foo</b>" بالكامل و<i>وما إلى ذلك</i>' كمطابقة كبيرة. المشكلة هي أن ** يذهب إلى أبعد ما يمكن، بدلاً من التوقف عند أول > (ما يُعرف أيضًا باسم "طمع").
هناك إضافة إلى التعبير العادي حيث تضيف علامة ؟ في النهاية، مثل *؟ أو .+?، لتغييرها لتصبح غير طموحة. والآن يتوقفون في أقرب وقت ممكن. لذا فإن النمط '(<.*?>)' ستحصل على '<b>' فقط كأول تطابق، و"</b>" كمطابقة ثانية، وهكذا في الحصول على كل <..> الزوج تباعًا. عادةً ما يستخدم النمط .*? تليها على الفور بعض العلامات الملموسة (> في هذه الحالة) التي ينتمي إليها .*? يضطر التشغيل إلى تمديد.
تشير علامة *? التي نشأت في Perl، والتعبيرات العادية التي تتضمن إضافات Perl تعرف باسم تعبيرات Perl العادية المتوافقة -- pcre. تتضمن بايثون دعم pcre. تحتوي العديد من برامج سطر الأوامر utils وغيرها على علامة تقبل فيها أنماط pcre.
تقنية قديمة ولكنها مستخدَمة على نطاق واسع لترميز فكرة "كل هذه الأحرف باستثناء التوقف عند X" نمط الأقواس المربعة. بالنسبة لما سبق، يمكنك كتابة النمط، ولكن بدلاً من .* للحصول على جميع الأحرف، استخدم [^>]* التي تتخطى جميع الأحرف غير > (يؤدي ^ البادئة إلى "قلب" مجموعة القوس المربع، بحيث يطابق أي حرف ليس بين القوسين).
الاستبدال (اختياري)
تبحث الدالة re.sub(pat, replace, str) عن جميع مثيلات النمط في السلسلة المحددة، وتستبدلها. يمكن أن تتضمن سلسلة الاستبدال '\1' و'\2' التي تشير إلى النص من المجموعة(1) والمجموعة(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
تمارين
للتدرّب على التعبيرات العادية، يمكنك الاطّلاع على ممارسة أسماء الأطفال.