FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

Předpověď počasí v terminálu & Relační roury

vydáno: 14. 8. 2021 02:14, aktualizováno: 18. 8. 2021 19:58

Když nepočítám televizi a rozhlas, pro předpovědi počasí si většinou chodíme na nějakou webovou stránku, kterou si zobrazujeme v prohlížeči. Dnes si ukážeme, jak si stáhnout předpověď počasí ve formátu XML přes API poskytovatele a zobrazit si ji v textovém terminálu díky jednoduchému skriptu, který si napíšeme.

předpověď počasí v terminálu (Relační roury)

Zdroj dat

V tomto příkladu využijeme služeb Norwegian Meteorological Institute. Vstupem pro nás jsou XML data získaná z URL https://www.yr.no/place/Czech_Republic/Prague/Prague/forecast.xml, která vypadají nějak takto:

<?xml version="1.0" encoding="utf-8"?>
<weatherdata>
	<location>
		<name>Prague</name>
		<type>Capital</type>
		<country>Czech Republic</country>
		<timezone id="Europe/Prague" utcoffsetMinutes="120" />
		<location altitude="202" latitude="50.08804" longitude="14.42076" geobase="geonames" geobaseid="3067696" />
	</location>
	<credit>
		<link text="Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and the NRK" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/" />
	</credit>
	<links>
		<link id="xmlSource" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/forecast.xml" />
		<link id="xmlSourceHourByHour" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/forecast_hour_by_hour.xml" />
		<link id="overview" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/" />
		<link id="hourByHour" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/hour_by_hour" />
		<link id="longTermForecast" url="http://www.yr.no/place/Czech_Republic/Prague/Prague/long" />
	</links>
	<meta>
		<lastupdate>2021-08-08T10:11:00</lastupdate>
		<nextupdate>2021-08-08T16:11:00</nextupdate>
	</meta>
	<sun rise="2021-08-08T05:41:38" set="2021-08-08T20:33:25" />
	<forecast>
		<tabular>
			<time from="2021-08-08T14:00:00" to="2021-08-08T18:00:00" period="2">
				<symbol number="2" numberEx="2" name="Fair" var="02d" />
				<precipitation value="0" />
				<windDirection deg="246.9" code="WSW" name="West-southwest" />
				<windSpeed mps="5.2" name="Gentle breeze" />
				<temperature unit="celsius" value="23" />
				<pressure unit="hPa" value="1012.4" />
			</time>
			<time from="2021-08-08T18:00:00" to="2021-08-09T00:00:00" period="3">
				<symbol number="3" numberEx="3" name="Partly cloudy" var="03n" />
				<precipitation value="0" />
				<windDirection deg="265.4" code="W" name="West" />
				<windSpeed mps="4.7" name="Gentle breeze" />
				<temperature unit="celsius" value="22" />
				<pressure unit="hPa" value="1013.6" />
			</time>
			<!-- zkráceno… -->
		</tabular>
	</forecast>
	<weatherstation stno="0" sttype="" name="" distance="0" lat="0" lon="0" source="">
		<symbol number="0" name="" time="2019-11-14T00:00:00Z" />
		<temperature unit="" value="0" time="2019-11-14T00:00:00Z" />
		<windDirection deg="0" code="" name="" time="2019-11-14T00:00:00Z" />
		<windSpeed mps="0" name="" time="2019-11-14T00:00:00Z" />
	</weatherstation>
</weatherdata>

Element <time/> se opakuje víckrát, běžně dostaneme předpověď na víc než týden dopředu. Pro získání představy o struktuře dat nám ale stačí tato zkrácená verze. Struktura je celkem přehledná a srozumitelná, takže si z ní i bez nějakého dlouhého studia dokumentace můžeme vzít to, co potřebujeme:

předpověď počasí: forecast.xml v Koala XML editoru

Nejzajímavější je pro nás právě obsah elementů <time/>, kde najdeme předpovědi pro jednotlivé časové úseky.

Dodnes se můžeme občas setkat s takovým nešvarem: „parsování“ XML pomocí regulárních výrazů a nástrojů typu grep, cut, sed (případně perl nebo python). Regulární výrazy a klasické unixové nástroje jsou bezesporu skvělá a velice užitečná věc, ale mají svoje limity. A právě parsování většiny formátů strukturovaných dat je už za hranicí jejich možností. Skript či program postavený tímto způsobem je pak velice křehký a funguje víceméně jen náhodou – rozsype se ve chvíli, kdy se jen trochu změní formátování vstupních dat nebo třeba pořadí atributů. Použitelné je to jen pro ad-hoc zpracování, kde máme vstupní data i celý proces pod kontrolou a kdyby se něco začalo rozpadat, hned to opravíme. Zde se ale zaměříme na robustnější řešení.

