מדריך ביטויים רגולריים בפייתון
אחד הכלים החשובים ביותר בכל שפת תכנות הוא ביטויים רגולריים שמזהים דפוסים במחרוזות. השימוש בביטויים רגולריים מאפשר לנו לכתוב קוד קצר שעושה הרבה. לדוגמה, כיצד נזהה מספר טלפון בתוך טקסט שמזין משתמש או בתוך דף שגירדנו מהאינטרנט? ניתן לכתוב סקריפט בן מספר שורות המשתמש בלולאה (עם כך וכך תנאים) או שאפשר פשוט להשתמש בשורה אחת של קוד המכיל ביטויים רגולריים.
ביטויים רגולריים יכולים לעזור לנו ב-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).
כיצד להמשיך מכאן?
ביטויים רגולריים יכולים לעזור לכם בהרבה מצבים, ולכן כדאי להמשיך וללמוד. כמה מקורות שעזרו לי במיוחד הם:
- מדריך מקיף ל- regular expressions
- שליף cheat sheet, שמסכם את נושא ה-regex
- כלי לבדיקה און-ליין של ביטויים רגולריים
לכל המדריכים בסדרה ללימוד פייתון
אהבתם? לא אהבתם? דרגו!
0 הצבעות, ממוצע 0 מתוך 5 כוכבים
המדריכים באתר עוסקים בנושאי תכנות ופיתוח אישי. הקוד שמוצג משמש להדגמה ולצרכי לימוד. התוכן והקוד המוצגים באתר נבדקו בקפידה ונמצאו תקינים. אבל ייתכן ששימוש במערכות שונות, דוגמת דפדפן או מערכת הפעלה שונה ולאור השינויים הטכנולוגיים התכופים בעולם שבו אנו חיים יגרום לתוצאות שונות מהמצופה. בכל מקרה, אין בעל האתר נושא באחריות לכל שיבוש או שימוש לא אחראי בתכנים הלימודיים באתר.
למרות האמור לעיל, ומתוך רצון טוב, אם נתקלת בקשיים ביישום הקוד באתר מפאת מה שנראה לך כשגיאה או כחוסר עקביות נא להשאיר תגובה עם פירוט הבעיה באזור התגובות בתחתית המדריכים. זה יכול לעזור למשתמשים אחרים שנתקלו באותה בעיה ואם אני רואה שהבעיה עקרונית אני עשוי לערוך התאמה במדריך או להסיר אותו כדי להימנע מהטעיית הציבור.
שימו לב! הסקריפטים במדריכים מיועדים למטרות לימוד בלבד. כשאתם עובדים על הפרויקטים שלכם אתם צריכים להשתמש בספריות וסביבות פיתוח מוכחות, מהירות ובטוחות.
המשתמש באתר צריך להיות מודע לכך שאם וכאשר הוא מפתח קוד בשביל פרויקט הוא חייב לשים לב ולהשתמש בסביבת הפיתוח המתאימה ביותר, הבטוחה ביותר, היעילה ביותר וכמובן שהוא צריך לבדוק את הקוד בהיבטים של יעילות ואבטחה. מי אמר שלהיות מפתח זו עבודה קלה ?
השימוש שלך באתר מהווה ראייה להסכמתך עם הכללים והתקנות שנוסחו בהסכם תנאי השימוש.