Python - Adatletöltés és -feldolgozás automatizálása

2019. január 9-én megjelent egy írás (Ínséges napok címmel) a Dunai Szigetek blogon, mely hidrológiai adatok statisztikai feldolgozásának eredményeit ismertette. A szerzővel a cikk megjelenése előtt röviden beszélgettünk erről az elemzésről, tettünk néhány javaslatot is az adatkezelésre, -vizualizációra vonatkozóan. Sajnos nem volt annyi időnk a közös munkára, hogy igazán elmélyedjünk a témában és részleteiben is megismerhessük az adatokat. Úgy tűnt azonban, hogy van még potenciál ebben a témában, így a szerző által is leírt adatgyűjtési utat követve ellátogattam a Hydroinfo archívumába, hogy letöltsem több mint 130 év budapesti vízmércére vonatkozó vízállás adatait. Először elkezdtem manuálisan leszedni az adatokat, ám nagyjából a tizedik év után eluntam és elkezdtem az automatizálás lehetőségén törni a fejemet. Ebben a cikkben ezt az automatizálási folyamatot és a hozzá készített szkriptet mutatom be, az adatok elemzésének eredményeit egy későbbi cikkben ismertetem majd.

Az automatizálás fontosságát újra és újra igyekszem hangsúlyozni, legfőképpen azért, mert rengeteg erőforrás (idő, energia, pénz, stb.) takarítható meg egy-egy jól megválasztott (rész)folyamat automatizálásának köszönhetően. Mutattam már példát, hogyan lehet billentyűmakrók vagy Excel makrók segítségével feladatok elvégzését gyorsítani, kényelmesebbé tenni. Az automatizálást különösen fontosnak érzem az adatelőkészítés területén, amelyről tapasztalatom szerint nagyon keveset beszélünk. Feltételezem azért, mert unalmasnak, monotonnak, "úgy-se-érdekel-senkit" jellegűnek, stb. érezzük és valóban, sokszor az is. Pedig majd' minden elemző, térinformatikai munkához szükséges idő oroszlánrésze az adatok megfelelő formába hozásával telik. Ha tehát ezen a területen sikerül növelni a hatékonyságot, jelentős lesz az erőforrás-megtakarítás.

Vizsgáljuk meg a feladatot ebben a konkrét esetben! A Hydroinfo oldalán kiválasztva a budapesti vízmércét, megtudjuk, hogy az adatok 1876 és 2003 közötti évekre állnak rendelkezésre. (Megjegyzés: Az adatok elérhetők 2004 és 2005 évekre is, így ezeket is letöltjük.) A beviteli mezőbe beírva egy évszámot (pl. 1876) és az OK gombra kattintva megjelenik az adott évre vonatkozó vízállás adatok.

Hydroinfo - A Duna vízállás adatai, 1876

Ha közben figyelünk a böngészősáv tartalmára, akkor láthatjuk, hogy a kattintás eredményeképpen az alábbi URL-re irányított minket a weboldal: http://www.hydroinfo.hu/vituki/archivum/bp1876.htm. Ha visszalépünk és a következő évet írjuk be a beviteli mezőbe (és az OK gombra kattintunk), akkor pedig erre az URL-re irányít: http://www.hydroinfo.hu/vituki/archivum/bp1877.htm. Kipróbáltam még néhány évszámot, s ezek is megerősítettek abban, hogy van szabályszerűség (névkonvenció) az adatok tárolásában és elérésében: a "bp" betűk után az évszámot írva és mögé a ".htm" kiterjesztést fűzve érhetjük el az adott évi adatokat. A szabály ismeretében egy ciklus segítségével végig tudjuk járni az összes év adatait tartalmazó oldalakat.

A következő, amit megvizsgáltam, hogy miként tartalmazza a weboldal a vízállás adatokat. Ehhez megnyitottam a böngésző fejlesztői eszköztárát az F12 funkcióbillentyűvel, s az Inspector (Elem vizsgáló) eszközzel kiválasztottam a táblázatot. Ennek hatására a Fejlesztő eszköztár egyik ablakában megjelent a weboldal HTML kódja, s azon belül az adatokat tartalmazó rész.

Hydroinfo - Vízállás adatok vizsgálata a böngésző fejlesztőeszközei segítségével

Úgy láttam, hogy a teljes adatlap egy <pre> tag-en belül helyezkedik el. A <pre> tag formázott szöveg megjelenítésére szolgál, amelynek sajátossága, hogy azonos szélességű betűkből álló betűtípussal, a szövegben megadott sortöréseket és szóközöket megtartva jelenik meg. Számomra azonban az volt a legfontosabb, hogy az adatok egyszerű szövegként szerepelnek a weboldalon és nem HTML táblázatként.

Ezt követően a <pre> tag tartalmát, az adatokat is tartalmazó szöveget vizsgáltam meg alaposabban. Mivel nekem nem kell az egész adatlap, olyan jellegzetességet kerestem, amellyel könnyen megragadható a számomra fontos rész (az adatok). Arra jutottam, hogy a "Nap" szó első előfordulásától érdekes, egészen a "Minimum" szó első előfordulásáig. Végeztem néhány ellenőrzést a böngésző kereső funkcióját (Ctrl+F) használva és több évnél is igaznak bizonyult ez a megállapítás.

Ezzel a bemeneti (input) adatok megismerésével végeztem, ideje volt, hogy megfogalmazzam, milyen formátumban szeretném látni az adatokat a kimeneti (output) oldalon. Mivel a bemeneti oldalon szöveg érkezik, a kimeneti oldalon is a szöveg tűnt a legegyszerűbbnek, így végeredményként egy CSV formátumú fájlt képzeltem el. Ebben pedig hat oszlopot szerettem volna:

  • year (évszám)
  • month (hónap)
  • day (nap)
  • str_value (az eredeti érték, ami az adatlapon szerepel)
  • num_value (az eredeti értékből eltávolítva a betűket)
  • flag (a num_value-ból eltávolított betűk (A, P vagy Z), amelyek a jég jelentére utalnak, jelentésük a Hydroinfo weboldalán megtalálható)

Ilyen módon megvan az eredeti érték is (str_value) ellenőrzésképpen, de akár rögtön a számszerű adatokkal (num_value) is lehet dolgozni. Az idő ilyen jellegű bontása pedig lehetővé teszi, hogy könnyen szűrjünk tetszőleges intervallumot a teljes idősorból, pl. csak a májusi vízállásokat vagy csak a hónapok első napján érvényes vízállásokat, stb.

Ismert az input, ismert az output, ideje a kettő közötti logikát összeállítani és valamilyen nyelven megfogalmazni. Ehhez a feladathoz a Python nyelvet választottam, amelynek roppant praktikus okai voltak: viszonylag jól ismerem és még a gépen is telepítve volt.

A Python egy (szerintem) könnyen tanulható programozási nyelv, amivel nagyon sok helyen találkozhattok, például térinformatikai szoftverek is előszeretettel használják. Ahhoz, hogy a kód futtatható legyen, a Pythont le kell tölteni és telepíteni kell. A Python esetében fontos, hogy melyik verziót használjuk, nálam a Python 2.7.15 volt fent, ezzel a verzióval biztosan működik a kód. (Ha 3.x verzióval szeretnétek használni, bizonyos részeit (pl. urllib) át kell írni, de a kód nagy része az újabb verziókkal is használható.)

A Python szkript megírásához a Notepad++-t használtam (a Language menüben kiválasztottam a Python nyelvet). A (fél)kész kódot pedig mindig elmentettem egy .py kiterjesztésű fájlként.

Kisebb kódrészletek teszteléséhez, ellenőrzéséhez a Python (command line) felületét használtam, a változók tartalmát is ezzel fogom bemutatni. (Ez a Python-al együtt települ.)

Komolyabb teszteléshez, s később az éles futtatáshoz megnyitottam a Windows parancssort, majd a Python telepítési könyvtárába navigáltam, ami nálam a C:\Programs\Python27 mappa.

cd c:\programs\python27

A Python könyvtárában kiadtam az alábbi parancsot, ahol a python utal arra, hogy egy Python nyelven írt szkriptet szeretnék futtatni, az elérési út pedig a szkriptemet tartalmazó fájl elérési útja.

python d:/adatterkep/projektek/dunaiSzigetek/insegesNapok/getAndProcessDataFromHydroinfo.py

Nagyon fontos! Az elérési útban sima perjeleket ( / ) használj, ne visszaperjeleket ( \ )!

Python kód futtatása Windows parancssorból

A szkript első három sorában importáltam 3 modult. A modulok a Python meghatározott feladatokat ellátó építőkövei, alkatrészei. Maga az "alap" (core) Python is sok mindent képes elvégezni, de tudása modulok segítségével bővíthető. Léteznek külön telepítést igénylő modulok, ám számos modul telepítésre kerül az "alap" Python mellé, annak telepítése során is. Ám annak érdekében, hogy a Python minél gyorsabban végre tudja hajtani a rá bízott feladatokat, még ezeket sem tölti be, csak akkor, ha erre külön megkérjük, vagyis a modulban leírt tudásra szükségünk van. A modulok, amiket importáltam, lehetővé teszik, hogy értelmezni tudjon egy URL-t (urllib modul) és reguláris kifejezéseket (re modul).

import urllib
import re

A modulok importálása után változókat hozunk létre és rögtön értéket is adunk nekik.

A változók elnevezésének a Pythonban is megvannak a maga szabályai, az értékadás azonban más nyelvekhez hasonlóan itt is az egyenlőségjellel történik: az egyenlőségjel jobb oldalán lévő kifejezés értéke kerül az egyenlőségjel bal oldalán lévő változóba, s aztán a változó nevével ez az érték később akármikor előhívható.

Az urlbase változóban a Hydroinfo weboldal linkjének elejét (az évszámot már nem) tároljuk majd el szövegként. A szöveg jelölésére használhatunk idézőjelet és aposztrófot is, én az utóbbit szoktam. A path változóban a munkakönyvtár elérési útját tároljuk el, ide kerül elmentésre a CSV fájl a szkript futása során. (Fontos: itt is perjeleket használjunk, ne visszaperjeleket!) A datafile változóban pedig a létrehozandó CSV fájlt nyitom meg szerkesztésre. Az open() parancsnak két paramétert kell átadnunk: a CSV fájl elérési útját (ezt a path változó és egy fájlnév összefűzésével állítjuk elő) és egy kódot ('w'), amely azt jelenti, hogy szerkesztésre, írásra alkalmas módon szeretnénk megnyitni a fájlt. Ha olyan fájlra mutatunk, amely még nem létezik (mint most), a Python létre fogja hozni a megadott helyen a CSV fájlt. Ebbe az üres fájlba rögtön bele is írom az elképzelt CSV fájl fejlécét: tagolókarakternek a pontosvesszőt (;) választottam, a sorvégén látható '\n' pedig a sortörést jelöli, azaz bármit is fűzünk a fejléc után, az már egy új sorban fog megjelenni.

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

Következő lépésben készítünk egy ciklust, egészen pontosan egy for ciklust. Azt szeretnénk, ha 1876-tól 2005-ig minden egyes évszámon végigmenne a ciklusunk és végrehajtaná a ciklusmagba írt utasításokat. Ehhez a range() funkcióval létrehozunk egy listát, melynek két paramétert kell átadni: a kezdőérték a lista első eleme lesz, a záróérték viszont már nem lesz része a listának. Így a range(1876, 2006) jelenti majd a számunkra érdemes időszakot. A ciklus fejléce a for kulcsszóval kezdődik, majd jön egy változó (esetünkben: y), ami az éppen aktuális értéket (esetünkben évet) fogja tartalmazni, majd az in kulcsszó és végül a lista, amit az imént a range(1876, 2006) funkcióval fogalmaztunk meg. A ciklus fejlécét egy kettőspont zárja le. A következő sorokba már a ciklusmag kerül, amit új jelzünk, hogy behúzást kapnak a sorok. Fontos, hogy a behúzás egységes legyen és ne keveredjenek a kódban a tabulátorok és szóközök, amikor behúzzuk a sorokat. Én általában 4 darab szóközt használok sorbehúzásként. A ciklusmag a lenti kódban még csak egy komment, aminek a jele Pythonban a kettőskereszt (#) a sor elején.

A kommentekről az Excel makró alapfogalmak bemutatásánál írtam korábban, azonban a lényeg igaz itt is: a kommentként megjelölt sorok nem kerülnek végrehajtásra, ezeket a program a futás során átlépi, kihagyja.

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    #Ciklusmag

A ciklusmag utasításai minden év esetén végrehajtásra kerülnek, tehát úgy kell gondolkodnunk ezekről, hogy minden adatlap esetén ezeket kell végrehajtani, függetlenül attól, hogy melyik évnél jár a ciklus. Elsőként az év aktuális értékét tároljuk el szövegként is (az y változóban számként tárolódik), hogy később szövegek összefűzésénél egyszerűbben használhassuk. A str() függvény használható arra, hogy a paraméterként kapott értéket szövegként tárolja el, így a year változóban tárolhatjuk a str(y) függvény eredményét. Az url változóban eltároljuk az adott évi adatlap URL-jét, összefűzve az urlbase változót, a year változót és fixen a '.htm' szöveget.

Az adatlap URL-jének ismeretében megszerezhetjük a weboldal HTML kódját szövegként és eltárolhatjuk egy újabb változóban (page). Ehhez az urllib modul urlopen() funkcióját fogjuk használni, melynek paraméterként egy URL-t adunk át szöveges formában, eredményül pedig a megadott webcímen elérhető weboldalt kapjuk egy (fájl)objektumként. Ebben a formában számunkra nem megfelelő, jobb lenne szövegként látni a HTML kódot. Szerencsére az objektumnak van egy read() funkciója, amely szöveges formában adja vissza a HTML fájl tartalmát. Ezeket a funkciókat össze is fűzhetjük, így a weboldal HTML-jét szöveges formátumban így nyerhetjük ki: page = urllib.urlopen(url).read().

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()

A page változóban tehát már benne van a weboldal teljes HTML kódja <html> tagtől </html> tagig.

Python - A page változó tartalma

Ennek azonban csak egy része kell, s korábban úgy határoztuk meg, hogy a "Nap" szó első előfordulásától a "Minimum" szó első előfordulásáig tartó rész lesz a nyers adat, amit aztán feldolgozunk. Valahogyan tehát darabolnunk kell a szöveget. A Pythonban a szövegben minden karakter rendelkezik egy indexszel, amely lényegében arra utal, hogy az a karakter hányadik helyen található a karaktersorozatban. Az első karakter indexe 0, a másodiké 1 és így tovább. Ezek segítségével darabolni is lehet a szöveget az alábbi szintaktikát követve: string[startIndex:stopIndex]. A startIndex karaktere része lesz az eredményül kapott karaktersorozatnak, a stopIndex pozíciójában lévő karakter azonban már nem. Ha tehát a 'weboldal' szóból a 'bold' részt szeretnénk kiemelni, azt így kell megfogalmazni: 'weboldal'[2:6].

A page változóban eltárolt szöveget is tudjuk ilyen módon darabolni (page[startIndex:stopIndex]), azonban nem tudjuk, mettől (startIndex) meddig (stopIndex) kellene kivágni. A "Nap" és "Minimum" szavakat a find() funkció segítségével fogjuk megkerestetni. Ez a metódus a szövegben a paraméterként átadott szöveg első előfordulásának helyét, indexét adja eredményül. Például a 'weboldal'.find('old') eredménye 3. A 'weboldal'.find('l') eredménye pedig 4, mert bár kétszer szerepel az "l" betű a szóban, csak az első előfordulást keressük a funkció segítségével. A startIndex helyére behelyettesítjük a page.find('Nap'), a stopIndex helyére pedig a page.find('Minimum') kifejezéseket és így már meg is kaptuk a számunkra érdekes szövegrészletet, amit tároljunk a raw változóban.

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]

Python - Az adatokat tartalmazó szövegrész kivágásának eredménye

Az így kapott nyers, szöveges adatok már nagyon hasonlítanak egy táblázatra, ezt a látszatot azonban évenként eltérő számú és elrendezésű új sor (sortörés) karakter kelti. Annak érdekében, hogy ne kelljen a különböző új sort jelölő speciális karakterekkel bajlódnunk, bízzuk a Pythonra a szöveg sorokra bontását! A splitlines() metódus segítségével egy szöveget az új sort jelölő karakterek mentén fel tudunk darabolni, a szöveg részeit pedig egy listában kapjuk vissza. A nyers szöveg darabolásával kapott listát tároljuk el a rows nevű változóban.

A listát szögletes zárójelek között a lista elemeinek felsorolásával kell elképzelni, pl. [item1, item2, item3, item4, ...]. A lista eleme bármi lehet, szöveg, szám vagy akár egy másik lista is.

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]
    rows = raw.splitlines()

Python - A rows változó tartalma

A rows listában kétféle sor található: üres (vagy legfeljebb szóközöket tartalmazó), valamint adatokat tartalmazó. Az előbbiekre nem lesz szükségünk, az utóbbiak vizsgálatából pedig az derül ki, hogy az értékeket elválasztó szóközök száma attól függ, hogy hány számjegyből álló értékeket választanak el, ráadásul a hiányzó értékek helyén is szóközök vannak. Például a január 31-hez tartozó érték után sok-sok szóköz helyezkedik el (mivel nincs február 31, ezért érték sem tartozhat hozzá), míg elérünk a március 31-hez tartozó értékig. Először tehát ezeket a "nincs érték" jelentésű szóközöket kellene valamilyen helyőrző karakterre cserélni. A helyőrző karakter lehetne pl. egy mínusz jel (-), de talán még jobb, ha egy szóközre, mínusz-jelre és még egy szóközre (' - ')cseréljük, hogy se a "nincs érték" jelölő előtti, se az azt követő értékhez ne "tapadhasson" hozzá a mínusz-jel. Ehhez a re modul sub() funkcióját használjuk, amely reguláris kifejezések segítségével képes helyettesítést, cserét végezni a paraméterként kapott szövegben. A sub() funkció számára 3 paramétert adunk át:

  • Mit cseréljen? Kis számolgatás és tesztelés után úgy láttam, hogy hét darab szóköz helyére kell a helyőrző karaktersorozatot beilleszteni, akkor kapunk jó eredményt. Tehát pontosan 7 darab egymást követő szóközt kell keresni, amit reguláris kifejezés segítségével így fogalmazhatunk meg: ' {7}'. Ez annyit tesz, hogy a {x} előtti karakterek (esetünkben egy darab szóköz) pontosan x számú (esetünkben 7) egymás utáni előfordulásait.
  • Mire cseréljen? Szóköz-mínusz-szóköz karaktersorozatra: ' - '.
  • Milyen szövegben végezze el a cserét? Minden egyes sorban, amit úgy érünk el, hogy egy for ciklussal végigmegyünk a rows minden elemén, az egyes sorokra pedig row-ként utalunk.

A megfelelő paramétereket beírva a funkció így fog kinézni: r = re.sub(' {7}', ' - ', row). Mivel még mást is szeretnénk csinálni az adott sorban lévő szöveggel, a funkció eredményeként kapott szöveget egy változóban (r) tároljuk.

Azért hozunk létre egy új változót, mert a ciklus fejlécében megadott változókon nem javasolt változtatni, mert a változtatás okozhat olyan hibát, amitől az egész ciklus és a program is hibára fut. Ha a row nem ciklusváltozó lenne, az eredményt akár a önmagába is visszaírhatnánk, hiszen a cserék után a régi szövegre már nincs szükségünk.

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]
    rows = raw.splitlines()
    for row in rows:
        r = re.sub(' {7}', ' - ', row)

Most, hogy a "nincs érték" jelentésű helyőrzők már megvannak, meg kell oldanunk valahogy azt a problémát, hogy nem tudjuk, mikor mennyi szóköz választja el az egyes értékeket. Ezért eltávolítjuk az egymást követő szóközöket úgy, hogy mindenhol csak egy maradjon. Ehhez ismét a re.sub() funkciót használjuk, a reguláris kifejezést azonban most így fogalmazzuk meg: '\s+'. A '\s' a whitespace karakterek általános jelölésére szolgál, azaz minden, karaktereket üres hellyel elválasztó speciális karaktert lefed, pl. szóköz, tabulátor, új sor, stb.). Ennek ismeretében a reguláris kifejezés annyit tesz, hogy a plusz jel előtti karakter '\s' egymás utáni előfordulásait cseréli, akármennyiszer követi is üres hely üres helyet (ebben az esetben szóköz szóközt). Az egymást követő üres helyekre pedig minden esetben egyetlen szóközt illesztünk. Az r = re.sub('\s+',' ') funkció hatására azonban a sorok elején és végén maradhat még számunkra felesleges szóköz (persze legfeljebb egy-egy). Ezek eltávolítására szolgál a strip() function, amelyet így az előbbi kifejezés mögé fűzünk: r = re.sub('\s+',' ').strip().

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]
    rows = raw.splitlines()
    for row in rows:
        r = re.sub(' {7}', ' - ', row)
        r = re.sub('\s+', ' ').strip()

Python - A raw változó tartalma a felesleges szóközök eltávolítása után

Ez már egészen közel áll ahhoz, amire szükségünk van. Ha sikerülne eltávolítani azt a néhány üres sort, a szóközök helyett pontosvesszővel tagolni, kaphatnánk egy olyan állományt, amelyet akár ki is írhatnánk egy CSV fájlba, s akkor lenne minden év adatairól egy-egy CSV fájlunk naptárszerű elrendezéssel. Mi azonban egyetlen CSV fájlt szeretnénk, amihez még tovább kell dolgoznunk az állományon, ezért ezt a szövegekből álló listát megpróbáljuk még inkább "táblázatosítani". A Pythonban ezt például listák (list) segítségével lehet megtenni.

Az adatokat tartalmazó, szöveges elemekből álló listát (rows változó tartalma) egy olyan egymásba ágyazott listává (nested list) alakíthatjuk, ahol minden sor

  • eleme az adathalmazt leíró listának (ezt a matrix változóban fogjuk tárolni) >> ez már kész is, a rows jelenleg is így tárolja az adatokat
  • maga is egy lista, melynek elemei a sorban található értékek >> ehhez minden sort darabolnunk kell az értékeket elválasztó szóközök mentén.

A sorokra darabolás már a splitlines() alkalmazásakor megtörtént, s jelenleg éppen egy soronként haladó iterációban vagyunk, a sor szövegében végeztünk cseréket. Az aktuális sor szövegének darabolását a split() funkció végzi el számunkra, melynek paramétere a tagolókarakter(eke)t tartalmazó szöveg, eredményül pedig rögtön egy listát ad vissza. Az r változóban tárolt szöveget szóközök mentén tagolva (r.split(' ')) egy listát kapunk, amelynek első eleme a nap sorszáma, a továbbiak pedig az egyes hónapokban az adott napon mért értékek. Ezeket a listákat adjuk hozzá a matrix változóban tárolt listához listaelemekként (a hozzáfűzéshez az append(item) funkciót használjuk majd). Ahhoz azonban, hogy a matrix mindig csak az adott évi adatokat tartalmazza, az évről évre haladó ciklus magjának elején "ki kell üríteni" a matrix változót, amit úgy tudunk megtenni, hogy egy új évre lépve mindig egy üres listaként definiáljuk a matrix változót: matrix = [].

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    matrix = []
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]
    rows = raw.splitlines()
    for row in rows:
        r = re.sub(' {7}', ' - ', row)
        r = re.sub('\s+', ' ').strip()
        if len(r) != 0:
            matrix.append(r.split(' '))

Beépítettem egy logikai vizsgálatot is (if len(r) != 0), amely ellenőrzi, hogy a sor milyen hosszú és csak akkor végzi el a darabolást és a matrix listához való hozzáfűzést, ha van a sorban valami, így ki tudjuk szűrni az üres sorokat.

Python - A matrix változó tartalma (listákat tartalmazó lista)

A matrix változóban tárolt beágyazott listából most már strukturált módon ki tudjuk nyerni az adatokat, hiszen a matrix listán belül minden elem (sor) indexe megfelel annak a napnak, amelyikre az érték vonatkozik. Az egyes elemeken (sorokon) belül pedig minden érték indexe megfelel annak a hónapnak, amelyikre az érték vonatkozik. Vagyis minden érték elérhető a hónap és a nap indexével: matrix[day][month]. A végeredményként elképzelt CSV fájl fejlécét ('year;month;day;str_value;num_value;flag\n') már beleírtuk a fájlunkba (amire a datafile változóval tudunk hivatkozni), most nincs más dolgunk, mint a matrix (majdnem) minden elemén végighaladva újabb sorokat írni a fájlba (datafile.write(string)). Egy beágyazott lista feldolgozásához beágyazott ciklus (nested loop) használata célszerű, vagyis a for ciklus magjában egy másik for ciklust hozunk létre. A külső ciklusban a hónapok sorszámain haladunk végig (for m in range(1,13)), a belső ciklusban a napok sorszámain (for d in range(1,32)). Mivel az első sorban a hónapok neve, az első oszlopban a napok száma található a 0 indexet mindkét ciklusból kihagyhatjuk, ezért indul a ciklus az 1-es indextől és nem a 0-tól.

Legbelül szükség lesz még egy logikai vizsgálatra, amely kiszűri a "nincs érték" jelentésű tartalmat ('-' karaktert írtunk egy korábbi lépésben ezekre a helyekre), hiszen felesleges nem létező napokhoz (pl. február 31.) nem létező értékeket felvennünk, azaz csak akkor rögzítsünk új sort a fájlba, ha a matrix-ban az adott helyen nem '-' szerepel.

Végezetül megfelelő sorrendben (év, hónap, nap, szöveges érték, számszerű érték, jelölő) össze kell fűznünk szövegeket, amelyeket a CSV fájl tartalmához fűzünk.

  • év: a year változóban tárolt érték
  • hónap: a külső ciklus változójának (m) aktuális értéke, amely azonban szám, így szöveggé kell alakítani: str(m)
  • nap: a belső ciklus változójának (d) aktuális értéke, amely azonban szám, így szöveggé kell alakítani: str(d)
  • szöveges érték: a matrix aktuális hónap és nap szerint indexelt értéke: matrix[d][m]
  • számszerű érték: a szöveges értékből egy reguláris kifejezést használó karakterhelyettesítéssel (a re.sub() funkciót használva) eltávolítjuk (gyakorlatilag üres szövegre cseréljük) a betűket: re.sub('[a-zA-Z]','', matrix[d][m])
  • jelölő: az előző kifejezést logikailag megfordítva a szöveges értékből a számjegyeket távolítjuk el, így csak a jelölő betűk maradnak vagy semmi: re.sub('[0-9]','', matrix[d][m])

A felsorolt kifejezések közé mindig egy pontosvesszőt (';') kell fűznünk, hogy új oszlopba kerüljenek, a jelölő értéke után pedig egy új sort kell nyitnunk, így hozzáfűzzük még a '\n' karaktersorozatot. 

import urllib
import re

urlbase = 'http://www.hydroinfo.hu/vituki/archivum/bp'
path = 'd:/adatterkep/projektek/dunaiSzigetek/insegesNapok/raw/'
datafile = open(path + 'database.csv','w')
datafile.write('year;month;day;str_value;num_value;flag\n')

for y in range(1876, 2006):
    matrix = []
    year = str(y)
    url = urlbase + year + '.htm'
    page = urllib.urlopen(url).read()
    raw = page[page.find('Nap'):page.find('Minimum')]
    rows = raw.splitlines()
    for row in rows:
        r = re.sub(' {7}', ' - ', row)
        r = re.sub('\s+', ' ').strip()
        if len(r) != 0:
            matrix.append(r.split(' '))
    for m in range(1, 13):
        for d in range(1, 32):
            if matrix[d][m] != '-':
                datafile.write(year + ';' + str(m) + ';' + str(d) + ';' + matrix[d][m] + ';' + re.sub('[a-zA-Z]','', matrix[d][m]) + ';' + re.sub('[0-9]','', matrix[d][m]) + '\n')
datafile.close()

Ezzel az adataink belekerültek a CSV fájlba, amely immár a merevlemezen is elérhető, s akár dolgozhatunk is vele pl. Excelben. Azonban a Python ekkor még "fogja" ezt a fájlt, így más szoftverek úgy érzékelik, hogy máshol is meg van nyitva (hiszen így is van), ezért csak olvasásra lehet megnyitni. Ha ezt szeretnénk elkerülni, akkor a fájlt további használat előtt zárjuk be a close() funkcióval.

Így néz ki az eredményül kapott CSV fájl Notepad++-ban megnyitva:

Notepad++ - A Python szkript által készített CSV fájl tartalma

... és az Excel is könnyedén megnyitja, a tagolókarakterek mentén jól tördeli:

Excel - A Python szkript által készített CSV fájl tartalma

Megvan tehát az adat, ami az elemzéshez kell, így hamarosan írunk az adatok elemzésének eredményeiről is!

A cikkben leírt módszer (sajnos) nem általános érvényű. Személyes megfigyeléseimen alapuló következtetésekre épít, pl. az egyes évek URL-jében mutatkozó névkonvenció, az adatokat körül záró 'Nap' és 'Minimum' szavak, stb. Ha ezek a feltételek nem teljesülnek (mert például a Hydroinfo oldalán megváltozik az adatok publikálásának szerkezete), a kód ebben a formában nem lesz érvényes. Az is elképzelhető, hogy egy másik vízmércénél az adatok kicsit másképpen vannak strukturálva és már ott sem lesz változtatás nélkül használható a szkript. Alkalmazás előtt tehát testreszabást és tesztelést igényel a kód, mindaddig, míg a Hydroinfo az adatok egységes, szabványos hozzáférése érdekében egy API-t nem biztosít. Ám míg ez megjelenik, addig is rengeteg erőforrást spórolhat egy-egy automatizáló szkript.

A Python szkript letölthető a GitHub oldalunkról!

Ha adatbázis-építés, adatelemzés, vizualizáció, térinformatika témában segítségre van szükséged, írj nekünk az Ez az e-mail-cím a szpemrobotok elleni védelem alatt áll. Megtekintéséhez engedélyeznie kell a JavaScript használatát. címre és megpróbálunk egy tippel, ötlettel segíteni!

Címkék:
© 2017 AdatTérKép