Relační roury

Použijeme Relační roury, což je datový formát určený pro předávání relačních dat a sada nástrojů pro práci s nimi. Princip je stejný jako u klasických unixových rour – používáme stejný shell (např. Bash) a posíláme STDOUT jednoho procesu na STDOUT druhého procesu, ale vylepšení spočívá v tom, že data mají jasně danou strukturu (posloupnost relací – tabulek) a nástroje používané v jednotlivých fázích těmto strukturovaným datům rozumí.

Prakticky všechny programy či skripty jsou tvořené nějakým vstupem, nějakým výstupem a uvnitř je nějaká transformace dat. Tím vstupem může být např. souborový formát, síťový protokol, kterým komunikujeme s nějakou službou či externím systémem nebo jiné API. Zprostředkovaně to může být komunikace s hardwarem potažmo vstup od uživatele. Pro výstupy platí totéž (souborové formáty, protokoly, jiné API, zobrazení v GUI, výpis na terminál atd.). Transformace jsou např. různá filtrování na základě podmínek, součty a jiné agregace, konverzní funkce pracující s textem, čísly nebo jinými daty atd. Když odhlédneme od jiných aspektů (např. výkon nebo bezpečnost), můžeme si jeden program znázornit jako kvádr, který má tři rozměry:

Relační roury: tři dimenze programu

Množinu všech programů si pak můžeme představit jako velký kvádr složený z menších kvádrů, který má opět tři rozměry:

Relační roury: vstupy × výstupy × transformace – krychle

Jde o teoretické maximum – ne všechny kombinace dávají smysl – ale pro jednoduchost budeme předpokládat, že potřebujeme všechny (ve skutečnosti potřebujeme jen některé z těchto programů, ale to na věci nic nemění, navíc každý uživatel potřebuje trochu jinou podmnožinu). Chceme tedy mít (až) M × N × O programů.

Např. v oblasti grafických programů budou těmi vstupy a výstupy grafické formáty (JPEG, PNG, GIF, WebP, XCF atd.) a transformacemi různé barevné filtry, ořezy, efekty atd. V oblasti strukturovaných dat budou vstupem či výstupem souborové formáty (XML, CSV, ODS, JSON, YAML, ASN.1, INI, XHTML atd.), místní či vzdálené systémy (relační/SQL databáze, RDF, XML, LDAP a jiné nerelační databáze, souborový systém, SOAP a jiné webové služby, grafický systém X11, textový terminál atd.), transformace pak budou obecně různé projekce, restrikce, výběry části stromových struktur, JOINy, UNIONy atd.

Jedním z hlavních principů Relačních rour je oddělení těchto tří úloh programu: získání dat (vstup), transformace, prezentace dat (výstup) do samostatných znovupoužitelných modulů. Díky tomuto principu a společnému formátu dat není potřeba napsat M × N × O programů, ale jen M + N + O. Jinými slovy: neprogramujeme celý objem kvádru, ale pouze jeho tři hrany.

Relační roury: naprogramujeme M+N+O, získáme M×N×O

Rozdíl mezi těmito dvěma přístupy (což už nejde ve 2D/3D prostoru graficky znázornit) se dále násobí díky tomu, že transformace můžeme řetězit – v programu či skriptu nemusí být jen jedna. Máme tak M × N2 × O programů, M × N3 × O atd. podle toho, kolik transformací za sebe napojíme nebo jak je naparametrizujeme.

Dalším důležitým aspektem je oddělení dvou rolí:

  1. autoři jednotlivých modulů, kteří programují vnitřky těchto malých kvádrů
  2. autoři skriptů či programů, kteří skládají tyto kvádry dohromady.

Což dovoluje oběma rolím se soustředit na svoji jednu věc:

The authors of cat, grep, cut or tr programs do not have to know anything about cats and dogs and our business domain. They can focus on their tasks which are reading files, filtering by regular expressions, doing some substrings and text conversions. And they do it well without being distracted by any animals.

And we do not have to know anything about the low-level programming in the C language or compile anything. We just simply build a pipeline in a shell (e.g. GNU Bash) from existing programs and focus on our business logic. And we do it well without being distracted by any low-level issues.

