Internet Info, s.r.o. Lupa Měšec Podnikatel Root Zdroják DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

Parser bankovních výpisů aneb hrátky s Ragel

Nedávno jsem dostal nelehký úkol: parsovat bankovní výpisy České spořitelny. Formátování vstupních dat je ale velmi nestandardní a často se nedrží ani vlastních pravidel. Hledal jsem proto vhodný parser, který by si s problémem poradil. Nakonec jsem využil Ragel, jehož použití je všestranné a pohodlné.

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

Tak nějak se mi přihodilo, že jsem potřeboval v jedné aplikaci zpracovávat větší než malé množství elektronických bankovních výpisů České spořitelny. Datové výpisy lze z běžného účtu vyexportovat buď ve formátu ABO, nebo CSV. ABO je jednoduchý formát s pevnou délkou polí a jeho zpracování by bylo velmi primitivní, nicméně textové popisy mohou být tím pádem nekompletní. V CSV exportu jsou texty sice kompletní, přístup banky k němu je ovšem poměrně kreativní:

"Account number","85732310/0800"
"Account currency","CZK"
"Statement number","11.00"
"Statement date","2007/11/30"
"Statement frequency","monthly"
"Account name","Vommit, Ltd."
"Total number of transactions","7"
"Pending transactions","0.00"
"Debit (-)","11 576.00"
"Credit","3 010.02"
"Initial balance","36 833.02"
"Final balance","28 267.04"

"Due date","Payment","Counter-acc. no.","Transaction","Currency", ... ,"-"
"2007/11/05","Trvalý příkaz","36443102/2400","-1 600.00","CZK", ... ,"-"
"2007/11/06","S24/IB JPU","80299027/0300","-16 138.50","CZK", ... ,"-"
"2007/11/11","Úhrada","52496/5500","3 961.00","CZK", ... ,"-"
"2007/11/30","Úrok kredit","/0800","27.02","CZK", ... ,"-"
"2007/11/06","Poplatek","/0800","-4.00","CZK", ... ,"-"
"2007/11/10","Poplatek","/0800","-7.50","CZK", ... ,"-"
"2007/11/10","Poplatek","/0800","-15.00","CZK", ... ,"-"

Údaje týkající se celého výpisu jsou v úvodu dokumentu s volnějším formátováním podobným CSV a po prázdném řádku následuje teprve řádek s názvy polí a řádky jednotlivých bankovních transakcí, tak jak je to v CSV běžné. Všimněte si i neobvyklého formátování čísel, kde mezi řádem tisíců a stovek je mezera. Občas se objeví i netradičně mezera po znaménku (- 100.00). Takovýto CSV soubor pak nelze jednoduchým způsobem načíst např. ani v Excelu nebo Calcu, protože čísla menší než tisíc jsou konvertována na číselný datový typ, ta větší (přesněji řečeno ta s mezerou) zůstávají jako text.

Pokusy s dostupnými parsery

Moje aplikace nicméně nebyla v Excelu, nýbrž v Ruby on Rails, kde jsem potřeboval připravit dávkové načítání transakcí z datových výpisů do databáze PostgreSQL. Ve standardní knihovně jazyka Ruby je sice jednoduchý CSV parser, ale ten se mi nepodařilo na souborech s výše uvedeným formátem uspokojivě rozchodit. Pak jsem se zahleděl do FasterCSV. Tahle knihovna rozhodně není špatná, nicméně mi na ní trochu vadilo, že se někdy rozhoduje až příliš samostatně. U standardních CSV souborů to sice vede k velmi pohodlnému použití, u nestandardních však někdy k problémům. Něco (mezery v číslech, konverze z CP1250 na UTF-8) lze sice nepříliš elegantně vyřešit naprogramováním vlastních konvertorů (FasterCSV je pak umí použít místo svých implicitních), nelze však ovlivnit, jaký konvertor FasterCSV kdy použije. Nepodařilo se mi např. jednoduše vyřešit korektní načtení referenčního čísla bankovní věty – FasterCSV se vzpíral uvěřit, že 1130E98001660 by mohl být obyčejný řetězec a ne číslo s plovoucí desetinou čárkou mimo povolený rozsah.

