2.35. יסודות התבניות¶
ביטוי רגולרי הוא שפה קטנה לתיאור מחרוזות לפי צורתן ולא לפי תוכנן המדויק. השתמשו בו כאשר מחרוזת מתאימה אם ורק אם היא עוקבת אחר תבנית שאתם יכולים לתאר אך לא למנות – ”רצף של ספרות ואחריו יחידה“, ”שורה שמתחילה ב-ERROR ומסתיימת במספר“, ”כל אחת מסיומות הקבצים האלה, בכל סדר, עם תחיליות v אופציונליות“.
פנו ל-re רק כאשר מתודת מחרוזת רגילה לא תספיק.
str.startswith(),str.endswith()– בדיקה של תחילית או סיומת קבועה.in– בדיקה האם תת-מחרוזת קבועה קיימת.str.split(),str.find(),str.replace()– עבודה עם תוחמים קבועים.
כל אחת מאלה מהירה יותר, קלה יותר לקריאה וקשה יותר לטעות בה מאשר ה-regex המקביל. השתמשו ב-regex כאשר הצורה של המחרוזת חשובה ותת-המחרוזת המדויקת אינה.
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. השתמשו תמיד במחרוזות גולמיות לתבניות regex.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 הוא מציאה-והחלפה ישירה על regex:
>>> 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 ברמת המודול. כדי לפצל לפי regex, הדרו תחילה את התבנית וקראו למתודת ה-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]או הזינו שורה אחת בכל פעם.
עם חלקים אלה תבנית יכולה להתאים כמעט לכל פלח טקסט קבוע-צורה. חילוץ נתונים מובנים בחזרה מתוך ההתאמה דורש קבוצות לוכדות.