Ενότητα 3 1 Compiler Lecture s 1.0 documentation Ενότητα 3-1 Regular expressions Regular expressions ( κανονικές εκφράσεις ) είναι ένα ισχυρό εργαλείο που προέρχεται από τη θεωρία των τυπικών γλωσσών (formal languages), το οποίο επιτρέπει την ευέλικτη αναζήτηση και ταίριασμα (matching) κειμένου σύμφωνα με μια προδιαγραφή (matching pattern). Μπορούμε να φανταστούμε την προδιαγραφή αυτή ως μία μίνι-γλώσσα, η οποία χρησιμοποιείται για τη συγκρότηση μιας μηχανής ταιριάσματος (matching engine). Η μηχανή επεξεργάζεται το κείμενο εισόδου σύμφωνα με τις οδηγίες της προδιαγραφής και επιστρέφει αν υπάρχει ταίριασμα (αληθές/ψευδές) σε ποια σημεία (θέση στο κείμενο) ποιο κομμάτι του κειμένου ταίριαξε με την προδιαγραφή επίσης, η μηχανή μπορεί να αντικαταστήσει τα κομμάτια που ταίριαξαν, με άλλο κείμενο, αν αυτό ζητηθεί Μια προδιαγραφή regular expression μπορεί να αποτελείται από άλλες μικρότερες ή από ένα σύνολο εναλλακτικών υπο-προδιαγραφών. Οι προδιαγραφές απαρτίζονται από χαρακτήρες: είτε απλοί χαρακτήρες όπως το a που ταιριάζει μόνο το γράμμα a, είτε ειδικοί χαρακτήρες ελέγχου, όπως η τελεία. που ταιριάζει (σχεδόν) οποιοδήποτε γράμμα. Η προδιαγραφή περνά από μια διαδικασία μεταγλώττισης (compilation) και κατασκευάζεται ένα σύνολο οδηγιών για τη μηχανή ταιριάσματος. Η βασική μηχανή είναι γραμμένη συνήθως σε C και πολύ γρήγορη σε εκτέλεση. Υπάρχουν δύο τύποι μηχανών: DFA, όπου η απόδοση εξαρτάται μόνο από το μέγεθος του κειμένου εισόδου (κι όχι από την πολυπλοκότητα της προδιαγραφής της regular expression). Πολύ γρήγορη μηχανή που για κάθε χαρακτήρα εισόδου παρακολουθεί ταυτόχρονα όλες τις πιθανές θέσεις ταιριάσματος (χρησιμοποιώντας διαφορετική κατάσταση για κάθε συνδυασμό ταιριάσματος). Δεν διαθέτει όμως πρόσθετα χαρακτηριστικά (θα δούμε αργότερα ποια), τα οποία είναι χρήσιμα στον προγραμματισμό. NFA, αργότερη μηχανή ταιριάσματος, δοκιμάζει διαδοχικά τις εναλλακτικές προδιαγραφές της regular expression αρχίζοντας από την αρχή του κειμένου εισόδου μέχρι να βρει ένα επιτυχές ταίριασμα. Παρέχει ενδιαφέροντα πρόσθετα εργαλεία, αλλά απαιτείται προσοχή στον τρόπο κατασκευής της προδιαγραφής: η απόδοση εξαρτάται από το πώς είναι γραμμένη. Σχεδόν όλες οι γλώσσες προγραμματισμού υλοποιούν NFA μηχανές για regular expressions. Η χρήση των regular expressions είναι δύσκολη για τους μη πεπειραμένους και πρέπει να γίνεται με προσοχή. Εξετάστε αν μπορείτε να λύσετε το πρόβλημά σας με πιο απλόν τρόπο πριν καταφύγετε στη λύση των regular expressions (RE). Some people, when confronted with a problem, think 'I know, I'll use regular expressions.' Now they have two problems. Jamie Zawinski, alt.religion.emacs (08/12/1997) Πολλά από τα παραδείγματα που ακολουθούν βασίζοντια σε υλικό από το βιβλίο Mastering Regular Expressions, 2nd ed., Jeffrey E.F. Friedl, O Reilly Media, July 2002. Python και regular expressions 1/7
Ενότητα 3 1 Compiler Lecture s 1.0 documentation Η υποστήριξη των regular expressions υπάρχει ενσωματωμένη στη γλώσσα, στο module re: import re Ξεκινάμε με την προδιαγραφή εκφρασμένη σε ένα string: restr = r'([ +]?[0 9]+(\.[0 9]*)?)\s*([CF])' Το string restr του παραδείγματος περιγράφει μια regular expression που ταιριάζει βαθμούς θερμοκρασίας, με προαιρετικό πρόσημο, ακέραιο και προαιρετικό δεκαδικό μέρος, πιθανά κενά και μετά τα γράμματα C ή F. Μην ανησυχείτε αν δεν την καταλαβαίνετε ακόμα! Παρατηρήστε πώς γράφεται το regular expression: η σταθερά string δίνεται ως r'. ', κάτι που συμβολίζει ένα raw string. Δεν σχετίζεται ειδικά με τα regular expressions, απλά βολεύει στην περίπτωσή μας: Η Python σε σταθερές string αντικαθιστά όπως και η C τα \n, \t κλπ με τον αντίστοιχο χαρακτήρα newline, tab, κ.ο.κ. Σε raw σταθερά string όμως, η αντικατάσταση αυτή δε γίνεται: το r'\n' είναι πάντα 2 χαρακτήρες, το \ και το n. Οι regular expressions χρησιμοποιούν πολύ τον μηχανισμό \χαρακτήρα, έτσι πρέπει να πούμε στην Python να μην προχωρήσει σε μετατροπές. Στη συνέχεια κατασκευάζουμε το αντικείμενο regular expression, το οποίο μπορούμε να θεωρήσουμε ως τη μηχανή ταιριάσματος. Είναι το αντικείμενο που μας παρέχει τις μεθόδους αναζήτησης και αντικατάστασης σύμφωνα με την προδιαγραφή του RE. rexp = re.compile(restr) Το rexp του παραδείγματος συμβολίζει το αντικείμενο-re και συνήθως κατασκευάζεται άπαξ, στην αρχή του προγράμματος. Στη συνέχεια, μπορούμε να το χρησιμοποιήσουμε όσες φορές θέλουμε για να ταιριάξουμε κείμενο. Στις επόμενες παραγράφους αναφέρονται μερικά μόνο από τα εργαλεία της Python για regular expressions. Για την πλήρη περιγραφή του module re, δείτε στο https://docs.python.org/3/library/re.html. Συνήθεις προδιαγραφές για regular expressions που αναγνωρίζονται από την Python είναι: abc ταιριάζει ακριβώς το γράμμα a, ακολουθούμενο από το b και μετά το c a b είτε το a, είτε το b [abc] character class: ένα από τα a, b ή c (ΠΡΟΣΟΧΗ: ΕΝΑΣ ΧΑΡΑΚΤΗΡΑΣ ΜΟΝΟ) [a-za-z] όπως προηγουμένως, αλλά σε ένα πεδίο τιμών, από a έως και z και από A έως και Z [^ab] ένας οποιοσδήποτε χαρακτήρας, εκτός από a ή b $ ταιριάζει στο τέλος του string (ρυθμίζεται να ταιριάζει και στο τέλος κάθε γραμμής του string) ^ ταιριάζει στην αρχή του string (ρυθμίζεται να ταιριάζει και στην αρχή κάθε γραμμής του string). ένας οποιοσδήποτε χαρακτήρας εκτός από newline (ρυθμίζεται να ταιριάζει και το newline) * 0 ή περισσότερες φορές ο χαρακτήρας που προηγείται + 1 ή περισσότερες φορές ο χαρακτήρας που προηγείται? 0 ή 1 φορά ο χαρακτήρας που προηγείται 2/7
Ενότητα 3 1 Compiler Lecture s 1.0 documentation {n} {m,n} n φορές / από n έως m φορές ο χαρακτήρας που προηγείται \b ταιριάζει στην αρχή και στο τέλος μιας λέξης \w \W οποιοσδήποτε αλφαριθμητικός / μη αλφαριθμητικός χαρακτήρας \s \S οποιοσδήποτε whitespace / μη whitespace χαρακτήρας *? +??? μη άπληστες (non-greedy) μορφές των *, + και? (μόνο σε NFA, βλ. πιο κάτω) () group ομαδοποίησης τμημάτων μιας RE, συγκρατούν και το κείμενο που ταιριάζει στο τμήμα (NFA μόνο) \1 \2 μέσα στη RE αντικαθίστανται από το τι έχει ταιριάξει ως τώρα στο αντίστοιχο group κλπ (NFA μόνο) Με βάση τα πιο πάνω μπορούμε να συντάξουμε σύνθετες προδιαγραφές, όπως εκείνη του παραδείγματος με τη θερμοκρασία. Μέθοδοι του αντικειμένου regular expression Η μέθοδος search() Η κύρια μέθοδος είναι η search() που παίρνει ως όρισμα ένα string και αναζητά ταιριάσματα οπουδήποτε μέσα στο string. Με το πρώτο ταίριασμα που θα βρεθεί, επιστρέφει ένα match object με τις πληροφορίες του ταιριάσματος. Αν δεν βρεθεί κανένα ταίριασμα, επιστρέφεται το κενό αντικείμενο (). >>> import re >>> restr = r'([ +]?[0 9]+(\.[0 9]*)?)\s*([CF])' >>> rexp = re.compile(restr) >>> m = rexp.search('abcd12.23') >>> m = rexp.search('abcd12.23 F') <_sre.sre_match object; span=(4, 11), match='12.23 F'> >>> m.group(0) '12.23 F' >>> m.group(1) '12.23' >>> m.group(2) '.23' >>> m.group(3) 'F' >>> m.groups() ('12.23', '.23', 'F') Τι είναι τα groups; Στην προδιαγραφή της RE, οι παρενθέσεις παίζουν ειδικό ρόλο, ομαδοποιώντας υποσύνολα της RE. Επιπλέον, η μηχανή θυμάται τι ταίριαξε σε κάθε group. Το group(0) συμβολίζει όλο το κείμενο που ταίριαξε. Για κάθε παρένθεση που ανοίγει -μετρώντας από τα αριστερά- ορίζονται και τα άλλα groups.: ([ +]?[0 9]+(\.[0 9]*)?)\s*([CF]) ^ ^ ^ 1 2 3 Αν ένα σετ παρενθέσεων έχει μπει μόνο για την εξυπηρέτηση του *, +,? κλπ και δεν θέλουμε τη συγκράτηση της τιμής του group, μπορούμε να χρησιμοποιήσουμε το (?:). Δείτε εδώ τη διαφορά από το προηγούμενο. Πόσα groups υπάρχουν τώρα; >>> import re >>> restr = r'([ +]?[0 9]+(?:\.[0 9]*)?)\s*([CF])' >>> rexp = re.compile(restr) >>> m = rexp.search('abcd12.23 F') 3/7
Ενότητα 3 1 Compiler Lecture s 1.0 documentation >>> m.groups() ('12.23', 'F') Παράδειγμα: βρείτε τη γάτα στην αρχή ή στο τέλος ενός string: >>> rexp = re.compile(r'^cat') >>> m = rexp.search('i see a cat') >>> m = rexp.search('category') <_sre.sre_match object; span=(0, 3), match='cat'> >>> m.group(0) 'cat' >>> rexp = re.compile(r'cat$') >>> m = rexp.search('in category') >>> m = rexp.search('black cat') >>> print m <_sre.sre_match object; span=(6, 9), match='cat'> >>> if m: print('') Για να ταιριάξουμε ένα άδειο string, θα μπορούσαμε (θεωρητικά -δεν χρειάζεται στην πράξη!) να χρησιμοποιήσουμε το '^$'. Παράδειγμα: κλάσεις χαρακτήρων με το []. Θυμηθείτε ότι μέσα στην κλάση, οι χαρακτήρες ελέγχου χάνουν τον ειδικό τους χαρακτήρα και το ^ σημαίνει κάτι διαφορετικό! >>> rexp = re.compile(r'gr[ae]y') >>> m = rexp.search('a gray cat') >>> if m: print('') >>> m = rexp.search('a grey cat') >>> if m: print('') Παράδειγμα: Βρείτε a που δεν ακολουθείται από b >>> rexp = re.compile(r'a[^b]') >>> m = rexp.search('ab') >>> m = rexp.search('acb') <_sre.sre_match object; span=(0, 2), match='ac'> >>> print(m.group(0)) ac >>> m = rexp.search('ba') Στο τελευταίο παράδειγμα βλέπουμε ότι ενώ το a δεν πρέπει να ακολουθείται από b, πρέπει όμως να ακολουθείται από κάτι άλλο! Παράδειγμα: Βρείτε το gray ή grey με εναλλαγή (alternation): 2 σωστοί και ένας λάθος τρόπος >>> rexp = re.compile(r'gray grey') >>> m = rexp.search('a gray cat') >>> if m: print('') >>> rexp = re.compile(r'gr(a e)y') >>> m = rexp.search('a gray cat') >>> if m: print('') 4/7
Ενότητα 3 1 Compiler Lecture s 1.0 documentation >>> rexp = re.compile(r'gra ey') >>> m = rexp.search('a gray cat') <_sre.sre_match object; span=(2, 5), match='gra'> >>> print(m.group(0)) gra >>> m = rexp.search('a grey cat') <_sre.sre_match object; span=(4, 6), match='ey'> >>> print(m.group(0)) ey Στο τελευταίο παράδειγμα έχουμε ταίριασμα, δεν είναι όμως αυτό που θέλουμε! Η χρήση των παρενθέσεων είναι αναγκαία για το σωστό ταίριασμα! Προσοχή! Η εναλλαγή στις μηχανές NFA, όταν ταιριάζουν δύο ή περισσότερες επιλογές, επιστρέφει την πρώτη, από αριστερά προς τα δεξιά κι όχι τη μεγαλύτερη σε μήκος!! Παράδειγμα: Γιατί πρέπει να προσέχουμε όταν γειτονεύουν *, + και.: >>> restr = r'^.*([0 9]+)' >>> rexp = re.compile(restr) >>> m = rexp.search('copyright 2003') >>> m.group(0) 'Copyright 2003' >>> m.group(1) '3' >>> Γιατί δεν μπήκε όλο το 2003 μέσα στο group(1); Το * είναι άπληστο και καταναλώνει όλο το string. Στη συνέχεια αποδεσμεύει το 3 για να επιτρέψει και στο + να πετύχει ταίριασμα. Η διαδικασία σταματά εδώ και ποτέ δεν δίνεται η ευκαιρία στο + να ταιριάξει όλο το 2003. Η παράθεση πολλαπλών * και + σε συνδυασμό με το ``.`` οδηγεί σχεδόν πάνοτε σε λάθος. Από την αποτυχία ταιριάσματος αυτού που θέλει ο χρήστης μέχρι την αποτυχία της ίδιας της μηχανής αναζήτησης! Έτσι, πρέπει πάντα να αποφεύγεται. Η μέθοδος match() Η μέθοδος match() λειτουργεί ακριβώς όπως η search(), με τη διαφορά ότι προσπαθεί να βρεί ταίριασμα μόνο από την αρχή του string (κι όχι οπουδήποτε μέσα στο string όπως η search). Η μέθοδος finditer() Δέχεται ως όρισμα ένα string και επιστρέφει ένα αντικείμενο iterator, το οποίο δίνει διαδοχικά όλα τα match objects για όλα τα σημεία ταιριάσματος μέσα στο string. Παράδειγμα: χρήση της τελείας. για να ταιριάξουμε οτιδήποτε (εκτός από newline! Για να ταιριάξετε και το newline στο re.compile() προσθέστε το επιπλέον όρισμα re.dotall) >>> rexp = re.compile(r'a.c') >>> mi = rexp.finditer('ab abc acc axc afc acb a\nc avc') >>> for m in mi: print(m.group(0)) abc acc axc afc avc Παράδειγμα: color ή colour? >>> rexp = re.compile(r'colou?r') >>> mi = rexp.finditer('some write colour instead of color') 5/7
>>> for m in mi: print(m.group(0)) colour color Ενότητα 3 1 Compiler Lecture s 1.0 documentation Παράδειγμα: Εύρεση λέξεων που αντιπροσωπεύουν σωστή ώρα σε 24ωρο format >>> rexp = re.compile(r'\b([01]?[0 9] 2[0 3]):[0 5][0 9]\b') >>> mi = rexp.finditer('4:24 3:56 25:02 12:45 9:62') >>> for m in mi: print(m.group(0)) 4:24 3:56 12:45 Η μέθοδος findall() Επιστρέφει όλα τα ταιριάσματα, αλλά ΠΡΟΣΟΧΗ!! Η επιστρεφόμενη τιμή δεν μοιάζει με εκείνες των προηγούμενων μεθόδων: Αν δεν υπάρχουν groups στην RE, επιστρέφει απλά μια λίστα με τα strings που ταίριαξαν. Αν υπάρχουν groups, επιστρέφει μόνο αυτά κι όχι το πλήρες ταίριασμα (δηλ. δεν σας δίνει το group(0)). Οταν τα groups είναι περισσότερα από ένα στην RE, κάθε στοιχείο της επιστρεφόμενης λίστας είναι ένα tuple με όλες τις τιμές των groups για το συγκεκριμένο ταίριασμα. Παράδειγμα: Ταίριασμα ονομάτων μεταβλητών: >>> rexp = re.compile(r'\b[a za z_][a za Z0 9_]*\b') = rexp.findall('var1 1var _var') ['var1', '_var'] Παράδειγμα: Ταίριασμα κειμένου μέσα σε.. >>> rexp = re.compile(r'"[^"]*"') = rexp.findall('"re" is a "better" name for "regular expressions"') ['"RE"', '"better"', '"regular expressions"'] Παράδειγμα: Λέξεις 5 κεφαλαίων από Α ώς Ζ (αγγλικά) >>> rexp = re.compile(r'\b[a Z]{5}\b') = rexp.findall('abcde aefghi AB ABCDEFGHIJ NNNNN') ['ABCDE', 'NNNNN'] Παράδειγμα: Βρείτε τις ετικέτες σε κώδικα HTML. Ένας λάθος (λόγω απληστίας του + που προσπαθεί να ταιριάξει όσο το δυνατόν περισσότερο κείμενο) και 2 σωστοί τρόποι (με κλάση χαρακτήρων και τη μη άπληστη μορφή του +?) >>> rexp = re.compile(r'<.+>') = rexp.findall('this <b>is</b> a <em>html</em> text') ['<b>is</b> a <em>html</em>'] >>> rexp = re.compile(r'<[^>]+>') = rexp.findall('this <b>is</b> a <em>html</em> text') ['<b>', '</b>', '<em>', '</em>'] >>> rexp = re.compile(r'<.+?>') = rexp.findall('this <b>is</b> a <em>html</em> text') ['<b>', '</b>', '<em>', '</em>'] 6/7
Ενότητα 3 1 Compiler Lecture s 1.0 documentation Δέχεται ως πρώτο όρισμα ένα string αντικατάστασης και ως δεύτερο ένα string αναζήτησης. Επιστρέφει ένα νέο string, όπου όλα τα σημεία του string αναζήτησης που ταιριάζουν με την RE έχουν αντικατασταθεί από το string αντικατάστασης. Παράδειγμα: απαλοιφή διπλών ίδιων συνεχόμενων λέξεων από ένα string (προσέξτε ότι το backreference \1 απαιτεί το 1ο όρισμα της sub να είναι raw string!) >>> rexp = re.compile(r'\b([a z]+)\s+\1\b', re.ignorecase) >>> s = rexp.sub(r'\1','this this is a a known fact.') >>> s 'This is a known fact.' 7/7