Nakonec jsem se rozhodl napsat si vlastní parser pomocí programu Ragel – což je kompilátor konečných automatů, který generuje z popisu automatu zdrojový kód v Ruby (příp. C/C++, Javě). Z dalších možností Ragelu stojí za zmínku vytvoření diagramu konečného automatu pomocí vizualizačního software Grap­hviz. Na Internetu lze najít spoustu článků doporučujících Ragel pro rychlý vývoj spolehlivých parserů. Na seriózní informace o Ragelu lze však narazit i poměrně bizarními způsoby. Příkladů se také dá nalézt poměrně dost, bohužel se téměř všechny týkají C/C++, ale ne už třeba Ruby nebo Javy. Nejvíce mi asi pomohl článek Hello World for Ruby on Ragel.

Instalace Ragelu

Nejnovější verze Ragelu je 6.0. Manuálbinární verze pro Windows se dá stáhnout, nicméně jsem nenašel RPM balíček šesté verze pro můj OpenSUSE 10.3. Stáhl jsem si tedy zdrojový archív, rozbalil a použil svatou trojkombinaci. Vše proběhlo bez problémů, dokonce i prefix pro instalaci byl implicitně nastaven logicky (/usr/local).

Začínáme s Ragelem

Vstupní informace pro Ragel je soubor (obvykle s příponou .rl), který obsahuje kód programu v cílovém jazyce (v našem případě tedy Ruby), do kterého je vnořen popis konečného automatu (včetně direktiv, kam a jak se má generovat kód v cílovém jazyce). Podívejme se ale, jak popsat parser pro výše uvedený bankovní výpis.

Na první pohled je jasné, že bankovní výpis obsahuje dvě hlavní části oddělené prázdným řádkem. První část se týká globálních informací a je formátována tak, že každý řádek obsahuje vždy nějaký atribut definovaný názvem a hodnotou. Jak název, tak i hodnota jsou uzavřeny v uvozovkách a odděleny čárkou.  Zkusme si napsat parser, který tuto první část výpisu načte do hashe (asociativního pole) v Ruby. Pro jednoduchost budeme předpokládat, že všechny hodnoty jsou textového typu a že se v názvu ani v hodnotě nemohou objevit uvozovky. Výsledný soubor vypis_parser.rl vypadá takto (čísla řádku jsou uvedena jen pro informaci, do souboru nepatří):

 1: %%{
 2:   machine parser_bank_vypis;
 3:
 4:   nazev_atr = ^'"'+
 5:             >{ nazev = "" }
 6:             ${ nazev << data[p] } ;
 7:
 8:   hodnota_atr = ^'"'+
 9:               >{ hodnota = "" }
10:               ${ hodnota << data[p] }
11:               %{ vypis[nazev]=hodnota } ;
12:
13:   globalni_atribut = '"' nazev_atr '","' hodnota_atr '"\r\n' ;
14:
15:   main := globalni_atribut+ '\r\n' ;
16: }%%
17:
18: %% write data;
19:
20: def parse_vypis(file_name)
21:   data = Array.new
22:   File.open(file_name, "rb") { |f| data = f.read.unpack("C*") }
23:
24:   vypis = Hash.new
25:
26:   %% write init;
27:   %% write exec;
28:
29:   p vypis
30: end
31:
32: parse_vypis 'vypis0711.csv'

Kromě vloženého popisu konečného automatu a direktiv jde o normální zdrojový kód v Ruby (zelený text). Na řádcích 20 až 30 je definována funkce (přesněji řečeno metoda) parse_vypis, na řádku 32 je pak volána s parametrem vypis0711.csv, což je jméno souboru s bankovním výpisem. Popis konečného automatu (černý text) je oddělen od kódu v Ruby buď uzavřením do speciálních závorek %%{ a }%% (pro souvislý blok – řádky 1 až 16), případně použitím dvojitých procent %% pro jednotlivé řádky (18, 26 a 27).

Ragel očekává vstup jako pole celých čísel v proměnné s názvem data (platí jen pro Ruby a Javu). Prvky pole reprezentují jednotlivé znaky zpracovávaného řetězce. Na řádku 22 je načtení CSV souboru do řetězce a jeho převod na požadované pole čísel pomocí standardní metody unpack třídy String (pozor, kdyby byl vstup v UTF-8, použijeme U* místo C*). Hash pro výsledek deklarujeme a inicializujeme na řádku 24.