Viz kapitola The great partsClassic pipeline example.

Dalším z principů Relačních rour je snaha nevynalézat znovu kolo – nevytváříme proto nový shell, ani programovací nebo dotazovací jazyk. Místo toho co nejvíc stavíme na existujících nástrojích, standardech a obvyklých postupech. Např. pro spojení dat z více zdrojů stačí použít příkaz cat nebo závorky a středník v shellu, stejně jako když bychom pracovali s prostým textem. Pro transformace pak lze použít dobře známé jazyky jako XPath, AWK, SQL, Scheme nebo regulární výrazy. Zároveň se řídíme pravidlem, že komplexita má být volitelná – uživatel proto není nucen instalovat moduly (a jejich závislosti), které nepoužívá (viz také Příčetné závislosti).

n.b. Relační roury jsou zatím stále ve vývoji a jejich rozhraní se (do vydání v1.0.0) jetště trochu může a bude měnit. Nicméně měly by být stabilní ve smyslu:

On the other hand, the already published tools (tagged as v0.x in v_0 branch) should work quite well (should compile, should run, should not segfault often, should not wipe your hard drive or kill your cat), so they might be useful for someone who likes our ideas and who is prepared to update own programs and scripts when the new version is ready.

Základní verze skriptu

Po teoretickém úvodu se teď vrátíme k našemu příkladu s předpovědí počasí. Celý skript i jeho výstup se vejdou na jednu obrazovku:

Skript pro stažení a zobrazení předpovědi počasí + jeho výstup v terminálu

Celé je to jedna unixová roura nebo také proudová funkce bez vedlejších efektů (žádné dočasné soubory atd. – pouze předáváme proud bajtů ze standardního výstupu jednoho programu na standardní vstup druhého).

Data s předpovědí stáhneme z internetu pomocí programu curl a předáme je vstupnímu filtru, který tento externí formát (XML) převede na relační data. Protože XML obsahuje stromovou strukturu (nedívejme se na XML jako na text plný ostrých závorek, ale jako na strom tvořený elementy, atributy, textovými a jinými uzly) použijeme jako vstupní filtr příkaz relpipe-in-xmltable.

Tento příkaz je inspirovaný funkcí XMLTable známou z relačních databází. Této funkci (či příkazu) předáme vstupní XML a XPath dotaz, který ukazuje na množinu uzlů, které budeme převádět na záznamy (řádky tabulky). Zároveň funkci předáme XPath dotazy, které budou (relativně) ukazovat na jednotlivé atributy (hodnoty sloupečků). Pokud jsme se s touto funkcí dosud nesetkali, může to vypadat na první pohled složitě. Ve skutečnosti ale pouze uvedeme cestu k elementům, které nás zajímají, oddělenou lomítky: /weatherdata/forecast/tabular/time a následně názvy jednotlivých atributů či elementů, ze kterých chceme udělat sloupečky naší tabulky: @from, @to, temperature/@value atd. Atributy od elementů rozlišíme tím, že mají na začátku zavináč.

#!/bin/bash

URL='https://www.yr.no/place/Czech_Republic/Prague/Prague/forecast.xml';

curl --silent "$URL" \
	| relpipe-in-xmltable \
		--relation "weather_forecast" \
			--records '/weatherdata/forecast/tabular/time' \
			--attribute 'od'                string  '@from' \
			--attribute 'do'                string  '@to' \
			--attribute 'teplota'           integer 'temperature/@value' \
			--attribute 'teplota_jednotka'  string  'temperature/@unit' \
			--attribute 'popis'             string  'symbol/@name' \
			--attribute 'vítr'              string  'windSpeed/@name' \
			--attribute 'teplota_graf'      string  '
				concat(
					substring("▓████▓████▓████▓████▓████▓████▓████▓████▓████▓████", 1, temperature/@value    ),
					substring("░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒",    temperature/@value + 1)
				)' \
	| relpipe-tr-sed \
		--relation '.+' \
			--attribute 'od|do' --value 'T'    --replacement ' ' \
			--attribute 'od|do' --value ':00$' --replacement '' \
	| relpipe-tr-cut --relation '.+' --attribute 'od|teplota|popis|vítr|teplota_graf' \
	| relpipe-tr-sql \
		--relation "dnes"      "SELECT * FROM weather_forecast WHERE od LIKE '$(date --iso-8601=d --date='+0 day')%'" \
		--relation "zítra"     "SELECT * FROM weather_forecast WHERE od LIKE '$(date --iso-8601=d --date='+1 day')%'" \
		--relation "pozítří"   "SELECT * FROM weather_forecast WHERE od LIKE '$(date --iso-8601=d --date='+2 day')%'" \
	| ( [[ -t 1 ]] && relpipe-out-tabular --write-types true --write-record-count false || cat )

