Κανονικές εκφράσεις (Regular expressions)

Μέχρι στιγμής διαβάζαμε αρχεία, αναζητούσαμε μοτίβα και εξαγάγαμε διάφορα κομμάτια γραμμών, που θεωρούσαμε ενδιαφέροντα. Χρησιμοποιήσαμε μεθόδους συμβολοσειρών, όπως split και find και χρησιμοποιούσαμε λίστες και διαμέριση συμβολοσειρών για να εξαγάγουμε τμήματα των γραμμών.

Αυτή η εργασία αναζήτησης και εξαγωγής είναι τόσο συνηθισμένη, που η Python έχει ένα πολύ ισχυρό άρθρωμα, που ονομάζεται κανονικές εκφράσεις, ή για συντομία regex, και χειρίζεται πολλές από αυτές τις εργασίες ιδιαίτερα έξυπνα. Ο λόγος που δεν έχουμε αναφερθεί στις κανονικές εκφράσεις νωρίτερα στο βιβλίο είναι επειδή, ενώ είναι πολύ ισχυρές, είναι λίγο περίπλοκες και η σύνταξή τους χρειάζεται μια κάποια εξοικείωση.

Οι κανονικές εκφράσεις είναι σχεδόν, από μόνες τους, μικρή γλώσσα προγραμματισμού για αναζήτηση και ανάλυση συμβολοσειρών. Στην πραγματικότητα, έχουν γραφτεί ολόκληρα βιβλία με θέμα τις κανονικές εκφράσεις. Σε αυτό το κεφάλαιο, θα καλύψουμε μόνο τα βασικά των κανονικών εκφράσεων. Για περισσότερες λεπτομέρειες σχετικά με τις κανονικές εκφράσεις, δείτε:

https://en.wikipedia.org/wiki/Regular_expression

https://docs.python.org/library/re.html

Το άρθρωμα κανονικών εκφράσεων re πρέπει να εισαχθεί στο πρόγραμμά σας για να μπορέσετε να τοη χρησιμοποιήσετε. Η απλούστερη χρήση του αρθρώματος κανονικών εκφράσεων είναι η συνάρτηση search(). Το παρακάτω πρόγραμμα δείχνει μια απλή χρήση της λειτουργίας αναζήτησης.

# Αναζητάμε τις γραμμές που περιέχουν τη λέξη 'From'
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    if re.search('From:', line):
        print(line)

# Code: http://www.py4e.com/code3/re01.py

Ανοίγουμε το αρχείο, με βρόχο διατρέχουμε κάθε γραμμή και χρησιμοποιούμε την κανονική έκφραση search() για να εκτυπώνουμε μόνο τις γραμμές που περιέχουν τη συμβολοσειρά “From:”. Αυτό το πρόγραμμα δεν χρησιμοποιεί την πραγματική ισχύ των κανονικών εκφράσεων, αφού θα μπορούσαμε να χρησιμοποιήσουμε εξίσου εύκολα το line.find() για να επιτύχουμε το ίδιο αποτέλεσμα.

Η ισχύς των κανονικών εκφράσεων αποκαλύπτεται όταν προσθέτουμε ειδικούς χαρακτήρες στη συμβολοσειρά αναζήτησης, που μας επιτρέπουν να ελέγχουμε με μεγαλύτερη ακρίβεια ποιες γραμμές ταιριάζουν με τη συμβολοσειρά. Η προσθήκη αυτών των ειδικών χαρακτήρων στην κανονική μας έκφραση μας επιτρέπει να κάνουμε πολύπλοκη αντιστοίχιση και εξαγωγή ενώ γράφουμε πολύ λίγο κώδικα.

Για παράδειγμα, ο χαρακτήρας περίφλεξης (^) χρησιμοποιείται σε κανονικές εκφράσεις για να ταιριάζει με την “αρχή” μιας γραμμής. Μπορούσαμε να αλλάξουμε το πρόγραμμά μας ώστε να εντοπίζει μόνο τις γραμμές στις οποίες το “From:” ήταν στην αρχή της γραμμής ως εξής:

# Αναζητάμε τις γραμμές που αρχίζουν με 'From'
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    if re.search('^From:', line):
        print(line)

# Code: http://www.py4e.com/code3/re02.py

Με αυτόν τον τρόπο θα εντοπίσουμε μόνο γραμμές που ξεκινούν με τη συμβολοσειρά “From:”. Αυτό είναι ένα ακόμη πολύ απλό παράδειγμα, που θα μπορούσαμε να είχαμε κάνει, ισοδύναμα, με τη μέθοδο startswith(), από τη βιβλιοθήκη συμβολοσειρών. Αλλά χρησιμεύει για να εμπεδώσουμε το γεγονός ότι οι κανονικές εκφράσεις περιέχουν ειδικούς χαρακτήρες ενεργειών, που μας δίνουν περισσότερο έλεγχο ως προς το τι θα ταιριάζει με την κανονική έκφραση.

Ταίριασμα χαρακτήρων σε κανονικές εκφράσεις

