משתמש:Mad dr/פייתון/מודולים/re

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

re היא ספריה סטנדרטית של פייתון שנותנת לנו יכולות טובות להפעיל regular expressions (או בקיצור regex - רגקס). כאשר יש מספר שיטות לשימוש ב-re. (מי שלא בקיא ברגקסים יכול להתחיל ללמוד באתר פרל בעברית http://perl.eitan.ac.il/main.php?id=00106)

search ו-match עריכה

search ו-match הן הפונקציות הבסיסיות של ביטויים רגולריים, הן מקבלות שני ערכים, הראשון הוא התבנית (pattern) והשני הוא המחרוזת (subject). ערכים נוספים שאפשר לשלוח הם נקודת התחלה, נקודת סיום ומודיפקטורים (modifiers).

במידה והתבנית מתאימה למחרוזת יחזור אובייקט מסוג re.MatchObject עליו נרחיב עוד מעט, אם לא יחזור לנו None.

ההבדלים בין search ל-match מסתכמים ב:

  1. search עובר על כל המחרוזת עד שהוא מוצא התאמה (או לא), match מחפש את התבנית בתחילת המחרוזת (במידה ונתנו נקודת התחלה שונה, הוא יחפש ממנה כמובן)
  2. ההתייחסות לשורה חדשה במצב של ריבוי שורות (מודיפקטור re.M) שונה, search מתייחס לכל שורה חדשה כהתחלה (עבור התו המיוחד ^) ואילו match מתייחס רק לתחילת המחרוזת:
>>> re.match('X', 'A\nB\nX', re.MULTILINE) # No match
>>> m = re.search('^X', 'A\nB\nX', re.MULTILINE) # Match
>>> m.start()
4
>>> m.end()
5

האובייקט המוחזר הוא מסוג re.MatchObject, המתודות המעניינות שלו הן group() שמחזירה מחרוזות שמורות (אוגרים) ומקבלת את המזהה של האוגר (החל מ-1), אם מצויין אוגר 0, יוחזר ההתאמה המלאה (ללא התייחסות לאוגרים), ואם ינתנו מספר פרמטרים כל הערכים המתאימים יחזרו כ-Tuple. פנייה לאוגר שלא קיים יזרוק חריגה IndexError: no such group

המתודה groups() מחזירה את כל האוגרים כ-Tuple, ומקבלת מחרוזת כפרמטר שיהיה ערך ברירת המחדל עבור אוגרים שאין להם תוכן (לא מחרוזות ריקות)

>>> m = re.match(r"(\d+)\.?(\d+)?", "24")
>>> m.groups()     # Second group defaults to None.
('24', None)
>>> m.groups('0') # Now, the second group defaults to '0'.
('24', '0')

המתודה groupdict מחזירה את האוגרים שיש להם שם ייחוס כמילון (ומתעלמת מאלו שאין להם). כדי לתת שם ייחוס משתמשים במבנה: סימן שאלה, P גדולה ושם הייחוס מוקף בסוגריים משולשים, לדוגמא:

>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+), (?P<profession>\w+)", "Albert Einstein, Physicist")
>>> m.groupdict()
{'first_name': 'Albert', 'last_name': 'Einstein', 'profession': 'Physicist'}

המתודות start ו-end מביאות את המיקום של ההתחלה והסוף של האוגר שניתן כפרמטר, אם לא צויין אוגר תחזור התוצאה המתאימה להתאמה המלאה (כמו במקרה של group(0))

>>> m.start()
0
>>> m.end()
26
>>> m.start(1)
0
>>> m.start(2)
7
>>> m.start(3)
17
>>> m.end(1)
6
>>> m.end(2)
15
>>> m.end(3)
26

קוד לדוגמא:

mad_dr@ubuntu:~$ cat url_parser.py
import re, sys, pprint
 
def parser(match):
    if match is None:
        print("    String doesn't match pattern")
        return
 
    template = '    group({}): {}'
    for i in range(10):
        try:
            print(template.format(i, match.group(i)))
        except IndexError:
            if i < 3:
                print(template.format(i, 'None'))
            else:
                break
 
    pprint.pprint(match.groupdict())
 