Popis konečného automatu v Ragelu je vlastně stavebnice. Z jednodušších automatů tvoříme složitější. K přechodům mezi stavy pak můžeme přiřadit akce (pozor, akce jsou psány v cílovém jazyce). Je potřeba ještě vědět, že Ragel kromě pole data používá i další veřejné proměnné, mimo jiné:

  • p – index právě zpracovávaného prvku pole data,
  • pe – index posledního zpracovávaného prvku v poli data,
  • cs – aktuální stav konečného automatu.

Podívejme se podrobněji na popis našeho konečného automatu: Na pojmenování automatu na řádku 2 není asi nic zajímavého. Pak následují definice velmi primitivních automatů ( nazev_atr na řádcích 4 až 6, hodnota_atr na řádcích 8 až 11), ze kterých je vytvořena definice automatu globalni_atribut (řádek 13). Automat globalni_atribut reprezentuje jeden řádek souboru se jménem a hodnotou globálního atributu. Konečně na řádku 15 definujeme konečný automat main, který odpovídá několika globálním atributům (tj. několika řádkům vstupního souboru) následovaných prázdným řádkem. Můžeme se všimnout odlišného znaku přiřazení u automatu main. Dvojice := znamená, že má být vytvořena instance tohoto automatu. Ostatní automaty jsou oproti tomu pouze definovány, nikoliv však instanciovány.

Všechny možnosti popisu a skládání automatů lze najít v manuálu, nicméně alespoň stručné vysvětlení použitých výrazů:

  • řádky 4 a 8: ^'"'+ označuje jeden nebo více znaků kromě znaku uvozovky
  • řádek 13: '"' nazev_atr '","' hodnota_atr '"\r\n' označuje posloupnost:
  • uvozovky
  • cokoliv, co přijme automat nazev_atr (tedy posloupnost znaků kromě uvozovek)
  • uvozovky, čárka, uvozovky
  • cokoliv, co přijme automat hodnota_atr (opět posloupnost znaků kromě uvozovek)
  • uvozovky následované koncem řádku – znaky CR (návrat vozíku) a LF (nový řádek).
  • řádek 15: jednou nebo vícekrát cokoliv, co přijme automat globalni_atribut, pak prázdný řádek

Kromě výrazů popisujících daný automat, jsou ještě použity akce (řádky 5,6 a 9 až 11). Jsou to fragmenty kódu v cílovém jazyce, které se provedou při určitých přechodech mezi stavy konečného automatu. V příkladu jsou použity tyto typy akcí:

  • > – akce se vykoná při vstupu do daného automatu (např. řádek 9: při vstupu do automatu hodnota_atr se proměnná se jménem hodnota inicializuje prázdným řetězcem)
  • $ – akce se vykoná při libovolném přechodu daného automatu (např. řádek 10: při každé změně stavu automatu hodnota_atr se na konec řetězce v proměnné hodnota přidá právě zpracovávaný znak)
  • % – akce se vykoná při opuštění daného automatu (např. řádek 11: při opuštění automatu hodnota_atr se proměnná hodnota přiřadí do hashe vypis s klíčem  nazev)

Popis automatu negeneruje žádný kód. Ten je vygenerován až direktivami (řádky 18, 26 a 27). Z označení je asi celkem jasné, že první direktiva deklaruje data potřebná pro automat, druhá jej inicializuje a třetí vkládá výkonný kód.

Spuštění

Nejprve je potřeba vygenerovat cílový kód konečného automatu:

$ ragel -R vypis_parser.rl

Vytvoří se soubor se stejným názvem, ale příponou .rb, který můžeme spustit v interpretu Ruby:

ruby vypis_parser.rb

Výsledkem pak je:

{"Final balance"=>"28 267.04", "Pending transactions"=>"0.00", "Initial balance"=>"36 833.02", "Statement frequency"=>"monthly", "Statement number"=>"11.00", "Debit (-)"=>"11 576.00", "Credit"=>"3 010.02", "Total number of transactions"=>"7", "Account name"=>"Vommit, Ltd.", "Account currency"=>"CZK", "Statement date"=>"2007/11/30", "Account number"=>"85732310/0800"}

Dokončení a vylepšení

