Με τις γνώσεις που αποκτήσαμε στο πρώτο άρθρο της σειράς, μπορούμε τώρα να περιγράφουμε σχεδόν οτιδήποτε. Όμως για ένα μεγάλο μέρος απ’ αυτό το “οτιδήποτε” πρέπει να καταστρώνουμε τεράστιες εκφράσεις. Στο παρόν άρθρο θα μάθουμε πώς να συντάσσουμε ακόμα πιο αφηρημένες εκφράσεις, ώστε να περιγράφουμε πιο σύνθετα strings γράφοντας λιγότερα.

Στο προηγούμενο άρθρο μιλήσαμε για τη φύση και την αποστολή των κανονικών εκφράσεων. Τονίσαμε ότι υπάρχουν πολλές διάλεκτοι με αρκετές διαφορές, τόσο στη σύνταξη όσο και στην εκφραστική δύναμη. Ακολούθως, αφού ξεκαθαρίσαμε με ποιες διαλέκτους ασχολούμαστε και γιατί, ξεκινήσαμε τη γνωριμία με τα συντακτικά σχήματα που συγκροτούν μια κανονική έκφραση. Αναφερθήκαμε συγκεκριμένα στις κλάσεις χαρακτήρων, στις ανεστραμμένες (συμπληρωματικές) κλάσεις, στο λεγόμενο escaping, στον χαρακτήρα μπαλαντέρ και στους χαρακτήρες που περιγράφουν μία ή περισσότερες επαναλήψεις. Τελειώνοντας το προηγούμενο άρθρο, ενδέχεται να νιώσατε ότι είστε έτοιμοι να ξεθάψετε οτιδήποτε κι από οπουδήποτε – αρκεί να σχηματίσετε την κατάλληλη κανονική έκφραση. Η αλήθεια είναι ότι αυτό δεν απέχει από την πραγματικότητα. Όμως το οπλοστάσιο των κανονικών εκφράσεων περιλαμβάνει αρκετές ακόμα αξιοσημείωτες δυνατότητες, και σ’ αυτό το άρθρο θα γνωρίσουμε ορισμένες εξ αυτών.

Σημεία αναφοράς

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

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

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

grep --color=auto "^This" test.txt

This is a test. This is a test. This is a test …

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

grep --color=auto "This ^is" test.txt

Η συγκεκριμένη αναζήτηση με το grep δεν επιστρέφει κάτι, ποτέ. Μελετήστε λίγο την κανονική έκφραση. Περιγράφει μια φράση που ξεκινά με τη λέξη This, συνεχίζει με το χαρακτήρα του κενού (space) κι ολοκληρώνεται με τη λέξη is, η οποία όμως πρέπει να βρίσκεται στην αρχή του string (λόγω του caret). Προφανώς, αυτός ο περιορισμός είναι ασυμβίβαστος με την ύπαρξη του space πριν από το is. Μ’ άλλα λόγια, είτε το is θα βρίσκεται στην αρχή του string και δεν θα υπάρχει τίποτα από πίσω του, είτε θα βρίσκεται σε οποιαδήποτε άλλη θέση και πίσω του θα υπάρχουν κι άλλοι χαρακτήρες. Είναι φανερό, λοιπόν, ότι ο χαρακτήρας caret δεν έχει νόημα να χρησιμοποιείται παρά μόνον στην αρχή μιας κανονικής έκφρασης.

Τέλος string. Το τέλος του εκάστοτε string συμβολίζεται με τον χαρακτήρα του δολαρίου ($). Με την ίδια λογική που το caret έχει νόημα μόνο στο ξεκίνημα μιας έκφρασης, ο χαρακτήρας του δολαρίου έχει νόημα μόνο στο τέλος της. Ο συνδυασμός των δύο χαρακτήρων αποκτά ιδιαίτερη αξία στα λεγόμενα sanity checks. Έστω ότι θέλουμε να τσεκάρουμε αν ο χρήστης εισήγαγε σε κάποια φόρμα έναν αριθμό. Εξετάστε το ακόλουθο προγραμματάκι:

$user_input = "jim49";
$regex = "/[0-9]+/";
if (preg_match($regex, $user_input) == 1)
    echo "acceptable input";
