KB » Computer » Programmeren met Perl

Programmeren met Perl

    Tweeten

Introductie


Reguliere expressies (Regular expressions)

Introductie

De kracht van Perl zit voor een deel in dat je in de taal gebruik kan maken van reguliere expressies. Het nadeel van die regex's (om het zo maar even af te korten) is dat het bijna onleesbare code oplevert.

Regex's zijn vooral handig voor tekstbewerking.

Patroonvergelijking (Pattern matching)

Patronen staan vaak tussen forward slashes, dus bv. /te?t$/.

De tekens in onderstaande tabel, zoals '^', '$', '/', '.' hebben speciale betekenissen en kunnen dus niet zonder meer in een patroon gebruikt worden. Om ze wel te gebruiken (dus als je een punt wilt matchen i.p.v. hetgeen waar de punt normaal gesproken voor staat) moet je er een '\' voor zetten. Dit soort tekens wordt metacharacters genoemd.

Laten we een voorbeeldzin nemen om in de onderstaande voorbeelden te gebruiken:
Alle bestanden die ouder zijn dan 5 november 2003

Symbool Betekenis Voorbeeld
^ Begin van de string /^Alle best/ levert een match, omdat de string er mee begint.
$ Eind van de string /2003$/ levert een match, omdat de string er mee eindigt.
\d Een cijfer van 0..9 /\d\d\d\d\d\d$/ levert geen match, omdat er geen 5 cijfers aan het eind van de string staan (/\d\d\d\d$/ had wel gewerkt).
\ Escape character. Hiermee geef je aan dat het teken dat erna komt een andere betekenis heeft dan normaal. Als je in het voorgaande voorbeeld geen escape character had gebruikt voor de 'd', had Perl gewoon gezocht naar 4 of 5 'd'-s op een rijtje.
\D Elk teken dat geen cijfer van 0..9 is. /\D2/ matcht, omdat er voor de '2' een niet-cijfer staat. /\D3/ matcht niet, omdat je geen '3' kan vinden met een niet-cijfer ervoor.
\w Letters (hoofdletters en gewone), cijfers van 0..9 en de underscore (_). /r\w2/ match niet, omdat het teken tussen november en 2003 een spatie is.
\W Elk teken dat niet aan \w voldoet.
\s Spaties, tabs, einde van de regel, etc. Dus eigenlijk alles waar geen zichtbare tekens staan. /r\s2/ matcht wel, omdat er precies 1 spatie tussen november en 2003 staat. /r\s\s2/ zou niet hebben gematched.
\S Alles wat niet aan \s voldoet.
. De punt matcht elk teken. /^.$/ matcht elke string die uit precies 1 teken bestaat.
? Matcht 0 of 1 keer het teken dat er voor staat. /sta?nd/ levert een match, omdat 'stand' in de zin zit. Maar /staa?nd/ levert ook een match. In dit geval wordt de 'a' voor de vraagteken vergeten.
* Match 0 of meer voorkomens van het teken voor de asterisk. /sx*tx*and/ matcht, omdat de 'x*' in dit geval 2 keer gewoon niet wordt meegeteld (0 voorkomens van 'x'). /Al*e/ matcht ook, omdat de 'l*' gelijk is aan 'll'.
+ Is bijna hetzelfde als *, maar nu moet het teken minimaal 1 keer voorkomen. /sx+tx+and/ matcht dus niet, maar /Al+e/ wel.
\b Match een woordgrens, de grens tussen een letterteken, cijfer of underscore ('_') en een ander teken, bv. een spatie. /Alle\b/ matcht, omdat er na 'Alle' een spatie komt. Maar /All\b/ match niet, omdat 'e' een word character is (overeenkomend met \w).
\B Het omgekeerde van \b. Match dus juist niet met een woordgrens. /oud\B/ matcht, omdat er nog 'er' na 'oud' komt.
| Matcht alternatieven. /ouder|oudst/ matcht, omdat 1 van de 2 in de zin voorkomt (ouder). /^Alle|2003$/ matcht ook, omdat ze allebei correct zijn. Speciale tekens worden alleen toegepast op het alternatief waar ze in staan. Dus /^ouder/Alle$/ match niet, omdat ze allebei incorrect zijn.

Groepen van tekens

Alternatieven kan je scheiden met een '|', maar als het om tekens gaat kan je ze ook tussen vierkante haken zetten.

Als je wilt testen op een oneven cijfer kan je dus de notatie [13579] gebruiken. Testen op een klinker doe je met [aeiou].

Binnen vierkante haken heeft '^' een andere betekenis dan erbuiten. Je kan er mee aangeven dat je de groep tekens die er op volgt juist niet wilt.

Dus als je wilt testen op een medeklinker kan je [^aeiou] gebruiken.

Als je wilt testen op een opeenvolgende reeks van tekens, bv. de cijfers van '2' t/m '7', dan kan je dat als volgt aangeven: [2-7].

Herhaling van een patroon

Soms wil je dat een bepaald patroon meerdere keren voorkomt, en vooral als dat meer dan 2 keer is kan het nogal onnodig ingewikkeld worden.

Stel dat je 3 klinkers achter elkaar wilt, dan krijg je [aeiou][aeiou][aeiou].

Gelukkig kan je tussen accolades het aantal repetities opgeven. Het voorgaande patroon wordt dan [aeiou]{3}.

Als het patroon een variabel aantal keren voorkomt, bv. tussen 3 en 5 keer, dan wordt het [aeiou]{3,5}.

Als het patroon minimaal een aantal keren moet voorkomen, laat je gewoon het 2e getal weg. Minimaal 5 keer een klinker wordt dus [aeiou]{5,}.

Refereren aan gevonden stukken

Ik begin weer met de voorbeeldzin:
Alle bestanden die ouder zijn dan 5 november 2003

Stel je wilt een patroon gebruiken waarmee je het stuk kan vinden na 'ouder zijn dan', en met het resultaat een nieuwe zin wilt maken.

Naar elk stuk binnen een reguliere expressie dat tussen haakjes staat kan je later terugverwijzen met bv. de variabelen $1, $2, $3, etc.

Het volgende programmaatje:

$line1 = "Alle bestanden die ouder zijn dan 5 november 2003"; if ($line1 =~ /ouder zijn dan (.*)$/) { print "Datum: $1\n" }

produceert als uitvoer:
Datum: 5 november 2003

De reguliere expressie eindigt met (.*)$
wat betekent: een willekeurig teken (.), willekeurig vaak herhaald (*) tot je het einde van de regel ($) tegenkomt.

Het stuk van de regel dat voldoet aan het gedeelte tussen haakjes krijgt de naam '$1', en daar kan in het programma verder mee werken tot je door een nieuwe pattern matching een nieuwe '$1' krijgt, of er zelf een waarde aan geeft.


Speciale variabelen

Een variabele, en dus ook een speciale variabele, kan een waarde hebben of ongedefinieerd zijn.

Soms wil je dat een speciale variabele niet zijn standaardwaarde heeft, en in dat geval kan je er de undef-functie op loslaten.

Variabele Betekenis Alternatieve namen Voorbeeld
$_ Als je geen variabele opgeeft op een plek waar er 1 nodig is wordt de waarde van $_ gebruikt. $ARG
$" Als je een array afdrukt wordt deze string na elk item afgedrukt (default spatie). $LIST_SEPARATOR
@_ De lijst met parameters die aan een subroutine worden doorgegeven.
$/ String die afgedrukt moet worden nadat een record is afgedrukt. $OUTPUT_RECORD_SEPARATOR
$ORS
$, String die afgedrukt moet worden nadat een argument is afgedrukt. $OUTPUT_FIELD_SEPARATOR
$OFS
$! De meest recente operating system error. $OS_ERROR
$= Aantal regels dat je op elke pagina wilt hebben. $FORMAT_LINES_PER_PAGE
$& In het 2e deel van een replacement operatie, hetgene wat gematched is in het 1e deel. $MATCH

Gebruik maken van pattern matching

Testen of een string een bepaald patroon bevat

Dat doe je als volgt:

if ($string =~ m/aap/) { statements... }

De 'm' staat voor match, en '=~' is de pattern-matching operator.

Er staat dus: als de variabele $string de string aap bevat, voer dan de commando's tussen de {} uit.

De 'm' is in dit geval optioneel.

Vaak wordt de default variabele $_ (i.p.v. zoals hier $string) gebruikt, en dan wordt de 1e regel dus:
if (/aap/) {

Uppercase en lowercase

Als je wilt dat case geen rol speelt bij de pattern-matching (dus case-insensitive), dan moet je de regulier expressie laten volgen door een 'i'.

/google/i matcht dus zowel 'Google' als 'GOOGLE' als 'GoOgLe'.

Variabele Betekenis Voorbeeld
i Er wordt niet gekeken naar hoofdletters of kleine letters
g Global. Wordt gebruikt bij replacement (substitutie) om meerdere voorkomens op 1 regel mee te nemen.
c In een tr(anslate)-operatie, het omdraaien (complement) van de operatie.
s In een tr(anslate)-operatie, het vervangen van een serie dezelfde tekens door 1 ervan (de 's' van squash).
d In een tr(anslate)-operatie, het niet verlengen van te korte replacement strings, maar het verwijderen (delete) van de overtollige tekens.

Werken met bestanden en mappen (files en folders)

De volgende onderwerpen:

Openen van een file

De volgende subsecties:

File handles

Om een file te kunnen lezen of er naar toe te kunnen schrijven heb je een 'handle' nodig naar die file. Om dezelfde file te kunnen lezen en beschrijven heb je 2 handles nodig.

De naam van een file handle is uppercase, moet beginnen met een letter, en kan verder ook cijfers en underscores bevatten.

Voorbeelden

Voorbeelden van het openen van een bestand:
open WP, 'computer.htm';
open HFO, '>d:\test\op.txt';

In het 1e geval geef je geen padnaam op, en open je het bestand computer.htm in de huidige directory (de map van waaruit je script draait, of een andere als je hem gewijzigd hebt).
De naam van de file handle is WP, en je gebruikt die naam om het bestand te benaderen.

In het 2e geval creëer je een file handle HFO naar een bestand met een volledige padnaam. Vanwege het '>'-teken betekent het dat je naar het bestand kan schrijven.
Let op: als het bestand al bestaat wordt de inhoud overschreven!!

Haakjes of niet?

Je kan bv. het 1e commando ook schrijven als
open (WP, 'computer.htm');
(dus met haakjes). Ik weet niet precies wanneer je wel of geen haakjes moet schrijven. Wel lijkt me veiliger.

Enkele of dubbele quotes?

In het 2e voorbeeld zijn de enkele quotes cruciaal. Als je nl. dubbele quotes had gebruikt, was het bestand als een string opgevat, en had '\t' gestaan voor een TAB-character. Als je toch perse dubbele quotes wilt gebruiken moet je elke '\' in een padnaam vervangen door '\\'.

Informatie aan het einde van een file plakken

Er is nog een 3e manier (naast lezen en schrijven) om een file te openen, en dat is voor het toevoegen van informatie aan het bestand:
open (WP, '>>computer.htm');

Wat is het resultaat van open, en wat als een bestand nog niet bestaat?

Het openen (voor lezen) van een bestand dat nog niet bestaat levert een fout op. En vanzelfsprekend kunnen er nog allerlei andere dingen mis gaan.

Het open-commando levert 1 van 2 resultaten op:
undef als het niet goed is gegaan, en 1 als het wel goed is gegaan.

Wat wil je graag doen als je een bestand opent en het lukt niet? Een foutmelding afdrukken en het script stoppen (in de meeste gevallen).

Gelukkig heeft Perl daar een mooie constructie voor:
open (WP, 'computer.htm') or die "Kan computer.htm niet openen: $!\n";

Als het fout gaat wordt de melding afgedrukt. '\n' staat voor een newline (nieuwe regel) aan het eind, en '$!' staat voor een eventuele foutmelding van het operating system, bv. Permission denied

Voorgedefinieerde files

Er zijn 3 file handles die altijd bestaan: STDIN, STDOUT en STDERR

Afdrukken naar en lezen van bestanden

Afdrukken naar het scherm

Het commando print "Hello world!"
drukt de tekst tussen dubbele quotes op het scherm af.

Als je dus geen file handle opgeeft (bij het afdrukken) wordt automatisch STDOUT gebruikt.


Arrays (en lists)

Arrays zijn geïndexeerde verzamelingen van 'scalar things'. De index van het eerste element is 0.

I.t.t. in veel andere talen hoeven de elementen niet allemaal hetzelfde type te hebben. Je kan bv. strings en getallen door elkaar gebruiken in 1 array.

De naam van een array begint met '@'. Je kan zonder problemen zowel een scalar met de naam $x als een array met de naam @x gebruiken.

Een element (bv. het 3e) in een array wordt weergeven als $x[2] (0 is het 1e element!).

Als je een element benadert dat niet bestaat krijg je een foutmelding (als je die aan hebt staan) of de waarde 0 of "".

Arrays creëren

Je kan een array bv. als volgt aanmaken:
@mix = (3, 'Europa', 4.21, 'soep', 'aap');

Er is een speciaal geval voor arrays die alleen strings van 1 woord bevatten:
@kleuren = qw(zwart, groen, rood, lila);
(waarbij 'qw' staat voor quoted words).

Een derde mogelijkheid is een array die een aaneengesloten serie (strings of getallen) moet bevatten:
@letters = ('d' .. 'p');

Aantal elementen in een array

Perl heeft een speciale manier om de index van het laatste element in een array weer te geven. Voor de array @letters zou dat bv. @#letters zijn.

Bovenstaande waarde geeft het aantal elementen - 1, maar je kan ook rechtstreeks het aantal elementen opvragen:
$numletters = @letters;

Arrays wijzigen of uitbreiden

Arrays sorteren

Sorteren is simpel:
@sortedx = sort @x;

Deze sortering is in ASCII volgorde, dus 392 komt voor 5. Numeriek sorteren is iets ingewikkelder:
@sortedx = sort {$a <==> $b}

Iets doen met elk element van een array

Vanzelfsprekend kan dit door een standaard loop te gebruiken, zoals met while, maar je kan ook gebruik maken van foreach.

foreach $elt (@x) { statements; }

Modules (en gebruik van andermans code)

Vanuit het oogpunt van hergebruik van (je eigen of andermans) code en het overzichtelijk houden van programma's, kan je gebruik maken van modules.

Er zijn 2 Perl-commando's om van modules gebruik te maken: use en require.

De module English.pm

In het begin van je programma's zou je bv. het statement use English kunnen zetten. English.pm ('pm' staat voor Perl module) is een stuk code waarin leesbare namen worden gedefiniëerd voor de vaak cryptische namen van ingebouwde Perl-variabelen.

In plaats van de $/ kan je dan bv. ook $RS of $INPUT_RECORD_SEPARATOR gebruiken.

Het zoeken van modules

Zie hier.

Het installeren van modules

Zie hier.


Fouten om van te leren

In deze sectie een serie fouten die mij de nodige hoofdbrekens hebben bezorgd. Probeer als puzzel evt. eerst zelf te kijken wat er mis is in het codefragment.

De volgende onderwerpen:

Regels zoeken in een invoerbestand

Ik wilde in een bestand regels zoeken die aan een bepaald patroon voldeden.

Het bestand specificeerde ik in het programma (dus niet via een parameter achter het perl-commando), als volgt:

use English; open ITC, 'C:\Projects\Output.txt' or die "Bestand bestaat niet\n"; while (<>) { print "$1$2$3\n" if (/PRO\s\w{2}\s(\w)\w{2}-(\w{2})-(\w{2})/); } close ITC;

Om 1 of andere duistere reden kwam er geen output op het scherm, hoewel ik vrijwel 100% zeker was dat er een aantal matchende regels waren.

Zelfs toen ik in de while-lus gewoon een keiharde print "Test" zette kwam er geen uitvoer.

Toen ging me eindelijk een lampje branden. Het is leuk dat ik een bestand open en het de naam ITC geef, maar ik zeg in de while-lus niet dat ik daaruit wil lezen.

Het moet dus zijn: while (<ITC>)

Pattern matching vindt niets

Ik wilde in een bestand regels zoeken die aan een bepaald patroon voldeden.

Het bestand specificeerde ik in het programma (dus niet via een parameter achter het perl-commando), als volgt:

use English; open ITC, 'C:\Projects\Output.txt' or die "Bestand bestaat niet\n"; while (<ITC>) { print "$1$2$3\n" if (/pro\s\w{2}\s(\w)\w{2}-(\w{2})-(\w{2})/); } close ITC;

Om 1 of andere duistere reden kwam er geen output op het scherm, hoewel ik vrijwel 100% zeker was dat er een aantal matchende regels waren.

Oplossing: ik had me niet gerealiseerd dat Perl case-sensitive is. In het bestand stond meerdere malen PRO, in hoofdletters dus.

File kan niet geopend worden

In het volgende stukje code kreeg ik steeds een foutmelding omdat de file niet geopend kon worden:

use English; $dir = "C:\Problems"; opendir COP, $dir or die "Can't open folder\n"; @files = readdir COP; foreach $f (@files) { if (-d $f) { next; } open OPL, "<", $f or die "Can't open file $f\n"; ...

Het if-statement in de regels na foreach dient om te testen of ik geen map (directory) te pakken heb. Dan ga ik verder met de volgende iteratie van de loop.

Ik kreeg de volgende foutmelding:
Use of uninitialized value in open at ...

Het probleem zit hem erin, dat de open een *volledige* filenaam nodig heeft, dus inclusief de map(pen) waarin het bestand zit.

Je kan daartoe bv. de laatste regel van het codefragment vervangen door:

$filepath = $dir . "\\" . $f; open OPL, "<", $filepath or die "Can't open file $filepath\n";

Can't find string terminator

Mijn programma begint met de volgende code:

use English; $website = 's:\website mas\stick\computer\';

Dat levert de volgende melding op:
Can't find string terminator "'" anywhere before EOF at Print text between title tags of random web page from folder to screen.pl line 2.

Oplossing: het probleem zit hem in de backslash voor de quote aan het eind. Als ik daar een extra backslash voor zet gaat het wel goed.

Waarom is $argv[0] niet goed?

Ik wil het Perl programma 1 argument meegeven, en zoals ik me (min of meer) correct herinnerde, doe je dat met $ARGV[0]. Maar waarom geeft onderstaande code dan de daaropvolgende foutmeldingen?

use English; $website = "s:\\website mas\\stick\\computer\\"; print "$website\n"; $page = $argv[0]; print "$page\n"; $fn = $website.$page; Name "main::argv" used only once: possible typo at Print text between title tags of random web page from folder to screen.pl line 4. s:\website mas\stick\computer\ Use of uninitialized value $page in concatenation (.) or string at Print text be tween title tags of random web page from folder to screen.pl line 5.

Oplossing: opnieuw vergat ik even dat Perl case-sensitive is. $ARGV is een voorgedefinieerde array, met naam in hoofdletters. Beide foutmeldingen zijn daarmee te verklaren.

Mismatch van haakjes

Ik kreeg de foutmelding
Unmatched ) in regex; marked by <-- HERE in m/\s{3}(\w{6})\s{22}ABSOLUTE\s{5}\(w {2}\sDEC\s14) <-- HERE / at itccompc.pl line 11.
in de volgende code:

while (<OPL>) { if (/\s{3}(\w{6})\s{22}ABSOLUTE\s{5}\(w{2}\sDEC\s14)/) { $comp{"$1"} = "$2"; } }

Het is niet zo dat er een haakje te veel of te weinig staat. Het probleem is dat er per ongeluk een backslash op de verkeerde plaats is beland.

In plaats van \(w{2} moet er natuurlijk staan (\w{2}

Het haakje voor de 'w' werd vanwege de backslash niet gezien.


Perl functies

De volgende functies worden besproken (in alfabetische volgorde):

chomp

Argument: string of list.

Newlines aan het eind van de string, of aan het eind van elk element in de list, worden verwijderd.

Resultaat: het aantal verwijderde tekens.

defined

Argument: variabele of array element.

Resultaat: true als de waarde ongelijk is aan undefined

Voorbeeld:

qw

qw heeft als argument een lijst van strings (zonder quotes), gescheiden door spaties.

Het resultaat is een list van individuele strings.

Je bespaart jezelf hiermee het intikken van een hoop quotes en komma's.

Voorbeeld

scalar

Argument: (bv.?) een array of list.

Resultaat: het aantal elementen in die list.
De functie behandelt het argument in een scalar context.

sort

Argument: een array of list.

Resultaat: een gesorteerde array of list.

undef

Voordat een variabele gebruikt wordt heeft hij een ongedefiniëerde waarde.

Je kan daar op testen met de de functie defined, en je kan een gedefiniëerde variabele weer ongedefinieerd maken met de functie undef.

Voorbeeld: undef $/;
Daarmee maak je de waarde van de "output record separator" undefined.




    Tweeten

© Henk Dalmolen
Reageer via E-mail (dalmolen@xs4all.nl)

Deze pagina is voor het laatst gewijzigd op: 9-6-2016 20:37:50