Рассмотрим пример использования Selenium на Scala, отвечая на вопрос "Сколько человек в каждой футбольной сборной имеют более одного гражданства?"
За основу возьмем данные с сайта transfermarkt.com:
-
На странице игрока есть поле Citizenship, которое предоставляет необходимую информацию
List of football/soccer teams
Переход на заданную страницу, если она реализует trait
org.scalatestplus.selenium.Page
, может осуществляться
так:
import org.scalatestplus.selenium.Pageimport org.scalatestplus.selenium.WebBrowser._import org.openqa.selenium.WebDriverimplicit def webDriver: WebDriver = ??? /* from container */class RankingListPage(implicit val webDriver: WebDriver) extends Page { val url = "https://www.transfermarkt.com/statistik/weltrangliste/statistik"}val rankingListPage = new RankingListPage()go to rankingListPage
После перехода прежде чем работать со страницей необходимо дождаться окончания отрисовки её элементов. Будем ориентироваться на кнопку Compact и дождемся, когда она станет видима.
Xpath локатор кнопки будет таким:
import org.scalatestplus.selenium.WebBrowser._val compactTab: Query = xpath("//div[.='Compact']")
Ожидание видимости элемента осуществляется так (timeout можно задать в конфиге, query - заданный элемент):
import org.openqa.selenium._import org.openqa.selenium.support.ui.WebDriverWaitimport org.openqa.selenium.support.ui.ExpectedConditionsimport org.scalatestplus.selenium.WebBrowser._import java.time.Durationdef waitVisible(query: Query, timeout: Int)(implicit webDriver: WebDriver): WebElement = new WebDriverWait(webDriver, Duration.ofSeconds(timeout)).until(ExpectedConditions.visibilityOfElementLocated(query.by))
Рассмотрим переход на закладку Compact.
Переход на закладку будет состоять из следующих шагов:
-
Проверяем, активна ли закладка (активная закладка в данном случае в атрибуте class содержит "active").
-
Если да, то ничего не делаем переход осуществлен.
-
Если нет, то кликаем на закладку и ждём, когда закладка станет активна.
Проверить, что элемент query: Query содержит заданное значение в атрибуте class можно так:
def doesClassContain(value: String): Boolean = (for { element <- find(query) attribute <- element.attribute("class") } yield attribute.contains(value)).contains(true)
Кликнуть на элемент можно так: clickOn(query)
Ожидание, когда атрибут class элемента будет содержать заданное значение, можно реализовать так:
def waitClassContain(value: String): Boolean = new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(ExpectedConditions.attributeContains(query.by, "class", value))
Итого:
def clickCompact(): Unit = if (!compactTab.doesClassContain("active")) { clickOn(compactTab) val _ = compactTab.waitClassContain("active") }
Теперь, когда мы находимся на закладке Compact можно начать вычисление списка всех сборных.
Для этого нужно последовательно обойти все страницы рейтинга и с каждой считать список.
Логика будет такой:
-
Проверяем, достигли ли последней страницы
-
Если нет, считываем список со страницы, переходим на следующую и возвращаемся на пункт выше
-
Если дошли до последней страницы, то считываем список с неё
Для того, чтобы проверить, достигли ли мы последней страницы,
достаточно проверить, есть ли кнопка перехода на следующую страницу
(см. скрин выше, css локатор li.naechste-seite >
a
):
val nextPageLink: Query = cssSelector("li.naechste-seite > a")def isPresent: Boolean = find(nextPageLink).isDefined
Для того, чтобы считать список стран, необходимо найти все
элементы с xpath локатором
//table/tbody//a[count(*)=0]
и у каждого элемента
считать text и значение атрибута
href (или
//table/tbody/tr[td[.='CONMEBOL']]//a[count(*)=0]
-
если интересна только одна конфедерация, например, самая маленькая
- CONMEBOL(Южная Америка)):
val itemLink: Query = xpath("//table/tbody//a[count(*)=0]")def items(): Seq[(String, Option[String])] = findAll(itemLink).map(el => (el.text.trim, el.attribute("href"))).toSeq
Для перехода на следующую страницу мало кликнуть на кнопку nextPageLink, необходимо ещё дождаться, когда этот переход произойдет.
Чтобы удостовериться, что мы перешли на следующую страницу, мы
можем считать номер текущей страницы (css локатор li.selected
> a
), а после клика на
nextPageLink дождаться, когда номер
текущей страницы станет на 1 больше:
val selectedPageLink: Query = cssSelector("li.selected > a")def clickNextPage(): Unit = { val nextPage = find(selectedPageLink).map(_.text).get().toInt + 1 clickOn(nextPageLink) val _ = webDriverWait(driver).until(ExpectedConditions.textToBe(selectedPageLink.by, nextPage.toString))}
Соединяем все воедино и получаем следующий список (на 13 марта 2021):
Теперь, когда у нас есть url страны можно составить список игроков сборной.
Эта задача ещё более простая, потому что страница сборной страны содержит практически все те же необходимые нам элементы, что и страница рейтинга сборных. Изменения будут касаться только элемента itemLink (ссылка на игрока).
Ссылка на страницу игрока - это xpath локатор
//table/tbody//span[@class='hide-for-small']/a[count(*)=0]
:
val itemLink: Query = xpath("//table/tbody//span[@class='hide-for-small']/a[count(*)=0]")
Осталось только определить гражданство игрока сборной, пользуясь его страницей, найденной на предыдущем этапе.
Для начала нужно перейти на закладку Profile.
Мы уже переходили на закладку на предыдущих страницах. Здесь будет то же самое, за исключением одного нюанса: если раньше у активной закладки менялся атрибут class,то теперь этот атрибут меняется не у самой ссылки, а у её родителя. Кстати, в этот раз немецкие разработчики сайта значение атрибута class активного элемента не стали переводить на английский, а оставили на немецком - "aktiv":
Создадим два элемента: ссылку и её родителя, а затем определим переход на закладку так:
val profileTab: Query = xpath("//li[@id='profile']")val profileLink: Query = xpath("//li[@id='profile']/a")def clickProfile(): Unit = if (!profileTab.doesClassContain("aktiv")) { clickOn(profileLink) val _ = profileTab.waitClassContain("aktiv") }
Теперь осталось только определить гражданство. Для этого возьмем элемент img из строки Citizenship: и считаем её атрибут title:
val citizenshipImg: Query = xpath("//th[.='Citizenship:']/following-sibling::td/img")def citizenship(): Seq[String] = findAll(citizenshipImg).flatMap(_.attribute("title")).toSeq
Вот и все, все страницы заполнены, осталось только написать автотест и тогда получим следующий результат.
Results (for Russia, Ukraine and Belarus)
Country name |
% |
Foreigners |
Russia |
11% (3/28) |
(Brazil (1) -> (Mrio Fernandes), Kyrgyzstan (1) -> (Ilzat Akhmetov), Germany (1) -> (Roman Neustdter)) |
Ukraine |
9% (3/33) |
(Brazil (2) -> (Marlos, Jnior Moraes), Hungary (1) -> (Igor Kharatin)) |
Belarus |
4% (1/25) |
(Cameroon (1) -> (Maks Ebong)) |
В наших сборных только 3 натурализованных игрока (и все из Бразилии). Остальные родились в СССР.
Results (for CONMEBOL)
Country name |
% |
Foreigners |
Brazil |
36% (9/25) |
(Spain (3) -> (Casemiro, Bruno Guimares, Vincius Jnior), Italy (1) -> (Alex Telles), France (1) -> (Thiago Silva), Portugal (4) -> (Ederson, Marquinhos, Allan, Lucas Paquet)) |
Argentina |
57% (13/23) |
(Spain (2) -> (Gonzalo Montiel, Lionel Messi), Italy (11) -> (Lucas Martnez Quarta, Wlter Kannemann, Nicols Tagliafico, Guido Rodrguez, Rodrigo de Paul, Giovani Lo Celso, Nicols Domnguez, ngel Di Mara, Joaqun Correa, Papu Gmez, Lucas Alario)) |
Uruguay |
51,5% (18/35) |
(Spain (7) -> (Jos Mara Gimnez, Sebastin Coates, Diego Godn, Agustn Oliveros, Damin Surez, Lucas Torreira, Federico Valverde), Paraguay (1) -> (Rodrigo Muoz), Italy (10) -> (Fernando Muslera, Martn Campaa, Sergio Rochet, Matas Via, Franco Pizzichillo, Nahitan Nndez, Matas Vecino, Giorgian de Arrascaeta, Diego Rossi, Cristhian Stuani)) |
Colombia |
22% (6/27) |
(Spain (4) -> (Jeison Murillo, Johan Mojica, James Rodrguez, Luis Surez), Argentina (1) -> (Frank Fabra), England (1) -> (Steven Alzate)) |
Chile |
21% (5/24) |
(Haiti (1) -> (Jean Beausejour), Spain (3) -> (Claudio Bravo, Gary Medel, Fabin Orellana), Italy (1) -> (Luis Jimnez)) |
Peru |
33% (12/36) |
(Venezuela (1) -> (Carlos Ascues), Spain (3) -> (Alexander Callens, Cristian Benavente, Sergio Pea), Uruguay (1) -> (Gabriel Costa), Italy (2) -> (Luis Abram, Gianluca Lapadula), Netherlands (1) -> (Renato Tapia), Switzerland (1) -> (Jean-Pierre Rhyner), Portugal (1) -> (Andr Carrillo), Croatia (1) -> (Ral Ruidaz), Lebanon (1) -> (Matas Succar)) |
Venezuela |
25% (7/28) |
(Spain (4) -> (Roberto Rosales, Juanpi Aor, Darwin Machs, Fernando Aristeguieta), Switzerland (1) -> (Rolf Feltscher), England (1) -> (Luis Del Pino Mago), Colombia (1) -> (Jan Hurtado)) |
Paraguay |
21% (7/33) |
(Spain (1) -> (Antonio Sanabria), Argentina (4) -> (Santiago Arzamendia, Gastn Gimnez, Andrs Cubas, Ral Bobadilla), Italy (2) -> (Antony Silva, Ivn Piris)) |
Ecuador |
12% (4/33) |
(Spain (3) -> (Erick Ferigra, Pervis Estupin, Leonardo Campana), Argentina (1) -> (Hernn Galndez)) |
Bolivia |
25% (7/28) |
(United States (2) -> (Adrin Jusino, Antonio Bustamante), Spain (1) -> (Jaume Cullar), Argentina (1) -> (Carlos Lampe), Brazil (1) -> (Marcelo Moreno), Switzerland (1) -> (Boris Cespedes), Portugal (1) -> (Erwin Snchez)) |
А вот в Южной Америке людей с двойным гражданством довольно много. Впрочем, это неудивительно: в чемпионатах Евросоюза жесткий лимит на легионеров (в заявке только 3 игрока с гражданством не ЕС), поэтому южноамериканцам, чтобы попасть в Европу, приходится либо пытаться получить гражданство бывшей митрополии (Бразилия -> Португалия, остальные -> Испания), либо искать среди своих предков итальянцев. Второе не так сложно, как кажется. Во время Второй Мировой войны Южная Америка хоть и была на бумаге нейтральной, по факту разделилась на два лагеря: Бразилия -> союзники, Аргентина + Уругвай -> фашисты. Поэтому неудивительно, что после 1945 года многие итальянцы в поисках лучшей жизни иммигрировали из разрушенной фашисткой Италии в симпатизировавшим ей Аргентине и Уругваю. Поэтому современному аргентинцу или уругвайцу получить гражданство Италии не сложнее, чем человеку по фамилии Зильберман - гражданство Израиля - кто-нибудь среди предков нужной национальности да найдётся!