Υπάρχει ένα πλήθος άλλων ειδικών χαρακτήρων, που μας επιτρέπουν να δημιουργήσουμε ακόμη πιο ισχυρές κανονικές εκφράσεις. Ο πιο συχνά χρησιμοποιούμενος ειδικός χαρακτήρας είναι η τελεία, που ταιριάζει με οποιονδήποτε χαρακτήρα.

Στο ακόλουθο παράδειγμα, η κανονική έκφραση F..m: θα ταιριάζει με οποιαδήποτε από τις συμβολοσειρές “From:”, “Fxxm:”, “F12m:” ή “F!@m:” καθώς οι χαρακτήρες τελείας στην τυπική έκφραση ταιριάζουν με οποιονδήποτε χαρακτήρα.

# Αναζητάμε τις γραμμές που αρχίζουν με 'F', ακολουθούμενο
# από 2 χαρακτήρες, ακολουθούμενους από 'm:'
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    if re.search('^F..m:', line):
        print(line)

# Code: http://www.py4e.com/code3/re03.py

Το σύμβολο της τελείας είναι ιδιαίτερα ισχυρό όταν συνδυάζεται με τη δυνατότητα να υποδεικνύει ότι ένας χαρακτήρας μπορεί να επαναληφθεί όσες φορές χρειαστεί, χρησιμοποιώντας τους χαρακτήρες * ή + στην κανονική σας έκφραση. Αυτοί οι ειδικοί χαρακτήρες σημαίνουν ότι αντί να ταιριάζουν με έναν χαρακτήρα στη συμβολοσειρά αναζήτησης, ταιριάζουν με κανέναν ή περισσότερους χαρακτήρες (στην περίπτωση του αστερίσκου) ή έναν ή περισσότερους χαρακτήρες (στην περίπτωση του συμβόλου συν).

Μπορούμε να περιορίσουμε περαιτέρω τις γραμμές που ταιριάζουν χρησιμοποιώντας έναν επαναλαμβανόμενο χαρακτήρα μπαλαντέρ όπως στο ακόλουθο παράδειγμα:

# Αναζητάμε τις γραμμές που αρχίζουν με 'From'
# και περιέχουν ένα σύμβολο at
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    if re.search('^From:.+@', line):
        print(line)

# Code: http://www.py4e.com/code3/re04.py

Η συμβολοσειρά αναζήτησης ^From:.+@ θα ταιριάξει με επιτυχία με τις γραμμές που ξεκινούν με “From:”, ακολουθούμενο από έναν ή περισσότερους χαρακτήρες (.+), ακολουθούμενων από ένα σύμβολο at. Άρα αυτό θα ταιριάξει με την ακόλουθη γραμμή:

From: [email protected]

Μπορούμε να πούμε ότι ο χαρακτήρας μπαλαντέρ .+ επεκτείνεται για να ταιριάζει με όλους τους χαρακτήρες μεταξύ του χαρακτήρα άνω και κάτω τελείας και του συμβόλου at.

From:.+@

Είναι καλό να σκεφτόμαστε τους χαρακτήρες συν και αστερίσκο ως “άπληστους”. Για παράδειγμα, η ακόλουθη συμβολοσειρά θα ταιριάζει με το τελευταίο στο σύμβολο at της συμβολοσειράς καθώς το .+ ωθεί προς τα έξω, όπως φαίνεται παρακάτω:

From: [email protected], [email protected], and cwen @iupui.edu

Είναι δυνατόν να πείτε σε έναν αστερίσκο ή ένα σύμβολο συν να μην είναι τόσο “άπληστο” προσθέτοντας έναν επιπλέον χαρακτήρα. Δείτε τη λεπτομερή τεκμηρίωση για πληροφορίες σχετικά με την απενεργοποίηση της άπληστης συμπεριφοράς.

Εξαγωγή δεδομένων με χρήση κανονικών εκφράσεων

Εάν θέλουμε να εξαγάγουμε δεδομένα από μια συμβολοσειρά στην Python, μπορούμε να χρησιμοποιήσουμε τη μέθοδο findall(), για να εξαγάγουμε όλες τις υποσυμβολοσειρές που ταιριάζουν με μια κανονική έκφραση. Ας χρησιμοποιήσουμε ως παράδειγμα το να εξαγάγουμε οτιδήποτε μοιάζει με διεύθυνση email, από οποιαδήποτε γραμμή, ανεξαρτήτως μορφής. Για παράδειγμα, θέλουμε να τραβήξουμε τις διευθύνσεις email από καθεμία από τις ακόλουθες γραμμές:

From [email protected] Sat Jan  5 09:14:16 2008
Return-Path: <[email protected]>
          for <[email protected]>;
Received: (from apache@localhost)
Author: [email protected]

Δεν θέλουμε να γράψουμε κώδικα για κάθε έναν από τους τύπους γραμμών, χωρίζοντας και τεμαχίζοντας διαφορετικά για κάθε γραμμή. Το πρόγραμμα που ακολουθεί χρησιμοποιεί το findall() για να βρει τις γραμμές με διευθύνσεις email και να εξαγάγει μία ή περισσότερες διευθύνσεις από καθεμία από αυτές τις γραμμές.