Sloupečky se samozřejmě mohou jmenovat jinak, než uzly v XML, takže si jejich názvy můžeme třeba přeložit do češtiny.

Hodnota teplota_graf se ve vstupním XML nevyskytuje – tu si dopočítáme pomocí XPath funkce substring(), a složíme si tak ze dvou řetězců sloupcový graf, který půjde zobrazit i v textové konsoli. U větších programů je rozumné tyto operace dělat až na konci, protože se jedná o prezentační logiku (v nižších vrstvách bychom měli pracovat se surovými daty). Ale u takto krátkých skriptů, které se vejdou doslova na jednu obrazovku, je to jedno.

(pozorný čtenář si jistě všimne, že před zimou bude potřeba skript trochu upravit, aby zobrazoval i teploty pod nulou)

V následujícím kroku si pomocí relpipe-tr-sed (relační obdoba klasického příkazu sed) upravíme formát data, aby se nám lépe četl – odmažeme vteřiny a oddělovač „T“.

Pozor: v tomto článku se používá již nová syntaxe parametrů relpipe-tr-sed, která bude od verze v0.18. Pokud si program chcete vyzkoušet ještě před vydáním v0.18, je potřeba si dát do skriptu RELPIPE_VERSION="tip" (viz komentáře pod článkem).

Poté provedeme projekci příkazem relpipe-tr-cut (relační obdoba cut) a vybereme si jen atributy (sloupečky), které nás zajímají. Stejně jako u relpipe-tr-sed a dalších příkazů specifikujeme názvy relací a atributů formou regulárních výrazů. Jedním výrazem tak můžeme zachytit výčet atributů nebo např. všechny atributy začínající určitou předponou nebo vyhvovující složitějšímu vzoru.

Protože proud obsahuje jen jednu relaci, můžeme místo weather_forecast psát jednoduše .+ či .* (libovolný název).

Poslední transformací je relpipe-tr-sql, která nám umožňuje provádět SQL dotazy nad relačními daty ze strandardního vstupu a jejich výsledky posílat na standardní výstup. Výchozí chování je takové, že data se zpracovávají v paměti pomocí SQLite – nemusíme si tedy zakládat žádnou databázi a nevznikají ani žádné dočasné soubory. Pokud ale potřebujeme, můžeme použít libovolnou databázi připojitelnou přes ODBC (takže prakticky jakoukoli, např. PostgreSQL či MySQL nebo třeba pracovat nad SQLite souborem). Tuto transformaci využijeme k tomu, abychom z jedné relace (weather_forecast) získali tři s předpověďmi pro jednotlivé následující dny (dnes, zítra, pozítří).

Vzor data použitý pro filtrování pochází z důvěryhodného zdroje (příkaz date), takže si můžeme dovolit ho takto přilepit k dotazu. Jinak ale relpipe-tr-sql podporuje parametrizované dotazy, takže nás nerozhází ani data pocházející od záškodníka:

relpipe-tr-sql … --parameter "libovolná hodnota'; DROP TABLE user; --"

Na konci naší roury se nachází výstupní filtr relpipe-out-tabular, který formátuje data jako tabulku, která se zobrazuje v terminálu. V tomto případě vypínáme výpis počtu záznamů, protože u předpovědi počasí není zajímavý: --write-record-count false a mohli bychom skrýt i datové typy: --write-types false.

V tomto posledním kroku zároveň vidíme užitečný trik, který se může hodit spíš v jiných skriptech: zjistíme, zda standardní výstup (souborový popisovač č. 1) je terminálem, a pokud ano, zobrazíme data ve formě tabulky, a pokud ne (jde např. o soubor nebo data posíláme rourou dalšímu příkazu), pošleme na výstup strojově čitelná relační data. Díky tomuto triku můžeme psát obojetné skripty, které zobrazují lidsky čitelný výstup, ale lze je nasměrovat i do dalšího příkazu a místo terminálu si výstup třeba zobrazit v GUI okně (relpipe-out-gui) nebo zapsat do souboru v jiném formátu (např. relpipe-out-csv, relpipe-out-xhtml, relpipe-out-yaml nebo relpipe-out-ini). Pro časté použití si tento krok můžeme dát do shellové funkce nebo skriptu.

