מדריך ביטויים רגולריים בפייתון

מחבר:
בתאריך:

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

ביטויים רגולריים יכולים לעזור לנו ב-4 משימות עיקריות:

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

אז אם אתם רוצים לשפר את יכולות התכנות שלכם, ולעבוד פחות (הרבה פחות) ולהשיג יותר אז כדאי מאוד שתלמדו את הנושא של ביטויים רגולריים. יאללה נתחיל!

מדריך ביטויים רגולריים בפייתון

 

ספריית re

כדי לעבוד עם ביטויים רגולריים בפייתון נתחיל מיבוא המודול re (קיצור של regex שהוא קיצור של regular expression).

import re

 

הפונקציה re.search

הפונקציה re.search בודקת האם ביטוי נמצא במחרוזת.

if re.search("regex", "regex are awesome"):
    print("regex exists")
else:
    print("no regex")

והתוצאה:

regex exists

מאוד פשוט.

וכדי להבין איך זה עובד, בואו נראה את התוצאה של התרגילים הבאים:

print(re.search("regex", "all the regexes are awesome"))

התוצאה:

re.Match object; span=(8, 13), match='regex'

הפונקציה מצאה מחרוזת מתאימה לביטוי בין עמדות 8-13.

אבל מה קורה במידה והפונקציה לא מוצאת התאמה?

print(re.search("regex", "all the ... are awesome"))

התוצאה:

None

re.search מוצא את ההתאמה הראשונה במחרוזת ואז מפסיק לחפש:

print(re.search("regex", "all the regexes are awesome including the regex [a-z]"))
re.Match object; span=(8, 13), match='regex'

כדי למצוא את כל ההתאמות במחרוזת משתמשים ב- re.findall, כפי שנראה בסעיף הבא.

מה יקרה אם אפילו נשנה אות אחת בביטוי החיפוש? לדוגמה נהפוך את האות הראשונה בביטוי מקטנה לגדולה: regex -> Regex

if re.search("Regex", "regex are awesome"):
    print("regex exists")
else:
    print("no regex")

והתוצאה היא שאין זיהוי.

no regex

כדי לפתור את הבעיה אנחנו צריכים ביטוי רגולרי, לדוגמה הביטוי הבא:

r"[Rr]egex"

הביטוי הרגולרי תחום בין מרכאות ומקדים אותו r.

r"[Rr]egex"

הביטוי הרגולרי שלנו מחפש מחרוזת שמתחילה ב-r קטנה או גדולה ואחריה הטקסט egex.

ננסה אותו:

if re.search(r"[Rr]egex", "regex are awesome"):
    print("regex exists")
else:
    print("no regex")

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

 

הפונקציה re.findall

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

בדוגמה הבאה, אנחנו מנסים למצוא במשפט את שמות האנשים.

str = 'George is 21 yrs. old and Kate is 19'

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

'[A-Z][a-z]+'

ביטוי רגולרי שתחום בין סוגריים מרובעים מחפש טווח של תווים. [A-Z] כדי למצוא אות ראשונה גדולה. [a-z]+ כדי למצוא לפחות אות קטנה אחת. כי התו + מחפש התאמה אחת או יותר לטווח שבא לפניו.

כך נראה קוד שמשתמש ב-re.findall כדי למצות מן המחרוזת את רשימת השמות.

str = 'George is 21 yrs. old and Kate is 19'

# re.findall() returns a list of all the matching names
names = re.findall(r'[A-Z][a-z]+', str)   
print (names)

והתוצאה היא:

['George', 'Kate']

 

טווח של תווים

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

הטווחמוצא התאמה ל
[ab]a או b
[abc]a, b או c
[A-Z]אותיות גדולות בלבד
[a-z]אותיות קטנות בלבד
[A-Za-z]אותיות קטנות או גדולות בלבד
[0-9]ספרות בלבד
[A-Za-z0-9]אותיות או ספרות בלבד
[א-ת]כל האותיות בעברית
[a-d]האותיות a-d בלבד

לדוגמה, הביטוי הרגולרי הבא מוצא התאמה לאותיות א' עד ג':

[א-ג]

ואם אנחנו רוצים למצוא התאמה לאותיות א-ד או לספרות 2-5 אז נכתוב זאת כך:

[2-5א-ד]

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

[^a-d]

