🔍 التعابير النمطية (Regex)

🔍 التعابير النمطية (Regex) في بايثون

ابحث عن الأنماط واستخرجها وبدّلها في أي نص — سلاح سري يوفّر لك مئات الأسطر من الكود.

🔤 الرموز الأساسية 📧 التحقق من الإيميل 📱 أرقام الهاتف ✂️ استخراج البيانات 🔄 البحث والاستبدال 🏷️ Groups

١. ما هو Regex؟

التعابير النمطية (Regular Expressions) هي لغة صغيرة لوصف أنماط النصوص. فكّر فيها كـ "بحث متطور جداً". بدلاً من البحث عن كلمة محددة، تصف شكل ما تريده.

مثال بسيط: للتحقق من أن إيميلاً صحيح، regex: ^[\w.+-]+@[\w-]+\.[a-z]{2,}$ — بسطر واحد يُغني عن عشرات الشروط.
Python — البداية
import re

text = "اتصل بنا على 0612345678 أو 0523456789"

# هل يحتوي النص على رقم هاتف مغربي؟
if re.search(r'\b0[5-7]\d{8}\b', text):
    print("✅ وُجد رقم هاتف")

# استخراج كل الأرقام
phones = re.findall(r'0[5-7]\d{8}', text)
print(phones)  # ['0612345678', '0523456789']
دائماً استخدم raw string: اكتب r'\d+' وليس '\d+' لتجنب تفسير الـ backslash.

٢. الرموز الأساسية — جدول مرجعي

رموز المطابقة

الرمزالمعنىمثاليطابق
.أي حرف (عدا سطر جديد)a.cabc, a1c, a-c
\dرقم (0-9)\d\d\d123, 456
\Dليس رقماً\D+abc, هاي
\wحرف أو رقم أو _\w+hello, test_1
\Wليس حرفاً أو رقماً\W!, @, مسافة
\sمسافة / tab / سطر جديد\s+فراغات
\Sليس مسافة\S+كلمات
^بداية النص^HelloHello world
$نهاية النصend$the end
\bحدود الكلمة\bcat\bcat (ليس catch)

رموز التكرار

الرمزالمعنىمثاليطابق
*صفر أو أكثرab*cac, abc, abbc
+واحد أو أكثرab+cabc, abbc (ليس ac)
?صفر أو واحدcolou?rcolor, colour
{n}بالضبط n مرة\d{4}2025
{n,m}من n إلى m مرة\d{2,4}12, 123, 1234
[abc]أي من هذه الأحرف[aeiou]أي حرف علة
[^abc]ليس هذه الأحرف[^0-9]أي حرف غير رقمي
a|ba أو bcat|dogcat أو dog

٣. دوال مكتبة re

Python — الدوال الأساسية
import re

text = "البريد: ahmed@gmail.com والهاتف: 0612345678"

# ===== re.search() — أول تطابق =====
match = re.search(r'\d{10}', text)
if match:
    print(match.group())   # 0612345678
    print(match.start())   # موقع البداية
    print(match.end())     # موقع النهاية

# ===== re.findall() — كل التطابقات =====
emails = re.findall(r'[\w.+-]+@[\w-]+\.\w+', text)
print(emails)  # ['ahmed@gmail.com']

# ===== re.finditer() — iterator من matches =====
long_text = "رقم 123 وآخر 456 وأخير 789"
for m in re.finditer(r'\d+', long_text):
    print(f"وجدت '{m.group()}' في موقع {m.start()}-{m.end()}")

# ===== re.sub() — بحث وإستبدال =====
dirty  = "السعر:   500   درهم"
clean  = re.sub(r'\s{2,}', ' ', dirty)  # يزيل المسافات الزائدة
print(clean)   # "السعر: 500 درهم"

# إخفاء أرقام البطاقات
card    = "رقم بطاقتك: 4532-1234-5678-9012"
hidden  = re.sub(r'\d{4}-\d{4}-\d{4}', 'XXXX-XXXX-XXXX', card)
print(hidden)  # رقم بطاقتك: XXXX-XXXX-XXXX-9012

# ===== re.split() — تقسيم بنمط =====
data   = "خليل,فاطمة;أحمد  سارة"
parts  = re.split(r'[,;\s]+', data)
print(parts)  # ['خليل', 'فاطمة', 'أحمد', 'سارة']

# ===== re.compile() — تجميع النمط للاستخدام المتكرر =====
phone_re = re.compile(r'0[5-7]\d{8}')
print(phone_re.findall("0661234567 و 0522345678"))

🧠 سؤال سريع

ما الفرق بين re.search() و re.findall()؟

لا فرق، كلاهما يعطي نفس النتيجة
search() يُرجع أول تطابق فقط (object أو None)، findall() يُرجع قائمة بكل التطابقات
findall() أبطأ من search()
search() يبحث في قاموس وليس نصاً
✅ صحيح! search() يقف عند أول تطابق، بينما findall() يمشي في كامل النص ويجمع الكل.
❌ خطأ. search() يُرجع Match object أو None للتطابق الأول فقط، أما findall() فيُرجع قائمة بكل التطابقات.

٤. Groups — استخراج أجزاء محددة

الأقواس () في regex تنشئ "مجموعة" يمكن استخراجها بشكل منفصل.

Python — Groups
import re

# استخراج أجزاء التاريخ
text  = "تاريخ الميلاد: 15/05/2011"
match = re.search(r'(\d{2})/(\d{2})/(\d{4})', text)
if match:
    print(match.group())   # 15/05/2011 (كامل)
    print(match.group(1))  # 15  (اليوم)
    print(match.group(2))  # 05  (الشهر)
    print(match.group(3))  # 2011 (السنة)

# Named groups — أوضح بكثير
pattern = re.compile(r'(?P<day>\d{2})/(?P<month>\d{2})/(?P<year>\d{4})')
m       = pattern.search(text)
if m:
    print(f"اليوم: {m['day']}, الشهر: {m['month']}, السنة: {m['year']}")

# استخراج كل الإيميلات مع domain منفصل
emails_text = "ahmed@gmail.com و sara@yahoo.fr و khalil@hotmail.com"
pat   = re.compile(r'([\w.+-]+)@([\w-]+\.\w+)')
for m in pat.finditer(emails_text):
    print(f"مستخدم: {m.group(1):15} | نطاق: {m.group(2)}")

# re.sub مع group reference
dates    = "تاريخ: 15/05/2025"
reversed_ = re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\2-\1', dates)
print(reversed_)  # تاريخ: 2025-05-15

٥. Flags — خيارات إضافية

Python — Flags
import re

# re.IGNORECASE — تجاهل حالة الأحرف
text  = "Python PYTHON python PyThOn"
found = re.findall(r'python', text, re.IGNORECASE)
print(found)   # ['Python', 'PYTHON', 'python', 'PyThOn']

# re.MULTILINE — ^ و $ يعملان مع كل سطر
multi  = """Python سهل
Python ممتع
Python مفيد"""
lines  = re.findall(r'^Python', multi, re.MULTILINE)
print(len(lines))   # 3

# re.DOTALL — النقطة تطابق السطر الجديد أيضاً
html   = "<div>\n  مرحباً\n</div>"
inner  = re.search(r'<div>(.+?)</div>', html, re.DOTALL)
print(inner.group(1).strip())   # مرحباً

# دمج flags
re.findall(r'python', text, re.IGNORECASE | re.MULTILINE)

🎮 جرّب بنفسك — مختبر Regex

🧪 اكتب نمطاً وشاهد النتيجة فوراً

📝 النص:
🔍 النمط (Regex):
📤 النتائج:
اضغط "تشغيل" لرؤية النتيجة...

🧠 سؤال سريع

ما الذي يطابقه النمط \d{3}-\d{4}؟

أي رقم من 3 إلى 4 أرقام
3 أرقام ثم شرطة ثم 4 أرقام — مثل: 061-2345
نص من 3 إلى 4 أحرف
رمز يساوي 7 أرقام متتالية
✅ ممتاز! \d{3} = 3 أرقام بالضبط، ثم - حرفية، ثم \d{4} = 4 أرقام.
❌ خطأ. \d{3} يطابق 3 أرقام بالضبط، والشرطة - تطابق نفسها، ثم \d{4} يطابق 4 أرقام.

٦. أمثلة حقيقية — Regex في العمل

Python — التحقق من الإيميل وكلمة المرور والهاتف
import re

# ===== ١. التحقق من الإيميل =====
def is_valid_email(email: str) -> bool:
    pattern = re.compile(r'^[\w.+-]+@[\w-]+\.[a-z]{2,}$', re.IGNORECASE)
    return bool(pattern.match(email))

emails = ["ok@gmail.com", "bad@", "test.user+tag@sub.domain.org"]
for e in emails:
    icon = "✅" if is_valid_email(e) else "❌"
    print(f"{icon} {e}")

# ===== ٢. التحقق من قوة كلمة المرور =====
def check_password(pwd: str) -> dict:
    return {
        "الطول ≥ 8"         : bool(re.search(r'.{8,}',  pwd)),
        "حرف كبير"         : bool(re.search(r'[A-Z]', pwd)),
        "حرف صغير"         : bool(re.search(r'[a-z]', pwd)),
        "رقم"              : bool(re.search(r'\d',   pwd)),
        "رمز خاص !@#$"    : bool(re.search(r'[!@#$%^&*]', pwd)),
    }

pwd    = "MyPass@2025"
checks = check_password(pwd)
for label, ok in checks.items():
    print(f"{'✅' if ok else '❌'} {label}")
