Creative Commons Attribution-ShareAlike 4.0 International License

Κανονικές εκφράσεις και Python

Στην Python (αλλά και σε πολλές άλλες γλώσσες προγραμματισμού) μπορούμε να φανταστούμε μια κανονική έκφραση ως μια μίνι-γλώσσα, η οποία χρησιμοποιείται για τη συγκρότηση μιας μηχανής ταιριάσματος (matching engine). Η μηχανή επεξεργάζεται το κείμενο εισόδου σύμφωνα με τις οδηγίες της κανονικής έκφρασης και επιστρέφει

  • αν υπάρχει ταίριασμα (αληθές/ψευδές)

  • σε ποια σημεία (θέση στο κείμενο)

  • ποιο κομμάτι του κειμένου ταίριαξε με την προδιαγραφή

  • επίσης, η μηχανή μπορεί να αντικαταστήσει τα κομμάτια που ταίριαξαν, με άλλο κείμενο, αν αυτό ζητηθεί

Η κανονική έκφραση περνά από μια διαδικασία μεταγλώττισης (compilation) και κατασκευάζεται ένα σύνολο οδηγιών για τη μηχανή ταιριάσματος. Η βασική μηχανή είναι γραμμένη σε C και πολύ γρήγορη σε εκτέλεση.

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

Χρήση κανονικών εκφράσεων στην Python

Στην Python η υλοποίηση των κανονικών εκφράσεων είναι αντικειμενοστρεφής.

Η υποστήριξη των regular expressions υπάρχει ενσωματωμένη στη γλώσσα, στο module re:

import re

Το import αυτό γίνεται μια φορά στην αρχή του προγράμματός σας και επιτρέπει να χρησιμοποιήσετε όλες τις μεθόδους, τις σταθερές και τα αντικείμενα του module re.

Στη συνέχεια, μπορείτε να φτιάξετε «μηχανές ταιριάσματος» με τη βοήθεια της συνάρτησης re.compile(), π.χ.:

rexp = re.compile('^cat')

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

m = rexp.search('category')

Η μέθοδος search() είναι η βασική μέθοδος της μηχανής αναζήτησης rexp και αναζητά το πρώτο σχέδιο ταιριάσματος της αντίστοιχης κανονικής έκφρασης (^cat) οπουδήποτε μέσα στο string category. Η επιστρεφόμενη τιμή στη μεταβλητή m περιέχει ένα «αντικείμενο ταιριάσματος» με πληροφορίες για το τι (αν) ταίριαξε στην αναζήτηση:

  • Το m είναι None σε περίπτωση αποτυχίας ταιριάσματος.

  • Αν υπάρχει ταίριασμα, το m περιέχει μεθόδους για να λάβουμε την πληροφορία του ταιριάσματος.

Έτσι, μπορούμε για παράδειγμα να γράψουμε:

if m:   # m is not None

        print('found! matched={}'.format(m.group(0)))

else:   # m is None
        print('not found')

Το m.group(0) επιστρέφει το συνολικό κείμενο που ταίριαξε. Τα groups είναι μέρη του ταιριάσματος και θα αναλυθούν στα επόμενα.

Υπενθυμίζεται ότι η μέθοδος search() δεν επιστρέφει όλα τα ταιριάσματα μέσα στο κείμενο παρά μόνο το πρώτο που θα συναντήσει.

Στη συνέχεια δίνεται το συνολικό προηγούμενο παράδειγμα:

  • Στο interactive shell της Python (αν θέλετε να το δοκιμάσετε, ξεκινήστε το interactive shell με την εντολή python3python) στη κονσόλα/τερματικό/command prompt του λειτουργικού σας. Θυμηθείτε ότι δεν πληκτρολογείτε τα >>>):

    >>> import re
    >>> rexp = re.compile('^cat')
    >>> m = rexp.search('I see a cat')
    >>> print(m)
    None
    >>> m = rexp.search('category')
    >>> print(m)
    <_sre.SRE_Match object; span=(0, 3), match='cat'>
    >>> print(m.group(0))
    cat
    >>>
    
  • Ως αυτόνομο αρχείο εκτέλεσης (τρέξτε με python3 yourfile.py ή python yourfile.py):

    import re
    
    rexp = re.compile('^cat')
    
    m = rexp.search('category')
    
    if m:
            print('found! matched={}'.format(m.group(0)))
    else:
            print('not found')
    