else
    echo "unacceptable input";

Το πρόγραμμα είναι γραμμένο σε PHP. Η συνάρτηση preg_match ελέγχει αν το string που περιέχει η μεταβλητή $user_input περιλαμβάνει το string που περιγράφει η κανονική έκφραση, η οποία περιέχεται στη μεταβλητή $regex. (Το όνομα της συνάρτησης preg_match προκύπτει από το “PCRE regular expression match”. Το PCRE αποτελεί μια υλοποίηση ανοιχτού κώδικα της μηχανής κανονικών εκφράσεων της Perl.) Η κανονική έκφραση που χρησιμοποιείται στο πρόγραμμα είναι η [0-9]+ και περιγράφει μια αλληλουχία αριθμητικών ψηφίων που περιλαμβάνει τουλάχιστον ένα ψηφίο. (Ο χαρακτήρας / τοποθετείται στην αρχή και στο τέλος της έκφρασης κατ’ απαίτηση της γλώσσας. Οι εμφανίσεις του δεν αποτελούν τμήμα της κανονικής έκφρασης.) Παρατηρήστε τώρα και το περιεχόμενο της $user_input. Αν εκτελεστεί το πρόγραμμα, το αποτέλεσμα θα είναι “acceptable input”. Δηλαδή, το πρόγραμμά μας θα έχει αποτύχει. Για να λειτουργήσει όπως επιθυμούμε και να ελέγξει το αν ο χρήστης εισήγαγε μόνο κάποιον αριθμό, θα έπρεπε να επιστρατεύσουμε και τους χαρακτήρες caret και dollar. Η μεταβλητή $regex θα έπρεπε να οριστεί ως εξής:

$regex = "/^[0-9]+$/";

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

Όρια λέξης. Οι ειδικοί χαρακτήρες που είδαμε παραπάνω αναφέρονται στα άκρα του υπό εξέταση string. Υπάρχει κι ένας χαρακτήρας που αναφέρεται στα άκρα οποιασδήποτε λέξης περιλαμβάνεται στο string που εξετάζουμε. Αναφερόμαστε στον ειδικό χαρακτήρα \b, ο οποίος συμβολίζει τη θέση ανάμεσα σε έναν χαρακτήρα λέξης και σε έναν χαρακτήρα που δεν μπορεί να ανήκει σε λέξη. Για τις περισσότερες μηχανές κανονικών εκφράσεων, οι χαρακτήρες που μπορούν να ανήκουν σε λέξη είναι όλα τα γράμματα, όλα τα αριθμητικά ψηφία και το underscore (_). Ας υποθέσουμε ότι αναζητάμε όλες τις εμφανίσεις της μεταβλητής sample, μέσα στον κώδικα ενός προγράμματος. Παρακάτω φαίνεται ο σωστός τρόπος για να πετύχουμε κάτι τέτοιο:

grep --color=auto "\bsample\b" synth.c

sample = phase.bytes[1];

if (sample < 128) {sample = sample * 2;}

sample = ((volume * sample) >> 8);

OCR2A = ((amp_envelope.bytes[1] * sample) >> 8);

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

grep --color=auto "sample" synth.c