Vylepšená verze skriptu

Skript v předchozí kapitole je psán tak, aby byl na jednu stranu krátký a na druhou stranu sloužil i jako ukázka toho, jak lze kombinovat různé přístupy a jazyky v jedné rouře. Kromě příkazů relpipe-tr-cutrelpipe-tr-sed bychom mohli použít i třeba relpipe-tr-awk, relpipe-tr-scheme nebo relpipe-tr-xpath a část logiky napsat v jazycích AWK, Scheme nebo XPath. Můžeme tedy použít jazyk, který máme nejradši a který se pro danou úlohu nejvíc hodí.

V tuhle chvíli ale náš skript trochu zjednodušíme a omezíme se na jazyky XPath (v relpipe-in-xmltable) a SQL (v relpipe-tr-sql). Jedntlivé kroky tedy trochu přeskupíme a skript strukturujeme do několika funkcí, které nakonec zase zavoláme v jedné rouře. Navíc si přidáme mezipaměť (cache), abychom zdrojové XML nestahovali pokaždé znova, ale maximálně jednou za hodinu. Skript tak bude rychlejší a bude fungovat i částečně offline. Dále si ze zdrojových dat vytáhneme ještě jednu relaci – čas východu a západu slunce. A navrch přidáme překlady do češtiny.

#!/bin/bash

ziskej_xml() {
	URL='https://www.yr.no/place/Czech_Republic/Prague/Prague/forecast.xml';
	DIR="$HOME/.cache/yr.no/"
	CACHE="$DIR/forecast.$(date --iso-8601=h).xml"
	mkdir -p "$DIR";
	[[ -f "$CACHE" ]] && cat "$CACHE" || curl --silent "$URL" | tee "$CACHE"
}

preved_na_relace() {
	relpipe-in-xmltable \
		--relation "předpověď_počasí" \
			--records '/weatherdata/forecast/tabular/time' \
			--attribute 'od'                string  '@from' \
			--attribute 'do'                string  '@to' \
			--attribute 'teplota'           integer 'temperature/@value' \
			--attribute 'teplota_jednotka'  string  'temperature/@unit' \
			--attribute 'popis'             string  'symbol/@name' \
			--attribute 'vítr'              string  'windSpeed/@name' \
		--relation "slunce" \
			--records "/weatherdata/sun" \
			--attribute "východ"            string "@rise" \
			--attribute "západ"             string "@set"
}

překlad() {
	# pravděpodobně bychom překlady načítali z CSV souboru: cat překlad.csv | relpipe-in-csv …
	# případně můžeme překlady zadat ve formě CTE (WITH překlad AS …) v rámci SELECTu
	echo -n "en,cs
Clear sky,jasná obloha
Partly cloudy,částečně zataženo
Light breeze,lehký vánek
" | relpipe-in-csv --relation "překlad"
}

sestav_predpoved() {
	SQL="
		SELECT 
			substr(od, 0, 11) AS den,
			substr(od, 12, 5) AS od,
			teplota,
			coalesce(překlad_popis.cs, popis) AS popis,
			coalesce(překlad_vítr.cs,  vítr)  AS vítr,
			substr('▓████▓████▓████▓████▓████▓████▓████▓████▓████▓████', 1, teplota) ||
			substr('░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒',    teplota + 1) AS teplota_graf
		FROM předpověď_počasí AS předpověď
		LEFT JOIN překlad AS překlad_popis ON (překlad_popis.en = předpověď.popis)
		LEFT JOIN překlad AS překlad_vítr  ON (překlad_vítr.en  = předpověď.vítr)
		WHERE
			od LIKE ?
			AND teplota_jednotka = 'celsius'
	"

	(cat ; překlad) \
		| relpipe-tr-sql \
			--relation "dnes"      "$SQL" --parameter "$(date --iso-8601=d --date='+0 day')%" \
			--relation "zítra"     "$SQL" --parameter "$(date --iso-8601=d --date='+1 day')%" \
			--relation "pozítří"   "$SQL" --parameter "$(date --iso-8601=d --date='+2 day')%" \
			--copy     "slunce"
}

formatuj_vystup() {
	[[ -t 1 ]] && relpipe-out-tabular --write-types true --write-record-count false || cat
}

ziskej_xml | preved_na_relace | sestav_predpoved | formatuj_vystup

