Cómo extraer Shadow DOM con Playwright y Scrapeless
Lead Scraping Automation Engineer
Resumen:
- El Shadow DOM puede mostrar texto que no existe en el DOM ligero del elemento anfitrión. JavaScript puede crear el contenido visible dentro de una raíz de sombra después de que se carga el elemento personalizado.
- Los localizadores de CSS y texto de Playwright pueden atravesar raíces de sombra abiertas. El mismo selector falla con
document.querySelectorporque las consultas de DOM a nivel de documento se detienen en el límite de sombra. - Las raíces de sombra cerradas y XPath requieren diferentes expectativas. Los localizadores de Playwright no atraviesan raíces cerradas, y los selectores de XPath no admiten la exploración automática del Shadow DOM.
- La lógica de extracción del Shadow DOM es independiente del transporte del navegador. Verifica el flujo de trabajo localmente, luego reutiliza los mismos localizadores a través de la conexión Scrapeless CDP cuando se requiera un navegador alojado o un egreso administrado.
- Libre para comenzar. Las nuevas cuentas de Scrapeless incluyen un tiempo de ejecución gratuito del navegador de raspado: regístrate en app.scrapeless.com.
Un componente de Shadow DOM puede mostrar texto que está ausente en los hijos del DOM ligero ordinario del elemento anfitrión. Esta guía utiliza Playwright para atravesar una raíz de sombra abierta real, muestra por qué un selector a nivel de documento no devuelve nada y separa esa lógica de extracción de la conexión del Navegador de Raspado Scrapeless que ejecuta el mismo código en un navegador en la nube.
Qué Cambia el Shadow DOM para un Raspador
El Shadow DOM le da a un elemento personalizado un subárbol encapsulado. El documento principal de la página puede ver la etiqueta anfitriona, pero las API de DOM ordinarias no cruzan automáticamente de ese anfitrión a su raíz de sombra.
En la demostración pública de MDN utilizada a continuación, la fuente contiene un anfitrión <popup-info> con un atributo data-text. Un script diferido actualiza esa etiqueta, llama a attachShadow({ mode: "open" }) y crea un span interno .info. El navegador muestra el texto de ayuda de ese span. El HTML inicial no contiene el span en absoluto.
La guía de Shadow DOM de MDN define el límite y la diferencia entre raíces abiertas y cerradas. La distinción es importante: una raíz abierta expone element.shadowRoot; una raíz cerrada devuelve null a través de esa API.
Por Qué Usar Playwright con Scrapeless
Los localizadores de Playwright atraviesan raíces de sombra abiertas para la búsqueda normal de CSS y texto. La documentación de localizadores de Playwright establece dos excepciones: XPath no atraviesa raíces de sombra, y las raíces en modo cerrado no son compatibles.
El código de recorrido no depende de dónde se ejecute Chromium. Un navegador local es suficiente para una demostración pública; el Navegador de Raspado Scrapeless se vuelve útil cuando el objetivo necesita un navegador alojado, una huella administrada, egreso residencial o una sesión en la nube persistente. Playwright llega a ese navegador a través de la API connect_over_cdp.
Requisitos Previos
- Python 3.10 o posterior.
playwright1.59.0 o una versión compatible actual.- Un runtime local de Chrome/Chromium para el fallback totalmente ejecutable a continuación.
- Una cuenta de Scrapeless financiada y
SCRAPELESS_API_KEYpara la conexión en la nube. La cuenta de verificación devolvió el código 14500 para nuevas sesiones de navegador, por lo que el bloque de conexión en la nube se marca como una brecha de requisito previo en lugar de mostrarse como exitoso.
Instalación
bash
python -m pip install "playwright==1.59.0"
El host de verificación ya tiene Google Chrome en /usr/bin/google-chrome. Si tu máquina no lo tiene, instala un Chromium administrado por Playwright con el comando de configuración normal del navegador del proyecto.
Conectar al Navegador de Raspado Scrapeless
Nota: Esta conexión requiere saldo de navegador Scrapeless financiado. La cuenta utilizada para la verificación final devolvió
saldo insuficiente, por favor recarga primero; ejecútala con tu propia clave financiada. Los bloques de recorrido después de esta sección se ejecutaron completamente en Chrome local contra la misma página pública.
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
params = urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": 120,
"proxyCountry": "US",
})
cdp_url = f"wss://browser.scrapeless.com/api/v2/browser?{params}"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_url)
page = browser.contexts[0].pages[0]
# Ejecuta los pasos de extracción a continuación con esta página.
browser.close()
Los documentos de desarrollador de Scrapeless son la fuente actual para los parámetros de conexión. Una vez que exista page, la búsqueda en el Shadow DOM es estándar de Playwright.
Inspeccionar el Anfitrión y Su Raíz Abierta
El demo del componente MDN es pequeño y público:
python
TARGET_URL = (
"https://mdn.github.io/web-components-examples/"
"popup-info-box-web-component/"
)
page.goto(TARGET_URL, wait_until="domcontentloaded")
page.wait_for_selector("popup-info .info")
host = page.locator("popup-info")
print("texto del DOM ligero del host:", repr(host.inner_text()))
print("modo sombra:", host.evaluate("element => element.shadowRoot.mode"))
text
texto del DOM ligero del host: ''
modo sombra: open
El texto vacío del host no es evidencia de que el componente no contenga datos. Indica que el host no tiene texto hijo ordinario. El nodo .info real vive debajo de la raíz de sombra abierta creada después de que se actualiza el elemento personalizado.
Prueba de que el Selector de Documento Se Detiene en el Límite
Ejecute una consulta DOM de navegador simple desde el documento:
python
plain_match = page.evaluate(
"document.querySelector('popup-info .info') !== null"
)
print("consulta del documento encontró nodo interno:", plain_match)
text
consulta del documento encontró nodo interno: False
El selector es CSS válido. El fallo proviene del alcance: document.querySelector busca en el árbol del documento y no desciende al árbol de sombra del host.
Permitir que un Localizador de Playwright Pene la Raíz Abierta
La cadena de localizadores ordinarios de Playwright cruza la raíz abierta:
python
info = page.locator("popup-info .info")
record = {
"host": "popup-info",
"shadow_mode": host.evaluate("element => element.shadowRoot.mode"),
"text": info.inner_text(),
"source_url": page.url,
}
print("coincidencias internas:", info.count())
print("texto de ayuda:", record["text"])
print("caracteres de texto:", len(record["text"]))
text
coincidencias internas: 1
texto de ayuda: El código de validación de tu tarjeta (CVC) es una característica de seguridad adicional: es los últimos 3 o 4 números en la parte posterior de tu tarjeta.
caracteres de texto: 118
Ese texto es el valor real data-text del componente copiado en el span de la raíz de sombra. El recuento de caracteres incluye puntuación y espacios, por lo que es una buena protección contra la extracción accidental de un elemento vacío o el texto alternativo del ícono.
¿Listo para mover el mismo localizador a un navegador alojado? Crea una cuenta gratuita en Scrapeless y mantén cada línea después de la creación del navegador sin cambios.
Script Completo de Verificación Local
Este script ejecuta todo el diagnóstico en Chrome local. Reemplazar launch(...) con la conexión CDP anterior lo mueve a Scrapeless sin cambiar la lógica de extracción:
python
from playwright.sync_api import sync_playwright
TARGET_URL = (
"https://mdn.github.io/web-components-examples/"
"popup-info-box-web-component/"
)
with sync_playwright() as p:
browser = p.chromium.launch(
executable_path="/usr/bin/google-chrome",
headless=True,
)
page = browser.new_page()
page.goto(TARGET_URL, wait_until="domcontentloaded")
page.wait_for_selector("popup-info .info")
host = page.locator("popup-info")
info = page.locator("popup-info .info")
host_text = host.inner_text()
shadow_mode = host.evaluate("element => element.shadowRoot.mode")
plain_match = page.evaluate(
"document.querySelector('popup-info .info') !== null"
)
help_text = info.inner_text()
assert shadow_mode == "open"
assert not plain_match
assert "código de validación de tarjeta" in help_text
print("título de la página:", page.title())
print("texto del DOM ligero del host:", repr(host_text))
print("modo sombra:", shadow_mode)
print("consulta del documento encontró nodo interno:", plain_match)
print("coincidencias internas:", info.count())
print("texto de ayuda:", help_text)
print("caracteres de texto:", len(help_text))
browser.close()
La ejecución demuestra el límite en lugar de solo mostrar un localizador exitoso: el texto del DOM ligero está vacío, el selector del documento es falso, la raíz informa que está abierta, y Playwright devuelve una cadena de ayuda de 118 caracteres.
Qué Significan las Raíces Abiertas y Cerradas
Raíz abierta
attachShadow({mode: "open"}) deja host.shadowRoot disponible. Los localizadores de Playwright pueden atravesarlo con selectores CSS/texto. Las raíces abiertas anidadas se manejan de la misma manera, aunque cada componente puede cargarse en un horario diferente.
Raíz cerrada
attachShadow({mode: "closed"}) hace que host.shadowRoot retorne null. Los localizadores normales de Playwright no pueden atravesarlo. No presente la inyección de script o la instrumentación del navegador como una solución universal: una raíz cerrada es un límite de encapsulación explícito, y cambiar el código de la página puede alterar el comportamiento que pretendía medir.
Contenido con ranuras
Un <slot> muestra hijos del DOM ligero dentro de un diseño de árbol de sombra. Los nodos asignados aún pertenecen al DOM ligero del host, por lo que su propiedad y comportamiento del selector difieren de los elementos creados dentro de la raíz de sombra. Inspeccione el DOM en vivo antes de decidir qué lado posee los datos.
Modos Comunes de Falla de Shadow DOM
El host existe pero el localizador interno se agota.
El elemento personalizado puede que aún no se haya actualizado, su JavaScript puede haber fallado, o el selector puede estar dirigido a una raíz cerrada. Espera en el elemento interno como el verdadero marcador de preparación y revisa los errores en la consola antes de cambiar los selectores.
XPath no encuentra nada
Utiliza un localizador CSS o de texto. La documentación de Playwright sobre el recorrido del shadow no se aplica a los selectores XPath.
Un componente anidado devuelve texto parcial
Localiza el elemento más estable y profundo en lugar de llamar a inner_text() en un host grande. Esto evita mezclar texto de slot, etiquetas visualmente ocultas y la salida del componente anidado.
HTML fuente no tiene nodo interno
Eso es normal cuando JavaScript construye el árbol shadow. Usa un navegador. Un analizador HTTP solo ve la respuesta fuente y no puede observar un subárbol creado después de la carga.
Conclusión
El scraping de Shadow DOM es un problema de alcance antes de ser un problema de selector. Prueba qué árbol posee el nodo, espera a que el elemento personalizado se actualice y utiliza los localizadores CSS/texto de Playwright para raíces abiertas. Mantén la frontera de raíz cerrada honesta. El transporte del navegador es separado: ejecuta localmente para una superficie de prueba pública y luego cambia la creación del navegador al endpoint CDP de Scrapeless cuando el objetivo necesite un navegador anti-detección alojado o salida gestionada.
Comienza con Scrapeless, revisa los precios de Scrapeless, y lee lo que controla el Protocolo de DevTools de Chrome para la arquitectura CDP circundante.
FAQ
P: ¿Puede Playwright hacer scraping de una raíz de Shadow DOM abierta?
Sí. Los localizadores CSS y de texto de Playwright pueden atravesar raíces shadow abiertas, por lo que un localizador como page.locator("popup-info .info") puede alcanzar el nodo interno.
P: ¿Por qué falla document.querySelector en el mismo selector?
document.querySelector busca en el árbol del documento y se detiene en el host shadow. Debes consultar a través de la raíz abierta tú mismo o usar un localizador de Playwright que realice ese recorrido.
P: ¿Puede Playwright atravesar una raíz shadow cerrada?
No, no a través de su comportamiento normal de localizador. Una raíz cerrada devuelve null desde host.shadowRoot y es un límite documentado como no soportado.
P: ¿Funciona XPath dentro de Shadow DOM?
No. La orientación oficial de localizadores de Playwright dice que los selectores XPath no atraviesan raíces shadow; utiliza localizadores CSS o de texto.
P: ¿Cómo manejo raíces shadow abiertas anidadas?
Encadena localizadores hacia el elemento interno estable y espera a que ese elemento esté disponible. Playwright puede cruzar múltiples raíces abiertas, pero cada elemento personalizado puede actualizarse de forma asincrónica.
P: ¿Necesito un navegador para hacer scraping de Shadow DOM?
Sí, cuando JavaScript crea el árbol shadow. Una respuesta HTTP simple contiene la marca y los scripts del host, no el subárbol en vivo producido después de la ejecución.
P: ¿Es legal el scraping de Shadow DOM?
El mecanismo DOM no decide el permiso. Revisa los términos del objetivo y directivas de robots, utiliza datos públicos, limita el volumen de solicitudes y busca asesoramiento legal para colecciones sensibles o comerciales.
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.



