Selenium – Configurando download automático e caminho do arquivo

Pois é, o Selenium entrou na minha vida de uma maneira que é difícil até em pensar em fazer algo sem ele… ok, exagerei, mas vocês entenderam o que eu quis dizer.

Fazer download de um arquivo é uma tarefa até básica, mas a princípio não há controle de onde o arquivo vai parar ou qual seu nome, nem se vai aparecer aquela caixa de confirmação. Pois bem, ao menos Chrome, isso é relativamente simples de configurar isso a partir de suas preferências, que via Selenium é chamado de Capabilities ou ChromeOptions. Até onde sei o Firefox também suporta isso, mas o Chrome me pareceu mais flexível.

Para definir isso, utiliza-se o método “SetPreference” do driver, que deve ser configurado antes de iniciar a navegação, ou seja, antes de chamar o Get “http://endereco.do.seu.site”.

O trecho de código abaixo configura o Chrome para:

  1. Ativar o download automático (sem pedir confirmação)
  2. Alterar o caminho de download do arquivo para a mesma pasta do arquivo em execução
Private Sub AbreEConfiguraOChrome()
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", Replace(ThisWorkbook.FullName, ThisWorkbook.name, "")
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.Get "http://endereco.do.seu.site"
    'aqui começa seu código
End Sub

Simples! Ok, demorou um bocando para eu descobrir isso, mas aí está.

Bom proveito!

Selenium – Obtendo o nome do último arquivo baixado no Chrome 70+

Tem alguns dias postei um artigo muito “louco” sobre Selenium, ShadowRoot e Polymer, longo, controverso e cansativo.

Aqui, farei uso de tudo aquilo para resolver um simples problema. Obter o nome do último arquivo baixado no seu Chrome instanciado via Selenium. Sem complicações, vamos ao código:

'Estou assumindo que a variável driver é do WebDriver de está declarada globalmente
Public Function UltimoDownloadFeito() As String
    Dim shadowRoot As WebElement, _
        downloadsManager As WebElement, _
        downloadsItem As WebElement, _
        name As WebElement
 
    Set driver = New ChromeDriver
    driver.Get "chrome://downloads"
 
    Set downloadsManager = driver.FindElementByTag("downloads-manager")
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", downloadsManager)
    Set downloadsItem = driver.ExecuteScript("return arguments[0].querySelectorAll('downloads-item')[0];", shadowRoot)
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", downloadsItem)
    Set name = shadowRoot.FindElementById("name")
 
    UltimoDownloadFeito = name.Text
End Function

É claro, se você executar essa rotina diretamente, vai receber um erro, já que não haverão itens na lista. Fica como desafio implementar uma lógica para tratar esse erro. Para ver funcionar na íntegra, efetue um download antes. Abaixo faço isso com o Modelo de Cadastro:

Public Function UltimoDownloadFeito() As String
    Dim shadowRoot As WebElement, _
        downloadsManager As WebElement, _
        downloadsItem As WebElement, _
        name As WebElement
 
    Set driver = New ChromeDriver
    'faz o download do arquivo antes
    driver.Get "http://www.tomasvasquez.com.br/downloads/ModeloCadastrov3ListView.zip"
    driver.Get "chrome://downloads"
 
    Set downloadsManager = driver.FindElementByTag("downloads-manager")
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", downloadsManager)
    Set downloadsItem = driver.ExecuteScript("return arguments[0].querySelectorAll('downloads-item')[0];", shadowRoot)
    Set shadowRoot = driver.ExecuteScript("return arguments[0].shadowRoot", downloadsItem)
    Set name = shadowRoot.FindElementById("name")
 
    UltimoDownloadFeito = name.Text
End Function

Bom proveito!

PS: Testei esse código na versão 70 do Chrome. Pode ser que ele funcione em algumas versões anteriores e/ou posteriores, mas não garanto. Teste antes de usar, ok? 🙂

Vídeo – VBA, Conhecendo o Rubberduck – Code Explorer, Todo List, Indent e mais

Rubberduck, o plugin que você ainda vai instalar no seu VBA!

🙂

Peço a todos perdão pelo áudio. Ainda estou me acostumando com este novo computador e com esse microfone maluco. Sim, ainda vou comprar um decente.

Acesse também

BLOG ► https://www.tomasvasquez.com.br/blog/
FÓRUM ► http://www.tomasvasquez.com.br/forum/
CURSO ONLINE DE C# ► http://www.tomasvasquez.com.br/cursocsharp

Aqui também!

FACEBOOK ► https://www.facebook.com/tomasvaquezsites
TWITTER ► https://twitter.com/tomamais
GOOGLE+ ► https://plus.google.com/+TomasvasquezBr/

Roteiro, apresentação, edição, etc, etc ► eu mesmo 🙂

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.