تقييم الموضوع :
  • 1 أصوات - بمعدل 5
  • 1
  • 2
  • 3
  • 4
  • 5
التعابير المنتظمة في بيثون والوحدة النمطية re
#1
التعابير المنتظمة في بيثون والوحدة النمطية re



## ملاحظات مهمة قبل مطالعة الموضوع: 
  • الحروف الابجدية الرقمية بالأنجليزية alphanumeric characters هي في بيثون3 جميع الحروف والارقام بترميز utf-8 وبالتالي الحروف العربية والارقام الهندية
  • نظرا لأن النص العربي يُكتب من اليمين الى اليسار، فمن الصعوبة بمكان تنسيق نص لاتيني داخل النص العربي اذا احتوى على رموز مشتركة، لاحظ مثلا `\t` يفترض به أن يكون منسقا من اليسار الى اليمين أي أن يظهر الرمز \ على اليسار وعلى يمينه t، فالرجاء أخذ الحيطة عند تجربة أكواد هذا الموضوع.
  • الأكواد الموجودة في هذا الدليل يجب أن تُنفّذ مرتبة من أول الدليل الى نهايته وخاصة سطر استيراد المكتبة (import re) وسطور تعريف الدالة nice التي تمت الاستعانة بها عديد المرات.
كود :
'ابتثجحخدذرزسشصضطظعغفقكلمنهـوي'.isalnum()
'٠١٢٣٤٥٦٧٨٩'.isalnum()
التعبير المنتظم هو كائن رياضي يقوم بتوصيف نص له خاصيات مشتركة. على سبيل المثال، قد تستخدم طرفية الوندوز وتكتب 
كود :
dir *.txt

أو تستخدم ls *.txt في بيئة لينكس وماك، أنت هنا استخدمت تعبيرا منتظما تقصد به جميع الملفات التي تنتهي أسماؤها بـ .txt نقول أن التعبير المنتظم يقوم بعملية غربلة/مطابقة جميع السلاسل النصية المنتهية بـ .txt (العبارة الأنجليزية المستخدمة هي pattern matching).


كانت لغة بيرل هي السباقة في انتشار استخدام التعابير المنتظمة عبر دعمها ضمن اللغة نفسها وليس عبر مكتبات.  

في بيثون، تتوفر التعابير المنتظمة عبر الوحدة النمطية re في المكتبة الاساسية والتي سنطّلع عليها في هذا الدليل. 

في مثالنا السابق، يعتبر التعبير المنتظم *.txt في غاية البساطة. تتيح لنا الوحدة re سبلا لبناء تعابير منتظمة متقدمة وأكثر قوة مما تتيحه الأداة dir او ls. لهذا سنجد أن البناء اللغوي regexps للوحدة re مختلف قليلا. على سبيل المثال لغربلة/مطابقة نفس السلاسل النصية كما في *.txt عبر الوحدة re سيتوجب علينا كتابة التعبير المنتظم بشكل مختلف بعض الشيء. 

