Skip to content

Groovy – Pobieranie kursów walut z NBP wykorzystując GPath

Wstęp

Wpis ten przedstawia stworzenie małego programu, pobierającego kursy walut o określonym typie z podanego przez użytkownika przedziału czasu z plików .xml znajdujących się na serwerach NBP. Całość jest bardzo prosta, ale posiada duży potencjał do rozbudowania w przyszłości. Przykładowy fragment źródłowego pliku .xml prezentuje się następująco:

<ArrayOfExchangeRatesTable xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExchangeRatesTable>
<Table>B</Table>
<No>1/B/NBP/2012</No>
<EffectiveDate>2012-01-04</EffectiveDate>
<Rates>
<Rate>
<Country>Afganistan</Country>
<Currency>afgani</Currency>
<Code>AFN</Code>
<Mid>0.079959</Mid>
</Rate>
<Rate>
<Country>Albania</Country>
<Currency>lek</Currency>
<Code>ALL</Code>
<Mid>0.032239</Mid>
</Rate>
<Rate>
<Country>Algieria</Country>
<Currency>dinar</Currency>
<Code>DZD</Code>
<Mid>0.045397</Mid>
</Rate>
<Rate>
<Country>Angola</Country>
<Currency>kwanza</Currency>
<Code>AOA</Code>
<Mid>0.0363</Mid>
</Rate>

[...]

Całość można zobaczyć tutaj: http://api.nbp.pl/api/exchangerates/tables/b/2012-01-01/2012-03-01/?format=xml

Struktura projektu

Całość składa się z dwóch klas reprezentujących wewnętrzną strukturę pliku .xml ze strony NBP: ExchangeRatesTable.groovy , Rate.groovy , 3 klas odpowiedzialnych za logikę związaną ze stworzeniem odpowiedniego adresu URL, a także pobrania danych z internetu. Całością steruje oczywiście kod znajdujący w klasie Main.groovy .

No to zaczynamy..

W pierwszej kolejności stwórzmy sobie klasy ExchangeRatesTable  oraz Rate :

package pl.lukaszpusz.currency_utils
/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class ExchangeRatesTable {
    private String table = null
    private String no = null
    private String effectiveDate = null
    private List<Rate> rates = new ArrayList<>()

    void setTable(String table) {
        this.table = table
    }

    void setNo(String no) {
        this.no = no
    }

    void setEffectiveDate(String effectiveDate) {
        this.effectiveDate = effectiveDate
    }

    List<Rate> getRates() {
        return rates
    }

    @Override
    String toString() {
        return "ExchangeRatesTable{" +
                "table='" + table + '\'' +
                ", no='" + no + '\'' +
                ", effectiveDate='" + effectiveDate + '\'' +
                '}'
    }
}

Jak widzimy klasa ta jest bardzo prosta. Składa się tylko z domyślnego konstruktora, paru mutatorów i akcesorów, a także przeciążonej metody toString() . Poniższa klasa Rate jest bardzo podobna:

package pl.lukaszpusz.currency_utils

/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class Rate {
    private String country = null
    private String currency = null
    private String code = null
    private String mid = null
    private String bid = null
    private String ask = null

    void setCountry(String country) {
        this.country = country
    }

    void setCurrency(String currency) {
        this.currency = currency
    }

    void setCode(String code) {
        this.code = code
    }

    void setMid(String mid) {
        this.mid = mid
    }

    void setBid(String bid) {
        this.bid = bid
    }

    void setAsk(String ask) {
        this.ask = ask
    }

    @Override
    String toString() {
        StringBuilder builder = new StringBuilder()

        if (country) builder.append(country).append("\n")
        if (currency) builder.append(currency).append("\n")
        if (code) builder.append(code).append("\n")
        if (mid) builder.append(mid).append("\n")
        if (bid) builder.append(bid).append("\n")
        if (ask) builder.append(bid).append("\n")

        return builder.toString()
    }
}

Sądzę, że powyższe klasy są na tyle trywialne, że nie ma sensu ich tłumaczyć. Następnym krokiem będzie prosty konsolowy interfejs odpowiedzialny za komunikację programu z użytkownikiem:

package pl.lukaszpusz.downloader_utils

/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class UrlStringCreator {
    static String createUrlString() {
        def sc = new Scanner(System.in)
        print "Podaj datę od (YYYY-MM-DD): "
        def dateFromString = sc.nextLine()

        print "Podaj datę do (YYYY-MM-DD): "
        def dateToString = sc.nextLine()

        println "Podaj rodzaj tabeli:"
        println "a - tabela kursów średnich walut obcych"
        println "b - tabela kursów średnich walut niewymienialnych"
        println "c - tabela kursów kupna i sprzedaży"
        print "Wybieram: "
        def tableType = sc.nextLine()
        println()

        def builder = new StringBuilder()
        builder.append("http://api.nbp.pl/api/exchangerates/tables/")
        builder.append(tableType)
        builder.append("/")
        builder.append(dateFromString)
        builder.append("/")
        builder.append(dateToString)
        builder.append("/?format=xml")

        return builder.toString()
    }
}

Cała klasa zawiera jedną statyczną metodę createUrlString()  odpowiedzialną za stworzenie odpowiedniego adresu na podstawie danych podanych przez użytkownika. Jednym z wariantów URL oferowanym w API NBP jest łańcuch następującej postaci:

http://api.nbp.pl/api/exchangerates/tables/{table}/{startDate}/{endDate}/

gdzie {table}  może przyjmować następujące wartości:

  • Tabela A kursów średnich walut obcych
  • Tabela B kursów średnich walut obcych
  • Tabela C kursów kupna i sprzedaży walut obcych

Natomiast {startDate} oraz {endDate} może przyjmować daty postaci YYYY-MM-DD. Od użytkownika poprzez obiekt klasy Scanner  zostaną pobrane właśnie te dane. Następnie zostaną połączone z resztą adresu URL sugerowanego przez API NBP przy użyciu obiektu klasy StringBuilder . Metoda zwraca gotowy adres.

Gdy mamy już nasz adres to wypadałoby zająć się logiką umożliwiającą pobieranie danych, które się pod nim znajdują. Stwórzmy w tym celu następującą klasę:

package pl.lukaszpusz.downloader_utils

/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class XmlDownloader {
    static def getXmlFileAsString(String urlString) {
        def builder = new StringBuilder()
        def inputStream = null
        def reader
        def line
        def url

        try {
            url = new URL(urlString)
            inputStream = url.openStream()
            reader = new BufferedReader(new InputStreamReader(inputStream))

            while ((line = reader.readLine()) != null) {
                builder.append(line)
            }
        } catch (MalformedURLException mue) {
            mue.printStackTrace()
        } catch (IOException ioe) {
            ioe.printStackTrace()
        } finally {
            try {
                if (inputStream != null) inputStream.close()
            } catch (IOException ioe) {
                ioe.printStackTrace()
            }
        }

        return builder.toString()
    }
}

Jej funkcjonalność również jest raczej trywialna i oczywista. Składa się z jednej statycznej metody przyjmującej jako parametr stworzony przez nas wcześniej adres. Odczytuje po kolei każdy wiersz podanego pliku i dodaje go to obiektu klasy StringBuilder . Jako wynik zwraca cały plik .xml w postaci pojedynczego łańcucha znaków.

Skoro mamy już nasz plik .xml to możemy zająć się tworzeniem logiki, która wyciągnie z niego interesujące dane, a następnie w jakiś sposób je przetworzy. Stwórzmy więc następującą klasę:

package pl.lukaszpusz.downloader_utils

import pl.lukaszpusz.currency_utils.ExchangeRatesTable
import pl.lukaszpusz.currency_utils.Rate