import re
s = 'A message from [email protected] to [email protected] about meeting @2PM'
lst = re.findall('\S+@\S+', s)
print(lst)

# Code: http://www.py4e.com/code3/re05.py

Η μέθοδος findall() αναζητά τη συμβολοσειρά στο δεύτερο όρισμα και επιστρέφει μια λίστα με όλες τις συμβολοσειρές που μοιάζουν με διευθύνσεις email. Χρησιμοποιούμε μια ακολουθία δύο χαρακτήρων που ταιριάζει με έναν μη λευκό χαρακτήρα (\S).

Η έξοδος του προγράμματος θα είναι:

['[email protected]', '[email protected]']

Μεταφράζοντας της κανονικής έκφρασης, λέμε, αναζητούμε υποσυμβολοσειρές που έχουν τουλάχιστον έναν μη λευκό χαρακτήρα, ακολουθούμενο από ένα σύμβολο at, ακολουθούμενο από τουλάχιστον έναν ακόμη μη λευκό χαρακτήρα. Το \S+ ταιριάζει με όσο το δυνατόν περισσότερους μη κενούς χαρακτήρες.

Η κανονική έκφραση θα ταιριάξει δύο φορές ([email protected] και [email protected]), αλλά δεν θα ταιριάξει με τη συμβολοσειρά “@2PM” επειδή δεν υπάρχουν μη κενοί χαρακτήρες πριν από το σύμβολο at. Μπορούμε να χρησιμοποιήσουμε αυτήν την κανονική έκφραση σε ένα πρόγραμμα για να διαβάσουμε όλες τις γραμμές ενός αρχείου και να εκτυπώσουμε οτιδήποτε μοιάζει με διεύθυνση email ως εξής:

# Αναζητάμε τις γραμμές που περιέχουν ένα σύμβολο at μεταξύ χαρακτήρων
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    x = re.findall('\S+@\S+', line)
    if len(x) > 0:
        print(x)

# Code: http://www.py4e.com/code3/re06.py

Διαβάζουμε κάθε γραμμή και μετά εξάγουμε όλες τις υποσυμβολοσειρές που ταιριάζουν με την κανονική μας έκφραση. Μιας και το findall() επιστρέφει μια λίστα, απλώς ελέγχουμε εάν ο αριθμός των στοιχείων στη λίστα που επιστρέφεται είναι μεγαλύτερος από το μηδέν για να εκτυπώσουμε μόνο γραμμές όπου βρήκαμε τουλάχιστον μία υποσυμβολοσειρά που μοιάζει με διεύθυνση email.

Αν τρέξουμε το πρόγραμμα στο mbox-short.txt θα έχουμε την ακόλουθη έξοδο:

...
['<[email protected]>;']
['<[email protected]>;']
['apache@localhost)']
['[email protected];']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']

Ορισμένες από τις διευθύνσεις ηλεκτρονικού ταχυδρομείου μας έχουν λανθασμένους χαρακτήρες, όπως “<” ή “;” στην αρχή ή στο τέλος. Ας δηλώσουμε ότι μας ενδιαφέρει μόνο το τμήμα της συμβολοσειράς που αρχίζει και τελειώνει με ένα γράμμα ή έναν αριθμό.

Για να το κάνουμε αυτό, χρησιμοποιούμε ένα άλλο χαρακτήρα των κανονικών εκφράσεων. Οι αγκύλες χρησιμοποιούνται για να υποδείξουν ένα σύνολο πολλών αποδεκτών χαρακτήρων. Κατά μία έννοια, το \S ζητά να ταιριάζει με το σύνολο των “μη λευκών χαρακτήρων”. Τώρα θα είμαστε λίγο πιο σαφείς ως προς τους χαρακτήρες που θα ταιριάξουμε.

Εδώ είναι η νέα μας κανονική έκφραση:

[a-zA-Z0-9]\S*@\S*[a-zA-Z]

Αυτό γίνεται λίγο περίπλοκο και μπορείτε να αρχίσετε να βλέπετε γιατί είπαμε ότι οι κανονικές εκφράσεις αποτελούν μια ξεχωριστή, μικρή γλώσσα. Μεταφράζοντας αυτήν την κανονική έκφραση, αναζητούμε υποσυμβολοσειρές που ξεκινούν με ένα μόνο πεζό γράμμα, κεφαλαίο γράμμα ή αριθμό “[a-zA-Z0-9]”, ακολουθούμενο από κανέναν ή περισσότερους μη λευκούς χαρακτήρες (\S *), ακολουθούμενων από ένα σύμβολο at, ακολουθούμενο από κανέναν ή περισσότερους μη λευκούς χαρακτήρες (\S*), ακολουθούμενων από ένα κεφαλαίο ή πεζό γράμμα. Σημειώστε ότι αλλάξαμε από + σε * για να υποδείξουμε κανέναν ή περισσότερους μη λευκούς χαρακτήρες, καθώς το [a-zA-Z0-9] είναι ήδη ένας μη κενός χαρακτήρας. Θυμηθείτε ότι το * ή + ισχύει για τον μεμονωμένο χαρακτήρα που βρίσκεται ακριβώς στα αριστερά του συν ή του αστερίσκου.