def main():
    str = sys.argv[1]
    print(str)
    pattern = r'(?P\w+)://(?P[^\s/]+)/(?P[^\s#?]+)#?(?P[^\s?]+)?\??(?P\S+)?'
    m = re.match(pattern, str)
    parser(m)
    return 0
 
if __name__ == '__main__':
    ret_code = main()
    sys.exit(ret_code)
mad_dr@ubuntu:~$
mad_dr@ubuntu:~$
mad_dr@ubuntu:~$ python3 url_parser.py 'https://docs.python.org/3/library/re.html#re.sub'
https://docs.python.org/3/library/re.html#re.sub
    group(0): https://docs.python.org/3/library/re.html#re.sub
    group(1): https
    group(2): docs.python.org
    group(3): 3/library/re.html
    group(4): re.sub
    group(5): None
{'headline': 're.sub',
 'hostname': 'docs.python.org',
 'parameters': None,
 'path': '3/library/re.html',
 'protocol': 'https'}

compile עריכה

המתודה compile מאפשרת לנו "לשמור" ביטוי רגולרי מסויים ולהשתמש בו עבור מספר מחרוזות, לדוגמא:

>>> email_pattern = re.compile(r'\w+@\w+[.].+')
>>> p.match(r'a@a.a')
<_sre.SRE_Match object; span=(0, 5), match='a@a.a'>
>>> p.match(r'a@a')
>>> p.match(r'a@a.')
>>> p.match(r'user@host.domain')
<_sre.SRE_Match object; span=(0, 16), match='user@host.domain'>

המשתנה email_pattern מחזיק בתוכו Regular Expression Object שמאפשר אחר-כך להשתמש כמעט בכל הפונקציות האחרות של re ללא צורך לחזור על התבנית (הפרמטר הראשון יהיה כבר המחרוזת הנבדקת).

split עריכה

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

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

>>> re.split('\W+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('(\W+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('\W+', 'Words, words, words.', 1)
['Words', 'words, words.']
>>> re.split('(\W+)', '...words, words...')
['', '...', 'words', ', ', 'words', '...', '']

שימו לב, אם ההפרדה נמצאת בהתחלה או בסוף המחרוזת תהיה מחרוזת ריקה ברשימה (בהתאם למיקום ההפרדה)

sub עריכה

sub מאפשרת לנו להחליף חלקים במחרוזת, זהו אחד המקומות בו שמירה על הערכים (מה שקראנו לו "אוגרים") יהעיל ביותר (למי שהתנסה בsed או awk הדברים ברורים)

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

sub מקבלת פרמטר נוסף המורה כמה החלפות יש לבצע, בדומה ל-split, המספר 0 אומר לבצע החלפה לכל מופע, ואילו מספר אחר יעצור את ההחלפות בהתאם.

>>> re.sub(r'\sAND\s', ' & ', 'Baked Beans And Rice', flags=re.IGNORECASE)
'Baked Beans & Rice'
>>> re.sub(r'(.*)\sAND\s(.*)', r'\2 & \1', 'Baked Beans And Rice', flags=re.IGNORECASE)
'Rice & Baked Beans'
>>> re.sub(r'(?P<first>.*)\sAND\s(?P<second>.*)', r'\g<2> & \g<first>', 'Baked Beans And Rice', flags=re.IGNORECASE)
'Rice & Baked Beans'

findall & finditer עריכה

findall מחזירה את כל הערכים שמתאימים לתבנית כרשימה, ואילו finditer מחזירה אותם כאיטרטור של MatchObject (ככה שצריך לעבור עליהם בלולאה)

>>> re.findall('\D+', 'asd3fghjk')
['asd', 'fghjk']
>>> for i in re.finditer(r'(?P<name>\w+\s\w+)[,\s]*(?P<address>.*)', 'Israel Israeli, Tel Aviv\nMoishe Oofnik, Sesame Street'):
...    print(i.groupdict())
...
{'address': 'Tel Aviv', 'name': 'Israel Israeli'}
{'address': 'Sesame Street', 'name': 'Moishe Oofnik'}

escape עריכה

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

>>> re.escape("{'address': 'Sesame Street', 'name': 'Moishe Oofnik'}")
"\\{\\'address\\'\\:\\ \\'Sesame\\ Street\\'\\,\\ \\'name\\'\\:\\ \\'Moishe\\ Oofnik\\'\\}"

תיעוד - https://docs.python.org/3/library/re.html