Wstęp
W pracy zawodowej związanej z branżą IT, nie ma chyba osoby, która prędzej czy później nie spotkałaby się dokumentami typu XML i HTML. W takim przypadku zwykle zachodzi potrzeba „wyciągnięcia” z pliku konkretnych danych – tutaj język XPath okazuje się nieoceniony. W tym artykule postaram się wam pokazać jak potężnym sojusznikiem może być w pracy testera/programisty, a także jak szybko jego nieodpowiednie zastosowanie może się na nas zemścić.
Poniższe przykłady będą dotyczyć wyszukiwania elementów na stronie internetowej – w tym celu sugeruję zaopatrzenie się w przeglądarkę Firefox, wraz z dwiema wtyczkami – FireBug i FirePath. Oczywiście nie jest to niezbędne do korzystania z XPath’ów, możesz też otworzyć Developer Tools w Chrome (F12), otworzyć pole wyszukiwarki (Ctrl + F) na zakładce Elements i tam wpisywać treść.
Jeśli jednak zaczynasz swoją przygodę z tym językiem, to zdecydowanie polecam Ci zestaw dla Firefox’a – będzie o wiele wygodniej.
Zaznaczanie elementu (względne i bezwzględne)
XPath – tak jak każdy inny język – charakteryzuje pewna gramatyka, słowa kluczowe. W tym przypadku nie ma ich tak wiele, natomiast jeżeli chcemy efektywnie posługiwać się tym językiem, to ich przyswojenie będzie niezbędne. Podstawowym elementem będzie / (slash) – pojedynczy i podwójny.
// – Oznacza zaznaczenie elementu bez względu na jego położenie
/ – Oznacza zaznaczenie elementu znajdującego się pod elementem nadrzędnym
Wszystko wyjaśni się na konkretnym przykładzie:
//div
Tak jak wspominałem, podwójny slash oznacza ścieżkę bezwzględną do elementu – w praktyce użyte polecenie zwróciło nam wszystkie znaczniki DIV, bez względu na ścieżkę do nich. Ta wiedza będzie dla nas niezbędna w bardziej zaawansowanych przykładach.
html/body/div/div
Pojedynczy slash pozwala nam ustalić element, którego ścieżka jest zależna od innych znaczników w modelu DOM. Chcąc otrzymać kontener zawierający treść strony, użyłem ścieżki zaczynającej się od znacznika HTML, poprzez BODY, następnie DIV’a będącego jednocześnie dzieckiem BODY, żeby na końcu wreszcie dostać się do DIV’a, który nas interesuje. Jak sami widzicie na powyższym przykładzie, utworzenie takiej ścieżki ręcznie ma bardzo mało wspólnego z efektywnością – natomiast czas potrzebny na wygenerowanie takiej ścieżki dla elementu znajdującego się głęboko w modelu DOM, to nie jedyne zmartwienie. Dużo większym problemem jest przypadek, kiedy zastosujemy taki XPath w kodzie programu – wystarczy niewielka zmiana w naszym drzewku (np. dodanie bezpośrednio pod BODY kolejnego DIV’a), aby nasz XPath przestał działać. W ten sposób utrzymanie automatycznych skryptów testowych, w których obiekty definiowane są za pomocą XPath’ów utworzonych w ten sposób, staje się koszmarem. Pojedynczego znaku slash należy zatem używać rozsądnie.
Atrybuty
Zarówno znaczniki HTML jak i elementy dokumentu XML mogą posiadać atrybuty. Najczęściej spotykane atrybuty dla znaczników HTML to id, class, style, src, href. Mogą one uchronić nas od wspomnianego koszmaru (względna ścieżka do elementu zaczynająca się od znacznika HTML) jeżeli zostaną odpowiednio wykorzystane.
@Nazwa_Atrybutu – Oznacza pobranie interesującego nas atrybutu
Załóżmy, że naszym celem jest znalezienie wszystkich liczników komentarzy na stronie głównej. W takim wypadku możemy użyć następującej komendy:
//*[@class=’comments-count’]
Przeanalizujmy krótko powyższe zapytanie – podwójny slash na początku oznacza, że szukamy dowolnego elementu (* oznacza dowolny element. Nic nie stoi na przeszkodzie, żebyśmy zastąpili gwiazdkę znacznikiem „span”) który posiada atrybut o nazwie „class” i wartości „comments-count” (Ważne – w takim przypadku wartość atrybutu musi zgadzać się w 100%, tzn. że jeżeli będziemy mieć przypadek elementu z trzema klasami, to musimy wpisać tutaj dokładnie wszystkie trzy. Opcja Contains() będzie opisana później). Na podobnej zasadzie możemy użyć dowolnego atrybutu.
Ciekawostka – możemy też wybrać element który będzie po prostu zawierał dany atrybut, bez względu na jego zawartość. W praktyce może to wyglądać następująco:
//*[@checked]
Powyższa komenda zwróci nam wszystkie elementy, bez względu na ich położenie, zawierające atrybut „checked”, bez względu na jego zawartość.
Numer porządkowy
Jeżeli przeczytałeś akapit dotyczący atrybutów, to zapewne nie będzie dla Ciebie problemem zaznaczenie wszystkich artykułów na stronie głównej. Dodajmy jednak wymaganie, które określa, że interesuje nas tylko ostatnio dodany artykuł. Aby to osiągnąć wystarczy niewielka modyfikacja – dodanie na końcu nawiasów kwadratowych, z numerem porządkowym:
//*[@itemprop=’blogPost’][1]
Dzięki zastosowaniu tego prostego zabiegu, zawsze otrzymamy kontener z ostatnio dodanym postem, jednocześnie pomijając zabiegi które musielibyśmy użyć w kodzie takie jak np sortowanie. Większa efektywność, większa stabilność, mniejsza redundancja – same plusy
Ciekawostka – w nawiasach kwadratowych możemy wpisać nie tylko numer porządkowy. Wrzucenie funkcji „last()”, spowoduje zaznaczenie ostatniego artykułu.
//*[@itemprop=’blogPost’][last()]
W nawiasach kwadratowych można również stosować działania matematyczne:
//*[@class=’comments-count’][text() < (10-2)]
Powyższy przykład jest może trochę przekoloryzowany, ale warto wiedzieć, że kiedy będziecie mieli potrzebę zastosowania tam jakiegoś wyrażenia matematycznego, to taka możliwość istnieje.
Łączenie XPath’ów
Zdarzają się sytuację, w których potrzebujemy wyciągnąć kilka różnych elementów jednym zapytaniem. Oczywiście można złapać każdy interesujący nas element osobnym XPath’em, a następnie „skleić” je w kodzie i będzie działać – ale bardziej optymalnie będzie użyć symbolu | (pipe).
| (Pipe) – pozwala na połączenie ze sobą rezultatów dwóch lub więcej XPath’ów.
Praktycznym przykładem, będzie pobranie pierwszego i ostatniego artykułu za pomocą dwóch połączonym XPath’ów. W jaki sposób pobrać je osobno, mięliśmy okazję nauczyć się w przykładach powyżej – wszystko co musimy teraz zrobić, to połączyć je ze sobą za pomocą pipe’a:
//*[@itemprop=’blogPost’][1] | //*[@itemprop=’blogPost’][last()]
Tak zastosowany XPath zwróci nam zawsze kontenery zawierające pierwszy i ostatni post na stronie, bez względu na ilość pozostałych. Mamy też możliwość dołączenia do tej komendy kolejnego pipe’a wraz z nowym XPath’em – możemy to robić właściwie bez ograniczeń (tutaj też należy pamiętać, aby używać tego rozwiązania z rozsądkiem. Zbyt długie XPath’y stają się mało czytelne i koszmarne w utrzymaniu).
Contains()
W przykładach dotyczących atrybutów, wspominałem o sytuacji w której dany znacznik posiada więcej niż jedną klasę – nawet zakładając, że każda z nich była na tyle unikalna by jej użyć, to mimo wszystko musieliśmy wpisywać dokładnie wszystkie trzy, aby je porównać. Naprzeciw naszym oczekiwaniom wychodzi funkcja Contains()
Contains(@Nazwa_Atrybutu, Szukana_Fraza) – daje nam możliwość odnalezienia elementu, porównując częściową zawartość jego atrybutu.
Istotnym elementem w tym wypadku jest nieco inna składnia dla funkcji Contains(). Zobaczymy to na praktycznym przykładzie – zakładamy, że chcemy pobrać listę dat publikacji postów ze strony głównej.
//time[contains(@class, 'entry-date’)]
Zwróć uwagę na zawartość atrybutu class dla wybranego przez nas elementu. Posiada on trzy różne klasy – dzięki contains() mamy możliwość złapania interesujących nas elementów poprzez podanie tylko jednej z nich.
Text()
Skoro znamy już funkcję Contains(). to należy wspomnieć kilka słów o innej funkcji, która idzie z nią bardzo często w parze – text().
Text() – pozwala na pobranie tekstu zaznaczonego elementu
Od razu na wstępie chciałbym Ci przypomnieć o rozsądku przy używaniu tej funkcji – musisz wziąć pod uwagę, że w tym wypadku będziesz wyszukiwać element po treści. Z doświadczenia wiem, że istnieje dużo większe prawdopodobieństwo na zmianę treści, niż id czy nazwy klasy.
Ważne – text() jest funkcją, ale składniowo używamy go podobnie jak atrybutu – należy przy tym pamiętać, żeby nim dodawać przed nim symbolu @.
//*[text() = 'Pierwsze kroki w Świecie Testów.’]
Powyższe zapytanie zwróciło nam element zawierający tekst dokładnie wpisany przez nas. Moglibyśmy przerobić takie zapytanie na funkcję contains(), co umożliwiłoby nam znalezienie elementu po fragmencie tekstu.
Funkcja text() ma też inne zastosowanie, niż wyszukiwanie elementów. Użyta w innej formie, spowoduje zwrócenie do programu nie obiektu, a zmiennej typu string.
//*[contains(@id, 'menu-item’)][last()]/a/text()
Przeanalizujmy krótko powyższą komendę – szukamy ostatniego elementu z wszystkich istniejących, z dowolnym znacznikiem, w dowolnym miejscu, który będzie posiadał atrybut id częściowo zawierający treść „menu-item”. Następnie przechodzimy do jego dziecka o znaczniku „a” i na końcu przechodzimy do tekstu tego znacznika. Zwróć uwagę, na zaznaczenie elementu w zakładce FirePath – wyraźnie widać, że został on zawężony tylko do tekstu znacznika a, nie zaś całego obiektu.
Operatory logiczne
Kolejną często przydatną opcją w języku XPath, jest możliwość stosowania operatorów logicznych (and, or). Jako realny przypadek można założyć, że mamy dwie listy pól typu checkbox. Pierwsza lista ma klasę „List1”, natomiast druga „List2”. Zaznaczony checkbox otrzymuje dodatkowo klasę checked. W takim przypadku wyciągnięcie wszystkich zaznaczonych checkboxów z listy pierwszej będzie wyglądało następująco:
//*[contains(@class, 'List1′) and contains(@class, 'checked’)]
Operatory logiczne and i or nie mają swoich symboli w języku XPath i należy wpisywać je słownie jak na przykładzie powyżej.
Following Sibling
XPath ma duże możliwości dostania się do obiektu na podstawie dziedziczenia. Nie będę w tym tutorialu omawiał wszystkich z nich, natomiast following-sibling uważam za bardzo praktyczny i często używany, wobec tego pozwoliłem sobie o nim wspomnieć.
Following-sibling – daje nam możliwość zaznaczenia „brata” (elementu będącego obok, na tym samym poziomie w modelu DOM).
Tworząc automatyczne skrypty testowe, bardzo często spotykam się z sytuacją w której potrzebny mi element nie posiada żadnej rozsądnej opcji pobrania go – natomiast element na tym samym poziomie, będący tuż obok posiada piękny, unikalny na stronie atrybut ID. W takim wypadku z pomocą przyjdzie nam following-sibling. Zwróć proszę uwagę składnię.
//*[@class=’entry-date entry-meta-element published’]/following-sibling::span
Przeanalizujmy powyższą komendę – szukamy wszystkich elementów o podanych trzech klasach (otrzymamy elementu z datami), a następnie przechodzimy do najbliższego elementu na tym samym poziomie o znaczniku span. Moglibyśmy zamiast span dać gwiazdkę, co spowodowałoby zaznaczenie następnego elementu o dowolnym znaczniku.
Podsumowanie
Powyższy tutorial opisuje tylko fragment możliwości języka Xpath – jednocześnie podane przeze mnie rozwiązania, są najczęściej używanymi podczas pisania testów automatycznych czy parsowania plików XML w kodzie. Dobrze napisany XPath potrafi być niemal tak dokładny jak atrybut ID. Mało tego, często jest po prostu znacznie lepszy (np. przy pobieraniu listy elementów). Jednak jeszcze raz powtórzę to, na co naciskałem w całym tekście – XPath musi być przemyślany i stosowany z rozsądkiem.