Výše uvedený příklad je velmi jednoduchý a rozhodně poskytuje prostor pro zlepšení. Například přidávat k řetězci znak po znaku nebude asi ta nejefektivnější varianta. Lepší řešení by mohlo být zapamatovat si při vstupu do automatu pozici – tj. proměnnou p a při opuštění automatu převést na řetězec příslušnou oblast pole data najednou (metoda pack).  Dále opakující se nebo složitější akce bývá vhodné pojmenovat a deklarovat zvlášť – mimo daný automat (viz např. řádky 6 až 9 nebo 11 až 15 ve výpisu níže) . Pokud bychom rozšířili úvodní příklad, aby zpracovával celý bankovní výpis, mohl by zdrojový soubor vypadat například takto:

 1: %%{
 2:   machine parser_bank_vypis;
 3:
 4:   action uloz_pozici { p0 = p }
 5:
 6:   action zacatek_transakce {
 7:     i = 0  # vynulovat index pole
 8:     vypis[:transactions] << Hash.new
 9:   }
10:
11:   action zpracuj_pole {
12:     nazev = vypis[:field_names][i]
13:     vypis[:transactions][-1][nazev] = data[p0..p-1].pack("C*")
14:     i += 1
15:   }
16:
17:   cokoliv_krome_uvozovek = ^'"'+ ;
18:
19:   nazev_atr = cokoliv_krome_uvozovek
20:             >uloz_pozici
21:             %{ nazev = data[p0..p-1].pack("C*") } ;
22:
23:   hodnota_atr = cokoliv_krome_uvozovek
24:               >uloz_pozici
25:               %{ vypis[nazev]=data[p0..p-1].pack("C*") } ;
26:
27:   eol = '\r\n' ;
28:
29:   globalni_atribut = '"' nazev_atr '","' hodnota_atr '"' eol ;
30:
31:   nazev_pole = cokoliv_krome_uvozovek
32:              >uloz_pozici
33:              %{ vypis[:field_names] << data[p0..p-1].pack("C*") } ;
34:
35:   nazvy_poli = ('"' nazev_pole '",')* ('"' nazev_pole '"') eol ;
36:
37:   pole = cokoliv_krome_uvozovek*
38:        >uloz_pozici
39:        %zpracuj_pole ;
40:
41:   transakce = ( ('"' pole '",')* ('"' pole '"') eol )
42:             >zacatek_transakce ;
43:
44:   main := globalni_atribut+ eol nazvy_poli transakce* ;
45: }%%
46:
47: %% write data;
48:
49: def parse_vypis(file_name)
50:   data = Array.new
51:   File.open(file_name, "rb") { |f| data = f.read.unpack("C*") }
52:
53:   vypis = Hash.new
54:   vypis[:field_names] = Array.new
55:   vypis[:transactions] = Array.new
56:   i = 0
57:
58:   %% write init;
59:   %% write exec;
60:
61:   p vypis
62: end
63:
64: parse_vypis 'vypis0711.csv'

Do hashe vypis se tak ještě přidá pole s názvy sloupců (polí) transakcí. Transakce jsou pak uloženy tamtéž jako pole hashů. Vlastní zdrojový kód v Ruby je opět označen zeleně, zbytek je popis automatu pro parsování. Bylo by samozřejmě ještě vhodné zkontrolovat, zda se zpracoval celý soubor (hodnoty proměnných p a pe se musejí rovnat), případně přidat nějakou rozumnou reakci na chyby. Ještě by bylo potřeba převést hodnoty z řetězců na odpovídající datové typy a texty do UTF-8, ale to bych osobně řešil až v rámci následného zpracování v Ruby.

Závěr

Napsat jednoduchý parser pomocí Ragelu je opravdu rychlé a jednoduché i pro vývojáře, kteří nepíší konečné automaty každý den. Nicméně i tak je potřeba důkladně přečíst manuál. Mne osobně třeba Ragel nachytal na švestkách, když prováděl akci specifikovanou pro přechod do cílového stavu automatu pro každý přechod. Až když jsem si nechal vykreslit diagram, pochopil jsem, že po minimalizaci má automat jen jeden stav, který musí být přirozeně i cílový.

A úplně nakonec shrnutí výhod a nevýhod:

Výhody Ragelu

  • multiplatformní (Linux, Windows, Mac)
  • žádné závislosti na externích knihovnách
  • jednoduché použití (není potřeba znát moc teorie)
  • možnost vygenerovat diagram
  • minimalizace konečného automatu
  • údajně generuje velmi rychlý cílový kód v C/C++ (nezkoušel jsem)
  • možnost přechodu do jiného jazyka (gramatika se nemění, jen akce)

Nevýhody

  • nenahrazuje plně dokonalejší kompilátory založené na LALR nebo LL(*) gramatikách (YACC, ANTLR)
  • horší práce s mixem cílového kódu a popisu automatu
  • problematičtější ladění cílového kódu

