VBA – IE Automation – guardar como PDF no funciona

Estoy tratando de descargar automáticamente archivos PDF desde un sitio web (publicación de trabajos) gracias a una automatización de IE en VBA y, por alguna razón, no logro generar un solo PDF.

Hacerlo manualmente yendo a la página web y haciendo un ‘guardar destino como’ en el ícono de pdf funciona bien y me da un PDF válido pero la automatización falla.

No entiendo por qué y espero que alguien pueda darme una pista.

Gracias,

VeeBee

A continuación, encontrará el código que tengo hasta ahora (las URL son públicas y he recogido ofertas al azar)

Private Declare Function DownloadFilefromURL Lib "urlmon" _ Alias "URLDownloadToFileA" _ (ByVal pCaller As Long, _ ByVal szURL As String, _ ByVal szFileName As String, _ ByVal dwReserved As Long, _ ByVal lpfnCB As Long) As Long Private Const ERROR_SUCCESS As Long = 0 Private Const BINDF_GETNEWESTVERSION As Long = &H10 Public Function DownloadFile(SourceUrl As String, LocalFile As String) As Boolean DownloadFile = DownloadFilefromURL(0&, SourceUrl, LocalFile, BINDF_GETNEWESTVERSION, 0&) = ERROR_SUCCESS End Function Sub TestSavePDF() Dim oNav As SHDocVw.InternetExplorer Dim oDoc As MSHTML.HTMLDocument Dim MyURL As String Set oNav = New SHDocVw.InternetExplorer oNav.Visible = True 'Test Altays Client A (Banque de France) MyURL = "https://www.recrutement.banque-france.fr/detail-offre/?NoSource=16001&NoSociete=167&NoOffre=2036788&NoLangue=1" 'Test Altays Client B (Egis) ' MyURL = "https://www.altays-progiciels.com/clicnjob/FicheOffreCand.php?PageCour=1&Liste=Oui&Autonome=0&NoOffre=2037501&RefOffrel=&NoFaml=0&NoParam1l=0&NoParam2l=0&NoParam3l=0&NoParam133l=0&NoParam134l=0&NoParam136l=0&NoEntite1=0&NoEntite=&NoPaysl=0&NoRegionl=0&NoDepartementl=0&NoTableOffreLieePl=0&NoTableOffreLieeFl=0&NoNivEtl=0&NoTableCCl=0&NoTableCC2l=0&NoTableCC3l=0&NoTableOffreUnl=0&NoTypContratl=0&NoTypContratProl=0&NoStatutOffrel=&NoUtilisateurl=&RechPleinTextel=#ancre3" oNav.navigate MyURL 'link provided to download the job offer in PDF. when clicked the PDF opens in a new tab MyURL = "https://www.altays-progiciels.com/clicnjob/ExportPDFFront.php" DownloadFile MyURL, "C:\[...Path...]\test.pdf" End Sub 

Shadow DOM y generación de enlace inválido:

La página de trabajo inicial automatizada al hacer clic en el href de destino no genera un enlace de página viable. Esto es presumiblemente porque las cosas importantes realmente ocurren en el lado del servidor.

Objetivo href:

introduzca la descripción de la imagen aquí

Puede hacer clic en el botón de descarga real en esta página

Descargar botón:

introduzca la descripción de la imagen aquí

Esto lanza una nueva ventana por lo que Selenium es genial. El selenium tiene métodos para cambiar a esta nueva ventana. De lo contrario, puede usar los métodos de FindWindow que detallo más adelante en la respuesta para encontrar la ventana Save As .

En esta nueva ventana no puede interactuar con los botones de la forma en que lo hace normalmente al raspar, ya que el contenido requerido no está disponible a través del DOM. Si examina detenidamente, verá que el botón pdf está en shadow-root es decir, donde no puede acceder. Esta es una opción de diseño. Necesito investigar esta posibilidad (seleccionando a través del DOM de sombra usando el combinador ‘/ deep /’) en algún momento, pero no creo que sea cierto en VBA.

Descargar botón en raíz raíz:

introduzca la descripción de la imagen aquí


Imitando las acciones del teclado:

Estoy usando la envoltura de VBA básica de selenium y las API para imitar las acciones en pantalla para guardarlas como pdf utilizando la ventana Save As (vea la imagen en la parte inferior). Particularmente haciendo uso del atajo de teclado Save través de SendKeys . Esto funciona. Utilicé Spy++ para verificar la estructura de árbol de la ventana y para verificar Class nombres y Titles Class ventana.

Utilizo SendKeys para automatizar la apertura del cuadro de diálogo Save As para el pdf. Luego desciendo a la estructura de árbol de la ventana para obtener los manejadores en el ComboBox donde se ingresa el nombre del archivo, por lo que puedo enviarle un mensaje, es decir, el nombre del archivo, y el botón Save para que pueda hacer clic en él. Es posible que necesite una espera más larga para garantizar que la descarga se realice correctamente. Este bit es un poco buggy en mi opinión y espero mejorar.