قبل استخدام هذه الوحدة النمطية، لابد من استيرادها (من المستحسن جدا أن تضع بين يديك دليل استخدامها وأنت تطالع هذا الموضوع: https://docs.python.org/3/library/re.html )  

كود :
import re


## مثال بسيط findall  

سنستخدم لهذا الغرض قائمة جمل كيفما اتفق (نظر في https://ar.lipsum.com/ ): 
كود :
sentences = ['Lacus a donec, vitae gravida proin sociis.',
            'Neque ipsum! rhoncus cras quam.']
يمكننا مثلا البحث عن جميع الكلمات التي تنتهي بالحرف a او الحرف m بواسطة الدالة re.findall 
كود :
for sentence in sentences:
   print(f"---- in <<{sentence}>>")
   print(re.findall(r"\w*[am]\W", sentence))


هذا الكود يتيح لنا البحث عن جميع الحالات (findall) التي ينطبق عليها التعبير المنتظم الذي استخدمناه وهو هنا: 
كود :
r"\w*[am]\W"


فلنحاول فهم هذا التعبير جزءا جزءا: 
  • الجزء `\w*` نقصد به أننا نرغب في البحث عن سلسلة فرعية تبدأ بعدد غير محدد(*) من الحروف الأبجدية الرقمية \w، بما في ذلك nul (الرموز الابجدية الرقمية = alphanumeric).
  • الجزء `[am]` نقصد به: مباشرة بعد ذلك، يجب علينا العثور على أحد الحرفين a او m؛
  • الجزء `\W` نقصد به أننا نبحث عن رمز **لا يكون** ابجدي رقمي (alphanumeric). هذا مهم بما أننا نبحث عن كلمات **تنتهي بـa او m**. اذا لم نفعل سنحصل على نتيجة مغايرة:
كود :
# the final \W is important
# this is what we get if we omit it
for sentence in sentences:
   print(f"---- in <<{sentence}>>")
   print(re.findall(r"\w*[am]", sentence))


## مثال ثان split  

طريقة ثانية تمكننا من استخدام regexps عبر الدالة re.split والتي تقدم لنا خدمة قريبة من str.split، لكن باستخدام تعابير منتظمة كفواصل: 
كود :
for sentence in sentences:
   print(f"---- in <<{sentence}>>")
   print(re.split(r"\W+", sentence))
   print()


استخدمنا التعبير المنتظم `\W+`في مثالنا كفاصلة والمقصود به هو كل سلسلة تتكون من رمز واحد على الاقل من الرموز غير الابجدية الرقمية (علامة تنقيط، المسافة، العودة للسطر، علامة التبويب...). لدينا إذا طريقة سهلة وأكثر كفاءة من str.split لتجزئة نص الى كلمات.  

## مثال ثالث sub  

تتيح لنا الأداة re.sub أن نعوض جميع الحالات التي ينطبق عليها التعبير المنظم :  

كود :
for sentence in sentences:
   print(f"---- in <<{sentence}>>")
   print(re.sub(r"(\w+)", r"X\1Y", sentence))
   print()


في مثالنا هذا، يحتوي التعبير المنتظم الاول `(\w+)` أي ما نريد تعويضه، على مجموعة (القوسان حول التعبير)، أما وسيط الدالة الثاني فهو أيضا تعبير منتظم (النص الذي سيعوض ما بحثنا عنه) وفيه **مؤشر** نحو نتيجة التعبير المنتظم الاول `\1` وهو ما معناه المجموعة الاولى. 

ما سنحصل في النهاية هو احاطة كل مجموعة من الحروف الابجدية الرقمية (alphanumeric) بالحرفين X و Y.  

## لماذا استخدمنا الـraw-string ؟  

لعلك لاحظت الحر r في الامثلة السابقة والذي يشير الى أننا مررنا التعبير المنتظم باستخدام raw-string . في الواقع لا شيء يمنع من استخدام سلسلة نصية بدونها، ولا باس هنا من التذكير أنه لا يوجد فرق في طبيعة string و raw-string، واليك الدليل: 
كود :
raw = r'abc'
regular = 'abc'
# uبما أننا اخترنا سلسلة نصية قصيرة فهما نفس الكائن
print(f"both compared with is → {raw is regular}")
# uوبالتالي يمكننا المقارنة
print(f"both compared with == → {raw == regular}")


لكن، لو لاحظت جيدا، فالرمز `\` كثير الاستخدام في التعابير المنتظمة، ولعلك تعرف أن نفس هذا الرمز يُستخدم أيضا كرمز افلات/خلوص (escape caracter) مثلا الرمز `\t` يتم تفسيره على أنه تبويب، والرمز `\n` هو رمز نهاية السطر، الخ... فلو استخدمنا سلسلة نصية عادية string لكان يتوجب علينا كتابة هذا الرمز مرتين كلما أردنا عدم تفسير ما يأتي بعده. لهذا السبب تصبح للـraw-string فائدة كبيرة ولهذا السبب ايضا يتم استخدامها بكثرة في التعابير المنتظمة.  


## مثال رابع match  

نمر الآن الى التعرف على كيفية التأكد مسبقا من تطابق سلسلة نصية مع المعايير المحددة في التعبير المنتظم واستخراج العناصر التي تتطابق مع مختلف أجزاء التعبير. سنستخدم من أجل مثالنا هذا قائمة سلاسل نصية جديدة تعمدنا تركيبها من 5 أجزاء: سلسة من الارقام ثم سلسلة من الحروف ثم سلسة من الارقام ثم سلسلة من الحروف وأخيرا سلسلة من الارقام.  

كود :
samples = ['890hj000nnm890',    # هذه تتطابق
          '123abc456def789',   # هذه ايضا تتطابق
          '8090abababab879',   # أما هذه فلا تتطابق
         ]


في البداية لنر كيف أنه يمكننا بسهولة التأكد من توافق سلسلة نصية مع شروطنا:  

كود :
regexp1 = "[0-9]+[A-Za-z]+[0-9]+[A-Za-z]+[0-9]+"
for sample in samples:
   match = re.match(regexp1, sample)
   print(f"{sample:16s} → {match}")


سنحاول جعل نتيجة هذا الكود أكثر وضوحا عبر استخدام دالة: 

للتوضيح نتيجة تنفيذ re.match إما None او الكائن match 

كود :
def nice(match):
   return "no" if match is None else "Match!"
والآن نحاول مرة أخرى مع الاستعانة بالدالة التي كتبناها  

كود :
print(f"REGEXP={regexp1}\n")
for sample in samples:
   match = re.match(regexp1, sample)
   print(f"{sample:>16s} → {nice(match)}")


لاحظ أننا هنا وعوض استخدام تعابير مختصرة كـ`\w` فضلنا كتابة جميع الرموز التي نبحث عنها في متغير regexp1. الهدف هو تحديد الرموز لأن المحارف الابجدية الرقمية في بيثون تتضمن جميع المحارف بترميز يونيكود كما ذكرنا أعلاه، أي أكثر بكثير من الحروف الانجليزية والارقام العربية. ولتفسير التعبير المنتظم، يمكنك أن تلاحظ أنه يتكون من جزئين متكررين: 
  • الارقام من 0 الى 9 رمزنا اليها بـ`[0-9]+`
  • الحروف الانجليزية الصغير والكبيرة رمزنا اليها بـ`[A-Za-z]+`
ثم كتبنا اجزاء التعبير بالترتيب الصحيح لنحصل على التعبير المنتظم الذي نريده (ارقام ثم حروف ثم ارقام ثم حروف ثم ارقام) 

## تسمية قطعة من التعبير (مجموعة)  

سنركز عملنا على مدخلة متوافقة مع شروطنا. 
كود :
haystack = samples[1]
haystack


يمكننا اعطاء اسم لجزء من تعبيرنا المنتظم : هنا سنسمي مجموعة الارقام الاولى group1 ومجموعة الارقام الثانية group2 ومجموعة الارقام الثالثة group3 
كود :
regexp2 = "(?P<group1>[0-9]+)[A-Za-z]+(?P<group2>[0-9]+)[A-Za-z]+(?P<group3>[0-9]+)"
print("group1 =", re.match(regexp2, haystack).group('group1'))
print("group2 =", re.match(regexp2, haystack).group('group2'))
print("group3 =", re.match(regexp2, haystack).group('group3'))


يبدو البناء اللغوي لتعبيرنا الذي قمنا بتعيينه للمتغير regexp2 معقدا على غير عادة بيثون وربما هو مستورد كما هو من لغة بيرل.  

  • استخدمنا القوسين () لتعريف كل مجموعة
  • اعتمدنا `?P<group1>` للاشارة أننا نريد تسمية تلك المجموعة باسم group1. لاحظ صعوبة تنسيق التعبير داخل النص العربي ولاحظ غموضه الراجع الى لغة بيرل على الارجح

## شروط إضافية  

يمكننا اضافة شرط آخر في تعبيرنا المنتظم بحيث لا يعطينا سوى نتيجة سبق لها أن ظهرت في السلسلة النصية. سنعود الى أمثلتنا السابقة ونضيف شرطا آخر يتمثل في وجوب تطابق group1 مع group3 (الذي عوضناه بـ `(?P=group1)` )  

كود :
regexp3 = "(?P<group1>[0-9]+)[A-Za-z]+(?P<group2>[0-9]+)[A-Za-z]+(?P=group1)"
print(f"REGEXP={regexp3}\n")
for sample in samples:
   match = re.match(regexp3, sample)
   print(f"{sample:>16s} → {nice(match)}")


اذا نفذت السطور سترى أننا لن نحصل على تطابق (match) سوى في السلسلة النصية الاولى.  

# كيف نستخدم المكتبة  

سنتحدث قليلا عن دليل استخدام المكتبة re قبل التدرب على كتابة التعابير المنتظمة.  

## دوال تسهيل العمل ورفع الكفاءة  

يمكن ترجمة تعبير منتظم كالذي استخدمناه سابقا `\w*[am]\W`في متحكم آلي يقوم بالمطابقة الآلية مع سلسلة نصية، مما يوفر تيسيرا وكفاءة أكبر لأن السكربت سيقوم بترجمة التعبير مرة واحدة ثم نستطيع استخدام الكائن الناتج عدد المرات التي نريد.  
  • تصبح هذه الطريقة ضرورية عندما نستخدم نفس التعبير بشكل متكرر عددا كبيرا من المرات، وهي تتمثل في: 
  • ترجمة التعبير **مرة واحدة** في متحكم آلي (automate) في شكل كائن باستخدام re.compile
ثم استخدام الكائن  على السلاسل النصية المراد معالجتها

استخدمنا في الأمثلة السابقة (وسنواصل مع نفس الامثلة لتبسيط الشرح)، بعضها قد يكون مناسبا عندما نعمل في وضع تفاعلي (من خلال طرفية بيثون) لكنها قد لا تناسب في جميع الحالات. استخدمنا دوال مساعدة وهي تعمل كلها بنفس المبدإ: 
`re.match(regexp, sample) ⟺⟺ re.compile(regexp).match(sample)`  

هذا المبدأ يتمثل في ترجمة التعبير في متحكم آلي كلما كنا بحاجة لاستخدامه أكثر من مرة.  
كود :
# uفي هذا المثال لم نستخدم متحكم آلي :

# uتصور لو كان لدينا 10**5 من السلاسل النصية لمعالجتها
for sample in samples:
   match = re.match(regexp3, sample)
   print(f"{sample:>16s} → {nice(match)}")

كود :
# uفي كود ذي كفاءة :
# uنترجم التعبير المنتظم في متحكم آلي مرة واحدة
re_obj3 = re.compile(regexp3)

# uثم نستخدمه في معالجاتنا اللاحقة
for sample in samples:
   match = re_obj3.match(sample)
   print(f"{sample:>16s} → {nice(match)}")


كما ترى، لم نقم بترجمة التعبير الامرة واحدة في مثالنا الأخير وهذا يوفر كفاءة أعلى للكود ككل. 

## وظائف RegexObject  

يمكن اختصار الوظائف الأكثر فائدة فيما يلي: 
  • الوظيفتان match و search واللتان تبحثان عن مطابقة سواء في أول السلسلة (match) أو في أي مكان منها (search)
  • الوظيفتان findall و split واللتان تبحثان عن جميع التطابقات (findall) أو عكس ذلك (split)
  • الوظيفة sub التي تقوم **بتعويض** سلسلة بأخرى حسب نمط مطابقة (كان يمكن تسميتها باسم أقرب لوظيفتها...)
## استثمار النتائج  

يمكن مراجعة الوظائف المتوفرة في الكلاس re.MathObject من الرابط التالي: https://docs.python.org/3/library/re.html#match-objects 

سبق لنا أن تعرضنا الى البعض منها، وهذا مسح سريع لها:  
  • * الوظيفتان re و string لاستعراض / استخراج بيانات المطابقة 
كود :
sample = "    Isaac Newton, physicist"
match = re.search(r"(\w+) (?P<name>\w+)", sample)
match.string    # استعراض / استخراج نتيجة المطابقة
match.re        # استعراض / استخراج نص او كود المطابقة
  • * الوظائف group  و  groups و groupdict لايجاد أجزاء السلسلة المدخلة والتي تمثل مجموعات التعبير المنتظم. يمكننا الوصول اليها حسب ترتيبها أو حسب اسمها (راجع الامثلة السابقة) 
كود :
match.groups()
match.group(1)
match.group('name')
match.group(2)
match.groupdict()

نلاحظ أن الترتيب يبدأ من 1 وليس من الصفر. لكن يمكننا الوصول الى المجموعة صفر باعتبارها الجزء من السلسلة النصية الاصلية الذي تمت غربلته/مطابقته بالتعبير المنتظم (والذي يمكن أن يكون موجودا في وسط السلسلة الاصلية) 
كود :
match.group()
  • * الوظيفة expand تتيح نوعا من التنسيق بين قيم المجموعات (ما يشبه str.format) 
كود :
match.expand(r"last_name \g<name> first_name \1")
5
  • * الوظيفة span للتعرف على مؤشري (indexes) مجموعة محددة في السلسلة النصية المدخلة 
كود :
begin, end = match.span('name')
sample[begin:end]


## الرايات flags  

نلاحظ أنه يمكن تمرير رايات (flags) لـ re.compile، هذه الرايات تعدل بشكل شامل تفسير السلسلة النصية مما يمكن أن يكون مفيدا. يمكنك مراجعة هذه الرايات من الرابط التالي: https://docs.python.org/3/library/re.htm...e-contents  

ستلاحظ أن لديها بصفة عامة اسماء واضحة المعنى واختصار من حرف واحد. نستعرض هنا بعضها مما يمكن أن يكون أكثر أهمية: 
  • الراية IGNORECASE (واختصارها I) لعدم التفريق بين الحروف الكبيرة والحروف الصغيرة؛
  • الراية ASCII (واختصارها A) تحدد استخدام `\w` على الرموز `[a-zA-Z0-9_]`
  • الراية LOCALE (واختصارها L) : هنا `\w` مرتبطة بالاعدادات المحلية مع التنبيه أن هذه الراية لا تعمل إلاّ على الترميز 8 بت، أي أنها مهملة.
  • الراية MULTILINE (واختصارها M) تتيح معالجة `^` و `$` كبداية السطر ونهايته في سلسلة نصية تحتوي على عدة سطور
  • الراية DOTALL (واختصارها S) تعتبر النقطة مثلا (.) نهاية سطر (افتراضيا يمكن أن يكون أي رمز ما عدا رمز نهاية السطر)
إذا أردنا استخدام عدة رايات، يتوجب علينا استخدام الرمز `|` (or) قبل تمرير الرايات الى re.compile، وهذا مثال على ذلك: 
كود :
regexp = "a*b+"
re_obj = re.compile(regexp, flags=re.IGNORECASE | re.DEBUG)
print(regexp, "->", nice(re_obj.match("AabB")))


## بناء تعبير منتظم  

سنحاول التدرب على بناء تعابير منتظمة مع التبسيط قدر الامكان، ويمكن لمن أراد المزيد من التوسع مراجعة دليل المكتبة الذي نعيد وضع رابطه: https://docs.python.org/3/library/re.html

القالب الأساسي: الحرف 

في البداية يجب تحديد محارف: 

1- حرف **واحد**
  • تذكره كما هو مع استخدام `\` إذا كان عنده معنى خاص في التعابير المنتظمة (مثل + و * و [ وغيرها)؛
2- الجوكير (wildcard)
  • النقطة (.) تعني أي رمز مهما كان؛
3- مجموعة رموز باستخدام `[...]` الذي يمكن أن يعني مثلا:
  • أحد الرموز المنفصلة a أو 1 أو = نعبر عنها  `[a1=]`
  • الحروف بين a و z نعبر عنها `[a-z]`
  • خليط مما سبق `[15d-g]` أي 1 أو 5 أو d أو e أو f أو g
  • عكس ما سبق `[^15d-g]` اي أي رمز عدا الرموز المذكورة: لاحظ أن الرمز `^` موضوع كأول رمز داخل `[]` ليعني العكس او الضد
4- مجموعات محددة سلفا من طرف لغة regexps من بينها:
  • مجموعة الرموز الأبجدية الرقمية `\w` (الحرف الصغير)، وبقية الرموز `\W` (الحرف الكبير)
  • الرموز غير المرئية `\s` (الحرف الصغير) وبقية الرموز `\S` (الحرف الكبير)
  • الأرقام `\d` والبقية `\D`
كود :
sample = "abcd"

for regexp in ['abcd', 'ab[cd][cd]', 'ab[a-z]d', r'abc.', r'abc\.']:
   match = re.match(regexp, sample)
   print(f"{sample} / {regexp:<10s} → {nice(match)}")

وهذا مثال آخر فيه رمز ذي دلالة في التعابير المنتظمة، ولذلك يتوجب استخدام `\` وبالطبع يفترض أن تكون هناك نقطة أو اكثر في السلسلة الاصلية:  


كود :
sample = "abc."

print(nice(re.match (r"abc\.", "abc.")))

## بالتسلسل أم بالتوازي؟  

لو قمنا بمقارنة مع تركيب الدارات الكهربائية، سنقول أننا الى حد الآن استخدمنا التركيب المتسلسل. نكتب التعابير المنتظمة بالتتالي لكي تقوم بمطابقة (match) السلسلة التي تم ادخالها بالتعاقب من البداية الى النهاية. لدينا حيز لكنه ضيق لتحديد بدائل عندما نكتب مثلا: `"ab[cd]ef"`، لكن هذا محدود بحرف واحد. فلو رغبنا في تحديد كلمتين لا تتشابهان كثيرا مثل abc او def، يتوجب علينا كتابة تعبيرين متوازيين عبر استخدام المعامل `|` كما يلي:  

كود :
regexp = "abc|def"

for sample in ['abc', 'def', 'aef']:
   match = re.match(regexp, sample)
   print(f"{sample} / {regexp} → {nice(match)}")


## آخر السلسلة النصية  

يمكنك أن تحدد إن كنت تهتم فقط بمطابقة (match) في أول السلسلة أو في أي مكان منها (search)، لكن وبشكل مستقل عن هذا، قد يكون مهما أن "نلصق" التعبير المنتظم في أول أو آخر السطر، ولهذا الغرض يوجد رموز خاصة: 
  • عندما نستخدم `^` خارج الـ`[]` ، فهذا يعني بداية السطر. تذكر أن الرمز `^` إذا كان موضوعا كأول رمز داخل `[]` فهو يفيد العكس او الضد
  • الرمز `\A` يعادل الرمز السابق أي يفيد بداية السطر **ما عدا** في حالة السطور المتعددة، فهو يعني عندها بداية السلسلة كلها.
  • الرمز `$` يفيد نهاية السطر
  • الرمز `\Z` يفيد أيضا نهاية السطر **ما عدا** في حالة السطور المتعددة، فهو يعني عندها نهاية السلسلة كلها.

انتبه عند استخدام `^` فهو من ناحية متعدد المعاني، ومن ناحية أخرى هو أحد رموز ASCII وليس رمزا من الرموز المشابهة له. 

كود :
sample = 'abcd'

for regexp in [r'bc', r'\Aabc', r'^abc',
              r'\Abc', r'^bc', r'bcd\Z',
              r'bcd$', r'bc\Z', r'bc$' ]:
   match = re.match(regexp, sample)
   search = re.search(regexp, sample)
   print(f"{sample} / {regexp:5s} match → {nice(match):6s} search → {nice(search)}")

## استخدام القوسين () للتجميع  

التدرب على وضع القوسين بشكل صحيح يمكن من بناء تعابير منتظمة أكثر تطورا. هذا مثال لتعبير نريد بواسطته مطابقة سلسلة تبدأ بالحرف a ثم في الوسط bc او de ثم في نهاية السلسلة الحرف f: 
كود :
regexp = "a(bc|de)f"
for sample in ['abcf', 'adef',  'abef', 'abf']:
   match = re.match(regexp, sample)
   print(f"{sample:>4s} → {nice(match)}")

يلعب القوسان () دورا اضافيا في المجموعات، ما يعني أننا نستطيع أن نستخرج النص الموافق للتعبير المنتظم المحدد بالقوسين. هذا المثال يوضح المقصود: 
كود :
regexp = "a(bc|de)f"
sample = 'abcf'
match = re.match(regexp, sample)
print(f"{sample}, {regexp} → {match.groups()}")


## تعداد التكرار  

لديك المعاملات التالية: 
  • النجمة * التي تفيد صفر أو أكثر. مثلا `(ab)*` تتطابق مع '' و 'ab' و 'abab' الخ.
  • العلامة + والتي تعني مطابقة واحدة على الأقل، مثلا `(ab)+` تتطابق مع ab و abab و ababab الخ
  • العلامة ? والتي تعني رمزا واحدا أو لاشيء بمعنى آخر `(ab)?` تتطابق مع '' و 'ab' فقط
  • التعبير {n} يعني عدد مطابقات مساوي لـ n فمثلا `(ab){3}` تتطابق مع ababab لكنها لا تتطابق مع abab
  • التعبير {m,n} أي ما بين m و n مرة ضمنا.
كود :
samples = [n*'ab' for n in [0, 1, 3, 4]] + ['baba']

for regexp in ['(ab)*', '(ab)+', '(ab){3}', '(ab){3,4}']:
   # on ajoute \A \Z pour matcher toute la chaîne
   line_regexp = r"\A{}\Z".format(regexp)
   for sample in samples:
       match = re.match(line_regexp, sample)
       print(f"{sample:>8s} / {line_regexp:14s} → {nice(match)}")


## المجموعات والقيود  

سبق أن تحدثنا عن المجموعات المعرفة بالاسم (راجع group1 group2 group3 أعلاه). يمكننا ذكر المعاملات التالية بهذا الخصوص: 
  • القوسان (...) لتعريف مجموعة مجهولة
  • التعبير `(?P<name>...)` يقوم بتعريف مجموعة بالاسم
  • التعبير `(?:...)` يتيح استخدام القوسين لكن بدون انشاء مجموعة، وذلك لزيادة كفاءة التنفيذ بما أننا لسنا بحاجة للاحتفاظ بروابط نحو السلسلة المدخلة.
  • التعبير `(?P=name)` والذي لا يتطابق إلا عند العثور في مكانه من السلسلة النصية المدخلة على نفس النص الجزئي المعرف بالمجموعة المسماة name
  • أخيرا `(?=...)` و `(?!...)` و `(?<=...)` تتيح قيودا أكثر تطورا. نترك لك المجال للتدرب عليها اذا كنت مهتما. تذكر على كل حال أن استخدام هذه التراكيب قد تجعل تفسير تعابيرك المنتظمة قليل الفاعلية
## الشره مقابل عدم الشره (Greedy vs non-greedy) 

قد تكون هناك طريقة أفضل لبناء تعبير منتظم عندما ترغب في تكراره عددا من المرات غير محدد. سواء استعنت بـ * أو + أو ? فالخوارزمية ستحاول دائما العثور على أطول سلسلة. لهذا نصف هذا التمشي بالشره.  

كود :
# uمقطع HTML
line='<h1>Title</h1>'

# u لو فرضنا أننا نبحث عن نص موجود بين ظفرين اي الرمزين أصغر وأكبر
# u بمعنى آخر التعبير المنتظم التالي "<.*>"
re_greedy = '<.*>'

# u سنحصل على ما يلي
# u نذكر أن group(0) يعيد الينا الجزء من مقطع
# HTML الذي يتطابق مع التعبير
match = re.match(re_greedy, line)
match.group(0)


ليس هذا ما نريده تحديدا. ربما يمكننا استخدام تمشي معاكس، أي ايجاد أصغر سلسلة مطابقة ضمن تمشي يُطلق عليه non-greedy وذلك بواسطة المعاملات التالية: 


*? : * but non-greedy
+? : + but non-greedy
?? : ? but non-greedy

كود :
# u هنا سنعوض النجمة بـ *? لجعل المعامل غير شره
re_non_greedy = re_greedy = '<.*?>'

# u لكننا سنواصل البحث عن نص بين <> بشكل عادي
# u وهذه المرة سنحصل على
match = re.match(re_non_greedy, line)
match.group(0)

## بخصوص معالجة آخر السطر  

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

تاريخيا، تقترن التعابير المنتظمة كما نجدها في مكتبات لغة سي، وبالتالي في sed و grep وغيرها من أدوات يونكس، قلنا تقترن بالنموذج الذهني حيث نطابق المدخلات سطرا سطرا. تحتفظ المكتبة re بأثر من ذلك. هذا مثال من معالجة ما يسمى 'newline' 
كود :
sample = """une entrée
sur
plusieurs
lignes
"""

كود :
match = re.compile("(.*)").match(sample)
match.groups()

كما هو منتظر، لم تقم النقطة باعتبارها جوكير (wildcard) باستخراج رمز نهاية السطر `\n` ، فلو أنها فعلت لكنا تحصلنا على كامل قيمة sample. يوجد راية (flag) باسم re.DOTALL تتيح لنا جعل النقطة جوكير لكل الرموز بما في ذلك رمز نهاية السطر: 
كود :
match = re.compile("(.*)", flags=re.DOTALL).match(sample)
match.groups()

رمز نهاية السطر يعتبر أحد رموز المحارف كغيره، لذلك يمكننا الاشارة اليه في تعبير منتظم كغيره. هذه بعض الامثلة التي توضح لنا ذلك:  

يمكننا اقتصار التطابق `\w` على محارف ASCII عبر استخدام الراية re.ASCII 
كود :
match = re.compile("([\w ]*)", re.ASCII).match(sample)
match.groups()


افتراضيا في بيثون3 تغطي `\w` جميع محارف يونيكود بما في ذلك الحروف العربية وبالتالي الحرف é 
كود :
match = re.compile("([\w ]*)").match(sample)
match.groups()


اذا أضفنا `\n` الى قائمة المحارف المنتظرة، نحصل فعليا على كامل المحتوى الاصلي. مع التنبيه هنا الى ضرورة عدم استخدام raw-string لأننا نريد كتابة رمز نهاية السطر في التعبير المنتظم 
كود :
match = re.compile("([\w \n]*)").match(sample)
match.groups()


# خاتمة  

من المؤكد أن تطوير التعابير المنتظمة يتطلب الكثير من الجهد ، ويتطلب الكثير من الممارسة ، ولكنه يتيح كتابة خوارزميات قوية جدًا في بضعة سطور وهو بالتأكيد استثمار مربح جدًا.  

أشير في الختام إلى وجود عدة مواقع وب تقوم بتقييم تعابير منتظمة بطريقة تفاعلية ربما تساعد في ضبط التعابير بأقل إثارة للملل. من بين هته المواقع أذكر على سبيل المثال: https://pythex.org مع الاشارة الى وجود العديد من المواقع غيره.  

## للمزيد من الاطلاع  

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

بواسطة التعابير المنتظمة، نعالج ناحية التحليل المعجمي، وبالنسبة للتحليل اللغوي فنلاحظ وجود العديد من البدائل نذكر منها:  

pyparsing       :   http://pyparsing.wikispaces.com/Download...stallation
PLY (Python Lex-Yacc)   :   http://www.dabeaz.com/ply 
ANTLR       :   http://www.antlr.org  

هذا الموقع الأخير يوفر أداة مكتوبة بلغة الجافا لكنها تستطيع توليد تحاليل للبيثون  
الرد


التنقل السريع :


مستخدمين يتصفحوا هذا الموضوع: 1 ضيف