Εάν χρησιμοποιήσουμε αυτήν την έκφραση στο πρόγραμμά μας, τα δεδομένα που προκύπτουν είναι πολύ πιο καθαρά:

# Αναζητάμε τις γραμμές που περιέχουν ένα σύμβολο at μεταξύ χαρακτήρων.
# Οι χαρακτήρες πρέπει να είναι γράμματα ή αριθμοί
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    x = re.findall('[a-zA-Z0-9]\S*@\S*[a-zA-Z]', line)
    if len(x) > 0:
        print(x)

# Code: http://www.py4e.com/code3/re07.py
...
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['apache@localhost']

Παρατηρήστε ότι στις γραμμές [email protected], η κανονική μας έκφραση απάλειψε δύο γράμματα στο τέλος της συμβολοσειράς (“>;”). Αυτό συνέβει επειδή προσθέτοντας το [a-zA-Z] στο τέλος της κανονικής μας έκφρασης, απαιτούμε ότι οποιαδήποτε συμβολοσειρά βρίσκει ο αναλυτής κανονικής έκφρασης πρέπει να τελειώνει με ένα γράμμα. Έτσι, όταν βλέπει το “>” στο τέλος του “sakaiproject.org>;” απλά σταματά στο τελευταίο “ταιριαστό” γράμμα που βρήκε (δηλαδή, το “g” ήταν το τελευταίο καλό ταίριασμα).

Σημειώστε επίσης ότι η έξοδος του προγράμματος είναι μια λίστα Python που έχει μια συμβολοσειρά ως μοναδικό στοιχείο στη λίστα.

Συνδυασμός αναζήτησης και εξαγωγής

Αν θέλουμε να βρούμε αριθμούς σε γραμμές που ξεκινούν με τη συμβολοσειρά “X-”, όπως:

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000

δεν θέλουμε απλώς αριθμούς κινητής υποδιαστολής από οποιαδήποτε γραμμή. Θέλουμε να εξαγάγουμε αριθμούς μόνο από γραμμές που έχουν την παραπάνω σύνταξη.

Μπορούμε να κατασκευάσουμε την ακόλουθη κανονική έκφραση για να επιλέξουμε τις γραμμές:

^X-.*: [0-9.]+

Μεταφράζοντάς το, λέμε, θέλουμε γραμμές που ξεκινούν με X-, ακολουθούμενο από κανέναν ή περισσότερους χαρακτήρες (.*), ακολουθούμενων από άνω και κάτω τελεία (:) και μετά ένα κενό. Μετά το κενό αναζητούμε έναν ή περισσότερους χαρακτήρες που είναι είτε ψηφίο (0-9) είτε τελεία [0-9.]+. Σημειώστε ότι μέσα στις αγκύλες, η τελεία ταιριάζει με μια πραγματική τελεία (δηλαδή, δεν είναι χαρακτήρας μπαλαντέρ μεταξύ των αγκύλων).

Αυτή είναι μια πολύ αυστηρή έκφραση, που θα ταιριάξει μόνο με τις γραμμές που μας ενδιαφέρουν, ως εξής:

# Αναζητάμε τις γραμμές που ξεκινούν με `X`,
# ακολουθούμενο από μη λευκούς χαρακτήρες,
# ":", ακολουθούμενο από ένα κενό και
# οποιονδήποτε αριθμό.
# Ο αριθμός μπορεί να περιλαμβάνει υποδιαστολή.

import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    if re.search('^X\S*: [0-9.]+', line):
        print(line)

# Code: http://www.py4e.com/code3/re10.py

Όταν εκτελούμε το πρόγραμμα, βλέπουμε τα δεδομένα, όμορφα φιλτραρισμένα, για να εμφανιστούν μόνο τις γραμμές που αναζητούμε.

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
X-DSPAM-Confidence: 0.6178
X-DSPAM-Probability: 0.0000
...

Αλλά τώρα πρέπει να λύσουμε άλλο ένα πρόβλημα, της εξαγωγής των αριθμών. Αν και θα ήταν αρκετά απλό να χρησιμοποιήσουμε το split, μπορούμε να χρησιμοποιήσουμε μια άλλη δυνατότητα κανονικών εκφράσεων, για αναζήτηση και ανάλυση της γραμμής, ταυτόχρονα.

Οι παρενθέσεις είναι άλλος ένας ειδικός χαρακτήρας των κανονικών εκφράσεων. Όταν προσθέτετε παρενθέσεις σε μια κανονική έκφραση, αυτές αγνοούνται όταν ταιριάζουν με τη συμβολοσειρά. Αλλά όταν χρησιμοποιείτε findall(), οι παρενθέσεις υποδεικνύουν ότι, ενώ θέλετε να ταιριάζει ολόκληρη η έκφραση, σας ενδιαφέρει να εξαγάγετε μόνο το τμήμα της υποσυμβολοσειράς, που ταιριάζει με την κανονική έκφραση που περιέχεται στις παρενθέσεις.

Κάνουμε λοιπόν την εξής αλλαγή στο πρόγραμμά μας:

# Αναζητάμε τις γραμμές που ξεκινούν με `X`,
# ακολουθούμενο από μη λευκούς χαρακτήρες,
# ":", ακολουθούμενο από ένα κενό και
# οποιονδήποτε αριθμό.
# Ο αριθμός μπορεί να περιλαμβάνει υποδιαστολή.

import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    x = re.findall('^X\S*: ([0-9.]+)', line)
    if len(x) > 0:
        print(x)

# Code: http://www.py4e.com/code3/re11.py

Αντί να καλέσουμε τη search(), προσθέτουμε παρενθέσεις γύρω από το τμήμα της κανονικής έκφρασης που αντιπροσωπεύει τον αριθμό κινητής υποδιαστολής, για να υποδείξουμε ότι θέλουμε το findall() να μας δώσει πίσω μόνο το τμήμα αριθμού κινητής υποδιαστολής, της συμβολοσειράς, που ταιριάζει .

Η έξοδος από αυτό το πρόγραμμα είναι η εξής:

['0.8475']
['0.0000']
['0.6178']
['0.0000']
['0.6961']
['0.0000']
...

Οι αριθμοί εξακολουθούν να βρίσκονται σε μια λίστα και να πρέπει να μετατραπούν από συμβολοσειρές σε κινητή υποδιαστολή, αλλά χρησιμοποιήσαμε τη δύναμη των κανονικών εκφράσεων για αναζήτηση και εξαγωγή των πληροφοριών που θωρούμε ενδιαφέρουσες.

Ως ένα άλλο παράδειγμα αυτής της τεχνικής, αν κοιτάξετε το αρχείο, υπάρχει ένας αριθμός γραμμών της μορφής:

Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772

Εάν θέλαμε να εξαγάγουμε όλους τους αριθμούς αναθεώρησης (τον ακέραιο αριθμό στο τέλος αυτών των γραμμών) χρησιμοποιώντας την ίδια τεχνική όπως παραπάνω, θα μπορούσαμε να γράψουμε το ακόλουθο πρόγραμμα:

# Αναζητάμε τις γραμμές που ξεκινούν με 'Details: rev='
# ακολουθούμενο από ψηφία.
# Στη συνέχεια εκτυπώνουμε τον αριθμό, αν βρεθεί.
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    x = re.findall('^Details:.*rev=([0-9]+)', line)
    if len(x) > 0:
        print(x)

# Code: http://www.py4e.com/code3/re12.py

Μεταφράζοντας την κανονική μας έκφραση, αναζητούμε γραμμές που ξεκινούν με Details:, ακολουθούμενο από οποιονδήποτε αριθμό χαρακτήρων (.*), ακολουθούμενων από rev= και μετά από ένα ή περισσότερα ψηφία. Θέλουμε να βρούμε γραμμές που ταιριάζουν με ολόκληρη την έκφραση, αλλά θέλουμε να εξαγάγουμε μόνο τον ακέραιο αριθμό στο τέλος της γραμμής, επομένως περιβάλλουμε το [0-9]+ με παρενθέσεις.

Όταν εκτελούμε το πρόγραμμα, έχουμε την ακόλουθη έξοδο:

['39772']
['39771']
['39770']
['39769']
...

Θυμηθείτε ότι το [0-9]+ είναι “άπληστο” και προσπαθεί να δημιουργήσει όσο το δυνατόν μεγαλύτερη σειρά ψηφίων πριν εξαγάγει αυτά τα ψηφία. Αυτή η “άπληστη” συμπεριφορά είναι ο λόγος που παίρνουμε και τα πέντε ψηφία για κάθε αριθμό. Η μονάδα κανονικής έκφρασης επεκτείνεται και προς τις δύο κατευθύνσεις μέχρι να συναντήσει ένα μη ψηφίο ή την αρχή ή το τέλος μιας γραμμής.

Τώρα μπορούμε να χρησιμοποιήσουμε κανονικές εκφράσεις για να επαναλάβουμε μια άσκηση από προηγούμενη ενότητα του βιβλίου, όπου μας ενδιέφερε η ώρα της ημέρας κάθε μηνύματος αλληλογραφίας. Αναζητούσαμε γραμμές της μορφής:

From [email protected] Sat Jan  5 09:14:16 2008

και θέλαμε να εξαγάγουμε την ώρα της ημέρας από αυτές τις γραμμές. Προηγουμένως το υλοποιήσαμς με δύο κλήσεις του split. Πρώτα η γραμμή χωρίστηκε σε λέξεις και μετά βγάλαμε την πέμπτη λέξη και τη χωρίσαμε ξανά στον χαρακτήρα άνω και κάτω τελείας, για να βγάλουμε τους δύο χαρακτήρες που μας ενδιέφεραν.

Ενώ αυτό λειτούργησε, στην πραγματικότητα οδηγεί σε αρκετά εύθραυστο κώδικα που υποθέτει ότι οι γραμμές είναι σωστά διαμορφωμένες. Εάν επρόκειτο να προσθέσετε έλεγχο σφαλμάτων (ή ένα μεγάλο μπλοκ try/except), για να διασφαλίσετε ότι το πρόγραμμά σας δεν αποτυγχάνει όταν αντιμετωπίσει εσφαλμένα μορφοποιημένες γραμμές, ο κώδικας θα μεταφερόταν σε 10-15 γραμμές κώδικα, που θα ήταν αρκετά δύσκολο να διαβαστούν.

Μπορούμε να το κάνουμε αυτό με πολύ πιο απλό τρόπο, με την ακόλουθη κανονική έκφραση:

^From .* [0-9][0-9]:

Η μετάφραση αυτής της κανονικής έκφρασης είναι ότι αναζητούμε γραμμές που ξεκινούν με From (προσέξτε το κενό διάστημα), ακολουθούμενο από οποιονδήποτε αριθμό χαρακτήρων (.*), ακολουθούμενων από ένα κενό, ακολουθούμενο από δύο ψηφία [0 -9][0-9], ακολουθούμενα από χαρακτήρα άνω και κάτω τελείας. Αυτός είναι ο ορισμός των ειδών γραμμών που αναζητούμε.

Για να εξάγουμε μόνο την ώρα χρησιμοποιώντας το findall(), προσθέτουμε παρενθέσεις γύρω από τα δύο ψηφία ως εξής:

^From .* ([0-9][0-9]):

Αυτό έχει ως αποτέλεσμα το ακόλουθο πρόγραμμα:

# Αναζητάμε τις γραμμές που ξεκινούν με 'From ' και ένα σύνολο χαρακτήρων
# ακολουθούμενων από δύο ψηφία, ακολουθούμενα από ':'
# Στη συνέχεια εκτυπώνουμε τα ψηφία, εάν βρεθούν
import re
hand = open('mbox-short.txt')
for line in hand:
    line = line.rstrip()
    x = re.findall('^From .* ([0-9][0-9]):', line)
    if len(x) > 0: print(x)

# Code: http://www.py4e.com/code3/re13.py

Όταν το πρόγραμμα εκτελείται, παράγει την ακόλουθη έξοδο:

['09']
['18']
['16']
['15']
...

Χαρακτήρας διαφυγής

Όταν χρησιμοποιούμε ειδικούς χαρακτήρες σε κανονικές εκφράσεις, όχι ως σύμβολα αλλά με την πραγματική τους αξία, για να ταιριάξουμε την αρχή ή το τέλος μιας γραμμής ή να καθορίσουμε μπαλαντέρ, χρειαζόμαστε έναν τρόπο για να υποδείξουμε ότι αυτοί οι χαρακτήρες είναι “κανονικοί” και θέλουμε να ταιριάξουμε τον πραγματικό χαρακτήρα, όπως ένα σύμβολο του δολαρίου ή έναν χαρακτήρα περίφλεξης (^) .

Μπορούμε να υποδείξουμε πως θέλουμε απλώς να ταιριάξουμε έναν χαρακτήρα προσθέτοντας ως πρόθεμα αυτού του χαρακτήρα μια ανάστροφη κάθετο. Για παράδειγμα, μπορούμε να βρούμε χρηματικά ποσά με την ακόλουθη κανονική έκφραση.

import re
x = 'We just received $10.00 for cookies.'
y = re.findall('\$[0-9.]+',x)

Εφόσον δίνουμε το πρόθεμα της ανάστροφης κάθετου πριν το σύμβολο του δολαρίου, η κανονική έκφραση το ταιριάζει με το σύμβολο του δολαρίου, στη συμβολοσειρά εισόδου, αντί να το ταιριάξει με το “τέλος γραμμής” και η υπόλοιπη τυπική έκφραση ταιριάζει με ένα ή περισσότερα ψηφία ή τον χαρακτήρα τελείας.

Σημείωση: Μέσα στις αγκύλες, οι χαρακτήρες δεν είναι “ειδικοί”. Έτσι, όταν λέμε [0-9.], σημαίνει πραγματικά ψηφία ή τελεία. Έξω από αγκύλες, η τελεία είναι ο χαρακτήρας “μπαλαντέρ” και ταιριάζει με οποιονδήποτε χαρακτήρα. Μέσα σε αγκύλες, η τελεία είναι τελεία.

Περίληψη

Αν και αυτά που αναφέραμε αγκίζουν μόνο την επιφάνεια της ένοιας των κανονικών εκφράσεων, μάθαμε λίγα πράγματα για τη γλώσσα των κανονικών εκφράσεων. Είναι συμβολοσειρές αναζήτησης με ειδικούς χαρακτήρες μέσα τους που μεταφέρουν τις επιθυμίες σας στο σύστημα κανονικής έκφρασης, ως προς το τι πρέπει να “ταιριάξει” και τι να εξάγεται από τις αντιστοιχισμένες συμβολοσειρές. Ακολουθούν μερικοί από αυτούς τους ειδικούς χαρακτήρες και τις ακολουθίες χαρακτήρων:

