elhacker.net cabecera Bienvenido(a), Visitante. Por favor Ingresar o Registrarse
¿Perdiste tu email de activación?.


Tema destacado: Introducción a la Factorización De Semiprimos (RSA)


+  Foro de elhacker.net
|-+  Programación
| |-+  Programación General
| | |-+  .NET (C#, VB.NET, ASP) (Moderador: kub0x)
| | | |-+  Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
0 Usuarios y 2 Visitantes están viendo este tema.
Páginas: 1 ... 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 [63] Ir Abajo Respuesta Imprimir
Autor Tema: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)  (Leído 684,035 veces)
Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #620 en: 22 Abril 2026, 15:47 pm »

Indagando más profundo en la automatización de Proton VPN, descubrí tres formas adicionales para alternar entre servidores, de las cuales he llegado a implementar las dos primeras:

 1- Una forma consiste en automatizar el menú contextual de la bandeja del sistema (System Tray), haciendo click en los comandos "Conectar" y "Desconectar" de dicho menú según sea el estado actual de la conexión VPN. Esto es una técnica bastante más eficiente que la estrategia de automatización de la GUI que compartí para la versión de pago en el post anterior aquí arriba, ya que no depende de coordenadas ni de averiguar el color del botón "Conectar/Desconectar", pero de todas formas si la barra de tareas estuviese oculta/colapsada o si el icono de ProtonVPN estuviese oculto en el área de notificación colapsado, supondría una barrera que requeriría implementar más lógica adicional de automatización para la barra de tareas. Al menos el método de automatización que ya compartí no tiene esas barreras.

 2- La otra forma, la cual descubrí investigando en un hilo de Reddit, y la cual considero que es la forma más efectiva de todas (o al menos la menos invasiva), consiste en enviar una señal de detención al servicio de Windows de ProtonVPN. Esto provocará una reconexión de la interfaz de red de Proton (y con ello un cambio aleatorio de servidor). El único requisito es estar corriendo ProtonVPN y tener activado el modo "País aleatorio" para que al reiniciar el servicio la nueva conexión se establezca alternando a otro servidor diferente.

 3- Existiría una tercera vía mediante la comunicación directa con la canalización con nombre (Named Pipe) de Proton. En teoría se podría lograr importando las librerías DLL necesarias de la aplicación (NET 8+), pero es una opción poco viable para poder compartirlo como solución de distribución general en un foro. En su defecto se supone que se podría implementar un proxy WCF (Windows Communication Foundation), pero esta opción tampoco sería muy viable, aparte del ingente esfuerzo para implementar todas las interfaces necesarias (ver aquí), la solución sería extremadamente frágil, ya que cualquier modificación en los contratos de interfaz del código fuente original de Proton invalidaría el proxy por completo. No llegué a implementar esta solución y, para ser sinceros, tampoco es que tenga experiencia en hacerlo.

En fin. Teniendo todo esto en cuenta, considero que llevar a cabo el "reinicio" del servicio de ProtonVPN es la solución que tiene el "trade-off" más factible entre todas las demás soluciones que descubrí. Aquí les dejo el código fuente:

Código
  1. ''' <summary>
  2. ''' Restarts the Proton VPN Windows Service to force a server reconnection.
  3. ''' </summary>
  4. '''
  5. ''' <remarks>
  6. ''' This method validates that a connection is active by checking the
  7. ''' "ProtonVPN" network interface status before attempting to restart the service.
  8. ''' </remarks>
  9. '''
  10. ''' <exception cref="InvalidOperationException">
  11. ''' Thrown when the Proton VPN network interface is not found or is not connected,
  12. ''' or if the <b>ProtonVPN</b> Windows service fails to complete the restart sequence.
  13. ''' </exception>
  14. '''
  15. ''' <exception cref="Exception">
  16. ''' Thrown when the <b>ProtonVPN</b> Windows service service restarted and network interface is UP,
  17. ''' but no data flow (Bytes Received) was detected.
  18. ''' </exception>
  19. <EditorBrowsable(EditorBrowsableState.Advanced)>
  20. <DebuggerStepThrough>
  21. Private Sub ChangeProtonVpnServerByWindowsService()
  22.  
  23.    Const ProtonVPN_NetworkInterfaceName As String = "ProtonVPN"
  24.    Const ProtonVPN_WindowsServiceName As String = "ProtonVPN Service"
  25.  
  26.    Dim isProtonVpnConnected As Boolean = False
  27.  
  28.    Dim interfaces As NetworkInterface() = NetworkInterface.GetAllNetworkInterfaces()
  29.    For Each ni As NetworkInterface In interfaces
  30.        ' We look for the specific name Proton uses for its virtual adapters
  31.        If ni.Name.Equals(ProtonVPN_NetworkInterfaceName, StringComparison.OrdinalIgnoreCase) Then
  32.            If ni.OperationalStatus = OperationalStatus.Up Then
  33.                isProtonVpnConnected = True
  34.                Exit For
  35.            End If
  36.        End If
  37.    Next ni
  38.  
  39.    If Not isProtonVpnConnected Then
  40.        Throw New InvalidOperationException(
  41.            $"The {ProtonVPN_NetworkInterfaceName} network interface is not connected to a server." & Environment.NewLine &
  42.            "The reconnection logic requires an active VPN connection to be present.")
  43.    End If
  44.  
  45.    ' Restart the ProtonVPN Windows service.
  46.    Using sc As New ServiceController(ProtonVPN_WindowsServiceName)
  47.  
  48.        Select Case sc.Status
  49.  
  50.            Case ServiceControllerStatus.StartPending, ServiceControllerStatus.Running
  51.                sc.Stop()
  52.                sc.WaitForStatus(ServiceControllerStatus.StopPending, TimeSpan.FromSeconds(10))
  53.  
  54.            Case ServiceControllerStatus.Stopped
  55.                sc.Start()
  56.  
  57.            Case Else
  58.                Throw New InvalidOperationException(
  59.                    $"The {ProtonVPN_WindowsServiceName} Windows service is in a unexpected state: {sc.Status}")
  60.        End Select
  61.  
  62.        sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10))
  63.    End Using
  64.  
  65.    ' Wait for the ProtonVPN network interface to return to 'Up' status and has verified data flow (bytes received).
  66.    Dim retryTimeout As TimeSpan = TimeSpan.FromSeconds(30)
  67.    Dim startTime As Date = Date.Now
  68.    Dim isDataFlowEstablished As Boolean = False
  69.    Dim initialBytes As Long = -1
  70.  
  71.    Do While (Date.Now - startTime) < retryTimeout
  72.        Dim protonVpnInterface As NetworkInterface = NetworkInterface.GetAllNetworkInterfaces().
  73.            SingleOrDefault(Function(ni) ni.Name.Equals(ProtonVPN_NetworkInterfaceName, StringComparison.OrdinalIgnoreCase))
  74.  
  75.        If (protonVpnInterface IsNot Nothing) AndAlso
  76.           (protonVpnInterface.OperationalStatus = OperationalStatus.Up) Then
  77.  
  78.            Dim stats As IPv4InterfaceStatistics = protonVpnInterface.GetIPv4Statistics()
  79.  
  80.            ' On first detection of 'Up' status, capture baseline.
  81.            If initialBytes = -1 Then
  82.                initialBytes = stats.BytesReceived
  83.            End If
  84.  
  85.            ' Check if we have received new data since being 'Up'.
  86.            If stats.BytesReceived > initialBytes Then
  87.                isDataFlowEstablished = True
  88.                Exit Do
  89.            End If
  90.        End If
  91.  
  92.        Thread.Sleep(500)
  93.    Loop
  94.  
  95.    If Not isDataFlowEstablished Then
  96.        Throw New Exception(
  97.            $"The {ProtonVPN_WindowsServiceName} Windows service restarted " & Environment.NewLine &
  98.            $"and {ProtonVPN_NetworkInterfaceName} network interface is UP, " & Environment.NewLine &
  99.            $"but no data flow (bytes received) was detected within {retryTimeout.TotalSeconds} seconds.")
  100.    End If
  101.  
  102.    Thread.Sleep(1000) ' Additional delay to ensure connection has established with a VPN server before return.
  103. End Sub

Simplemente llamar al método ChangeProtonVpnServerViaWindowsService, esperar a que termine su ejecución, y listo.



« Última modificación: 22 Abril 2026, 16:26 pm por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #621 en: 7 Mayo 2026, 17:55 pm »

Comparto con ustedes una solución completa que sirve, entre otros posibles fines, para poder calcular la luminosidad promedio perceptible de una imagen, mediante el estándar del espacio de color CIE L*A*B*. Para quien desee detalles más técnicos al respecto, está todo documentado en el código fuente, incluyendo URLs a Wikipedia.

El siguiente código fuente contiene dos métodos de extensión con nombre ComputeAverageLightness para los tipos System.Drawing.Bitmap y System.Drawing.Image, así como una sobrecarga adicional, o mejor dicho, el mismo método pero definido en otra clase diferente con nombre UtilImage, al que poder pasarle un archivo de imagen como parámetro.

El código fuente además provee dos funciones reutilizables, ComputeLabF y BuildLinearSRGBLookupTable, en la clase con nombre UtilColor.



Casos de uso real

 - Clasificar un set de imágenes, por ejemplo, fondos de escritorio, según el porcentaje de luminosidad de cada imagen, separando imágenes oscuras, equilibradas y sobreexpuestas.
 - Selección de "keyframes" en video para extraer una miniatura que lo represente, descartando aquellos frames que sean extremadamente oscuros.
 - Automatizar el proceso de revisión y selección de imágenes tras una sesión fotográfica para conservar solo las mejores y descartar las fallidas (a esto se le llama "Culling").
 - Determinar si una imagen necesita ecualización de histograma o normalización tonal antes de post-procesamiento.
 - Analizar páginas escaneadas para distinguir documentos limpios frente a documentos oscuros o degradados.
 - Determinar qué imágenes requieren corrección gamma.
 - Otras 80 sugerencias que cualquier IA te podrá mencionar si le muestras este código fuente...



El código fuente