Στις επόμενες παραγράφους αναφέρονται μερικά μόνο από τα εργαλεία της Python για κανονικές εκφράσεις. Για την πλήρη περιγραφή του module re, δείτε στο https://docs.python.org/3/library/re.html.

Βασικά σύμβολα κανονικών εκφράσεων

Ο πίνακας που ακολουθεί περιέχει τα βασικά σύμβολα που χρησιμοποιούνται για την κατασκευή κανονικών εκφράσεων στην 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 ή περισσότερες φορές ο χαρακτήρας που προηγείται

{n} {m,n}

n φορές / από n έως m φορές ο χαρακτήρας που προηγείται

\b

ταιριάζει στην αρχή και στο τέλος μιας λέξης

\w \W

οποιοσδήποτε αλφαριθμητικός / μη αλφαριθμητικός χαρακτήρας

\s \S

οποιοσδήποτε whitespace / μη whitespace χαρακτήρας

*? +? ??

μη άπληστες (non-greedy) μορφές των *, + και ? (μόνο υλοποιήσεις backtracking)

()

group ομαδοποίησης τμημάτων μιας κανονικής έκφρασης, συγκρατούν το κείμενο που ταιριάζει στο τμήμα (μόνο υλοποιήσεις backtracking)

\1 \2 κλπ

backreferences: μέσα στην κανονική έκφραση αντικαθίστανται από το τι έχει ταιριάξει ως τώρα στο αντίστοιχο group (μόνο υλοποιήσεις backtracking)

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

Παραδείγματα

Τα παραδείγματα δίνονται στο interactive shell της Python, θεωρήστε ότι έχει προηγηθεί το import re.

Στην αρχή ή στο τέλος

Βρείτε το string cat στην αρχή (με το ^) ή στο τέλος ($) ενός string:

>>> rexp = re.compile(r'^cat')
>>> m = rexp.search('I see a cat')
>>> print(m)
None
>>> m = rexp.search('category')
>>> print(m)
<_sre.SRE_Match object; span=(0, 3), match='cat'>
>>> m.group(0)
'cat'
>>> rexp = re.compile(r'cat$')
>>> m = rexp.search('in category')
>>> print(m)
None
>>> m = rexp.search('black cat')
>>> print m
<_sre.SRE_Match object; span=(6, 9), match='cat'>
>>> if m: print('found!')
...
found!

Για να ταιριάξουμε ένα άδειο string, θα μπορούσαμε (θεωρητικά -δεν χρειάζεται στην πράξη!) να χρησιμοποιήσουμε το '^$'.

Κλάσεις χαρακτήρων

Κλάσεις χαρακτήρων με το []. Θυμηθείτε ότι μέσα στην κλάση, οι χαρακτήρες ελέγχου χάνουν το ειδικό τους νόημα και το ^ σημαίνει κάτι διαφορετικό:

>>> rexp = re.compile(r'gr[ae]y')
>>> m = rexp.search('a gray cat')
>>> if m: print('found!')
...
found!
>>> m = rexp.search('a grey cat')
>>> if m: print('found!')
...
found!

Βρείτε a που δεν ακολουθείται από b

>>> rexp = re.compile(r'a[^b]')
>>> m = rexp.search('ab')
>>> print(m)
None
>>> m = rexp.search('acb')
>>> print(m)
<_sre.SRE_Match object; span=(0, 2), match='ac'>
>>> print(m.group(0))
ac
>>> m = rexp.search('ba')
>>> print(m)
None

Στο τελευταίο παράδειγμα βλέπουμε ότι ενώ το a δεν πρέπει να ακολουθείται από b, πρέπει όμως να ακολουθείται από κάτι άλλο.

Εναλλαγή

Βρείτε το gray ή grey με εναλλαγή (alternation): 2 σωστοί και ένας λάθος τρόπος

>>> rexp = re.compile(r'gray|grey')
>>> m = rexp.search('a gray cat')
>>> if m: print('found!')
...
found!

>>> rexp = re.compile(r'gr(a|e)y')
>>> m = rexp.search('a gray cat')
>>> if m: print('found!')
...
found!