print(f"النتيجة: {sum(checks.values())}/5")

# ===== ٣. تنظيف HTML =====
def strip_html(html: str) -> str:
    return re.sub(r'<[^>]+>', '', html).strip()

print(strip_html("<h1>عنوان</h1><p>نص مهم</p>"))
# عنوان نص مهم

# ===== ٤. استخراج URLs من نص =====
def extract_urls(text: str) -> list:
    return re.findall(r'https?://[\w./%-]+', text)

news = "زر https://python.org و https://docs.python.org/3/"
print(extract_urls(news))

📝 تمارين تطبيقية

تمرين ١ — محلل بيانات السجلات (Log Parser)

لديك سجل خادم (server log). استخرج: التاريخ، رمز الحالة HTTP، وعنوان IP لكل سطر.

Python — الحل
import re

log = """
192.168.1.1 - [15/Mar/2025:10:30:00] "GET /index.html" 200 1234
10.0.0.5 - [15/Mar/2025:10:31:05] "POST /login" 401 56
172.16.0.2 - [15/Mar/2025:10:32:10] "GET /data.json" 200 890
"""

pattern = re.compile(
    r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3})'
    r'.+\[(?P<date>[^\]]+)\]'
    r'.+"(?P<req>[^"]+)"'
    r'\s(?P<status>\d{3})'
)

print(f"{'IP':<16} {'Status':<8} {'Date':<25} Request")
print("-" * 70)

for m in pattern.finditer(log):
    status_icon = "✅" if m['status'].startswith('2') else "❌"
    print(f"{m['ip']:<16} {status_icon}{m['status']:<6} {m['date']:<25} {m['req']}")

تمرين ٢ — محلل جهات الاتصال

اكتب دالة تأخذ نصاً وتستخرج منه جميع الإيميلات وأرقام الهاتف المغربية، وتُرجعها كقاموس.

Python — الحل
import re
from typing import Dict, List

def extract_contacts(text: str) -> Dict[str, List[str]]:
    return {
        "emails" : re.findall(r'[\w.+-]+@[\w-]+\.[a-z]{2,}', text, re.I),
        "phones" : re.findall(r'\b0[5-7]\d{8}\b', text),
    }

sample = """
للتواصل: admin@website.ma أو support@help.com
هاتف: 0661234567 أو 0522345678
البريد الثانوي: info@test.org
"""

result = extract_contacts(sample)
print(f"إيميلات ({len(result['emails'])}):")
for e in result['emails']:
    print(f"  📧 {e}")
print(f"\nهواتف ({len(result['phones'])}):")
for p in result['phones']:
    print(f"  📱 {p}")

🚀 المشروع — أداة تنظيف وتحليل النصوص

Python — text_analyzer.py
import re
from collections import Counter

class TextAnalyzer:
    """أداة تحليل وتنظيف النصوص باستخدام Regex"""

    def __init__(self, text: str):
        self.text = text

    def extract_emails(self) -> list:
        return re.findall(r'[\w.+-]+@[\w-]+\.[a-z]{2,}', self.text, re.I)

    def extract_urls(self) -> list:
        return re.findall(r'https?://\S+', self.text)

    def extract_numbers(self) -> list:
        return [float(n) if '.' in n else int(n)
                for n in re.findall(r'\b\d+(?:\.\d+)?\b', self.text)]

    def word_frequency(self, top=5) -> list:
        words = re.findall(r'\b[a-zأ-ي]{3,}\b', self.text, re.I)
        return Counter(words).most_common(top)

    def clean(self) -> str:
        t = re.sub(r'<[^>]+>', '', self.text)      # إزالة HTML
        t = re.sub(r'https?://\S+', '[رابط]', t)    # استبدال URLs
        t = re.sub(r'\s{2,}', ' ', t)               # إزالة مسافات زائدة
        return t.strip()

    def report(self):
        print("="*40)
        print("  📊 تقرير التحليل")
        print("="*40)
        print(f"📧 إيميلات    : {self.extract_emails()}")
        print(f"🔗 روابط      : {self.extract_urls()}")
        print(f"🔢 أرقام      : {self.extract_numbers()}")
        print(f"📝 أكثر كلمات : {self.word_frequency()}")
        print(f"\n✨ نص نظيف:\n{self.clean()}")

# ---- اختبار ----
sample = """
  <h1>مرحباً!</h1>  تواصل معنا على contact@example.com
  السعر 99.5 درهم. زر https://python.org لمزيد من المعلومات.
  رقم التواصل: 0612345678 أو info@site.ma
"""

TextAnalyzer(sample).report()

🎉 أتقنت التعابير النمطية!

Regex هو أداة قوية ستستخدمها في كل مشروع حقيقي — من التحقق من المدخلات إلى تحليل الملفات الضخمة.

الدرس التالي: APIs و Web Scraping ←