ביטויים רגולריים ב-Python

ביטויים רגולריים הם שפה מצוינת להתאמת דפוסי טקסט. הדף הזה מספק מבוא בסיסי לביטויים רגולריים מספיקים לתרגילי Python שלנו ומראה איך ביטויים רגולריים פועלים ב-Python. המודול 're' ב-Python מספק תמיכה בביטויים רגולריים.

ב-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) מאחסן את תוצאת החיפוש במשתנה בשם 'התאמה'. לאחר מכן הפקודה if-statement בודקת את ההתאמה. אם היא true, החיפוש הצליח ו-match.group() הוא הטקסט התואם (למשל, 'word:cat'). אחרת, אם ההתאמה היא False (ללא כדי להיות ספציפי יותר), החיפוש לא הצליח ואין טקסט תואם.

התו 'r' בתחילת מחרוזת התבנית מציין מחרוזת "גולמית" של פיתון שעוברת דרך לוכסנים הפוכים ללא שינוי, וזה שימושי מאוד לביטויים רגולריים (ל-Java יש צורך כבד בתכונה הזו!). מומלץ תמיד לכתוב מחרוזות דפוס עם ה-'r' כמנהג.

תבניות בסיסיות

חזקה של ביטויים רגולריים היא שהם יכולים לציין דפוסים, ולא רק תווים קבועים. הנה הדפוסים הבסיסיים ביותר שתואמים לתווים בודדים:

  • a, X, 9, < -- תווים רגילים פשוט מתאימים לעצמם בדיוק. המטא-תווים שאינם תואמים לעצמם מפני שיש להם משמעויות מיוחדות הם: . ^ $ * + ? { [ ] \ | ( ) (פרטים בהמשך)
  • . (נקודה) -- תואם לכל תו בודד מלבד השורה החדשה '\n'
  • \w -- (באותיות קטנות w) תואם לתו "מילה": אות או ספרה או סרגל תחתון [a-zA-Z0-9_]. הערה: למרות שהמילה "מילה" מופיעה במילון, היא תואמת רק לתו אחד ולא למילה שלמה. \W (אותיות רישיות W) תואם לכל תו שאינו מילה.
  • \b – גבול בין מילה לבין מילה שאינה מילה
  • \s -- (אותיות קטנות s) תואם לתו יחיד של רווח לבן -- רווח, שורה חדשה, return, Tab, טופס [ \n\r\t\f]. \S (אותיות רישיות S) תואם לכל תו שאינו רווח לבן.
  • \t, \n, \r -- כרטיסייה, שורה חדשה, החזרה
  • \d – ספרה עשרונית [0-9] (חלק מהכלים הישנים יותר לביטויים רגולריים לא תומכים ב- \d, אבל בכולם יש תמיכה בתווים \w ו-\s)
  • ^ = start, $ = end – תואם להתחלה או לסופה של המחרוזת
  • \ -- מפחיתים את ה "מומחיות" של דמות מסוימת. לדוגמה, אפשר להשתמש ב-\. כדי להתאים נקודה או ב-\\ כדי להתאים קו נטוי. אם לא ברור לך אם לתו מסוים יש משמעות מיוחדת, כמו '@', אפשר לנסות להוסיף לפניו קו נטוי, \@. אם הוא לא רצף תווי בריחה (escape) תקין, כמו \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"

חזרה על מילים

דברים נעשים מעניינים יותר כשמשתמשים ב-+ וב-* כדי לציין חזרה בתבנית

  • + -- 1 או יותר מופע של הדפוס משמאל לו, למשל '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 קוף סגול'. נשתמש בו כדוגמה פעילה כדי להדגים עוד תכונות של ביטויים רגולריים. הנה ניסיון להשתמש בתבנית 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'.

חילוץ קבוצה

התכונה "קבוצה" של ביטוי רגולרי מאפשרת לבחור חלקים מהטקסט התואם. נניח שעבור בעיית האימיילים אנחנו רוצים לחלץ את שם המשתמש והמארח בנפרד. כדי לעשות זאת, מוסיפים סוגריים ( ) סביב שם המשתמש והמארח בתבנית, כמו בדוגמה הבאה: 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

Findall() היא ככל הנראה הפונקציה היחידה החזקה ביותר במודול מחדש. למעלה השתמשנו ב- re.search() כדי למצוא את ההתאמה הראשונה של תבנית מסוימת. findall() מוצא את *all* ההתאמות ומחזיר אותן כרשימה של מחרוזות, כאשר כל מחרוזת מייצגת התאמה אחת.
  ## 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)

findall עם קבצים

לקבצים, יש מקרים שבהם נוהגים לכתוב לולאה כדי לחזור על שורות הקובץ, ואז לקרוא ל-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 וקבוצות Google

ניתן לשלב את מנגנון קבוצת הסוגריים ( ) עם findall(). אם הדפוס כולל 2 קבוצות סוגריים או יותר, אז במקום להחזיר רשימה של מחרוזות, findall() מחזירה רשימה של *tuples*. כל טפל מייצג התאמה אחת של הדפוס, ובתוך ה-tuple הוא מכיל את הנתונים של הקבוצה(1), הקבוצה(2) .. אם מוסיפים 2 קבוצות סוגריים לתבנית האימייל, searchall() מחזירה רשימה של צמדים, כל אורך 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() מחזירה רשימה של מחרוזות שתואמות לאותה קבוצה. (תכונה אופציונלית מעורפלת: לפעמים יש קיבוצי סוגריים ( ) בתבנית, אך אינך רוצה לחלץ אותם. במקרה כזה, כותבים את הסוגריים עם התו ?: למשל (?: ) והסוגריים השמאליים לא ייספרו כתוצאה מהקבוצה).

תהליך עבודה וניפוי באגים ב-RE

דפוסי ביטויים רגולריים מספקים משמעות רבה למספר קטן של תווים בלבד, אבל הם כל כך צפופים שאפשר להקדיש זמן רב לניפוי באגים בתבניות. מגדירים את זמן הריצה כך שאפשר להריץ תבנית ולהדפיס את מה שהיא תואמת בקלות. לדוגמה, אפשר להריץ אותה על טקסט בדיקה קטן והדפסת התוצאה של findall() . אם הדפוס לא תואם לשום דבר, אפשר לנסות להחליש את התבנית ולהסיר חלקים ממנה כדי לקבל התאמות רבות מדי. אם אין התאמה לאף אחד מהגורמים, לא תוכלו להתקדם כי אין לנו שום קונספט. ברגע שתהיה התאמה יותר מדי, תוכל לעבוד על חיזוקו בהדרגה כדי להשיג בדיוק את מה שרצית.

אפשרויות

הפונקציות re משתמשות באפשרויות לשינוי ההתנהגות של התאמת הדפוס. סימון האפשרות נוסף כארגומנט נוסף לפונקציות 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 Compatible regular Expressions -- pcre. Python כוללת תמיכה ב-pcre. להרבה תוכנות בשורת הפקודה וכדומה יש דגל שבו הם מקבלים דפוסי pcre.

שיטה ישנה יותר, אבל משתמשים בה הרבה, כדי לקודד את הרעיון של "כל התווים האלה חוץ מעצירה ב-X", בסגנון של סוגריים מרובעים. עבור הקטע שלמעלה, אפשר לכתוב את הדפוס, אבל במקום .* כדי לקבל את כל התווים, יש להשתמש ב-XXXXXXXX>]* שמדלג על כל התווים שאינם > (סימן ^ מוביל "הופך" את קבוצת הסוגריים המרובעים כך שיתאים לכל תו שלא נמצא בסוגריים).

החלפה (אופציונלי)

הפונקציה re.sub(pat, replacement, 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

תרגיל

כדי לתרגל ביטויים רגולריים, יש לעיין בתרגיל לשמות לתינוקות.