Código
  1. Imports System.Buffers
  2. Imports System.Drawing
  3. Imports System.Drawing.Drawing2D
  4. Imports System.Drawing.Imaging
  5. Imports System.IO
  6. Imports System.Runtime.CompilerServices
  7. Imports System.Runtime.InteropServices
  8.  
  9. Public Module BitmapExtensions
  10.  
  11.    ''' <summary>
  12.    ''' Precomputed look-up table that maps each sRGB encoded byte value [0, 255]
  13.    ''' to its corresponding linear-light (physical) value in the range [0.0, 1.0].
  14.    ''' </summary>
  15.    '''
  16.    ''' <remarks>
  17.    ''' Used by function: <see cref="BitmapExtensions.ComputeAverageLightness(Bitmap, Byte)"/>
  18.    ''' to convert sRGB pixel values to linear light for accurate luminance calculations.
  19.    ''' </remarks>
  20.    Private LinearSrgbLut As IReadOnlyList(Of Double)
  21.  
  22.    ''' <summary>
  23.    ''' Computes the average CIE L* lightness of the specified <see cref="Bitmap"/>.
  24.    ''' <para></para>
  25.    ''' The bitmap must use <see cref="PixelFormat.Format32bppArgb"/> layout;
  26.    ''' any other format will raise a <see cref="NotSupportedException"/>.
  27.    ''' <para></para>
  28.    ''' For arbitrary bitmaps, prefer <see cref="ImageExtensions.ComputeAverageLightness"/> function,
  29.    ''' which handles proper <see cref="Bitmap"/> format conversion automatically.
  30.    ''' </summary>
  31.    '''
  32.    ''' <param name="bitmap">
  33.    ''' Source bitmap in <see cref="PixelFormat.Format32bppArgb"/> format.
  34.    ''' </param>
  35.    '''
  36.    ''' <param name="alphaThreshold">
  37.    ''' Optional alpha threshold below which a pixel is considered fully transparent and excluded from the lightness average.
  38.    ''' <para></para>
  39.    ''' Range: 0–255. A value of 0 includes all pixels; 255 includes only fully opaque pixels.
  40.    ''' <para></para>
  41.    ''' Default value is 8.
  42.    ''' </param>
  43.    '''
  44.    ''' <returns>
  45.    ''' Average CIE L* value in the range [0.0, 100.0], or 0.0 if all pixels are below the alpha threshold.
  46.    ''' </returns>
  47.    <Extension>
  48.    Public Function ComputeAverageLightness(sourceBitmap As Bitmap, Optional alphaThreshold As Byte = 8) As Double
  49.  
  50. #If NETCOREAPP Then
  51.        ArgumentNullException.ThrowIfNull(sourceBitmap)
  52. #Else
  53.        If bitmap Is Nothing Then
  54.            Throw New ArgumentNullException(NameOf(bitmap))
  55.        End If
  56. #End If
  57.  
  58.        If sourceBitmap.PixelFormat <> PixelFormat.Format32bppArgb Then
  59.            Throw New NotSupportedException(
  60.                $"Expected PixelFormat {NameOf(PixelFormat.Format32bppArgb)}, got {sourceBitmap.PixelFormat}.")
  61.        End If
  62.  
  63.        ' ITU-R BT.709 standard coefficients for relative luminance (Y)
  64.        ' These weights reflect human visual sensitivity to RGB channels.
  65.        Const LumaR As Double = 0.2126R
  66.        Const LumaG As Double = 0.7152R
  67.        Const LumaB As Double = 0.0722R
  68.  
  69.        ' CIE L* (Lightness) formula constants
  70.        ' Used to transform relative luminance (Y) into perceptual lightness (L*).
  71.        Const LStarScale As Double = 116.0R
  72.        Const LStarOffset As Double = 16.0R
  73.  
  74.        If BitmapExtensions.LinearSrgbLut Is Nothing Then
  75.            BitmapExtensions.LinearSrgbLut = UtilColor.BuildLinearSRGBLookupTable()
  76.        End If
  77.  
  78.        Dim rect As New Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height)
  79.        Dim bmpData As BitmapData = sourceBitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)
  80.        Dim pixels As Byte() = Nothing
  81.  
  82.        Try
  83.            Dim stride As Integer = bmpData.Stride
  84.            If stride <= 0 Then
  85.                Throw New NotSupportedException("Negative or zero stride is not supported.")
  86.            End If
  87.  
  88.            Dim bufferSize As Integer = stride * sourceBitmap.Height
  89.            pixels = ArrayPool(Of Byte).Shared.Rent(bufferSize)
  90.            Marshal.Copy(bmpData.Scan0, pixels, 0, bufferSize)
  91.  
  92.            Dim totalL As Double = 0.0R
  93.            Dim pixelCount As Long = 0L
  94.  
  95.            Dim y As Integer
  96.            For y = 0 To sourceBitmap.Height - 1
  97.                Dim rowOffset As Integer = y * stride
  98.  
  99.                Dim x As Integer
  100.                For x = 0 To sourceBitmap.Width - 1
  101.                    Dim idx As Integer = rowOffset + (x * 4)
  102.  
  103.                    ' Format32bppArgb byte layout: B=idx+0, G=idx+1, R=idx+2, A=idx+3
  104.                    Dim alpha As Byte = pixels(idx + 3)
  105.                    If alpha <= alphaThreshold Then
  106.                        Continue For
  107.                    End If
  108.  
  109.                    Dim r As Byte = pixels(idx + 2)
  110.                    Dim g As Byte = pixels(idx + 1)
  111.                    Dim b As Byte = pixels(idx)
  112.  
  113.                    ' Step 1: Calculate CIE Relative Luminance (Y) using the LUT
  114.                    ' This represents the physical light intensity.
  115.                    Dim relY As Double =
  116.                        (BitmapExtensions.LinearSrgbLut(r) * LumaR) +
  117.                        (BitmapExtensions.LinearSrgbLut(g) * LumaG) +
  118.                        (BitmapExtensions.LinearSrgbLut(b) * LumaB)
  119.  
  120.                    ' Step 2: Calculate Perceptual Lightness (L*)
  121.                    ' This transforms physical light into human perceived brightness [0-100].
  122.                    ' Note: ComputeLabF FUNCTION applies the cube root or linear slope per CIE specs.
  123.                    totalL += (LStarScale * UtilColor.ComputeLabF(relY)) - LStarOffset
  124.                    pixelCount += 1L
  125.                Next
  126.            Next
  127.  
  128.            Return If(pixelCount = 0L, 0.0R, totalL / pixelCount)
  129.  
  130.        Finally
  131.            sourceBitmap.UnlockBits(bmpData)
  132.            If pixels IsNot Nothing Then
  133.                ArrayPool(Of Byte).Shared.Return(pixels, clearArray:=False)
  134.            End If
  135.        End Try
  136.    End Function
  137.  
  138. End Module
  139.  
  140. Public Module ImageExtensions
  141.  
  142.    ''' <summary>
  143.    ''' Computes the average CIE L* lightness of the specified <see cref="Image"/>.
  144.    ''' </summary>
  145.    '''
  146.    ''' <param name="bitmap">
  147.    ''' Source bitmap in <see cref="PixelFormat.Format32bppArgb"/> format.
  148.    ''' </param>
  149.    '''
  150.    ''' <param name="alphaThreshold">
  151.    ''' Optional alpha threshold below which a pixel is considered fully transparent and excluded from the lightness average.
  152.    ''' <para></para>
  153.    ''' Range: 0–255. A value of 0 includes all pixels; 255 includes only fully opaque pixels.
  154.    ''' <para></para>
  155.    ''' Default value is 8.
  156.    ''' </param>
  157.    '''
  158.    ''' <returns>
  159.    ''' Average CIE L* value in the range [0.0, 100.0], or 0.0 if all pixels are below the alpha threshold.
  160.    ''' </returns>
  161.    <Extension>
  162.    Public Function ComputeAverageLightness(sourceImage As Image, Optional alphaThreshold As Byte = 8) As Double
  163.  
  164. #If NETCOREAPP Then
  165.        ArgumentNullException.ThrowIfNull(sourceImage)
  166. #Else
  167.        If sourceImage Is Nothing Then
  168.            Throw New ArgumentNullException(NameOf(sourceImage))
  169.        End If
  170. #End If
  171.  
  172.        Dim bmp As Bitmap = TryCast(sourceImage, Bitmap)
  173.        Dim mustDispose As Boolean = False
  174.  
  175.        If bmp Is Nothing OrElse bmp.PixelFormat <> PixelFormat.Format32bppArgb Then
  176.            bmp = New Bitmap(sourceImage.Width, sourceImage.Height, PixelFormat.Format32bppArgb)
  177.            Using gfx As Graphics = Graphics.FromImage(bmp)
  178.                gfx.CompositingMode = CompositingMode.SourceCopy
  179.                gfx.InterpolationMode = InterpolationMode.NearestNeighbor
  180.                gfx.PixelOffsetMode = PixelOffsetMode.HighSpeed
  181.                gfx.CompositingQuality = CompositingQuality.HighSpeed
  182.  
  183.                gfx.DrawImage(sourceImage, 0, 0, sourceImage.Width, sourceImage.Height)
  184.            End Using
  185.            mustDispose = True
  186.        End If
  187.  
  188.        Try
  189.            Return BitmapExtensions.ComputeAverageLightness(bmp, alphaThreshold)
  190.        Finally
  191.            If mustDispose Then
  192.                bmp.Dispose()
  193.            End If
  194.        End Try
  195.  
  196.    End Function
  197.  
  198. End Module
  199.  
  200. Public Class UtilImage
  201.  
  202.    ''' <summary>
  203.    ''' Computes the average CIE L* lightness of the specified image file.
  204.    ''' </summary>
  205.    '''
  206.    ''' <example> This is a code example.
  207.    ''' <code language="VB">
  208.    ''' Dim imageFile As String = "C:\Wallpaper.jpg"
  209.    ''' Dim avgLightness As Double = UtilImage.ComputeAverageLightness(imageFile)
  210.    '''
  211.    ''' Console.WriteLine($"Image File: {Path.GetFileName(imageFile)}")
  212.    ''' Console.WriteLine($"Average Lightness (CIE L*): {(avgLightness / 100.0R):P2}")
  213.    ''' </code>
  214.    ''' </example>
  215.    '''
  216.    ''' <param name="filePath">
  217.    ''' Full path to the source image file.
  218.    ''' </param>
  219.    '''
  220.    ''' <param name="alphaThreshold">
  221.    ''' Optional alpha threshold below which a pixel is considered fully transparent and excluded from the lightness average.
  222.    ''' <para></para>
  223.    ''' Range: 0–255. A value of 0 includes all pixels; 255 includes only fully opaque pixels.
  224.    ''' <para></para>
  225.    ''' Default value is 8.
  226.    ''' </param>
  227.    '''
  228.    ''' <returns>
  229.    ''' Average CIE L* value in the range [0.0, 100.0], or 0.0 if all pixels are below the alpha threshold.
  230.    ''' </returns>
  231.    Public Shared Function ComputeAverageLightness(filePath As String, Optional alphaThreshold As Byte = 8) As Double
  232.  
  233.        Using sourceStream As New FileStream(filePath, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read)
  234.  
  235.            Using sourceImage As Image = Image.FromStream(sourceStream, useEmbeddedColorManagement:=False, validateImageData:=True)
  236.                Return ImageExtensions.ComputeAverageLightness(sourceImage, alphaThreshold)
  237.            End Using
  238.        End Using
  239.    End Function
  240.  
  241. End Class
  242.  
  243. Public Class UtilColor
  244.  
  245.    ''' <summary>
  246.    ''' Applies the CIE XYZn to L*a*b* (CIELAB) transfer function (also known as the f-function)
  247.    ''' used in the conversion from relative luminance (<c>Y</c>) to perceptual lightness (<c>L*</c>).
  248.    ''' </summary>
  249.    '''
  250.    ''' <example> This example shows how to calculate the Perceptual Lightness (<c>L*</c>) of a 18% "Middle Gray" value:
  251.    ''' <code language="VB">
  252.    ''' Dim relativeY As Double = 0.18R ' 18% reflectance
  253.    ''' Dim fValue As Double = ComputeLabF(relativeY)
  254.    '''
  255.    ''' ' Formula for L* (Lightness): 116 * f(Y) - 16
  256.    ''' Dim lightness As Double = (116.0R * fValue) - 16.0R
  257.    '''
  258.    ''' Console.WriteLine($"Relative Luminance (Y): {relativeY}")
  259.    ''' Console.WriteLine($"Perceptual Lightness (L*): {lightness:F2}")
  260.    ''' </code>
  261.    ''' </example>
  262.    '''
  263.    ''' <param name="value">
  264.    ''' The relative luminance (<c>Y</c>) or color component, expected in the normalized range <c>[0.0, 1.0]</c>.
  265.    ''' </param>
  266.    '''
  267.    ''' <returns>
  268.    ''' The transformed value <c>f(Y)</c> to be used in <c>L*</c>, <c>a*</c>, or <c>b*</c> calculations.
  269.    ''' </returns>
  270.    '''
  271.    ''' <remarks>
  272.    ''' This function is part of the CIE L*a*b* (CIELAB) color space definition.
  273.    ''' It models how humans perceive brightness differences.
  274.    ''' <para></para>
  275.    '''
  276.    ''' The constants used by this function are defined by the CIE (International Commission on Illumination):
  277.    ''' <para></para>
  278.    ''' - <b>Epsilon (&#949;)</b>: The transition point between linear and non-linear behavior, calculated as <c>(6/29)^3 &#8776; 0.008856</c>.
  279.    ''' <para></para>
  280.    ''' - <b>Kappa (&#954;)</b>: The slope of the linear segment near black, calculated as <c>(29/3)^3 &#8776; 903.3</c>.
  281.    ''' <para></para>
  282.    '''
  283.    ''' For more technical details, see:
  284.    ''' <para></para>
  285.    ''' <see href="https://en.wikipedia.org/wiki/CIELAB_color_space#Forward_transformation"/>
  286.    ''' <para></para>
  287.    ''' <see href="https://en.wikipedia.org/wiki/Relative_luminance"/>
  288.    ''' </remarks>
  289.    <DebuggerStepThrough>
  290.    Public Shared Function ComputeLabF(value As Double) As Double
  291.  
  292.        Const Epsilon As Double = 0.008856R
  293.        Const Kappa As Double = 903.3R
  294.  
  295.        ' CIE L* (L-Star / Lightness) formula constants
  296.        Const LStarScale As Double = 116.0R
  297.        Const LStarOffset As Double = 16.0R
  298.  
  299.        ' If the value is above Epsilon, we use the power function (cube root).
  300.        ' If the value is below Epsilon, we use a linear slope (Kappa) to avoid
  301.        ' infinite gradients at the zero point, and handle image noise better.
  302.        Return If(value > Epsilon,
  303.                  Math.Pow(value, 1.0R / 3.0R),
  304.                  (Kappa * value + LStarOffset) / LStarScale)
  305.    End Function
  306.  
  307.    ''' <summary>
  308.    ''' Precomputes a look-up table (LUT) containing the linearized sRGB values [0.0, 1.0] for each possible 8-bit channel value [0, 255].
  309.    ''' </summary>
  310.    '''
  311.    ''' <example>
  312.    ''' This example demonstrates how to use the look-up table (LUT) to linearize a standard 8-bit RGB channel value:
  313.    ''' <code language="VB">
  314.    ''' Dim lut As IReadOnlyList(Of Double) = BuildLinearSRGBLookupTable()
  315.    '''
  316.    ''' ' Get the linear intensity of a middle-gray byte (127)
  317.    ''' Dim grayByte As Integer = 127
  318.    ''' Dim linearLight As Double = lut(grayByte)
  319.    '''
  320.    ''' ' Output the result (approx. 0.21 or 21% light intensity)
  321.    ''' Console.WriteLine($"Byte Value: {grayByte}")
  322.    ''' Console.WriteLine($"Linear Light Intensity: {linearLight:P2}")
  323.    ''' </code>
  324.    ''' </example>
  325.    '''
  326.    ''' <returns>
  327.    ''' A readon-only list of 256 <see cref="Double"/> values representing normalized linear light intensities in the range [0.0, 1.0].
  328.    ''' </returns>
  329.    '''
  330.    ''' <remarks>
  331.    ''' Standard digital images (sRGB) are gamma-corrected to optimize bit depth for human perception.
  332.    ''' Before performing any physical light calculations (like luminosity), we must "undo" this correction.
  333.    ''' This process is known as <b>Gamma Expansion</b>.
  334.    ''' <para></para>
  335.    ''' For more technical details, see: <see href="https://en.wikipedia.org/wiki/SRGB#The_forward_transformation_(gamma_compression)"/>
  336.    ''' </remarks>
  337.    <DebuggerStepThrough>
  338.    Public Shared Function BuildLinearSRGBLookupTable() As IReadOnlyList(Of Double)
  339.  
  340.        ' sRGB Standard Constants
  341.  
  342.        ' The exponent used for the power-law (gamma) section.
  343.        ' While often simplified to 2.2, the official sRGB standard uses 2.4.
  344.        Const GammaExponent As Double = 2.4R
  345.  
  346.        ' The threshold that separates the linear slope from the curve.
  347.        Const GammaThreshold As Double = 0.04045R
  348.  
  349.        ' The divisor used for the linear segment near black.
  350.        Const LinearSlope As Double = 12.92R
  351.  
  352.        ' The offset (magic number) used to align the linear and curve segments.
  353.        Const Offset As Double = 0.055R
  354.  
  355.        Dim lut As Double() = New Double(255) {}
  356.  
  357.        For i As Integer = 0 To 255
  358.            ' Normalize the 8-bit integer [0, 255] to a double [0.0, 1.0]
  359.            Dim v As Double = i / 255.0R
  360.  
  361.            ' The "Piecewise" function:
  362.            ' If the value is very dark (below threshold), use a simple linear division;
  363.            ' Otherwise, use the exponential formula (Gamma Expansion).
  364.            lut(i) = If(v <= GammaThreshold,
  365.                        v / LinearSlope,
  366.                        Math.Pow((v + Offset) / (1.0R + Offset), GammaExponent))
  367.        Next
  368.  
  369.        Return Array.AsReadOnly(lut)
  370.    End Function
  371.  
  372. End Class



Ejemplos de uso

Función ComputeAverageLightness:
Código
  1. Dim imageFile As String = "C:\Wallpaper.jpg"
  2. Dim avgLightness As Double = UtilImage.ComputeAverageLightness(imageFile)
  3.  
  4. Console.WriteLine($"Image File: {Path.GetFileName(imageFile)}")
  5. Console.WriteLine($"Average Lightness (CIE L*): {(avgLightness / 100.0R):P2}")

Función ComputeLabF:
Código
  1. Dim relativeY As Double = 0.18R ' 18% reflectance
  2. Dim fValue As Double = ComputeLabF(relativeY)
  3.  
  4. ' Formula for L* (Lightness): 116 * f(Y) - 16
  5. Dim lightness As Double = (116.0R * fValue) - 16.0R
  6.  
  7. Console.WriteLine($"Relative Luminance (Y): {relativeY}")
  8. Console.WriteLine($"Perceptual Lightness (L*): {lightness:F2}")



Comentarios adicionales

Me he apoyado en el uso de la IA para implementar de la forma más rigurosa que me ha sido posible la implementación que se pueda considerar como el estándar de la fórmula de CIELAB, y las constantes LUMA y demás constantes, consultando segunda y tercera opinión en distintas IA, todo esto por que no soy ningún experto en detalles avanzados de fotografía profesional y colorimetría, pero intento hacer las cosas bien dentro de la limitación del conocimiento inadquirido en campos en los que no estoy especializado... ni tengo la menor intención de aprender más de lo necesario al respecto para resolver un problema específico que yo tenía, y que este código fuente me lo soluciona. Vamos, que si hay algún fogógrafo o diseñador profesional de artes gráficas en la sala, que no se ofenda si algún valor constante o algún cálculo en concreto no es exactamente del todo correcto, aunque en principio debería serlo.

He comparado el uso de esta estrategia con métodos más rudimentarios o tradicionales para calcular la luminosidad promedio sobre miles de imágenes, y el uso de CIELAB produce resultados perceptualmente mucho más precisos. Dicho de forma más simple, le da mil patadas a lo tradicional, garantizado.


« Última modificación: 7 Mayo 2026, 18:26 pm por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #622 en: 14 Mayo 2026, 16:25 pm »

Hoy les traigo varios herramientas de utilidad general para la automatización web mediante Selenium.

Primero les comparto estas dos funciones, una síncrona y la otra asíncrona, que sirven para inicializar automáticamente el entorno de Selenium utilizando Selenium Manager, descargando y resolviendo dinámicamente tanto el navegador Chrome como ChromeDriver, sin necesidad de mantener versiones sincronizadas entre estos.

Uno de los problemas clásicos al trabajar con Selenium es la gestión de versiones entre Chrome y ChromeDriver. Cuando cualquiera de estos componentes queda desincronizado, aparecen errores típicos como:
Citar
This version of ChromeDriver only supports Chrome version XXX

Estas dos funciones que comparto, eliminan completamente ese problema mediante el uso de Selenium Manager para la descarga automática de binarios y la resolución dinámica de rutas, y pudiendo especificar una ruta de caché local configurable.

Estas funciones deben utilizarse siempre ANTES de cualquier uso de Selenium dentro del proceso actual. Simplemente llamar a una de estas dos funciones, y olvidarse de problemas.

Ejemplo de uso:

Código
  1. Dim seleniumCacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "cache\selenium")
  2. Dim forceBrowserDownload As Boolean = True
  3. Dim result As SeleniumEnvironmentInitializationResult =
  4.    InitializeSeleniumEnvironmentForChrome(seleniumCacheDirPath, forceBrowserDownload)
  5.  
  6. Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}")
  7. Console.WriteLine($"Driver located at: {result.DriverFilePath}")
  8. Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}")