/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class CurrencyXmlParser {
    static def parseXmlAndAddToList(String xml, List<ExchangeRatesTable> exchangeRatesTables) {
        def document = new XmlParser().parseText(xml)
        ExchangeRatesTable table = null
        Rate rate = null

        document.ExchangeRatesTable.each {
            bk ->
                table = new ExchangeRatesTable()

                table.setTable("${bk.Table.text()}")
                table.setNo("${bk.No.text()}")
                table.setEffectiveDate("${bk.EffectiveDate.text()}")

                bk.Rates.Rate.each {
                    bt ->
                        rate = new Rate()

                        rate.setCountry("${bt.Country.text()}")
                        rate.setCurrency("${bt.Currency.text()}")
                        rate.setCode("${bt.Code.text()}")
                        rate.setMid("${bt.Mid.text()}")
                        rate.setAsk("${bt.Ask.text()}")
                        rate.setBid("${bt.Bid.text()}")

                        table.getRates().add(rate)
                }
                exchangeRatesTables.add(table)
        }
    }
}

Składa się ona z jednej metody statycznej, która jako parametry przyjmuje nasz plik .xml w postaci łańcucha znaków, a także listy, do której będziemy dodawać stworzone przez nas obiekty. Jak widać klasa jest bardzo prosta. Jedyną ciekawostką mogą być tzw. Groovy closure, czyli te bloki kodu znajdujące się pomiędzy klamrami. Do wyciągnięcia poszczególnych danych używam zintegrowanego w Groovy’m języka wyrażeń ścieżek GPath (link do dokumentacji tradycyjnie na dole). Cała sztuczka polega na tym, że odwołujemy się do poszczególnych pól w pliku .xml bezpośrednio z poziomu kodu!

W tym momencie zostało nam tylko stworzenie prostej klasy Main:

package pl.lukaszpusz

import pl.lukaszpusz.currency_utils.ExchangeRatesTable
import pl.lukaszpusz.downloader_utils.CurrencyXmlParser
import pl.lukaszpusz.downloader_utils.UrlStringCreator
import pl.lukaszpusz.downloader_utils.XmlDownloader

/**
 * Created by Lukasz Pusz on 09.07.2017.
 * www.lukaszpusz.pl
 */
class Main {
    static void main(String[] args) {
        String urlString = UrlStringCreator.createUrlString()
        String xml = XmlDownloader.getXmlFileAsString(urlString)

        List<ExchangeRatesTable> tables = new ArrayList<>()
        CurrencyXmlParser.parseXmlAndAddToList(xml, tables)

        for (ex in tables) {
            println ex

            for (rx in ex.getRates()) {
                println rx
            }
        }
    }
}

W przykładzie ograniczymy się po prostu do wyświetlenia pobranych danych w konsoli. Przetestujmy więc teraz działanie naszego programu:

Wnioski

Jak widać program jest bardzo prosty i ma parę poważnych wad:

  1. Brak kompleksowej obsługi błędów
  2. Mało elastyczny schemat działania (można dołożyć więcej funkcjonalności niż tylko pobieranie kursów spomiędzy dwóch dat)
  3. Tak naprawdę z danymi nic po pobraniu się nie dzieje oprócz ich wyświetlenia

O jakie dodatkowe elementy można by przykładowo wzbogacić funkcjonalność:

  1. Patrz drugą wadę
  2. Zapisywanie pobranych danych do np. bazy danych, arkuszy kalkulacyjnych czy plików tekstowych w przystępnej dla użytkownika postaci
  3. Analiza pobranych danych :
    1. Tworzenie różnych wykresów
    2. Statystyczne tworzenie prognoz
  4. Naniesienie danych na mapę świata

Zachęcam do zabawy z kodem i dalszej jego rozbudowy.


Źródła:

Całość w najbliższej przyszłości pojawi się oczywiście na moim GitHubie.


EDYCJA

Jeżeli chcemy tylko coś na szybko wyciągnąć z xml’a i nie potrzebujemy pobierania całego kodu strony, to można postąpić przykładowo tak:

        def document = new XmlParser().parse("http://www.nbp.pl/kursy/xml/LastA.xml")

        println document.numer_tabeli.text()
        println document.data_publikacji.text()
        document.pozycja.each {
            pos ->
                print pos.nazwa_waluty.text() + " "
                print pos.przelicznik.text() + " "
                print pos.kod_waluty.text() + " "
                println pos.kurs_sredni.text()
        }

 

Facebooktwitterredditlinkedinmail
Published inJava