Lokalizace zde funguje jednoduše tak, že se přeloží, co přeložit lze, a zbytek zůstane v původním jazyce. V případě potřeby můžeme SQL dotaz snadno upravit, aby nepřeložené texty byly zvýrazněné nějakým prefixem nebo zvláštním znakem, abychom na první pohled viděli, co ještě zbývá přeložit.

předpověď počasí v terminálu: výstup vylepšeného skriptu

Na zimu si můžeme přidat variantu grafu, která zobrazuje i záporné hodnoty, -25 až +25:

CASE WHEN teplota >= 0 THEN
	       '▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░' ||
	substr('▓████▓████▓████▓████▓████', 1, teplota) ||
	substr('░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒',    teplota + 1)
ELSE
	substr('▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░', 1, 25 + teplota) ||
	substr('████▓████▓████▓████▓████▓',    25 + teplota + 1) ||
	       '░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒'
END AS teplota_graf

Podpora těchto jednoduchých sloupcových grafů se možná časem objeví v relpipe-out-tabular (podobně jako sql-dk --formatter barchartSQL-DK), takže si je uživatel nebude muset počítat ručně. Nicméně zatím tato funkce chybí a lze to řešit pomocí pár řádku kódu v SQL nebo XPath (případně AWK, Scheme…).

Skript rozdělený na funkce je trochu delší, ale díky lepšímu strukturování i flexibilnější – jednotlivé funkce lze použít i samostatně z jiných skriptů nebo snáze upravovat celou rouru, která se teď vejde na jeden řádek. Funkce preved_na_relace() nám vrací čistá data a prezentační logika se řeší až v sestav_predpoved(), nicméně i tato funkce má na výstupu strojově čitelná data, která lze dále zpracovávat. Až poslední krok formatuj_vystup() je převede do lidsky čitelné podoby. Díky pojmenování funkcí by mělo být i jasnější, co která část dělá.

Tabulky a grafy v GUI

Teď se konečně dostáváme k tomu, abychom využili trik s detekcí terminálu, který jsme si ukázali na začátku. Kromě zobrazení dat v textové konsoli můžeme náš skript nasměrovat i do jiného výstupního filtru, jako je relpipe-out-gui, který zobrazí data v Qt okně. Stejným způsobem bychom mohli data převést i do jiného formátu (např. XHTML, YAML, ASN.1/BER, INI, CSV, ODS atd.) a výsledek typicky poslat do souboru.

./předpověď-počasí.sh | relpipe-out-gui

Tento příkaz nám zobrazí stejná data, jako jsme měli v terminálu:

předpověď počasí v GUI: textové grafy

Kupodivu ani ten graf není úplně nepoužitelný, nicméně vykreslovat v GUI grafy pomocí textu je poněkud hloupé.

Příkaz relpipe-out-gui sám podporuje hezčí grafy. V současnosti zde funguje autodetekce a do grafu se kreslí hodnoty posledních číselných sloupců a pro popis se používá hodnota prvního sloupce. Proto si relaci upravíme příkazem relpipe-tr-cut:

./předpověď-počasí.sh \
	| relpipe-tr-cut \
		--relation "dnes|zítra|pozítří" \
		--attribute "od" \
		--attribute "popis" \
		--attribute "vítr" \
		--attribute "teplota" \
	| relpipe-out-gui --title "Předpověď počasí"

# n.b. jak relace tak atributy specifikujeme pomocí regulárního výrazu
# atributy jsme ale zadali pomocí více výrazů, což nám umožní ovlivnit jejich pořadí

a už se nám graf v GUI kreslí:

předpověď počasí v GUI: skutečné grafy

V některé z příštích verzí dostane relpipe-out-gui CLI parametry, pomocí kterých půjde pro jednotlivé relace nastavit typy grafů (nejen sloupcové) a atributy obsahující data (nebude záležet na pořadí).

Make a Makefile

Dosud jsme psali jen shellovské skripty a snažili jsme se, aby se veškeré zpracování odehrálo v rámci jedné roury a pokud možno nám nevznikaly žádné dočasné soubory. Nyní si ukážeme ještě jeden přístup – napíšeme si Makefile a spustíme ho příkazem make. Pokud nám vznik dočasných souborů nevadí nebo je dokonce žádoucí, je Makefile zajímavá alternativa k shellovským skriptům a často je i předčí. Příkaz make, který se běžně používá při sestavení (buildu) softwaru, za nás řeší závislosti mezi jednotlivými kroky (spustí je ve správném pořadí) a díky dočasným souborům spustí jen ty, které je potřeba spustit (když jejich data chybí nebo jsou zastaralá).

