adelton

Perl: korektně, standardně, čistě

Jan Pazdziora

16. května 2004
EurOpen

Hotel Sněžník, Dolní Morava

Česká společnost uživatelů otevřených systémů EurOpen.CZ

Abstrakt

V tomto tutoriálu o programovacím jazyku Perl se zaměříme na korektní a pěkné postupy řešení některých úkolů, se kterými se můžeme při práci často setkat. Budeme se snažit využít standardních prostředků, nástrojů a modulů programovacího jazyka Perl. Tento text předpokládá verzi alespoň 5.8.1.

1. Úvodní přehled

Zcela jemný úvod do Perlu

1.1. Co je Perl?

Perl je programovací jazyk, ve kterém najdeme prvky C, sedu, awk, shellu a mnoha dalších. Používá se hlavně jako skriptovací jazyk, i když je možné skripty i předkompilovat, případně použít Perl v embedded zařízeních. Perl je tradičně silný ve zpracování textů, i když zpracování binárních dat samozřejmě nepředstavuje žádný problém. Široká nabídka modulů dostupných z CPANu (Comprehensive Perl Archive Network, www.cpan.org) dovoluje pracovat s většinou současných standardů, formátů a protokolů. Modul je často pouze tenkou vrstvou nad nativní knihovnou psanou v C, takže se používají standardní knihovny, s výhodou skriptovacího přístupu.

Perl znamená Practical Extraction and Report Language. Slovo Perl označuje jazyk. Slovo perl se používá pro označení interpretu, kterým provedeme skript zapsaný v Perlu. Zápis PERL nepoužívejte.

Existuje několik hesel používaných ve spojitosti s Perlem, nejdůležitější jsou TIMTOWTDI a DWIM.

1.2. Hello World

Je nutné začít vypsáním řetězce na standardní výstup:

$ perl -e 'print "Hello World!\n"'
Hello World!

Zde je celý skript uveden jako argument na příkazovém řádku za -e. Takto se často zapisují krátké jednoúčelové skripty.

Většinou je ale skript uložen v souboru, jehož jméno předáme interpretu jako argument (v příkladu opraveno řádkování):

$ cat skript.pl
#!/usr/bin/perl
print "Hello World!\n";
$ perl skript.pl
Hello World! 

Pokud dáme skriptu práva pro provádění a máme na prvním řádku za #! správnou cestu k interpretu, můžeme pak skript pustit přímo:

$ chmod a+x skript.pl
$ ./skript.pl
Hello World!

1.3. Skalární proměnné, řetězce

Základní proměnné v Perlu jsou skalární, pro uložení jedné hodnoty. Hodnota může být numerická, řetězcová, nedefinovaná, případně to může být reference. Skalární proměnné začínají znakem dolar ($):

my $x = 4;
my $y = 'Slovo';
my $z;
print "Hodnota x: $x, hodnota y: $y, hodnota z: $z\n";

Deklarací my označujeme proměnnou jako lokální, pro naše potřeby lokální proměnné budou zcela postačovat. V řetězci ovroubeném uvozovkami jsou interpolovány hodnoty skalárních proměnných. Vyzkoušejte, co se stane, pokud poslední řátek zapíšeme jako

print 'Hodnota x: $x, hodnota y: $y\n';

Perl má pro řetězcové a numerické operace různé operátory, byť hodnoty proměnných používá dle potřeby. Operátor sčítání (+) provádí aritmetické sčítání, a to i s řetězcovými hodnotami, které Perl dle potřeby převede na číslo, naopak operátor spojení řetězců (.) pracuje s řetězcovou podobou numerické hodnoty:

$z = $x + $y;
my $zz = $x . $y;
print $zz++;

Protože hodnota proměnné $y Slovo není řetězcovou reprezentací žádného čísla, její numerická hodnota je nula.

1.4. Čtení vstupu

Pro čtení jednoho řádku vstupu slouží operátor <>. Pokud neuvedeme jméno filehandlu, jsou (unixovým stylem) postupně otvírány a čteny všechny soubory uvedené jako parametry příkazového řádku, a pokud žádný soubor uveden není, tak standardní vstup:

my $line;
my $i = 1;
while (defined($line = <>)) {
	print "$i: ", $line;
	$i++;
}

