Wprowadzenie do tematyki artykułu
Jeśli jesteś informatykiem doskonale znasz format XML i wiesz czym się charakteryzuje. Wiesz też, że pobierać dane z dokumentów XML można m.in. przy pomocy parserów XML, ciekawym faktem jest to, iż można parsować również niektóre strony internetowe (zgodne z XMLem np. napisane przy użyciu
XHTML).
Format XML jest często używany do:
Główną zaletą jest dostępność bardzo wielu parserów w każdym szanującym się języku programowania (w tym nawet
w assemblerze). Kolejną zaletą jest to, że format ten jest na ogół czytelny dla człowieka.
Do formatu XML istnieją nawet narzędzia do jego walidacji, takie jak m.in.: XML Scheema, czy XML DTD.
|
Dla ułatwienia zrozumienia artykułu polecam zapoznanie się ze
specyfikacją formatu XML.
Przykładowy plik XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
...
</root>
Struktura ta zawiera opcjonalną deklarację informującą, że mamy do czynienia z plikiem XML, a ponadto znajduje się w niej główny korzeń, w którym umieszcza się pozostałe węzły jakie mają być przechowywane w strukturze pliku XML.
Biblioteka RapidXML
Dlatego RapidXML
Dla języka C++ również jest wiele parserów do XMLa. Świetne, graficzne ich porównanie znajduje się na
stackoverflow, co ciekawe owa strona wpłynęła na moją decyzję wyboru biblioteki RapidXMLa. Wybór ten argumentuję tym, iż bibliotekę ta jest łatwa w użyciu (zawiera ona jedynie pliki nagłówkowe). Zgodnie z dokumentacją celem biblioteki jest najszybsze możliwe parsowanie plików XML, a głównie dlatego używamy C++, oczywiście kod biblioteki z tego powodu może nie być łatwy w zrozumieniu. W temacie szybkości dodam, że na chwilę obecną nie jest to niestety najszybsza biblioteka do parsowania XMLi. Zalety tej biblioteki zostały zauważone przez programistów
biblioteki boost w komponencie
Boost Property Tree.
Dodam, że dokumentacja ten portal posiada polską dokumentację poświęconą RapidXML:
RapidXML!
Instalacja biblioteki RapidXML
Bibliotekę tą możesz pobrać ze
strony domowej. Nie trzeba jej kompilować ani instalować, gdyż są to tylko pliki nagłówkowe. Wystarczy je umieścić w odpowiednim miejscu, zależnie gdzie w stosunku do naszego kodu umieścimy bibliotekę może się okazać konieczne podanie ścieżek do kompilatora (np. dla gcc
-isystem/sciezka/do/naglowkow).
Jak używać biblioteki RapidXML we własnych programach
Mając rozpakowaną bibliotekę i podane odpowiednie ścieżki do kompilatora, aby użyć biblioteki wystarczy włączyć plik
rapidxml.hpp za pomocą dyrektywy
#include
. Jeśli ponadto chcemy mieć dostęp do funkcji wyświetlających dokument XML na różne sposoby można również dołączyć plik
rapidxml_print.hpp. Jest dostępny jeszcze nagłówek
rapidxml_iterators.hpp, udostępniający iteratory oraz
rapidxml_utils.hpp oferujący pewne dodatkowe funkcje przydatne podczas korzystania z biblioteki.
Wszystkie definicje są w przestrzeni nazw
namespace rapidxml { }
.
rapidxml | Główna przestrzeń nazw tej biblioteki. (przestrzeń nazw) |
---|
Parsowanie dokumentów XML
Aby dokonać parsowania dowolnego tekstu należy użyć
xml_document podając do funkcji parsującej łańcuch znaków, którego zawartością będzie plik w formacie XML. Można to zrobić w następujący sposób:
#include <rapidxml.hpp>
using namespace rapidxml;
int main()
{
const char * xmlDocument = "...";
xml_document <> document;
try
{
document.parse < 0 >( text );
}
catch( const parse_error & e )
{
std::cerr << e.what() << " here: " << e.where < char >() << std::endl;
return - 1;
}
}
Jak widać w razie błędu parsowania zostanie wyrzucony
parse_error. O innych flagach parsowania rozpiszę się później, dociekliwi mogą jednak wcześniej zapoznać się z
Flagi.
Kończenie pracy z dokumentem
Po przetworzeniu wg naszej woli dokumentu XML należy wyczyścić zaalokowaną pamięć, można to zrobić przy użyciu metody:
clear. Oczywiście destruktor automatycznie zawoła tę metodę.
Nawigacja po danych po parsowaniu XMLa
Dokument XML składa się z węzłów, reprezentowanych przez
xml_node, oraz atrybutów reprezentowanych przez
xml_attribute.
Struktura pliku XML i dostęp do korzenia
Aby po dokonaniu parsowania z naszego dokumentu dostać się do korzenia najprościej zrobić tak:
xml_node <>* root = document.first_node();
. Oczywiście może się okazać, że przy użyciu pewnych flag parsowania trzeba będzie jeszcze poskakać celem dostania się do korzenia (np. przed nim może być deklaracja dokumentu XML).
Metody do nawigowania po węzłach
xml_node, a co za tym idzie również dziedziczący po nim
xml_document zawierają poniższe metody:
first_node | Zwraca wskaźnik do pierwszego węzła. (metoda) |
---|
last_node | Zwraca wskaźnik do ostatniego węzła. (metoda) |
---|
namespace rapidxml
{
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::first_node();
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::last_node();
}
Na uwagę zasługuje metoda:
parent | Zwraca wskaźnik do nadrzędnego węzła. (metoda) |
---|
namespace rapidxml
{
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::parent();
}
Dzięki której można sprawdzić, czy poniższe metody mają sens (jeśli nie ma rodzica, to przechodzenie na rodzeństwo jest niezdefiniowane):
next_sibling | Zwraca wskaźnik do następnego węzła zawartego w tym samym węźle nadrzędnym. (metoda) |
---|
previous_sibling | Zwraca wskaźnik do poprzedniego węzła zawartego w tym samym węźle nadrzędnym. (metoda) |
---|
namespace rapidxml
{
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::next_sibling();
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::previous_sibling();
}
Wszystkie wymienione wyżej metody zwracają wartość
nullptr w przypadku, gdy szukany element nie zostanie znaleziony w dokumencie XML. Należy pamiętać, aby zawsze sprawdzać jaka wartość jest zwracana przez wywoływaniu metody.
Dygresja dla doświadczonych w używaniu parserów XML w innych językach programowania zgodnych ze specyfikacją XML DOMMającą taką strukturę:
<nadwezel> <wezel>Niezwykle wazna tresc</wezel> </nadwezel>
i mając wskazany w danej chwili <wezel> w RapidXML możemy od razu odczytać zarówno nazwę, jak i wartość. Natomiast w parserach zgodnych ze specyfikacją XML DOM wg W3C, będąc aktualnie na tym węźle, nazwę możemy odczytać od razu, natomiast celem odczytania wartości musimy wejść do wgłąb i dopiero wtedy możemy ją odczytać, z kolei jak wejdziemy wgłąb to już nie możemy odczytać nazwy bez przeskoczenia na rodzica. Wiedza o tym oszczędzi zastanawiania, dlaczego nasz program nie czyta tak jakbyśmy chcieli. |
Nawigacja po atrybutach węzła
Chcąc od węzła przejść do atrybutu możemy użyć następujących funkcji:
namespace rapidxml
{
template < typename Ch = char >
xml_attribute < Ch >* xml_node < Ch >::first_attribute();
template < typename Ch = char >
xml_attribute < Ch >* xml_node < Ch >::last_attribute();
}
Między atrybutami przechodzimy przy użyciu funkcji:
namespace rapidxml
{
template < typename Ch = char >
xml_attribute < Ch >* previous_attribute() const;
template < typename Ch = char >
xml_attribute < Ch >* next_attribute() const;
}
Aby przejść po wszystkich atrybutach wystarczy najprostsza pętla:
for( xml_attribute <>* a = node->first_attribute(); a; a = a->next_attribute() )
cout << a->name() << " = " << a->value() << endl;
W ramach wyjaśnienia przypominam, iż w momencie nieznalezienia kolejnego atrybutu do zmiennej
a zostanie przypisana wartość
nullptr.
Pobieranie nazwy i wartości węzłów i atrybutów
Nasze
xml_node oraz
xml_attribute posiadają jeszcze m.in. następujące metody:
name | Ustawia lub pobiera nazwę elementu. (metoda) |
---|
value | Ustawia lub pobiera wartość elementu. (metoda) |
---|
Wyświetlanie dokumentu XML
Chcąc wyświetlić zawartość naszego XML mamy do dyspozycji pewne dodatkowe funkcje, które jednak są dostępne dopiero po włączeniu nagłówka
#include <rapidxml_print.hpp>
:
operator<< | Przekazuje dane węzła do strumienia wyjściowego. (operator - funkcja) |
---|
print | Przekazuje dane węzła na wyjście. (funkcja) |
---|
można ich użyć w następujący sposób:
using namespace rapidxml;
xml_document <> document;
cout << document;
print( std::cout, document, 0 );
string s;
print( std::back_inserter( s ), document, 0 );
Flagi wyświetlania dokumentu XML
Poruszmy kwestie flag, poza
0, która ładnie wyświetla dokument, mamy flagę odpowiadającą za wyświetlanie bez wcięć, porównajmy je:
Jak widać możemy RapidXML użyć zarówno do usunięcia zbędnych białych znaków z naszego pliku XML, jak i do wypisania go w czytelniejszej formie.
Problem z kompilacją nagłówka rapidxml_print.hppCzasami kod, który niegdyś się kompilował, po zmianach w kompilatorze przestaje się kompilować. W związku z tym możemy się natknąć na pewien problem włączając nagłówek rapidxml_print.hpp. Problemem są niezadeklarowane przed użyciem funkcje. Dokładniejszy opis problemu oraz rozwiązanie znajduje się na Stack Overflow. W skrócie należy zadeklarować metody w następujący sposób (poniżej cytuje rozwiązanie ze Stack Overflow):
template < class OutIt, class Ch > inline OutIt print_children( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_attributes( OutIt out, const xml_node < Ch > * node, int flags ); template < class OutIt, class Ch > inline OutIt print_data_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_cdata_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_element_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_declaration_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_comment_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_doctype_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); template < class OutIt, class Ch > inline OutIt print_pi_node( OutIt out, const xml_node < Ch > * node, int flags, int indent ); należy to wkleić niestety wewnątrz pliku nagłówkowego rapidxml_print.hpp tuż przed funkcją print_node. |
Przykład odczytu z dokumentu XML
Wiemy już jak nawigować po węzłach i atrybutach, czas na wykorzystanie tej wiedzy w praktyce.
Przykładowy plik XML
Poniżej znajduje się przykładowy plik XML, którego użyję w najbliższym przykładzie:
<?xml version="1.0" encoding="UTF-8"?>
<ceo>
<manager id="1" name="Jan" working_years="10">
<developer id="2" name="Stefan" working_years="0">komunikator</developer>
<developer id="3" name="Zygmunt" working_years="2">komunikator</developer>
</manager>
<manager id="2" name="Jan" working_years="10">
<developer id="4" name="Andrzej" working_years="0">klient poczty</developer>
<tester id="5" name="Andrzej" working_years="3">klient poczty</tester>
</manager>
</ceo>
Przykładowy program czytający plik XML
Poniżej kod odczytujący powyższą treść, demonstrujący użycie możliwości poruszania po węzłach. Pod przykładem jest wyjaśnienie trudniejszych miejsc w programie:
#include <iostream>
#include <fstream>
#include <string>
#include <memory>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
using namespace rapidxml;
auto file2char( const char * fileName )
{
ifstream file( fileName );
if( !file )
{
throw runtime_error( "File '" + string( fileName ) + " can't be opened!" );
}
file.seekg( 0, file.end );
const auto fileSize = file.tellg();
file.seekg( 0 );
unique_ptr < char[] > fileContext( new char[ fileSize + static_cast < decltype( fileSize ) >( 1 ) ] );
file.read( fileContext.get(), fileSize );
return fileContext;
}
void printAttributes( xml_node <>* node )
{
for( xml_attribute <>* a = node->first_attribute(); a; a = a->next_attribute() )
{
cout << a->name() << "' = '" << a->value() << "'\t";
}
}
int main()
{
const char * fileName = "...";
auto xmlFileContext = file2char( fileName );
xml_document <> document;
try
{
document.parse < 0 >( xmlFileContext.get() );
}
catch( const parse_error & e )
{
std::cerr << e.what() << " here: " << e.where < char >() << std::endl;
return - 1;
}
print( cout, document, 0 );
xml_node <>* nodeCeo = document.first_node();
for( xml_node <>* manager = nodeCeo->first_node(); manager; manager = manager->next_sibling() )
{
cout << "Manager: '" << manager->name() << "', known as:\t";
printAttributes( manager );
cout << "\tfor:\n";
for( xml_node <>* realWorker = manager->first_node(); realWorker; realWorker = realWorker->next_sibling() )
{
cout << "\t'" << realWorker->name() << "' id=" << realWorker->first_attribute()->value() << " working on: '" << realWorker->value() << "'\n";
}
}
}
Output powyższego programu:
<?xml version="1.0" encoding="UTF-8"?>
<ceo>
<manager id="1" name="Jan" working_years="10">
<developer id="2" name="Stefan" working_years="0">komunikator</developer>
<developer id="3" name="Zygmunt" working_years="2">komunikator</developer>
</manager>
<manager id="2" name="Jan" working_years="10">
<developer id="4" name="Andrzej" working_years="0">klient poczty</developer>
<tester id="5" name="Andrzej" working_years="3">klient poczty</tester>
</manager>
</ceo>
Manager: 'manager', known as: id' = '1' name' = 'Jan' working_years' = '10' for:
'developer' id=2 working on: 'komunikator'
'developer' id=3 working on: 'komunikator'
Manager: 'manager', known as: id' = '2' name' = 'Jan' working_years' = '10' for:
'developer' id=4 working on: 'klient poczty'
'tester' id=5 working on: 'klient poczty'
Funkcje do nawigacji po węzłach i atrybutach -pełna sygnatura
Mamy za sobą już po trochu teorii i praktyki, czas na częściowe skomplikowanie, a zarazem ułatwienie korzystania z parsera, funkcje dla
xml_node oraz
xml_attribute, które wcześniej wymieniłem tak naprawdę mają następującą sygnaturę:
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::first_node( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::last_node( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::previous_sibling( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
template < typename Ch = char >
xml_node < Ch >* xml_node < Ch >::next_sibling( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
template < typename Ch = char >
xml_attribute < Ch >* xml_node < Ch >::first_attribute( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
template < typename Ch = char >
xml_attribute < Ch >* xml_node < Ch >::last_attribute( const Ch * name = 0, std::size_t name_size = 0, bool case_sensitive = true ) const;
Parametry mają następujące znaczenie:
Zapewne wyda się dziwnym podawanie
name_size, ale będzie to bardziej zrozumiałe w momencie opisania przeze mnie polityki obsługi własności tej biblioteki.
Inne ciekawe funkcje biblioteki
Biblioteka dostarcza jeszcze wiele drobnych, ale użytecznych funkcjonalności.
Inne użyteczności biblioteki
Użyteczności (ang. utilities, w skrócie utils), aby z nich skorzystać konieczne jest włączenie nagłówka:
#include <rapidxml_utils.hpp>
są to funkcje:
oraz szablon klasy
file, posiadająca takie metody jak:
file | Wczytuje dane do pamięci. (konstruktor) |
---|
data | Zwraca wskaźnik do danych. (metoda) |
---|
size | Zwraca rozmiar danych. (metoda) |
---|
Modyfikacja dokumentu XML
Odczytywanie to jedno, ale potrzebna jest również analogiczna funkcjonalność jaką jest modyfikowanie dokumentu XML, oczywiście biblioteka takową dostarcza (wprawdzie nie przy wszystkich flagach parsowania, ale o tym później).
Funkcje zmieniające węzły i atrybuty
Zmiana nazwy i wartości
Zarówno
xml_node jak i
xml_attribute posiadają nazwę i wartość, co ciekawe do pobierania i modyfikowania tych wartości służą funkcje o tej samej nazwie. Zachowanie zależy od podanych parametrów, jeśli nie podamy żadnego, zostanie zwrócona nazwa/wartość węzła/atrybutu, jeśli podamy, zostanie ona ustawiona. Oprócz nazwy możemy podać długość napisu, co jest przydatne w sytuacji, gdy nie jest on zakończony
'\0'
. Szczegóły na temat tych funkcji:
name | Ustawia lub pobiera nazwę elementu. (metoda) |
---|
value | Ustawia lub pobiera wartość elementu. (metoda) |
---|
template < typename Ch = char >
Ch * xml_base < Ch >::name() const;
template < typename Ch = char >
Ch * xml_base < Ch >::value() const;
template < typename Ch = char >
void xml_base < Ch >::name( const Ch * name, std::size_t size = 0 );
template < typename Ch = char >
void xml_base < Ch >::value( const Ch * value, std::size_t size = 0 );
template < typename Ch = char >
std::size_t xml_base < Ch >::name_size() const;
template < typename Ch = char >
std::size_t xml_base < Ch >::value_size() const;
Należy zwrócić uwagę, że funkcje te ze względów optymalizacyjnych nie kopiują sobie tekstu, dlatego chcąc dokonać kopii powinniśmy użyć:
template < class Ch = char >
Ch * memory_pool < Ch >::allocate_string( const Ch * source = 0, std::size_t size = 0 );
xml_document dziedziczy po
memory_pool, dlatego najlepszym rozwiązaniem jest użycie instancji dokumentu do wszelakich alokacji, dzięki temu wszystkie zaalokowane napisy będą żyły tak długo jak owa instancja. Podanie
0 jako pierwszego argumentu powoduje zaalokowanie pamięci, bez jej inicjowania.
Funkcje dodające węzły i atrybuty
W tym celu zarówno
xml_node jak i
xml_document zawierają następujące funkcje:
append_node | Wstawia węzeł podrzędny na końcu. (metoda) |
---|
prepend_node | Wstawia węzeł podrzędny na początku. (metoda) |
---|
insert_node | Wstawia węzeł podrzędny przed wybranym węzłem podrzędnym. (metoda) |
---|
Wywołujemy je na rzecz obiektu, do którego chcemy wstawić węzeł/atrybut
template < typename Ch = char >
void xml_node < Ch >::append_node( xml_node < Ch >* node );
template < typename Ch = char >
void xml_node < Ch >::prepend_node( xml_node < Ch >* node );
template < typename Ch = char >
void xml_node < Ch >::insert_node( xml_node < Ch >* where, xml_node < Ch >* node );
Anologicznie atrybuty dodajemy przy użyciu następujących funkcji:
template < typename Ch = char >
void xml_node < Ch >::prepend_attribute( xml_attribute < Ch >* attribute );
template < typename Ch = char >
void xml_node < Ch >::append_attribute( xml_attribute < Ch >* attribute );
template < typename Ch = char >
void xml_node < Ch >::insert_attribute( xml_attribute < Ch >* where, xml_attribute < Ch >* attribute );
Usuwanie atrybutów oraz węzłów
Funkcje do usuwania węzłów:
template < typename Ch = char >
void xml_node < Ch >::remove_first_node();
template < typename Ch = char >
void xml_node < Ch >::remove_last_node();
template < typename Ch = char >
void xml_node < Ch >::remove_node( xml_node < Ch >* node );
template < typename Ch = char >
void xml_node < Ch >::remove_all_nodes();
Analogiczne funkcje do usuwania atrybutów:
template < typename Ch = char >
void xml_node < Ch >::remove_first_attribute();
template < typename Ch = char >
void xml_node < Ch >::remove_last_attribute();
template < typename Ch = char >
void xml_node < Ch >::remove_attribute( xml_attribute < Ch >* attribute );
template < typename Ch = char >
void xml_node < Ch >::remove_all_attributes();
Przykład modyfikacji dokument XML
Teoria bez praktyki nie trafia do serca. Używam tego samego pliku XML co poprzednio. Opis cięższych części programu znajduje się poniżej.
#include <iostream>
#include <string>
#include <memory>
#include <iomanip>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
#include <rapidxml_utils.hpp>
using namespace rapidxml;
int main()
{
const char * fileName = "...";
unique_ptr < rapidxml::file <>> xmlFile;
xml_document <> document;
try
{
xmlFile.reset( new rapidxml::file <>( fileName ) );
document.parse < 0 >( xmlFile->data() );
}
catch( const parse_error & e )
{
std::cerr << e.what() << " here: " << e.where < char >() << std::endl;
return - 1;
}
catch( const exception & e )
{
cerr << e.what() << endl;
return - 2;
}
cout << document;
cout << setfill( '_' ) << setw( 60 ) << '\n' << endl;
xml_node <>* ceo = document.first_node();
ceo->name( "President" );
xml_node <>* managerBetter = ceo->first_node();
xml_node <>* managerWorse = ceo->last_node();
xml_node <>* tester = managerWorse->first_node( "tester" );
managerWorse->remove_node( tester );
ceo->remove_node( managerWorse );
xml_node <>* developerReportingToWorseManager = document.clone_node( managerWorse->first_node() );
managerBetter->append_node( developerReportingToWorseManager );
developerReportingToWorseManager->previous_sibling()->value( "synchronizacja miedzy komunikatorem a klientem poczty" );
{
xml_attribute <>* workingYears = managerBetter->first_attribute( "working_years" );
workingYears->name( "sizeOfTeam" );
auto sizeOfTeam = count_children( managerBetter );
auto sizeOfTeamAsString = to_string( sizeOfTeam );
workingYears->value( document.allocate_string( sizeOfTeamAsString.c_str() ) );
}
cout << document;
}
Na komentarz zasługuje
(1), gdyż szablon klasy
file nie posiada domyślnego konstruktora, zamiast tego w konstruktorze przyjmuję nazwę pliku, w razie problemów z odczytem pliku zostaje wyrzucony wyjątek. Z tego powodu konstrukcje obiektu potrzebowałem mieć w bloku
try-catch, z drugiej strony metoda
parse nie kopiuje tekstu, dlatego obiekt czytający plik musi istnieć tak samo długo jak nasz
xml_document. Jeszcze parę słów o
(2), mimo iż tylko chcemy przenieść węzeł z jednego miejsca do drugiego konieczne jest dokonanie kopiowania, nie ma obecnie metody przenoszącej węzeł.
Jak widać z powyższego kodu -jest tam opowiedziana pewna historia, której efekt widać na poniższym outpucie:
<ceo>
<manager id="1" name="Jan" working_years="10">
<developer id="2" name="Stefan" working_years="0">komunikator</developer>
<developer id="3" name="Zygmunt" working_years="2">komunikator</developer>
</manager>
<manager id="2" name="Jan" working_years="10">
<developer id="4" name="Andrzej" working_years="0">klient poczty</developer>
<tester id="5" name="Andrzej" working_years="3">klient poczty</tester>
</manager>
</ceo>
___________________________________________________________
<President>
<manager id="1" name="Jan" sizeOfTeam="3">
<developer id="2" name="Stefan" working_years="0">komunikator</developer>
<developer id="3" name="Zygmunt" working_years="2">komunikator</developer>
<developer id="4" name="Andrzej" working_years="0">klient poczty</developer>
</manager>
</President>
Generowanie od ZERA własnego dokumentu XML
Wiemy jak modyfikować plik XML, ale bywa, że chcemy wygenerować go od zera.
Funkcje alokujące
Do alokacji atrybutów i węzłów służą funkcje:
template < typename Ch = char >
xml_node < Ch >* memory_pool::allocate_node( node_type type, const Ch * name = 0, const Ch * value = 0, std::size_t name_size = 0, std::size_t value_size = 0 );
template < typename Ch = char >
xml_attribute < Ch >* memory_pool::allocate_attribute( const Ch * name = 0, const Ch * value = 0, std::size_t name_size = 0, std::size_t value_size = 0 );
Jak wcześniej wspomniałem,
xml_document dziedziczy po
memory_pool oraz do alokacji dla konkretnego dokumentu należy użyć instancji nie innej niż tegoż dokumentu. Alokacja ma to do siebie, że może zostać wyrzucony wyjątek
bad_alloc.
Przydatna może się okazać jeszcze funkcja klonująca węzeł:
clone_node | Kopiuje węzeł oraz zawarte w nim węzły podrzędne i atrybuty. (metoda) |
---|
template < typename Ch = char >
xml_node < Ch >* memory_pool < Ch >::clone_node( const xml_node < Ch >* sourceNode, xml_node < Ch >* destinationNode = 0 );
Funkcja kopiuje dany węzeł ze wszystkimi potomkami i atrybutami, natomiast nazwy i wartości są współdzielone wskaźnikiem (jak ktoś chce to zmienić musi je osobno alokować).
Przykład programu generującego własny dokument XML
Opis trudniejszych fragmentów jak zwykle pod kodem.
#include <iostream>
#include <string>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
using namespace rapidxml;
int main()
{
xml_document <> document;
xml_node <>* html = document.allocate_node( node_element, "html" );
xml_node <>* head = document.allocate_node( node_element, "head" );
xml_node <>* metaHeader = document.allocate_node( node_element, "meta" );
xml_attribute <>* attribute = document.allocate_attribute( "http-equiv", "Content-Type" );
metaHeader->append_attribute( attribute );
metaHeader->append_attribute( document.allocate_attribute( "content", "text/html; charset=iso-8859-2" ) );
head->append_node( metaHeader );
head->append_node( document.allocate_node( node_element, "title", "Strona wygenerowana w C++" ) );
html->append_node( head );
document.append_node( html );
xml_node <>* body = document.allocate_node( node_element, "body" );
xml_node <>* paragraph = document.allocate_node( node_element, "p" );
const char * content1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fringilla vestibulum massa sed euismod.";
paragraph->value( document.allocate_string( content1 ) );
paragraph->append_attribute( document.allocate_attribute( "style", "text-align: center; color: red;" ) );
body->append_node( paragraph );
paragraph = document.clone_node( paragraph );
const char * content2 = "Phasellus id ante ipsum. Nunc at volutpat neque, vel iaculis metus. ";
paragraph->value( document.allocate_string( content2 ) );
body->append_node( paragraph );
xml_node <>* comment = document.allocate_node( node_comment, nullptr, "let's finally create content!" );
body->insert_node( paragraph->previous_sibling(), comment );
html->append_node( body );
cout << document;
}
Zacznę od wyjaśnienia nietechnicznego, skąd wziąłem ten tekst -na stronach internetowych zanim pojawi się treść często używa się tzw.
Lorem ipsum. W punkcie
(1) tworzę nowy węzeł, będący komentarzem, jak wiemy komentarze zawierają jedynie wartość, a nie zawierają nazwy, dlatego jako drugi argument podaję wartość
nullptr.
A nasza wygenerowana strona internetowa wygląda tak:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2"/>
<title>Strona wygenerowana w C++</title>
</head>
<body>
<!--jeszcze fajnie byloby dopisac 2 akapity-->
<p style="text-align: center; color: red;">Phasellus id ante ipsum. Nunc at volutpat neque, vel iaculis metus. </p>
<p style="text-align: center; color: red;">w tym akapicie</p>
</body>
</html>
Jeśli ktoś byłby zainteresowany bardziej fachową generacją stron internetowych w C++ odsyłam do innego mojego artykułu:
[C++, cgicc] Tworzenie strony Internetowej w C++ jako skrypt CGI.
Inne funkcje biblioteki
Pliki XML mogą być znacznie bardziej skomplikowane niż te, które znamy na codzień, dlatego szkoda byłoby, gdyby nie były obsługiwane dodatkowe aspekty, które jednak rzadko, bo rzadko, ale jednak mogą się pojawiać w dokumentach XML.
Aby poznać możliwości XMLa należałoby przeczytać o nim np.
na stronie konsorcium W3C. Czytając warto zwrócić uwagę na następujące tematy:
Tryby parsowania
We wszystkich powyższych przykładach zawsze używałem domyślnego trybu parsowania, jednak mamy ich znacznie więcej. Zależnie od trybu parsowania mamy wpływ na to, co chcemy wyszukiwać w naszym dokumencie XML. Oczywiście im więcej chcemy wykryć, tym wolniej dokonuje się parsowanie, dlatego najlepiej parsować tylko to, co wiemy, że będzie nas interesowało. Dostępne flagi parsowania:
parse_default | Używa domyślnych ustawień podczas przetwarzania tekstu. (stała) |
---|
parse_fastest | Przetwarza dane tak szybko, jak to możliwe bez utraty istotnych informacji. (stała) |
---|
parse_no_data_nodes | Wyłącza tworzenie węzłów rodzaju rapidxml::node_data podczas przetwarzania tekstu. (stała) |
---|
parse_no_element_values | Ignoruje wartości węzłów rodzaju rapidxml::node_element podczas przetwarzania tekstu. (stała) |
---|
parse_no_string_terminators | Nie umieszcza znaków zerowych w przetwarzanym tekście. (stała) |
---|
parse_no_entity_translation | Nie przekształca odwołań znakowych w przetwarzanym tekście. (stała) |
---|
parse_no_utf8 | Wyłącza obsługę UTF-8 i używa ośmiobitowych znaków przy przetwarzaniu tekstu. (stała) |
---|
parse_declaration_node | Włącza tworzenie węzłów rodzaju rapidxml::node_declaration podczas przetwarzania tekstu. (stała) |
---|
parse_comment_nodes | Włącza tworzenie węzłów rodzaju rapidxml::node_comment podczas przetwarzania tekstu. (stała) |
---|
parse_doctype_node | Włącza tworzenie węzłów rodzaju rapidxml::node_doctype podczas przetwarzania tekstu. (stała) |
---|
parse_pi_nodes | Włącza tworzenie węzłów rodzaju rapidxml::node_pi podczas przetwarzania tekstu. (stała) |
---|
parse_validate_closing_tags | Włącza sprawdzanie nazw znaczników zamykających podczas przetwarzania tekstu. (stała) |
---|
parse_trim_whitespace | Usuwa wszystkie białe znaki znajdujące się przed oraz za danymi węzłów. (stała) |
---|
parse_normalize_whitespace | Zamienia ciąg białych znaków na jedną spację podczas przetwarzania tekstu. (stała) |
---|
parse_non_destructive | Nie zmienia danych źródłowych podczas przetwarzania tekstu. (stała) |
---|
parse_full | Dąży do uzyskania jak największej ilości danych podczas przetwarzania tekstu. (stała) |
---|
int parse_default = 0;
int parse_no_data_nodes = 0x1;
int parse_no_element_values = 0x2;
int parse_no_string_terminators = 0x4;
int parse_no_entity_translation = 0x8;
int parse_no_utf8 = 0x10;
int parse_declaration_node = 0x20;
int parse_comment_nodes = 0x40;
int parse_doctype_node = 0x80;
int parse_pi_nodes = 0x100;
int parse_validate_closing_tags = 0x200;
int parse_trim_whitespace = 0x400;
int parse_normalize_whitespace = 0x800;
int parse_non_destructive = parse_no_string_terminators | parse_no_entity_translation;
int parse_fastest = parse_non_destructive | parse_no_data_nodes;
int parse_full = parse_declaration_node | parse_comment_nodes | parse_doctype_node | parse_pi_nodes | parse_validate_closing_tags;
Jak widać można je ze sobą łączyć.
Typy węzłów
Każdy element ma swój typ, który można sprawdzić, zmienić przy użyciu:
type. Dostępne typy węzłów znajdują się tutaj:
node_type.
Przykład na bardziej wyszukane parsowanie
Czymże byłoby powyższe wypisanie flag bez przykładu? Weźmy sobie przykładowy plik XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<!-- Nobody likes ceo -->
<![CDATA[
In the CDATA section we can use all sorts of reserved characters, even <, >, ", &and the document is still correct!
]]>
<!DOCTYPE note
[
<!ELEMENT manager (developer,tester)>
<!ELEMENT developer (#PCDATA)>
<!ELEMENT tester (#PCDATA)>
]>
<?processing instruction, named PI?>
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
<node>
<subnode>context</subnode>
</node>
<specialNode/>
</root>
Czas na przykłady parsowania przy użyciu paru wybranych flag parsowania:
Iteratory
Parser zawiera także iteratory dla węzłów i atrybutów, ale ich użycie nie jest wcale potrzebne. Dla zainteresowanych znajdują się one w pliku:
rapidxml_iterators.hpp. Są to iteratory dwukierunkowe:
node_iterator i
attribute_iterator. Przykład prostego ich użycia:
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
#include <rapidxml_utils.hpp>
#include <rapidxml_iterators.hpp>
using namespace rapidxml;
int main()
{
const char * fileName = "...";
unique_ptr < rapidxml::file <>> xmlFile;
xml_document <> document;
try
{
xmlFile.reset( new rapidxml::file <>( fileName ) );
document.parse < 0 >( xmlFile->data() );
}
catch( const parse_error & e )
{
std::cerr << e.what() << " here: " << e.where < char >() << std::endl;
return - 1;
}
catch( const exception & e )
{
cerr << e.what() << endl;
return - 2;
}
xml_node <>* root = document.first_node();
for_each( node_iterator < char >( root ), node_iterator < char >(),[]( auto & node )
{
cout << node.name() << " ";
for( attribute_iterator < char > it( & node ); it != attribute_iterator < char >(); ++it )
{
cout << it->name() << " = " << it->value() << ", ";
}
cout << endl;
} );
}
wydruk:
manager id = 1, name = Jan, working_years = 10,
manager id = 2, name = Jan, working_years = 10,
Dodatkowe informacje na temat biblioteki RapidXML
Prawa własności
Zostały jeszcze prawa własności do opisania. Sprawa jest prosta, sprowadza się do następujących rzeczy:
W momencie parsowania parser dąży do jak najszybszego parsowania, dlatego nie alokuje ani nie kopiuje tekstu, a jedynie trzyma do niego wskaźniki. Ponadto tekst, jeśli nie jest włączona flaga parse_non_destructive, jest modyfikowany przez parser (są wstawiane w odpowiednie miejsca znaki końca tekstu). Węzły i atrybuty nie roszczą sobie prawa własności do wskazywanego tekstu, dlatego go nie niszczą w obliczu własnej destrukcji.Wyłączenie wyrzucania wyjątków
Jest to możliwe poprzez makro:
RAPIDXML_NO_EXCEPTIONS, w razie błędu zostanie wtedy wywołana funkcja:
parse_error_handler.
Dla chcących dowiedzieć się więcej
Dla dociekliwych zachęcam do obejrzenia
dokumentacji oficjalnej, a także na tej właśnie stronie jest
porównanie szybkości parsowania, niestety sprzed paru lat.
Mankamenty biblioteki RapidXML
Wszystko co informatyczne ma pewne wady. Nie jest od nich wolna biblioteka RapidXML, część z nich poznaliśmy w tym artykule. Poważną niedogodnością jest fakt, iż biblioteka nie jest rozwijana od paru lat, czego konsekwencją jest np. konieczność modyfikacji pliku
rapidxml / rapidxml_print.hpp
.
Lekką niedogodnością jest brak funkcji do wyjmowania wartości w innej postaci niż
char *
, ale od czego mamy m.in.funkcję
std::stoi().
Jeszcze jednym mankamentem jest brak specjalnej obsługi
przestrzeni nazw, na szczęście dla łaknących takiej funkcjonalności powstał fork biblioteki z dodaną obsługą przestrzenią nazw zwanym RapidXMLns, do pobrania na swojej
stronie domowej. Biblioteka ta posiada m.in. następujące funkcje (i analogiczne do nich):
namespace rapidxml_ns
{
template < typename Ch = char >
Ch * xml_base < Ch >::prefix() const;
template < typename Ch = char >
std::size_t xml_base < Ch >::prefix_size() const;
template < typename Ch = char >
Ch const * xml_base::namespace_uri() const;
template < typename Ch = char >
xml_attribute < Ch >* xml_attribute::next_attribute_ns( const Ch * namespace_uri, const Ch * local_name, bool local_name_case_sensitive = true ) const;
template < typename Ch = char >
xml_node < Ch >* xml_node::first_node_ns( const Ch * namespace_uri, const Ch * local_name, bool local_name_case_sensitive = true ) const;
}
Co ciekawe używanie przestrzeni nazw można wyłączyć przy użyciu flagi:
const int parse_no_namespace = 0x1000;
Jeszcze jedną zaletą tej biblioteki jest fakt wprowadzenia poprawek (m.in. pliku
rapidxml/rapidxml_print.hpp).
Bonus na zakończenie -parsowanie stron internetowych
Jak wcześniej wspominałem niektóre strony internetowe da się parsować parserem XML, są takie, które wymagają tylko drobnych modyfikacji treści, aby dokonać parsowania (przykładowo strona
Niepodam.pl daje się parsować po wyrzuceniu
<script>...</script>
). Co jeśli proste zabiegi nie pomogą, a bardzo zależy nam na parsowaniu strony?
Parsowanie danych przy użyciu API
Jeśli dana strona nie daje się parsować, a nie chcemy robić skomplikowanych operacji na tekście, warto sprawdzić, czy dana strona oferuje API. Używanie API jest znacznie lepszym pomysłem niż bezpośrednie parsowanie strony, gdyż strona może się w każdej chwili zmienić, a API z założenia rzadziej się zmienia, a nawet gdyby się zmieniło znacznie łatwiej jest dostosować się do zmian API niż strony internetowej.
Bywa, że dostępu do API wymaga rejestracji, na szczęście nie na każdej stronie.
Wzorem dla większości stron internetowych jest wg mnie
Wikipedia.pl, która udostępnia rozbudowane
https://www.mediawiki.org/wiki/API:Main_page. Co fajne dla nas oferuje ona wiele formatów przesyłania odpowiedzi, w tym format XML.
Pobieranie strony internetowej
Tutaj musimy się zaopatrzyć w odpowiednią bibliotekę, dobry interfejs do pobierania stron internetowych z poziomu C++ oferują m.in.
https://stackoverflow.com/questions/4383864/downloading-file-in-qt-from-url,
https://pocoproject.org/docs/Poco.Net.html. Jednak zdecydowałem się na użycie
libCurl.
Dla Windowsa na tym serwisie jest opis jak instalować biblioteki w Code:Block
[C++] Instalacja bibliotek w Code::Blocks, jeśli ktoś by się zdecydował na VS, oto
instrukcje. Dla Linuxa wystarczy
sudo apt-get install curl
.
Przykładowy kod parsujący tekst zwrócony przez API Wikipedii
#include <iostream>
#include <memory>
#include <string>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
using namespace rapidxml;
#include <curl/curl.h>
int bufferMoreData( char * data, size_t size, size_t nmemb, string * buffer )
{
int result = 0;
if( buffer != NULL )
{
buffer->append( data, size * nmemb );
result = size * nmemb;
}
return result;
}
unique_ptr < char[] > downloadPage( const char * page2Download )
{
string buffer;
buffer.reserve( 10000 );
bool problemOccured = false;
do
{
CURL * curl;
CURLcode result;
curl = curl_easy_init();
if( curl )
{
curl_easy_setopt( curl, CURLOPT_URL, page2Download );
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, bufferMoreData );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, & buffer );
result = curl_easy_perform( curl );
if( CURLE_OK == result )
{
problemOccured = false;
}
else
{
cout << "!Problem occured during downloading website: " << page2Download << "!\n";
problemOccured = true;
}
curl_easy_cleanup( curl );
}
}
while( problemOccured );
long long N = buffer.length();
unique_ptr < char[] > out( new char[ N + 1 ] );
buffer.copy( out.get(), N + 1 );
out[ N ] = '\0';
return out;
}
template < typename Ch = char >
char * getPageContent( xml_document < Ch >& document )
{
xml_node < Ch >* nodeApi = document.first_node();
xml_node < Ch >* nodeParse = nodeApi->first_node();
xml_node < Ch >* nodeText = nodeParse->first_node();
return nodeText->value();
}
template < typename Ch = char >
string getTextWithoutHiperlinks( xml_node < Ch >* node )
{
string outputText;
for( auto child = node->first_node(); child; child = child->next_sibling() )
{
if( rapidxml::node_data == child->type() )
{
outputText += child->value();
}
else if( rapidxml::node_element == child->type() )
{
outputText += child->value();
}
}
return outputText;
}
template < typename Ch = char >
xml_node < Ch >* getNodeWithSententias( xml_document < Ch >& document )
{
xml_node < Ch >* nodeDiv = document.first_node( "div" );
return nodeDiv->first_node( "ul" );
}
int main()
{
const char * pageUrlAddress = "https://pl.wikipedia.org/w/api.php?action=parse&format=xml&page=Izydor%20z%20Sewilli&prop=text";
unique_ptr < char[] > wikipediaApiXmlPage = downloadPage( pageUrlAddress );
xml_document <> dokument;
try
{
dokument.parse < 0 >( wikipediaApiXmlPage.get() );
}
catch( const parse_error & p )
{
cerr << p.where < char >() << ": " << p.what() << endl;
return - 1;
}
char * pageContent = getPageContent( dokument );
xml_document <> documentInner;
try
{
documentInner.parse < 0 >( pageContent );
}
catch( const parse_error & p )
{
cerr << p.where < char >() << ": " << p.what() << endl;
return - 2;
}
const auto nodeWithSententias = getNodeWithSententias( documentInner );
for( auto nodeLi = nodeWithSententias->first_node( "li" ); nodeLi; nodeLi = nodeLi->next_sibling( "li" ) )
{
cout << getTextWithoutHiperlinks( nodeLi ) << endl;
}
}
Output trzeba pozyskać na własną rękę, ale warto.
Aby zrozumieć ten kod trzeba wiedzieć, jak wygląda tekst zwrócony dla powyższego
https://pl.wikipedia.org/w/api.php?action=parse&format=xml&page=Izydor%20z%20Sewilli&prop=text. Opiszę jednak dwie rzeczy
(1) można było uniknąć jednego kopiowania tablicy znaków, jeśli nie używałbym
std::string
kosztem czytelności.
W tekście przeszkadzały mi hiperłącza, dlatego w taki
(2) sposób się ich pozbywam, oczywiście można też było uniknąć kopiowania.
Patrząc na strukturę kodu strony powinienem skakać do każdego elementu
<h2>
, następnie sprawdzać wartość pierwszego
<span>
i w razie natrafienia na
Sentencje z dzieł dopiero wtedy przeskoczyć na element
<ul>
-to byłoby bezpieczniejsze w perspektywie przyszłości, ja poszedłem na skróty.
Pojawia się jeszcze pytanie, czemu parsuję dwukrotnie? Proszę zobaczyć na wygląd kodu zwróconego przez API Wikipedii -tam zamiast
< i
> są encje takie jak odpowiednio:
< i
>, natomiast w momencie zawołania
value zwrócony tekst posiada zamienione encje na właściwe znaki.
Parsowanie danych gdy nie ma ani API ani kod strony nie daje się parsować
Innymi słowy, jeśli strona nie jest zgodna z formatem XML. Wydaje się, że nie da się nic zrobić, a jednak jest wyjście -istnieje biblioteka w języku C, która konwertuje strony na format XHTML, nazywa się
HTML Tidy.
Instalacja biblioteki Tidy HTML
Instrukcje jak zainstalować bibliotekę znajduje się w pliku
README/BUILD.md, wcześniej jednak trzeba sobie pobrać i rozpakować.
Oto przykład wypisujący
artykuły serwisu Cpp0x.pl, oczywiście przed dokonaniem parsowania należy stronę naprawić przy użyciu Tidy.
#include <iostream>
#include <fstream>
#include <memory>
#include <string>
#include <algorithm>
using namespace std;
#include <rapidxml.hpp>
#include <rapidxml_print.hpp>
using namespace rapidxml;
#include <cstdio>
#include <cerrno>
#include <tidy/tidy.h>
#include <tidy/tidybuffio.h>
#include <curl/curl.h>
int bufferMoreData( char * data, size_t size, size_t nmemb, string * buffer )
{
int result = 0;
if( buffer != NULL )
{
buffer->append( data, size * nmemb );
result = size * nmemb;
}
return result;
}
unique_ptr < char[] > downloadPage( const char * page2Download )
{
string buffer;
buffer.reserve( 10000 );
bool problemOccured = false;
do
{
CURL * curl;
CURLcode result;
curl = curl_easy_init();
if( curl )
{
curl_easy_setopt( curl, CURLOPT_URL, page2Download );
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, bufferMoreData );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, & buffer );
result = curl_easy_perform( curl );
if( CURLE_OK == result )
{
problemOccured = false;
}
else
{
cout << "!Problem occured during downloading website: " << page2Download << "!\n";
problemOccured = true;
}
curl_easy_cleanup( curl );
}
}
while( problemOccured );
long long N = buffer.length();
unique_ptr < char[] > out( new char[ N + 1 ] );
buffer.copy( out.get(), N + 1 );
out[ N ] = '\0';
return out;
}
unique_ptr < char[] > websiteSource2Xhtml( const char * websiteContent )
{
TidyBuffer output = { }, errbuf = { };
TidyDoc tdoc = tidyCreate();
int rc = - 1;
Bool ok = tidyOptSetBool( tdoc, TidyXhtmlOut, yes );
if( ok )
rc = tidySetErrorBuffer( tdoc, & errbuf );
if( rc >= 0 )
rc = tidyParseString( tdoc, websiteContent );
if( rc >= 0 )
rc = tidyCleanAndRepair( tdoc );
if( rc >= 0 )
rc = tidyRunDiagnostics( tdoc );
if( rc > 1 )
rc =( tidyOptSetBool( tdoc, TidyForceOutput, yes ) ? rc
: - 1 );
if( rc >= 0 )
rc = tidySaveBuffer( tdoc, & output );
if( rc < 0 )
{
tidyRelease( tdoc );
tidyBufFree( & errbuf );
tidyBufFree( & output );
throw runtime_error( "A severe error occurred with code: " + to_string( rc ) );
}
unique_ptr < char[] > websiteXhtml( new char[ output.size + 1 ] );
strncpy( websiteXhtml.get(), reinterpret_cast < char *>( output.bp ), output.size + 1 );
tidyRelease( tdoc );
tidyBufFree( & errbuf );
tidyBufFree( & output );
return websiteXhtml;
}
template < typename Ch = char >
auto getDivWithArticles( xml_document < Ch >& document )
{
auto nodeHtml = document.first_node( "html" );
auto nodeBody = nodeHtml->first_node( "body" );
auto nodeForm = nodeBody->first_node( "form" );
auto nodeDiv = nodeForm->first_node( "div" );
for( auto n = nodeDiv->first_node( "div" ); n; n = n->next_sibling( "div" ) )
{
auto classAttribute = n->first_attribute( "class" );
if( classAttribute && 0 == strcmp( "Article", classAttribute->value() ) )
{
return n;
}
}
throw invalid_argument( "No articles" );
}
char * removeNewlines( char * text )
{
const auto textSize = strlen( text );
std::replace_if( text, text + textSize,[]( auto letter )
{
return '\n' == letter || '\r' == letter;
}, ' ' );
return text;
}
template < typename Ch = char >
bool hasH2ClassStatic( const xml_node < Ch >* node )
{
if( strcmp( "h2", node->name() ) )
return false;
const auto attributeClass = node->first_attribute( "class" );
return attributeClass && 0 == strcmp( "Static", attributeClass->value() );
}
template < typename Ch = char >
void printAllHiperlinkValues( xml_node < Ch >* node )
{
if( nullptr == node || hasH2ClassStatic( node ) )
{
return;
}
for( auto n = node->first_node(); n; n = n->next_sibling() )
{
if( rapidxml::node_element == n->type() )
{
if( 0 == strcmp( "a", n->name() ) )
{
static unsigned articleCounter;
char * articleTitle = n->value();
cout << ++articleCounter << ")\t" << removeNewlines( n->value() ) << '\n';
}
else
{
printAllHiperlinkValues( n );
}
}
}
}
int main( int argc, char ** argv )
{
unique_ptr < char[] > pageSource, fixedPageSource;
xml_document <> document;
try
{
pageSource = downloadPage( "http://cpp0x.pl/artykuly/" );
fixedPageSource = websiteSource2Xhtml( pageSource.get() );
document.parse < 0 >( fixedPageSource.get() );
}
catch( const parse_error & e )
{
std::cerr << e.what() << " here: " << e.where < char >() << std::endl;
return - 1;
}
catch( const std::exception & e )
{
std::cerr << e.what() << std::endl;
return - 2;
}
auto nodeArticles = getDivWithArticles( document );
for( auto divWithArticleCategory = nodeArticles->first_node( "div" ); divWithArticleCategory; divWithArticleCategory = divWithArticleCategory->next_sibling( "div" ) )
{
auto attributeClass = divWithArticleCategory->first_attribute( "class" );
if( attributeClass && 0 == strcmp( "DispCateg", attributeClass->value() ) )
{
printAllHiperlinkValues( divWithArticleCategory );
}
}
}
Czas na wyjaśnienie, oczywiście nie będę uzasadniał pewnego przechodzenia, które wynika z obecnej struktury strony
Cpp0x.pl. Jeśli chodzi o
(1) typ jest
byte *
, dlatego konieczne jest rzutowanie.
W trybie DEBUG niestety biblioteka Tidy bardzo pluje po ekranie wszystkie logi, na moje oko nie da się tego wyłączyć bez modyfikacji kodu tejże biblioteki. |