Poslední články

Web scraping

aneb když to nejde po dobrém ...

(Update: there will probably be some amount of English-speaking people trying to read this article. Sorry, it's in Czech. My next programming articles will all be in English. You can start with Rock Paper Scissors with core.async!)

Challenge: dostat adresář nakladatelů Národní knihovny ČR do Excelové tabulky.

Adresář nepodporuje nějaké hromadné stažení celé databáze - stáhne jen to, co vyhledáte, a má limit 1000 záznamů na stažený soubor. Takže na to musíme nějak chytře. A abychom se neuklikali k smrti, tak si to radši zautomatizujeme.

Krok první: nějak dotazy na tu databázi pokrýt celé spektrum a všechno to stáhnout.

První pokus byl nějak šikovně zformovat URLka a nechat Bash a wget udělat všechnu tu těžkou práci za mě. Jenže adresy se mění a dělá se tam nějaká magie s cookies, není to moc předvídatelné a nedařilo se mi.

Druhý pokus byl přes Twill - pythonovou knihovnu, která imituje prohlížeč - go("http://..."), follow("Link"), get_html() atd. Jenže adresář má nějakou ochranu proti botům, kterou se mi nepovedlo obejít ani změnou User-Agenta. Takže tohle taky ne.

UPDATE: po funuse jsem našel clj-webdriver ... Damn :)

Takže třetí, finální a musím dodat nejbrutálnější pokus: Automator a AppleScript. Jde o to, že programem ovládáte Safari a klávesnici. Vypadá to nějak takhle:

on run {input, parameters}
    set theCharacters to {"a*", "b*", "c*", "d*", "e*", "f*", "g*", "h*", "i*", "j*", "k*", "l*", "m*", "n*", "o*", "p*", "q*", "r*", "s*", "t*", "u*", "v*", "w*", "x*", "y*", "z*","0*","1*","2*","3*","4*","5*","6*","7*","8*","9*"}
    repeat with theChar in theCharacters
        try
            tell application "Safari"
                activate
                do JavaScript "window.open('http://aleph.nkp.cz/F/')" in document 1
                delay 2
                do JavaScript "document.querySelectorAll('a')[15].click()" in document 1
                delay 2
                set theJS to "document.querySelector('input[name=request]').value = '" & theChar & "'"
                do JavaScript theJS in document 1
                do JavaScript "document.querySelector('input[type=image]').click()" in document 1
                delay 2
                
                do JavaScript "(document.querySelector('td[class="title"]') === null) ? 1 : 0" in document 1
                
                if the result = 1 then
                    tell application "System Events"
                        keystroke "w" using command down
                    end tell
                    error 0
                end if
                
                do JavaScript "document.querySelector('td[class="title"]').innerHTML.trim().match(/\d+$/)[0].length" in document 1
                
                if the result > 3 then error 0
                
                do JavaScript "document.querySelector('a[title="Vybrat vše"]').click()" in document 1
                delay 2
                do JavaScript "document.querySelector('a[title="Uložit/odeslat"]').click()" in document 1
                delay 2
                do JavaScript "document.querySelector('input[value="ALL"]').click()" in document 1
                do JavaScript "document.querySelector('input[value="NONE"]').click()" in document 1
                do JavaScript "document.querySelector('input[alt="Uložit/odeslat"]').click()" in document 1
                delay 2
                do JavaScript "document.querySelector('a[href$=".sav"]').click()" in document 1
                delay 2
                set theURL to URL of document 1
            end tell
            tell application "System Events"
                tell application "Safari" to activate
                keystroke "w" using command down
                tell application "iTerm" to activate
                keystroke "wget -q " & theURL & " # " & theChar
                keystroke return
            end tell
        end try
    end repeat
    return input
end run

Má to pár much - když má hledání více než 1000 výsledků, musím ten dotaz rozdělit na menší části. Tedy z "a*" udělám "a*a*", "a*b*", atd. Tohle jsem tam ale už nenakódoval - v AppleScriptu si nejsem tak jistý. Jen jsem nechal AppleScript daný tab nezavřít a nechal jsem si ho na ruční posouzení později. Potom jsem to rozdělení dělal "ručně" s pomocí makra ve VIMu - udělal jsem si řádek s abcdefghijklmnopqrstuvwxyz0123456789, vybral ho a zavolal na něm:

:s/\(.\)/"a*1*",/g

Výsledkem je "a*a*","a*b*",...

Nechal jsem to běžet přes noc (s počítačem se totiž během toho nemůže moc věcí dělat, vyžaduje to focus toho Safari a podobně) a výsledkem je 704 souborů s celkovou velikostí okolo 50MB.

Je tam hodně duplicit - ty by se snad mohly řešit nějakým chytřejším pokládáním dotazů (adresář povoluje dělat ORy atd.) - ale už by mi to komplikovalu logiku toho AppleScriptu. Tohle je simple enough.

Krok druhý: nějak to zparsovat do CSVčka.

Pro představu, každý soubor vypadá asi takhle:

     Záznamy vyhledané v databázích NK ČR
                                    Datum: 26/08/2013


     Číslo záznamu:         1
     Název nakl.         AAA
                          ##spol. s r.o.
     Adresa               Dvojdomí 609/1, 400 01 Ústí nad Labem
     Adresa               Hálkova 4, 120 00 Praha 2
     E-mail               petr.vrzak@centrum.cz
     Identifikátor       978-80-85995
     Prof. zaměření    * Maps
     Datum aktualizace    20060905 zden
     Systém. číslo     000008082

     Číslo záznamu:         2
     Název nakl.         Aaada English School for Children
     Adresa               Hloubětínská 26, 198 00 Praha 9
     E-mail               heck@volny.cz
     Identifikátor       Neregistrován v ISBN
     Datum aktualizace    20060322 sama
     Systém. číslo     000014300

Asi by to šlo udělat AWKem, napadl mě i Haskellovský Parsec, ale protože jsem Clojure fanboy, rozhodl jsem se prozkoumat, co nabízí jeho knihovny. A narazil jsem na Instaparse. Jde o to, že mu předáte ve stringu EBNF grammar, Instaparse vytvoří funkci a té prostě už jen předáváte string s těmi daty. Vypadá to asi takhle:

(ns nakladatelstvi.core
  (:require [instaparse.core :as insta]))

(def zaznamy
  (insta/parser
   "soubor = header, {zaznam}, <whitespace>, Epsilon

    header = <whitespace>,
             'Záznamy vyhledané v databázích NK ČR',
             <whitespace>,
             datum_souboru

    datum_souboru = <'Datum: '>,
                    #'\d{2}/\d{2}/\d{4}'

    zaznam = <whitespace>,
             <cislo_zaznamu>,
             <whitespace>,
             nazev,
             {(<whitespace>, adresa)
              | (<whitespace>, email)
              | (<whitespace>, isbn)
              | <(<whitespace>, odkaz)>
              | (<whitespace>, ico)
              | (<whitespace>, url)
              | (<whitespace>, prof_zamereni)
              | <(<whitespace>, aktualizace)>
              | (<whitespace>, system_cislo)},

    cislo_zaznamu = <'Číslo záznamu:'>,
                    <whitespace>,
                    number

    nazev = <'Název nakl.'>,
            <whitespace>,
            sentence,
            [<whitespace>, <'##'>, sentence]

    adresa = <'Adresa'>,
             <whitespace>,
             sentence

    email = <'E-mail'>,
            <whitespace>,
            word

    isbn = <'Identifikátor'>,
           <whitespace>,
           isbnre

    odkaz = <'Odkaz. forma'>,
            <whitespace>,
            sentence

    ico = <'IČ'>,
          <whitespace>,
          number

    url = <'URL'>,
          <whitespace>,
          word

    prof_zamereni = <'Prof. zaměření'>,
                    <whitespace>,
                    zamereni+

    <zamereni> = <' '?>,
               <'* '>,
               word

    aktualizace = <'Datum aktualizace'>,
                  <whitespace>,
                  sentence

    system_cislo = <'Systém. číslo'>,
                   <whitespace>,
                   number

    <whitespace> = #'\s+'
    <sentence> = ('(', sentence)
               | (')', [[<' '>], sentence])
               | (word, [[<' '>], sentence])

    <word> = #'[a-zA-Z0-9ĚŠČŘŽÝÁÍÉŮÚĎŤŇÓěščřžýáíéůúďťňó*/\-_=?:.,@#!&]+'

    <isbnre> = #'(\d+-\d+(-\d+)?)|Neregistrován v ISBN'
    <number> = #'[0-9]+'"))

(def test-soubor (slurp "adr10124698.sav"))

(def parsed (zaznamy test-soubor))

No a ono to vyhodí něco jako Hiccupovský zápis XMLka (který převedeme na XML samotným Hiccupem):

[:soubor
 [:header
  "Záznamy vyhledané v databázích NK ČR"
  [:datum_souboru "26/08/2013"]]
 [:zaznam
  [:nazev "AAA" "spol." "s" "r.o."]
  [:adresa "Dvojdomí" "609/1," "400" "01" "Ústí" "nad" "Labem"]
  [:adresa "Hálkova" "4," "120" "00" "Praha" "2"]
  [:email "petr.vrzak@centrum.cz"]
  [:isbn "978-80-85995"]
  [:prof_zamereni "Maps"]
  [:system_cislo "000008082"]]
 [:zaznam
  [:nazev "Aaada" "English" "School" "for" "Children"]
  [:adresa "Hloubětínská" "26," "198" "00" "Praha" "9"]
  [:email "heck@volny.cz"]
  [:isbn "Neregistrován v ISBN"]
  [:system_cislo "000014300"]]]

V reále to je ještě trochu složitější - musíme escapovat ampersandy, apostrofy a podobně, aby nás XML mělo rádo, přidat umlauty do podporovaných znaků a podobně - ale nakonec se přece jen povede a všechny soubory jsou přečteny. Potom se už jen sloučí jedním reduce a vyplivne se XML.

Mimochodem, je úžasné, jak jednoduše Clojure umožňuje využít více jader procesoru. Můj kód chroustal hlavně dvě věci - první bylo samozřejmě vyparsovat z toho souboru data do té hierarchické struktury - a druhý bylo "zálohování" na disk. Prvně jsem se díval, že mě to, že jedou sekvenčně po sobě, víceméně brzdí. Tak jsem to zálohování obalil do future, a bylo. Naráz oba ty úkoly jely zároveň. Žádné locky, žádné mutexy, žádné semafory ... Jedna future. Úžasné!

No, ale to XML. Do CSVčka se dá docela jednoduše převést pomocí XSLT:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <xsl:for-each select="//zaznam">
      <xsl:text>
</xsl:text>
      <xsl:value-of select="system_cislo"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="nazev"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="email"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="adresa"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="isbn"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="prof_zamereni"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="ico"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="url"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

(Pozn.: asi by šlo použít i clojure.data.csv nebo tak něco, ale když už to Instaparse vyhazuje v Hiccup formátu a Hiccup exportuje do XML... :) )

Krok třetí: vyhodit duplicity.

Tohle je nejjednodušší: Otevřu CSV v Excelu a vyfiltruju to podle sloupce Systémové číslo.

Done!

Myšlenky z UNITED 2013

aneb nebylo to jen o hudbě

Tohle je můj přepis myšlenek různých řečníků z UNITED fetivalu - těch, které se mi zdály, že stojí za to si je zapsat a přemýšlet o nich. Nebrat jako dogma - ale zkoumat! Tak třeba zaujmou i někoho jiného.


Bůh zemřel, abys ty nebyl nula!

Kontext: 2. Samuelova 9 - David a Mefíbošet. Mefíbošetovo sebevědomí je na úrovni "mrtvého psa" (to jsem si nevymyslel, to jsou jeho slova). "Co je tvůj služebník? Proč se obracíš k mrtvému psu, jako jsem já?"

Ale Bůh nám tím, co udělal ON, dal hodnotu, kterou neztrácíme ničím, co děláme MY. Bylo za tebe zaplaceno, akorát se můžeš rozhodnout přehlížet to a platit za sebe sám! Jsi dítě KRÁLE, ale můžeš to stále ignorovat a chovat se jako sirotek.


Vytvořili jsme kulturu posluchačů.

Nepřemýšlíme nad tím, co k nám pastor mluví, ale jde nám o to, jestli to je krátké a vtipné. Možná alternativa (je na vás, zda to je krok správným směrem): Kultura čtenářů Bible, kteří nad tím přemýšlí. Jako Berienští (Skutky 17,10-12):

Bratři tedy hned v noci vyslali Pavla i Sílu do Berie. A když tam dorazili, šli do židovské synagogy. Tito Židé však byli ušlechtilejší než tesaloničtí. Přijali Slovo se vší dychtivostí a každý den zkoumali Písma, zda jsou ty věci tak. Mnozí z nich tedy uvěřili, a také nemálo ctihodných řeckých žen a mužů.


Děláme Satanu větší reklamu než Kristu.

"Metalová hudba je ďábel, piercing je ďábel, tohle a tohle a tohle je ďábel" - místo abychom mluvili o Kristu! Ale ten, kdo je v nás, je přece silnější než ten, kdo je venku! Nesnaž se dostat ze svého života hřích - snaž se dostat do svého života Ježíše! On je silnější - on ho vytlačí! On si uklidí!

Přeneseno z bojů v nás samých na naše upejpavé komentáře k lidem jiným: Představte si člověka, na kterém vidíte něco, co si myslíte, že by měl změnit. Spíš než říkat mu, aby dělal tohle nebo nedělal tamto, má větší smysl nechat to za vás udělat Ducha Svatého. Na vás je v tu chvíli vést člověka k Ježíši, ne tahat ho ze všech různých věcí, které VY nemáte rádi. Udělali byste z něj sebe. JEŽÍŠ z něj chce udělat sebe! (Tím nemizí individualita toho člověka.)


 David a Goliáš

Kdo podle vás vyhrává? Ten silnější nebo ten slabší? Je v tom chyták - ve skutečnosti vyhrává ten silnější - Bůh. David se jen nebál "postavit obrům" - postavit se tomu, kdo řve, zastrašuje, vysmívá se a působí nevíru, když to neudělal nikdo jiný. Co máme vedle sebe my? Šikanu? Rasismus? Něco jiného?

Bůh chce, abychom se dívali, co jsou ti Goliášové, a abychom se jim postavili. "Ty proti mně jdeš s mečem, kopím a šavlí, ale já jdu proti tobě ve jménu Hospodina zástupů, Boha izraelských šiků, které jsi urážel."

Nemůžeme ale proti Goliášům bojovat zbraněmi Goliášů! Nemůžeme bojovat nenávistí, intrikami, manipulací a lží. Bůh má své zbraně. Musíme se ptát Boha. David - ikdyž věděl, jak něco udělat - se pokaždé dotazoval Boha.

Vzepření se obrům vyžaduje boj a oběti. Nebude to zadarmo. Může nás to stát modřinu, výpověď i život. Ježíš se postavil největšímu Goliáši - a své vítězství zaplatil životem. Máme ho následovat - ale jsme ochotni jít do plánu, který pro nás má, ať to stojí, co to stojí?


Evangelium v postmoderní době

Jsme zaplaveni slovy, informacemi a nabídkami - naše kultura je jako tržnice. Ve vzduchu je neustále "mučivá nutnost volit". Duchovní hodnoty jsou také zboží - nemůžete nabízet něco, o co není zájem. A není pravda, že by o duchovno nebyl zájem. Jako příklad si vemte "zážitkovou spiritualitu" (experimentování) - horoskopy, amulety, lucidní snění, out-of-body-experience, atd.

Jak zní evangelium do tohoto prostředí? Možnosti:

  • bojovat proti duchu doby - je třeba se tomu vyhnout, udělat si pevnost a zabarikádovat se
  • přijmout ducha doby - církev stylem Disneyland, je to o srandě
  • nebo - nebát se a nezdržovat se určitých hodnot jen proto, že se k nim hlásí někdo jiný (New Age atd.)

Jaké to jsou hodnoty?

  • příroda - máme God's Word a God's World! Příroda není tak nádherná, abychom se jí učili vyhýbat!
  • fantazie, představivost - Bible je (kromě všeho jiného) knihou básníků a zpěváků!
  • příběh - Matouš 13,34 - "Toto všechno Ježíš říkal zástupům v podobenstvích a bez podobenství k nim vůbec nemluvil." Kdo jede abstraktně, bez děje a příběhů, nemá moc posluchačů.
  • zkušenost - autentický prožitek je pro tuto generaci to hlavní. Křesťanství je o tom, že jsi součástí příběhu! Můžeš zažít ten příběh sám - ne jen o tom číst! Ale naopak - být součástí aktuální kapitoly!
  • tělo - smysly - máme je zbytečně? Proč si Bůh se vším tak vyhrál, když se tomu bráníme? Tělo není schránka, ve které je duše uvězněna - my JSME tělem!
  • vztahy - epidemicky se šíří izolace a samota lidí, ikdyž máme FB a podobně. Lidé prahnou po hlubokých vztazích. Církev - společnství - to nabízí. Lidé tam "jsou fajn a jsou věřící a možná to nějak souvisí?!"
  • nebe - krása a dobro je ochutnávka něčeho věčného a lidé po tom touží. Všechny ty obrazy v Bibli... Říkáme, že to vyhlížíme s nadějí - ale možná jen říkáme...? Proč je tak málo křeťanů, kteří se těší na nebe?

Máme postoptimismus. Už nikdo nemluví o pevných krocích k šťastnějším zítřkům a lidé většinou nemají naději do budoucna - ale můžeme jim nabídnout možnost, že Země je přes to všechno v dobrých rukou, že to má smysl a že to někam směřuje.


Mimochodem, na Twitteru jsem objevil G. K. Chestertona. Tedy spíš jeho reinkarnaci skrze citáty. Jsou si s C. S. Lewisem docela podobní!

"Weak things must boast of being new, like so many German philosophies. But strong things can boast of being old."

"Slabé věci se musí chvástat tím, že jsou nové, jako spousta německých filozofických směrů. Ale silné věci se mohou chvástat tím, že jsou staré."

"Yielding to a temptation is like yielding to a blackmailer: you pay to be free, and find yourself the more enslaved."

"Ustoupit závislosti je jako ustoupit vyděrači: zaplatíš za to, abys byl svobodný, a zjistíš, že jsi tím více zotročen."

Dojmy po dni s Lumií 520

aneb rozbitý iPhone jde na střídačku

(Žel, Nokii Nokií vyfotit nemůžu. V zrcadle ji fotit nebudu, ačkoliv neoblečen na to jsem dost a duckface bych určitě taky nějak sesmolil. Takže tady máte aspoň pro představu fotku z - už - neostřícího iPhonu :) )

Focus. Focus. FOCUS!!!

  • Kupoval jsem ji od VKservis.cz. Doteď netuším, jestli je moje Lumia ze zámoří, nebo určena pro ČR. Třeba takové offline mapy se pro ČR, SR a PL stahují bez problémů, takže ... asi mi to je jedno :) Nicméně v nastavení se občas mihla Malajsie (klávesnice, regional settings, ...) a manuál je anglicky - takže kdo ví.

So. Many. Smileys.

  • "Nabíječka" - tedy spíš ten převodník USB→220V - je od LG :))

  • USB káblík je superkrátký (tak 20cm?). Uvidíme, jestli to bude vadit nebo to bude win. Zatím se nemůžu rozhodnout.

  • Sluchátka jsem nezkoušel (mám svoje Bluetoothové), ale za tu cenu bych nic extra neočekával :) Recenze ostatních říkají, že to je šunt.

  • 8 GB je málo. Čtyři roky staré 3GSko mělo 32GB a to jsem v životě nezaplnil. Nemusel jsem řešit, které MP3ky z iTunes tam syncnu a které ne. Tady jo, už mám paměť plnou. Ale to vyřeší SD karta.

  • Balast od výrobce (Angry Birds Roost, App Picks, Panorama, ...) jde v pohodě smazat. To by se Androidy a Apply mohly naučit :) A jiný balast je zase děsně užitečný - řeč je o Nokia HERE Mapách. Stáhnout do offline paměti jde, na co si vzpomenete. Zkoušel jsem kromě ČR, SR, a PL třeba i takový Nový Zéland - jen jestli to jde. Jde.

  • Když už mluvíme o mapách ... GPS je CRAZY FAST. Nevím, jestli to je jen tím, že jsem čtyři roky byl na 3GSku, ale ... tak rychlé zaměření a tak rychlé updaty jsem ještě neviděl. <3

  • A ještě jednou o mapách - i v takových prčicích, jako je Návsí, znají spoustu "places" - školy, bankomaty, pošta, vlak, všechno. A co neznají Nokia HERE Mapy, to znají Mapy.cz (u těch ale verze pro WP ale nepodporuje stažení map, jede čistě online).

  • Propojení Facebook chatu s nativními Messages funguje, ale ke zprávě nejde třeba připojit fotku a duplikuje se mi to s notifs v normální Facebookové aplikaci. Takže to mám vypnuté.

  • WP8 nemá orientation lock. Zatím mi to TROCHU!!! vadí.

  • Ve stejném duchu Bing tlačítko. Naučím se mu vyhýbat? A ignorovat ho? Doufám, že ano.

  • Klávesnice se nejdřív snažila být chytřejší než já, tak jsem si na ni došláp. Hned je to lepší!

Nechci chytrou klávesnici.

  • Jinak, klávesy jsou trošku menší a jinak "pod sebou" než u 3GSka, takže troška zvykání přece jen bude.

  • Nevím, co se recenzentům nezdálo na sloupávání zadního krytu (pro výměnu baterie atd.) ... Asi zrovna měli ostříhané nehty :) Některé telefony, se kterými jsem měl tu čest, to mají mnohem těžší. U téhle Lumie to jde v pohodě.

  • Černá sice opravdu není úplně černá, ale nejspíš jsem zvyklý z 3GSka na ne-tak-peckové displeje a nepoznám to. Přejít na tuhle Lumii z nějakého Super AMOLEDu, tak asi nadávám jak špaček. Jo a šmouhy od palců vidět JSOU :D

  • Designově WP8 a tahle Lumia <33333

Vyfoť svoji plochůůůůů!!!

  • Celkově? Myslím si, že za tu cenu (3500,- s DPH, tedy reálně míň) bych si snad nic víc ani přát nemohl. Není to high-end, ale není ani zasekanej nebo něco. Všecko stíhá, jen se prostě nejspíš šetřilo na displeji, na přední kamerce (hint: žádná tam není) a podobně. Uvidíme za pár měsíců!