Uložte tento skript s příslušným prvním řádkem (#!) do souboru numit.pl a spusťte

$ ./numit.pl /etc/passwd /etc/group /etc/hosts

Je výstup takový, jaký jste očekávali?

Uvnitř <> může být uvedeno i jméno filehandlu, například <STDIN> čte ze standardního vstupu. Chceme-li otevřít soubor, použijeme pro získání filehandlu funkci open:

local *FILE;
unless (open FILE, $name) {
	die "Error reading [$name]: $!\n";
}

Jako proměnná uchovávající hodnotu filehandlu se používají tzv. globy bez dolaru na začátku. Proto také ta trošku jiná syntaxe lokalizace, pomocí local *FILE.

1.5. Proměnná $_

Kromě námi deklarovaných skalárních proměnných zná Perl několik speciálních globálních, které existují vždy a za dolarem mají jiný znak než písmeno. Z nich nevýznamnější je $_, který je defaultním parametrem, se kterým pracují mnohé funkce Perlu, nedáme-li jim svoje vlastní data. Předchozí skript můžeme přepsat například takto:

my $i = 0;
while (<>) {
        print ++$i, ': ';
	print;
}

Zde operátor <> ve while čte řádky vstupu a přiřazuje je do $_, print bez parametrů posílá na výstup hodnotu $_.

Podobně například funkce chomp, která z konce hodnotyřetězce odstraní znak nového řádku, je-li přítomen, nebo funkce length, která vrátí délku parametru:

while (<>) {
	chomp;
	print length, "\n";
}

1.6. Regulární výrazy

Perl má velmi komplexní podporu regulárních výrazů, úzce integrovanou do své syntaxe. Chceme-li vypsat na výstup všechny řádky ze vstupu, které obsahují pětipísmenné nebo delší slovo začínající písmenem r a končící a, můžeme použít například:

while (<>) {
	print if /\br\w{3,}a\b/;
}

V cyklu while je uveden operátor //. Ten prohledává řetězec načtený do $_ pomocí regulárního výrazu \br\w{3,}a\b. V tomto výrazu \b znamená hranici mezi slovy (tedy před r musí být nepísmeno nebo začátek řetězce), r požaduje v řetězci právě toto písmeno, \w značí jakékoli písmeno, které se může objevit ve slově (standardně písmena, číslice a podtržítko), {3,} pak označuje alespoň tři opakování předchozího konstruktu. Pokud regulární výraz nad hodnotou v $_ matchuje, vrátí // pravdivou hodnotu, a proto díky if se provede print bez parametru, tedy tisk hodnoty $_.

Působí tento skript příliš nečitelně, příliš perlově? Můžeme ho přepsat i způsobem možná bližším programátorům zvyklým na C:

my $line;
while (defined($line = <>)) {
	if ($line =~ /\br\w\w\w\w*a\b/) {
		print $line;
	}
}

Novým operátorem je zde =~, který říká, nad kterou hodnotou se má provést hledání regulárním výrazem namísto $_.

Kromě vyhledávání regulárním výrazem (matchování) se často používají náhrady, substituce, pomocí operátoru s///:

	if ($line =~ s/\br(\w+)a\b/R$+A/g) {
		print $line;
	}

Zde substituce hledá alespoň třípísmenná slova začínající na r a končící na a, a nahrazuje je velkými písmeny na začátku a na konci. Speciální proměnná $+ obsahuje řetězec odpovídající ozávorkované části regulárního výrazu, tedy prostředek slova. Volba /g zajistí náhradu všech odpovídajících slov na řádku, nikoli pouze prvního.

V operátoru s/// můžeme použít k oddělení levé a pravé části i jiný znak než lomítko (/), což se často využívá, pokud chceme v regulárních výrazem použít právě lomítka a vedlo by to na méně čitelný zápis se zpětnými lomítky (backslash — \), jak ukazuje následující příkaz, který odstraní lomítko z konce řetězce:

	s/\/$//;
	s!/$!!;

1.7. Pole a seznamy

Kromě skalárních proměnných a hodnot podporuje Perl i dvě agregované struktury: pole a asociativní pole. Proměnná typu pole má na začátku zavináč (@) a obsahuje libovolný počet skalárních prvků, jejichž pozice jsou indexovené od nuly.

my @w = ( 12, 'slon', "pes", undef, 34.5);
$w[2] = $w[0] - 3;
$w[3] = $w[1] + 3;
print "Pole w: @w\n";

V Perlu je možné přiřazovat nejen skalární hodnoty do skalárních proměnných, ale i seznamy do seznamů a polí. Tak jsou i vloženy hodnoty do pole @w. Následně provádíme manipulace s jednotlivými prvky tohoto pole. Protože jde o jednotlivé skalární hodnoty, je přístup k nim uvozen znakem dolar. Je rozdíl mezi $w$w[1] — první zápis je skalární proměnná, druhý označuje druhý prvek pole @w. Výstup předchozího programu bude

Pole w: 12 slon 9 3 34.5

a ukazuje, že i proměnné typu pole jsou v uvozovkách interpolovány, s hodnotami oddělenými standardně mezerami.

Pro práci s poli používáme nejen numerické indexy, ale máme k dispozici i funkce, které přidávají a ubírají prvky na / ze začátek / konec pole: shift, unshift, pushpop:

push @w, 'next element', 312;
my $c = shift @w;

Pokud bychom se potřebovali odkázat na prvky pole odzadu, bez znalosti jeho délky, pomohou nám záporné indexy: $w[-1].

1.8. Asociativní pole, hashe

Dalším způsobem, jak uchovat v jedné proměnné více skalárních hodnot, je asociativní pole, zvané hash. Zde nejsou jednotlivé prvky adresovány (indexovány) číselnou pozicí, ale textovým klíčem. Jména proměnných typu hash začínají procentem (%), jména prvků hashů jsou opět skalární hodnoty a začínají dolarem:

my %h = ( 'Petr' => 23, 'Martina' => undef, 'Jirka' => 1, 'Milan' => 'N/A' );
$h{'Martin'} = 52 if not exists $h{'Martin'};
for (keys %h) {
	if (not defined $h{$_}) {
		$h{$_} = 0;
	}
}

Pro přístup k seznamu klíčů hodnot v hashi slouží funkce keys, podobně seznam hodnot vrátí values. Pořadí takto získaných hodnot není dáno (nejsou seřazené). Prvek hashe může existovat, ale nebýt definován, jak ukazuje příklad na hodnotě $h{'Martina'}. Na tyto rozdílné aspekty se pak dotazujeme pomocí definedexists.

1.9. Kontexty

Operace a volání funkcí Perlu se mohou vyskytovat ve dvou kontextech: skalárním a seznamovém. Například operátor matchování regulárním výrazem vrací ve skalárním kontextu 1 při úspěchu a 0 při neúspěchu, nebo v seznamovém kontextu seznam nalezených podřetězců.

my %values;
while (defined(my $line = <FILE>)) {
	if ($line =~ /^item: /) {
		my @x = ($line =~ /\d+/g);
		print "@x\n";
	} else {
		my ($key, $value) = ($line =~ /^(\S+):\s*(.*)/);
		$values{$key} = $value;
	}
}

Také funkce localtime je příkladem duálního chování v závislosti na kontextu: ve skalárním vrací řetězec ctime popisující aktuální čas, v seznamovém jednotlivé prvky data a času.

my $now = localime;
my ($sec, $min, $hour, $mday, $mon, $year, $way, $yday, $dst) = 
							localtime(time);
print "Date $now; month $mon\n";

Na kontext je třeba dávat pozor, protože například

print localtime;

vrátí řadu čísel a ne řetězec, neboť print svým parametrům dává seznamový kontext. Skalární kontext je možné explicitně vynutit:

print scalar(localtime);

Funkce localtime je také dokládá důležitost čtení dokumentace, protože hodnoty vrácené v seznamu nemusí být takové, jaké bychom intuitivně očekávali.

I proměnné typu pole mají různé chování v závislosti na kontextu — v seznamovém vrací seznam svých prvků, ve skalárním jejich počet. Ověřme výsledky následujících přiřazení:

my ($a, $b);
$a = @w;
($b) = @w;

1.10. Speciální proměnné

Kromě $_$+ poskytuje Perl velké množství speciálních globálních proměnných, například:

@ARGV
Pole argumentů příkazového řádku.
@_
Pole argumentů funkce (uvnitř funkce).
$$
PID aktuálního procesu.
@INC
Pole jmen adresářů, které Perl prohledává, když hledá moduly.
%ENV
Hash proměnných prostředí procesu.
$0
Jméno aktuálně prováděného skriptu.
$1, $2, $3, $4, ...
Po úspěšném hledání regulárním výrazem obsahuje podřetězce odpovídající ozávorkovaným částem regulárního výrazu.
$/
Řetězec označující jak jsou odděleny řádky při čtení pomocí <>, standardně "\n".
$!
Pokud dojde k chybě při systémovém volání či volání knihovních funkcí, obsahuje tato proměnná řetězec popisující chybu či numerickou hodnotu chyby.

1.11. Funkce

Uživatelské funkce se deklarují slovem sub:

sub add {
	my $total = 0;
	for (@_) {
		$total += $_;
	}
	return $total;
}

Uvnitř funkce se pak dostaneme k předaným argumentům pomocí pole @_. Návratová hodnota funkce je hodnota return, který provádění funkce ukončí, resp. hodnota posledního výrazu ve funkci, pokud k návratu dojde přes uzavírací složenou závorku. Volání je pak jednoduché, podobně jako u standardních funkcí Perlu můžeme a nemusíme používat závorky:

my $soucet = add(3, 5, 12);
my $soucet1 = add 3, 5, 12;

Funkce si často pro větší přehlednost hned za otevírací složenou závorkou vytvářejí kopie předaných parametrů v lokálních proměnných. To mimo jiné zajistí, že ve funkci omylem nezměníme prvky pole @_, které jsou aliasem na hodnoty ve volající funkci:

sub process {
        my ($dbh, $message) = @_;
	# ...
	$message =~ s/line/Line/;
	# ...
}

1.12. Package, moduly

Perl sám o sobě obsahuje množství funkcí, které dávají přímý přístup ke standardním knihovním funkcím a systémovým voláním, jmenujme namátkou fork, listen, getpwent. Ty, které nejsou přímo v jádře Perlu, jsou dostupné v modulech, z nichž mnohé jsou ve standardních distribucích Perlu. Například funkci strftime Perl jako takový nezná, ale je dostupná v modulu POSIX. Abychom funkce z daného modulu mohli ve svém programu použít, musíme provést use:

use POSIX;
print POSIX::strftime('%H:%M', localtime), "\n";

Jméno funkce pak musíme kvalifikovat plným jménem s uvedením jména modulu / package. Moduly ale často dovolují naimportovat funkci do volajícího programu uvedením jejího jména za jménem modulu v use:

use POSIX 'strftime';
print strftime('%H:%M', localtime), "\n";

V mnoha případech jsou často používané funkce z modulu naimportovány automaticky. Například funkce chdir je přímo v jádře Perlu, což už neplatí o opačné funkci, která by řekla, jaký je aktuální adresář programu. Funkci getcwd najdeme v modulu Cwd a po use Cwd je dostupná přímo ve volajícím programu:

use Cwd;
chdir '/tmp' or die "Error chdiring to /tmp: $!\n";
print "Current directory: ", getcwd, "\n";

1.13. Reference, složitější datové struktury

Skalární proměnné mohou kromě řetězců a čísel obsahovat i reference. Je tak možné se odkazovat na hodnotu jiné proměnné a pomocí referencí na anonymní pole a hashe vytvářet složitější zanořené datové struktury. Referenci proměnné získáme předřazením zpětného lomítka (\), anonymní pole vytvoříme pomocí hranatých závorek ([]) a anonymní hash pomocí závorek složených ({}):

my ($x, $y) = ( 132, 'Volvo' );
my $z = \$x;
my $people = {
	'Petr' => {
		'age' => 26,
		'children' => [ 'Petr', 'Jitka' ],
		'wife' => 'Jana',
	},
	'Tom' => {
		'age' => 35,
		'children' => undef,
	},
};

V tomto příkladu jsou $x$y skalární proměnné s normálními hodnotami, do proměnné $z je pak uložena reference na $x. V proměnné $people je uložena reference na anonymní hash, který má dva klíče, jejich hodnoty jsou opět reference na anonymní hashe, ...

Je samozřejmě otázka, jak se k takto referencovaným datům následně dostat, tedy jak reference dereferencovat. Jsou dvě cesty: předřadit znak určující typ proměnné nebo šipkou:

print "Value referenced by z: ", $$z, "\n";
print "Petr's childern: @{ $people->{'Petr'}{'children'} }\n";

Při ladění programů se složitějšími strukturami může být výhodné nechat si je vypsat. Ve standardní distrituci Perlu k tomu slouží například modul Data::Dumper, jehož použití je následující:

use Data::Dumper;
print Dumper $people;

1.14. Objekty

Perl pro objekty používá standardní datové struktury, které mohou být pomocí bless přetvořeny na objekt nějakého package. Vytváření takových packagů je mimo rozsah tohoto jemného úvodu, pro nás je ale podstatné, že objekty budeme bez obav používat. Zde je příklad použití modulu File::Temp, který poskytuje přístup k dočasným souborům. Modul má jednak funkční, ale také objektově-orientované rozhraní, které je vidět na příkladu:

use File::Temp ();
my $tempfile = new File::Temp(
	'DIR' => '/tmp',
	'TEMPLATE' => 'pomocXXXX',
	);
my $filename = $tempfile->filename;
print "Filename: ", $filename, "\n";
print $tempfile "Test\n";
$tempfile->flush();
print "Length: ", -s $filename, "\n";
$tempfile->close;
system "cat $filename";
$tempfile = undef;
if (not open FILE, $filename) {
	print "File was removed\n";
}

Pomocí new File::Temp vytvoříme objekt a do proměnné $tempfile je vrácena reference na tento objekt. Od této chvíle je tato reference vstupní bod pro práci s tímto objektem, primárně tedy pro volání jeho metod. Jaké metody objekt poskytuje je vždy třeba najít v dokumentaci (ani jméno konstruktoru nemusí být vždy new), zde voláme například metodu filename pro zjištění jména vytvořeného pomocného souboru.

1.15. Kontrola chyb

Perl poskytuje několik úrovní kontroly chyb v našem kódu. Jednak při kompilaci, kdy převádí zdrojový text do interní podoby, a jednak při běhu, kdy nás může informovat o některých méně čistých praktikách.

Modul strict dokáže okontrolovat, pokud používáme proměnné, které jsme nedeklarovali jako lokální:

$ perl
use strict;
$r = 'OK';
__END__
Global symbol "$r" requires explicit package name at - line 2.
Execution of - aborted due to compilation errors.

Chybové hlášení se odkazuje na globální proměnné (o kterých jsme řekli, že jim nebudeme dávat prostor), náprava je jednoduchá — deklarovat proměnnou $r pomocí my.

Modul warnings dokáže ohlídat například použití nedefinovaných proměnných. Standardně je Perl považuje za prázdné řetězce a bude s nimi bez problémů pracovat, ale možná jsme měli v úmyslu hodnotu definovat?

$ perl
use strict;
use warnings;
my ($r, $s) = ( 'OK' );
print "[$r] [$s]\n";
__END__
Use of uninitialized value in concatenation (.) or string at - line 4.
[OK] []

Modul warnings je náhradou za -w a globální proměnnou $^W, používané ve starších verzích Perlu. Ovlivňuje pouze ten blok, ve kterém je uveden, a tak se změna nepromítá například do volaných modulů. Dále umožňuje jemněji upřesnit, na jaké kategorie varování se má zaměřit:

$ perl
use warnings 'numeric';
my ($r, $s) = ( 'OK' );
print $r + $s, "\n";
__END__
Argument "OK" isn't numeric in addition (+) at - line 4.
0

Vidíme, že kategorie numeric ovlivňuje varování o tom, že Perl převádí řetězcovou hodnotu na numerickou, ale varování o neinicializované hodnotě proměnné $s zmizelo.

Chceme-li ihned získat i přesnější popis chyby, použijeme use diagnostics. Spouštěním perlu s volbou -c zase provedeme přeložení (do vnitřní podoby v paměti) skriptu s kontrolou, bez vlastního běhu programu.

1.16. Instalace, dokumentace

Ukázali jsme základní příklady programování v Perlu. Naskýtá se samozřejmě otázka: kde vzít perl a kde se dozvíme detaily?

Pro un*xové platformy existují binární distribuce přímo v podobě instalačních balíčků vhodných pro danou platformu. Pro Windows je pak nejoblíbenější ActivePerl od ActiveState. Chceme-li provést instalaci ze zdrojových kódů, půjdeme pravděpodobně na CPAN.

S každou distribucí dostaneme nejen perl samotný, ale i sadu standardních modulů, a samozřejmě také dokumentaci. Ta je v závislosti na platformě buď v podobě manových stránek či HTML dokumentaci. Začátek v hledání je tedy man perl, můžeme zkusit i perldoc perl, nebo v dokumentačních adresářích hledat perl.html. Dokumentace je také on-line na www.perldoc.com, a samozřejmě nám může pomoci i náš oblíbený vyhledávač.

Česká diskusní skupina o perlu je cz.comp.lang.perl, anglické jsou comp.lang.perl.misc, .modules a .moderated.

2. Znakové sady, Unicode, čeština

Jaká vlastně máme data?

2.1. Dva soubory v různých kódováních

Jen málokdy pracujeme s programem, který nezpracovává žádná vstupní data a nevytváří žádný výstup. Ve většině případů potřebujeme načíst vstup a v našem kulturním prostředí se velice rychle ocitneme uprostřed široké nabídky znakových sad a jejich kódování, kterými bychom podchytili i ony české znaky s diakritikou. Předpokládejme tedy, že máme následující dva soubory, jeden s obsahem v UTF-8 a druhý v ISO-8859-2:

$ cat input
Šípková Růženka
$ od -tx1z input
0000000 c5 a0 c3 ad 70 6b 6f 76 c3 a1 20 52 c5 af c5 be  >....pkov.. R....<
0000020 65 6e 6b 61 0a					 >enka.<
0000025
$ iconv --from-code UTF-8 --to-code ISO-8859-2 input > input2
$ # nebo cstocs utf8 il2 input > input2
$ od -tx1z input2
0000000 a9 ed 70 6b 6f 76 e1 20 52 f9 be 65 6e 6b 61 0a  >..pkov. R..enka.<
0000020

Jaké máme v Perlu možnosti tyto soubory zpracovat?

2.2. Zpracování pomocí locales

Perl standardně považuje za znaky slova pouze byty s alfanumerickou hodnotou (a-zA-Z0-9_). Tedy bez nastavení locales (či s LC_ALL=C) bude výstup skriptu klasifikujícího znaky následující:

$ perl -wlpe 's/\W/_/g' input
____pkov___R____enka
$ perl -lpe 's/\W/_/g' input2
__pkov__R__enka

Překvapivě ale ani nastavení LC_CTYPE či LC_ALL nepomůže:

$ LC_CTYPE=cs_CZ.utf8 perl -wlpe 's/\W/_/g' input
____pkov___R____enka
$ LC_ALL=cs_CZ perl -wlpe 's/\W/_/g' input2
__pkov__R__enka

Aby Perl vzal locales na vědomí, poskytují starší verze perlu možnost podporu locales explicitně zapnout pomocí use locale či zkráceně -Mlocale. Tento postup zde ukazujeme pouze pro zopakování a pro srovnání, níže bude následovat novější, standardnější a všeobecně hezčí řešení:

$ LC_CTYPE=cs_CZ perl -Mlocale -wlpe 's/\W/_/g' input2 | od -tx1z
0000000 a9 ed 70 6b 6f 76 e1 5f 52 f9 be 65 6e 6b 61 0a  >..pkov._R..enka.<
0000020
$ LC_CTYPE=cs_CZ perl -Mlocale -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input2
a9 ed 70 6b 6f 76 e1 20 52 f9 be 65 6e 6b 61

Vidíme, že načtená a zpracovávaná zůstávají stejná, první načtený byte má stále hodnotu 0xa9 = 0251 = 169 = Š v ISO-8859-2. Modul locale pouze zajistí chápání příslušných bytů v regulárních výrazech (například dle \w, \l či [[:...:]]) a v příkazech lc nebo ucfirst podle kategorií aktivních locales.

Nicméně, pro UTF-8 toto neplatí, a to ani když použijeme správné UTF-8 locales:

$ LC_CTYPE=cs_CZ.utf8 perl -Mlocale -wlpe 's/\W/_/g' input
____pkov___R____enka

Je čas přejít na Unicode.

2.3. Čtení UTF-8 dat

$ LC_CTYPE=cs_CZ.utf8 perl -CSADL -wlpe 's/\W/_/g' input
Šípková_Růženka
$ perl -CSADL -wlpe 's/\W/_/g' input
____pkov___R____enka
$ LC_CTYPE=cs_CZ.utf8 perl -CSAD -wlpe 's/\W/_/g' input
Šípková_Růženka
$ perl -CSAD -wlpe 's/\W/_/g' input
Šípková_Růženka
$ LC_CTYPE=cs_CZ.utf8 perl -C -wlpe 's/\W/_/g' input
Šípková_Růženka

Volba -C zapíná interní unicodový pohled na zpracovávaná data, -CDS je ekvivalentem use open ':utf8', ':std'. Přepínač D zapne UTF-8 vstupně-výstupní vrstvu, S toto zapnutí rozšíří i na standardní filehandly STDIN, STDOUT a STDERR, A to samé provede s argumenty příkazového řádku. Volba L učiní toto chování podmíněným na nastavených UTF-8 localech.

Tato interní podpora Unicode v Perlu je nezávislá na nainstalovaných a nastavených localech. Tedy má-li program číst data v UTF-8, volbou -CSD vynutíme chápání bytů na vstupu jako UTF-8, bez ohledu na to, s jakými locales bude program spuštěn. Oproti localovému přístupu je zde podstatná změna: znaky jsou vnitřně chápány jako unicodové code pointy, bez ohledu na to, jestli byly na vstupu tvořeny jedním či čtyřmi bajty:

$ perl -CSAD -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input
160 ed 70 6b 6f 76 e1 20 52 16f 17e 65 6e 6b 61

Vidíme, že první znak 0x160 = LATIN CAPITAL LETTER S WITH CARON vznikl z prvních dvou bytů, následující znak je 0xed = LATIN SMALL LETTER I WITH ACUTE. Hodnota 0x160 není reprezentovatelná v osmi bitech, evidentně tedy Perl místo bytů pracuje se znaky. Toto znakově-orientované zpracování se netýká pouze regulárních výrazů, vyzkoušejme test délky řetězce:

$ perl -CSAD -wlpe 's/(\S+)/length $1/ge' input
7 7
$ perl -wlpe 's/(\S+)/length $1/ge' input
10 9

Perl nyní obsahuje informaci o všech znacích z repertoáru Unicode včetně definice kategorií. V regulárních výrazech se můžeme ptát na znaky pomocí \x{160} či \N{LATIN SMALL LETTER U WITH RING ABOVE} (je třeba use charnames ':full') nebo vlastnosti jako \p{UppercaseLetter}, \p{DashPunctuation} či \p{Cyrillic}.

2.4. Vstupně-výstupní překódování

Umíme tedy zpracovat UTF-8 vstup. Jsme ale stejný mechanismus schopni použít i na data v ISO-8859-2, nebo budeme nuceni používat na různá data různý kód? Unicodová magie totiž pracuje pouze s korektním UTF-8:

$ perl -CSAD -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input2
utf8 "\xA9" does not map to Unicode, <> line 1.
Malformed UTF-8 character (unexpected continuation byte 0xa9, with no preceding start byte) in substitution iterator at -e line 1, <> line 1.
Malformed UTF-8 character (unexpected non-continuation byte 0x70, immediately after start byte 0xed) in substitution iterator at -e line 1, <> line 1.
Malformed UTF-8 character (unexpected non-continuation byte 0x20, immediately after start byte 0xe1) in substitution iterator at -e line 1, <> line 1.
Malformed UTF-8 character (unexpected non-continuation byte 0x65, 1 byte after start byte 0xf9, expected 5 bytes) in substitution iterator at -e line 1, <> line 1.
a9 ed 6f 76 e1 f9 61

Řešení spočívá ve využití automatického překladu vstupu (či výstupu) do vnitřní unicodové podoby, například pomocí use open ':locale':

$ LC_CTYPE=cs_CZ perl -Mopen=:locale -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input2
160 ed 70 6b 6f 76 e1 20 52 16f 17e 65 6e 6b 61

Tato direktiva (zde zapsaná na příkazovém řádku) říká, že se má použít taková vstupně-výstupní vrstva, která znakové charakteristiky odvodí z aktuálních locales. Oproti -Mlocale je zde podstatné, že vnitřně jsou data zpracovávána jako Unicode a ISO-8859-2 je použito pouze pro vstup a výstup. Samozřejmě, neodpovídají-li locales datům na vstupu, nebude ani výsledek správný:

$ perl -Mopen=:locale -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input2
ascii "\xA9" does not map to Unicode.
ascii "\xED" does not map to Unicode.
ascii "\xE1" does not map to Unicode.
ascii "\xF9" does not map to Unicode.
ascii "\xBE" does not map to Unicode.
5c 78 41 39 5c 78 45 44 70 6b 6f 76 5c 78 45 31 20 52 5c 78 46 39 5c 78 42 45 65 6e 6b 61

Pokud nechceme mít program závislý na uživatelově nastavení locales a přitom chceme načíst data v ISO-8859-2, můžeme kódování uvést přímo:

$ perl '-Mopen=:encoding(iso-8859-2)' -wlpe 's/(.)/sprintf "%x ", ord $1/ge' input2
160 ed 70 6b 6f 76 e1 20 52 16f 17e 65 6e 6b 61

Pokud ale zapíšeme poslední příklad v těle programu, musíme navíc použít :std, protože :encoding se standardně týká pouze nově vytvářených filehandlů:

$ cat dump_il2
#!/usr/bin/perl -wl
use open ':std', ':encoding(iso-8859-2)';
while (<>) {
	print join " ", map { sprintf "%x", ord } split //;
}
__END__
$ ./dump_il2 input2
160 ed 70 6b 6f 76 e1 20 52 16f 17e 65 6e 6b 61 a

Je samozřejmě věcí cti vyzkoušet, co se stane, pokud ten samý program spustíme nad vstupem v UTF-8:

$ ./dump_il2 input
139 a0 102 ad 70 6b 6f 76 102 104 20 52 139 17b 139 17e 65 6e 6b 61 a

Podpora různých kódování Unicode v nových verzích Perlu dovoluje napsat velice snadno konvertor:

use open ":std",
	IN => ":encoding(iso-8859-2)",
	OUT => ":encoding(windows-1250)";
while (<>) { print }

Zde jsou ovlivněny všechny vstupy a výstupy. Pomocí tří argumentové funkce open nebo funkcí binmode, která nyní může mít i druhý argument, je možné požadovanou vstupně-výstupní vrstvu zvolit a měnit i u jednotlivých filehandlů:

#!/usr/bin/perl -w
open FILE2, '<:encoding(iso-8859-2)', 'input2' or die $!;
binmode STDOUT, ':utf8';
while (<FILE2>) { print }
open FILE, '<:utf8', 'input' or die $!;
binmode STDOUT, ':encoding(utf7)';
while (<FILE>) { print }

Výstup je pak očekávaný:

Šípková Růženka
+AWAA7Q-pkov+AOE- R+AW8Bfg-enka

2.5. Texty v perlovém kódu

Pokud ve zdrojovém kódu použijeme byty s hodnotou vyšší než 127, jejich interpretace opět může být různá. Nové verze Perlu zavádějí jako primární kódování zdrojových kódů UTF-8. Protože je ale na světě mnoho (stále asi většina) skriptů a modulů v kódováních jiných, je nutné Perl na zdroj v UTF-8 upozornit použitím use utf8:

use utf8;
$_ = "Šípková Růženka\n";
print;

Do proměnné $_ je pak uloženo 16 znaků namísto 21 bytů. Překvapivě ale spuštění hlásí varování:

$ perl skript.pl
Wide character in print at skript.pl line 3.
Šípková Růženka

Ano, snažíme se protlačit znak s hodnotou větší než 255 na STDOUT, který ale nemá definovanou žádnou vstupně-výstupní vrstvu, která by ho uměla zobrazit. Perl ale i přes varování provede převod do UTF-8, jak se můžeme přesvědčit:

$ perl skript.pl | od -tx1z
Wide character in print at skript.pl line 3.
0000000 c5 a0 c3 ad 70 6b 6f 76 c3 a1 20 52 c5 af c5 be  >....pkov.. R....<
0000020 65 6e 6b 61 0a					 >enka.<
0000025

Jak se tedy zbavit onoho varovného hlášení? Odstraněním příčiny. Pomocí argumentů -CSD, use open ':utf8', ':std', nebo třeba binmode STDOUT, ':utf8' Perlu řekneme, že je v pořádku, že přes STDOUT půjdou znaky a že je má převést do UTF-8. Dokážete z předchozích příkladů odvodit, jak zařídit, aby texty zapsané v kódu v UTF-8 šly na výstup v ISO-8859-2? A jak zařídit, aby reagovaly na nastavené locales, tedy pro LANG=en_US.utf8 byl výstup v UTF-8 a pro LANG=cs_CZUTF-8ISO-8859-2, bez nutnosti změny kódu?

Poté, co jsme schopní mít zdrojový text v UTF-8 a řádně na to Perl upozornit, objeví se zákonitě námitka, že by bylo vhodné použít nové znakově orientované chování Perlu i pro zdrojové kódy v jiných kódováních. Modul encoding dovoluje analogicky k use utf8 říci, že vstup je v daném kódování, například use encoding 'iso-8859-2'.

2.6. Změny kódování uvnitř skriptu

Jsou samozřejmě případy, kdy vstup nemá pouze znakové charakteristiky. Například .dbf soubory, komunikace s GSM ústřednou či RPM formát obsahují jak binární řídící informaci, tak textová data, v nějaké znakové sadě / kódování. Abychom nepřišli o binární údaje, musíme vstup načíst pomocí binárního vstupu. K tomu slouží vstupně-výstupní vrstva :bytes, případně binmode s jedním argumentem. Data jsou pak načtena jako řetězec bytů. S těmito řetězci pak můžeme dále pracovat a provést například převod do kódování dle naší volby. Nejsme tedy odkázaní na konverze pouze při vstupu a výstupu.

Pro převody mezi znakovými sadami slouží modul Encode a jeho funkce encodedecode. Tyto funkce na jedné straně pracují s řetězcem bytů ve specifikovaném kódování a na straně druhé s interními unicodovými řetězci. Načtěme nyní data v ISO-8859-2 jako byty a převeďme je do Windows-1250. Do výstupu dále vložíme řetězec uvedený v těle skriptu v UTF-8:

use Encode;
use utf8;
open FILE, '<:bytes', 'input2';
binmode STDOUT;
while (<FILE>) {
	my $u = "Výstup: " . Encode::decode('iso-8859-2', $_);
	print Encode::encode('windows-1250', $u);
}

V proměnné $u je tedy uložen řetězec v interní znakové podobě, přes STDOUT (nastavený pomocí binmode) pak jdou ven byty ve Windows-1250. Pokud bychom chtěli přímo převod mezi různými kódováními bez mezikroku, kterým v tomto případě byl řetězec znaků, můžeme z modulu Encode použít například i funkci from_to.

Protože jednotlivé znakové sady / kódování Unicode nemusí zahrnovat stejné množiny znaků, mají funkce decode, encode či from_to i dodatečný volitelný parametr, kterým můžeme říci, co se má stát v případě, že vstup neodpovídá specifikovanému vstupnímu kódování, resp. kdy znak není v cílovém kódování nalezen. Možnosti se liší od vložení náhradního znaku, přes die až po částečné zpracování, které dovolí libovolnou opravu. Pokud bychom v předchozím příkladu převáděli do ISO-8859-1, kde české znaky nejsou, můžeme provést následující nahrazení námi specifikovaným znakem:

	my $u = "Výstup: " . Encode::decode('iso-8859-2', $_);
	while ($u ne '') {
		print Encode::encode('iso-8859-1', $u, Encode::FB_QUIET);
		$y$u =~ s/^./-/;
	}

V úvahu přichází i libovolné jiné zpracování, od postupné transliterace až po hlášení o chybě emailem.

3. Databáze

Perl jako jediný databázový klient

3.1. DBI

Pro přístup k databázovým systémům se v Perlu používá DBI, Database Interface. Jde o modul a specifikaci poskytující sadu metod pro připojení k databázovému systému, zadání příkazu či dotazu a získání vysledku, spolu s transparentním mapováním datových typů mezi Perlem a jednotlivými databázovými implementacemi. Cílem je mít stejný Perlový kód pro stejné akce nad různými databázemi, například pro odeslání dotazu či načtení jednoho záznamu výsledku. Na druhou stranu se DBI nesnaží skrývat a smazávat rozdíly v SQL syntaxích, datových typech, rozšířeních či nedostatcích jednotlivých systémů — na to existují na CPANu jiné moduly.

3.2. Připojení k databázovému serveru

Abychom mohli s databázovým systémem pracovat, musíme se k němu nejprve připojit, a získat tak databázový handle. Na to slouží v DBI metoda connect:

use DBI;
my $dbh = DBI->connect('dbi:Oracle:prod9i', 'scott', 'tiger',
	{ AutoCommit => 0, RaiseError => 1 });

Jako první argument předáváme řetězec popisující databázový zdroj. Řetězec začíná dbi:, následuje jméno databázového driveru, a za další dvojtečkou podrobnější popis, jehož syntaxe je závislá na typu databáze a použitém driveru. Jako další dva argumenty uvádíme jméno databázového uživatele a heslo (mohou být nedefinované, tedy uvedené jako undef), jako čtvrtý pak anonymní hash s atributy spojení.

DBI se pak pokusí natáhnout modul s driverem odpovídajícím dané databázi, v tomto případě DBD::Oracle a použít ho pro vytvoření databázového spojení. Pro databází PostgreSQL slouží driver DBD::Pg a za druhou dvojtečkou pak následuje jméno databáze, případně jméno stroje ve formátu dbi:Pg:host=ax241;dbname=telis, pro MySQL driver DBD::mysql, kde pokračování popisu databázového zdroje je hodně podobné DBD::Pg, pro přístup k MS SQL doporučuji DBD::Sybase s knihovnou FreeTDS, případně DBD::ODBC, a pro zpracování .dbf souborů DBD::XBase.

3.3. Zadání příkazu

Máme-li databázový handle, můžeme s databází komunikovat. Můžeme například vytvořit tabulku:

$dbh->do('create table tst1 (id integer not null, name varchar(20))');

Jako parametr metody do předáváme SQL příkaz, v textové podobě. Jeho syntaxe je závislá na tom, s jakým databázovým systémem pracujeme — některé podporují not null, některé dovolují již v create table specifikovat primary key, některé umí auto_increment, jiné jim říkají serial, další vůbec tuto funkčnost nemají. Z hlediska DBI jde pouze o textový příkaz, který je předán ke zpracování serveru.

Návratová hodnota metody do je definovaná při úspěchu. Pokud máme navíc $dbh s atributem RaiseError, neúspěch přímo vyvolá výjimku, tedy die.

Databázové servery se liší i v tom, zda DDL operace jako je vytváření tabulek automaticky potvrzují transakce. Pokud nikoli, může následovat

$dbh->commit;

Opět, pokud bychom neměli databázový handle s nastaveným RaiseError, bylo by vhodné testovat návratovou hodnotu.

Následně chceme do tabulky vložit data. Můžeme opět použít metodu do:

$dbh->do("insert into tst1 values (1, 'krtek')");

Pokud budeme ten samý příkaz provádět opakovaně, můžeme si ho nejprve připravit a následně provádět již připravený přes tzv. statement handle:

my $sth = $dbh->prepare("insert into tst1 values (1, 'krtek')");
$sth->execute;

Tento příklad není příliš užitečný, máme v programu o jeden řádek a jednu proměnnou navíc. Ovšem mnohé databázové servery podporují placeholdery a bindované parametry, ať již nativně, nebo emulací v databázovém driveru. Jeden připravený příkaz tak můžeme provádět s různými daty:

my $sth = $dbh->prepare('insert into tst1 values (?, ?)');
while (<>) {
	chomp;
	my ($id, $name) = split /:/;
	$sth->execute($id, $name);
}
$sth$dbh->commit;
$sth$dbh->disconnect;

Tento postup je nejen hezčí, rychlejší a čistší, ale také bezpečnější, oproti často viděným případům

my $q = new CGI;
my $data = $q->param('udaj');
$dbh->do("insert into tst1 values ('$data')");

kdy je velice snadné vložit do prováděného příkazu kód místo dat. Pokud to tedy databázový server podporuje, vždy budeme používat placeholdery a bindované parametry. Pokud nikoli, je třeba vkládané hodnoty alespoň správně ošetřit proti nepěkným znakům metodou quote:

my $q = new CGI;
my $data = $dbh->quote($q->param('udaj'));
$dbh->do("insert into tst1 values ($data)");

3.4. Čtení dat

Příkaz pro vytvoření tabulky nebo vložení záznamu vrací pouze jednu hodnotu, signalizující úspěch či neúspěch, resp. počet ovlivněných záznamů. Následně ale budeme chtít data přečíst. Můžeme na to použít už naznačený postup prepare, execute, a následně čtení jednotlivých záznamů:

my $sth = $dbh->prepare('select name, street, city from tst2 where name like ?');
$sth->execute('%' . $n . '%');
while (my $row = $sth->fetchrow_arrayref) {
	# v $row je reference na tříprvkové pole
	my $street = $row->[1];
	# ... další zpracování
}

Kromě fetchrow_arrayref jsou k dispozici i fetchrow_arrayfetchrow_hashref, které vrátí postupně jednotlivé záznamy jako pole (místo reference na pole u fetchrow_arrayref), resp. referenci na hash, kde klíče jsou názvy sloupců. Můžeme ale také chtít vrátit všechny záznamy naráz, pomocí fetchall_arrayref, kdy výsledkem je reference na pole referencí na pole, která tvoři jednotlivé záznamy. Pokud metodě fetchall_arrayref dáme jako první parametr anonymní hash, jsou jednotlivé záznamy vráceny jako hashe:

my $sth = $dbh->prepare('
	select comp_name, street, city from tst2
	where city = ?
	order by name');
$sth->execute($_);
my $data = $sth->fetchall_arrayref({});
for my $row (@$data) {
	print $row->{'COMP_NAME'}, "\n";
	# ... další zpracování
}

Při načítání záznamů způsoby, které vracejí hashe, je vhodné si ověřit, jaké jsou klíče odpovídající jménům sloupců. Chceme-li mít program přenositelný mezi různými databázovými servery, můžeme pomocí

$dbh->{FetchHashKeyName} = 'NAME_uc';
# dbh->{FetchHashKeyName} = 'NAME_lc';

před provedením prepare či atributem při connectu říci, že jméno má být vždy převedeno na velká či písmena.

Načtení všech záznamů naráz samozřejmě znamená jejich uchovávání v paměti alokované perlovým procesem, takže se nehodí pro akce s miliony záznamů. Může být ale vhodné pro menší akce, kde v následném postprocessingu sadu výsledků projdeme a zpracujeme v Perlu, kdy se například akce mohou lišit podle toho, jaký je následující záznam.

Načtení všech záznamů je možné i bez vytváření statement handlu, pomocí selectall_arrayref:

my $data = $dbh->selectall_arrayref('select comp_name, street, city from tst2
        where city = ?
	order by name', { Slice => {} }, $_);

Druhý argument za řetězcem s SQL příkazem je hash atributů, zde jsme zadali Slice, který je následně předán fetchall_arrayref, kde zajistí vrácení jednotlivých záznamů jako hashů. Není jasné, jaká je výsledná datová struktura? Pomůže nám

use Data::Dumper;
print Dumper $data;

V některých případech víme, že náš dotaz vrátí nejvýše jeden záznam, případně nás zajímá pouze první záznam. V takovém případě můžeme svůj program zkrátit použitím selectrow_array či selectrow_arrayref:

my $name = $dbh->selectrow_array('select name from companies where id = ?',
								{}, $id);

3.5. Přístup na vzdálené stroje

Při použití DBI se pro komunikaci s konkrétním databázovým serverem používají databázové drivery, které jsou typicky linkované s nativními klientskými knihovnami daného serveru. Co ale v případě, že chceme přistupovat k databázovému systému na vzdáleném stroji a na klientském systému nejsou knihovny, které by v databázovém driveru zajistily síťovou komunikaci, dostupné? Případně jde o databázi, ktará vůbec vzdálený přístup nepodporuje?

Existuje driver DBD::Proxy, který má za úkol zajistit síťové spojení a překlenout tak vzdálenost mezi klientským počítačem a serverem. Na serveru se pak použije driver s nativními knihovnami pro připojení k (tam) lokálnímu serveru.

Na straně serveru je třeba spustit proxy server, který bude přijímat spojení od klientů. Přímo s distribucí DBI přichází dbiproxy, který nabízí vysoce konfigurovatelnou možnost, jak nastavit autentizaci a autorizaci klientů pro přístup k jednotlivým akcím. Na straně klienta se pak připojujeme k databázovému zdroji dbi:Proxy:, za druhou dvojtečkou pak je uveden cílový stroj, případně port a další podrobnosti, a za dsn= databázový zdroj, který se má použít na cílovém serveru:

my $dbh = DBI->connect('dbi:Proxy:hostname=as3;dsn=dbi:XBase:/home/telis/db');

3.6. Znakové sady

Standard DBI sám o sobě problematiku znakových sad neřeší. Je tedy třeba vycházet z dokumentace jednotlivých databázových serverů a databázových driverů.

Například Oracle odvozuje znakovou sadu klienta z proměnné prostředí NLS_LANG a zajišťuje případný překlad řetězců. Je-li znaková sada klienta UTF-8, driver DBD::Oracle pak u načtených řetězců nastaví UTF-8 příznak. U PostgreSQL se znaková sada klienta nastavuje SQL příkazem set names či set client_encoding, případně proměnnou prostředí PGCLIENTENCODING. Má-li $dbhDBD::Pg nastaven atribut pg_enable_utf8 a klientské kódování je nastaveno na UTF-8, mají vrácené řetězce nastaven UTF-8 příznak. V MySQL se překlad znakových sad zapíná SQL příkazem set character set a pro zpracování UTF-8 není žádná zvláštní podpora — je třeba provádět Encode::decode nebo utf8::decode zvlášť.

3.7. Interaktivní práce

V distribuci DBI najdeme také klientský terminálový program dbish, který může být vhodnou alternativou ke klientům jednotlivých databází. Poskytuje totiž jednotné prostředí, navíc se snadnou možností přesměrování výstupu do roury či spuštění perlového příkazu přímo z interaktiní řádky. Spuštění je snadné:

$ dbish 'dbi:Pg(AutoCommit=0):dbname=voice' user pass

a tento příklad ukazuje, že při správném použití apostrofů můžeme atributy spojení uvést i v závorkách za jménem databzového driveru.

4. Web a Internet

Síťové služby

4.1. LWP::Simple

Pro práci snad s každým otevřeným protokolem dnešního Internetu existuje v Perlu modul, který nám poskytne jednoduché objektově-orientované rozhraní. Zde se podíváme na některé z nich. Obecně platí, že než začneme kódovat vlastní modul na zpracování protokolu XYZ, je dobré se podívat na CPAN, zda modul s XYZ v názvu již neexistuje a nedělá přesně to, co potřebujeme.

Začneme jednoduše, modulem LWP::Simple. Dovoluje nám provést jednoduché GET požadavky protokolem HTTP, například funkcí getprint, která po stažení dokument jednoduše vytiskne na standardní výstup:

$ perl -MLWP::Simple -e 'getprint "http://www.europen.cz/"' | head -5
<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.0 Final//EN">
<html>
<head>
<title>
EurOpen CZ

Modul ale umí nejen HTTP (a máme-li nainstalován modul IO::Socket::SSL či Crypt::SSLeay, pak také HTTPS), ale například i protokol FTP:

$ perl -MLWP::Simple -e 'getprint "ftp://ftp.linux.cz/"'
drwxr-xr-x   3 ftpadm   ftpadm         60 Nov  5  2000 etc
drwxr-xr-x   2 ftpadm   ftpadm       4096 Dec 18  2000 http
drwxr-xr-x   3 ftpadm   ftpadm          0 May  6 04:37 mount
drwxr-xr-x  28 ftpadm   ftpadm       4096 May  6 04:54 pub

4.2. LWP plné

Modul LWP::Simple je jednoduché funkční rozhraní nad LWP, Library for WWW in Perl. Jde o sadu modulů, kterými ovlivníme snad každý aspekt komunikace, kterou chceme se vzdálenými servery vést:

use LWP;
my $agent = new LWP::UserAgent(agent => 'test', timeout => 10);
my $request = new HTTP::Request(GET => 'http://www.fi.muni.cz/~adelton/perl/',
	new HTTP::Headers( 'Accept-Language' => 'en' ) );
my $response = $agent->request($request);
if ($response->is_success) {
	print $response->header('Content-Language'), "\n";
	print $response->content;
} else {
	print $response->error_as_HTML;
}

Čtení dokumentace je důležité, protože LWP je postaven na vzájemném předávání a vracení objektů, například metoda request nevrací obsah odpovědi, ale objekt, kterého se dále metodami můžeme ptát na vlastnosti odpovědi, jako jsou status, hlavičky či tělo.

4.3. Delší interakce s Webovými službami

Webové služby, se kterými chceme programově pracovat, často vyžadují sérii kroků k vytvoření nějakého stavu či transakce. To zahrnuje buďto zpracování a předávání cookies, nebo alespoň průchod přes sadu formulářů. Pomocí HTML::Parser můžeme obsah vrácených webových stránek zpracovat a napsat tak skript, který bude na jejich obsah reagovat, i zde ale existují moduly, které nám dovolí pracovat na pokud možno co nejvyšší úrovni. Jedním z nich je WWW::Mechanize:

use WWW::Mechanize;
my $m = new WWW::Mechanize(
        agent => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)',
        autocheck => 1,
        );
$m->add_header('Accept-Language' => 'en');

$m->get('http://www.google.com/');
$m->follow_link(text_regex => qr/advanced.+search/i);

$m->submit_form(
        fields => {
                as_q => 'adelton',
                lr => 'lang_en',
		as_qdr => 'm3',
        });
if ($m->find_link(text_regex => qr/perl/i)) {
	$m->follow_link(text_regex => qr/perl/i);
	print $m->uri, "\n";
}

4.4. Pošta a MIME

Předpokládejme, že máme systém, který přijímá emaily a zpracovává je, ukládá do databáze. Zároveň by měl být schopen správně uložit Subject a seznam adresátů, pro další použití. Moduly MIME::ParserMail::Address nám mohou pomoci:

use MIME::Parser;
use Mail::Address;
use Encode;
$/ = undef;					# budeme číst celý vstup
my $mime_parser = new MIME::Parser;
$mime_parser->output_to_core(1);
$mime_parser->tmp_to_core(1);
my $entity = $mime_parser->parse_data(<STDIN>);
my $head = $entity->head if defined $entity;
my (@adresati, $subject);
if (defined $head) {
        my $to = $head->get('Resent-To');
        if (not defined $to) {
                $to = $head->get('To');
        }
	for my $address (Mail::Address->parse($to)) {
		push @adresati, $address->address;
	}
	$subject = Encode::decode("MIME-Header", $head->get('Subject'));
}

Pokud naopak potřebujeme email vytvořit, použijeme MIME::Entity a příbuzné moduly:

my $entity = build MIME::Entity (
	'Type' => 'text/plain',
	'Charset' => 'ISO-8859-2',
	'Encoding' => '8bit',
	'MIME-Version' => '1.0',
	'Message-Id' => $message_id,
	'Subject' => $subject,
	'To' => $to,
	'Data' => $email,
	);
if (@attachments) {
	$entity->make_multipart;
}
for my $part (@attachments) {
	$entity->add_part($part);
}

A samozřejmě i samotné poslání emailu zajistíme přímo v Perlu, třeba přes Mail::Mailer nebo Mail::Sender. Různé moduly poskytují různě detailní možnosti nastavení, z čehož na jedné straně plyne možnost vyhrát si i s drobnostmi nebo naopak použít rozumná standardní nastavení, na druhé straně pak použití vede na různě dlouhý kód.

4.5. XML

Potřebujeme-li zpracovat data v XML, můžeme si napsat vlastní sadu regulárních výrazů ... nebo použít existující moduly, které nejenže budou pracovat rychleji, ale zajistí za nás i validaci, jmenné prostory (namespace), entity, a vše, co k tomu patří:

use XML::LibXML ();
use XML::LibXML::XPathContext ();

my $parser = new XML::LibXML;		# v produkčním kódu testovat
my $dom = $parser->parse_string($data);		# návratové hodnoty
my $xc = new XML::LibXML::XPathContext($dom);
$xc->registerNs('epp', 'urn:ietf:params:xml:ns:epp-1.0');
my %data;
for my $node ( $xc->findnodes('
	//epp:response/epp:trID/epp:svTRID | //epp:response/epp:trID/epp:clTRID
	| //epp:response/epp:result/@code | //epp:response/epp:result/epp:msg
	') ) {
	my $name = $node->nodeName;
	if ($name eq 'code') {
		$data{$name} = $node->nodeValue;
	} else {
		$data{$name} = $node->textContent;
	}
}

Modul XML::LibXML využívá knihovnu libxml2 a na pár řádcích jsme zde napsali kód, který korektně zpracuje vstupní XML a použije XPath na nalezení elementů a atributů uvnitř dat. Síla Perlu spočívá právě v tom, že můžeme snadno mixovat elementární postupy jako jsou regulární výrazy a manipulace s textem, s moduly, které pracují na poměrně vysoké úrovni.

5. mod_perl

Kde se Perl setkává s Apachem

5.1. Co je mod_perl

Perl se často používá k psaní CGI skriptů, za pomoci různých modulů, jako je například CGI.pm, který poskytuje sadu metod pro přístup k parametrům požadavku, i pro vytváření HTML výstupu. Ovšem nejextrémnější způsob, jak je možné použít funkčnost Perlu na WWW serveru, je embedded interpret Perlu přímo v Apache serveru, mod_perl.

Module mod_perl je ve verzi 1.99, která pracuje s Apache2, tenkým rozhraním nad vnitřnostmi Apache. Dovoluje nám tak sáhnout na mnohé části vnitřního fungování Apache stejně, jako bychom to dokázali v C. Samozřejmě je možné se zaměřit pouze na fázi generování výstupního (HTML) dokumentu, kde získáme výhodu rychlosti předkompilovaných modulů a skriptů.

5.2. Handler generující výstup

V konfiguračním souboru Apache HTTP serveru (httpd.conf) je třeba říci, že se má modul mod_perl natáhnout, a následně interpretu říci, že má použít základní modul perlové části mod_perlu:

LoadModule perl_module modules/mod_perl.so
PerlModule Apache2

Uveďme nyní krátký modul generující výstup:

package MP::test_handler;
use Apache::RequestIO ();
use Apache::RequestRec ();
use Apache::Const -compile => qw(OK);
sub handler {
	my $r = shift;
	$r->content_type('text/plain');
	$r->print("test handleru\n");
	return Apache::OK;
}
1;

Nyní musíme nakonfigurovat, pro jaká URL bude handler v tomto modulu spouštěn:

<Location /test>
	SetHandler perl-script
	PerlResponseHandler MP::test_handler
</Location>

Pokud jsme modul MP::test_handler (v souboru MP/test_handler.pm) uložili do adresáře, který není v cestě perlu, je možné interpretu přímo v konfiguračním souboru Apache řici dodatečnou cestu:

PerlSwitches -I/home/adelton/mod_perl

Nastartujeme Apache a zkusíme, co na nás čeká na URL http://localhost/test — buď oblíbeným Webovým prohlížečem, nebo telnetem, či třeba řádkovým nástrojem z LWP, lwp-request. Je vidět, že odpověď je zcela dle očekávání — správný status, správný Content-Type i tělo odpovědi:

$ lwp-request -s -e http://localhost/test
200 OK
Connection: close
Date: Mon, 03 May 2004 19:36:08 GMT
Server: Apache/2.0.48 (Fedora) mod_perl/1.99_12 Perl/v5.8.2
Content-Type: text/plain
Client-Date: Mon, 03 May 2004 19:36:08 GMT
Client-Peer: 127.0.0.1:80
Client-Response-Num: 1
Client-Transfer-Encoding: chunked
 
test handleru

Pomocí ab můžeme ověřit, že provádění tohoto handleru je asi o 30 procent pomalejší, než prosté odeslání souboru na výstup, ale asi čtyřikrát rychlejší, než spuštění externího procesu a provedení CGI skriptu

#!/usr/bin/perl
print "Content-Type: text/plain\n\ntest CGI\n";

5.3. Registry

Protože mnoho aplikací je napsáno právě jako CGI skripty v Perlu a jejich přepsání do podoby korektních handlerů s použitím modulů mod_perl by vzalo příliš mnoho času, můžeme pro jednodušší skripty zkusit použít modul ModPerl::Registry či ModPerl::RegistryBB:

SetHandler perl-script
PerlResponseHandler ModPerl::RegistryBB
PerlOptions +ParseHeaders
Options +ExecCGI

Pak budou i externí CGI skripty zkompilovány interpretem v Apache serveru a použity opakovaně pro opakované přístupy a výkon bude podobný jako u specializovaných handlerů. Je vhodné provést kontrolu skriptů, pro které chceme persistentní provádění pomocí Registry zapnout. Vzhledem k tomu, že skripty jsou převedeny do podoby perlové funkce a volány opakovaně, je vhodné odstranit veškerá použití globálních proměnných či správně uzavírat soubory ... vše, co by mohlo vést na nepříjemné ovlivňování jednoho požadavku druhým.

5.4. Přístup k databázím

S databázemi se v prostředí mod_perlu pracuje pomocí DBI stejně jako v běžných programech. Protože je ale interpret perlu ponechán v paměti, může být výhodné využít možnosti modulu Apache::DBI, který si udržuje seznam otevřených spojení, maskuje disconnect a při dalším požadavku na to samé databázové spojení vrátí spojení již otevřené.

Díky tomu odpadá režie navazování spojení s databázovým serverem. Modul Apache::DBI provádí automaticky rollback všech neukončených transakcí, takže se nemusíme obávat, že by akce provedené (a necommitnuté) v jednom skriptu byly následně chybně potvrzeny ve skriptu dalším, který databázové spojení zdědil. Je ale třeba dávat si pozor na to, že počet otevřených databázových spojení může výrazně narůst, protože tyto nejsou sdíleny mezi jednotlivými procesy Apache, a jsou udržovány otevřeny i poté, co byl HTTP požadavek vyřízen.

6. Závěr

V úvodu jsme si ukázali základy obraty v Perlu, v dalších částech jsme se pak zaměřili na možnosti nových verzí Perlu, které dokáží zrychlit a zpřehlednit skripty i pro dlouholeté perlové programátory.

Nyní je prostor pro ty otázky, které jste nestihli položit v průběhu tutoriálu.