× Aktuálně z oboru

Programátoři po celém světě dnes slaví Den programátorů [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]
Celá zprávička [ clanek/2018091300-programatori-po-celem-svete-dnes-slavi-den-programatoru/ ]

Expat – parsujeme XML

[ http://programujte.com/profil/6101-zdenko-vrabel/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/7799-martin-valent/ ]Google [ ?rel=author ]       6. 4. 2007       17 686×

V dnešnej dobe asi nikomu netreba predstavovať, čo je XML. Skôr či neskôr dôjde čas, kedy programátora prestane baviť písať cvičné programy podľa kníh a bude chcieť vytvoriť niečo užitočné. Vtedy sa nevyhne XML-kam, či už bude mať program konfiguračný súbor v tomto formáte, alebo bude priamo spracovávať XML súbory. Technológie ako .NET a JAVA už s týmto rátali a majú implementovanú sadu objektov na parsovanie. Čo však v prípade C/C++? Tu môžeme siahnuť po rôznych knižniciach, ktoré nám parsovanie umožňujú. Ja som sa rozhodol, že vám v tomto článku predstavím konkrétne knižnicu EXPAT, ktorá patrí k najrýchlejším a, čo sa tyká úspory pamäte, k najúspornejším.

Knižnica EXPAT je C knižnica a nieje ju problém implementovať ako do C kódu, tak do objektového C++ kódu. Knižnica je tiež pomerne jednoduchá, nieje závislá na ďalších knižniciach a nemal by s ňou byt problém. Prvé dve kapitoly sú určené skôr menej zdatnejším, ktorí ešte len programovanie v C objavujú. Skúsenejším, ktorým je build proces známy, môžu tieto kapitoly preskočiť.

Čo najskôr?

Ako prvé si je potrebne túto knižnicu zaobstarať. Knižnica existuje pre Windows, Linux a iné OS. Je napísaná tak, aby bola ľahko prenosná na akúkoľvek platformu, na ktorej je možné rozbehať C kompilátor. Nebudem sa rozpisovať o všetkých možnostiach, ako si túto knižnicu zaobstarať. Užívatelia UBUNTU môžu použiť APT, Windows užívatelia si stiahnu inštalačný súbor a spustia. Všetko potrebne nájdeme na sourceforge.net/projects/expat/ [ http://sourceforge.net/projects/expat/ ] . Ja osobne dávam prednosť kompilácii zdrojových kódov na OS Linux. K tomu je potrebne stiahnuť zdrojový kód. Ten nakopírujeme do domovského adresára a rozbalíme ho:


sn3d@sn3d-laptop:~$ gunzip expat-2.0.0.tar.gz
sn3d@sn3d-laptop:~$ tar -xvvf expat-2.0.0.tar

Po tom ako sme súbor rozbalili, prejdeme ku kompilácii. Najprv spustime konfiguračný script. Konfiguračný script ma množstvo nastavení. Ich kompletný popis je možné získať zadaním príkazu ./configure –help. Ja som nepotreboval nič dodatočné meniť a tak som použil nasledujúci príkaz:


sn3d@sn3d-laptop:~/expat-2.0.0$ ./configure

Po úspešnom zbehnutí configure skriptu, prejdeme ku samotnej kompilácii a inštalácii. K tomu nám poslúži dvojica príkazov:


sn3d@sn3d-laptop:~/expat-2.0.0$ make 
sn3d@sn3d-laptop:~/expat-2.0.0$ make install

Kompilácia prebehne bezproblémovo. Nakoľko knižnica nieje príliš previazaná na ďalšie knižnice, jej kompilácia by nemala byt problémom. Teraz už mame EXPAT v našom systéme.

Prípravy

Nato, aby sme v našom programe mohli používať funkcie knižnice EXPAT, je potrebne túto knižnicu prilinkovať. Väčšina IDE programov, ako Visual C++, Dev-C++ alebo KDEvelop si proces kompilovania a linkovania riadia po svojom a prilinkovanie je záležitosť troch klikov. Ja som človek, ktorý potrebuje mať všetko pod kontrolou, preto svoje programy kompilujem pomocou Makefile. Vytvoríme si teda adresár, v ktorom budeme vytvárať náš experimentálny program example. V ňom si vytvoríme Makefile, ktorého obsah je nasledujúci.

Makefile:

CC=g++
LDFLAGS= -lexpat
BIN=out

all: $(BIN)

$(BIN): main.cc
   $(CC) $(LDFLAGS) -o $(BIN) main.cc

Všimnime si, že LDFLAGS obsahuje -lexpat. Týmto informujeme kompilátor, aby pri zostavovaní výslednej binárky prilinkoval EXPAT knižnicu. Ďalej si v tom istom adresári vytvoríme main.cc, ktorý zatiaľ nič nebude vykonávať.

main.cc:


#include <iostream>
#include <expat.h>

using namespace std;

int main()
{
   return 0;
}

Aby v našom programe bolo možné používať funkcie z knižnice EXPAT, je potrebne pridať hlavičkový súbor tejto knižnice expat.h. V tomto momente, by sme mali mať všetko pripravené na to, aby sme sa mohli pustiť do parsovania.

A čo teraz?

Bolo by vhodne si ukázať, ako rozparsovať XML súbor. Prejdeme rovno k jednoduchému praktickému príkladu, na ktorom si vysvetlime, ktorá funkcia čo vykonáva a ako prebieha proces parsovania. EXPAT obsahuje omnoho viac funkcii, článok ma ukázať, ako EXPAT jednoducho použiť, nemá byť prekladom referenčnej dokumentácie. Vytvoríme si teda nasledovný XML súbor, ktorý budeme parsovať:

input.xml:

<kniznica>

   <sekcia meno="Programovani">
      <kniha nazov="UML srozumitelne">
         <autor>Hana Kanisová</autor>
         <autor>Miroslav Müller</autor>
         <pocet_stran>176</pocet_stran>
         <cena>269 Sk</cena>
      <kniha>

      <kniha nazov="Programovací jazyk C">
         <autor>Brian W. Kernighan</autor>
         <autor>Dennis M. Ritchie</autor>
         <pocet_stran>288</pocet_stran>
         <cena>402 Sk</cena>
      </kniha>
   </sekcia>

   <sekcia meno="Komunikace a site">
      <kniha nazov="Počítačové sítě">
         <autor>Wendell Odom</autor>
         <pocet_stran>384</pocet_stran>
         <cena>537 Sk</cena>
      </kniha>
   </sekcia>

</kniznica>

Napíšeme si náš prvý jednoduchý program, ktorý bude parsovať tento súbor. Do main.cc vložíme nasledujúci kód. Kód je rozsiahlejší, ale nemajte strach, postupne si ho prejdeme.

main.cc:


#include <iostream>
#include <expat.h>

#define BUFFER_SIZE 30

using namespace std;

void startElement(void *userData, const char *name, const char **attrs)
{
   int* level = (int*) userData;
   (*level)++;

   cout << "start element:" << name << endl;

   for(int i = 0; attrs[i] != NULL; i+=2) {
      cout << "Attribute's name:" << attrs[i] << endl;
      cout << "Attribute's value:" << attrs[i+1] << endl;
   }

}

void characterData(void *userData, const XML_Char *buffer, int len)
{
   int* level = (int*)userData;
   string content(buffer, len);

   if ((*level) == 4) {
      cout << "element's data:" << content << endl;
   }
}

void endElement(void *userData, const char *name)
{
   int* level = (int*) userData;
   (*level)--;

   cout << "end element:" << name << endl;
}

int main()
{
   //deklaracia potrebnych premennych
   FILE*         file;
   XML_Parser    parser;
   char          buffer[BUFFER_SIZE];
   int           isFinal;
   int           level;
   size_t        len;
   int           result;

   //inicializacia parsera
   parser = XML_ParserCreate(NULL);
   XML_SetUserData (parser, &level);
   XML_SetElementHandler(parser, startElement, endElement);
   XML_SetCharacterDataHandler(parser, characterData);


   //otvorenie input.xml suboru
   file = fopen( "input.xml", "r");
   if (!file) {
      cout << "ERROR:nepodarilo sa otvorit subor, pravdepodobne neexistuje" << endl;
      return 1;
   }

   //parsujeme data v subore
   do {
      len = fread(buffer, 1, sizeof(buffer), file);
      isFinal = len - sizeof(buffer);
      result = XML_Parse(parser, buffer, len, isFinal);
      if (!result) {
         cout << "ERROR PARSING:" << XML_ErrorString(XML_GetErrorCode(parser)) << " at line:" << XML_GetCurrentLineNumber(parser) << endl;
      }
   } while(!isFinal);

   //upratanie
   XML_ParserFree(parser);
   fclose(file);

   return 0;
}

Začneme teda postupne od funkcie main().Ak by som mal napišať v krátkosti, čo main() vykonáva, napísal by som: main otvorí súbor, postupne ho načíta do bufferu a následne buffer postupne parsuje. Poďme však poporiadku. Na prvom mieste sa nachádza deklarácia premenných, ktoré použijeme v nasej funkcii. K tomu nieje čo dodať. Ďalšia časť je však zaujímavejšia. Jedna sa o inicializáciu EXPAT parsera:


   //inicializacia parsera
   parser = XML_ParserCreate(NULL);
   XML_SetUserData (parser, &level);
   XML_SetElementHandler(parser, startElement, endElement);
   XML_SetCharacterDataHandler(parser, characterData);

Najprv sa vytvorí handler parsera. Na to nám slúži funkcia XML_ParserCreate(). Na konci programu je potrebne tento handler uvolniť volaním XML_ParserFree(). Nasleduje funkcia XML_SetUserData(). Všimnite si, že pri funkciách, ako je startElement(), endElement() je prvý argument ukazovateľ userData. Pomocou funkcie XML_SetUserData() nastavíme, čo sa ma predávať do argumentu userData. Ďalšia funkcia je XML_SetElementHandler(). Tejto funkcii predávame mena funkcii, ktoré sa majú volať, keď sa narazí na začiatok a koniec elementu v XML. Poslednou funkciou, nastavíme aká funkcia sa ma volať pri spracovávaní dát.

Ešte netušíte na čo funkcie startElement(), endElement() a characterData() sú? EXPAT funguje tak, že prechádza text XML, v prípade, že narazí na začiatok elementu (napríklad <kniha>), zavolá sa funkcia startElement(), ktorá bude mať v name meno nového elementu, argument attrs bude ukazovať na pole, v ktorom sa nachádzajú mena a hodnoty atribútov tohto elementu (v našom prípade názov).Následne sa zavolá characterData() funkcia. V jej argumente buffer sa nachádza ukazovateľ na pole dát, ktoré je medzi <kniha> a </kniha>.Keď EXPAT narazí na koniec elementu (v našom prípade </kniha>), v tom momente je volaná funkcia endElement() a jej argument name obsahuje meno elementu, ktorý sa konči. Toto je vlastne základný princíp, na ktorom EXPAT funguje. Takýto štýl parsovania sa zvykne označovať ako SAX parser. Tento štýl parsovania je extrémne rýchli, ma nízke nároky na pamäť a je schopný parsovať aj dáta v prúdoch.

Parser inicializovaný a môžeme pristúpiť k ďalšiemu kroku. Tým je otvorenie input.xml súboru, z ktorého budeme čítať dáta. K tejto časti nieje čo dodať, pravdepodobne každý z vás by už mal vedieť, ako otvoriť a čítať dáta zo súboru. Načítanie súboru je v tomto prípade o trošku zaujímavejšie.


   //parsujeme data v subore
   do {
      len = fread(buffer, 1, sizeof(buffer), file);
      isFinal = len - sizeof(buffer);
      result = XML_Parse(parser, buffer, len, isFinal);
      if (!result) {
         cout << "ERROR PARSING:" << XML_ErrorString(XML_GetErrorCode(parser)) << " at line:" << XML_GetCurrentLineNumber(parser) << endl;
      }
   } while(!isFinal);

Ide o cyklus, ktorý bude postupne čítať dáta z input.xml súboru do bufferu. Následne kontrolujeme počet načítaných dát. Ak je tento počet menší ako celková veľkosť bufferu, jedna sa o&em>. Táto funkcia postupne parsuje dáta, vykonáva zmienený proces volania funkcii startElement(), endElement() a characterData(). Za touto funkciou nasleduje vyhodnotenie jej priebehu.

Zoberme si situáciu, že nastane chyba pri parsovaní, z dôvodu zlého XML. Chyba je v tom, že jeden z elementov nieje uzavretý. Aby sme sa netrápili z dlhým hľadaním preklepu, je možné chybu rýchlo detekovať nasledovným spôsobom. Funkcia XML_GetErrorCode() vráti číslo chyby. Následne funkcia XML_ErrorString() toto číslo prevedie na zmysluplný text a posledná informácia je číslo riadku v XML, kde chyba nastala. To zistime volaním funkcie XML_GetCurrentLineNumber().

Keď dôjde k úspešnému načítaniu a sparsovaniu všetkých dát, program ukončí cyklus. Potom sa vykoná uvolnenie parser handlera volaním XML_ParserFree(), uzavretie súboru a ukončenie programu.

Do funkcii startElement(), endElement() a characterData() som umiestnil kód, ktorý nás bude informovať o priebehu parsovania.


void startElement(void *userData, const char *name, const char **attrs)
{
   int* level = (int*) userData;
   (*level)++;

   cout << "start element:" << name << endl;

   for(int i = 0; attrs[i] != NULL; i+=2) {
      cout << "Attribute's name:" << attrs[i] << endl;
      cout << "Attribute's value:" << attrs[i+1] << endl;
   }

}

Asi najdôležitejší je začiatok. Ako som spomínal, na začiatku sa vola stratElement(). V argumente userData sa nachádza ukazovateľ na naše číslo, v ktorom budeme udržiavať informáciu o hĺbke vnorenia elementu. Princíp je taký, že každý nový začatý element, toto číslo zvýši o 1 a každý koniec elementu, zase, číslo zníži o 1.

Zaujímavý je argument attrs. Jedna sa o pole ukazovateľov na char*. Atribúty sú v ňom radene tak, že vždy existuje pár, ktorý je tvorený z mena atribútu a jeho hodnoty. Ako posledná je hodnota NULL. V pamäti potom prípad <elm attr1=”val1” attr2=”val2”> vyzerá nasledovne:


attrs[0] = “attr1”
attrs[1] = “val1”
attrs[2] = “attr2”
attrs[3] = “val2”
attrs[4] = NULL

Takže, už vieme, ako pristupovať k atribútom a for cyklus v startElement() funkcii by už mal byt jasný. For prechádza všetky dvojice a vypisuje ich na výstup, až kým nenarazí na NULL. Pohnime sa teda ďalej.


void characterData(void *userData, const XML_Char *buffer, int len)
{
   int* level = (int*)userData;
   string content(buffer, len);

   if ((*level) == 4) {
      cout << "element's data:" << content << endl;
   }
}

Funkcia characterData() je volaná po startElement(). V tejto funkcii sa nenachádza veľa kódu. Totižto, ak element je na úrovni 4, dôjde k vypísaniu jeho obsahu na výstup. Tato podmienka zabraní tomu, aby neboli na výstup zapísané nadradené elementy, ktoré neobsahujú žiadne konkrétne textové dáta a ani ďalšie vnorené elementy. Na štvrtéj úrovni sa nachádzajú elementy ako <cena> <autor> <pocet_stran>, ktorých obsahom je konkrétna informácia. Po spracovaní obsahu elementu je volaná funkcia endElement().


void endElement(void *userData, const char *name)
{
   int* level = (int*) userData;
   (*level)--;

   cout << "end element:" << name << endl;
}

Táto funkcia zníži úroveň o 1 a vypíše meno elementu, ktorý sa uzatvára. Kód by nemal byt problém skompilovať. Po spustení by sme mali dostať rozsiahlejší výpis priebehu parsovania, na ktorom zreteľne môžeme vidieť, ako prebieha proces parsovania. Tento príklad nepatri pravé k tým praktickejším. Dôvod tohto príkladu je skôr ukázať, ako presne EXPAT parser funguje.

Praktická ukážka

Vytvorili sme si jednoduchý program, ktorý nieje pravé prakticky, no na študijné účely nám bohato stačil. Keď už vieme, ako to všetko funguje, môžeme sa pustiť do niečoho zmysluplnejšieho. Predstavme si situáciu, kedy vytvárame aplikáciu, napríklad nejakú klientskú aplikáciu. Chceme, aby táto aplikácia mala konfiguračný súbor vo formáte XML, kde by užívateľ mohol jednoducho meniť určíte parametre. Mame teda aplikáciu, ktorá na načíta konfiguráciu z nasledujúceho config.xml:

config.xml:


<config>
   <client id='111-111-111' password='******' />
   <server>icq.com</server>
   <port>6043</port>
</config>

Na tento účel si vytvoríme triedu Config, ktorá na začiatku aplikácie načíta konfiguračný súbor, rozparuje ho a náplní určíte členské premenne. Základom tejto triedy je load() funkcia, ktorá inicializuje EXPAT parser, načíta súbor a parsuje ho, parse() funkcia, ktorá je volaná EXPAT-om a napĺňa členské premenne. Táto imaginárna aplikácia, sa bude skladať zo súborov:


config.xml
Makefile
config.h
config.cc
main.cc

Do súborov vložíme nasledujúci kód.

Makefile:


CC=g++
LDFLAGS= -lexpat
INCLUDE= 
BIN=out

all: $(BIN)

$(BIN): main.o config.o
   $(CC) $(LDFLAGS) $(INCLUDE) -o $(BIN) main.o config.o

%.o: %.cc
   $(CC) $(CFLAGS) ${INCLUDE} -c -o  $@ $<

config.h:


#if !defined(_CONFIG_H_)
#define _CONFIG_H_

#include <vector>
#include <map>
#include <string>
#include <expat.h>
#include <iostream>

#define BUFFER_SIZE 512

typedef std::vector<std::string> path_t;
typedef std::map<std::string, std::string> args_t;

class Config
{
   public:
      Config();
      virtual ~Config();

   ////////////////////
   //    FUNCTIONS
   ////////////////////
   public:
      bool         load(const char* filename);
      virtual void parse(std::string& path, args_t& args, std::string& data);

   protected:
      static void  startElement(void *userData, const char *name, const char **attrs);
      static void  characterData(void *userData, const XML_Char *buffer, int len);
      static void  endElement(void *userData, const char *name);

   ////////////////////
   // MEMBER VARIABLES
   ////////////////////
   public:
      std::string mId;
      std::string mPassword;
      std::string mServer;
      std::string mPort;

   protected:
      path_t      mPath;
      args_t      mArgs;
      std::string mData;
};

#endif



config.cc:

#include "config.h"

using namespace std;

Config::Config()
{
   mPath.clear();
   mArgs.clear();
}

Config::~Config()
{
   mPath.clear();
   mArgs.clear();
}

void Config::startElement(void *userData, const char *name, const char **attrs)
{
   Config* conf = (Config*)userData;
   conf->mPath.push_back(name);

   //do mArgs vlozime atributy aktalneho elementu
   for(int i = 0; attrs[i] != NULL; i+=2) {
      conf->mArgs[attrs[i]] = attrs[i+1];
   }
}



void Config::characterData(void *userData, const XML_Char *buffer, int len)
{
   Config* conf = (Config*)userData;
   conf->mData = string(buffer, len);
}



void Config::endElement(void *userData, const char *name)
{
   Config*          conf = (Config*)userData;
   path_t::iterator iElement;
   string           path;

   //prejdeme vsetky prvky v poli mPath a vygenerujeme cestu
   for (iElement = conf->mPath.begin(); iElement != conf->mPath.end(); iElement++) {
      path = path + "/" + (*iElement);
   }

   //zavolame parse funkciu
   conf->parse(path, conf->mArgs, conf->mData);

   conf->mPath.pop_back();
   conf->mArgs.clear();
}

bool Config::load(const char* filename)
{
   //deklaracia potrebnych premennych
   FILE*         file;
   XML_Parser    parser;
   char          buffer[BUFFER_SIZE];
   int           isFinal;
   size_t        len;
   int           result;
   bool          out = true;

   //otvorenie konfiguracneho subofru na citanie
   file = fopen( filename, "r");
   if (!file) {
      cout << "ERROR: Can't open file:" << filename << endl;
      return false;
   }

   //inicializacia parsera
   parser = XML_ParserCreate(NULL);
   XML_SetUserData (parser, this);
   XML_SetElementHandler(parser, startElement, endElement);
   XML_SetCharacterDataHandler(parser, characterData);

   //parsujeme data v subore
   do {
      len = fread(buffer, 1, sizeof(buffer), file);
      isFinal = len - sizeof(buffer);
      result = XML_Parse(parser, buffer, len, isFinal);
      if (!result) {
         cout << "ERROR PARSING:"
              << XML_ErrorString(XML_GetErrorCode(parser))
              << " at line:"
              << XML_GetCurrentLineNumber(parser) << endl;

         out = false;
         break;
      }
   } while(!isFinal);

   //upratanie
   XML_ParserFree(parser);
   fclose(file);

   return out;
}

void Config::parse(string& path, args_t& args, string& data)
{
   if (path == "/config/client") {
      mId = args["id"];
      mPassword = args["password"];
   }

   if (path == "/config/server") {
      mServer = data;
   }

   if (path == "/config/port") {
      mPort = data;
   }
}


main.cc:

#include <iostream>
#include "config.h"

using namespace std;

int main()
{
   Config conf;

   conf.load("config.xml");

   cout << "Id:" << conf.mId << endl;
   cout << "Password:" << conf.mPassword << endl;
   cout << "Server:" << conf.mServer << endl;
   cout << "Port:" << conf.mPort << endl;

   return 0;
}

Ak ste pozorne čítali, tak väčšina kódu by vám už mala byt známa. Main vytvorí inštanciu triedy Config a zavolá funkciu load(). Po úspešnom prevedení load() funkcie, main vypíše na výstup obsah načítaných premenných z konfiguračného súboru.

Čo sa skrýva vo vnútri funkcie load()? Prakticky ten istý kód, ktorý sme už raz písali v našej prvej aplikácii. Funkcia inicializuje EXPAT parser, otvory súbor a postupne ho parsuje. V Config triede sa nachádza trojica statických funkcii startElement(), characterData(), endElement(). Ich význam, by nám už mal byt tiež jasný. Funkcie však vykonávajú o niečo viac ako v prvom príklade.

Funkcia startElement() pridá na koniec vektora mPath meno elementu, ktorý sa pravé začal parsovať. Tento vektor je dosť dôležitý. Na základe tohto vektora, sa vygeneruje takzvaný path, ktorý ďalej budeme používať. Funkcia tiež prejde všetky atribúty a náplní nimi asociatívne pole mArgs, ktoré taktiež budeme ďalej používať. Ďalšia funkcia v poradí, ktorá môže byt volaná ale nemusí (v prípade, ak element neobsahuje žiadne dáta) je characterData(). Funkcia naplní premennú mData. Tretia a asi najzaujímavejšia funkcia je endElement(). Tu to všetko dostane zmysel. EndElement() je volaná v momente, ako dôjde k ukončeniu elementu, čiže v tejto chvíli, by sme mali mat všetky potrebne informácie o elemente. Funkcia vygeneruje path z mPath vektora a zavolá funkciu parse().

Funkcia parse() je posledným kúskom do celej skladačky. V tejto funkcii budeme prevádzať napĺňanie premenných. Na presne určenie, o akú premennú ide, nám poslúži argument funkcie path. Argument obsahuje informáciu, v ktorom elemente sa momentálne nachádzame. Okrem tohto argumentu ma funkcia ešte argumenty args, v ktorom sú uložené argumenty elementu formou asociatívneho pola a data, v ktorom je uložený obsah elementu formou stringu.

Ak sme nemali problém skompilovať prvý príklad, nemal by byť ani problém skompilovať túto našu aplikáciu. Po úspešnej kompilácii aplikáciu spustime a mali by sme dostať nasledujúci výstup:


sn3d@sn3d-laptop:~/clanky/expat$ ./out
Id:111-111-111
Password:******
Server:icq.com
Port:6043

Pozornejší si určite všimli, že parse() funkciu som v kóde deklaroval ako virtuálnu. Tým som chcel naznačiť, že triedu Config je možné upraviť tak, aby bola znovu použiteľná. Šikovnejší programátor už určite vidí možnosť, ako tuto triedu vylepšiť tak, aby spĺňala požiadavky znovu použiteľného kódu.

Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2007031403-expat-parsujeme-xml/ ].