.PHONY: all clean vypiš-předpověď

all: vypiš-předpověď

clean:
	rm -f forecast.xml
	rm -f předpověď-data.rp
	rm -f předpověď-formátovaná.rp
	rm -f překlad.rp
	rm -f překlad.csv
	rm -f předpověď.sql

forecast.xml:
	wget -O $(@) "https://www.yr.no/place/Czech_Republic/Prague/Prague/forecast.xml"

předpověď-data.rp: forecast.xml
	cat $(<) \
	| relpipe-in-xmltable \
		--relation "předpověď_počasí" \
			--records '/weatherdata/forecast/tabular/time' \
			--attribute 'od'                string  '@from' \
			--attribute 'do'                string  '@to' \
			--attribute 'teplota'           integer 'temperature/@value' \
			--attribute 'teplota_jednotka'  string  'temperature/@unit' \
			--attribute 'popis'             string  'symbol/@name' \
			--attribute 'vítr'              string  'windSpeed/@name' \
		--relation "slunce" \
			--records "/weatherdata/sun" \
			--attribute "východ"            string "@rise" \
			--attribute "západ"             string "@set" \
	> $(@)

překlad.csv:
	echo "en,cs"                               > $(@)
	echo "Clear sky,jasná obloha"             >> $(@)
	echo "Partly cloudy,částečně zataženo"    >> $(@)
	echo "Light breeze,lehký vánek"           >> $(@)

překlad.rp: překlad.csv
	cat $(<) | relpipe-in-csv --relation "překlad" > $(@)

předpověď.sql:
	echo "SELECT"                                                                                                                        > $(@)
	echo "	substr(od, 0, 11) AS den,"                                                                                                  >> $(@)
	echo "	substr(od, 12, 5) AS od,"                                                                                                   >> $(@)
	echo "	teplota,"                                                                                                                   >> $(@)
	echo "	coalesce(překlad_popis.cs, popis) AS popis,"                                                                                >> $(@)
	echo "	coalesce(překlad_vítr.cs,  vítr)  AS vítr,"                                                                                 >> $(@)
	echo "	substr('▓████▓████▓████▓████▓████▓████▓████▓████▓████▓████', 1, teplota) ||"                                                >> $(@)
	echo "	substr('░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒░▒▒▒▒',    teplota + 1) AS teplota_graf -- TODO: teploty pod nulou"    >> $(@)
	echo "FROM předpověď_počasí AS předpověď"                                                                                           >> $(@)
	echo "LEFT JOIN překlad AS překlad_popis ON (překlad_popis.en = předpověď.popis)"                                                   >> $(@)
	echo "LEFT JOIN překlad AS překlad_vítr  ON (překlad_vítr.en  = předpověď.vítr)"                                                    >> $(@)
	echo "WHERE"                                                                                                                        >> $(@)
	echo "	od LIKE ?"                                                                                                                  >> $(@)
	echo "	AND teplota_jednotka = 'celsius'"                                                                                           >> $(@)

# n.b. Pozor na make clean, pokud budeme dělat ruční změny v CSV a SQL souborech.
# Běžně je nebudeme generovat přes make, ale verzovat v samostatných souborech.
# To jen zde v příkladu je to uvnitř Makefile, abychom měli vše v jednom souboru.

předpověď-formátovaná.rp: předpověď-data.rp překlad.rp předpověď.sql
	cat předpověď-data.rp překlad.rp \
	| relpipe-tr-sql \
		--relation "dnes"      "$$(cat předpověď.sql)" --parameter "$$(date --iso-8601=d --date='+0 day')%" \
		--relation "zítra"     "$$(cat předpověď.sql)" --parameter "$$(date --iso-8601=d --date='+1 day')%" \
		--relation "pozítří"   "$$(cat předpověď.sql)" --parameter "$$(date --iso-8601=d --date='+2 day')%" \
		--copy     "slunce" \
	> $(@)

vypiš-předpověď: předpověď-formátovaná.rp
	cat $(<) | relpipe-out-tabular --write-record-count false

zobraz-předpověď: předpověď-formátovaná.rp
	cat $(<) \
	| relpipe-tr-cut \
		--relation "dnes|zítra|pozítří" \
		--attribute "od" \
		--attribute "popis" \
		--attribute "vítr" \
		--attribute "teplota" \
	| relpipe-out-gui --title "Předpověď počasí"