Estructura de la ventana a través de Spy ++

Es bastante robusto. Utilicé Selenium Basic por la facilidad de trabajar con iframes y solucionar los problemas de la misma política de origen . Con IE, no puede simplemente tomar el enlace src del iframe y navegar felizmente en la página para la impresión en pdf desde el complemento original. Lo que puede hacer, creo, es emitir una solicitud inicial de XMLHTTP y tomar el valor del atributo src , es decir, el enlace. Luego pase ese enlace src a IE y luego continúe como se muestra a continuación para las partes de manejo de Windows.

Con más tiempo, podría agregar la versión de IE y veré un método más robusto, que agregar un tiempo de espera explícito, para monitorear la descarga de archivos antes de salir de la instancia de IE. Probablemente en el sentido de esto (como se indica en una de las respuestas: use SetWindowsHookEx para configurar un gancho WH_SHELL y busque el evento HSHELL_WINDOWCREATED ).


Notas:

  1. Esto está escrito para 64 bits. 32 Bit eliminar PtrSafe . Puedes cambiar LongPtr por Long pero creo que sigue siendo compatible.
  2. Muchísimas gracias a @ErikvonAsmuth por su enorme paciencia al analizar las API conmigo. Echa un vistazo a su excelente respuesta aquí para trabajar con Windows .

VBA:

 Option Explicit Declare PtrSafe Function SendMessageW Lib "User32" (ByVal hWnd As LongPtr, ByVal wMsg As LongPtr, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr Declare PtrSafe Function FindWindowExW Lib "User32" (ByVal hWndParent As LongPtr, _ Optional ByVal hwndChildAfter As LongPtr, Optional ByVal lpszClass As LongPtr, _ Optional ByVal lpszWindow As LongPtr) As LongPtr Public Declare PtrSafe Function FindWindowW Lib "User32" (ByVal lpClassName As LongPtr, Optional ByVal lpWindowName As LongPtr) As LongPtr Public Const WM_SETTEXT = &HC Public Const BM_CLICK = &HF5 Public Sub GetInfo() Dim d As WebDriver, keys As New Selenium.keys Const MAX_WAIT_SEC As Long = 5 Dim t As Date Set d = New ChromeDriver Const URL = "https://www.recrutement.banque-france.fr/detail-offre/charge-de-recrutement-confirme-hf-2037343/" With d .start "Chrome" .get URL .SwitchToFrame .FindElementById("altiframe") .FindElementById("btn-pdf").Click .SwitchToNextWindow .SendKeys keys.Control, "s" Dim str1 As String, cls As String, name As String Dim ptrSaveWindow As LongPtr str1 = "#32770" & vbNullChar t = Timer Do DoEvents ptrSaveWindow = FindWindowW(StrPtr(str1)) If Timer - t > MAX_WAIT_SEC Then Exit Do Loop While ptrSaveWindow = 0 Dim duiViewWND As LongPtr, directUIHWND As LongPtr Dim floatNotifySinkHWND As LongPtr, comboBoxHWND As LongPtr, editHWND As LongPtr If Not ptrSaveWindow > 0 Then Exit Sub duiViewWND = FindWindowExW(ptrSaveWindow, 0&) If Not duiViewWND > 0 Then Exit Sub directUIHWND = FindWindowExW(duiViewWND, 0&) If Not directUIHWND > 0 Then Exit Sub floatNotifySinkHWND = FindWindowExW(directUIHWND, 0&) If Not floatNotifySinkHWND > 0 Then Exit Sub comboBoxHWND = FindWindowExW(floatNotifySinkHWND, 0&) If Not comboBoxHWND > 0 Then Exit Sub editHWND = FindWindowExW(comboBoxHWND, 0&) If Not editHWND > 0 Then Exit Sub Dim msg As String msg = "myTest.pdf" & vbNullChar SendMessageW editHWND, WM_SETTEXT, 0, StrPtr(msg) .SendKeys keys.Control, "s" Dim ptrSaveButton As LongPtr cls = "Button" & vbNullChar name = "&Save" & vbNullChar ptrSaveButton = FindWindowExW(ptrSaveWindow, 0, StrPtr(cls), StrPtr(name)) SendMessageW ptrSaveButton, BM_CLICK, 0, 0 Application.Wait Now + TimeSerial(0, 0, 4) .Quit End With End Sub 

Guardar como ventana de diálogo:


Referencias:

  1. Sombra DOM
  2. Usando shadow DOM – Desarrollador de páginas de Mozilla.

Referencias del proyecto:

  1. Biblioteca de tipos de selenium

    `