מחפש התאמה לכל מה שאינו בסט התווים a-d. זה יכול להיות האות e או 9 או כל אות בעברית.

בואו נראה דוגמאות נוספות לביטויים רגולריים שהם סטים משלימים:

הסטמוצא התאמה ל
[^A-Z]כל מה שאינו אותיות גדולות
[^a-z]כל מה שאינו אותיות קטנות
[^A-Za-z]כל מה שאינו אותיות אנגליות
[^0-9]כל מה שאינו ספרות
[^A-Za-z0-9]אותיות או ספרות בלבד
[^א-ת]כל מה שאינו אות בעברית
[^a-d]כל מה שאינו בטווח האותיות a-d

 

כמתים (quantifiers)

נחזור לרגע לדוגמה שכבר ראינו:

str = 'George is 21 yrs. old and Kate is 19'

# re.findall() returns a list of all the found names
names = re.findall(r'[A-Z][a-z]+', str)

אפשר לראות שהביטוי:

r'[A-Z][a-z]+'

מכיל טווח של תווים וגם את הסימן + שמשמעותו שהביטוי שבא לפניו צריך להופיע פעם אחת או יותר.

הסימן + הוא דוגמה לכמתים (quantifier) שתפקידם לציין את כמות החזרות על הביטוי הרגולרי שאחריו הם באים.

הטבלה הבאה מציגה את הכמתים:

הכמתמשמעותו
*אפס פעמים או יותר
+1 או יותר פעמים
?0 פעמים או פעם אחת
{3}בדיוק 3 פעמים
{3,8}בין 3 ל-8 פעמים
{3, }לפחות 3 פעמים
{ ,8}לכל היותר 8 פעמים

 

תווים מיוחדים

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

Meta characterמשמעות
.כל תו מלבד שורה חדשה
\wאותיות אנגליות קטנות וגדולות, מספרים וקו תחתון
\dמתאים לכל ספרה 0-9
\sדוגמת רווח, טאב ושורה חדשה white space
\bרווח בין מילים
$סוף מחרוזת
^תחילת מחרוזת

זה יכול להיות שימושי, לדוגמה אם אנחנו רוצים למצוא את כל הגילאים במחרוזת הבאה:

str = "George is 21 yrs. old and Kate is 19"
ages = re.findall(r"\d+",str)
print(ages)

והתוצאה:

['21', '19']

נשתמש בדוגמת הקוד הבאה כדי לקבל מערך של אובייקטים המכיל את שמות האנשים והגילאים שלהם:

str = "George is 21 yrs. old and Kate is 19"
ages = re.findall(r"\d+",str)
names = re.findall(r'[A-Z][a-z]+', str)   
 
people = []
for i in range(0, len(names)):
    people.append({'name':names[i], 'age':ages[i]})
 
print(people)

אפשר לכתוב את זה יותר בקצרה בעזרת zip ו list comprehensions

people = [{'name': n, 'age': a} for (n, a) in list(zip(names, ages))]

מה קורה כשאנחנו רוצים למצוא התאמה לנקודה או ל-$ (למרות שיש להם משמעות מיוחדת)? במקרים אילו, נשתמש בקו נטוי (\), שגורם לאיבוד המשמעות המיוחדת של המטה תגיות, ולהתייחסות אליהם כפשוטם.

הדפוסמוצא התאמה ל
\.פשוט נקודה
\$$ פשוט
\^^ פשוט
\\פשוט קו נטוי

לכל אחד מהתווים המיוחדים יש את התו המציין את הסט המשלים שאותו מציינים באמצעות אות גדולה במקום הקטנה. לדוגמה, אם התו \d מציין את כל הספרות 0-9 אז את הסט המשלים נציין באמצעות \D שיציין את סט התווים שאינם 0-9.

Meta characterמשמעות
\Wמה שאינו אותיות אנגליות קטנות וגדולות, מספרים וקו תחתון
\Dמה שאינו סיפרה
\Sמה שאינו white space

 

החלפה

כדי להחליף ביטוי אחד באחר נשתמש בפונקציה-re.sub.

הקוד הבא מחליף את כל מה שאינו מספר במחרוזת ריקה, ומשאיר אותנו עם ספרות בלבד.

only_numbers = re.sub(r'\D','','088-9123-4100')
print(only_numbers)