Při prvním spuštění make (pokud jako parametr neuvedeme konkrétní krok) se provede krok all (protože je první), který závisí na vypiš-předpověď a ten závisí zase na předpověď-formátovaná.rp atd. čímž se postupně dostaneme až ke stažení forecast.xml. Na základě těchto závislostí make rozhodne, co a v jakém pořadí je potřeba vykonat. Při opakovaném volání make se už nic znovu nestahuje ani negeneruje a zavolá se pouze jeden relpipe-out-tabular v rámci vypiš-předpověď. Řádek začínající .PHONY: říká, které kroky se mají provádět, i kdyby stejnojmený soubor či adresář exitoval (takže např. touch vypiš-předpověď nám nic nenaruší).

Přepisem shellovských či jiných skriptů na Makefile často získáme přehlednější kód a zároveň i lepší výkon, protože se zbytečně neprovádí kroky, které nejsou nutné (jejich výsledek je znám od minule a je uložen v dočasném souboru). Nástroj make se tak hodí nejen pro sestavování softwaru (jeho primární účel), ale v kombinaci s Relačními rourami i pro různé osobní agendy, např. správu výdajů, PIM (Personal information management) a jinou automatizaci. Osobně je mi tento minimalistický přístup bližší než různé kancelářské balíky, tabulkové kalkulátory a jiné těžkopádné aplikace.

Závěr

Dnes jsme si na příkladu předpovědi počasí ukázali, jak načíst XML soubor, vytěžit z něj data, která potřebujeme a ta dále transformovat. Sám tenhle skript občas používám, nicméně primárně mělo jít spíš o inspiraci a ukázku, protože je to na jednu stranu dost jednoduchá úloha, ale na druhou stranu na ní lze ukázat různé zajímavé techniky a postupy. Co se týče Relačních rour, tak ještě prozradím, že brzy vyjde verze v0.18, která bude obsahovat řadu nových modulů – zejména sadu vstupních filtrů, které fungují stejně jako relpipe-in-xmltable, ale pracují nad jinými formáty: ASN.1/BER, CBOR, HTML, INI, MIME, YAML a JSON. Nad všemi těmito daty můžeme dělat XPath dotazy, vytěžit z nich relace a ty následně zpracovávat pomocí relpipe-tr-* příkazů. Můžeme např. přes XPath získat informace z X.509 certifikátu a následně je transformovat pomocí SQL, AWKu nebo Scheme. Filtr pro čtení INI souborů zase podporuje i všemožné další formáty konfiguračních souborů včetně Java .properties, takže i z nich můžeme snadno číst a zapojit je do našich relačních rour, skriptů a makefilů. Dále přibyl vstupní filtr pro čtení čárových a QR kódů, vstup z X11 serveru (výpis vstupních zařízení, oken a zachtávání událostí z klávesnice a myši), XPath transformace a výstup do YAMLu a X11 (emulace stisků kláves a pohybů myši). Také tu máme podporu datových typů v CSV a jinde. Bude to celkem velké vydání obsahující změny za několik dlouhých měsíců.

Odkazy a zdroje:

  • Classic pipeline example – silné a slabé stránky klasických „unixových“ rour a příkazů
  • Relational pipes – Relační roury: formát pro popis relačních dat a sada nástrojů pro práci s nimi
  • XMLTable – SQL funkce sloužící k převodu XML na relace (tabulky) pomocí XPath dotazů
  • SQL-DK – univerzální klient k relačním databázím pro příkazovou řádku

Témata: [GNU/Linux] [softwarové inženýrství] [softwarová architektura] [SQL] [XML] [Bash]

Komentáře čtenářů


MM, 18. 8. 2021 10:38, Základní verze skriptu [odpovědět]

Ahoj,
nějak mi nefunguje základní verze skriptu na uvolněné verzi 0.17.1.
píše
Caught CLI exception: Usage: relpipe-tr-sed relationNameRegExp attributeNameRegExp searchRegExp replacement


Franta, 18. 8. 2021 20:00, nejnovější verze [odpovědět]

Díky za upozornění, přidal jsem tam poznámku – při psaní článku jsem použil nejnovější verzi všech nástrojů a u relpipe-tr-sed došlo ke změně syntaxe (sjednocení s ostatními nástroji). Pro kompilaci úplně nejnovější verze stačí dát do skriptu RELPIPE_VERSION="tip". Případně jsem nahrál na web upravený skript: release-v0.18.sh, který obsahuje i ty nové moduly, které budou až v té v0.18.

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře