DOM (Document Object Model)

Existují dva přístupy k práci s XML formátem. Prvnímu se říká DOM (o tom bude tato část) a druhému se říká SAX (o tom bude další kapitola).

DOM vytvoří tzv. Document Object Model. Načte celý dokument do paměti a udělá z něj nějakou strukturu, ve které jsou uložené všechny informace, které dokument obsahuje. Navíc k tomu dostanete funkce, kterými můžete s touto strukturu manipulovat (dotazovat se na hodnoty elementů a atributů, měnit je, přidávat nové, mazat staré atp.).

SAX naproti tomu funguje tak, že jak se čte XML dokument, volají se vámi vytvořené funkce. Když parser XML dokumentu narazí na začátek elementu, na konec elementu, na začátek nebo konec dokumentu, na komentář atp. Vaše funkce dostanou v parametrech všechno důležité, co potřebujete o dané části XML dokumentu vědět. O–vytvoření nějaké struktury (vnitřní reprezentace) se musíte postarat už sami (SAX nic jako DOM nevytváří). Výhodou SAX přístupu je, že se nenačítá celý dokument do paměti, ale čte se „řádek po řádce“ (element po elementu), takže není SAX tak náročný na paměť jako DOM. Hodí se tedy hlavně pro zpracovávání dlouhatánských XML dokumentů.

Knihovna libxml2

Oba tyto přístupy podporuje knihovna libxml2. Takto knihovna byla původně vytvořena pro projekt Gnome, ale nemá na něm žádné závislosti. Knihovnu ve svém Linuxu už pravděpodobně budete mít nainstalovanou, ale budete si muset doinstalovat vývojový balík (jméno balíku záleží na distribuci, kterou používáte, např. v OpenSuSE je to libxml2-devel).

Existují i další knihovny pro práci s XML soubory, třeba AsmXml – knihovna napsaná v assembleru :-), eXpat a další. Knihovna libxml2 je ale klasika, proto jsem se rozhodl popsat ji.

Jedno se libxml2 musí nechat. Jeho dokumentace je otřesná. Jedná se hlavně o automaticky vygenerovaný seznam funkcí ze zdrojových kódů a pár příkladů. Pro tuto a další kapitolu jsem připravil několik příkladů, které by vám měli pomoci lépe proniknout do tajů libxml2.

DOM

Teď ale zpět k DOM. Elementům, ale i třeba komentářům a dalším „věcem“, co se mohou v XML dokumentu oběvit, se říká node (uzel). Existuje 7 druhů uzlů:

  • Dokument (kořenový uzel)
  • Element
  • Atribut
  • Text
  • Jmenný prostor (namespace)
  • Procesní instrukce
  • Komentář

libxml2 používá jako odkaz na uzel datový typ xmlNodePtr.

DOM, který libxml2 vytvoří, je struktura xmlDoc. S tou se pracuje přes odkaz xmlDocPtr.

typedef xmlNode * xmlNodePtr;
typedef xmlDoc * xmlDocPtr;

A teď k příkladu. Vytvoření struktury xmlDoc z XML souboru je velice jednoduché. V zásadě je to jen zavolání jedné funkce (uvidíte dále). Nejdříve si ale deklaruji funkci parseConfigDocument(), ve které načtu informace ze struktury xmlDoc a uložím si ji do své vlastní struktury Config. Druhým argumentem funkce je kořenový uzel (což je <sx:config>).

  1. /*------------------------------------------------*/
  2. /* 27aDom/dom-parser.h                            */
  3. #ifndef _DOM_PARSER
  4. #define _DOM_PARSER
  5.  
  6. #include <libxml/xmlmemory.h>
  7. #include <libxml/parser.h>
  8. #include "config.h"
  9.  
  10. void parseConfigDocument(xmlDocPtr doc, xmlNodePtr cur, Config * config);
  11. #endif
  12.  
  13. /*------------------------------------------------*/

Funkce main() nejdřív vytvoří mou strukturu Config. Pak zavolá funkci xmlParseFile(), která načte XML soubor a (podkud je validní) vrátí odkaz na strukturu xmlDoc.

Pak získám kořenový element pomocí funkce xmlDocGetRootElement() a zavolám svou víše zmíněnou funkci xmlDocGetRootElement(). Ani nevím, proč jsem funkci xmlDocGetRootElement() nezavolal až uvnitř parseConfigDocument() :-).

  1. /*------------------------------------------------*/
  2. /* 27aDom/dom.c                                   */
  3.  
  4. #include <stdio.h>
  5. #include "config.h"
  6. #include "dom-parser.h"
  7.  
  8. int main(void)
  9. {
  10.     xmlDocPtr doc;
  11.     xmlNodePtr cur;
  12.     Config *config = createConfigStructure(2);
  13.     if (!config) {
  14.         fprintf(stderr, "Nepodařilo se vytvořit Config\n");
  15.         return 1;
  16.     }
  17.  
  18.     doc = xmlParseFile("config.xml");
  19.     if (doc == NULL) {
  20.         fprintf(stderr, "Document se nepodařilo naparsovat.\n");
  21.         return 1;
  22.     }
  23.  
  24.     cur = xmlDocGetRootElement(doc);
  25.     if (cur == NULL) {
  26.         fprintf(stderr, "Prázdný dokument\n");
  27.         xmlFreeDoc(doc);
  28.         return 1;
  29.     }
  30.  
  31.     parseConfigDocument(doc, cur, config);
  32.  
  33.     printConfigStructure(config);
  34.     freeConfigStructure(config);
  35.     /* xmlSaveFile("-", doc); // */
  36.     xmlFreeDoc(doc);
  37.     return 0;
  38. }
  39.  
  40. /*------------------------------------------------*/

Funkce parseConfigDocument() vyplní strukturu Config. Tu si pak vytisknu, uvolním ji a ještě uvolním strukturu xmlDoc (voláním funkce xmlFreeDoc()).

Strukturu xmlDoc můžete uložit do souboru funkcí xmlSaveFile(). Pokud zadáte jako jméno souboru "-", vytiskne se struktura na obrazovku.

Ještě než se dostanu k parseConfigDocument(), podívejte se na deklarace pomocných funkcí, které kopírují hodnoty z xmlNodePtr. V jejich definicích se neděje nic zvláštního, jen se zkopíruje text z jednoho místa paměti (z xmlNodePtr) do jiného (např. do *dest).

  1. /*------------------------------------------------*/
  2. /* 27aDom/dom-utils.h                             */
  3. #ifndef _DOM_UTILS
  4. #define _DOM_UTILS
  5.  
  6. #include <libxml/parser.h>
  7.  
  8. // zkopiruje hodnotu elementu do dest
  9. void copyXMLString(xmlDocPtr doc, xmlNodePtr cur, char *dest, size_t destLength);
  10. // prevede hodnotu elementu na int a ulozi do dest
  11. void copyXMLInteger(xmlDocPtr doc, xmlNodePtr cur, int *dest);
  12. //ulozi hodnotu attributu do dest
  13. bool getProperty(xmlNodePtr cur, char *name, char *dest, size_t destLength);
  14. #endif
  15.  
  16. /*------------------------------------------------*/

Funkce parseConfigDocument() prochází potomky kořenového elemetnu (potomky <sx:config>). Když narazí na element version nebo databases, zavolá funkce, které jsem napsal na parsování těchto elementů:

  1. /*------------------------------------------------*/
  2. /* 27aDom/dom-parser.c                            */
  3.  
  4. #include <stdbool.h>
  5. #include <string.h>
  6. #include "dom-parser.h"
  7. #include "dom-utils.h"
  8.  
  9. static bool parseVersion(xmlDocPtr doc, xmlNodePtr cur, Config * config);
  10. static int parseDatabases(xmlDocPtr doc, xmlNodePtr cur, Config * config);
  11. static bool parseDatabase(xmlDocPtr doc, xmlNodePtr cur, Database * database);
  12.  
  13. void parseConfigDocument(xmlDocPtr doc, xmlNodePtr cur, Config * config)
  14. {
  15.     cur = cur->xmlChildrenNode;
  16.     while (cur != NULL) {
  17.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"version"))) {
  18.             parseVersion(doc, cur, config);
  19.         }
  20.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"databases"))) {
  21.             parseDatabases(doc, cur, config);
  22.         }
  23.         cur = cur->next;
  24.     }
  25. }
  26.  

Není v tom žádná magie. Jen vás může zarazit datový typ xmlChar *. V zásadě je to jen alias pro unsigned char *. Pozor, libxml2 předpokládá, že váš dokument je v UTF-8! Dokáže si poradit i s jinými kódováními, ale to už je nad rámec této kapitoly.

Taky si je potřeba uvědomit, že text mezi elementy, i když jde jen o tabulátory, mezery a nové znaky, tvoří textové elementy. Proto nemá element xs:config jen 2 potomky (version a databases), ale má jich 5 (3 textové uzly navíc).

Funkce parseVersion() prochází potomky elementu <version> a hledá elementy major a minor. Když je najde, zkopíruje jejich obsah do struktury Config. Pokud najde oba elementy, vrátí funkce true, jinak false.

  1. static bool parseVersion(xmlDocPtr doc, xmlNodePtr cur, Config * config)
  2. {
  3.     bool nalezeno[2] = { false, false };
  4.     cur = cur->xmlChildrenNode;
  5.     while (cur != NULL) {
  6.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"major"))) {
  7.             nalezeno[0] = true;
  8.             copyXMLString(doc, cur, config->version.major,
  9.                       sizeof(config->version.major) /
  10.                       sizeof(config->version.major[0]));
  11.         }
  12.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"minor"))) {
  13.             nalezeno[1] = true;
  14.             copyXMLString(doc, cur, config->version.minor,
  15.                       sizeof(config->version.major) /
  16.                       sizeof(char));
  17.         }
  18.         cur = cur->next;
  19.     }
  20.     return nalezeno[0] && nalezeno[1];
  21. }
  22.  

Funkce parseDatabases() jenom hledá mezi svými potomky elementy database a zavolá funkci parseDatabase(), která zkopíruje data pro jednu položku databáze. parseDatabase() nezískává hodnoty jen z hodnot elementů, ale i atributů type a enabled. Pokud nalezne všechny položky, vrací true.

  1. static int parseDatabases(xmlDocPtr doc, xmlNodePtr cur, Config * config)
  2. {
  3.     size_t i = 0;
  4.     cur = cur->xmlChildrenNode;
  5.  
  6.     while ((i < config->databasesSize) && cur) {
  7.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"database"))) {
  8.             parseDatabase(doc, cur, &(config->databases[i]));
  9.             i++;
  10.         }
  11.         cur = cur->next;
  12.     }
  13.     return i;
  14. }
  15.  
  16. static bool parseDatabase(xmlDocPtr doc, xmlNodePtr cur, Database * database)
  17. {
  18.     bool nalezeno[] = { false, false, false, false, false, false };
  19.     bool ntrues[] = { true, true, true, true, true, true };
  20.     char tmp[10];
  21.  
  22.     if (getProperty(cur, "type", tmp, sizeof(tmp))) {
  23.         nalezeno[0] = true;
  24.         database->type = getDatabaseTypeFromText(tmp);
  25.     }
  26.  
  27.     if (getProperty(cur, "enabled", tmp, sizeof(tmp))) {
  28.         nalezeno[1] = true;
  29.         database->enabled = !strcmp(tmp, "true");
  30.     }
  31.  
  32.     cur = cur->xmlChildrenNode;
  33.     while (cur != NULL) {
  34.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"name"))) {
  35.             nalezeno[2] = true;
  36.             copyXMLString(doc, cur, database->name,
  37.                       sizeof(database->name));
  38.         } else if ((!xmlStrcmp(cur->name, (const xmlChar *)"password"))) {
  39.             nalezeno[3] = true;
  40.             copyXMLString(doc, cur, database->password,
  41.                       sizeof(database->password));
  42.         }
  43.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"schema"))) {
  44.             nalezeno[4] = true;
  45.             copyXMLString(doc, cur, database->schema,
  46.                       sizeof(database->schema));
  47.         }
  48.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"host"))) {
  49.             nalezeno[5] = true;
  50.             copyXMLString(doc, cur, database->host,
  51.                       sizeof(database->host));
  52.         }
  53.         if ((!xmlStrcmp(cur->name, (const xmlChar *)"port"))) {
  54.             copyXMLInteger(doc, cur, &(database->port));
  55.         }
  56.         cur = cur->next;
  57.     }
  58.     if (!database->port) {
  59.         database->port = getDefaultPort(database->type);
  60.     }
  61.     return !memcmp(nalezeno, ntrues,
  62.                sizeof(nalezeno) / (sizeof(nalezeno[0])));
  63. }
  64.  
  65. /*------------------------------------------------*/

Příklad je to dlouhý, ale sami vidíte, že celé kouzlo je jen v procházení struktury xmlNode pomocí odkazu na děti (cur->xmlChildrenNode) pomocí cur->next a porovnávání názvu elementu pomocí xmlStrcmp().

Struktura xmlNode obsahuje kromě odkazu na děti ->xmlChildrenNode (alias ->children) taky odkazy na parent (rodič), next (další), prev(předchozí) nebo next (předchozí). Díky těmto odkazům můžete brouzdat po struktuře XML dokumentu jak se vám zachce.

Jako poslední jsou tu pomocné funkce pro získávání hodnot z elementů a atributů. K tomu používají funkce xmlNodeListGetString() a xmlGetProp(). Tyto funkce pro výsledek alokují paměť, takže ji musíte uvolnit pomocí xmlFree().

  1. /*------------------------------------------------*/
  2. /* 27aDom/dom-utils.c                             */
  3.  
  4. #include <stdbool.h>
  5. #include <string.h>
  6. #include "dom-utils.h"
  7.  
  8. void copyXMLString(xmlDocPtr doc, xmlNodePtr cur, char *dest, size_t destLength)
  9. {
  10.     xmlChar *string;
  11.     string = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
  12.     if (string != NULL) {
  13.         strncpy(dest, (char *)string, destLength);
  14.         dest[destLength - 1] = '\0';    // pro jistotu
  15.         xmlFree(string);
  16.     }
  17. }
  18.  
  19. /**
  20. * Pozn: funkce atoi() nedetekuje chyby.
  21. * Lepsi by bylo pouzit strtol()
  22. */
  23. void copyXMLInteger(xmlDocPtr doc, xmlNodePtr cur, int *dest)
  24. {
  25.     xmlChar *string;
  26.     string = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
  27.     if (string != NULL) {
  28.         *dest = atoi((char *)string);
  29.         xmlFree(string);
  30.     }
  31. }
  32.  
  33. bool getProperty(xmlNodePtr cur, char *name, char *dest, size_t destLength)
  34. {
  35.     xmlChar *value;
  36.     value = xmlGetProp(cur, (xmlChar *) name);
  37.     if (value == NULL)
  38.         return false;
  39.     strncpy(dest, (char *)value, destLength);
  40.     dest[destLength - 1] = '\0';    // pro jistotu
  41.     xmlFree(value);
  42.     return true;
  43. }
  44.  
  45. /*------------------------------------------------*/

A to je vše. Snad to ani moc nebolelo :-). I když je příklad opravdu dlouhý, vlastně jsem vám představil jen minimum z funkcí a vlastnotí xmlNode či xmlPtr.

Nainstalujte si libxml2-devel. Bez toho tento příklad nepřeložíte. Překlad může vypadat takto:

$ gcc -o dom -g -Wall `xml2-config --cflags --libs` dom.c config.c dom-parser.c dom-utils.c

Výstup z programu:

Version: 1.0.
Database: MySQL; enabled
    Username: petr
    Password: *****
    Schema: test
    Host: localhost
    Port: 3306

Database: PostgreSQL; 
    Username: pavel
    Password: 
    Schema: petr
    Host: localhost
    Port: 5432

Nezmínil jsem se třeba vůbec o tom, jak měnit hodnoty elementů a atributů, přidávat nové atp. To už si můžete nastudovat z dokumentace. Hledejte funkce xmlNewNode(), xmlNewProp(), xmlNewText(), xmlUnlinkNode(), nebo xmlUnsetProp().

Také by se asi slušelo zmínit se o funkci xmlParseMemory(), která, na rozdíl od xmlParseFile(), neparsuje XML ze souboru ale z paměti (z nějakého řetězce).

Použité funkce

Aby se vám nepletli funkce které jsem napsal já s funkcemi z libxml2, tak tady máte seznam libxml2 funkcí, které jsem v této kapitole použil.

/**
* Parse an XML file and build a tree.
* Automatic support for ZLIB/Compress compressed document is
* provided by default if found at compile-time.
* @param filename the filename
* @return the resulting document tree if the file was wellformed, NULL otherwise.
*/

xmlDocPtr xmlParseFile (const char * filename);

Dokumentace: xmlParseFile

/**
* Get the root element of the document
* (doc->children is a list containing possibly comments, PIs, etc ...).
* @param doc the doument
* @return the xmlNodePtr for the root or NULL
*/

xmlNodePtr xmlDocGetRootElement (const xmlDoc * doc);

Dokumentace: xmlDocGetRootElement

/**
* Free up all the structures used by a document, tree included.
* @param cur pointer to the document
*/

void xmlFreeDoc (xmlDocPtr cur);

Dokumentace: xmlFreeDoc

/**
* a strcmp for xmlChar's
* @param str1 the first xmlChar *
* @param str2 the second xmlChar *
* @return the integer result of the comparison
*/

int xmlStrcmp (const xmlChar * str1, const xmlChar * str2);

Dokumentace: xmlStrcmp

/**
* Build the string equivalent to the text contained in the Node list made of TEXTs and ENTITY_REFs
* @param  doc the document
* @param  list a Node list
* @param  inLine should we replace entity contents or show their external form
* @return a pointer to the string copy, the caller must free it with xmlFree().
*/

xmlChar * xmlNodeListGetString (xmlDocPtr doc, const xmlNode * list, int inLine);

Dokumentace: xmlNodeListGetString

/**
* Search and get the value of an attribute associated to a node
* This does the entity substitution. This function looks in DTD attribute
* declaration for #FIXED or default declaration values unless DTD use has
* been turned off.
* NOTE: this function acts independently of namespaces associated to the
* attribute. Use xmlGetNsProp() or xmlGetNoNsProp() for namespace aware
* processing.
* @param node the node
* @param name the attribute name
* @return the attribute value or NULL if not found. It's up to the caller to free the memory with xmlFree().
*/

xmlChar * xmlGetProp (const xmlNode * node, const xmlChar * name);

Dokumentace: xmlGetProp

/**
* Uvolní dříve alokovanou paměť některou z libxml2 funkcí
* @param mem Ukazatel na paměť, která se má uvolnit
*/

void xmlFree(void * mem);

Dokumentace: ? (nenašel jsem)

Kromě toho jsem používal atributy struktur:

  • xmlNode (např. cur->xmlChildrenNode je alias pro cur->children).
Komentář Hlášení chyby
Created: 6.9.2014
Last updated: 18.11.2015
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..