if (sample_cnt == 0) {

if (sample_cnt == 120) {

sample = phase.bytes[1];

if (sample < 128) {sample = sample * 2;}

sample = ((volume * sample) >> 8);

OCR2A = ((amp_envelope.bytes[1] * sample) >> 8);

sample_cnt++;

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

Εναλλαγή

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

grep --color=auto "apples|oranges" sometext.txt 

Όπως βλέπετε, για να πετύχουμε αυτό που θέλουμε χωρίσαμε τις εναλλακτικές εκφράσεις με το χαρακτήρα της κάθετης μπάρας (pipe). Σε πολλές γλώσσες προγραμματισμού, ο ίδιος χαρακτήρας χρησιμοποιείται με την έννοια του λογικού OR. Κάπως έτσι μπορούμε να τον ερμηνεύουμε και στις κανονικές εκφράσεις, αν κι εδώ η συγκεκριμένη λειτουργία ονομάζεται alternation. Ας υποθέσουμε τώρα ότι αναζητάμε τις μεταβλητές amp_rise, amp_decay και amp_envelope, στον πηγαίο κώδικα ενός προγράμματος. Γι’ αυτή την αναζήτηση θα μπορούσαμε να σχηματίσουμε την ακόλουθη έκφραση:

grep --color=auto "amp_rise|amp_decay|amp_envelope" synth.c 

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

grep --color=auto "amp_(rise|decay|envelope)" synth.c 

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

Ενθουσιώδεις μηχανές

Τα περισσότερα παραδείγματα που έχουμε εξετάσει μέχρι στιγμής βασίζονται στο grep. Αυτό δεν ήταν τυχαίο. Το συγκεκριμένο εργαλείο επιτρέπει τον εύκολο σχηματισμό απλών και κατανοητών παραδειγμάτων. Η μηχανή του grep, ωστόσο, έχει κάποιες παραπλανητικές ιδιαιτερότητες. Όταν ζητάμε από το grep να πραγματοποιήσει μια αναζήτηση, φροντίζει να εξαντλήσει το string που του δίνουμε και να εντοπίσει όλες τις εμφανίσεις του string που ψάχνουμε. Αυτό δεν ισχύει σε όλες τις μηχανές. Για την ακρίβεια, οι περισσότερες μηχανές κανονικών εκφράσεων παρουσιάζουν την αντίθετη συμπεριφορά και σταματούν αμέσως μόλις εντοπίσουν μια εμφάνιση του string που ψάχνουμε. Θα μπορούσε κανείς να πει ότι αυτές οι μηχανές ανυπομονούν να επιστρέψουν στο χρήστη κάποιο αποτέλεσμα. Στην ορολογία των κανονικών εκφράσεων, αυτή η “ενθουσιώδης” συμπεριφορά ονομάζεται eagerness. Αναρωτιέστε τι σημασία έχουν όλα αυτά; Δείτε το ακόλουθο πρόγραμμα:

$string = "fish, catfish, dogfish, goldfish";
$regex = "/cat|catfish/";
preg_match($regex, $string, $matches);
echo $matches[0];

Το παραπάνω αναζητά την έκφραση που περιέχει η μεταβλητή $regex μέσα στο περιεχόμενο της μεταβλητής $string και στη συνέχει εμφανίζει το αποτέλεσμα. Η μηχανή κανονικών εκφράσεων που χρησιμοποιείται εδώ (PCRE) έχει την ίδια συμπεριφορά με εκείνη της Perl. Αυτό σημαίνει ότι σταματάει την αναζήτηση αμέσως μόλις εντοπίσει μία εμφάνιση του ζητούμενου string. Τι πιστεύετε ότι θα εμφανίσει το πρόγραμμα σαν αποτέλεσμα;

cat

Δεν ξέρουμε τι περιμένατε, αλλά το πρόγραμμα εντόπισε το cat. Η μηχανή, βλέπετε, ξεκίνησε την αναζήτηση με την πρώτη εναλλακτική (cat). Μόλις εντόπισε το cat μέσα στο catfish διέκοψε την αναζήτηση, αγνόησε την άλλη εναλλακτική (catfish) κι έσπευσε να επιστρέψει στο χρήστη τα αποτελέσματα της αναζήτησης. Νομίζουμε ότι τώρα μπορείτε να κατανοήσετε καλύτερα τι εννοούμε όταν λέμε ότι η συμπεριφορά της είναι… eager.

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

$regex = "/catfish|cat/";

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

$regex = "/\b(cat|catfish)\b/";

Με αυτή την έκφραση περιγράφουμε μία μεμονωμένη λέξη, η οποία μπορεί να είναι είτε η cat, είτε η catfish. Η χρήση των \b απαγορεύει στη μηχανή να σταματήσει, όταν εντοπίσει το cat μέσα σε κάποια άλλη λέξη.

Αναφορές

Τα στοιχεία που έχουμε γνωρίσει ως τώρα είτε περιγράφουν τους χαρακτήρες του ζητούμενου string είτε τη θέση τους μέσα σε ένα ευρύτερο string (σε αυτό εντός του οποίου πραγματοποιούμε την αναζήτηση). Οι κανονικές εκφράσεις, όμως, διαθέτουν κι έναν ακόμα πιο αφηρημένο μηχανισμό. Με τη βοήθειά του μπορούμε να αναφερθούμε σε τμήματα των ίδιων των εκφράσεων, τα οποία εντοπίστηκαν επιτυχώς κατά την αναζήτηση. Για να κατανοήσετε τι ακριβώς εννοούμε, φανταστείτε ότι αναζητάμε όλα τα ζεύγη από tags <a> και <p>, μέσα σε ένα αρχείο ΗΤΜL. Δείτε το ακόλουθο παράδειγμα:

grep -Ε --color=auto "<([pa]).*>.*</\1>" index.html

Στο αποτέλεσμα που ακολουθεί, λόγω θεμάτων rendering τα matches της έκφρασης είναι ανάμεσα σε διπλές κάτω παύλες (__).

<li>__<a href="/download.cgi">From a Mirror</a>__</li> </plain>
<h1 id="documentation">__<a href="/docs/">Documentation</a>__</h1>
<li>__<a href="/mod_fcgid/">mod_fcgid</a>__</li>
<li>__<a href="/mod_ftp/">mod_ftp</a>__</li>
<h1 id="miscellaneous">__<a href="/info/">Miscellaneous</a>__</h1>
__<p>with additional contributions from</p>__
<li>__<a href="/contributors/">Contributors</a>__</li>
<li>__<a href="http://www.apache.org/foundation/thanks.html">Sponsors</a>__</li>

Η συγκεκριμένη έκφραση περιγράφει ένα string το οποίο ξεκινά με το χαρακτήρα <, συνεχίζει με το a ή με το p, αμέσως μετά εμφανίζει οποιουσδήποτε κι οσουσδήποτε χαρακτήρες, συμπληρώνεται με ένα > και συνεχίζει πάλι με οποιουσδήποτε κι οσουσδήποτε χαρακτήρες. Το string καταλήγει σε ένα </, το οποίο συνοδεύεται από το tag που εντοπίστηκε προηγουμένως (το a ή το p) κι ολοκληρώνεται με το χαρακτήρα >. Αν η μηχανή που ερμηνεύει την κανονική έκφραση εντοπίσει αρχικά ένα p, θα ολοκληρώσει επιτυχώς την αναζήτηση μόνον όταν μετά από τους χαρακτήρες </ συναντήσει πάλι ένα p. Αντίστοιχα, αν εντοπιστεί ένα a τότε η αναζήτηση θα ολοκληρωθεί επιτυχώς μόνον όταν μετά από τους χαρακτήρες </ βρεθεί πάλι ένα a. Για να λειτουργήσει αυτό το σχήμα πρέπει να έχουμε δηλώσει εξαρχής σε πιο τμήμα της έκφρασης θα αναφερθούμε αργότερα. Αυτό επιτυγχάνεται με τις παρενθέσεις που περικλείουν την κλάση [pa]. Η αναφορά σε αυτό το τμήμα επιτυγχάνεται με το \1. Χωρίς αυτόν το μηχανισμό, θα ήμασταν υποχρεωμένοι να σχηματίσουμε μια έκφραση σαν αυτή:

grep -Ε --color=auto "<[pa].*>.*</[pa]>" index.html

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

<p>blah, blah blah <a href="...">blah, blah, blah</a>

Όπως βλέπετε, χωρίς το μηχανισμό της αναφοράς στο στοιχείο που βρέθηκε νωρίτερα η μηχανή κανονικών εκφράσεων επέστρεψε ένα string το οποίο ξεκινά με το tag <p> και τελειώνει με το tag </a>.

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

(εξάσκηση) με το grep ή κάποια γλώσσα προγραμματισμού και πάλι \1

Άρθρα της σειράς

Μέρος 14: special chars, escaping, classes, ranges, negation, repetition

Μέρος 24: anchors, concatenation, alternation, eagerness, backreference

Μέρος 34: lookahead, lookbehind, greedy machines, lazy machines

Μέρος 44: backreference, anchors, character escaping, lookahead

~Spir@l Evolut10n

Σας άρεσε το post; Αν ναι μπορείτε να στηρίξετε το ðhacker, χωρίς κατ’ ανάγκη να ξοδέψετε χρήματα.