Paquetes NuGet necesarios:
 - https://www.nuget.org/packages/selenium.webdriver
 - https://www.nuget.org/packages/selenium.webdriver.chromedriver

El código:

Código
  1. ''' <summary>
  2. ''' Initializes Selenium for the current process by configuring a
  3. ''' local cache directory path through the <c>"SE_CACHE_PATH"</c> environment variable,
  4. ''' and optionally forcing the download of latest Chrome and ChromeDriver executables into the specified cache directory.
  5. ''' <para></para>
  6. ''' This method must be called before any Selenium usage in the current process
  7. ''' to ensure that Selenium Manager can locate the ChromeDriver and Chrome browser executables.
  8. ''' </summary>
  9. '''
  10. ''' <example>
  11. ''' This example shows how to initialize the environment:
  12. ''' <code language="VB">
  13. ''' Dim cacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "Cache\Selenium")
  14. ''' Dim forceBrowserDownload As Boolean = True
  15. ''' Dim result As SeleniumEnvironmentInitializationResult =
  16. '''     InitializeSeleniumEnvironmentForChrome(cacheDirPath, forceBrowserDownload)
  17. '''
  18. ''' Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}")
  19. ''' Console.WriteLine($"Driver located at: {result.DriverFilePath}")
  20. ''' Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}")
  21. ''' </code>
  22. ''' </example>
  23. '''
  24. ''' <param name="cacheDirPath">
  25. ''' The directory path where Chrome and ChromeDriver will be stored. For example, <c>".\cache\Selenium"</c>
  26. ''' </param>
  27. '''
  28. ''' <param name="forceBrowserDownload">
  29. ''' A <see cref="Boolean"/> value indicating whether to force the download of latest Chrome
  30. ''' binaries in the directory specified in <paramref name="cacheDirPath"/> parameter.
  31. ''' </param>
  32. '''
  33. ''' <param name="seleniumManagerFilePath">
  34. ''' Optional. Full path to <c>selenium-manager.exe</c> file.
  35. ''' <para></para>
  36. ''' If not specified, the default runtime path inside the current application directory is used:
  37. ''' <c>".\runtimes\win\native\selenium-manager.exe"</c>
  38. ''' </param>
  39. '''
  40. ''' <returns>
  41. ''' A <see cref="SeleniumEnvironmentInitializationResult"/> object containing the Selenium Manager process exit code,
  42. ''' resolved Selenium driver file path, and resolved browser binary file path.
  43. ''' </returns>
  44. '''
  45. ''' <exception cref="FileNotFoundException">
  46. ''' Thrown when the Selenium Manager file path cannot be resolved,
  47. ''' or if the Selenium Manager process execution resolves driver or browser file paths that do not exist.
  48. ''' </exception>
  49. '''
  50. ''' <exception cref="TimeoutException">
  51. ''' Thrown when the execution of Selenium Manager process exceeds the allowed time limit (10 minutes).
  52. ''' </exception>
  53. '''
  54. ''' <exception cref="InvalidOperationException">
  55. ''' Thrown when Selenium Manager execution completes successfully but the expected output information
  56. ''' (driver and browser paths) cannot be determined or validated.
  57. ''' </exception>
  58. <DebuggerStepThrough>
  59. Public Shared Function InitializeSeleniumEnvironmentForChrome(cacheDirPath As String,
  60.                                                              forceBrowserDownload As Boolean,
  61.                                                     Optional seleniumManagerFilePath As String = Nothing) As SeleniumEnvironmentInitializationResult
  62.  
  63.    If String.IsNullOrEmpty(seleniumManagerFilePath) Then
  64. #If NETCOREAPP Then
  65.        seleniumManagerFilePath = Path.Combine(AppContext.BaseDirectory, "runtimes\win\native\selenium-manager.exe")
  66. #Else
  67.        seleniumManagerFilePath = Path.Combine(My.Application.Info.DirectoryPath, "runtimes\win\native\selenium-manager.exe")
  68. #End If
  69.        If Not File.Exists(seleniumManagerFilePath) Then
  70.            Throw New FileNotFoundException("selenium-manager.exe not found.", seleniumManagerFilePath)
  71.        End If
  72.    End If
  73.  
  74.    ' Set env var for this process (it MUST be done before any Selenium usage).
  75.    Environment.SetEnvironmentVariable("SE_CACHE_PATH", cacheDirPath, EnvironmentVariableTarget.Process)
  76.  
  77.    Dim argumentsList As New List(Of String) From {
  78.        If(forceBrowserDownload, "--force-browser-download", String.Empty),
  79.        "--browser chrome",
  80.        "--driver chromedriver",
  81.        $"--cache-path ""{cacheDirPath}"""
  82.    }
  83.    Dim arguments As String =
  84.        argumentsList.Where(Function(arg) Not String.IsNullOrWhiteSpace(arg)).Aggregate(Function(acc, arg) $"{acc} {arg}")
  85.  
  86.    Dim outputBuilder As New StringBuilder()
  87.    Dim errorBuilder As New StringBuilder()
  88.  
  89.    Using p As New Process
  90.        With p.StartInfo
  91.            .FileName = seleniumManagerFilePath
  92.            .Arguments = arguments
  93.            .UseShellExecute = False
  94.            .RedirectStandardOutput = True
  95.            .RedirectStandardError = True
  96.            .CreateNoWindow = True
  97.            .StandardOutputEncoding = Encoding.UTF8
  98.            .StandardErrorEncoding = Encoding.UTF8
  99.            .WindowStyle = ProcessWindowStyle.Hidden
  100.        End With
  101.  
  102.        AddHandler p.OutputDataReceived,
  103.            Sub(sender As Object, e As DataReceivedEventArgs)
  104.                If Not String.IsNullOrWhiteSpace(e.Data) Then
  105.                    outputBuilder.AppendLine(e.Data)
  106.                End If
  107.            End Sub
  108.  
  109.        AddHandler p.ErrorDataReceived,
  110.            Sub(sender As Object, e As DataReceivedEventArgs)
  111.                If Not String.IsNullOrWhiteSpace(e.Data) Then
  112.                    errorBuilder.AppendLine(e.Data)
  113.                End If
  114.            End Sub
  115.  
  116.        p.Start()
  117.        p.BeginOutputReadLine()
  118.        p.BeginErrorReadLine()
  119.  
  120.        Dim exited As Boolean = p.WaitForExit(CInt(TimeSpan.FromMinutes(10).TotalMilliseconds))
  121.        If Not exited Then
  122.            Try
  123.                p.Kill()
  124.            Catch
  125.            End Try
  126.  
  127.            Throw New TimeoutException("selenium-manager.exe execution timed out.")
  128.        End If
  129.  
  130.        ' Ensure async buffers are fully flushed.
  131.        p.WaitForExit()
  132.  
  133.        Dim combinedOutput As String = $"{outputBuilder}{Environment.NewLine}{errorBuilder}"
  134.        ' Example:
  135.        ' [INFO] Driver path: C:\...\chromedriver.exe
  136.        ' [INFO] Browser path: C:\...\chrome.exe
  137.  
  138.        Dim driverMatch As Match =
  139.            Regex.Match(combinedOutput, "Driver path:\s*(.+?chromedriver\.exe)", RegexOptions.IgnoreCase Or RegexOptions.Multiline)
  140.  
  141.        Dim browserMatch As Match =
  142.            Regex.Match(combinedOutput, "Browser path:\s*(.+?chrome\.exe)", RegexOptions.IgnoreCase Or RegexOptions.Multiline)
  143.  
  144.        Dim result As New SeleniumEnvironmentInitializationResult(
  145.            p.ExitCode, combinedOutput,
  146.            If(driverMatch.Success, driverMatch.Groups(1).Value.Trim(), String.Empty),
  147.            If(browserMatch.Success, browserMatch.Groups(1).Value.Trim(), String.Empty)
  148.        )
  149.  
  150.        ' Extra validation for successful execution.
  151.        If p.ExitCode = 0 Then
  152.            If String.IsNullOrWhiteSpace(result.DriverFilePath) Then
  153.                Throw New InvalidOperationException(
  154.                    $"Could not determine ChromeDriver path from Selenium Manager output.{Environment.NewLine}{Environment.NewLine}" &
  155.                    $"Output:{Environment.NewLine}{combinedOutput}")
  156.            End If
  157.  
  158.            If String.IsNullOrWhiteSpace(result.BrowserFilePath) Then
  159.                Throw New InvalidOperationException(
  160.                    $"Could not determine Chrome browser path from Selenium Manager output.{Environment.NewLine}{Environment.NewLine}" &
  161.                    $"Output:{Environment.NewLine}{combinedOutput}")
  162.            End If
  163.  
  164.            If Not File.Exists(result.DriverFilePath) Then
  165.                Throw New FileNotFoundException(
  166.                    "Resolved ChromeDriver.exe file was not found.", result.DriverFilePath)
  167.            End If
  168.  
  169.            If Not File.Exists(result.BrowserFilePath) Then
  170.                Throw New FileNotFoundException(
  171.                    "Resolved Chrome.exe file was not found.", result.BrowserFilePath)
  172.            End If
  173.        End If
  174.  
  175.        Return result
  176.    End Using
  177.  
  178. End Function
  179.  
  180. ''' <summary>
  181. ''' Asynchronously initializes Selenium for the current process by configuring a
  182. ''' local cache directory path through the <c>"SE_CACHE_PATH"</c> environment variable,
  183. ''' and optionally forcing the download of latest Chrome and ChromeDriver executables into the specified cache directory.
  184. ''' <para></para>
  185. ''' This method must be called before any Selenium usage in the current process
  186. ''' to ensure that Selenium Manager can locate the ChromeDriver and Chrome browser executables.
  187. ''' </summary>
  188. '''
  189. ''' <example>
  190. ''' This example shows how to initialize the environment:
  191. ''' <code language="VB">
  192. ''' Dim cacheDirPath As String = Path.Combine(My.Application.Info.DirectoryPath, "Cache\Selenium")
  193. ''' Dim forceBrowserDownload As Boolean = True
  194. ''' Dim result As SeleniumEnvironmentInitializationResult =
  195. '''     Await InitializeSeleniumEnvironmentForChromeAsync(cacheDirPath, forceBrowserDownload)
  196. '''
  197. ''' Console.WriteLine($"Selenium Manager exit code: {result.SeleniumManagerExitCode}")
  198. ''' Console.WriteLine($"Driver located at: {result.DriverFilePath}")
  199. ''' Console.WriteLine($"Browser binary located at: {result.BrowserFilePath}")
  200. ''' </code>
  201. ''' </example>
  202. '''
  203. ''' <param name="cacheDirPath">
  204. ''' The directory path where Chrome and ChromeDriver will be stored. For example, <c>".\cache\Selenium"</c>
  205. ''' </param>
  206. '''
  207. ''' <param name="forceBrowserDownload">
  208. ''' A <see cref="Boolean"/> value indicating whether to force the download of latest Chrome
  209. ''' binaries in the directory specified in <paramref name="cacheDirPath"/> parameter.
  210. ''' </param>
  211. '''
  212. ''' <param name="seleniumManagerFilePath">
  213. ''' Optional. Full path to <c>selenium-manager.exe</c> file.
  214. ''' <para></para>
  215. ''' If not specified, the default runtime path inside the current application directory is used:
  216. ''' <c>".\runtimes\win\native\selenium-manager.exe"</c>
  217. ''' </param>
  218. '''
  219. ''' <returns>
  220. ''' A <see cref="Task(Of SeleniumEnvironmentInitializationResult)"/> representing the asynchronous operation,
  221. ''' containing the Selenium Manager process exit code, resolved Selenium driver file path, and resolved browser binary file path.
  222. ''' </returns>
  223. '''
  224. ''' <exception cref="FileNotFoundException">
  225. ''' Thrown when the Selenium Manager file path cannot be resolved,
  226. ''' or if the Selenium Manager process execution resolves driver or browser file paths that do not exist.
  227. ''' </exception>
  228. '''
  229. ''' <exception cref="TimeoutException">
  230. ''' Thrown when the execution of Selenium Manager process exceeds the allowed time limit (10 minutes).
  231. ''' </exception>
  232. '''
  233. ''' <exception cref="InvalidOperationException">
  234. ''' Thrown when Selenium Manager execution completes successfully but the expected output information
  235. ''' (driver and browser paths) cannot be determined or validated.
  236. ''' </exception>
  237. <DebuggerStepThrough>
  238. Public Shared Async Function InitializeSeleniumEnvironmentForChromeAsync(cacheDirPath As String,
  239.                                                                         forceBrowserDownload As Boolean,
  240.                                                                Optional seleniumManagerFilePath As String = Nothing) As Task(Of SeleniumEnvironmentInitializationResult)
  241.  
  242.    Return Await Task.Run(
  243.        Function() As SeleniumEnvironmentInitializationResult
  244.            Return InitializeSeleniumEnvironmentForChrome(cacheDirPath, forceBrowserDownload, seleniumManagerFilePath)
  245.        End Function
  246.    ).ConfigureAwait(continueOnCapturedContext:=False)
  247.  
  248. End Function