Soubory ke stažení

Školení Google+ pro firmy

DW - Školení PPC
  • Jak využít Google+ pro firemní komunikaci a marketing.
  • Čím se liší Google+ od Twitteru a Facebooku z pohledu firemního využití.
  • Jak využít Google+ v souladu s pravidly užívání.
  • Založení Google+ Page (Stránky) krok po kroku, včetně praktických tipů.

Detailní informace o školení Google+ »

Ohodnoťte jako ve škole:
Průměrná známka 3,20

Přehled názorů

Degenerat?
rawww 15. 2. 2008 00:58
Nový
└ 
Re: Degenerat?
Pavel Sýkora 15. 2. 2008 08:57
Nový
 
└ 
Re: Degenerat?
Plague 15. 2. 2008 10:44
Nový
 
 
└ 
Re: Degenerat?
Pavel Sýkora 15. 2. 2008 11:45
Nový
Degenerat^2
rawww 15. 2. 2008 01:02
Nový
├ 
Re: Degenerat^2
anonymní uživatel 15. 2. 2008 09:01
Nový
├ 
Re: Degenerat^2
Pavel Sýkora 15. 2. 2008 09:02
Nový
│
└ 
Re: Degenerat^2
David Majda 15. 2. 2008 09:21
Nový
│
 
└ 
Re: Degenerat^2
Pavel Sýkora 15. 2. 2008 10:56
Nový
│
 
 
└ 
Re: Degenerat^2
Lael Ophir 15. 2. 2008 11:53
Nový
│
 
 
 
└ 
Re: Degenerat^2
Pavel Sýkora 15. 2. 2008 12:14
Nový
│
 
 
 
 
└ 
Re: Degenerat^2
Lael Ophir 15. 2. 2008 18:35
Nový
│
 
 
 
 
 
└ 
Re: Degenerat^2
segur 19. 2. 2008 11:29
Nový
│
 
 
 
 
 
 
└ 
Re: Degenerat^2
Lael Ophir 19. 2. 2008 21:40
Nový
│
 
 
 
 
 
 
 
└ 
Re: Degenerat^2
Rejpal 19. 2. 2008 23:52
Nový
│
 
 
 
 
 
 
 
 
└ 
Re: Degenerat^2
Lael Ophir 20. 2. 2008 09:19
Nový
└ 
Re: Degenerat^2
Keny 15. 2. 2008 09:09
Nový
 
├ 
Re: Degenerat^2
anonymní uživatel 15. 2. 2008 09:53
Nový
 
│
├ 
Re: Degenerat^2
Keny 15. 2. 2008 10:18
Nový
 
│
└ 
Re: Degenerat^2
Keny 15. 2. 2008 10:20
Nový
 
└ 
Re: Degenerat^2
Pepa 15. 2. 2008 10:19
Nový
 
 
└ 
Re: Degenerat^2
anonymní uživatel 19. 2. 2008 15:28
Nový
 
 
 
└ 
Re: Degenerat^2
Rejpal 19. 2. 2008 16:06
Nový
awk? awk
b*d 15. 2. 2008 01:12
Nový
├ 
Re: awk? awk
Rejpal 15. 2. 2008 01:19
Nový
└ 
Re: awk? awk
Jakub Šťastný 15. 2. 2008 10:39
Nový
 
└ 
Re: awk? awk
Plague 15. 2. 2008 10:45
Nový
 
 
└ 
Re: awk? awk
Pavel Sýkora 15. 2. 2008 11:21
Nový
 
 
 
└ 
Re: awk? awk
Plague 15. 2. 2008 14:14
Nový
 
 
 
 
└ 
Re: awk? awk
Pavel Sýkora 15. 2. 2008 16:32
Nový
 
 
 
 
 
└ 
Re: awk? awk
b*d 16. 2. 2008 02:34
Nový
Cvicenie
Palo 15. 2. 2008 01:50
Nový
├ 
Re: Cvicenie
Pavel Sýkora 15. 2. 2008 09:58
Nový
│
└ 
Re: Cvicenie
Palo 15. 2. 2008 23:53
Nový
└ 
Re: Cvicenie
Jan Molič 15. 2. 2008 19:39
Nový
Execl
Lael Ophir 15. 2. 2008 05:00
Nový
└ 
Re: Execl
Rejpal 15. 2. 2008 05:19
Nový
 
├ 
Re: Execl
Michal Vyskočil 15. 2. 2008 07:54
Nový
 
└ 
Re: Execl
Lael Ophir 15. 2. 2008 12:00
Nový
 
 
└ 
Re: Execl
Pavel Sýkora 15. 2. 2008 12:34
Nový
 
 
 
└ 
Re: Execl
Lael Ophir 15. 2. 2008 18:48
Nový
Import do excelu
TM 15. 2. 2008 08:04
Nový
Neco podobneho pro XML?
Let_Me_Be 15. 2. 2008 08:11
Nový
├ 
Re: Neco podobneho pro XML?
Pavel Sýkora 15. 2. 2008 09:31
Nový
│
└ 
Re: Neco podobneho pro XML?
Let_Me_Be 15. 2. 2008 11:06
Nový
├ 
Re: Neco podobneho pro XML?
Pepa 15. 2. 2008 10:21
Nový
├ 
Re: Neco podobneho pro XML?
ghostmonk 15. 2. 2008 10:29
Nový
└ 
Re: Neco podobneho pro XML?
Harvie 15. 2. 2008 11:06
Nový
Treetop
Jakub Šťastný 15. 2. 2008 10:32
Nový
├ 
Re: Treetop
Pavel Sýkora 15. 2. 2008 11:05
Nový
└ 
Re: Treetop
Rejpal 15. 2. 2008 19:48
Nový
Zajimavy clanek
Milan 15. 2. 2008 14:36
Nový
└ 
Re: Zajimavy clanek
oemge 15. 2. 2008 15:09
Nový
nadherny vypis
lobo 15. 2. 2008 16:23
Nový
kratsi
VM 15. 2. 2008 16:31
Nový
├ 
Re: kratsi
Pavel Sýkora 15. 2. 2008 16:56
Nový
│
└ 
Re: kratsi
VM 15. 2. 2008 17:17
Nový
│
 
└ 
Re: kratsi
Pavel Sýkora 15. 2. 2008 18:07
Nový
│
 
 
└ 
Re: kratsi
VM 15. 2. 2008 19:54
Nový
│
 
 
 
└ 
Re: kratsi
Rejpal 16. 2. 2008 03:29
Nový
└ 
Uuuu
Lael Ophir 15. 2. 2008 18:52
Nový
 
└ 
Re: Uuuu
VM 15. 2. 2008 19:04
Nový
 
 
├ 
Re: Uuuu
anonymní uživatel 15. 2. 2008 19:25
Nový
 
 
│
└ 
Re: Uuuu
VM 15. 2. 2008 20:40
Nový
 
 
└ 
Re: Uuuu
Lael Ophir 15. 2. 2008 21:31
Nový
 
 
 
├ 
Re: Uuuu
VM 15. 2. 2008 21:54
Nový
 
 
 
│
├ 
Re: Uuuu
Lael Ophir 16. 2. 2008 00:25
Nový
 
 
 
│
└ 
Re: Uuuu
Inkvizitor 16. 2. 2008 10:35
Nový
 
 
 
├ 
Re: Uuuu
Biktop 15. 2. 2008 22:14
Nový
 
 
 
│
└ 
Re: Uuuu
Lael Ophir 16. 2. 2008 00:44
Nový
 
 
 
│
 
├ 
Re: Uuuu
Biktop 16. 2. 2008 18:14
Nový
 
 
 
│
 
│
├ 
Re: Uuuu
tukan 16. 2. 2008 18:46
Nový
 
 
 
│
 
│
└ 
Re: Uuuu
Lael Ophir 16. 2. 2008 23:30
Nový
 
 
 
│
 
└ 
Re: Uuuu
tukan 16. 2. 2008 20:19
Nový
 
 
 
│
 
 
└ 
Re: Uuuu
Lael Ophir 16. 2. 2008 23:53
Nový
 
 
 
│
 
 
 
└ 
Re: Uuuu
Pavel Stěhule 17. 2. 2008 08:57
Nový
 
 
 
│
 
 
 
 
└ 
Re: Uuuu
Lael Ophir 17. 2. 2008 13:47
Nový
 
 
 
└ 
Re: Uuuu
polymorpheus 15. 2. 2008 22:24
Nový
To má být vtip?
Miloš 15. 2. 2008 17:22
Nový
└ 
Re: To má být vtip?
Pavel Sýkora 15. 2. 2008 18:25
Nový
 
└ 
Re: To má být vtip?
Miloš 15. 2. 2008 19:44
Nový
 
 
└ 
Re: To má být vtip?
Pavel Sýkora 15. 2. 2008 21:04
Nový
haha
zh 15. 2. 2008 19:22
Nový
└ 
Re: haha
Pavel Sýkora 15. 2. 2008 21:06
Nový
Každý názor musí mít titulek.
Tomas 16. 2. 2008 00:51
Nový
├ 
Re: Každý názor musí mít titulek.
tukan 16. 2. 2008 01:11
Nový
│
├ 
Re: Každý názor musí mít titulek.
Pavel Sýkora 16. 2. 2008 20:51
Nový
│
│
├ 
Re: Každý názor musí mít titulek.
Palo 16. 2. 2008 23:26
Nový
│
│
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 00:23
Nový
│
│
 
├ 
Re: Každý názor musí mít titulek.
Rejpal 17. 2. 2008 01:21
Nový
│
│
 
│
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 03:19
Nový
│
│
 
├ 
Re: Každý názor musí mít titulek.
Lael Ophir 17. 2. 2008 01:56
Nový
│
│
 
│
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 03:58
Nový
│
│
 
│
 
└ 
Re: Každý názor musí mít titulek.
Lael Ophir 17. 2. 2008 05:46
Nový
│
│
 
│
 
 
├ 
Re: Každý názor musí mít titulek.
Rejpal 17. 2. 2008 07:20
Nový
│
│
 
│
 
 
│
└ 
Re: Každý názor musí mít titulek.
Lael Ophir 17. 2. 2008 13:33
Nový
│
│
 
│
 
 
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 13:40
Nový
│
│
 
├ 
Re: Každý názor musí mít titulek.
Pavel Sýkora 17. 2. 2008 12:10
Nový
│
│
 
└ 
Re: Každý názor musí mít titulek.
polymorpheus 17. 2. 2008 13:41
Nový
│
│
 
 
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 14:48
Nový
│
│
 
 
 
├ 
Re: Každý názor musí mít titulek.
Lael Ophir 17. 2. 2008 15:10
Nový
│
│
 
 
 
│
└ 
Re: Každý názor musí mít titulek.
tukan 17. 2. 2008 16:24
Nový
│
│
 
 
 
│
 
└ 
Re: Každý názor musí mít titulek.
Pavel Tavoda 17. 2. 2008 21:14
Nový
│
│
 
 
 
│
 
 
└ 
Re: Každý názor musí mít titulek.
tukan 18. 2. 2008 00:53
Nový
│
│
 
 
 
│
 
 
 
└ 
Re: Každý názor musí mít titulek.
Pavel Tavoda 18. 2. 2008 08:18
Nový
│
│
 
 
 
└ 
Re: Každý názor musí mít titulek.
polymorpheus 17. 2. 2008 16:59
Nový
│
└ 
Re: Každý názor musí mít titulek.
Tomas 17. 2. 2008 14:11
Nový
└ 
Re: Každý názor musí mít titulek.
Pavel Stěhule 16. 2. 2008 11:16
Nový
 
├ 
Re: Každý názor musí mít titulek.
tukan 16. 2. 2008 15:26
Nový
 
│
└ 
Re: Každý názor musí mít titulek.
Václav Štěpán 17. 2. 2008 13:03
Nový
 
└ 
Re: Každý názor musí mít titulek.
Tomas 17. 2. 2008 14:19
Nový
 
 
└ 
Re: Každý názor musí mít titulek.
tukan 18. 2. 2008 01:52
Nový
 
 
 
├ 
Re: Každý názor musí mít titulek.
Tomas 18. 2. 2008 17:22
Nový
 
 
 
├ 
Re: Každý názor musí mít titulek.
Jakub Safar 18. 2. 2008 18:17
Nový
 
 
 
└ 
Re: Každý názor musí mít titulek.
Pavel Sýkora 20. 2. 2008 10:29
Nový
OOCalc z rychliku?
František Bublík 19. 2. 2008 12:36
Nový
└ 
Re: OOCalc z rychliku?
Pavel Sýkora 20. 2. 2008 11:35
Nový
 
└ 
Re: OOCalc z rychliku?
František Bublík 20. 2. 2008 15:55
Nový
 
 
└ 
Re: OOCalc z rychliku?
Pavel Sýkora 20. 2. 2008 16:35
Nový
       

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem