Selenium – Brincando com as configurações do Chrome, Polymer e ShadowRoot

Uau! Mas o que é tudo isso? Sim caro leitor, quando você entra do mundo do Web Scrapping, a palavra Web traz um quinquilhão de coisas junto com ela, de benefícios a dores de cabeça.

A Web não pára e apesar desta frase ser um baita clichê, ela é assustadoramente verdadeira. Vou dar um exemplo do porquê. Tem alguns anos um treco HTML5 foi lançado. Já ouviu falar dele? Pois é, junto com ele, uma tonelada de novas tags HTML e apis Javascript também vieram. Seus scripts Selenium precisaram ser atualizados.

Vida que segue e… boom! Mais atualizações. Duas delas foram mencionadas no título e é sobre elas que falarei aqui.

Eu só queria extrair uma configuração do Chrome

Sim, tudo o que eu queria era extrair uma configuração do Google Chrome instalado na máquina do usuário. A parte fácil é que essa página de configurações é um HTML exposto sob a URL chrome://settings. Ótimo! É só dar um get nessa URL e começar a mágica.

O problema é, bem, esse é o Google Chrome, criado e mantido pelo Google, provavelmente a empresa que mais expõe e propõe inovações para especificações Web atualmente. Não surpreendentemente, a página de configurações usa um bocado de coisas novas (só precisa funcionar no Chrome mesmo), entre elas, o ShadowRoot e Polymer.

Não vou me aprofundar no que cada um desses carinhas é além de uma breve explicação.

ShadowRoot

É uma espécie de elemento HTML que cria um novo contexto a partir dele. Você pode pensar nele como um iframe + ajax com muita esperteza embutida, criado para separar contextos da hierarquia. Para mais informações: https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot

Polymer

É basicamente um padrão proposto pelo Google para criação de elementos HTML personalizados. Se você sabe algo de XML, é exatamente isso, só que, cada elemento que você criar, ganha uma nova representação visual numa página HTML. Já há vários elementos criados nesse padrão. Para mais informações: https://www.polymer-project.org/

Como não poderia ser surpresa, a página de configurações do Google está recheada dessas duas coisas.

E o Selenium nisso tudo?

Bem, primeiro deixe-me explicar porque resolvi fazer tudo isso. Primeiro, porque todo programador teimoso gosta de resolver um problema chato e difícil. Segundo, tanto o ShadowRoot como o Polymer uma hora se tornarão largamente utilizados no mercado. Resolver esse problema agora me traria um melhor entendimento sobre o assunto e me prepararia para futuro. Vamos lá.

Mas calma, mas porque isso é tão difícil com o Selenium? O XPath resolve tudo para mim! Aí é que está o problema. Não resolve. A biblioteca atual do Selenium (não só VBA, mas todas as outras, já que elas dependem do driver para implementar funcionalidades) não suporta esses tipos de elemento.

O FindElementByTag simplesmente ignora tags criadas com Polymer e o ShadowRoot precisa ser explicitamente declarado ou expandido antes de ser possível acessar qualquer elemento dentro dele. Por sorte, há como resolver esse problema, com muito suor.

Expandindo o ShadowRoot

O ShadowRoot é basicamente o primeiro filho de uma determinada tag. O Javascript lhe dá uma ajuda nisso, aliás é o único meio no momento.

Supondo que você encontro este elemento:

....
</p>
<div id="id"><p>
#shadow-root
	</p>
<div id="outroId"></div>
<p>
</div>
<p>
...

Infelizmente, um driver.FindElementById(“outroId”) retornará nulo se você não expandir o ShadowRoot antes. Como fazer isso? Em VBA:

Set divId = driver.FindElementById("id")
Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", divId)
Set divOutroId = shadowRoot.FindElementById("outroId")

A primeira linha é mais do que clara.
A segunda faz a mágica de expandir o ShadowRoot que pertence à primeira div e o retorna para a variável shadowRoot.
A terceira, já de posse o elemento que representa o ShadowRoot, aplica o FindElementById para a captura do elemento procurado, desta vez retornando o de forma válida.

Capturando tags Polymer

Elementos Polymer como explicado anteriormente, são basicamente tags HTML, porém, personalizadas. Portanto, ao invés de ter algo assim:

<div id="endereco"><p>
	</p>
<div id="rua">Rua Principal</div>
<p>
	</p>
<div id="numero">1000</div>
<p>
	</p>
<div id="bairro">Capital</div>
<p>
</div>

Teriámos algo assim:

<endereco>
	<rua>Rua Principal</rua>
	<numero>1000</numero>
	<bairro>Capital</bairro>
</endereco>

É de falto mais semântico, legível e auto-explicativo. E segundo exemplo não existe na especificação HTML e um navegador não sabe reconhecê-lo, mas o Polymer torna isso possível.

O porém para nós, nobres web scrappers, a coisa não é tão simples. Veja que pela auto declaração dos elementos pela sua própria semântica, não há necessidade de ids ou outros atributos identificadores. Onde antes faríamos um:

driver.FindElementById("rua")
'ou
driver.FindElementsByTag("div")(2)
'ou
driver.FindElementByXPath("//div[@id=""rua""]")

Com Polymer faríamos:

driver.FindElementByTag("rua")

Mas não! O Selenium não sabe reconhecer esse tipo de tag! Qualquer tentativa de fazê-lo com FindElementByTag ou FindElementByXPath falhará. Como resolver isso? Assim:

AVISO: O FindElementByTag curiosamente funciona para o primeiro elemento da cadeia, ou seja, um elemento pai. Por isso, o código abaixo usa o FindElementByTag no elemento endereço sem maiores erros.

Set endereco = driver.FindElementByTag("endereco")
Set rua = driver.ExecuteScript("return arguments[0].querySelectorAll('rua')[0];", endereco)

Mais uma vez, o Javascript resolvendo o problema. A variável rua é um WebElement válido a partir de agora.

Um caso real – Obtendo o diretório local de downloads do Chrome

Pois é, essa simples necessidade gerou todo o artigo acima. Basimente, precisei navegar até a página de configurações do navegador do Google, toda feita usando ShadowRoot e Polymer e extrair a informação. Como disso anteriormente, é uma página HTML, por isso, convido-o a acessar a mesma e olhar o código fonte.

No fim, o código que executa a proeza é:

Dim driver As WebDriver
 
Sub ObtemCaminhoDownload()
    Dim shadowRoot As WebElement, _
        settingsUI As WebElement, _
        main As WebElement, _
        settingsBasicPage As WebElement, _
        advancedPage As WebElement, _
        settingsSection As WebElement, _
        settingsDownloadsPage As WebElement, _
        settingsAnimatedPage As WebElement, _
        neonAnimatable As WebElement, _
        defaultDownloadPath As WebElement
 
    Set driver = New ChromeDriver
    driver.Get "chrome://settings"
    Set settingsUI = driver.FindElementByTag("settings-ui")
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", settingsUI)
    Set main = shadowRoot.FindElementById("main")
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", main)
    Set settingsBasicPage = driver.ExecuteScript("return arguments[0].querySelectorAll('settings-basic-page')[0];", shadowRoot)
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", settingsBasicPage)
    Set advancedPage = shadowRoot.FindElementById("advancedPage")
    Set settingsSection = driver.ExecuteScript("return arguments[0].querySelectorAll('settings-section')[2];", advancedPage)
    Set settingsDownloadsPage = driver.ExecuteScript("return arguments[0].querySelectorAll('settings-downloads-page')[0]", settingsSection)
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", settingsDownloadsPage)
    Set settingsAnimatedPage = driver.ExecuteScript("return arguments[0].querySelectorAll('settings-animated-pages')[0];", shadowRoot)
    Set neonAnimatable = driver.ExecuteScript("return arguments[0].querySelectorAll('neon-animatable')[0];", settingsAnimatedPage)
    Set defaultDownloadPath = neonAnimatable.FindElementById("defaultDownloadPath")
    Debug.Print defaultDownloadPath.Text
End Sub

Basicamente, usando todas os truques citados acima. Todas as outras tentativas, usando os caminhos tradicionais não funcionarem, sendo esse o único caminho atual.

Vai ser sempre assim?

Não. Encontrei várias referências de que o time dos drivers está trabalhando para adicionar métodos nativos para tratar com ShadowRoot e fazer os métodos baseados em tags suportarem o Polymer. Não há previsão de quando isso será lançado, mas é um trabalho em andamento e a complexidade mostrada acima pode não ser mais necessária, o que será um alívio.

Comentários

comentários