Clase necesria SeleniumEnvironmentInitializationResult, que representa el resultado de la inicialización:

Código
  1. #Region " Option Statements "
  2.  
  3. Option Strict On
  4. Option Explicit On
  5. Option Infer Off
  6.  
  7. #End Region
  8.  
  9. #Region " Imports "
  10.  
  11. #End Region
  12.  
  13. #Region " SeleniumEnvironmentInitializationResult "
  14.  
  15. 'Namespace DevCase.ThirdParty.Selenium
  16.  
  17.    ''' <summary>
  18.    ''' Represents the result of Selenium environment initialization process performed by
  19.    ''' functions like <see cref="UtilSelenium.InitializeSeleniumEnvironmentForChrome"/>  
  20.    ''' and <see cref="UtilSelenium.InitializeSeleniumEnvironmentForChromeAsync"/>,
  21.    ''' containing the Selenium Manager process exit code, resolved Selenium driver file path,
  22.    ''' and resolved browser binary file path.
  23.    ''' </summary>
  24.    Public NotInheritable Class SeleniumEnvironmentInitializationResult
  25.  
  26. #Region " Properties "
  27.  
  28.        ''' <summary>
  29.        ''' Gets the exit code returned by the Selenium Manager process execution.
  30.        ''' <para></para>
  31.        ''' A value of 0 indicates success, while non-zero values indicate an error condition.
  32.        ''' </summary>
  33.        Public ReadOnly Property SeleniumManagerExitCode As Integer
  34.  
  35.        ''' <summary>
  36.        ''' Gets the console output produced by the Selenium Manager process execution,
  37.        ''' which may contain informational messages, warnings, or error details related to the environment initialization process.
  38.        ''' </summary>
  39.        Public ReadOnly Property SeleniumManagerConsoleOutput As String
  40.  
  41.        ''' <summary>
  42.        ''' Gets the full file path of the resolved Selenium driver executable,
  43.        ''' as determined by Selenium Manager process execution.
  44.        ''' </summary>
  45.        Public ReadOnly Property DriverFilePath As String
  46.  
  47.        ''' <summary>
  48.        ''' Gets the full file path of the resolved browser executable,
  49.        ''' as determined by Selenium Manager process execution.
  50.        ''' </summary>
  51.        Public ReadOnly Property BrowserFilePath As String
  52.  
  53. #End Region
  54.  
  55. #Region " Constructors "
  56.  
  57.        ''' <summary>
  58.        ''' Prevents a default instance of the <see cref="SeleniumEnvironmentInitializationResult"/> class from being created.
  59.        ''' </summary>
  60.        Private Sub New()
  61.        End Sub
  62.  
  63.        ''' <summary>
  64.        ''' Initializes a new instance of the <see cref="SeleniumEnvironmentInitializationResult"/> class.
  65.        ''' </summary>
  66.        '''
  67.        ''' <param name="seleniumManagerExitCode">
  68.        ''' The Selenium Manager exit code.
  69.        ''' </param>
  70.        '''
  71.        ''' <param name="seleniumManagerConsoleOutput">
  72.        ''' The Selenium Manager console output,
  73.        ''' which may contain informational messages, warnings,
  74.        ''' or error details related to the environment initialization process.
  75.        ''' </param>
  76.        '''
  77.        ''' <param name="driverFilePath">
  78.        ''' The resolved Selenium driver file path.
  79.        ''' </param>
  80.        '''
  81.        ''' <param name="browserFilePath">
  82.        ''' The resolved browser file path.
  83.        ''' </param>
  84.        Public Sub New(seleniumManagerExitCode As Integer, seleniumManagerConsoleOutput As String,
  85.                       driverFilePath As String, browserFilePath As String)
  86.  
  87.            Me.SeleniumManagerExitCode = seleniumManagerExitCode
  88.            Me.seleniumManagerConsoleOutput = seleniumManagerConsoleOutput
  89.  
  90.            Me.DriverFilePath = driverFilePath
  91.            Me.BrowserFilePath = browserFilePath
  92.        End Sub
  93.  
  94. #End Region
  95.  
  96.    End Class
  97.  
  98. 'End Namespace
  99.  
  100. #End Region



Adicionalmente, les dejo por aquí estas otras dos funciones, que sirven para resolver de forma programática la ruta a la versión de chrome y chromedriver más reciente dentro del directorio de cache local de Selenium:

GetLatestCachedChromeDriverFilePath
Código
  1. ''' <summary>
  2. ''' Resolves the full file path of the most recent (latest) cached Chrome browser binary version
  3. ''' available in the specified Selenium cache directory path.
  4. ''' </summary>
  5. '''
  6. ''' <example> This is a code example.
  7. ''' <code language="VB">
  8. ''' Dim seleniumCacheDirPath As String = Nothing ' Or set to a specific path if desired. For example, ".\Cache\Selenium".
  9. ''' Dim latestChromeDriverFilePath As String = GetLatestCachedChromeDriverFilePath(seleniumCacheDirPath)
  10. ''' Console.WriteLine($"Latest ChromeDriver.exe File Path: {latestChromeDriverFilePath}")
  11. ''' </code>
  12. ''' </example>
  13. '''
  14. ''' <param name="seleniumCacheDirPath">
  15. ''' Optional. The Selenium cache directory path. For example, ".\Cache\Selenium".
  16. ''' <para></para>
  17. ''' This is the base directory where Selenium Manager stores downloaded browser binaries and drivers.
  18. ''' <para></para>
  19. ''' If this value is not set, the function will attempt to read the cache path from
  20. ''' the <c>SE_CACHE_PATH</c> environment variable for the current process.
  21. ''' </param>
  22. '''
  23. ''' <returns>
  24. ''' A <see cref="String"/> containing the full file path to the latest Chrome version directory.
  25. ''' </returns>
  26. '''
  27. ''' <exception cref="DirectoryNotFoundException">
  28. ''' Thrown when the Selenium cache directory path does not exist,
  29. ''' or when the base directory for Chrome versioned directories is not found.
  30. ''' </exception>
  31. '''
  32. ''' <exception cref="InvalidOperationException">
  33. ''' Thrown when the Selenium cache directory path is not provided and <c>SE_CACHE_PATH</c> environment variable is not set,
  34. ''' or when no valid versioned Chrome directories are found in the expected location.
  35. ''' </exception>
  36. <DebuggerStepThrough>
  37. Public Shared Function GetLatestCachedChromeDriverFilePath(Optional seleniumCacheDirPath As String = Nothing) As String
  38.  
  39.    If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then
  40.        seleniumCacheDirPath = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process)
  41.  
  42.        If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then
  43.            Throw New InvalidOperationException("Selenium cache directory path is not provided and SE_CACHE_PATH environment variable is not set.")
  44.        End If
  45.  
  46.        If Not Directory.Exists(seleniumCacheDirPath) Then
  47.            Throw New DirectoryNotFoundException($"Selenium cache directory specified in SE_CACHE_PATH does not exist: {seleniumCacheDirPath}")
  48.        End If
  49.  
  50.    ElseIf Not String.IsNullOrWhiteSpace(seleniumCacheDirPath) AndAlso Not Directory.Exists(seleniumCacheDirPath) Then
  51.        Throw New DirectoryNotFoundException($"The provided Selenium cache directory path does not exist: {seleniumCacheDirPath}")
  52.    End If
  53.  
  54.    Dim baseDirPath As String = Path.Combine(seleniumCacheDirPath, "chromedriver", "win64")
  55.    If Not Directory.Exists(baseDirPath) Then
  56.        Throw New DirectoryNotFoundException($"Base directory for ChromeDrover versioned directories does not exist: {baseDirPath}")
  57.    End If
  58.  
  59.    Dim subDirs As String() = Directory.GetDirectories(baseDirPath, "*.*", SearchOption.TopDirectoryOnly)
  60.  
  61.    Dim latestVersionedDir As String =
  62.    subDirs.Select(Function(dir As String) New With {.Path = dir, .FolderName = Path.GetFileName(dir)}).
  63.            Where(Function(x)
  64.                      Dim ver As Version = Nothing
  65.                      Return Version.TryParse(x.FolderName, ver)
  66.                  End Function).
  67.                OrderByDescending(Function(x) New Version(x.FolderName)).
  68.                Select(Function(x) x.Path).
  69.                FirstOrDefault()
  70.  
  71.    If String.IsNullOrEmpty(latestVersionedDir) Then
  72.        Throw New InvalidOperationException($"No valid versioned ChromeDriver directories were found in: ""{baseDirPath}""")
  73.    End If
  74.  
  75.    Return Path.Combine(latestVersionedDir, "chromedriver.exe")
  76. End Function

GetLatestCachedChromeFilePath
Código
  1. ''' <summary>
  2. ''' Resolves the full file path of the most recent (latest) cached Chrome browser binary version
  3. ''' available in the specified Selenium cache directory path.
  4. ''' </summary>
  5. '''
  6. ''' <example> This is a code example.
  7. ''' <code language="VB">
  8. ''' Dim seleniumCacheDirPath As String = Nothing ' Or set to a specific path if desired. For example, ".\Cache\Selenium".
  9. ''' Dim latestChromeFilePath As String = GetLatestCachedChromeFilePath(seleniumCacheDirPath)
  10. ''' Console.WriteLine($"Latest Chrome.exe File Path: {latestChromeFilePath}")
  11. ''' </code>
  12. ''' </example>
  13. '''
  14. ''' <param name="seleniumCacheDirPath">
  15. ''' Optional. The Selenium cache directory path. For example, ".\Cache\Selenium".
  16. ''' <para></para>
  17. ''' This is the base directory where Selenium Manager stores downloaded browser binaries and drivers.
  18. ''' <para></para>
  19. ''' If this value is not set, the function will attempt to read the cache path from
  20. ''' the <c>SE_CACHE_PATH</c> environment variable for the current process.
  21. ''' </param>
  22. '''
  23. ''' <returns>
  24. ''' A <see cref="String"/> containing the full file path to the latest Chrome version directory.
  25. ''' </returns>
  26. '''
  27. ''' <exception cref="DirectoryNotFoundException">
  28. ''' Thrown when the Selenium cache directory path does not exist,
  29. ''' or when the base directory for Chrome versioned directories is not found.
  30. ''' </exception>
  31. '''
  32. ''' <exception cref="InvalidOperationException">
  33. ''' Thrown when the Selenium cache directory path is not provided and <c>SE_CACHE_PATH</c> environment variable is not set,
  34. ''' or when no valid versioned Chrome directories are found in the expected location.
  35. ''' </exception>
  36. <DebuggerStepThrough>
  37. Public Shared Function GetLatestCachedChromeFilePath(Optional seleniumCacheDirPath As String = Nothing) As String
  38.  
  39.    If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then
  40.        seleniumCacheDirPath = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process)
  41.  
  42.        If String.IsNullOrWhiteSpace(seleniumCacheDirPath) Then
  43.            Throw New InvalidOperationException("Selenium cache directory path is not provided and SE_CACHE_PATH environment variable is not set.")
  44.        End If
  45.  
  46.        If Not Directory.Exists(seleniumCacheDirPath) Then
  47.            Throw New DirectoryNotFoundException($"Selenium cache directory specified in SE_CACHE_PATH does not exist: {seleniumCacheDirPath}")
  48.        End If
  49.  
  50.    ElseIf Not String.IsNullOrWhiteSpace(seleniumCacheDirPath) AndAlso Not Directory.Exists(seleniumCacheDirPath) Then
  51.        Throw New DirectoryNotFoundException($"The provided Selenium cache directory path does not exist: {seleniumCacheDirPath}")
  52.    End If
  53.  
  54.    Dim baseDirPath As String = Path.Combine(seleniumCacheDirPath, "chrome", "win64")
  55.    If Not Directory.Exists(baseDirPath) Then
  56.        Throw New DirectoryNotFoundException($"Base directory for Chrome versioned directories does not exist: {baseDirPath}")
  57.    End If
  58.  
  59.    Dim subDirs As String() = Directory.GetDirectories(baseDirPath, "*.*", SearchOption.TopDirectoryOnly)
  60.  
  61.    Dim latestVersionedDir As String =
  62.    subDirs.Select(Function(dir As String) New With {.Path = dir, .FolderName = Path.GetFileName(dir)}).
  63.            Where(Function(x)
  64.                      Dim ver As Version = Nothing
  65.                      Return Version.TryParse(x.FolderName, ver)
  66.                  End Function).
  67.                OrderByDescending(Function(x) New Version(x.FolderName)).
  68.                Select(Function(x) x.Path).
  69.                FirstOrDefault()
  70.  
  71.    If String.IsNullOrEmpty(latestVersionedDir) Then
  72.        Throw New InvalidOperationException($"No valid versioned Chrome directories were found in: ""{baseDirPath}""")
  73.    End If
  74.  
  75.    Return Path.Combine(latestVersionedDir, "chrome.exe")
  76. End Function
« Última modificación: 15 Mayo 2026, 00:34 am por Eleкtro » En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #623 en: 14 Mayo 2026, 17:00 pm »

Siguiendo con las herramientas generales para automatización mediante Selenium, aquí les dejo una función por nombre CreateOptimizedChromeDriver, que sirve para crear una instancia de ChromeDriver preconfigurada y optimizada para tareas de automatización web.

La función provee varios parámetros opcionales de personalización de rutas, la posibilidad de añadir argumentos opcionales durante la creación de la instancia, y, además, poder ocultar la ventana de Chrome cuando se trabaja con interfaz gráfica / modo non-headless.

La instancia creada por esta función del proceso Chrome con interfaz gráfica / modo non-headless (con la ventana visible u oculta), no debería dar problemas de ningún tipo al intentar completar los desafíos de Cloudflare. Utilizo esta función en numerosos proyectos de scraping.

Ejemplo de uso:
Código
  1. Dim driverService As ChromeDriverService = Nothing
  2. Dim headless As Boolean = False
  3. Dim hideNonHeadlessWindow As Boolean = False
  4. Dim driverFilePath As String = Nothing    ' Optional, as the function automatically resolves the latest cached ChromeDriver.exe file path.
  5. Dim driverLogFilePath As String = Nothing ' Optional, as the function automatically resolves a default log file path if no specified.
  6. Dim chromeFilePath As String = Nothing    ' Optional, as the function automatically resolves the latest cached Chrome.exe file path.
  7. Dim userDataDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache\chrome user data")
  8. Dim profileName As String = "Automation Profile"
  9. Dim additionalArguments As String() = {"--window-position=0,0"}
  10.  
  11. Try
  12.    Using driver As ChromeDriver = CreateOptimizedChromeDriver(
  13.        driverService,
  14.        driverFilePath:=driverFilePath,
  15.        driverLogFilePath:=driverLogFilePath,
  16.        chromeFilePath:=chromeFilePath,
  17.        userDataDir:=userDataDir,
  18.        profileName:=profileName,
  19.        headless:=headless,
  20.        hideNonHeadlessWindow:=hideNonHeadlessWindow,
  21.        additionalArguments)
  22.  
  23.        driver.Navigate().GoToUrl("https://www.google.com/")
  24.        Console.WriteLine($"Page Title: {driver.Title}")
  25.    End Using
  26. Finally
  27.    driverService?.Dispose()
  28. End Try

La función CreateOptimizedChromeDriver:
Código
  1. ''' <summary>
  2. ''' Initializes and returns a <see cref="ChromeDriver"/> instance
  3. ''' with a preconfigured set of options optimized for browser automation,
  4. ''' and optionally hiding the Chrome window if not running in headless mode and specified by the caller.
  5. ''' </summary>
  6. '''
  7. ''' <example> This is a code example.
  8. ''' <code language="VB">
  9. ''' Dim driverService As ChromeDriverService = Nothing
  10. ''' Dim headless As Boolean = False
  11. ''' Dim hideNonHeadlessWindow As Boolean = False
  12. ''' Dim driverFilePath As String = Nothing    ' Optional, as the function automatically resolves the latest cached ChromeDriver.exe file path.
  13. ''' Dim driverLogFilePath As String = Nothing ' Optional, as the function automatically resolves a default log file path if no specified.
  14. ''' Dim chromeFilePath As String = Nothing    ' Optional, as the function automatically resolves the latest cached Chrome.exe file path.
  15. ''' Dim userDataDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Cache\Chrome")
  16. ''' Dim profileName As String = "AutomationProfile"
  17. ''' Dim additionalArguments As String() = {"--window-position=-0,0"}
  18. '''
  19. ''' Try
  20. '''     Using driver As ChromeDriver = CreateOptimizedChromeDriver(
  21. '''         driverService,
  22. '''         driverFilePath:=driverFilePath,
  23. '''         driverLogFilePath:=driverLogFilePath,
  24. '''         chromeFilePath:=chromeFilePath,
  25. '''         userDataDir:=userDataDir,
  26. '''         profileName:=profileName,
  27. '''         headless:=headless,
  28. '''         hideNonHeadlessWindow:=hideNonHeadlessWindow,
  29. '''         additionalArguments)
  30. '''
  31. '''         driver.Navigate().GoToUrl("https://www.google.com/")
  32. '''         Console.WriteLine($"Page Title: {driver.Title}")
  33. '''     End Using
  34. ''' Finally
  35. '''     driverService?.Dispose()
  36. ''' End Try
  37. ''' </code>
  38. ''' </example>
  39. '''
  40. ''' <param name="refDriverService">
  41. ''' When this function returns, receives the <see cref="ChromeDriverService"/> instance created.
  42. ''' </param>
  43. '''
  44. ''' <param name="driverFilePath">
  45. ''' File path to ChromeDriver.exe; For example, ".\chromedriver.exe"
  46. ''' <para></para>
  47. ''' This value can be null, in which case the function will attempt to resolve the latest cached ChromeDriver.exe file path
  48. ''' from the Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable)
  49. ''' using the <see cref="UtilSelenium.GetLatestCachedChromeDriverFilePath"/> function.
  50. ''' </param>
  51. '''
  52. ''' <param name="driverLogFilePath">
  53. ''' Full path to the log file where ChromeDriver logs will be written. For example, ".\chromedriver.log"
  54. ''' <para></para>
  55. ''' This value can be null, in which case a default log file path is used in the same directory as the
  56. ''' <paramref name="userDataDir"/> dierctory path, with the name "chromedriver.log".
  57. ''' </param>
  58. '''
  59. ''' <param name="chromeFilePath">
  60. ''' File path to Chrome.exe; For example, "C:\Program Files\Google Chrome\chrome.exe"
  61. ''' <para></para>
  62. ''' This value can be null, in which case the function will attempt to resolve the latest cached Chrome.exe file path
  63. ''' from the Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable)
  64. ''' using the <see cref="UtilSelenium.GetLatestCachedChromeFilePath"/> function.
  65. ''' </param>
  66. '''
  67. ''' <param name="userDataDir">
  68. ''' The directory path for Chrome's user data. For example, ".\Cache\Chrome User Data"
  69. ''' <para></para>
  70. ''' This directory is used to store user-specific settings and data.
  71. ''' <para></para>
  72. ''' This value can be null, in which case the function will attempt to resolve the
  73. ''' Selenium cache directory (as configured in <c>SE_CACHE_PATH</c> environment variable)
  74. ''' and create a subdirectory inside it for Chrome user data, with the name "chrome user data".
  75. ''' If the Selenium cache directory cannot be resolved, the function will create the
  76. ''' user data directory inside the current application base directory, with the name "chrome user data".
  77. ''' </param>
  78. '''
  79. ''' <param name="profileName">
  80. ''' The name of the Chrome profile directory to use within the <paramref name="userDataDir"/>.
  81. ''' For example, "Default" or "Profile 1".
  82. ''' <para></para>
  83. ''' This value can be null, in which case "Default" profile name is assumed.
  84. ''' </param>
  85. '''
  86. ''' <param name="headless">
  87. ''' If <see langword="True"/>, launches Chrome in headless mode.
  88. ''' </param>
  89. '''
  90. ''' <param name="hideNonHeadlessWindow">
  91. ''' If <see langword="True"/> and <paramref name="headless"/> is set to <see langword="False"/>,
  92. ''' attempts to hide the Chrome window and taskbar icon.
  93. ''' <para></para>
  94. ''' Has no effect if <paramref name="headless"/> is set to <see langword="True"/>,
  95. ''' since Chrome windows are not shown in headless mode.
  96. ''' </param>
  97. '''
  98. ''' <param name="additionalArguments">
  99. ''' Optional. Additional arguments to add to the underlying <see cref="ChromeOptions"/> object
  100. ''' through its <see cref="ChromeOptions.AddArguments"/> method.
  101. ''' <para></para>
  102. ''' For example, when not running in headless mode you can use
  103. ''' <c>"--window-position=-32000,0"</c> to set the Chrome window off-screen,
  104. ''' or <c>"--window-size=1200,800"</c> to set the initial Chrome window size.
  105. ''' </param>
  106. '''
  107. ''' <returns>
  108. ''' The resulting <see cref="ChromeDriver"/> instance.
  109. ''' </returns>
  110. <DebuggerStepThrough>
  111. Public Shared Function CreateOptimizedChromeDriver(ByRef refDriverService As ChromeDriverService,
  112.                                                         driverFilePath As String, driverLogFilePath As String,
  113.                                                         chromeFilePath As String, userDataDir As String, profileName As String,
  114.                                                         headless As Boolean, hideNonHeadlessWindow As Boolean,
  115.                                              ParamArray additionalArguments As String()) As ChromeDriver
  116.  
  117.    If String.IsNullOrWhiteSpace(driverFilePath) Then
  118.        Try
  119.            driverFilePath = UtilSelenium.GetLatestCachedChromeDriverFilePath()
  120.        Catch ex As Exception
  121.            Throw New InvalidOperationException($"Failed to resolve ChromeDriver.exe file path from Selenium cache. See inner exception for details.", ex)
  122.        End Try
  123.    ElseIf Not String.IsNullOrWhiteSpace(driverFilePath) AndAlso Not File.Exists(driverFilePath) Then
  124.        Throw New FileNotFoundException($"ChromeDriver.exe not found at the specified path: {driverFilePath}", driverFilePath)
  125.    End If
  126.    Dim driverDirPath As String = Path.GetDirectoryName(driverFilePath)
  127.    Dim driveFileName As String = Path.GetFileName(driverFilePath)
  128.  
  129.    If String.IsNullOrWhiteSpace(chromeFilePath) Then
  130.        Try
  131.            chromeFilePath = UtilSelenium.GetLatestCachedChromeFilePath()
  132.        Catch ex As Exception
  133.            Throw New InvalidOperationException($"Failed to resolve Chrome.exe file path from Selenium cache. See inner exception for details.", ex)
  134.        End Try
  135.    ElseIf Not String.IsNullOrWhiteSpace(chromeFilePath) AndAlso Not File.Exists(chromeFilePath) Then
  136.        Throw New FileNotFoundException($"Chrome.exe not found at the specified path: {chromeFilePath}", chromeFilePath)
  137.    End If
  138.  
  139.    If String.IsNullOrWhiteSpace(userDataDir) Then
  140.        Dim seCachePathEnvVar As String = Environment.GetEnvironmentVariable("SE_CACHE_PATH", EnvironmentVariableTarget.Process)
  141.  
  142.        If Not String.IsNullOrWhiteSpace(seCachePathEnvVar) Then
  143.            Dim ass As Assembly = Assembly.GetEntryAssembly()
  144.            Dim assName As String = ass.GetName().Name
  145.            userDataDir = Path.Combine(seCachePathEnvVar, $"chrome user data - {assName}")
  146.        Else
  147.            userDataDir = Path.Combine(AppContext.BaseDirectory, "chrome user data")
  148.        End If
  149.    End If
  150.    userDataDir = Path.GetFullPath(userDataDir)
  151.  
  152.    If String.IsNullOrWhiteSpace(profileName) Then
  153.        profileName = "Default"
  154.    End If
  155.  
  156.    If String.IsNullOrWhiteSpace(driverLogFilePath) Then
  157.        driverLogFilePath = Path.Combine(userDataDir, $"{profileName}\chromedriver.log")
  158.    End If
  159.  
  160.    Dim options As New ChromeOptions() With {
  161.        .AcceptInsecureCertificates = True,
  162.        .EnableDownloads = False,
  163.        .LeaveBrowserRunning = False,
  164.        .UnhandledPromptBehavior = UnhandledPromptBehavior.Ignore,
  165.        .UseStrictFileInteractability = True,
  166.        .BinaryLocation = chromeFilePath
  167.    }
  168.  
  169.    options.AddAdditionalOption("useAutomationExtension", False)
  170.    options.AddExcludedArgument("enable-automation")
  171.  
  172.    ' Add a set of default arguments optimized for browser automation.
  173.    Dim currentArgs As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) From {
  174.        "--allow-insecure-localhost",
  175.        "--disable-background-networking",
  176.        "--disable-backgrounding-occluded-windows",
  177.        "--disable-blink-features=AutomationControlled",
  178.        "--disable-default-apps",
  179.        "--disable-dev-shm-usage",
  180.        "--disable-features=Translate,TranslateUI",
  181.        "--disable-hang-monitor",
  182.        "--disable-notifications",
  183.        "--disable-popup-blocking",
  184.        "--disable-prompt-on-repost",
  185.        "--disable-sync",
  186.        "--ignore-certificate-errors",
  187.        "--ignore-ssl-errors",
  188.        "--lang=en-US",
  189.        "--no-first-run",
  190.        "--no-sandbox",
  191.        "--noerrdialogs",
  192.        "--remote-debugging-port=0", ' Or "--remote-debugging-pipe"
  193.        "--test-type",
  194.        $"--user-data-dir={userDataDir}",    ' Do NOT use double quotes around the path, as Chrome does not recognize it.
  195.        $"--profile-directory={profileName}" ' Do NOT use double quotes around the name, as Chrome does not recognize it.
  196.    }
  197.  
  198.    ' Add additional specific optimized arguments if headless mode is specified by the caller.
  199.    If headless Then
  200.        Dim headlessAdditionalArgs As String() = {
  201.            "--headless=New",
  202.            "--start-maximized",
  203.            "--disable-site-isolation-trials",
  204.            "--disable-web-security",
  205.            "--disable-gpu"
  206.        }
  207.        currentArgs.AddRange(headlessAdditionalArgs)
  208.    End If
  209.  
  210.    ' Add additional arguments specified by the caller, avoiding duplicates with the current arguments.
  211.    If additionalArguments IsNot Nothing AndAlso additionalArguments.Length > 0 Then
  212.  
  213.        Dim filteredAdditionalArgs As IEnumerable(Of String) =
  214.            From arg As String In additionalArguments
  215.            Where Not String.IsNullOrWhiteSpace(arg)
  216.            Select arg.Trim()
  217.  
  218.        currentArgs.AddRange(additionalArguments)
  219.    End If
  220.  
  221.    ' Add the final set of arguments to ChromeOptions.
  222.    options.AddArguments(currentArgs)
  223.  
  224.    ' Initialize ChromeDriverService with the specified driver file path and log file path, and configure its options.
  225.    refDriverService = ChromeDriverService.CreateDefaultService(driverDirPath, driveFileName)
  226.    With refDriverService
  227.        .DisableBuildCheck = False
  228.        .EnableAppendLog = False
  229.        .EnableVerboseLogging = False
  230.        .HideCommandPromptWindow = True
  231.        .LogLevel = Chromium.ChromiumDriverLogLevel.Info
  232.        .LogPath = driverLogFilePath
  233.        .ReadableTimestamp = True
  234.        .SuppressInitialDiagnosticInformation = False ' Note: If True, it hangs ChromeDriver initialization.
  235.    End With
  236.  
  237.    ' chromedriver.exe sometimes may cause an error if log file does not exists.
  238.    If Not File.Exists(driverLogFilePath) Then
  239.        Try
  240.            Directory.CreateDirectory(Path.GetDirectoryName(driverLogFilePath))
  241.        Catch ' Ignore errors.
  242.        End Try
  243.        Try
  244.            File.WriteAllText(driverLogFilePath, String.Empty)
  245.        Catch ' Ignore errors.
  246.        End Try
  247.    End If
  248.  
  249.    Dim driver As New ChromeDriver(refDriverService, options)
  250.  
  251.    ' Hide the Chrome.exe window / taskbar icon if not running in headless mode and specified by the caller.
  252.    If Not headless AndAlso hideNonHeadlessWindow Then
  253.        Dim chromeChilds As List(Of Process) = GetChildProcesses(refDriverService.ProcessId)
  254.        For Each chromeChild As Process In chromeChilds
  255.            Dim hwnd As IntPtr = chromeChild.MainWindowHandle
  256.            If hwnd <> IntPtr.Zero Then
  257.                Const SW_HIDE As Integer = 0
  258.                NativeMethods.ShowWindow(hwnd, SW_HIDE)
  259.            End If
  260.        Next
  261.    End If
  262.  
  263.    Return driver
  264. End Function

Módulo necesario NativeMethods:
Código
  1. Public Module NativeMethods
  2.  
  3.    <DllImport("user32.dll", SetLastError:=True)>
  4.    Public Function ShowWindow(hWnd As IntPtr, nCmdShow As Integer) As Boolean
  5.    End Function
  6.  
  7. End Module



Aquí les dejo también una función llamada KillDriverAndChildBrowsers, cuya finalidad es realizar una limpieza forzada de procesos asociados a drivers de automatización (como chromedriver) que hayan sido lanzados por la aplicación.

Esta función resulta especialmente útil en escenarios donde las sesiones de automatización no se cierran correctamente, ya sea por errores inesperados o cierres abruptos del proceso principal. En estos casos, quedarán procesos huérfanos corriendo en segundo plano, tanto del driver como de los navegadores asociados.

Esta función, la cual se debería llamar al conrolar el cierre de nuestro proceso principal, actúa como una medida de seguridad para garantizar una limpieza completa del entorno de ejecución del driver y el navegador, evitando acumulación de procesos residuales.

Código
  1. ''' <summary>
  2. ''' Forcefully terminates all instances of a specific Selenium driver process (e.g., "chromedriver")
  3. ''' that were spawned as child processes of the current application.
  4. ''' </summary>
  5. '''
  6. ''' <remarks>
  7. ''' This method is intended as a cleanup safeguard to prevent orphaned driver and child browser processes
  8. ''' when the application shuts down unexpectedly or fails to properly dispose Selenium sessions.
  9. ''' <para></para>
  10. ''' It ensures that any child processes created during automated browser sessions are fully terminated,
  11. ''' avoiding background instances that may continue running after the current process exits.
  12. ''' </remarks>
  13. '''
  14. ''' <param name="driverName">
  15. ''' The name of the driver executable to terminate, without the extension.
  16. ''' For example, <c>"chromedriver"</c>.
  17. ''' </param>
  18. <DebuggerStepThrough>
  19. Public Shared Sub KillDriverAndChildBrowsers(driverName As String)
  20.  
  21. #If NETCOREAPP Then
  22.    ArgumentNullException.ThrowIfNullOrWhiteSpace(driverName, NameOf(driverName))
  23.  
  24.    Dim processId As Integer = Environment.ProcessId
  25. #Else
  26.    If String.IsNullOrWhiteSpace(driverName) Then
  27.        Throw New ArgumentNullException(NameOf(driverName))
  28.    End If
  29.  
  30.    Dim processId As Integer = Process.GetCurrentProcess().Id
  31. #End If
  32.    Dim childProcesses As List(Of Process) = GetChildProcesses(processId)
  33.  
  34.    Dim childDriverProcesses As IEnumerable(Of Process) =
  35.        From p As Process In childProcesses
  36.        Where Not p?.HasExited AndAlso p?.ProcessName.Equals(driverName, StringComparison.InvariantCultureIgnoreCase)
  37.  
  38.    For Each p As Process In childDriverProcesses
  39.  
  40. #If NETCOREAPP Then
  41.        p.Kill(entireProcessTree:=True)
  42. #Else
  43.        Using taskkillProcess As New Process()
  44.            taskkillProcess.StartInfo.FileName = "taskkill"
  45.            taskkillProcess.StartInfo.Arguments = $"/PID {p.Id} /T /F"
  46.            taskkillProcess.StartInfo.UseShellExecute = False
  47.            taskkillProcess.StartInfo.CreateNoWindow = True
  48.  
  49.            taskkillProcess.Start()
  50.        End Using
  51. #End If
  52.    Next
  53.  
  54. End Sub



Por último, comparto esta otra función utilitaria, GetChildProcesses, que es necesaria por las otras dos funciones que he compartido en este mismo post.

Código
  1. ''' <summary>
  2. ''' Retrieves a list of child processes for the specified parent process ID.
  3. ''' </summary>
  4. '''
  5. ''' <param name="parentProcessId">
  6. ''' The ID of the parent process whose child processes should be retrieved.
  7. ''' </param>
  8. '''
  9. ''' <returns>
  10. ''' A <see cref="List(Of Process)"/> containing all child processes of the specified parent process.
  11. ''' </returns>
  12. <DebuggerStepThrough>
  13. Private Shared Function GetChildProcesses(parentProcessId As Integer) As List(Of Process)
  14.  
  15.    Dim children As New List(Of Process)()
  16.  
  17.    Dim scope As New ManagementScope("root\CIMV2")
  18.    Dim query As New ObjectQuery($"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId={parentProcessId}")
  19.    Dim options As New System.Management.EnumerationOptions() With {
  20.        .EnsureLocatable = False,
  21.        .ReturnImmediately = True,
  22.        .Rewindable = False,
  23.        .Timeout = TimeSpan.FromSeconds(5)
  24.    }
  25.  
  26.    scope.Connect()
  27.  
  28.    Using searcher As New ManagementObjectSearcher(scope, query, options)
  29.  
  30.        For Each proc As ManagementObject In searcher.Get()
  31.  
  32.            Dim pid As Integer = Convert.ToInt32(proc("ProcessId"))
  33.            Try
  34.                Dim childProc As Process = Process.GetProcessById(pid)
  35.                children.Add(childProc)
  36.            Catch ' Ignore. Process can no longer exists.
  37.            End Try
  38.        Next
  39.    End Using
  40.  
  41.    Return children
  42. End Function
En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #624 en: 14 Mayo 2026, 17:16 pm »

Ahora les muestro la clase ConsoleTerminationWatcher, generada por IA (la comparto tal cual, sin ninguna refactorización ni organización de código por mi parte... más allá de haberle añadido algo de documentación inicial), cuyo propósito es interceptar los eventos de cierre y terminación de una aplicación de consola para ejecutar una lógica arbitraria de limpieza, de forma segura y controlada.

💡 Tip: Esta clase es el complemento ideal para combinar con el uso del método KillDriverAndChildBrowsers que compartí en el post anterior a este, sobre la automatización con Selenium.

Ejemplo de uso generado por IA:
Código
  1. Imports System
  2. Imports System.Threading
  3.  
  4. Module Program
  5.  
  6.    ' Declared outside Main to keep it alive during the entire process lifetime
  7.    Private _watcher As ConsoleTerminationWatcher
  8.  
  9.    Sub Main()
  10.  
  11.        Console.WriteLine("Application started.")
  12.        Console.WriteLine("Press CTRL + C or close the console window to trigger termination handler.")
  13.        Console.WriteLine()
  14.  
  15.        _watcher = New ConsoleTerminationWatcher(
  16.            Sub()
  17.                Console.WriteLine()
  18.                Console.WriteLine("Termination event captured!")
  19.  
  20.                Try
  21.                    Console.WriteLine("Executing cleanup logic...")
  22.  
  23.                    ' Example: KillDriverAndChildBrowsers("chromedriver")
  24.  
  25.                    ' Generic cleanup simulation
  26.                    Thread.Sleep(1000)
  27.  
  28.                    Console.WriteLine("Cleanup finished successfully.")
  29.                Catch ex As Exception
  30.                    Console.WriteLine($"Error during cleanup: {ex.Message}")
  31.                End Try
  32.            End Sub)
  33.  
  34.        ' Simulated long-running process
  35.        Dim counter As Integer = 0
  36.  
  37.        While True
  38.            counter += 1
  39.            Console.WriteLine($"Running iteration {counter}")
  40.            Thread.Sleep(1500)
  41.        End While
  42.  
  43.    End Sub
  44.  
  45. End Module

La clase ConsoleTerminationWatcher:
Código
  1. Imports System.ComponentModel
  2. Imports System.Diagnostics
  3. Imports System.Runtime.InteropServices
  4.  
  5. ''' <summary>
  6. ''' Provides a wrapper for the Win32 SetConsoleCtrlHandler function.
  7. ''' <para></para>
  8. ''' This class ensures that cleanup logic is executed when the console window is closed or interrupted.
  9. ''' </summary>
  10. Public Class ConsoleTerminationWatcher : Implements IDisposable
  11.  
  12.    ''' <summary>
  13.    ''' Adds or removes an application-defined HandlerRoutine function from the list of handler functions for the calling process.
  14.    ''' </summary>
  15.    '''
  16.    ''' <param name="handler">
  17.    ''' A pointer to the application-defined HandlerRoutine function to be added or removed.
  18.    ''' </param>
  19.    '''
  20.    ''' <param name="add">
  21.    ''' If this parameter is TRUE, the handler is added; if it is FALSE, the handler is removed.
  22.    ''' </param>
  23.    '''
  24.    ''' <returns>
  25.    ''' Returns TRUE if the function succeeds; otherwise, FALSE.
  26.    ''' </returns>
  27.    <DllImport("kernel32.dll", SetLastError:=True)>
  28.    Private Shared Function SetConsoleCtrlHandler(handler As ConsoleEventDelegate, add As Boolean) As Boolean
  29.    End Function
  30.  
  31.    ''' <summary>
  32.    ''' An application-defined function used with the SetConsoleCtrlHandler function.
  33.    ''' </summary>
  34.    '''
  35.    ''' <param name="eventType">
  36.    ''' The type of control signal received by the handler.
  37.    ''' </param>
  38.    '''
  39.    ''' <returns>
  40.    ''' If the function handles the control signal, it should return TRUE.
  41.    ''' <para></para>
  42.    ''' If it returns FALSE, the next handler function in the list is called.
  43.    ''' </returns>
  44.    Private Delegate Function ConsoleEventDelegate(eventType As ConsoleEventType) As Boolean
  45.  
  46.    ''' <summary>
  47.    ''' Enumerates the control signal types received by the console control handler.
  48.    ''' </summary>
  49.    Private Enum ConsoleEventType As Integer
  50.  
  51.        ''' <summary>
  52.        ''' A CTRL+C signal was received.
  53.        ''' </summary>
  54.        CTRL_C_EVENT = 0
  55.  
  56.        ''' <summary>
  57.        ''' A CTRL+BREAK signal was received.
  58.        ''' </summary>
  59.        CTRL_BREAK_EVENT = 1
  60.  
  61.        ''' <summary>
  62.        ''' The user closed the console window.
  63.        ''' </summary>
  64.        CTRL_CLOSE_EVENT = 2
  65.  
  66.        ''' <summary>
  67.        ''' The user is logging off.
  68.        ''' </summary>
  69.        CTRL_LOGOFF_EVENT = 5
  70.  
  71.        ''' <summary>
  72.        ''' The system is shutting down.
  73.        ''' </summary>
  74.        CTRL_SHUTDOWN_EVENT = 6
  75.  
  76.    End Enum
  77.  
  78.    ''' <summary>
  79.    ''' Holds the delegate reference to prevent it from being collected by the Garbage Collector.
  80.    ''' </summary>
  81.    Private ReadOnly _handler As ConsoleEventDelegate
  82.  
  83.    ''' <summary>
  84.    ''' The action to be executed when a termination event occurs.
  85.    ''' </summary>
  86.    Private ReadOnly _cleanupAction As Action
  87.  
  88.    ''' <summary>
  89.    ''' Tracks the disposal status of the instance.
  90.    ''' </summary>
  91.    Private _disposedValue As Boolean = False
  92.  
  93.    ''' <summary>
  94.    ''' Initializes a new instance of the <see cref="ConsoleTerminationWatcher"/> class.
  95.    ''' </summary>
  96.    '''
  97.    ''' <param name="cleanupAction">
  98.    ''' The action to execute when the console terminates (e.g., process cleanup).
  99.    ''' </param>
  100.    '''
  101.    ''' <exception cref="ArgumentNullException">
  102.    ''' Thrown when cleanupAction is null.
  103.    ''' </exception>
  104.    '''
  105.    ''' <exception cref="Win32Exception">
  106.    ''' Thrown when the Win32 API registration fails.
  107.    ''' </exception>
  108.    <DebuggerStepThrough>
  109.    Public Sub New(cleanupAction As Action)
  110.  
  111.        If cleanupAction Is Nothing Then
  112.            Throw New ArgumentNullException(NameOf(cleanupAction))
  113.        End If
  114.  
  115.        Me._cleanupAction = cleanupAction
  116.        Me._handler = New ConsoleEventDelegate(AddressOf Me.ConsoleEventCallback)
  117.  
  118.        If Not SetConsoleCtrlHandler(Me._handler, True) Then
  119.            Dim errorCode As Integer = Marshal.GetLastWin32Error()
  120.            Throw New Win32Exception(errorCode, $"Failed to register console control handler. Win32 Error Code: {errorCode}")
  121.        End If
  122.    End Sub
  123.  
  124.    ''' <summary>
  125.    ''' The callback method invoked by the Windows Operating System when a console event occurs.
  126.    ''' </summary>
  127.    '''
  128.    ''' <param name="eventType">
  129.    ''' The type of console event triggered.
  130.    ''' </param>
  131.    '''
  132.    ''' <returns>
  133.    ''' Always returns FALSE to allow the process to terminate normally after cleanup.
  134.    ''' </returns>
  135.    <DebuggerStepThrough>
  136.    Private Function ConsoleEventCallback(eventType As ConsoleEventType) As Boolean
  137.  
  138.        Select Case eventType
  139.            Case ConsoleEventType.CTRL_C_EVENT,
  140.                 ConsoleEventType.CTRL_BREAK_EVENT,
  141.                 ConsoleEventType.CTRL_CLOSE_EVENT,
  142.                 ConsoleEventType.CTRL_LOGOFF_EVENT,
  143.                 ConsoleEventType.CTRL_SHUTDOWN_EVENT
  144.  
  145.                ' Trigger the external cleanup logic
  146.                Me._cleanupAction.Invoke()
  147.  
  148.            Case Else
  149.                ' Ignore other events.
  150.        End Select
  151.  
  152.        Return False
  153.    End Function
  154.  
  155.    ''' <summary>
  156.    ''' Releases the resources used by the <see cref="ConsoleTerminationWatcher"/>.
  157.    ''' </summary>
  158.    '''
  159.    ''' <param name="disposing">
  160.    ''' True to release both managed and unmanaged resources; False to release only unmanaged resources.
  161.    ''' </param>
  162.    <DebuggerStepThrough>
  163.    Protected Overridable Sub Dispose(disposing As Boolean)
  164.        If Not Me._disposedValue Then
  165.            If disposing Then
  166.                ' Free managed objects here if any.
  167.            End If
  168.  
  169.            ' Unregister the Win32 handler (Unmanaged resource cleanup)
  170.            ' This must happen even if disposing is False (called from Finalizer)
  171.            SetConsoleCtrlHandler(Me._handler, False)
  172.  
  173.            Me._disposedValue = True
  174.        End If
  175.    End Sub
  176.  
  177.    ''' <summary>
  178.    ''' Finalizes an instance of the <see cref="ConsoleTerminationWatcher"/> class.
  179.    ''' </summary>
  180.    <DebuggerStepThrough>
  181.    Protected Overrides Sub Finalize()
  182.        ' Ensure the Win32 hook is removed if the programmer forgot to call Dispose
  183.        Me.Dispose(disposing:=False)
  184.        MyBase.Finalize()
  185.    End Sub
  186.  
  187.    ''' <summary>
  188.    ''' Releases the resources used by the <see cref="ConsoleTerminationWatcher"/>.
  189.    ''' </summary>
  190.    <DebuggerStepThrough>
  191.    Public Sub Dispose() Implements IDisposable.Dispose
  192.        Me.Dispose(disposing:=True)
  193.        GC.SuppressFinalize(Me)
  194.    End Sub
  195.  
  196. End Class

En línea



Eleкtro
Ex-Staff
*
Desconectado Desconectado

Mensajes: 9.997



Ver Perfil
Re: Librería de Snippets para VB.NET !! (Compartan aquí sus snippets)
« Respuesta #625 en: 14 Mayo 2026, 17:46 pm »

Por último, les muestro tres métodos de extensión: WaitForPageReady, WaitForElement e IsCloudflareChallengeRequired, para usar con la interfaz IWebDriver de Selenium, implementada en el tipo ChromeDriver. Loss tres muy útiles, y para mi son de uso muy común.

WaitForPageReady y WaitForElement encapsulan patrones habituales de espera y validación que normalmente se terminan reescribiendo en múltiples proyectos, centralizando la lógica en una capa reutilizable y consistente.

WaitForPageReady permite esperar a que la página web alcance el estado de carga completa (document.readyState = "complete"), asegurando que el navegador ha finalizado la carga inicial del documento HTML. Adicionalmente, incorpora mecanismos opcionales para esperar un tiempo adicional tras la carga inicial y detectar estabilidad del DOM (evitando cambios asíncronos posteriores).

WaitForElement implementa un patrón de espera explícita para elementos del DOM. Su funcionalidad es simple pero crítica: Espera hasta que un elemento exista en el DOM, verifica que esté visible (Displayed), verifica que sea interactuable (Enabled), y devuelve directamente el elemento listo para su uso.

La tercerca extensión, IsCloudflareChallengeRequired, permite detectar si la página actual está bloqueada por un desafío de Cloudflare. Este tipo de protección puede impedir completamente la automatización web si no se detecta correctamente, por lo que esta función actúa como una capa de diagnóstico temprano. Nota: Puede que mi implementación de detección no sea sofisticada, es realmente una heurística muy simple basada en validación de texto, la cual ha sido eficaz 100% en todos los escenarios que he probado, pero no sé si abarcará escenarios variantes que puedan ocasionar las páginas protegidas por Cloudflare.

Módulo IWebDriverExtensions:

Código
  1. #Region " Option Statements "
  2.  
  3. Option Strict On
  4. Option Explicit On
  5. Option Infer Off
  6.  
  7. #End Region
  8.  
  9. #Region " Imports "
  10.  
  11. Imports OpenQA.Selenium
  12. Imports OpenQA.Selenium.Internal
  13. Imports OpenQA.Selenium.Support.UI
  14.  
  15. #End Region
  16.  
  17. #Region " IWebDriver Extensions "
  18.  
  19. ' ReSharper disable once CheckNamespace
  20.  
  21. 'Namespace DevCase.ThirdParty.Selenium.Extensions.IWebDriverExtensions
  22.  
  23.    ''' <summary>
  24.    ''' Provides extension methods to use with the <see cref="IWebDriver"/> interface.
  25.    ''' </summary>
  26.    '''
  27.    ''' <remarks>
  28.    ''' Note: Some functionalities of this assembly may require to install one or all of the listed NuGet packages:
  29.    ''' <para></para>
  30.    ''' <see href="https://www.nuget.org/packages/Selenium.Support">Selenium.Support by Selenium Committers</see>
  31.    ''' <para></para>
  32.    ''' <see href="https://www.nuget.org/packages/Selenium.WebDriver">Selenium.WebDriver by Selenium Committers</see>
  33.    ''' <para></para>
  34.    ''' </remarks>
  35.    <HideModuleName>
  36.    Public Module IWebDriverExtensions
  37.  
  38. #Region " Public Extension Methods "
  39.  
  40.        ''' <summary>
  41.        ''' Waits for the current web page in the specified <see cref="IWebDriver"/> instance
  42.        ''' to report a ready state of <c>"complete"</c>. And optionally, it can also
  43.        ''' wait for any pending dynamic updates in the DOM to complete after the page
  44.        ''' has reported a ready state of <c>"complete"</c>.
  45.        ''' </summary>
  46.        '''
  47.        ''' <remarks>
  48.        ''' Note: Some functionalities of this assembly may require to install one or all of the listed NuGet packages:
  49.        ''' <para></para>
  50.        ''' <see href="https://www.nuget.org/packages/Selenium.Support">Selenium.Support by Selenium Committers</see>
  51.        ''' <para></para>
  52.        ''' <see href="https://www.nuget.org/packages/Selenium.WebDriver">Selenium.WebDriver by Selenium Committers</see>
  53.        ''' <para></para>
  54.        ''' </remarks>
  55.        '''
  56.        ''' <param name="driver">
  57.        ''' The <see cref="IWebDriver"/> instance.
  58.        ''' </param>
  59.        '''
  60.        ''' <param name="afterPageReadyDelay">
  61.        ''' Optional. A <see cref="TimeSpan"/> representing the delay to wait
  62.        ''' <b>after</b> the web page reports a ready state of <c>"complete"</c>,
  63.        ''' before the method returns.
  64.        ''' <para></para>
  65.        ''' This can be useful to allow background scripts, animations, or
  66.        ''' asynchronous content to finish initializing after the document is loaded.
  67.        ''' <para></para>
  68.        ''' Default value is null.
  69.        ''' </param>
  70.        '''
  71.        ''' <param name="waitForDomIdle">
  72.        ''' Optional. When set to <see langword="True"/>, the method starts waiting for any pending dynamic updates in the DOM
  73.        ''' to complete after the page has reported a ready state of <c>"complete"</c>.
  74.        ''' <para></para>
  75.        ''' Default value is <see langword="False"/>.
  76.        ''' <para></para>
  77.        ''' &#9888;&#65039; Do not set this parameter to <see langword="True"/> for web pages with continuously changing DOM elements
  78.        ''' (e.g., pages with animations, snow effects, or real-time updates).
  79.        ''' </param>
  80.        '''
  81.        ''' <param name="timeoutSeconds">
  82.        ''' Optional. The maximum time in seconds to wait for the page to report a ready state of <c>"complete"</c>,
  83.        ''' and for any pending dynamic updates in the DOM to complete if
  84.        ''' <paramref name="waitForDomIdle"/> is set to <see langword="True"/>.
  85.        ''' <para></para>
  86.        ''' Default value is 30 seconds.
  87.        ''' <para></para>
  88.        ''' If the condition is not met within this time, a <see cref="WebDriverTimeoutException"/> is thrown.
  89.        ''' </param>
  90.        '''
  91.        ''' <param name="throwOnTimeout">
  92.        ''' Optional. When set to <see langword="True"/>,
  93.        ''' a <see cref="WebDriverTimeoutException"/> will be thrown if the time specified in
  94.        ''' <paramref name="timeoutSeconds"/> parameter reaches while waiting the
  95.        ''' web page to report a ready state of <c>"complete"</c>,
  96.        ''' or while waiting for any pending dynamic updates in the DOM to complete after the
  97.        ''' page has reported a ready state of <c>"complete"</c>.
  98.        ''' <para></para>
  99.        ''' Default value is <see langword="True"/>.
  100.        ''' </param>
  101.        <DebuggerStepThrough>
  102.        <Extension>
  103.        <EditorBrowsable(EditorBrowsableState.Always)>
  104.        Public Sub WaitForPageReady(driver As IWebDriver,
  105.                           Optional afterPageReadyDelay As TimeSpan = Nothing,
  106.                           Optional waitForDomIdle As Boolean = False,
  107.                           Optional timeoutSeconds As Integer = 30,
  108.                           Optional throwOnTimeout As Boolean = True)
  109.  
  110.            If timeoutSeconds <= 0 Then
  111.                Throw New ArgumentException("Timeout must be a value greater than zero.", NameOf(timeoutSeconds))
  112.            End If
  113.  
  114.            Dim js As IJavaScriptExecutor = TryCast(driver, IJavaScriptExecutor)
  115.            If js Is Nothing Then
  116.                Throw New ArgumentException("Driver must support javascript execution", NameOf(driver))
  117.            End If
  118.  
  119.            Dim wait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With {
  120.                .PollingInterval = TimeSpan.FromMilliseconds(500)
  121.            }
  122.  
  123.            Dim startTime As Date = Date.Now
  124.            Dim domLength As Integer
  125.  
  126.            wait.Until(
  127.                Function(d As IWebDriver)
  128.                    Try
  129.                        Dim readyState As String = js.ExecuteScript("if (document.readyState) return document.readyState;").ToString()
  130.                        If readyState.Equals("complete", StringComparison.OrdinalIgnoreCase) Then
  131.                            domLength = d.PageSource.Length
  132.                            Return True
  133.                        Else
  134.                            Return False
  135.                        End If
  136.  
  137.                    Catch ex As WebDriverTimeoutException
  138.                        If throwOnTimeout Then
  139.                            Throw
  140.                        End If
  141.                        Return True
  142.  
  143.                    Catch ex As InvalidOperationException ' Window is no longer available
  144. #If Not NETCOREAPP Then
  145.                        Return ex.Message.IndexOf("unable to get browser", StringComparison.OrdinalIgnoreCase) >= 0
  146. #Else
  147.                        Return ex.Message.Contains("unable to get browser", StringComparison.OrdinalIgnoreCase)
  148. #End If
  149.  
  150.                    Catch ex As WebDriverException ' Browser is no longer available
  151. #If Not NETCOREAPP Then
  152.                        Return ex.Message.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0
  153. #Else
  154.                        Return ex.Message.Contains("unable to connect", StringComparison.OrdinalIgnoreCase)
  155. #End If
  156.  
  157.                    Catch ex As Exception
  158.                        Return True
  159.  
  160.                    End Try
  161.                End Function)
  162.  
  163.            If afterPageReadyDelay <> Nothing Then
  164.                Thread.Sleep(afterPageReadyDelay)
  165.            End If
  166.  
  167.            ' Even when "document.readyState()" returns "complete", web pages can continue to modify
  168.            ' the DOM dynamically after the initial load. This can occur due to asynchronous scripts,
  169.            ' client-side rendering frameworks (such as React, Angular, or Vue), AJAX/fetch requests
  170.            ' that inject additional content and rewrites portions of the page, etc.
  171.            '
  172.            ' As a result, the page source may still change for a short period of time even though the
  173.            ' web browser reports that the document has finished loading.
  174.            '
  175.            ' This check ensures that the HTML content remains stable (IDLE) before exiting.
  176.            If waitForDomIdle Then
  177.  
  178.                Dim newDomLength As Integer = driver.PageSource.Length
  179.                If newDomLength <> domLength Then
  180.  
  181.                    Dim elapsedTime As TimeSpan = Date.Now - startTime
  182.                    timeoutSeconds -= CInt(elapsedTime.TotalSeconds)
  183.                    If timeoutSeconds <= 0 Then
  184.                        timeoutSeconds = 1
  185.                    End If
  186.  
  187.                    Dim domWait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With {
  188.                        .PollingInterval = TimeSpan.FromSeconds(1)
  189.                    }
  190.  
  191.                    domWait.Until(Function(d As IWebDriver)
  192.                                      Try
  193.                                          Dim length As Integer = d.PageSource.Length
  194.                                          Dim idle As Boolean = (length = newDomLength)
  195.                                          newDomLength = length
  196.                                          Return idle
  197.  
  198.                                      Catch ex As WebDriverTimeoutException
  199.                                          If throwOnTimeout Then
  200.                                              Throw
  201.                                          End If
  202.                                          Return True
  203.  
  204.                                      Catch ex As InvalidOperationException ' Window is no longer available
  205. #If Not NETCOREAPP Then
  206.                                          Return ex.Message.IndexOf("unable to get browser", StringComparison.OrdinalIgnoreCase) >= 0
  207. #Else
  208.                                          Return ex.Message.Contains("unable to get browser", StringComparison.OrdinalIgnoreCase)
  209. #End If
  210.  
  211.                                      Catch ex As WebDriverException ' Browser is no longer available
  212. #If Not NETCOREAPP Then
  213.                                          Return ex.Message.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0
  214. #Else
  215.                                          Return ex.Message.Contains("unable to connect", StringComparison.OrdinalIgnoreCase)
  216. #End If
  217.  
  218.                                      Catch ex As Exception
  219.                                          Return True
  220.  
  221.                                      End Try
  222.                                  End Function)
  223.                End If
  224.            End If
  225.        End Sub
  226.  
  227.        ''' <summary>
  228.        ''' Waits until an element matching the specified <see cref="By"/> selector is present
  229.        ''' in the DOM of the specified <see cref="IWebDriver"/>.
  230.        ''' </summary>
  231.        '''
  232.        ''' <param name="driver">
  233.        ''' The <see cref="IWebDriver"/> instance.
  234.        ''' </param>
  235.        '''
  236.        ''' <param name="by">
  237.        ''' The <see cref="By"/> selector used to locate the element.
  238.        ''' </param>
  239.        '''
  240.        ''' <param name="timeoutSeconds">
  241.        ''' Optional. The maximum number of seconds to wait. Default is 30 seconds.
  242.        ''' <para></para>
  243.        ''' If the condition is not met within this time, a <see cref="WebDriverTimeoutException"/> is thrown.
  244.        ''' </param>
  245.        '''
  246.        ''' <returns>
  247.        ''' If the function succeds, returns the found <see cref="IWebElement"/>.
  248.        ''' </returns>
  249.        <Extension>
  250.        <DebuggerStepThrough>
  251.        <EditorBrowsable(EditorBrowsableState.Always)>
  252.        Public Function WaitForElement(driver As IWebDriver, by As By, Optional timeoutSeconds As Integer = 30) As IWebElement
  253.  
  254.            If timeoutSeconds <= 0 Then
  255.                Throw New ArgumentException("Timeout must be a value greater than zero.", NameOf(timeoutSeconds))
  256.            End If
  257.  
  258.            Dim wait As New WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds)) With {
  259.                .PollingInterval = TimeSpan.FromMilliseconds(250)
  260.            }
  261.  
  262.            Return wait.Until(Function(d As IWebDriver)
  263.                                  Try
  264.                                      Dim element As IWebElement = d.FindElement(by)
  265.                                      ' Check if element is displayed and enabled (interactable).
  266.                                      If (element IsNot Nothing) AndAlso
  267.                                          element.Displayed AndAlso
  268.                                          element.Enabled Then
  269.  
  270.                                          Return element
  271.                                      End If
  272.  
  273.                                  Catch ex As NoSuchElementException
  274.                                      ' Ignore.
  275.  
  276.                                  End Try
  277.  
  278.                                  ' Return Nothing to continue waiting until timeout.
  279.                                  Return Nothing
  280.                              End Function)
  281.  
  282.        End Function
  283.  
  284.        ''' <summary>
  285.        ''' Determines whether the current web page loaded in the specified <see cref="IWebDriver"/> is protected by a Cloudflare challenge,
  286.        ''' so a navigation block or anti-bot challenge is currently being displayed instead of the expected content.
  287.        ''' </summary>
  288.        '''
  289.        ''' <param name="driver">
  290.        ''' The <see cref="IWebDriver"/> instance.
  291.        ''' </param>
  292.        '''
  293.        ''' <returns>
  294.        ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page;
  295.        ''' otherwise, <see langword="False"/>.
  296.        ''' </returns>
  297.        <Extension>
  298.        <DebuggerStepThrough>
  299.        Public Function IsCloudflareChallengeRequired(driver As IWebDriver) As Boolean
  300.  
  301. #If NETCOREAPP Then
  302.            ArgumentNullException.ThrowIfNull(driver)
  303. #Else
  304.            If driver Is Nothing Then
  305.                Throw New ArgumentNullException(NameOf(driver))
  306.            End If
  307. #End If
  308.            Dim pageSource As String = driver.PageSource
  309.            Dim pageTitle As String = driver.Title
  310.  
  311.            Return UtilWeb.IsCloudflareChallengeRequired(pageSource, pageTitle)
  312.        End Function
  313.  
  314. #End Region
  315.  
  316.    End Module
  317.  
  318. 'End Namespace
  319.  
  320. #End Region

Clase adicional UtilWeb, necesaria para poder usar la extensión IsCloudflareChallengeRequired (del módulo IWebDriverExtensions compartido aquí arriba):

Código
  1. #Region " Option Statements "
  2.  
  3. Option Strict On
  4. Option Explicit On
  5. Option Infer Off
  6.  
  7. #End Region
  8.  
  9. #Region " Imports "
  10.  
  11. #End Region
  12.  
  13. #Region " Web Util "
  14.  
  15. ' ReSharper disable once CheckNamespace
  16.  
  17. 'Namespace DevCase.Core.Networking.Common
  18.  
  19. ''' <summary>
  20. ''' Provides web-related utility functions.
  21. ''' </summary>
  22. Public NotInheritable Class UtilWeb
  23.  
  24. #Region " Constructors "
  25.  
  26.    ''' <summary>
  27.    ''' Prevents a default instance of the <see cref="UtilWeb"/> class from being created.
  28.    ''' </summary>
  29.    <DebuggerNonUserCode>
  30.    Private Sub New()
  31.    End Sub
  32.  
  33. #End Region
  34.  
  35. #Region " Public Methods "
  36.  
  37.    ''' <summary>
  38.    ''' Determines whether the provided HTML source-code indicates that a Cloudflare challenge is required to access the page.
  39.    ''' </summary>
  40.    '''
  41.    ''' <param name="pageSource">
  42.    ''' The raw HTML source code.
  43.    ''' </param>
  44.    '''
  45.    ''' <param name="pageTitle">
  46.    ''' Optional. The title of the web page.
  47.    ''' </param>
  48.    '''
  49.    ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page;
  50.    ''' otherwise, <see langword="False"/>.
  51.    <DebuggerStepThrough>
  52.    Public Shared Function IsCloudflareChallengeRequired(pageSource As String, pageTitle As String) As Boolean
  53.  
  54.        If String.IsNullOrWhiteSpace(pageSource) Then
  55.            Return False
  56.        End If
  57.  
  58.        Dim challengeIndicators As String() = {
  59.            "challenge-error-text",
  60.            "/cdn-cgi/challenge-platform",
  61.            "window._cf_chl_opt",
  62.            "<title>Just a moment...</title>"
  63.        }
  64.  
  65.        For Each indicator As String In challengeIndicators
  66. #If NETCOREAPP Then
  67.            If pageSource.Contains(indicator, StringComparison.OrdinalIgnoreCase) Then
  68.                Return True
  69.            End If
  70. #Else
  71.            If pageSource.IndexOf(indicator, StringComparison.OrdinalIgnoreCase) >= 0 Then
  72.                Return True
  73.            End If
  74. #End If
  75.        Next
  76.  
  77.        Return String.Equals(pageTitle, "Just a moment...", StringComparison.OrdinalIgnoreCase)
  78.    End Function
  79.  
  80. '    ''' <summary>
  81. '    ''' Sends an HTTP request to the specified URL to determine whether
  82. '    ''' a Cloudflare challenge is required to load the web page that points to.
  83. '    ''' </summary>
  84. '    '''
  85. '    ''' <param name="url">
  86. '    ''' The URL to check.
  87. '    ''' </param>
  88. '    '''
  89. '    ''' <returns>
  90. '    ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page;
  91. '    ''' otherwise, <see langword="False"/>.
  92. '    ''' </returns>
  93. '    <DebuggerStepThrough>
  94. '    Public Shared Function IsCloudflareChallengeRequired(url As String) As Boolean
  95. '
  96. '        Using handler As New HttpClientHandler() With {
  97. '                .AllowAutoRedirect = True,
  98. '                .AutomaticDecompression = DecompressionMethods.GZip Or DecompressionMethods.Deflate
  99. '            }
  100. '
  101. '            Using client As New HttpClient(handler)
  102. '                Dim resp As HttpResponseMessage = client.GetAsync(url).ConfigureAwait(False).GetAwaiter().GetResult()
  103. '                Dim pageSource As String = resp.Content.ReadAsStringAsync().ConfigureAwait(False).GetAwaiter().GetResult()
  104. '
  105. '                Return (resp.StatusCode <> HttpStatusCode.OK) AndAlso
  106. '                    UtilWeb.IsCloudflareChallengeRequired(pageSource, pageTitle:=Nothing)
  107. '            End Using
  108. '        End Using
  109. '    End Function
  110. '
  111. '    ''' <summary>
  112. '    ''' Sends an HTTP request to the specified <see cref="Uri"/> to determine whether
  113. '    ''' a Cloudflare challenge is required to load the web page that points to.
  114. '    ''' </summary>
  115. '    '''
  116. '    ''' <param name="uri">
  117. '    ''' The <see cref="Uri"/> to check.
  118. '    ''' </param>
  119. '    '''
  120. '    ''' <returns>
  121. '    ''' <see langword="True"/> if a Cloudflare challenge is required to load the web page;
  122. '    ''' otherwise, <see langword="False"/>.
  123. '    ''' </returns>
  124. '    <DebuggerStepThrough>
  125. '    Public Shared Function IsCloudflareChallengeRequired(uri As Uri) As Boolean
  126. '
  127. '        Return UtilWeb.IsCloudflareChallengeRequired(uri.ToString())
  128. '    End Function
  129.  
  130. #End Region
  131.  
  132. End Class
  133.  
  134. 'End Namespace
  135.  
  136. #End Region
« Última modificación: 15 Mayo 2026, 00:43 am por Eleкtro » En línea



Páginas: 1 ... 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 [63] Ir Arriba Respuesta Imprimir 

Ir a:  

WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines