You are on page 1of 45

IDZ DO

PRZYKADOWY ROZDZIA
SPIS TRECI

KATALOG KSIEK
KATALOG ONLINE
ZAMW DRUKOWANY KATALOG

Przetwarzanie danych
dla programistw
Autor: Greg Wilson
Tumaczenie: Marek Ptlicki
ISBN: 83-246-0407-3
Tytu oryginau: Data Crunching
Format: B5, stron: 264

TWJ KOSZYK
DODAJ DO KOSZYKA

CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK

CZYTELNIA
FRAGMENTY KSIEK ONLINE

Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl

Przetwarzanie danych to czynno, ktr programici na caym wiecie wykonuj


niemal codziennie. Konwersja danych pomidzy systemami informatycznymi, zmiana
formatw plikw tekstowych, przeszukiwanie logw serwerw WWW to wszystko
mona nazwa przetwarzaniem danych. Znajomo technik, dziki ktrym takie
procesy odbywaj si szybko i efektywnie, to jedna z najwaniejszych umiejtnoci
programisty. Trudnoci moe okaza si fakt, e nie istnieje brak jednego,
uniwersalnego sposobu przetwarzania danych. Do kadego typu problemu naley
podej w sposb indywidualny, prbujc rozoy go na sekwencj prostych
przeksztace, atwych do implementacji i testowania.
Czytajc ksik Przetwarzanie danych dla programistw, poznasz metody
rozwizywania problemw programistycznych zwizanych z konwersj danych rnego
typu. Nauczysz si analizowa istot zagadnienia i dobiera najbardziej optymalny
sposb realizacji zadania. Dowiesz si, jak w systemach Unix/Linux wykorzysta
wyraenia regularne i powoki tekstowe systemw z rodziny Unix/Linux do
przetwarzania danych tekstowych., Pprzeczytasz o uytecznych, lecz czsto
niedocenianych cechach jzykw Java i Python oraz innych jzykw programowania.
Przekonasz si, e mimo rozbienocirnic pomidzy rnymi typami danych istnieje
kilka oglnych wzorcw, ktre powtarzaj si w wielu zastosowaniach niezalenie
od uytegozastosowanego jzyka programowania lub detali implementacyjnych.
Przetwarzanie danych tekstowych za pomoc powoki Uniksa
Stosowanie wyrae regularnych
Analiza dokumentw XML
Pakowanie i rozpakowywanie danych binarnych
Zapytania w relacyjnych bazach danych
Testowanie mechanizmw konwersji danych
Opanuj jedn z podstawowych umiejtnoci profesjonalnego programisty

Rozdzia 1. Wprowadzenie .................................................... 5


1.1. Narysowa moleku ................................................................5
1.2. Czarna owca ...........................................................................7
1.3. Mora .....................................................................................8
1.4. Pytania o przetwarzanie danych ................................................9
1.5. Plan ksiki ...........................................................................13
Rozdzia 2. Tekst ................................................................. 17
2.1. Odwracanie kolejnoci wierszy w pliku ....................................17
2.2. Przeformatowanie danych ......................................................20
2.3. Obsuga rekordw wielowierszowych .......................................29
2.4. Testowanie kolizji ..................................................................36
2.5. Wczanie plikw zewntrznych ..............................................42
2.6. Powoka Uniksa ....................................................................47
2.7. Bardzo due zbiory danych ....................................................56
2.8. Podsumowanie ......................................................................57
Rozdzia 3. Wyraenia regularne ......................................... 59
3.1. Powoka ................................................................................61
3.2. Podstawy wzorcw dopasowa ...............................................63
3.3. Wydobywanie dopasowanych wartoci ....................................72

Przetwarzanie danych dla programistw

3.4. Zastosowania praktyczne .......................................................83


3.5. Rne jzyki ..........................................................................95
3.6. Inne systemy ..........................................................................99
3.7. Podsumowanie ....................................................................104
Rozdzia 4. XML ................................................................ 105
4.1. Szybkie wprowadzenie .........................................................106
4.2. SAX ..................................................................................112
4.3. DOM .................................................................................126
4.4. XPath ................................................................................137
4.5. XSLT ................................................................................143
4.6. Podsumowanie ....................................................................153
Rozdzia 5. Dane binarne ................................................... 157
5.1. Liczby ................................................................................158
5.2. Wejcie i wyjcie ..................................................................165
5.3. Cigi znakw .......................................................................171
5.4. Podsumowanie ....................................................................182
Rozdzia 6. Relacyjne bazy danych .................................... 185
6.1. Proste zapytania ..................................................................186
6.2. Zagniedanie i negacja .......................................................197
6.3. Agregacje i perspektywy .......................................................203
6.4. Tworzenie, modyfikacja i usuwanie .......................................207
6.5. Zastosowanie SQL-a w programach .....................................216
6.6. Podsumowanie ....................................................................220
Rozdzia 7. Diabe tkwi w szczegach .............................. 223
7.1. Testy jednostkowe ...............................................................223
7.2. Kodowanie i dekodowanie ....................................................235
7.3. Arytmetyka zmiennoprzecinkowa ..........................................239
7.4. Daty i czas ..........................................................................242
7.5. Podsumowanie ....................................................................248
Dodatek A Bibliografia ....................................................... 249
Skorowidz ........................................................................... 251

EKST JEST JEDNYM Z NAJSTARSZYCH FORMATW

danych
i nadal naley do najbardziej popularnych. Jednym z powodw popularnoci takiego formatu jest fakt, e dane w nim zapisane mona
przetwarza i przeglda za pomoc dowolnego edytora tekstw. Gbszy
powd tej popularnoci ley w tym, e pisanie programw do automatycznej
manipulacji tekstem jest bardzo atwe. W tym rozdziale przyjrzymy si programom tego typu, ich moliwociom i podstawowej strukturze. Zatrzymamy si te chwil przy wierszu polece systemw Unix, rodowisku zorientowanemu tekstowo, ktre nadal trzyma si bardzo dobrze, pomimo
niemal czterdziestoletniej tradycji.

2.1. Odwracanie kolejnoci wierszy


w pliku
Na pocztek wemiemy pod uwag zupenie podstawowy problem przetwarzania danych tekstowych, jakim jest odwrcenie kolejnoci wierszy w pliku.
Oto proste rozwizanie tego problemu w Pythonie:
import sys
# Odczyt
input = open(sys.argv[1], "r")
lines = input.readlines()

18

Przetwarzanie danych dla programistw


input.close()
# Przetwarzanie
lines.reverse()
# Zapis
output = open(sys.argv[2], "w")
for line in lines:
print >> output, line.strip()
output.close()

Kod jest trywialny, posiada jednak struktur charakterystyczn dla wikszoci


procedur przetwarzajcych dane:
1. odczyt danych wejciowych,
2. przetwarzanie,
3. zapis wyniku.
W wielu przypadkach moliwe jest zapisywanie wyniku na bieco w ramach
przetwarzania, lecz prawie zawsze najlepiej jest podzieli problem na powysze etapy. Po pierwsze, dziki temu kod staje si czytelniejszy i atwiej
go wykorzysta ponownie (problemy tego typu bywaj bardzo powtarzalne).
Po drugie, to podejcie jest niezwykle uniwersalne i dziaa prawie zawsze1,
natomiast odmiana polegajca na poczeniu etapu drugiego z trzecim nie
zawsze zadziaa (przykadem jest wanie odwracanie kolejnoci wierszy
pliku).
Zwolennicy mniej dynamicznych jzykw programowania mog preferowa
nastpujc wersj napisan w Javie:
import java.util.*;
import java.io.*;
public class ReverseLines {
public static void main(String[] args) {
try {
// Odczyt
BufferedReader input = new BufferedReader(new
FileReader(args[0]));
ArrayList list = new ArrayList();
String line;
while ((line = input.readLine()) != null) {
list.add(line);
}

Chyba e danych jest zbyt wiele, by mogy zmieci si w pamici. Do tego tematu
wrcimy w dalszej czci rozdziau.

Rozdzia 2. Tekst

19

input.close();
// Przetwarzanie
Collections.reverse(list);
// Zapis
PrintWriter output =
new PrintWriter(new BufferedWriter(new FileWriter(args[1])));
for (Iterator i=list.iterator(); i.hasNext(); ) {
output.println((String)i.next());
}
output.close();
}
catch (IOException e) {
System.err.println(e);
}
}

Nietrudno zauway, e ten kod jest duszy od jego odpowiednika w Pythonie, lecz obydwa realizuj dokadnie t sam funkcj w niemale identyczny sposb.
Oczywicie bezporednie przeoenie kodu z jednego jzyka na drugi nie
zawsze daje najbardziej naturalne lub optymalne rozwizanie. Prawdziwi
programici Javy z pewnoci preferuj umieszczenie odczytu, przetwarzania i zapisu danych w osobnych metodach klasy, zechc te pomin wywoanie metody reverse(), a raczej zechc odczytywa elementy pojedynczo, jednak w odwrconej kolejnoci:
PrintWriter output =
new PrintWriter(new BufferedWriter(new FileWriter(args[1])));
for (ListIterator i=list.listIterator(list.size()); i.hasPrevious(); )
{
output.println((String)i .previous());
}
output.close();

A co z obsug bdw? W jaki sposb ma zachowa si program, jeli plik


wejciowy nie istnieje lub plik wyjciowy nie moe by utworzony? Co zrobi,
jeli plik wynikowy ju istnieje: czy naley go nadpisa, zada pytanie uytkownikowi (Zamaza wynik caego roku pracy [t]ak, [n]ie?), czy wypisa komunikat o bdzie i zatrzyma dziaanie? Co z obsug wielu plikw
jednoczenie lub z czeniem plikw w jedn cao? Co z godem na wiecie
i efektem cieplarnianym?
To wszystko s z pewnoci bardzo wane pytania, lecz dotycz ergonomii
aplikacji, nie przetwarzania danych. Jeli jednak istnieje prawdopodobiestwo, e pisany program bdzie wykorzystywany przez nieznane nam osoby

20

Przetwarzanie danych dla programistw

w czasie naszej nieobecnoci, warto powici im przynajmniej kilka minut


uwagi. A jeli mamy za zadanie wykonanie jednorazowej operacji przeformatowania pliku w starym formacie w taki sposb, aby mg by wczytany
przez nowy program, nie warto zaprzta sobie gowy takimi detalami.

2.2. Przeformatowanie danych


Wrmy do problemu zmiany formatu zapisu definicji czsteczek chemicznych na potrzeby wizualizacji 3D. Kada definicja czsteczki jest zapisana
w pliku PDB o nastpujcej postaci:
COMPND
AUTHOR
ATOM
ATOM
ATOM
ATOM
TER
END

1
2
3
4
5

Ammonia
DAVE WOODCOCK
N
1
H
1
H
1
H
1
1

97 10 31
0.257
0.257
0.771
0.771

-0.363
0.000
0.727 0.000
-0.727
0.890
-0.727 -0.890

W pierwszym wierszu definicji czsteczki zapisana jest nazwa potoczna


zwizku. Drugi wiersz zawiera nazwisko autora pliku i dat jego utworzenia. Kady wiersz rozpoczynajcy si od sowa kluczowego ATOM definiuje
typ i pooenie pojedynczego atomu. Nie wiadomo dokadnie, co oznaczaj
wiersze rozpoczynajce si od sowa kluczowego TER ani do czego suy
liczba 1 w czwartej kolumnie kadego wiersza ATOM, lecz w naszym przypadku nie ma to znaczenia.
W pierwszym odruchu chciaoby si zacz kodowanie od razu, lecz dowiadczenie2 uczy nas, e warto chwil zastanowi si i rozway pewne
zaoenia dotyczce formatu wejciowego. Na przykad: czy format PDF
moe zawiera puste wiersze? Czy wiersze COMPND i AUTHOR wystpuj zawsze i to w takiej samej kolejnoci?
Swoim pocztkujcym studentom czsto powtarzam, e godzina cikiej
pracy pozwoli zaoszczdzi szedziesit sekund szukania za pomoc Google.
Wyszukiwanie hasa PDB format zwraca kilkadziesit adresw, w tym jeden
2

Dowiadczenie to nazwa, jak nadajemy naszym bdom Oscar Wilde.

Rozdzia 2. Tekst

21

do oficjalnej specyfikacji3 napisanej pseudoprawniczym argonem, ktry


programici chtnie stosuj w przypadkach, gdy podejrzewaj, e wrd
odbiorcw moe znajdowa si jaki prawnik. Po chwili stwierdzamy, e
pliki PDB mog zawiera kilkadziesit rnych typw rekordw zorganizowanych w sekcje. Nam jednak potrzebne s tylko wsprzdne atomw,
zatem na razie moemy z powodzeniem zignorowa wszelkie nierozpoznawalne przez nas dane. Zobaczymy, co z tego wyniknie.

Ja pyta...
Czy tak wolno?
Ignorowanie nierozpoznanych danych kojarzy si do niebezpiecznie. Co si stanie, jeli atomy zostan zdefiniowane w wierszach
nierozpoczynajcych si sowem kluczowym ATOM? Co si stanie,
jeli ignorowane rekordy zmodyfikuj znaczenie rekordw ATOM?
Czy nie naley przestudiowa specyfikacji z zakrelaczem w rku,
a program napisa po upewnieniu si co do susznoci decyzji?
W tym przypadku odpowied brzmi nie. Czytanie specyfikacji
formatu PDB zajoby wicej czasu, ni rczne przeksztacenie
formatw. Oczywicie dogbna znajomo specyfikacji moe
przyczyni si do uniknicia bdw, lecz koszt takich bdw
jest bliski zeru. Tego typu decyzja oczywicie byaby nieodpowiedzialna, jeli mielibymy do czynienia z formatem danych kartoteki pacjentw szpitala lub dokumentacji silnikw odrzutowych
samolotw pasaerskich, lecz w tym przypadku jest to podejcie
jak najbardziej praktyczne. Zadanie realizujemy w jak najprostszy
sposb i poprawiamy pniej, jeli wystpi bdy. W tym przypadku jednak kluczowe jest, aby dokadnie zna format wyjciowy,
aby natychmiast zauway wszelkie bdy.

Przyjrzyjmy si zatem formatowi wyjciowemu:


-- atom azotu
1 17 0.5 0.257 -0.363 0.000

http://www.rcsb.org/pdb/docs/format/pdbguide2.2/guide2.2_frame.html

22

Przetwarzanie danych dla programistw


-- atomy wodoru
1 6 0.2 0.257 0.727 0.000
1 6 0.2 0.771 -0.727 0.890
1 6 0.2 0.771 -0.727 -0.890

Wiersze rozpoczynajce si od dwch mylnikw to oczywicie komentarz,


mona zatem zaoy, e moemy w ich charakterze wykorzysta dowolne
informacje lub zupenie je pomin. Pierwsza kolumna o wartoci 1 oznacza kul, nastpnie wystpuje kod koloru, rednica atomu oraz wsprzdne
XYZ.
Na pocztek wiadomo, e do tumaczenia symboli atomw (w przykadzie
N i H) z pliku PDB na pierwsze trzy kolumny formatu VU3 potrzebna
bdzie tablica przegldowa. Na razie jednak odmy ten problem na bok
i skupmy si na przeksztaceniu wsprzdnych XYZ z pliku PDB. Oglny
schemat dziaania algorytmu bdzie nastpujcy:
dla kadego pliku PDB:
odczyt atomw z pliku
ustalenie nazwy pliku wynikowego
zapis danych atomw i innych danych w pliku wynikowym

W Pythonie zakodujemy to nastpujco:


import sys
for inputName in sys.argv[1:]:
atoms = readPdb(inputName)
outputName = translateName(inputName)
writeVu3(outputName, atoms)

Odczyt atomw z pliku PDB to do proste zadanie: wystarczy wyowi


wiersze rozpoczynajce si sowem kluczowym ATOM i podzieli je na indywidualne pola. Trzecie pole informuje o typie atomu; pite, szste i sidme
zawiera wsprzdne XYZ. Listy Pythona s indeksowane od zera (podobnie, jak tablice w C i Javie), wic musimy odczyta pola o indeksach 2 i 4
odpowiednio dla pl 3. i 5.
def readPdb(inputName):
input = open(inputName, 'r')
result = []
for line in input:
if line[:4] == 'ATOM':
fields = line.split()
atom = fields[2] + fields[4:7]
result.append(atom)
input.close()
return result

Rozdzia 2. Tekst

23

Wyraenie line.split() dokonuje podziau wiersza na pola. Jako separator


pl moemy poda dowolny cig znakw, na przykad w celu wykorzystania
rednika jako separatora naley zastosowa wyraenie line.split(';').
Domylnie jako separator stosowany jest dowolny cig biaych znakw, co
dokadnie odpowiada naszym potrzebom w tym przypadku.
Nie zadajemy sobie trudu sprawdzania poprawnoci skadowej danych
wejciowych. Jeli rekord ATOM bdzie zawiera mniejsz od oczekiwanej
liczb pl, wyraenie fields[2] + fields[4:7] wywoa wyjtek przekroczenia zakresu. Nie jest to wielki problem, poniewa w tej postaci kodu spowoduje to wypisanie komunikatu, stosu wywoa oraz zatrzymanie dziaania
programu. W przypadku produktu przeznaczonego dla uytkownikw
kocowych takie zachowanie programu raczej nie byoby wskazane, lecz
w przypadku prostego narzdzia do przetwarzania danych jest jak najbardziej do przyjcia.
Zanim przejdziemy dalej, zastanwmy si, co ta prosta funkcja ma rzeczywicie do zrobienia. Jednym ze sposobw sprawdzenia tego jest uruchomienie kodu w debugerze i ledzenie zawartoci listy atoms zwracanej
przez funkcj readPdb(). Inny sposb polega na zmodyfikowaniu gwnej
ptli w taki sposb, aby na bieco wypisywaa na ekranie list atomw:
for inputName in sys.argv[1:]:
atoms = readPdb(inputName)
for a in atoms:
print a
outputName = translateName(inputName)
writeVu3(outputName, atoms)

Uruchommy ten kod z plikiem ammonia.pdb:


Traceback (most recent call last):
File "pdb2vu3.py", line 15, in ?
atoms = readPdb(inputName) File "pdb2vu3.py", line 7, in readPdb
atom = fields[2] + fields[4:7]
TypeError: cannot concatenate 'str' and 'list' objects

Bd: wyraenie fields[2] wybiera z listy pojedynczy cig znakw, natomiast fields[4:7] wybiera podlist, a Python nie wie, co w rzeczywistoci
chcemy zrobi, dodajc do siebie cig znakw i list. W kadym jzyku
programowania wystpuj tego typu przypadki specjalne, gdy rne
uyteczne koncepcje nie pasuj do siebie wzajemnie. Jednym z powodw,

24

Przetwarzanie danych dla programistw

dla ktrych jzyki Python, Ruby czy Java zdobywaj stopniowo przewag
nad jzykami Perl czy C++ jest to, e w tych pierwszych, dziki jasno
zdefiniowanym reguom, ilo tego typu niejednoznacznoci jest znacznie
zmniejszona. Sytuacj mona naprawi, na przykad opakowujc cig
znakw w list jednoelementow ([]):
def readPdb(inputName):
input = open(inputName, 'r')
result = []
for line in input:
if line[:4] == 'ATOM':
fields = line.split()
atom = [fields[2]] + fields[4:7]
result.append(atom)
input.close()
return result

Tym razem wynik bdzie nastpujcy:


['N',
['H',
['H',
['H',

'0.257',
'0.257',
'0.771',
'0.771',

'-0.363', '0.000']
'0.727', '0.000']
'-0.727', '0.890']
'-0.727', '-0.890']

Ten przykad nie mia na celu pokaza skutku tego banalnego (aczkolwiek
do czsto popenianego) bdu; chodzio mi o to, aby pokaza konieczno
testowania napisanego kodu przed przejciem do kolejnego etapu prac.
Podzia pracy na etapy wczytywania danych, przetwarzania i zapisu w przetwarzaniu danych jest przydatny midzy innymi wanie do tego typu przyrostowego testowania.
Nastpny etap polega na okreleniu nazwy pliku wynikowego. W tym
przypadku regua jest prosta: naley zastpi rozszerzenie .pdb rozszerzeniem .vu3:
def translateName(inputName):
return inputName[ :-4] + '.vu3'

Ju sysz okrzyki purystw Pythona: waciwy sposb wydobycia rozszerzenia z nazwy pliku polega na zastosowaniu funkcji os.path.splitext().
Jednake ten przykadowy kod pisz dla wasnego uytku, jestem pewny,
e uyj go tylko kilka razy w yciu i gdy odkryj, e wystpi problem
z danymi, mog go usun i uruchomi skrypt jeszcze raz, a przede wszystkim nie chc odstrasza mniej dowiadczonych Czytelnikw, oszaamiajc

Rozdzia 2. Tekst

25

ich nadmiern liczb bibliotek, ktrych nigdy wczeniej nie spotkali. Dlatego
zdecydowaem, e w tym konkretnym przypadku zadanie nieco uproszcz.
Moe si wyda, e nareszcie nadszed czas, aby zapisa plik VU3. Jednake jeszcze nie wiadomo, w jaki sposb przeksztaci symbole atomowe
formatu PDB na kod ksztatu, koloru i rednicy, typowe dla formatu VU3.
Najprostszy sposb polega po prostu na zakodowaniu w programie tablicy
przegldowej. Jeli program napotka symbol atomu, ktrego nie znajdzie
w tej tablicy, jego dziaanie zakoczy si bdem, moemy jednak szybko
uzupeni tablic o brakujce wpisy i ponownie uruchomi program. Taka
decyzja jest do przyjcia. Mamy wic:
Lookup = {
'H' : (1, 6, 0.2),
'N' : (1, 17, 0.5)
}
def writeVu3(outputName, atoms):
output = open(outputName, 'w')
for (symbol, X, Y, Z) in atoms:
if symbol not in Lookup:
print >> sys.stderr, 'Nieznany symbol atomu "%s"' % symbol
sys.exit(1)
shape, color, radius = Lookup[symbol]
print >> output, shape, color, radius, X, Y, Z
output.close()

Tablice przegldowe s prawie zawsze o wiele czytelniejsze od dugich serii


warunkw ifthenelse. Jednym z powodw, dla ktrych zrczne jzyki,
do jakich zalicza si Python, s tak uyteczne, jest fakt, e duo informacji
i logiki aplikacji mona w nich zapisa w postaci struktury danych, w przeciwiestwie do bardziej sztywnych jzykw, do ktrych zalicza si na przykad Java.
W tym momencie moemy przeksztaci pierwszy plik z formatu PDB do
formatu VU3. Czy uda si przeksztaci kolejny? Przejdmy nieco dalej
w kolejce plikw i sprbujmy definicji czsteczki mentolu, ktra skada si
z trzydziestu jeden atomw. Prawie natychmiast program wyrzuci nastpujcy komunikat:
Nieznany symbol atomu "C"

26

Przetwarzanie danych dla programistw

Dobrze, wanie tego si spodziewalimy. Po wprowadzeniu poprawek


w kodzie i uzupenieniu tablicy przegldowej o definicje atomw wgla
i tlenu (ktre rwnie wystpuj w mentolu) nasz program bez dalszych
problemw wygeneruje odpowiedni plik VU3.
Najwyszy czas na ostateczny test. Po skopiowaniu wszystkich 112 plikw
PDB do jednego katalogu uruchamiamy nastpujce polecenie:
$ python pdb2vu3.py *.pdb

Potrzebujemy czterech podej, aby uzupeni wszystkie brakujce pierwiastki (siarka, chlor, elazo i brom). Przy okazji okazuje si, e symbole
atomw mog by zapisywane wielkimi literami, np. CL, jak i za pomoc
kombinacji wielkich i maych liter, np. Cl. Tego typu niespjnoci s na
porzdku dziennym w zadaniach przetwarzania danych, poniewa dla
czowieka nie stanowi problemu, s rozumiane intuicyjnie. Jednak program
musi by odpowiednio zakodowany, aby by w stanie radzi sobie z nimi
w sposb waciwy.
Mamy dwa wyjcia: albo dopisa do tablicy przegldowej odpowiednie
wpisy reprezentujce kad z moliwych pisowni symbolu pierwiastka, albo
znormalizowa symbole przed odczytem z tablicy przegldowej. Pierwszy
sposb doprowadzi nas do tablicy o nastpujcej postaci:
Lookup = {
'BR' (1, 2, 0.9),
'Br' (1, 2, 0.9),
'C' (1, 3, 0.5),
'CL' (1, 8, 0.6),
'Cl' (1, 8, 0.6),
'FE' (1, 13, 1.1),
'Fe' (1, 13, 1.1),
'H' (1, 6, 0.2),
'N' (1, 17, 0.5),
'O' (1, 19, 0.5),
'S' (1, 17, 0.7)
}

To jest dobra opcja w tej konkretnej sytuacji: liczba zduplikowanych wierszy


jest niewielka, zadanie mona sobie uproci przez kopiowanie i wklejanie
istniejcych wierszy i modyfikacj symbolu. A jeli jaka wersja pisowni
zostanie pominita, program si o to upomni.

Rozdzia 2. Tekst

27

DRY
Jedn z oglnych zasad programowania okrela si akronimem
DRY: ang. Dont Repeat Yourself (nie powtarzaj si) [HT00]. Jeli
informacja zostanie powielona w dwch miejscach lub ich wikszej
liczbie, prdzej czy pniej zapomni si uzupeni jedn z tych
kopii, co doprowadzi do bdu trudnego do wykrycia, poniewa
programista bdzie przekonany, e przecie ju to poprawi. Fakt,
e programy do przetwarzania danych bardzo czsto z zaoenia
su rozwizaniu jednorazowego problemu, nie powinien by
wymwk dla niedbaego programowania. Dowiadczenie uczy
bowiem, e jednorazowy kod czsto przydaje si wielokrotnie,
a ze nawyki nabyte przy okazji rozwiza prowizorycznych pozostaj na stae i ujawniaj si rwnie przy tworzeniu bardziej odpowiedzialnego kodu.

Co si jednak stanie, gdy symbole pierwiastkw bd w plikach rdowych


wystpoway we wszystkich moliwych kombinacjach wielkich i maych liter?
elazo mona zapisa jako FE, Fe, fE i fe. Wszystkie symbole jednoliterowe
miayby w pliku po dwa wpisy, natomiast symbole dwuliterowe a po cztery.
Wydaje si do mao prawdopodobne, e ktokolwiek celowo zapisa symbol
elaza jako fE. Jeli jednak dane s wprowadzane rcznie, jest to zupenie
moliwe w wyniku przypadkowego wcinicia klawisza Caps Lock lub innej
omyki4.
Co si jednak stanie, gdy zdecydujemy si zmieni kolor lub rednic kul
reprezentujcych atomy okrelonych pierwiastkw? Jeli w tabeli bdziemy
mieli cztery wpisy dotyczce elaza, musimy pamita o tym, aby zmieni
wszystkie cztery. Jeli pominlibymy jeden z nich, nie miaoby to wikszego
znaczenia, lecz w przypadku tysicy plikw zawierajcych kilkadziesit rnych pierwiastkw istnieje wysokie prawdopodobiestwo, e w kocowym
rozrachunku mielibymy na rnych prezentacjach odmienne rozmiary
i kolory kul reprezentujcych atomy tego samego pierwiastka.
4

I win za takie sytuacje nie zawsze ponosi ludzkie zmczenie. Uytkownicy


inteligentnych edytorw tekstu niejednokrotnie padaj ofiar opcji Poprawiaj dwa
pocztkowe wersaliki przeksztacajcej na przykad skrt PL na Pl.

28

Przetwarzanie danych dla programistw

To sprowadza nas do drugiej z dostpnych opcji: normalizacji symboli


atomw. Jeli zdecydujemy, e symbole musz zawsze mie poprawn posta (pierwsza litera wielka, druga maa) niezalenie od tego, w jaki sposb
byy zapisane w oryginalnym pliku, nasza tabela przegldowa zawiera
bdzie po jednym wpisie dla kadego atomu. W takim przypadku warto
jednak dopisa komentarz, e przetwarzane w programie nazwy atomw
nie musz odpowiada temu, co jest odczytywane z plikw. Taki komentarz
to dodatkowe dziesi sekund pracy, ktre mog oszczdzi wielu minut
poszukiwa przyczyny problemu w przyszoci, gdy przyjdzie nam poprawi
spostrzeony bd w danych wyjciowych.
Pozosta jeszcze jeden wybr. Czy symbole pierwiastkw powinny by
normalizowane przy odczycie danych z pliku PDB, czy te lepiej wykona
dodatkowy przebieg po licie atomw wczytanych z pliku zamieniajcy ich
symbole w tej licie? Aby kady z etapw dziaania algorytmu by jak najbardziej czytelny i zwarty, lepiej jest zdecydowa si na drug opcj. W ten
sposb gwna ptla programu przyjmie nastpujca posta:
for inputName in sys.argv[1:]:
atoms = readPdb(inputName)
normalizeSymbols(atoms)
outputName = translateName(inputName)
writeVu3(outputName, atoms)

Nowa funkcja normalizeSymbols() bdzie wyglda tak:


def normalizeSymbols(atoms):
for record in atoms:
record[0] = record[0].capitalize()

Jak pamitamy, kady element listy atoms jest list zoon z podcigw
wczytanej wczeniej definicji atomu w czsteczce (zapisanej w pojedynczym
wierszu pliku wejciowego). Jeli wiersz mia poprawny format, nazwa atomu
znalaza si w elemencie o indeksie zero. Metoda capitalize() obiektu tekstowego zwraca jego kopi z pierwsz liter zamienian na wielk i pozostaymi zamienionymi na mae, na przykad cig aBC zostanie zamieniony na Abc.
Musz co wyzna. Gdy pisaem przedstawiony tutaj kod, nie przyszo mi
nawet do gowy, aby duplikowa wiersze w tabeli przegldowej. Gdy si
przez wiele lat spotyka podobne problemy, normalizacja danych staje si

Rozdzia 2. Tekst

29

nawykiem, podobnie jak logiczny podzia operacji przetwarzania na osobne etapy. Nawyki tego typu zapewne nie w kadej sytuacji prowadz do
optymalnego kodu, lecz dziki nim nie musz kadego przypadku rozpatrywa w zupenym oderwaniu od innych.

2.3. Obsuga rekordw wielowierszowych


Znamy ju sposb przetwarzania plikw po jednym wierszu, zdarza si
jednak spotyka pliki, w ktrych rekordy mog zajmowa wiksz liczb
wierszy. Aby nieco urealni nasz kod, zajmijmy si przetwarzaniem plikw
.ini, typowych dla starszych wersji systemu Windows, w ktrych zapisywao
si ustawienia konfiguracyjne. Przeksztamy tego typu pliki .ini na pliki
XML. Z tego typu zadaniem spotkaem si kilka lat temu, gdy firma,
w ktrej pracowaem, zostaa zaangaowana do wsparcia w przejciu ze
starego programu CAD do nowej wersji, z zaoeniem, e ustawienia uytkownikw miay by przeniesione do tego nowego systemu.
Jak wiele nieustandaryzowanych formatw, pliki konfiguracyjne systemu
Windows posiadaj wasn skadni, ktra z czasem stawaa si coraz
bardziej skomplikowana. Nie musimy obsugiwa wszystkich detali i dziwactw, skupimy si na nastpujcych, podstawowych zagadnieniach:
t Plik .ini zawiera zero lub wiksz liczb sekcji.
t Kada sekcja posiada tytu i tre, tre moe by pusta.
t Tytu skada si z wiersza zawierajcego tekst ujty w nawiasy
kwadratowe, np. [laser6] czy [recently used]. adna z sekcji nie
moe wystpi w jednym pliku wicej, ni jeden raz.
t Tre zawiera zero lub wiksz liczb waciwoci. Kada waciwo
skada si z klucza i wartoci oddzielonych znakiem rwnoci,
np. color=blue czy file3=C:\book\intro.pml.
t Komentarze rozpoczynaj si w dowolnym miejscu wiersza
od znaku # i obowizuj do koca wiersza.

30

Przetwarzanie danych dla programistw

Typowy plik .ini ma nastpujc posta:


# Ustawienia instalacyjne
[Bootstrap]
Location=$SYSUSERCONFIG/sversion.ini
BaseInstallation=$ORIGIN/..
buildid=645m44(Build:8784)
InstallMode=STANDALONE&ALL_USERS
ProductPatch=
# pusta
# Obsuga bdw
[ErrorReport]
ErrorReportPort=80
ErrorReportServer=services.caribou.org

Na razie nie bdziemy zajmowa si specjalnym znaczeniem wartoci rozpoczynajcych si od znaku $ i skupimy si na odczytywaniu pliku i przeksztacaniu do postaci:
<configure>
<section title= "Bootstrap">
<entry key="Location">$ SYSUSERCONFIG/sversion.ini</entry>
<entry key="BaseInstallation">$ORIGIN/..</entry>
<entry key="buildid">645m44(Build:8784)</entry>
<entry key="InstallMode">STANDALONE&amp;ALL_USERS</entry>
<entry key="ProductPatch"></entry>
</section>
<section title="ErrorReport">
<entry key="ErrorReportPort">80</entry>
<entry key="ErrorReportServer">services.caribou.org</entry>
</section>
</configure>

Odczyt pliku w formacie .ini jest trudniejszy od odczytu pliku PDB, poniewa tam mielimy zagwarantowane, e kady rekord mieci si w jednym
wierszu. W tym przypadku tak nie jest. Najprostsze podejcie do tego
problemu polega na odfiltrowaniu podczas odczytu wszystkich elementw
pliku, ktre nie s poddawane przeksztaceniu, przeksztaceniu tego, co
zostanie, i zapisaniu wyniku w pliku XML. Oczywicie naley rwnie
przeksztaci znaki &, < i > odpowiednio w &amp;, &lt; i &gt;, co jest wymagane przez format XML.
Zaczniemy od zwyczajowego szablonu program przetwarzajcego:
for inputName in sys.argv[1:]:
lines = readIni(inputName)
settings = process(lines)
outputName = translateName(inputName)
writeXml(outputName, settings)

Rozdzia 2. Tekst

31

Funkcja readIni() nie polega na zwykym wczytaniu wierszy tekstu z pliku


.ini, do jej zada naley te oczyszczenie danych z komentarzy i pustych
wierszy. Najprostszy sposb realizacji tego zadania polega na odrzuceniu
komentarza (o ile istnieje) i usuniciu biaych znakw na pocztku i kocu
pozostaego wiersza. Jeli wynik takiej obrbki jest pustym cigiem znakw,
po prostu go odrzucamy.
W przeciwnym wypadku dopisujemy go na kocu listy wierszy, ktre bd
zwrcone jako wynik tej funkcji:
def readIni(inputName):
input = open(inputName, 'r')
result = []
for line in input:
# Usunicie czci wiersza od znaku # do koca
first = line.find('#')
if first >= 0:
line = line[:first]
# Oczyszczenie tekstu z okalajcych biaych znakw
line = line.strip()
# Jeli nic nie zostao, pomijamy wiersz
if not line:
continue
# W przeciwnym wypadku zapisujemy to, co zostao
result.append(line)
# Koniec pracy
input.close()
return result

To do proste. Mamy jednak do czynienia z sytuacj potencjalnego bdu


w przypadku, gdy dopuszczamy wystpowanie znaku # w wartociach atrybutw. Naley bowiem odpowiedzie sobie na pytanie, czy w naszych plikach
.ini dopuszczalne s wartoci nastpujcej postaci:
SongTitle="Love Potion #9" # ostatni odtwarzany utwr

Jeli taki zapis jest poprawny, nasz algorytm zwrci bdn warto, poniewa odrzuca wszystkie znaki od znaku # do koca wiersza. Co gorsza, ten
bd wystpi w sposb niezauwaony zamiast wywoa wyjtek, kod
odrzuci w peni poprawne dane.

32

Przetwarzanie danych dla programistw

Opisany problem mona rozwiza, wyszukujc w wierszach znaki cudzysoww i inne znaczniki specjalne. Zadanie znacznie uprociyby wyraenia
regularne, ktre omwi w rozdziale 3., a na razie przejd do nastpnego
zadania naszego kodu: wygenerowania pliku XML zawierajcego oczyszczone definicje z pliku .ini.
Krtkie przypomnienie: gwnym blokiem w naszym pliku XML bdzie
<configure> i </configure>. Za kadym razem, gdy w pliku napotkamy sekcj
[nazwa], w pliku wynikowym musi znale si zapis <section title="nazwa">.
Oczywicie przed otwarciem nowej sekcji naley zamkn poprzedni:
</section>. Ponadto kady atrybut postaci nazwa=warto musi by odzwierciedlony w wyniku jako <entry key="nazwa">warto</entry>.
Nie ma problemu, kod realizujcy te funkcje jest nastpujcy:
def process(lines):
result = ['<configure>']
for line in lines:
# Pocztek nowej sekcji
if line[0] == '[':
# Zamknicie poprzedniej sekcji
result.append('</section>')
# Pocztek nowej sekcji
title = line[1:-1]
result.append(' <section title="%s">' % title)
# Wpis w biecej sekcji
else:
key, value = line.split('=', 1)
value = escape(value)
result.append(' <entry key="%s">%s</entry>' % (key, value))
# Gotowe
result.append('</configure>')
return result

Wywoanie line.split('=', 1) powoduje rozoenie zmiennej line na


elementy z zastosowaniem znaku rwnoci w charakterze separatora.
Funkcja escape() zastpuje znaki specjalne dla formatu XML na odpowiadajce im kody XML:
def escape(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>',
'&gt;').replace("'", '&apos;').replace('"', '&quot;')

Czy ten program jest ju poprawny? Nie. Popeniem dwa bdy i proponuj przyjrze si mu jeszcze raz. A jeszcze lepiej: proponuj wykona go
z nastpujcym, testowym plikiem .init:

Rozdzia 2. Tekst

33

# Ustawienia instalacyjne
[Bootstrap]
ProductKey=Caribou CAD 1.1
Location=$SYSUSERCONFIG/sversion.ini
# Obsuga bdw
[ErrorReport]
ErrorReportPort=80

W wyniku funkcji process() powstanie nastpujcy plik:


<configure>
</section>
<section title= "Bootstrap">
<entry key="ProductKey">Caribou CAD 1.1</entry>
<entry key="Location">$SYSUSERCONFIG/sversion.ini</entry>
</section>
<section title="ErrorReport">
<entry key="ErrorReportPort">80</entry>
</configure>

W tym pliku znalazy si dwa bdy. Po pierwsze, znacznik zamykajcy


</section> pojawi si przed znacznikiem otwierajcym <section title="Boot
strap">, poniewa znacznik zamykajcy wypisujemy zawsze, nawet przed
pierwsz sekcj w pliku. Po drugie, nie zamknlimy ostatniej sekcji w pliku,
jak rwnie nie zapisalimy znacznika koczcego konfiguracj.
Pierwszy bd naprawimy, wykorzystujc znacznik boolowski zawierajcy
informacj o tym, czy przetwarzana sekcja jest pierwsza w pliku. Rozwizanie drugiego bdu polega na tym, e zamykajcy znacznik </section> jest
zapisywany zawsze przed zamykajcym znacznikiem </configure>. Spowoduje to, e w przypadku prby przeksztacenia pustego pliku .ini powstanie
nastpujcy plik wynikowy:
<configure>
</section>
</configure>

Zamykajcy znacznik </section> powinien by zapisywany wycznie w przypadku, gdy program przetworzy przynajmniej jedn sekcj. Do tego celu
wykorzystamy kolejny znacznik boolowski. Inne rozwizanie mogoby polega na zastpieniu licznikiem wystpie pierwszego z pomocniczych znacznikw boolowskich. Jeli licznik jest rwny zeru i znajdujemy si w ptli,
oznacza to, e nie naley wypisywa zamykajcych znacznikw </section>.
Jeli licznik jest rny od zera i znajdujemy si poza ptl, naley wypisa
ten znacznik. Oto ostateczna posta naszej funkcji:

34

Przetwarzanie danych dla programistw


def process(lines):
result = ['<configure>']
count = 0
for line in lines:
# Pocztek nowej sekcji
if line[0] == '[':
# Zamknicie poprzedniej sekcji
if count > 0:
result.append('</section>')
# Pocztek nowej sekcji
title = line[1:-1]
result.append('<section title="%s">' % title)
count += 1
# Wpis w biecej sekcji
else:
key, value = line.split('=', 1)
value = escape(value)
result.append(' <entry key="%s">%s</entry>' % (key, value))
# Gotowe
if count > 0:
result.append('</section>')
result.append('</configure>')
return result

Ostatnie brakujce dwie funkcje naszego programu s raczej oczywiste:


def translateName(inputName):
return inputName[:-4] + '.xml'
def writeXml(outputName, settings):
output = open(outputName, 'w')
for line in settings:
print >> output, line
output.close()

Wystarczyo kilka prostych testw, aby przekona si, e zaimplementowane funkcje realizuj poprawnie swoje zadania, zatem uznaem program
za ukoczony.
Przykadowe bdy w funkcji process() s bardzo reprezentatywne dla tej
klasy problemw, jako e bdy w przetwarzaniu danych pojawiaj si najczciej na pocztku i kocu procesu. Dlatego podczas sprawdzania poprawnoci kodu przetwarzajcego dane, warto zastosowa nastpujce dane
testowe:
t puste dane wejciowe (o ile przetwarzany format na to zezwala);
t pojedynczy rekord;

Rozdzia 2. Tekst

Znaczniki ograniczajce i automaty skoczone


Ograniczanie blokw tekstu okrelonymi znacznikami pocztku
i koca naley do bardzo powszechnych technik w informatyce.
Obsuga tego typu formatw danych polega na zastosowaniu
nastpujcego algorytmu:
for (dla kadego rekordu wejciowego)
if (rekord jest znacznikiem pocztku sekcji)
if (nie pierwsza sekcja)
zamknij poprzedni sekcj
zacznij now sekcj
else
utwrz rekord wyjciowy
if (utworzona jakakolwiek sekcja)
zamknij sekcj gwn

Jeli przetwarzany tekst moe by wczytany w caoci do pamici,


mona zastosowa nastpujc uproszczon posta:
for (dla kadej sekcji)
rozpocznij now sekcj
for (dla kadego rekordu wejciowego w sekcji)
utwrz rekord wyjciowy
zakocz sekcj

Drugi z algorytmw upraszcza logik przetwarzania, program


napisany w ten sposb atwiej jest zrozumie i usuwa jego bdy,
wymaga jednak zastosowania bardziej skomplikowanych struktur
danych (mamy w tym przypadku do czynienia z list list zamiast
jednej, paskiej listy). Zadania zwizane z przetwarzaniem danych
z reguy wi si z koniecznoci podejmowania kompromisw
pomidzy poziomem komplikacji danych a poziomem komplikacji
kodu.
Dobrzy programici wykorzystuj w takich zastosowaniach technik
noszc ogln nazw automatw skoczonych (lub automatw
o skoczonej liczbie stanw). Automaty skoczone wybiegaj poza
tematyk tej ksiki, zainteresowani znajd bardzo czytelne wprowadzenie do tego zagadnienia w pozycji [HF04].

t
t

dwa rekordy (to znaczy mamy tu do czynienia wycznie z pierwszym


i ostatnim rekordem, nie ma rekordw rodkowych);
trzy rekordy lub ich wiksza liczba.

35

36

Przetwarzanie danych dla programistw

Jeli zasady rzdzce przetwarzanymi danymi s proste, dane testowe mona


przygotowa, wykorzystujc rzeczywiste dane. Jeli przetwarzane dane s
bardziej skomplikowane, najczciej najprociej jest przygotowa dane
testowe samodzielnie.

2.4. Testowanie kolizji


Zadowolony z siebie przesaem ten program do szefa. Kilka minut pniej
otrzymaem odpowied: Nie dziaa, zapraszam do siebie.
Zrozumienie przyczyny bdu zajo poniej minuty. Stary program CAD
zezwala na to, aby w plikach .ini te same klucze wystpoway wielokrotnie,
na przykad:
[View]
WindowSize=1024,768
BackgroundColor=green7
WindowSize=1280,1024

Nowy program CAD zezwala natomiast, aby kada warto wystpowaa


w jednej sekcji tylko raz. W przypadku, gdy w pliku XML w jednej sekcji
wystpio kilka wartoci tego samego klucza, program nie uruchamia si.
Szybkie ledztwo w kilku rnych plikach .ini potwierdzio, e wartoci
atrybutw pomidzy sekcjami, a nawet w ramach sekcji, nie musz by
unikalne. Na przykad nastpujca zawarto pliku .ini jest zupenie legalna:
[View]
WindowSize=1024,768
BackgroundColor=green7
WindowSize=1280,1024
[Print]
BackgroundColor=green5

To doskonae pole do popisu dla sownikw.


Sowniki
Jeli przetwarzane dane musz by w jaki sposb unikalne, najczciej
okazuje si, e najprostszym rozwizaniem jest wykorzystanie sownika.
W zalenoci od jzyka programowania sowniki nosz nazw odwzorowa

Rozdzia 2. Tekst

37

(ang. map, na przykad w Javie), haszy (ang. hash, np. Perl) albo tablic
asocjacyjnych lub skojarzeniowych. Niezalenie od nazwy, sownik dziaa
jak tabela dwukolumnowa, gdzie w lewej kolumnie wpisywane s klucze,
a w prawej skojarzone z nimi wartoci. Doskonaym przykadem sownika
moe by tabela cen, w ktrej kodom produktw (kluczom) przyporzdkowane s ich ceny (wartoci):
ANW-400 179.95
ANW-407 179.95
ANW-460 209.95

Sowniki maj nastpujce, bardzo wane cechy:


t Kady klucz moe wystpi w sowniku tylko jeden raz. Pojedynczy
sownik nie moe zawiera na przykad trzech pozycji o kluczu
ANW-400. Jeli jedna pozycja w sowniku musi zawiera wiksz
liczb informacji, najczciej w wartoci umieszcza si list (lub zbir)
wartoci.
t Klucze nie posiadaj ustalonej kolejnoci. Powysza tabela przedstawia
klucze w kolejnoci alfabetycznej, lecz fizyczna kolejno kluczy
w sowniku moe by odwrotna lub zupenie dowolna. Kady dobry
podrcznik na temat struktur danych (jak [Sed97]) wyjani dokadnie
przyczyn takiego stanu rzeczy. W tym miejscu wystarczy pamita,
e tak wanie jest, i unika zaoenia o ustalonej kolejnoci kluczy.
t Wyszukiwanie w sowniku jest szybkie. Ta cecha jest najwiksz
zalet sownikw i gwnym powodem ich stosowania w miejsce na
przykad list par wartoci. Zamiast sprawdza wszystkie wartoci klucza
(co oznacza N porwna dla sownika o liczbie kluczy rwnej N),
sownik przeszukuje klucze w czasie prawie niezalenym od rozmiaru5.
W przypadku, gdy sownik zawiera kilkanacie elementw, rnica
w stosunku do przeszukania zwykej listy moe by niezauwaalna,
ale moe mie zasadnicze znaczenie przy sownikach o rozmiarze
milionw kluczy.
5

Rzeczywisty czas wyszukiwania jest uzaleniony od tego, jak bardzo klucze rni si
od siebie, co z kolei ma wpyw na potencjalne wystpienie kolizji. Szczegy mona
znale w [Sed97].

38

Przetwarzanie danych dla programistw

Warto ponadto zauway, e klucze nie musz by liczbami cakowitymi, jak


indeksy list, mog by na przykad cigami znakw lub obiektami. W praktyce wikszo sownikw wykorzystuje klucze w postaci cigw znakw,
lecz mona stosowa liczby cakowite, wsprzdne XY, wskaniki funkcji
wybr jest bardzo szeroki.
Zamy wic, e mamy list nieunikalnych adresw e-mail i chcemy zliczy,
ile razy kady z tych adresw powtarza si na licie. Lista adresw moe
pochodzi na przykad z listy dyskusyjnej, a taka funkcja pozwoli nam
okreli najbardziej aktywnych uczestnikw. Oto przykad tego typu listy:
see@spot.run.com
see@spot.run.com
jane@up-the-hill.org
see@spot.run.com
jane@up-the-hill.org
purple.dinosaur@bad.tv
jane@up-the-hill.org

A oto przykadowy program, ktry realizuje nasze zadanie:


Line 1 import sys
count = {}
- for address in sys.stdin:
5
address = address.rstrip()
if address not in count:
count[address] = 1
else:
count[address] += 1
10
- addresses = count.keys()
- addresses.sort()
- for address in addresses:
print address, count[address]

Przyjrzyjmy si temu programowi nieco bliej. W wierszu 3. tworzony jest


pusty sownik i przypisany zmiennej count. Ptla rozpoczynajca si w wierszu 4. odczytuje adresy e-mail po jednym, w wierszu 5. nastpuje oczyszczenie biaych znakw (to znaczy sekwencji znakw koca wiersza odpowiednich dla rnych systemw operacyjnych, na przykad Unix i Windows).
W wierszu 6. nastpuje sprawdzenie, czy dany adres by ju odczytany.
Jeli nie, naley go dopisa do sownika z licznikiem rwnym 1. Jeli pozycja
wystpuje ju w sowniku, naley zwikszy o jeden warto przypisanego
jej licznika.

Rozdzia 2. Tekst

39

Druga cz programu otrzymuje list kluczy sownika zawierajc wszystkie


odczytane adresy (wiersz 11.), sortuje je alfabetycznie (wiersz 12.), po czym
wypisuje kolejno wraz z licznikiem. Program wywouje si nastpujco:
python freq.py < email.txt

Przy powyszym wywoaniu program zwrci nastpujcy wynik:


jane@up-the-hill.org 3
purple.dinosaur@bad.tv 1
see@spot.run.com 3

To samo zadanie moemy rwnie atwo zrealizowa w Javie (do zliczania


adresw musimy wykorzysta obiekty klasy Integer, nie prymitywne wartoci cakowite, poniewa w Javie wartociami sownikw mog by wycznie obiekty):
import java.util.*;
import java.io.*;
class Freq {
public static void main(String[] args) {
BufferedReader input =
new BufferedReader(new InputStreamReader(System.in));
Map m = new HashMap() ;
String line;
try {
while ((line = input.readLine()) != null) {
line = line.trim();
if (m.containsKey(line)) {
Integer tmp = (Integer)m.get(line);
m.put(line, new Integer(tmp.intValue() + 1));
} else {
m.put(line, new Integer(1));
}
}
input.close();
}
catch (IOException e) {
System.err.println(e);
}
Set keySet = m.keySet();
List keyList = new LinkedList(keySet);
Collections.sort(keyList);
for (Iterator i=keyList.iterator() ; i.hasNext(); )
{
String key = (String)i.next();
Integer value = (Integer)m.get(key);
System.out.println(key + " " + value);
}
}
}

40

Przetwarzanie danych dla programistw

Program w Javie jest dwa i p raza wikszy od programu w Pythonie


i w przypadku plikw o niewielkich rozmiarach bdzie te dziaa wolniej6.
Przed uruchomieniem musimy go skompilowa, lecz oprcz tego wszystkiego jest prawie tak samo prosty
Powrt do plikw konfiguracyjnych
Wrmy zatem do naszych plikw konfiguracyjnych XML i zadbajmy o to,
aby warto kadego atrybutu w sekcji bya ustawiona tylko jeden raz. Funkcja wczytujca zawarto pliku .ini nie wymaga adnych modyfikacji, podobnie funkcja zastpujca znaki specjalne &, < i > ich sekwencjami. Zmodyfikowa musimy natomiast funkcj process().
Na pocztku kadej sekcji utworzymy pusty sownik. Jako e atrybuty s
same w sobie kluczami i wartociami, moemy wykorzysta to w naszym
sowniku. Jeli dowolny klucz wystpi w sekcji wicej, ni jeden raz, kolejne
wystpienia przesoni poprzednie, co spowoduje, e na kocu bdzie
widoczna tylko ostatnio ustawiona warto:
def process(lines):
result = []
section = None
content = {}
for line in lines:
# Pocztek nowej sekcji
if line[0] == '[':
# Zapisanie starych wartoci (jeli s)
if section:
entry = [section, content]
result.append(entry)
# Pocztek nowej sekcji
section = line[1:-1]
content = {}

Java z reguy dziaa szybciej od Pythona, poniewa w Javie typy zmiennych s


kontrolowane na etapie kompilacji, natomiast w przypadku Pythona typ zmiennych
musi by kontrolowany w trakcie wykonania. Maszyna wirtualna Javy ma jednak
o wiele wiksze rozmiary od interpretera Pythona, podobnie jej biblioteki maj wiksze
wymagania pamiciowe. To powoduje, e w przypadku przetwarzania plikw o niewielkich
rozmiarach czas niezbdny na zaadowanie do pamici caej maszyny wirtualnej sprawia,
i pomimo faktu, e Java jest szybsza w dziaaniu, program bdzie wykonywa si duej.

Rozdzia 2. Tekst

41

# Dodaj do biecego sownika.


else:
key, value = line.split("=", 1)
content[key] = escape(value)
# Zakocz
if section:
entry = [section, content]
result.append(entry)
return result

Ta funkcja ponownie musi wzi pod uwag przypadki specjalne na pocztku i kocu przetwarzania. Mimo zmian, jej dziaanie nadal jest bardzo
intuicyjne. Zwracany wynik jest list par. Pierwszym elementem kadej pary
jest tytu sekcji, drugim natomiast sownik zawierajcy ostateczn warto
kadego atrybutu w tej sekcji.
Zmienilimy format danych zwracanych z funkcji process(), musimy zatem
zmieni te wykorzystujc je funkcj writeXml():
def writeXml(outputName, settings):
output = open(outputName, 'w')
print >> output, '<configure>'
for (section, content) in settings:
print >> output, '<%s>' % section
for key in content:
value = content[key]
print >> output, ' <%s>%s</%s>' % (key, value, key)
print >> output, '</%s>' % section
print >> output, '</configure>'
output.close()

Na koniec naley przetestowa dziaanie kodu. Przede wszystkim naley


sprawdzi go w tych danych, ktrych porak poniosa pierwsza wersja
programu. Na potrzeby testw przygotowaem wic specjalnie spreparowany
plik .ini zawierajcy szczeglnie wane cechy:
[Section1]
Key=a
Key=b
[Section2]
Key=c
[Section3]
Red=crimson
Green=lime
Red=vermilion
Green=chartreuse
[Section4]

42

Przetwarzanie danych dla programistw

Wynik dziaania programu jest nastpujcy:


<configure>
<Section1>
<Key>b</Key>
</Section1>
<Section2>
<Key>c</Key>
</Section2>
<Section3>
<Green>chartreuse</Green>
<Red>vermilion</Red>
</Section3>
<Section4>
</Section4>
</configure>

Jeszcze chwila Przyjrzyjmy si sekcji Section3. W pliku .ini klucz Red wystpi przed kluczem Green. W pliku .xml pojawi si pniej. Czy to bd?
Nie, to dziaanie zamierzone. Jak wspominaem wczeniej, klucze w sownikach nie maj uporzdkowanej kolejnoci. Ta cecha sownikw spowodowaa, e Green pojawi si przed Red, nawet pomimo tego, e w danych
wejciowych wystpowa wczeniej.
Czy to jest problem? To zaley wycznie od tego, czy znaczenie ma kolejno kluczy w sekcji, co z kolei zaley od programu CAD wykorzystujcego t konfiguracj. Jeli nie ma to znaczenia dla programu, nie warto si
tym przejmowa. Jeli jednak ma to znaczenie, mona zmodyfikowa kod,
na przykad zachowujc kolejno kluczy atrybutw w dodatkowych sownikach, dla kadej sekcji z osobna.

2.5. Wczanie plikw zewntrznych


Na szczcie w tym przypadku kolejno kluczy nie miaa znaczenia. Pojawi
si jednak inny problem. W trakcie konwersji plikw .ini na format XML
okazao si, e pliki .ini mogy odwoywa si do innych plikw .ini, dziki
czemu cz konfiguracji moga by zdefiniowana w jednym, staym miejscu.
Na przykad jedna z grup inynierw w firmie wykorzystywaa plik .ini
o nastpujcej zawartoci:

Rozdzia 2. Tekst

43

%general.ini%
[View]
WindowSize=1024,768
BackgroundColor=green7
WindowSize=1280,1024

Natomiast zawarto pliku general.ini bya nastpujca:


[Drawing]
LineWidth=2
Corners=Rounded
[File]
DefaultName=$PROJECT.$VERSION
DefaultTitle=off

Niestety, nowy format XML nie uwzgldnia tej moliwoci: kady plik
musia by samodzieln caoci.
W pierwszej kolejnoci naley oszacowa skal problemu. W firmie byo
zatrudnionych okoo osiemdziesiciu inynierw, z czego tylko pitnastu
wykorzystywao konfiguracje wczajce pliki zewntrzne. aden z przypadkw nie by wielokrotnie zagniedony (to znaczy drugi plik nie zawiera
wczenia kolejnego pliku). Pliki konfiguracyjne musz by skonwertowane
tylko raz, zdecydowaem wic, e po prostu rcznie pocz te pitnacie
plikw .ini w caoci i uruchomi procedur przeksztacajc.
To zadanie zajo okoo dwudziestu minut, z ktrych wikszo spdziem
na poszukiwaniu administratora sieci w celu udostpnienia mi tych plikw
do odczytu. Gdyby plikw do przetwarzania byo okoo setki lub gdyby
okazao si, e wykorzystuj wielokrotne, rekurencyjne wczanie, z pewnoci zdecydowabym si zmodyfikowa swj program ini2xml.py.
Jak zawsze na pocztek zdecydowabym, w ktrym miejscu skryptu musz
wprowadzi poprawki. Jednym ze sposobw moe by napisanie kolejnej
funkcji filtrujcej, zastpujcej zawartoci pliku wszystkie wystpienia instrukcji wczajcej plik zewntrzny. Ta metoda zadziaa jedynie w przypadku, gdy nie mamy do czynienia z rekurencyjnym wczaniem, jeli jednak
byoby inaczej, naley zastosowa odpowiedni ptl, na przykad:
lines = readIni(filename)
while containsIncludes(lines):
index = findFirstInclude(lines)
expansion = readIni(index)
lines = insert(lines, index, expansion)

44

Przetwarzanie danych dla programistw

To nie wyglda najgorzej, lecz wszyscy Czytelnicy, ktrzy mieli okazj przetwarza pliki tekstowe wczajce inne pliki, z pewnoci umiechn si
z powtpiewaniem. Naley bowiem pamita o moliwoci wystpienia
bdnej sytuacji samowczenia pliku, na przykad (plik nosi nazw selfinclusion.ini):
[Foo]
a = b
%selfinclusion.ini%

Po pierwszym przebiegu ptli program przetwarzajcy bdzie zawiera


nastpujce dane:
[Foo]
a = b
[Foo]
a = b
%selfinclusion.ini%

Po drugim przebiegu posta danych bdzie nastpujc:


[Foo]
a = b
[Foo]
a = b
[Foo]
a = b
%selfinclusion.ini%

Przetwarzanie bdzie odbywa si w nieskoczono, a raczej do momentu


zapenienia pamici lub przerwania dziaania programu kombinacj Ctrl+C.
Wystpienie takiego przypadku w rzeczywistoci wydaje si do mao prawdopodobne. W kocu, komu przyszoby do gowy wcza plik w nim samym?
Zupenie prawdopodobny jest jednak tylko troch bardziej skomplikowany
przypadek, gdy plik A wcza plik B, ktry wcza C, ktry z kolei wcza
na powrt plik A (rysunek 2.1). Taka sytuacja jest zupenie powszechna
w jzykach programowania C i C++, gdzie czsto zdarza si, e biblioteki
standardowe s uzalenione wzajemnie od siebie.
Standardowe rozwizanie tego problemu wykorzystuje stos przetwarzanych
aktualnie plikw i wwczas gdy zostanie napotkany przypadek wczenia
jednego z przetwarzanych plikw, procedura jest przerywana. W naszym
przypadku moemy to zaimplementowa za pomoc rekurencji w funkcji

Rozdzia 2. Tekst

Rysunek 2.1.

45

Cykliczne wczanie plikw nagwkowych

readIni().

Wraz z nazw wczanego pliku redIni() musi otrzyma list


(a dokadniej: stos) nazw aktualnie przetwarzanych plikw. Gdy natrafi
na instrukcj wczenia pliku, sprawdza, czy nazwa tego pliku nie znajduje
si aktualnie na stosie. Jeli nie, umieszcza t nazw na szczycie stosu, wywouje si rekurencyjnie z t nazw pliku i zmodyfikowanym stosem. Wynik
wywoania rekurencyjnego jest zapisywany na licie przetworzonych wierszy.
Zmodyfikowany fragment skryptu znajduje si na poniszym listingu:
def error(msg):
print >> sys.stderr, msg
sys.exit(1)
def readIni(inputName, stack):
input = open(inputName, 'r')
result = []
for line in input:
# ten fragment pozostaje bez zmian
line = line.strip()
first = line.find('#')
if first >= 0:
line = line[:first]

46

Przetwarzanie danych dla programistw


if (not line) or (line[0] == '#'):
continue
# zwyky wiersz
if line[0] != '%':
result.append(line)
# wczenie
else:
filename = line[1:-1]
if filename in stack:
error("Rekurencyjne wczenie pliku %s: %s" %
(filename, repr(stack)))
newStack = stack + [filename]
inclusion = readIni(filename, newStack)
result = result + inclusion
return result

Wywoanie funkcji
nastpujce:

readIni()

w gwnej czci programu zmieniamy na

lines = readIni(inputName, [inputName])

Czas na przetestowanie nowej wersji. Czy skrypt dziaa poprawnie w przypadku plikw .ini nie wczajcych innych plikw? Czy dziaa poprawnie
w przypadku wczenia pliku w samym sobie? Praktycznie natychmiast
zwraca komunikat o bdzie i koczy dziaanie7. Co si stanie w przypadku,
plik A wcza plik B, ktry wcza plik C, ktry wcza plik A? Przygotowanie odpowiedniego zestawu testowego zajmuje dosownie chwil,
warto wic sprawdzi taki przypadek. Wszystko w porzdku, rwnie w tym
przypadku program zachowuje si zgodnie z zaoeniami.
Wnioski
To wiczenie z przetwarzania danych tekstowych pozwala zdoby sporo
uniwersalnych dowiadcze zwizanych z przetwarzaniem danych. Po pierwsze: nie mona zbyt wczenie uzna, e zadanie jest skoczone. Pierwsze
rozwizanie problemu zwizanego z przetwarzaniem danych z reguy nie
uwzgldni kilku wanych szczegw, kluczowe jest zatem, aby kod pisa
w sposb czytelny, nawet w przypadku gdy kod z zaoenia ma by jednorazowego uytku. Warto te przechowa taki kod w systemie kontroli wersji,
aby mc odszuka go za jaki czas, gdy mimo wszystko okae si potrzebny.
7

W zalenoci od potrzeb, zamiast koczy dziaanie z bdem, mona po prostu


pomija plik wczany w sposb rekurencyjny.

Rozdzia 2. Tekst

47

Drugie spostrzeenie dotyczy uytecznoci programowalnych edytorw


tekstu, jak Emacs czy Vim. W opisanej sytuacji edycji pitnastu plikw
(gdy wczaem w nich zawarto innych plikw .ini) oczywicie nie wykonywaem tej czynnoci pitnacie razy. Zamiast rcznej edycji zapisaem
makro podczas edycji pierwszego pliku i wywoaem je czternacie razy
(po czym zapisaem na wszelki wypadek). Gdybym mia do czynienia
z przypadkiem rekurencyjnego wczania lub innych skomplikowanych
struktur danych, zapewne nie powaybym si na ich obrbk w edytorze
tekstu8, lecz zdumiewajco wiele przypadkw z ycia udaje si sprowadzi
do tak prostych rozwiza.

2.6. Powoka Uniksa


Gdyby jako produktu mierzy czasem jego aktywnego uytkowania, powok Uniksa9 naleaoby uzna za najlepsze narzdzie do przetwarzania
danych tekstowych w historii komputeryzacji. Powoka Uniksa ma ponad
trzydzieci pi lat i jest nadal wykorzystywana przez wielu programistw
jako ulubione narzdzie do przetwarzania informacji.
Jak stwierdzili ich twrcy [KP99], jzyki powoki zawdziczaj swoje
moliwoci filozofii duej liczby niewielkich, specjalizowanych narzdzi.
Zamiast bowiem oferowa wielkie programy do wszystkiego, powoka
Uniksa daje do uytku wiele maych programikw, ktre realizuj zaledwie pojedyncze funkcje, za to realizuj je doskonale. Co wicej, powoka
uatwia czenie tych narzdzi na rne sposoby oraz dodawanie nowych.
Aby ta filozofia sprawdzaa si w yciu, dobre narzdzie powoki Uniksa
powinno by napisane w zgodzie z nastpujcymi zasadami:
8
9

Programici znajcy doskonale Emacs Lisp zapewne mog by innego zdania.


Jeli chodzi o ciso, nie powinnimy mwi powoka Uniksa, poniewa nie istnieje
jeden jedyny produkt noszcy t nazw. Mamy bowiem do czynienia z dziesitkami
jzykw skryptowych, ktre mog dziaa jako powoki Uniksa, poczwszy
od klasycznego ju /bin/sh, po mj ulubiony bash. W tej ksice zastosuj jak
najbardziej przenon skadni zgodn z wikszoci jzykw powoki.

48
t

t
t

Przetwarzanie danych dla programistw

pobieranie danych ze standardowego wejcia i wypisywanie wynikw


na standardowym wyjciu, chyba e zostanie to okrelone inaczej
za pomoc parametru wywoania;
na wejciu oczekiwanie wierszy tekstu i zwracanie wyniku w takiej
samej formie na wyjciu;
jeli przetwarzanie zakoczy si powodzeniem, program powinien
zwrci powoce kod wyjcia rwny 0 (zero), kada inna warto
powinna sygnalizowa sytuacj bdu.

I to ju wszystko, to s reguy, ktre musz by przestrzegane przez program przeznaczony do pracy w powoce. Dziki nim bez problemu bdzie
mg wsppracowa z dziesitkami innych programw napisanych zgodnie
z t sam filozofi. Przestrzeganie tych regu umoliwia bowiem przekierowania (ang. redirection) wyniku dziaania programu oraz czenie kilku
programw w potoki (ang. pipe). Przekierowanie suy przekazaniu danych
z pliku na wejcie programu, zamiast na przykad wpisywania ich z klawiatury. Druga funkcja przekierowania suy zapisywaniu danych z wyjcia
programu. Pierwsza z tych funkcji realizuje si za pomoc operatora <,
na przykad:
myprog < somefile.txt
# przekazanie zawartoci pliku file.txt na
wejcie programu myprog

Drug z funkcji przekierowania obsuguje si za pomoc operatora >, na


przykad:
myprog > anotherfile.txt # przekazanie danych z wyjcia programu
myprog do pliku anotherfile.txt

Zamy na przykad, e chcemy zapisa w pliku wszystkie nazwy plikw


Javy zapisanych w katalogu biecym. Do sporzdzania listingw zawartoci katalogw suy program ls, lecz standardowo wyniki swojego dziaania wypisuje on na ekranie. Nie ma jednak problemu, poniewa dziki
przekierowaniu wyjcia do pliku moemy zrobi co takiego:
ls *.java > javafiles.txt

Potok to po prostu poczenie wyjcia jednego programu z wejciem innego.


Zamy na przykad, e chcemy policzy, ile w katalogu biecym zostao

Rozdzia 2. Tekst

49

zapisanych plikw Pythona. Program ls sporzdzi ich list, natomiast


zliczy znaki, sowa oraz wiersze w danych przekazanych mu na wejciu:

wc

ls *.py | wc

Gdy wywoaem to polecenie w katalogu, w ktrym zapisaem przykady


z tego rozdziau, otrzymaem nastpujcy wynik;
10

10

147

Dziesi wierszy, dziesi sw (w kadym wierszu znajduje si jedno sowo)


oraz 147 znakw w caoci. Aby zapisa ten wynik do pliku, wystarczy
w jednym wywoaniu wykorzysta potoki i przekierowanie:
ls *.py | wc > numfiles.txt

Podobnych moliwoci jest nieskoczenie wiele. Ten mechanizm jest prosty,


lecz elastyczny i tak uyteczny, jak prawdziwy jzyk programowania.
W rzeczywistoci jzyki powoki s jzykami programowania, poniewa
obsuguj zawansowane mechanizmy kontroli przebiegu, jak ptle, warunki,
zmienne czy funkcje. Poniszy listing prezentuje prosty program, ktry mona
wykorzysta do odszukania katalogw domowych uytkownikw, ktrych
identyfikatory systemowe (ID) s zapisane w pliku:
cat $1 | while read uid
do
echo $uid
grep $uid /etc/passwd | cut -d : -f 5
done

Zamy, e na wejciu podamy taki plik:


gvwilson:x:182:9:Greg Wilson:/h/6/gvwilson:/bin/bash
dave:x:180:7:Dave Thomas:/h/3/dave:/bin/bash
andy:x:181:7:Andy Hunt:/h/3/andy:/bin/tcsh
alant:x:196:9:Alan Turing:/h/6/alant:/bin/tcsh

Program naley uruchomi w nastpujcy sposb:


./findhome.sh findhome-in.txt

W wyniku jego dziaania otrzymamy wynik:


gvwilson
/h/6/gvwilson
dave
/h/3/dave
andy

50

Przetwarzanie danych dla programistw


/h/3/andy
alant
/h/6/alant

Przeanalizujmy dziaanie tego skryptu:


t $1 to argument wiersza polece o indeksie 1 (pod indeksem 0 znajduje
si nazwa skryptu), zatem polecenie cat $1 spowoduje wypisanie
na standardowym wyjciu zawarto pliku, ktrego nazwa zostanie
podana jako argument wywoania skryptu.
t Zamiast wysya zawarto pliku na standardowe wyjcie, skrypt
przekazuje wynik dziaania polecenia cat jako strumie wejciowy
ptli while. Ptla ta przetwarza plik wiersz po wierszu (zmienna uid).
t Gwna cz ptli wypisuje na wyjciu identyfikatory uytkownika
(UID). Przed nazw zmiennej uywany jest znak $. Wymusza
on wydobycie wartoci zmiennej w miejscu zastosowania jej nazwy.
t W nastpnym wierszu wykorzystane jest polecenie grep, za pomoc
ktrego w pliku /etc/passwd poszukiwany jest identyfikator uytkownika.
Ten plik zawiera baz kont uytkownikw w wikszoci systemw
Unix. Wiersze wyszukane przez to polecenie s przekazywane do
polecenia cut. Ten fragment algorytmu jest do nieprecyzyjny: jeli
w pliku wejciowym wystpi nazwa konta jan, zostan uwzgldnione
konta jan, janusz, janek itp. oraz wszelkie inne pozycje, ktre zawieraj
ten cig znakw. W nastpnym rozdziale opisz sposoby lepszego
definiowania wzorcw dopasowa dla polecenia grep.
t Polecenie cut dzieli wiersze wejciowe na pola, w charakterze
separatora uywajc znaku dwukropka (parametr -d), po czym
wybierane jest pite pole utworzonego w ten sposb rekordu
(parametr -f), w ktrym to polu znajduje si wanie cieka
katalogu domowego uytkownika.
Korzystanie z wiersza polece wymaga nieco czasu, aby si do niego przyzwyczai , lecz po poznaniu mechaniki dziaania powoki za pomoc kilku
polece tekstowych mona wykona wiele zada. Tabela 2.1 zawiera kilka
polece szczeglnie przydatnych przy przetwarzaniu danych.

Rozdzia 2. Tekst
Tabela 2.1.

Przydatne polecenia

cat

poczenie plikw i wypisanie ich na wyjciu

cd

zmiana katalogu roboczego

chmod

zmiana uprawnie do plikw i katalogw

cut

wybr pl z rekordu tekstowego

cp

kopiowanie plikw i katalogw

date

wypisanie biecej daty i czasu

diff

okrelenie rnic pomidzy dwoma plikami

du

wypisanie zajtoci dysku przez wskazane pliki i katalogi

echo

wypisanie argumentw

env

wypisanie zmiennych rodowiska

find

wyszukiwanie plikw i katalogw o zadanych waciwociach

grep

wypisanie wierszy zgodnych z wzorcem dopasowania

head

wypisanie pierwszych wierszy pliku

lpr

przesanie pliku do drukarki

ls

wypisanie nazw plikw i katalogw

man

dokumentacja polece

mkdir

tworzenie katalogw

mv

przesunicie lub zmiana nazwy plikw i katalogw

od

wypisanie zrzutu zawartoci pliku w kilku formatach

ps

wypisanie statusu procesw systemowych

pwd

wypisanie cieki do biecego katalogu

rm

usuwanie plikw

rmdir

usuwanie katalogw

sort

sortowanie wierszy

tail

wypisanie ostatnich wierszy pliku

tar

archiwizowanie plikw

uniq

usuwanie zduplikowanych wierszy

wc

obliczenie liczby wierszy, sw i znakw pliku

zip

kompresowanie i dekompresowanie plikw

51

52

Przetwarzanie danych dla programistw

Wiersz polece stanowi bardzo proste narzdzie do wykonywania prostych zada i jest do efektywny, jeli wemie si pod uwag czas i nakad
pracy; naley jednak pamita o jego ograniczeniach. Najwaniejszym z nich
(z punktu widzenia przetwarzania danych) jest brak obsugi danych strukturalnych10. W klasycznych systemach Unix wszelkie dane przetwarzane
z poziomu powoki stanowi listy cigw znakw. Jeli kto potrzebuje
bardziej zaawansowanej struktury danych, na przykad drzewiastej, zapewne
atwiej bdzie sign po Pythona, Rubyego czy podobny jzyk programowania. Alternatywnym podejciem moe by zainwestowanie w komercyjne
narzdzia (na przykad TextPipe Pro firmy Crystal Software: http://www.
crystalsoftware.com.au), ktre pozwalaj wykorzysta wiele nietekstowych
struktur danych rwnie z poziomu jzykw powoki.
Jak zbudowa poprawnie dziaajce narzdzie
Jak wspominaem wczeniej, poprawnie dziaajce narzdzie wiersza polece
powinno czyta dane ze standardowego wejcia, a wyniki swoich dziaa
zapisywa na standardowym wyjciu, dziki czemu programici bd mieli
moliwo czenia tego narzdzia z innymi za pomoc potokw. Gdy mamy
pewno, e narzdzie do przetwarzania danych nie bdzie wykorzystane
wicej, ni raz, nie warto upiera si przy zachowaniu tego wymogu. Jeli
jednak istnieje prawdopodobiestwo, e ten program bdzie wykorzystywany
wielokrotnie lub w przypadku gdy tworzony program ma by elementem
wikszego systemu do przetwarzania danych, warto go stworzy w zgodzie
z pewn konwencj (ktra nie zawsze jest speniona w peni przez narzdzia uniksowe):
t jednoliterowe parametry wiersza polece powinny rozpoczyna si
od jednego mylnika, na przykad -p;
t wieloliterowe parametry wiersza polece powinny rozpoczyna si
od dwch mylnikw, na przykad --print;

10

By moe wolna od tego ograniczenia bdzie powoka nowej generacji firmy Microsoft,
o nazwie kodowej Monad.

Rozdzia 2. Tekst
t

53

jeli nie zostanie podana nazwa pliku, program powinien czyta dane
wejciowe ze standardowego wejcia i zapisywa wyniki swoich dziaa
na standardowym wyjciu;
jeli zostanie podana jedna nazwa pliku, program powinien
odczyta z niego swoje dane wejciowe, a wyniki dziaa zapisa
na standardowym wyjciu;
jeli zostan podane dwie nazwy plikw, program powinien
odczyta swoje dane wejciowe z pierwszego z nich, a wyniki dziaa
zapisa w drugim;
alternatywna zasada jest taka, e program odczytuje swoje dane
wejciowe ze wszystkich plikw okrelonych w wywoaniu po kolei,
a wyniki dziaa zapisuje na standardowym wyjciu;
komunikaty o bdach powinny by wypisywane na standardowym
strumieniu bdw (co spowoduje, e zostan wypisane na ekranie
nawet w przypadku, gdy wynik bdzie przekierowany do pliku);
w przypadku poprawnego wykonania program powinien zwrci
status rwny zeru, poniewa powoka interpretuje kod wyjcia rny
od zera jako sygnalizacj bdu wykonania.

Zamy na przykad, e chcemy pobiera prbki danych co N-ty wiersz.


Jeli uytkownik nie wskae liczby N za pomoc parametru wywoania n,
zostanie przyjta domylna warto N=10. Program bdzie odczytywa dane
z plikw wskazanych przez uytkownika. Jeli nie zostanie okrelona adna
nazwa pliku, dane bd czytane ze standardowego wejcia. Wynik dziaania
bdzie zapisywany na standardowym wyjciu, chyba e zostanie okrelona
nazwa pliku wyjciowego za pomoc opcji -o.
W pierwszym kroku naley przetworzy argumenty wiersza polece:
# ustawienie znacznikw
input = []
output = None
sampling = None
# analiza parametrw wywoania
argdex = 1
while argdex < len(sys.argv):
if sys.argv[argdex] in ["-n", "number"]:
if sampling is not None:
fail("Bd: czstotliwo ustawiona wielokrotnie")

54

Przetwarzanie danych dla programistw


argdex, arg = getArg(argdex, sys.argv)
try:
sampling = int(arg)
except ValueError:
fail("Bd: parametr n/--number wymaga podania wartoci
liczbowej (nie '%s')" % arg)
if sampling <= 0:
fail("Bd: czstotliwo musi by liczb dodatni (nie
%d)" % sampling)
elif sys.argv[argdex] in ["-o", "output"]:
if output is not None:
fail("Bd: za dua liczba plikw wyjciowych")
argdex, arg = getArg(argdex, sys.argv)
try:
output = open(arg, "w")
except IOError:
fail("Bd: nie mona otworzy pliku '%s' w trybie do
zapisu" % arg)
elif sys.argv[argdex][0] == "-":
fail("Bd: nierozpoznany parametr '%s'" % sys . argv[argdex])
else:
input.append(sys.argv[argdex])
argdex += 1
# sprawdzenie parametrw, ustawienie wartoci domylnych
if input == []:
input = ["-"]
if output is None:
output = sys.stdout
if sampling is None:
sampling = 10

Moe si pojawi konieczno przetwarzania wikszej liczby plikw wejciowych, zmienna input jest ustawiona pocztkowo na pust list. Jeli
po przetworzeniu parametrw ta zmienna jest nadal pust list, dane bd
odczytane ze standardowego wejcia. Zmienne output i sampling s ustawione na None, wic istnieje moliwo zweryfikowania, czy kto nie usiowa
ustawi ich wartoci wielokrotnie (gdyby na samym pocztku ustawi ich
wartoci domylne, trudno byoby stwierdzi, czy parametr zosta ustawiony
przez uytkownika). Program zawiera sporo kontroli bdw. Warto zadba
o tego typu szczegy, jeli program ma by uywany wielokrotnie, warto
uatwi uytkownikom jego uytkowanie.
Funkcje przetwarzajce parametry wywoania maj bardzo prost konstrukcj:
# Wypisanie komunikatu o bdzie i zakoczenie pracy z kodem bdu
def fail(msg):
print >> sys.stderr, msg
sys.exit(1)

Rozdzia 2. Tekst

55

Ja pyta...
Czy zawsze musz pisa tak wielk ilo kodu?
Nie. Wikszo jzykw programowania zawiera biblioteki i moduy
przetwarzajce listy parametrw wywoania, najczciej biblioteki
te nosz nazw getopt lub podobn. W swoim kodzie zaprezentowaem samodzielny sposb przetwarzania listy parametrw
wywoania wycznie dla celw demonstracji i na potrzeby programowania w jzyku, w ktrym nie mamy dostpnej biblioteki
realizujcej t funkcj.

# Odczyt argumentu lub zakoczenie pracy z kodem bdu


def getArg(argdex, args):
flag = args[argdex]
if argdex >= len(args)-1:
fail("Naley poda warto parametru %s " % flag)
return argdex+2, args[argdex+1]

Gwny kod programu rwnie jest bardzo prosty:


# Przetwarzanie jednego pliku lub strumienia
def process(filename, output, sampling):
if filename == "-":
stream = sys.stdin
else:
try:
stream = open(filename, "r")
except IOError:
fail("Nie mona otworzy '%s' w trybie do odczytu" % filename)
count = 0
for line in stream:
count += 1
if count == sampling:
output.write(line)
count = 0
stream.close()
# Przetwarzanie danych
for filename in input:
process(filename, output, sampling)
output.close()

Warto zwrci uwag na funkcj process(), w ktrej znajduje si sprawdzenie, czy nazw pliku nie jest mylnik. W takim przypadku dane nie s odczytywane z pliku, tylko ze standardowego wejcia. Dziki temu uytkownik

56

Przetwarzanie danych dla programistw

ma moliwo kontroli rda danych dla programu, uwzgldniajce standardowe wejcie.


Ten mechanizm mona uproci jeszcze bardziej, w zalenoci od uywanego jzyka programowania. W Pythonie na przykad mona wykorzysta
modu fileinput, ktry automatycznie przetwarza pliki podane w wierszu
polece. Dziki temu kod przetwarzajcy pliki mona sprowadzi do nastpujcych trzech wierszy:
import sys, fileinput
for line in fileinput.input():
doSomething(line)

W ten sposb zostan kolejno przetworzone wszystkie wiersze plikw wejciowych, modu wie nawet, e znak oznacza standardowe wejcie.
W Perlu jest jeszcze prociej:
while (<>) {
doSomething($_);
}

Symbol <> (diament) oznacza domylny uchwyt pliku, natomiast symbol


$_ oznacza aktualnie przetwarzany fragment danych.

2.7. Bardzo due zbiory danych


Wasno Parkinsona [Par93] mwi, e kade zadanie prdzej lub pniej spuchnie tak, e zajmie cay dostpny czas i zasoby. Programici maj
bardzo czsto okazj stwierdzi, e dane zachowuj si bardzo podobnie:
niewane, jak due dyski mamy w swoich komputerach, przez 99% czasu
swojego funkcjonowania s one zapenione w 99%.
Dla przetwarzania danych oznacza to, e naley spodziewa si przypadkw, w ktrych zaadowanie danych do pamici spowoduje jej przepenienie. Roczna zawarto logw popularnego serwera WWW moe zaj od
10 do 15 gigabajtw miejsca na dysku. Jeli komputer ma 1 lub 2 gigabajty
pamici operacyjnej (wraz z przestrzeni wymiany), w celu przetworzenia
danych o takich rozmiarach naley podzieli je na mniejsze kawaki.

Rozdzia 2. Tekst

57

Jeli mamy szczcie, dane moemy podzieli na czci w ramach dwuetapowego procesu. W pierwszym dane s filtrowane w taki sposb, e to
co pozostao mieci si ju w pamici, w drugim etapie wykonujemy nasze
przetwarzanie z tymi odfiltrowanymi danymi. Zamy na przykad, e
firma realizuje miliard transakcji rocznie i chcemy znale milion najwikszych z nich. Zamy, e w pamici zmieci si najwyej sto milionw
rekordw. Problem moemy rozwiza w nastpujcy sposb:
for (kady blok zoony ze 100 milionw rekordw)
wczytaj do pamici
posortuj
zapisz w pliku tymczasowym milion najwyszych wartoci
wczytaj plik tymczasowy (o rozmiarze 10 milionw rekordw)
posortuj
wypisz milion najwyszych wartoci

Co zrobi jednak, gdy nie moemy podzieli danych na mniejsze kawaki?


Na przykad: co zrobi z multispektralnymi zdjciami satelitarnymi albo
obrazami z tomografu komputerowego i nie ma wyjcia potrzebne
s od razu te 22 gigabajty danych? W tym przypadku jestemy skazani na
niestandardowe algorytmy. Wiele z nich wywodzi si z czasw komputerw
mainframe i pamici tamowych. Tego typu rozwizania s z reguy o wiele
bardziej skomplikowane od wersji cae dane w pamici. Jeli kto znajdzie
si w takiej sytuacji, oznacza to, e czas zwykego przetwarzania danych
skoczy si, nadszed czas na odejcie od klawiatury i wykonanie solidnej
pracy analitycznej i projektowej.

2.8. Podsumowanie
Ten rozdzia zawiera wprowadzenie kilku podstawowych koncepcji i technik. Niektre z nich s specyficzne dla przetwarzania strumieni tekstu, inne
maj oglne zastosowanie w technikach przetwarzania danych. Podsumujmy
najwaniejsze z nich:
t Naley rozpocz od uoglnienia i dodawa do rozwizania
kolejne szczegy dopiero wwczas, gdy napotkamy przypadki,
ktrych uoglnienie nie obsuguje. Naley unika rozwiza
przekombinowanych, implementujcych rozwizania nieistniejcych
problemw. W strategii Extreme Programming tego typu rozwizania

58

Przetwarzanie danych dla programistw

Struktury danych
Przetwarzanie danych opiera si w wikszoci przypadkw na
listach i katalogach, lecz czasem rozwizanie problemu wymaga
zastosowania technik opisanych w bardziej zaawansowanych algorytmach informatycznych. Na przykad problem wyszukania miliona
najwyszych wartoci z listy miliarda mona rozwiza za pomoc kolejki priorytetowej, to znaczy listy, w ktrej q[k] ma zawsze
wiksz warto od q[2*k+1] do q[2*k+2]. Za kadym razem, gdy
do tej listy jest dodawany element, kolejka porzdkuje si automatycznie, zachowujc t zasad. Jeli ograniczymy jej rozmiar do
miliona rekordw, mona przetworzy miliard rekordw po kolei
(pojedynczo), a w efekcie otrzymamy milion najwikszych wartoci.
Standardowa biblioteka Pythona zawiera obsug kolejki priorytetowej, suy do tego modu heapq. Podobne moduy s dostpne
dla wikszoci jzykw programowania. Jeli Czytelnik wykorzystuje
jzyk, do ktrego nie ma odpowiedniej biblioteki, mona samemu zaimplementowa odpowiedni struktur zgodnie z opisem
w [Sed97] lub wielu innych podrcznikach omawiajcych algorytmy i struktury danych.

t
t

okrela si akronimem YAGNI: ang. You Aint Gonna Need It,


czyli nie bdziesz tego potrzebowa.
Naley oddzieli etap odczytu od etapw przetwarzania i zwracania
wynikw. Dziki temu kady z etapw bdzie atwiej napisa,
testowa, uy ponownie i rozwija.
Nie wolno si powtarza.
Nie naley szuka wymwki w fakcie, e rozwizanie jest
jednorazowe, mona sobie wic pozwoli na nonszalanckie techniki
programistyczne. Najczciej okazuje si bowiem, e modularyzacja
i testowanie kodu pozwalaj szybciej uzyska poprawne wyniki.
Uczmy si swoich narzdzi. W szczeglnoci warto pozna
standardow bibliotek swojego ulubionego jzyka programowania
i moliwoci standardowych narzdzi Uniksa. Warto te jak najlepiej
opanowa moliwoci uywanego edytora tekstu. Taka nauka znacznie
procentuje w przyszoci.

You might also like