>>> rexp = re.compile(r'gra|ey')
>>> m = rexp.search('a gray cat')
>>> print(m)
<_sre.SRE_Match object; span=(2, 5), match='gra'>
>>> print(m.group(0))
gra
>>> m = rexp.search('a grey cat')
>>> print(m)
<_sre.SRE_Match object; span=(4, 6), match='ey'>
>>> print(m.group(0))
ey

Στο τελευταίο παράδειγμα έχουμε ταίριασμα, δεν είναι όμως αυτό που θέλουμε. Η χρήση των παρενθέσεων είναι αναγκαία για το σωστό ταίριασμα!

Ένας οποιοσδήποτε χαρακτήρας

Χρήση της τελείας . για να ταιριάξουμε οτιδήποτε (εκτός από newline! Για να ταιριάξετε και το newline στο re.compile() προσθέστε το επιπλέον όρισμα re.DOTALL)

>>> rexp = re.compile('a.c')
>>> m = rexp.search('abc')
>>> print(m)
<_sre.SRE_Match object; span=(0, 3), match='abc'>
>>> m = rexp.search('acb')
>>> print(m)
None
>>> m = rexp.search('a\nc')
>>> print(m)
None
>>> rexp = re.compile('a.c',re.DOTALL)
>>> m = rexp.search('a\nc')
>>> print(m)
<_sre.SRE_Match object; span=(0, 3), match='a\nc'>

Προαιρετικό

Προαιρετικό: color ή colour;

>>> rexp = re.compile('colou?r')
>>> m = rexp.search('color')
>>> print(m)
<_sre.SRE_Match object; span=(0, 5), match='color'>
>>> m = rexp.search('colour')
>>> print(m)
<_sre.SRE_Match object; span=(0, 6), match='colour'>

Τελεστές επανάληψης

Τελεστές επανάληψης: * (0 ή περισσότερες φορές) και + (1 ή περισσότερες φορές). Οι τελεστές αυτοί εφαρμόζονται σε ό,τι βρίσκεται στα αριστερά τους (για να τους εφαρμόσετε σε πάνω από έναν χαρακτήρα χρησιμοποιήστε παρενθέσεις). Εξ’ ορισμού οι τελεστές επανάληψης είναι άπληστοι (greedy): προσπαθούν να ταιριάξουν όσο το δυνατόν περισσότερο κείμενο. Προσοχή με τους τελεστές επανάληψης: μην καταλήξετε σε κανονικές εκφράσεις που δεν επιλέγουν καθόλου κείμενο (ή, για την ακρίβεια, ταιριάζουν το κενό string, π.χ. re.compile('a*')!).

Στο παράδειγμα που ακολουθεί η κανονική έκφραση ταιριάζει σειρές από ψηφία 0 έως 9, οποιουδήποτε μήκους (ένα ή περισσότερα ψηφία):

>>> rexp = re.compile('[0-9]+')
>>> m = rexp.search('abc1234def')
>>> if m: print(m.group(0))
...
1234
>>>

Ολόκληρες λέξεις

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

>>> rexp = re.compile(r'\b[0-9]+\b')
>>> m = rexp.search('abc1234def')
>>> if m: print(m.group(0))
...
>>> m = rexp.search('1234')
>>> if m: print(m.group(0))
...
1234
>>>

Στο επόμενο παράδειγμα αναζητούμε κλασματικούς αριθμούς με ακέραιο και δεκαδικό μέρος. Παρατηρήστε ότι για να ταιριάξουμε την υποδιαστολή πρέπει να χρησιμοποιήσουμε τον συνδυασμό \. γιατί η τελεία . από μόνη της είναι ειδικό σύμβολο στις κανονικές εκφράσεις!

>>> rexp = re.compile(r'[0-9]+\.[0-9]+')
>>> m = rexp.search('1234.567')
>>> if m: print(m.group(0))
...
1234.567
>>>

Μέρη ταιριασμάτων (groups)

Τι είναι τα groups; Στις κανονικές εκφράσεις οι παρενθέσεις παίζουν ειδικό ρόλο, ομαδοποιώντας υποσύνολα της κανονικής έκφρασης. Επιπλέον, η μηχανή ταιριάσματος θυμάται τι ταίριαξε σε κάθε group:

>>> rexp = re.compile(r'([0-9]+)\.([0-9]+)')
>>> m = rexp.search('12.54')
>>> print(m.group(0))
12.54
>>> print(m.group(1))
12
>>> print(m.group(2))
54

Στο προηγούμενο παράδειγμα, οι παρενθέσεις γύρω από το ακέραιο και το δεκαδικό μέρος ορίζουν δύο πρόσθετα groups (group(1) και group(2)), εκτός από το group(0) που περιλαμβάνει όλο το κείμενο που έχει ταιριάξει.

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

Αν ένα σετ παρενθέσεων έχει μπει μόνο για την εξυπηρέτηση του *, +, ? κλπ και δεν θέλουμε τη συγκράτηση της τιμής του group, μπορούμε να χρησιμοποιήσουμε το (?:...). Στη μορφή αυτή οι παρενθέσεις δεν ορίζουν νέο group.

Η απληστία των τελεστών * και +

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

  • θα προτιμήσει να συνεχίσει να ταιριάζει με τον τελεστή

  • παρά να σταματήσει και να προχωρήσει στο επόμενο τμήμα της κανονικής έκφρασης.

Η απληστία των τελεστών * και + είναι η επιθυμητή συμπεριφορά. Μερικές φορές όμως πρέπει να είμαστε προσεκτικοί για να επιτύχουμε αυτό που θέλουμε. Στο επόμενο παράδειγμα προσπαθούμε με λάθος τρόπο να ταιριάξουμε ετικέτες (tags) HTML:

>>> rexp = re.compile('<.+>')
>>> m = rexp.search('this <b>is</b> a <em>HTML</em> text')
>>> if m: print(m.group(0))
...
<b>is</b> a <em>HTML</em>

Η απάντηση <b>is</b> a <em>HTML</em> διαφέρει από την αναμενόμενη, το πρώτο <b> δηλαδή. Αυτό συμβαίνει γιατί ο τελεστής + θα συνεχίσει να ταιριάζει χαρακτήρες έως το τέλος του string και στη συνέχεια μέσω backtracking θα αρχίσει να αποδεσμεύει χαρακτήρες προς τα πίσω μέχρι να μπορέσει και το > να ταιριάξει. Έτσι η απάντηση θα περιλαμβάνει τα πάντα από το πρώτο μέχρι το τελευταίο tag.

Πώς μπορεί να βρεθεί η σωστή λύση; Στην περίπτωση του παραδείγματος υπάρχουν δύο τρόποι. Ο πρώτος τρόπος αποφεύγει εντελώς τη χρήση του «οτιδήποτε» (., τελεία) με τον τελεστή +:

>>> rexp = re.compile('<[^>]+>')
>>> m = rexp.search('this <b>is</b> a <em>HTML</em> text')
>>> if m: print(m.group(0))
...
<b>

Η προηγούμενη μέθοδος είναι η βέλτιστη γιατί δεν εκτελεί καθόλου backtracking. Θα πρέπει πάντα να την προτιμάτε όταν ψάχνετε μέχρι να βρείτε έναν συγκεκριμένο χαρακτήρα (όπως εδώ το >).

Η δεύτερη μέθοδος είναι διαθέσιμη μόνο όταν οι κανονικές εκφράσεις υλοποιούνται με backtracking (στις περισσότερες γλώσσες προγραμματισμού δηλαδή). Εδώ χρησιμοποιούμε τη μη-άπληστη (lazy) μορφή των τελεστών επανάληψης (συμβολίζεται με τα *?, +? και ??). Η μη-άπληστη μορφή προτιμάει να σταματήσει παρά να συνεχίσει το ταίριασμα, ακόμα κι αν έχει τη δυνατότητα:

>>> rexp = re.compile('<.+?>')
>>> m = rexp.search('this <b>is</b> a <em>HTML</em> text')
>>> if m: print(m.group(0))
...
<b>

Στο παράδειγμα, ο μη-άπληστος τελεστής +?, αφού ταιριάξει το b από το πρώτο <b> tag, θα παραιτηθεί από τη συνέχεια, επιτρέποντας και στο > να ταιριάξει.

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

Στο επόμενο παράδειγμα θέλουμε να ταιριάξουμε σχόλια HTML, οτιδήποτε δηλαδή βρίσκεται μεταξύ <!-- και -->:

>>> rexp = re.compile('<!--.*?-->')
>>> m = rexp.search('this <b>is</b> <!--a <em>--HTML--</em>--> text')
>>> if m: print(m.group(0))
...
<!--a <em>--HTML--</em>-->

Ασκήσεις

  1. Κατασκευάστε κανονική έκφραση για την αναγνώριση έγκυρων μορφών 24ωρης ώρας (00:00 έως 23:59) με τη βοήθεια των [ ], |, ? και \b.

  • Η ώρα μπορεί να εκφράζεται με ένα ή δύο ψηφία (π.χ. 3:45 αλλά και 03:45).

  • Τα λεπτά θα έχουν πάντα δύο ψηφία.

  • Κάθε άκυρη μορφή ώρας θα απορρίπτεται (π.χ. 31:13 ή 9:72).

  1. Γράψτε κανονική έκφραση που αναγνωρίζει κλασματικούς αριθμούς, οποιουδήποτε μήκους

  • Στις μορφές 123.456, 123. και .456.

  • Προσοχή μην καταλήξετε σε κανονική έκφραση που δεν επιλέγει τίποτα ή επιλέγει μόνο την τελεία!

  • Θυμηθείτε ότι θα πρέπει να γράψετε την τελεία με escape ως \. (αλλιώς έχει ειδική έννοια).

  1. Γράψτε κανονική έκφραση που ταιριάζει

  • Κλασματικούς αριθμούς στη μορφή ΧΧΧ ή ΧΧΧ.ΥΥΥ (τα Χ και Υ είναι τα ψηφία 0 έως 9 σε οποιοδήποτε πλήθος), με προαιρετικό + ή - μπροστά από τον αριθμό.

  • Μετά τον αριθμό ακολουθούν 0 ή περισσότερα κενά (whitespaces, χρησιμοποιήστε το \s που ταιριάζει οποιονδήποτε whitespace χαρακτήρα).

  • Και τέλος, υποχρεωτικά, τα γράμματα C ή F (κεφαλαία μόνο).

  • Ο σκοπός είναι να αναγνωρίσετε θερμοκρασίες, σε κλίμακα Κελσίου ή Φαρενάιτ.

  • Ο χρήστης εισάγει μια θερμοκρασία

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

    • Θυμηθείτε να χρησιμοποιήσετε groups για να πάρετε μόνο τα κομμάτια που σας ενδιαφέρουν (θερμοκρασία και κλίμακα).

    • Θυμηθείτε πριν κάνετε πράξεις να μετατρέψετε το string του αριθμού σε float.

    • Τις πράξεις μετατροπής μπορείτε να τις βρείτε σε πολλά site online.

  1. Γράψτε κανονική έκφραση που να αναγνωρίζει ονόματα μεταβλητών:

  • Ο πρώτος χαρακτήρας είναι γράμμα (Α-Ζ ή a-z) ή _

  • Ακολουθούν 0 ή περισσότεροι χαρακτήρες A-Z, a-z, 0-9, _

  • Προσοχή: θέλουμε ταίριασμα ολόκληρων λέξεων μόνο!

  1. Γράψτε κανονική έκφραση για να αναγνωρίζετε κείμενο μέσα σε " ".

  • Υπόδειξη: χρησιμοποιήστε κλάση χαρακτήρων [^ ] και τη μέθοδο finditer().

  1. Ετοιμάστε πρόγραμμα Python που ανοίγει το αρχείο του κειμένου δοκιμών και διαβάζει όλο το περιεχόμενό του σε μια μεταβλητή τύπου string.

    Προσθέστε κανονική έκφραση που θα βρίσκει και θα τυπώνει το περιεχόμενο όλων των <meta> tags, δηλαδή οτιδήποτε βρίσκεται μεταξύ των <meta και />.

    • Πρέπει να χρησιμοποιήσετε μη-άπληστο τελεστή; Ή μπορείτε να το κάνετε με κλάση χαρακτήρων;

    • Χρησιμοποιήστε τη μέθοδο finditer() του αντικειμένου της κανονικής έκφρασης για να βρείτε όλα τα ταιριάσματα σε ένα for.

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

    • Θα αναγνωρίζει τα property="..." ή content="..." (ξεχωριστά, όχι στο ίδιο ταίριασμα).

      • Προσέξτε να μην αναγνωρίζετε άλλα όπως π.χ. notaproperty="..."!

    • Μέσω groups θα πρέπει να εξάγει χωριστά τα (property ή content) και το αντίστοιχο περιεχόμενο μέσα στα "...".

  2. Χρησιμοποιήστε διαδοχικά τη μέθοδο sub(). Το πρόγραμμά σας:

    • Θα διαβάζει ολόκληρο το αρχείο δοκιμαστικού κειμένου (πηγή: Ταινιοθήκη της Ελλάδας) σε μια μεταβλητή.

    • Θα χρησιμοποιεί πρώτη κανονική έκφραση για να μετατρέψει συνεχόμενους whitespace χαρακτήρες σε ακριβώς έναν κενό χαρακτήρα (space).

    • Στη συνέχεια θα απαλείφει με δεύτερη κανονική έκφραση όλα τα σημεία στίξης, αφήνοντας μόνο λέξεις στο κείμενο, χωρισμένες από ένα κενό μεταξύ τους.

    • Θα τυπώνει το τελικό αποτέλεσμα.

  3. Κατεβάστε το εξής δοκιμαστικό κείμενο. Τι παρατηρείτε; Στην άσκηση θα διορθώσετε το πρόβλημα με τους τόνους και τα διαλυτικά με τη βοήθεια της sub().

    • Η πληροφορία στο αρχείο αυτό βρίσκεται αυτοτελής σε κάθε γραμμή. Έτσι μπορείτε να το επεξεργαστείτε γραμμή προς γραμμή. Αν έχετε προβλήματα encoding, προσθέστε στην open() το όρισμα encoding='utf-8'.

    • Τι συμβαίνει στο κείμενο: τα τονισμένα φωνήεντα δεν έχουν αποθηκευτεί σωστά.

      • Για τους τόνους, προηγείται ο χαρακτήρας 'GREEK TONOS' (U+0384) (στην κανονική σας έκφραση θα το γράψετε ως \u0384) και μετά το αντίστοιχο φωνήεν.

      • Για τα διαλυτικά, προηγείται ο χαρακτήρας 'DIAERESIS' (U+00A8) (στην κανονική σας έκφραση θα το γράψετε ως \u00A8) και μετά το αντίστοιχο φωνήεν.

      • Δεν υπάρχουν συνδυασμοί τόνου+διαλυτικών.

    • Γράψτε κανονική έκφραση που θα:

      • Αναγνωρίζει έναν από τους χαρακτήρες 'GREEK TONOS' (U+0384) ή ‘DIAERESIS' (U+00A8) και τον κρατά στο group(1).

      • Κρατά τον επόμενο χαρακτήρα (οποιονδήποτε) στο group(2).

    • Αντιγράψτε τα δύο λεξικά που ακολουθούν στο πρόγραμμά σας:

      # the tonos replacement dict
      td = { 'α':'ά','ε':'έ','η':'ή','ι':'ί','ο':'ό','υ':'ύ','ω':'ώ',
                 'Α':'Ά','Ε':'Έ','Η':'Ή','Ι':'Ί','Ο':'Ό','Υ':'Ύ','Ω':'Ώ' }
      
      # the dialytika replacement dict
      dd = { 'ι':'ϊ','υ':'ϋ','Ι':'Ϊ','Υ':'Ϋ' }
      
    • Στη συνέχεια, χρησιμοποιήστε συνάρτηση «callback» στη sub() της κανονικής έκφρασης που σχεδιάσατε προηγουμένως, έτσι ώστε να επιστρέφετε τη σωστή αντικατάσταση με βάση τα δύο προηγούμενα λεξικά.

      • Υποδειξη: επιστρέψτε ό,τι τιμή σας δίνει το λεξικό για το κλειδί m.group(2).

      • Προσοχή: θα πρέπει να ελέγξετε αν το m.group(2) υπάρχει στο λεξικό. Αν όχι, επιστρέψτε όλο το m.group(0), αφήνοντας ουσιαστικά το σημείο αναλλοίωτο.