^ Ταιριάζει την αρχή μιας γραμμής.

$ Ταιριάζει το τέλος μιας γραμμής

. Ταιριάζει οποιονδήποτε χαρακτήρα (ένα μπαλαντέρ).

\s Ταιριάζει ένα λευκό χαρακτήρα (μη ορατό χαρακτήρα).

\S Ταιριάζει ένα μη λευκό χαρακτήρα (ορατό χαρακτήρα) (αντίθετο του \s).

* Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες καμία ή περισσότερες φορές.

*? Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες καμία ή περισσότερες φορές “μη-άπληστα”.

+ Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες μία ή περισσότερες φορές.

+? Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες μία ή περισσότερες φορές “μη-άπληστα”.

? Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες καμία ή μία φορά.

?? Επαναλαμβάνει τον/τους αμέσως προηγούμενο/ους χαρακτήρα/ες καμία ή μία φορά “μη-άπληστα”.

[aeiou] Ταιριάζει έναν μόνο χαρακτήρα από το δοθέν σύνολο. Στο παράδειγμα, θα ταιριάξει κάποιο από τα “a”, “e”, “i”, “o” ή “u”, αλλά όχι κάποιον άλλο χαρακτήρα.

[a-z0-9] Μπορείτε να καθορίσετε εύρος χαρακτήρων χρησιμοποιώντας το σύμβολο μείον. Αυτό το παράδειγμα αντιπροσωπεύει έναν μεμονωμένο χαρακτήρα, που πρέπει να είναι πεζός ή ψηφίο.

[^A-Za-z] Όταν ο πρώτος χαρακτήρας του συνόλου είναι η περίφλεξη, αντιστρέφει τη λογική. Αυτό το παράδειγμα ταιριάζει με έναν μεμονωμένο χαρακτήρα που είναι οτιδήποτε εκτός από ένα κεφαλαίο ή πεζό γράμμα.

( ) Όταν προστίθενται παρενθέσεις σε μια κανονική έκφραση, αγνοούνται από την αντιστοίχιση, αλλά σας επιτρέπουν, όταν χρησιμοποιείτε το findall(), να εξαγάγετε ένα συγκεκριμένο υποσύνολο της αντιστοιχισμένης συμβολοσειράς αντί ολόκληρης της συμβολοσειράς .

\b Ταιριάζει με την κενή συμβολοσειρά, αλλά μόνο στην αρχή ή στο τέλος μιας λέξης.

\B Ταιριάζει με την κενή συμβολοσειρά, αλλά όχι στην αρχή ή στο τέλος μιας λέξης.

\d Ταιριάζει με οποιοδήποτε δεκαδικό ψηφίο. Ισοδύναμο με το σύνολο [0-9].

\D Ταιριάζει με οποιονδήποτε χαρακτήρα, μη-ψήφιο. Ισοδύναμο με το σύνολο [^0-9].

Μπόνους ενότητα για χρήστες Unix / Linux

Η υποστήριξη για αναζήτηση αρχείων με χρήση κανονικών εκφράσεων ενσωματώθηκε στο λειτουργικό σύστημα Unix από τη δεκαετία του 1960 και είναι διαθέσιμη σε όλες σχεδόν τις γλώσσες προγραμματισμού με τη μία ή την άλλη μορφή.

Στην πραγματικότητα, υπάρχει ένα πρόγραμμα γραμμής εντολών, ενσωματωμένο στο Unix που ονομάζεται grep (Generalized Regular Expression Parser) που κάνει σχεδόν ότι και τα παραδείγματα με τη search() σε αυτό το κεφάλαιο. Επομένως, εάν έχετε σύστημα Macintosh ή Linux, μπορείτε να δοκιμάσετε τις ακόλουθες εντολές στο παράθυρο της γραμμής εντολών σας.

$ grep '^From:' mbox-short.txt
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]

Αυτό λέει στο grep να σας δείξει τις γραμμές που ξεκινούν με τη συμβολοσειρά “From:” στο αρχείο mbox-short.txt. Εάν πειραματιστείτε λίγο με την εντολή grep και διαβάσετε την τεκμηρίωση για το grep, θα βρείτε κάποιες ανεπαίσθητες διαφορές μεταξύ της υποστήριξης κανονικών εκφράσεων στην Python και της υποστήριξης κανονικών εκφράσεων στο grep. Για παράδειγμα, το grep δεν υποστηρίζει τον μη λευκό χαρακτήρα \S, επομένως θα χρειαστεί να χρησιμοποιήσετε τον ελαφρώς πιο περίπλοκο συμβολισμό συνόλου [^[:space:]], που σημαίνει απλώς αντιστοίχιση ενός μη λευκού χαρακτήρα.

Εκσφαλμάτωση

Η Python έχει κάποια απλή και στοιχειώδη ενσωματωμένη τεκμηρίωση που μπορεί να είναι αρκετά χρήσιμη εάν χρειάζεστε ένα γρήγορο φρασκάρισμα της μνήμης σας, σχετικά με το ακριβές όνομα μιας συγκεκριμένης μεθόδου. Αυτή η τεκμηρίωση μπορεί να προβληθεί στον διερμηνέα Python σε διαδραστική λειτουργία.

Μπορείτε να εμφανίσετε ένα διαδραστικό σύστημα βοήθειας χρησιμοποιώντας το help().

>>> help()

help> modules

Εάν γνωρίζετε ποιο άρθρωμα θέλετε να χρησιμοποιήσετε, μπορείτε να χρησιμοποιήσετε την εντολή dir() για να βρείτε τις μεθόδους στη λειτουργική μονάδα ως εξής:

>>> import re
>>> dir(re)
[.. 'compile', 'copy_reg', 'error', 'escape', 'findall',
'finditer', 'match', 'purge', 'search', 'split', 'sre_compile',
'sre_parse', 'sub', 'subn', 'sys', 'template']

Μπορείτε επίσης να λάβετε μια μικρή τεκμηρίωσης για μια συγκεκριμένη μέθοδο χρησιμοποιώντας την εντολή dir.

>>> help (re.search)
Help on function search in module re:

search(pattern, string, flags=0)
    Scan through string looking for a match to the pattern, returning
    a match object, or None if no match was found.
>>>

Η ενσωματωμένη τεκμηρίωση δεν είναι ιδιαίτερα εκτενής, αλλά μπορεί να είναι χρήσιμη όταν βιάζεστε ή δεν έχετε πρόσβαση σε πρόγραμμα περιήγησης ιστού ή μηχανή αναζήτησης.

Γλωσσάριο

brittle code - εύθραυστος κώδικας
Κωδικός που λειτουργεί όταν τα δεδομένα εισόδου είναι σε συγκεκριμένη μορφή, αλλά είναι επιρρεπής σε σφάλματα εκτέλεσης, εάν υπάρχει κάποια απόκλιση από τη σωστή μορφή. Αυτό το ονομάζουμε “εύθραυστος κώδικας” γιατί “σπάει” εύκολα.
grep
Μια εντολή διαθέσιμη στα περισσότερα συστήματα Unix, που αναζητά μέσα σε αρχεία κειμένου γραμμές, που ταιριάζουν με κανονικές εκφράσεις. Το όνομα της εντολής σημαίνει “Generalized Regular Expression Parser - Αναλυτής Γενικοποιημένης Κανονική Έκφραση”.
άπληστο ταίριασμα
Η έννοια ότι οι χαρακτήρες + και * σε μια κανονική έκφραση επεκτείνονται προς τα έξω για να ταιριάζουν με τη μεγαλύτερη δυνατή συμβολοσειρά.
κανονική έκφραση - regular expression
Μια γλώσσα για την έκφραση πιο σύνθετων συμβολοσειρών αναζήτησης. Μια κανονική έκφραση μπορεί να περιέχει ειδικούς χαρακτήρες, που υποδεικνύουν ότι μια αναζήτηση ταιριάζει μόνο στην αρχή ή στο τέλος μιας γραμμής ή πολλές άλλες παρόμοιες δυνατότητες.
μπαλαντέρ - wild card
Ένας ειδικός χαρακτήρας, που ταιριάζει με κάθε χαρακτήρα. Στις κανονικές εκφράσεις ο χαρακτήρας μπαλαντέρ είναι η τελεία.

Ασκήσεις

Άσκηση 1: Γράψτε ένα απλό πρόγραμμα για την προσομοίωση της λειτουργίας της εντολής grep στο Unix. Ζητήστε από τον χρήστη να εισαγάγει μια κανονική έκφραση και μετρήστε τον αριθμό των γραμμών του αρχείου mbox.txt, που ταιριάζουν με την κανονική έκφραση:

$ python grep.py
Εισαγάγετε μια κανονική έκφραση: ^Author
mbox.txt had 1798 lines that matched ^Author

$ python grep.py
Εισαγάγετε μια κανονική έκφραση: ^X-
mbox.txt had 14368 lines that matched ^X-

$ python grep.py
Εισαγάγετε μια κανονική έκφραση: java$
mbox.txt had 4175 lines that matched java$

Άσκηση 2: Γράψτε ένα πρόγραμμα για να αναζητήσετε γραμμές της μορφής:

New Revision: 39772

** Εξάγετε τον αριθμό, από κάθε γραμμή, χρησιμοποιώντας μια κανονική έκφραση και τη μέθοδο findall(). Υπολογίστε τον μέσο όρο των αριθμών και εκτυπώστε τον μέσο όρο ως ακέραιο.**

Εισαγάγετε το αρχείο:mbox.txt
38549

Εισαγάγετε το αρχείο:mbox-short.txt
39756

Αν εντοπίσετε κάποιο λάθος σε αυτό το βιβλίο μην διστάσετε να μου στείλετε τη διόρθωση στο Github.