Selenium sigue siendo una opción popular para el web scraping y las pruebas mediante automatización del navegador. Estas tareas se escriben tradicionalmente utilizando lenguajes de scripting, incluido PowerShell. Para aprovechar las capacidades de Selenium con PowerShell, la comunidad de desarrolladores creó enlaces dedicados que hacen posible esta integración.
Sin embargo, el web scraping se ha vuelto cada vez más desafiante debido a los sofisticados sistemas anti-bot como Cloudflare. En esta guía completa, explorará:
Comenzar con Selenium y PowerShell
Interactuar con páginas web en un navegador
Evitar bloqueos de sistemas anti-bot
Resolver desafíos de Cloudflare con Scrapeless Scraping Browser
¡Vamos a sumergirnos!
¿Se puede usar Selenium con PowerShell?
Selenium es la biblioteca de navegador headless más popular en la comunidad de TI. Su API rica y consistente la hace perfecta para crear scripts de automatización del navegador para pruebas y web scraping.
PowerShell es un shell de comandos potente disponible en entornos de cliente y servidor de Windows. La automatización del navegador complementa perfectamente la creación de scripts, razón por la cual la comunidad de desarrolladores creó selenium-powershell, un puerto de Selenium WebDriver para PowerShell.
Aunque la biblioteca actualmente busca mantenedores, sigue siendo el módulo principal para la automatización web en PowerShell.
Nota: Si necesita repasar los conceptos básicos antes de comenzar con este tutorial de Selenium PowerShell, revise los fundamentos del scraping de navegador headless y el web scraping con PowerShell.
Web Scraping con Selenium y PowerShell: Conceptos básicos
Nota: Los ejemplos de este artículo están diseñados para sistemas operativos Windows, ya que algunas funciones como el módulo Selenium-PowerShell y los objetos COM son específicos de Windows. Si está utilizando macOS o Linux, es posible que deba adaptar estos scripts según corresponda.
En esta sección, presentaremos los conceptos básicos del uso de Selenium con PowerShell. Antes de pasar a sitios protegidos por Cloudflare más complejos como G2, es útil comprender los fundamentos.
Nuestro objetivo: Reseñas de G2
Para este tutorial, nos centraremos en el scraping de reseñas de G2, específicamente reseñas de Airtable en: https://www.g2.com/products/airtable/reviews. Este es un ejemplo del mundo real de un sitio protegido por Cloudflare que presenta desafíos para los métodos de scraping tradicionales.
Esto es lo que hace que G2 sea un objetivo interesante:
Está protegido por Cloudflare, que bloquea la mayoría de los intentos básicos de scraping
Contiene reseñas valiosas de software empresarial que podrían ser útiles para la investigación de mercado
Las reseñas están estructuradas de una manera que las hace ideales para la extracción de datos
Representa el tipo de datos empresariales de alto valor que a menudo se encuentran detrás de los sistemas anti-bot
Primero, veamos cómo un enfoque tradicional de Selenium con PowerShell tendría dificultades con este sitio y luego cómo Scrapeless puede resolver el problema.
Obtener el controlador Chrome correcto (febrero de 2025)
Un paso crítico al configurar Selenium con PowerShell es asegurarse de tener la versión correcta de ChromeDriver que coincida con su navegador Chrome. A partir del 28 de febrero de 2025, estas son las últimas versiones de Chrome y sus controladores web correspondientes:
Chrome Estable (133.0.6943.141):
ChromeDriver para Windows de 64 bits: chromedriver-win64.zip
ChromeDriver para Windows de 32 bits: chromedriver-win32.zip
ChromeDriver para Mac (Intel): chromedriver-mac-x64.zip
ChromeDriver para Mac (M1/M2): chromedriver-mac-arm64.zip
ChromeDriver para Linux: chromedriver-linux64.zip
Si está utilizando una versión diferente de Chrome (Beta, Dev o Canary), asegúrese de descargar el ChromeDriver correspondiente de la sección correspondiente de la página de descarga de Chrome para pruebas.
Descargue el ChromeDriver adecuado para su sistema, extraiga el archivo ZIP y coloque el ejecutable en la carpeta de su proyecto. Luego, haga referencia a esta ubicación en su script de PowerShell:
El uso de la versión correcta de ChromeDriver es esencial; si hay una discrepancia de versión, Selenium arrojará errores y no podrá controlar Chrome correctamente.
Intentando raspar G2 con Selenium básico
Ahora, intentemos acceder a las reseñas de G2 con nuestra configuración básica de Selenium:
Copy
Import-Module -Name Selenium
# Inicializar Selenium WebDriver
$Driver = Start-SeChrome -WebDriverDirectory './chromedriver-win64' -Headless
# Visitar la página de reseñas de G2
Enter-SeUrl 'https://www.g2.com/products/airtable/reviews' -Driver $Driver
# Esperar un momento a que se cargue la página
Start-Sleep -Seconds 5
# Obtener el título de la página
$Title = $Driver.Title
Write-Host "Título de la página: $Title"
# Imprimir el origen de la página
$Html = $Driver.PageSource
$Html | Out-File -FilePath "g2_response.html"
# Cerrar el navegador
Stop-SeDriver -Driver $Driver
Cuando ejecute este script, en lugar de obtener la página real de reseñas de G2, probablemente verá:
La página de desafío de Cloudflare
Un título como "Solo un momento..." o "Comprobando su navegador..."
HTML que contiene el desafío de JavaScript de Cloudflare
Esto se debe a que Cloudflare detecta navegadores automatizados como Selenium y presenta un desafío que las configuraciones básicas de Selenium no pueden resolver automáticamente.
El desafío de scraping de G2: Comprender la estructura de la reseña
Antes de intentar extraer datos, comprendamos lo que estamos intentando raspar. Las reseñas de G2 tienen un formato estructurado que incluye:
Título de la reseña: generalmente un resumen citado de la reseña
Calificación: se muestra como estrellas (de 5)
Fecha: cuándo se publicó la reseña
Texto de la reseña: dividido en secciones como "¿Qué te gusta más?" y "¿Qué no te gusta?"
Información del usuario: detalles sobre quién escribió la reseña
Así es como se ve una reseña típica de G2 en la estructura HTML:
Copy
<div class="paper__bd">
<div class="d-f mb-1">
<div class="f-1 d-f ai-c mb-half-small-only">
<div class="stars large xlarge--medium-down stars-10"></div>
<div class="time-stamp pl-4th ws-nw">
<span><span class="x-current-review-date" data-poison-date="">
<meta content="2025-02-11" itemprop="datePublished">
<time datetime="2025-02-11">Feb 11, 2025</time>
</span></span>
</div>
</div>
</div>
<div class="d-f">
<div class="f-1">
<div data-poison="">
<a class="pjax" href="https://www.g2.com/products/airtable/reviews/airtable-review-10822745">
<div class="m-0 l2" itemprop="name">"Helped us get a handle on our complicated team-based scheduling and management."</div>
</a>
</div>
<div data-poison-text="">
<div itemprop="reviewBody">
<div>
<div class="l5 mt-2">What do you like best about Airtable?</div>
<div>
<p class="formatted-text">The best part about airtable is it's customiizable nature. We worked with a tech specialist to configure Airtable to meet our unique needs and it has so far been adaptable to everything we needed it to do...</p>
</div>
</div>
<!-- More review sections... -->
</div>
</div>
</div>
</div>
</div>
La solución: Scrapeless Scraping Browser para evitar Cloudflare
Incluso con configuraciones avanzadas de Selenium, muchos proyectos de scraping son bloqueados por los sistemas de seguridad de Cloudflare. Aquí es donde Scrapeless Scraping Browser entra como una solución que cambia el juego.
¿Qué es Scrapeless Scraping Browser?
Scrapeless Scraping Browser es una solución de alto rendimiento que proporciona un entorno de navegador headless, lo que le permite evitar desafíos de JavaScript sin mantener su propia infraestructura. Se integra con Puppeteer y Playwright para una automatización perfecta y ofrece:
Impulsado por IA, adaptándose automáticamente a las políticas de seguridad más recientes
Cobertura global para acceder a contenido restringido geográficamente
Inicie sesión en Scrapeless panel de control inmediatamente y cree su CLAVE API en la administración de CLAVE API
Uso de Scrapeless Scraping Browser para extraer reseñas de G2
Ahora que entendemos por qué Selenium estándar con PowerShell tiene dificultades con sitios protegidos por Cloudflare como G2, implementemos una solución utilizando Scrapeless Scraping Browser.
Configuración de Scrapeless Scraping Browser con Puppeteer
La forma más eficaz de usar Scrapeless Scraping Browser es a través de Node.js con Puppeteer. Así es como se configura:
Verifique la instalación abriendo un símbolo del sistema y escribiendo:
Copy
node --version
npm --version
Crear una carpeta de proyecto
Cree una nueva carpeta para su proyecto de scraping
Abra un símbolo del sistema en esta carpeta
Instalar el paquete requerido
Instale puppeteer-core con npm:
Copy
npm install puppeteer-core
Crear su script de scraping
Cree un archivo llamado scrape.js con el siguiente código:
Copy
// scrape.js
const puppeteer = require('puppeteer-core');
async function scrapeG2Reviews() {
// Reemplace con su token de API Scrapeless real
const API_TOKEN = 'YOUR_API_TOKEN_HERE';
// Configurar la conexión a Scrapeless Scraping Browser
const connectionURL = `wss://browser.scrapeless.com/browser?token=${API_TOKEN}&session_ttl=180&proxy_country=ANY`;
console.log('Conectando a Scrapeless Scraping Browser...');
const browser = await puppeteer.connect({
browserWSEndpoint: connectionURL,
defaultViewport: null,
});
console.log('¡Conectado!');
const page = await browser.newPage();
// Establecer un tiempo de espera más largo para la navegación
page.setDefaultNavigationTimeout(120000);
// Arreglo para guardar todas las reseñas
const allReviews = [];
// Número de páginas para raspar
const pagesToScrape = 3;
for (let currentPage = 1; currentPage <= pagesToScrape; currentPage++) {
console.log(`Navegando a la página ${currentPage}...`);
// Navegar directamente a la página específica
const pageUrl = currentPage === 1
? 'https://www.g2.com/products/airtable/reviews'
: `https://www.g2.com/products/airtable/reviews?page=${currentPage}`;
await page.goto(pageUrl, {
waitUntil: 'networkidle2',
timeout: 60000
});
console.log(`Página ${currentPage} cargada. Tomando captura de pantalla...`);
// Tomar una captura de pantalla para verificar que la página se cargó correctamente
await page.screenshot({ path: `g2_page_${currentPage}.png` });
// Esperar a que se cargue el contenido de la página
// Probar diferentes selectores posibles ya que la estructura podría haber cambiado
try {
console.log('Esperando a que se cargue el contenido...');
// Primero intentar con el selector original
await page.waitForSelector('.paper__bd', { timeout: 10000 })
.then(() => console.log('Selector encontrado: .paper__bd'))
.catch(() => console.log('Selector .paper__bd no encontrado, intentando alternativas...'));
// Si todavía estamos aquí, vamos a verificar la estructura de la página usando otro enfoque
console.log('Analizando la estructura de la página...');
// Obtener el título de la página para verificar que estamos en la página correcta
const pageTitle = await page.title();
console.log('Título de la página:', pageTitle);
// Extraer reseñas con un enfoque más genérico
console.log('Extrayendo reseñas...');
// Intentar extraer reseñas con un enfoque más resistente
const pageReviews = await page.evaluate(() => {
// Buscar contenedores de reseñas con múltiples selectores
const reviewElements = Array.from(
document.querySelectorAll('.paper__bd, .review-item, .reviews-section article, [data-testid="review-card"]')
);
console.log(`Se encontraron ${reviewElements.length} elementos de reseña potenciales`);
if (reviewElements.length === 0) {
// Si no se encuentran reseñas con selectores específicos, vamos a volcar la estructura de la página
const html = document.documentElement.outerHTML;
return { error: 'No se encontraron reseñas', html: html.substring(0, 5000) }; // Primeros 5000 caracteres para depuración
}
return reviewElements.map(review => {
// Intentar obtener el título con múltiples selectores potenciales
const titleElement =
review.querySelector('[itemprop="name"], .review-title, h3, h4') ||
review.querySelector('a.pjax div');
const title = titleElement ? titleElement.innerText.trim() : 'Sin título';
// Intentar obtener la fecha con múltiples selectores potenciales
const dateElement =
review.querySelector('time, .review-date, [data-testid="review-date"]') ||
review.querySelector('.time-stamp');
const date = dateElement ? dateElement.innerText.trim() : 'Sin fecha';
// Intentar obtener la calificación con múltiples enfoques
let rating = 0;
const starsElement = review.querySelector('.stars, .rating, [data-testid="star-rating"]');
if (starsElement) {
// Verificar si hay una clase stars-X
const starsClass = starsElement.className;
const starsMatch = starsClass.match(/stars-(\d+)/);
if (starsMatch) {
rating = parseInt(starsMatch[1]) / 2;
} else {
// De lo contrario, verificar la etiqueta aria que podría contener la calificación
const ariaLabel = starsElement.getAttribute('aria-label');
if (ariaLabel) {
const ratingMatch = ariaLabel.match(/(\d+(\.\d+)?)/);
if (ratingMatch) {
rating = parseFloat(ratingMatch[1]);
}
}
}
}
// Intentar obtener el texto de la reseña con múltiples selectores potenciales
const textElements = review.querySelectorAll('.formatted-text, .review-content, p, [data-testid="review-content"]');
let content = '';
textElements.forEach(el => {
const text = el.innerText.trim();
if (text && !text.includes('Review collected by and hosted on G2.com')) {
content += text + '\n\n';
}
});
return {
title,
date,
rating,
content: content.trim() || 'No se encontró contenido'
};
});
});
if (pageReviews.error) {
console.log('Error al extraer reseñas:', pageReviews.error);
console.log('Fragmento de HTML de la página:', pageReviews.html);
} else {
console.log(`Se extrajeron ${pageReviews.length} reseñas de la página ${currentPage}`);
// Agregar a nuestra colección
allReviews.push(...pageReviews);
}
} catch (error) {
console.error('Error en la página:', error);
// Tomar otra captura de pantalla en caso de error para depuración
await page.screenshot({ path: `error_page_${currentPage}.png` });
// Volcar el HTML de la página para depuración
const html = await page.content();
require('fs').writeFileSync(`error_page_${currentPage}.html`, html);
console.log(`HTML de la página de error guardado en error_page_${currentPage}.html`);
}
// Pequeña pausa entre páginas para ser amables con el servidor
await new Promise(r => setTimeout(r, 2000));
}
console.log(`Total de reseñas extraídas: ${allReviews.length}`);
// Guardar todas las reseñas en un archivo
require('fs').writeFileSync('g2_reviews.json', JSON.stringify(allReviews, null, 2));
console.log('Reseñas guardadas en g2_reviews.json');
await browser.close();
console.log('¡Hecho!');
}
scrapeG2Reviews().catch(console.error);
Ejecutar el script
Ejecute el script con:
Copy
node scrape.js
El formato de salida JSON
Después de ejecutar el script, tendrá un archivo g2_reviews.json con contenido que se parece a esto:
Copy
[
{
"title": "\"Helped us get a handle on our complicated team-based scheduling and management.\"",
"date": "Feb 11, 2025",
"rating": 5,
"content": "What do you like best about Airtable?\nThe best part about airtable is it's customiizable nature. We worked with a tech specialist to configure Airtable to meet our unique needs and it has so far been adaptable to everything we needed it to do. It has a huge capacity in terms of features and we were able to get it up and running in a fairly short period of time.\n\nWhat do you dislike about Airtable?\nThere are no real downsides except the intial learning curve. I believe we could have built Airtable out as we needed but it would have been a bigger intial time investment. Working with a tech specialist who knew Airtable well really helped us get up and running fast. It was worth the cost."
},
{
"title": "\"Great for organization, has changed our workflow\"",
"date": "Feb 8, 2025",
"rating": 4.5,
"content": "What do you like best about Airtable?\nWhat I like best about Airtable is its flexibility and customizability. It allows us to create databases that perfectly fit our needs, with custom fields, views, and formulas. The ability to link records between tables is incredibly powerful.\n\nWhat do you dislike about Airtable?\nSometimes the learning curve can be steep for new users, especially when dealing with more complex formulas and automations. The pricing can also become quite expensive as your needs grow and you require more records or features."
},
{
"title": "\"Powerful database tool with a spreadsheet interface\"",
"date": "Jan 29, 2025",
"rating": 4,
"content": "What do you like best about Airtable?\nAirtable combines the familiarity of spreadsheets with the power of a database. I appreciate how it allows for different views of the same data - grid, kanban, calendar, etc. It's intuitive enough for non-technical users but powerful enough for complex data management.\n\nWhat do you dislike about Airtable?\nThe free tier is quite limited, and pricing jumps significantly for premium features. Sometimes performance can slow down with large datasets, and there are occasional sync issues when multiple team members are editing simultaneously."
}
]
Integración de PowerShell: Uso de Scrapeless Scraping Browser desde PowerShell
Si prefiere trabajar con PowerShell, puede crear un script envoltorio que llame al script de Node.js y procese los resultados:
Crear un script envoltorio de PowerShell
Cree un archivo llamado run-scraper.ps1 con el siguiente código:
Copy
# Archivo: run-scraper.ps1
Write-Host "Iniciando el scraping de G2.com con Scrapeless Scraping Browser..."
# Crear carpeta de salida
$outputFolder = "G2_Reviews_" + (Get-Date -Format "yyyyMMdd_HHmmss")
New-Item -ItemType Directory -Path $outputFolder -Force | Out-Null
# Ejecutar el script de Node.js y capturar la salida
$jsonFile = Join-Path $outputFolder "reviews.json"
node scrape.js > $jsonFile
Write-Host "Scraping de Node.js completado. Procesando resultados..."
# Verificar si el archivo contiene datos JSON válidos
try {
# Leer el contenido del archivo
$content = Get-Content -Path $jsonFile -Raw
# Extraer la parte JSON (asumiendo que es la salida después de todos los mensajes console.log)
if ($content -match '\[.*\]') {
$jsonContent = $Matches[0]
Set-Content -Path $jsonFile -Value $jsonContent
# Analizar el JSON en objetos de PowerShell
$reviews = $jsonContent | ConvertFrom-Json
# Exportar a CSV
$csvPath = Join-Path $outputFolder "g2_reviews.csv"
$reviews | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "Se extrajeron correctamente $($reviews.Count) reseñas"
Write-Host "Resultados guardados en: $csvPath"
# Análisis básico
$averageRating = ($reviews | Measure-Object -Property rating -Average).Average
Write-Host "Calificación promedio: $averageRating de 5"
$ratingDistribution = $reviews | Group-Object -Property rating | Sort-Object -Property Name
foreach ($ratingGroup in $ratingDistribution) {
Write-Host "$($ratingGroup.Name) estrellas: $($ratingGroup.Count) reseñas"
}
} else {
Write-Error "No se pudieron encontrar datos JSON en la salida"
}
} catch {
Write-Error "Error al procesar los datos JSON: $_"
}
Ejecutar el script envoltorio de PowerShell
Abra PowerShell
Navegue a la carpeta de su proyecto
Ejecute el script:
Copy
.\run-scraper.ps1
Revisar los resultados
El script crea una carpeta con fecha con resultados JSON y CSV
Las estadísticas básicas se muestran en la consola
El archivo CSV se puede abrir en Excel u otras aplicaciones para un análisis posterior
Ejemplo avanzado: Scraping de varias páginas con Scrapeless Scraping Browser
Para un caso de uso más avanzado, aquí hay un ejemplo que muestra cómo raspar reseñas de varias páginas:
Copy
// Archivo: scrape-g2-multipage.js
const puppeteer = require('puppeteer-core');
async function scrapeG2ReviewsMultiPage() {
// Reemplace con su token de API Scrapeless real
const API_TOKEN = 'YOUR_API_TOKEN_HERE';
// Configurar la conexión a Scrapeless Scraping Browser
const connectionURL = `wss://browser.scrapeless.com/browser?token=${API_TOKEN}&session_ttl=300&proxy_country=ANY`;
console.log('Conectando a Scrapeless Scraping Browser...');
const browser = await puppeteer.connect({
browserWSEndpoint: connectionURL,
defaultViewport: null,
});
console.log('¡Conectado! Abriendo G2.com...');
const page = await browser.newPage();
// Establecer un tiempo de espera de navegación razonable
page.setDefaultNavigationTimeout(60000);
// Arreglo para guardar todas las reseñas
const allReviews = [];
// Función para extraer reseñas de la página actual
const extractReviews = async () => {
return page.evaluate(() => {
return Array.from(document.querySelectorAll('.paper__bd')).map(review => {
// Título
const titleElement = review.querySelector('a.pjax div[itemprop="name"]');
const title = titleElement ? titleElement.innerText : 'Sin título';
// Fecha
const dateElement = review.querySelector('time');
const date = dateElement ? dateElement.innerText : 'Sin fecha';
// Calificación
const starsElement = review.querySelector('.stars');
let rating = 0;
if (starsElement) {
const starsClass = starsElement.className;
const match = starsClass.match(/stars-(\d+)/);
if (match) {
rating = parseInt(match[1])/2;
}
}
// Contenido de la reseña
const contentElements = review.querySelectorAll('.formatted-text');
let content = '';
contentElements.forEach(el => {
const text = el.innerText;
if (!text.includes('Review collected by and hosted on G2.com')) {
content += text + '\n\n';
}
});
return {
title,
date,
rating,
content: content.trim()
};
});
});
};
// Navegar a la primera página
await page.goto('https://www.g2.com/products/airtable/reviews', {
waitUntil: 'networkidle2'
});
// Esperar a que se carguen las reseñas
await page.waitForSelector('.paper__bd', {timeout: 30000});
// Tomar una captura de pantalla de la primera página
await page.screenshot({ path: 'page1.png' });
// Número de páginas para raspar (límite para demostración)
const pagesToScrape = 3;
let currentPage = 1;
while (currentPage <= pagesToScrape) {
console.log(`Raspando página ${currentPage}...`);
// Extraer reseñas de la página actual
const pageReviews = await extractReviews();
console.log(`Se encontraron ${pageReviews.length} reseñas en la página ${currentPage}`);
// Agregar a nuestra colección
allReviews.push(...pageReviews);
// Verificar si hay un botón de siguiente página
javascriptCopy
const hasNextPage = await page.evaluate(() => {
const nextButton = document.querySelector('a.next_page');
return nextButton && !nextButton.classList.contains('disabled');
});
if (hasNextPage && currentPage < pagesToScrape) {
// Navegar directamente a la URL de la siguiente página
currentPage++;
await page.goto(`https://www.g2.com/products/airtable/reviews?page=${currentPage}`, {
waitUntil: 'networkidle2'
});
// Esperar a que se carguen las reseñas en la nueva página
await page.waitForSelector('.paper__bd', {timeout: 30000});
// Tomar una captura de pantalla de la nueva página
await page.screenshot({ path: `page${currentPage}.png` });
} else {
break;
}
}
console.log(`Reseñas totales extraídas: ${allReviews.length}`);
// Mostrar todas las reseñas como JSON
console.log(JSON.stringify(allReviews, null, 2));
await browser.close();
console.log('¡Listo!');
}
scrapeG2ReviewsMultiPage().catch(console.error);
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables.
El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora.
No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros.
Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.