2.35. أساسيات الأنماط

التعبير النمطي هو لغة صغيرة لوصف السلاسل بالشكل بدلاً من محتواها الدقيق. استخدمه عندما تطابق السلسلة إذا، وفقط إذا، اتبعت نمطاً يمكنك وصفه لكن لا يمكنك تعداده -- "سلسلة من الأرقام يتبعها وحدة"، "سطر يبدأ بـ ERROR وينتهي برقم"، "أي من امتدادات الملفات هذه، بأي ترتيب، مع بادئات v اختيارية".

لا تلجأ إلى re إلا عندما لا تفي طريقة سلسلة عادية بالغرض.

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

2.35.1. الأمور الأربعة التي ستستخدمها

تكشف وحدة re الخاصة بـ MicroPython عن أربعة أمور:

  • re.compile() -- تحويل سلسلة نمط إلى كائن نمط مُصرَّف يمكنك إعادة استخدامه.

  • re.match() -- تجربة النمط عند بداية سلسلة. يكون النمط مرتكزاً عند الموضع 0.

  • re.search() -- تجربة النمط في أي مكان داخل سلسلة. يُعيد أول مطابقة.

  • re.sub() -- إيجاد كل مطابقة واستبدالها.

حالات حذف ملحوظة مقارنةً بـ CPython: لا توجد re.findall ولا re.finditer ولا re.split على مستوى الوحدة (تملك الأنماط المُصرَّفة دالة split بدلاً من ذلك)، ولا re.fullmatch، ولا ثوابت رايات مثل re.IGNORECASE. وحيثما تلجأ إلى واحدة منها في CPython، ابنِ المكافئ من re.search() داخل حلقة.

2.35.2. أول نمط

يطابق النمط r'\d+' رقماً واحداً أو أكثر:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

بضعة أمور يجدر الانتباه إليها:

  • يُكتب النمط كـ سلسلة خام (r'...') بحيث يصل الخط المائل العكسي في \d إلى re بدلاً من معالجته كهروب سلسلة في Python. استخدم دائماً السلاسل الخام لأنماط التعبيرات النمطية.

  • تُعيد re.search() كائن مطابقة عند النجاح وNone عند الفشل. تحقق دائماً قبل استدعاء match.group().

  • إن m.group(0) هو النص الكامل الذي طابقه النمط. أما المجموعة 1 و2 و... فتظهر لاحقاً، بمجرد أن يحتوي النمط على أقواس التقاط.

النمط نفسه مع re.match() يُعيد None لأن السلسلة لا تبدأ برقم:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. أجزاء النمط

تُبنى معظم الأنماط المفيدة من مجموعة صغيرة من الأجزاء. وأما تلك التي تعمل في MicroPython:

الأحرف الحرفية -- أي حرف ليس خاصاً يطابق نفسه. إن hello يطابق hello.

الأحرف الخاصة -- . ^ $ * + ? { } [ ] \ | ( ) لها جميعاً معانٍ مذكورة أدناه. لمطابقة أحدها حرفياً، اهرب منه بخط مائل عكسي: إن \. يطابق نقطة حرفية.

فئات الأحرف -- اختصارات لمجموعات أحرف شائعة:

  • \d -- أي رقم 0-9

  • \D -- أي حرف غير رقمي

  • \s -- أي حرف مسافة بيضاء (مسافة، جدولة، سطر جديد)

  • \S -- أي حرف ليس مسافة بيضاء

  • \w -- أي حرف "كلمة": حروف، أرقام، شَرطة سفلية

  • \W -- أي حرف ليس من أحرف الكلمات

  • . -- أي حرف باستثناء السطر الجديد

محدّدات الكمية -- كم مرة يجب أن يطابق الجزء السابق:

  • * -- صفر أو أكثر (جشع)

  • + -- واحد أو أكثر (جشع)

  • ? -- صفر أو واحد

  • {n} -- بالضبط n

  • {m,n} -- بين m وn (شامل)

الدمج: إن \d{3}-\d{4} يطابق ثلاثة أرقام، فشَرطة، فأربعة أرقام. إن \s+ يطابق حرف مسافة بيضاء واحداً أو أكثر. إن hello.*world يطابق hello، ثم أي شيء (بما في ذلك لا شيء)، ثم world.

ملاحظة

جشع يعني أن محدّد الكمية يستهلك أكبر قدر ممكن من المدخل مع ترك بقية النمط قادرة على المطابقة. ففي مقابل hello x world y world، فإن .* في hello.*world يطابق أطول مقطع يترك world في النهاية -- فيلتقط x world y، لا الأقصر x. وينطبق الأمر نفسه على + وصيغة النطاق {m,n}: يأخذ المحرّك أطول مطابقة ممكنة، ثم يتراجع فقط إذا فشلت بقية النمط.

2.35.4. الاستبدال

تجد re.sub() كل مطابقة وتستبدلها بسلسلة. يمكن للاستبدال أن يشير إلى المجموعات الملتقطة عبر \1 و\2 و... (يُغطّى مع بقية صياغة المجموعات لاحقاً). دون مجموعات، تكون re.sub مجرد عملية إيجاد واستبدال مباشرة على تعبير نمطي:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

الوسيط الثالث هو السلسلة المراد العمل عليها؛ والنتيجة سلسلة جديدة مع استبدال كل مطابقة.

2.35.5. التقسيم -- على نمط مُصرَّف فقط

لا توجد re.split على مستوى الوحدة. للتقسيم على تعبير نمطي، صرّف النمط أولاً واستدعِ دالته split

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

الوسيط الثاني الاختياري يحدّد سقف عدد التقسيمات:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. التصريف لإعادة الاستخدام

إذا كان النمط نفسه يعمل مرات عديدة -- داخل حلقة أو في دالة كثيرة الاستدعاء -- صرّفه مرة واحدة وأعد استخدام الكائن المُصرَّف:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

إن استدعاء pattern.match() وpattern.search() على كائن مُصرَّف يكافئ دوال مستوى الوحدة لكنه يتجنب تكلفة إعادة التصريف عند كل استدعاء.

2.35.7. أنماط لا تطابق أي شيء

هناك ثلاثة أنماط بالأخص تُربك المطوّرين:

  • إن .* يطابق السلسلة الفارغة. فإن re.search(r'.*', s).group(0) يُعيد '' لأي مدخل.

  • نمط يحتوي على حرف خاص غير مهروب منه هو خطأ صياغي. إن re.compile(r'cost: $5') يثير ValueError لأن $ تعني "نهاية السلسلة". استخدم r'cost: \$5'.

  • إن النقطة . لا تطابق سطراً جديداً. للمطابقة عبر الأسطر الجديدة، اكتب النمط ليتعامل معها صراحةً باستخدام [\s\S] أو مرّر سطراً واحداً في كل مرة.

بهذه الأجزاء يمكن لنمط أن يطابق أي شريحة نص ثابتة الشكل تقريباً. أما إخراج البيانات المنظّمة من المطابقة فيتطلب مجموعات التقاط.