והתוצאה:

08891234100

 

ביטויים עצלים

כמתים של ביטויים רגולריים נוטים להיות חמדנים ולמצוא התאמה רחבה יותר מכפי שהתכוונו. לדוגמה, בביטוי הבא יש שני אנשים. אני מעוניין להחליף את צבעם של האנשים בביטוי במחרוזת "[@!?]" באמצעות הקוד הבא:

str = "אמר האיש <span>הירוק</span> לאיש <span>הכחול</span>."
res = re.sub(r"<span>.+<\/span>",'[@!?]',str)
print(res)

והתוצאה היא:

אמר האיש [?!@].

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

הביטוי המשוכתב יכלול עכשיו את סימן השאלה, שיהפוך את הכמת מ"חמדן" ל"עצל":

str = "אמר האיש <span>הירוק</span> לאיש <span>הכחול</span>."
res = re.sub(r"<span>.+?<\/span>",'[@!?]',str)
print(res) 

והתוצאה היא :

אמר האיש [@!?] לאיש [@!?].

בדיוק מה שאנחנו רוצים.

 

בחירה בין אפשרויות

כדי לבחור בין אפשרויות, נפריד ביניהן באמצעות צינור (|). לדוגמה, הביטוי הבא מאפשר לזהות קבצים הכוללים בשמם הרחבות מסוג תמונה - png, gif, jpg.

r"(png|gif|jpg)"

נראה את זה בפעולה:

filename = "car.jpg"
if re.search(r"(png|gif|jp(e)?g)$",filename):
    print('%s is a valid file name' % filename) 
else:
    print('%s is not a valid file name' % filename) 

והתוצאה היא:

car.jpg is a valid file name
  • הביטוי: jp(e)?g מאפשר לזהות הרחבות jpg וגם jpeg.
  • השימוש ב-$ מורה לביטוי לחפש את ההתאמה בסוף המחרוזת.

 

קבוצות של ביטויים

כשמקיפים ביטוי בסוגריים, ניתן להתייחס אליו אחר כך באמצעות reference, שמציינים באמצעות קו נטוי \. אל הסוגריים הראשונים משמאל נתייחס כ-\1, אל השניים משמאל כ-\2, וכיו"ב. בדוגמה הבאה, אנחנו לוקחים את התאריך בפורמט האמריקאי (09-28-2019) שבו החודש מופיע ראשון משמאל, וממירים אותו לפורמט הישראלי שבו היום מופיע ראשון:

il_date = re.sub( 
           r"(\d{2})-(\d{2})-(\d{4})", 
           r"\2.\1.\3",
           "09-28-2019"
       )
print(il_date)
28.09.2019

במידה ואנחנו לא מעוניינים לתפוס את אחת הקבוצות נוסף ?: בתחילת הסוגריים. לדוגמה, אם איננו מעוניינים לתפוס את השנה:

il_date = re.sub( 
           r"(\d{2})-(\d{2})-(?:\d{4})", 
           r"\2.\1.\3",
           "09-28-2019"
       )
print(il_date)

התוצאה בהתאם:

28.09.

 

מציאת התאמות במערכים

כדי לסנן ממערך רק את הפריטים המתאימים לביטוי נשתמש בפונקציה re.search בתוך לולאה.

לדוגמה, נמצא ברשימה הבאה את המזונות ששמם מתחיל ב-פ:

foods = ["טונה", "פלאפל", "צ'יפס", "פופקורן", "חומוס", "פיצה"] 

קודם כל נכתוב את הביטוי:

r"^פ[א-ת]+"

הביטוי מתחיל באות פ ואח"כ לפחות אות אחת נוספת.

את ההתאמה לביטוי נחפש בתוך הרשימה באמצעות לולאה ו-re.search:

foods = ["טונה", "פלאפל", "צ'יפס", "פופקורן", "חומוס", "פיצה"] 
p_foods = [f for f in foods if re.search(r"^פ[א-ת]+",f)]
print(p_foods)
['פיצה','פופקורן','פלאפל']

את הלולאה כתבתי באמצעות list comprehension - תחביר מקוצר לכתיבת לולאה (מדריך list comprehension).

 

כיצד להמשיך מכאן?

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

לכל המדריכים בסדרה ללימוד פייתון

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

הוסף תגובה חדשה

 

= 7 + 3