Volver al blog

Cómo extraer datos de IndexedDB con Playwright y Scrapeless

Sophia Martinez
Sophia Martinez

Specialist in Anti-Bot Strategies

30-Jul-2026

TL;DR:

  • IndexedDB puede contener datos estructurados del navegador que nunca aparecen en el HTML inicial. Leerlo requiere ejecutar código dentro del origen de la página después de que la aplicación abra su base de datos.
  • La extracción de IndexedDB debe comenzar con el descubrimiento de bases de datos y esquemas. Enumera bases de datos, almacenes de objetos e índices antes de leer registros en lugar de asumir sus nombres.
  • Playwright puede vincular las solicitudes basadas en callbacks de IndexedDB en un flujo de extracción awaitable. Envuelve solicitudes como indexedDB.open() y getAll() en Promesas dentro de page.evaluate().
  • El DOM renderizado y IndexedDB pueden exponer diferentes subconjuntos del mismo estado de la aplicación. Preserva ambos conteos para detectar filtrado, paginación o datos de UI obsoletos.
  • Almacenes grandes o sensibles requieren extracción limitada. Prefiere rangos de clave, conteos o cursores sobre una llamada getAll() sin restricciones, e inspecciona solo sesiones de navegador autorizadas.
  • La misma consulta de IndexedDB funciona localmente y a través de Scrapeless. Pasar a Scrapeless Scraping Browser cambia la creación del navegador, no la lógica de la base de datos del lado de la página.
  • Libre para empezar. Las nuevas cuentas de Scrapeless incluyen tiempo de ejecución gratuito para Scraping Browser—regístrate en app.scrapeless.com.

IndexedDB puede contener registros estructurados que nunca aparecen en el HTML inicial de una página. Esta guía utiliza Playwright para enumerar una base de datos pública real, inspeccionar su almacén de objetos e índices, leer todos los registros y compararlos con la tabla renderizada antes de mover la misma extracción al Scrapeless Scraping Browser.

Lo que IndexedDB aporta a la extracción web

IndexedDB es una base de datos asíncrona, limitada por origen, integrada en el navegador. A diferencia del mapa de cadenas de localStorage, almacena valores estructurados de JavaScript en almacenes de objetos y admite índices, claves y transacciones. La guía de IndexedDB de MDN describe ese modelo.

Las aplicaciones lo utilizan para registros sin conexión, datos de API en caché, colas y estados que pueden ser demasiado grandes o estructurados para el Almacenamiento Web. Un extractor de navegador puede leer el estado de la aplicación pública después de que la página haya abierto su base de datos. Ese acceso no hace que las bases de datos privadas de usuarios sean objetivos de recolección apropiados; este tutorial utiliza solo el fixture de contactos públicos de MDN.

Por qué usar Playwright con Scrapeless

Playwright evalúa una función asíncrona en el origen de la página, por lo que la función puede llamar a indexedDB.open, esperar callbacks de solicitud y devolver datos simples a Python. El Scrapeless Scraping Browser mantiene esa API del lado de la página dentro de una sesión de Chromium alojada.

Playwright se conecta a través de connect_over_cdp. Una vez que la página objetivo está cargada, la consulta de IndexedDB permanece sin cambios.

Requisitos previos

  • Python 3.10 o posterior.
  • playwright 1.59.0 o una versión compatible actual.
  • Chrome/Chromium local para la ruta ejecutable completa.
  • Una cuenta de Scrapeless financiada y SCRAPELESS_API_KEY para la conexión en la nube. La cuenta de verificación devolvió el código 14500 para nuevas sesiones de navegador, por lo que esa conexión es un requisito previo etiquetado.

Instalar

bash Copy
python -m pip install "playwright==1.59.0"

Conectar al Scrapeless Scraping Browser

Nota: Esta conexión requiere un saldo financiado en Scraping Browser. La cuenta de verificación final devolvió saldo insuficiente, por favor recargue primero. La extracción completa de IndexedDB a continuación se ejecutó en Chrome local.

python Copy
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]
    # Navegar y evaluar la consulta de IndexedDB a continuación.
    browser.close()

Utiliza la documentación para desarrolladores de Scrapeless para los parámetros de conexión actuales.

Paso 1 — Descubrir las bases de datos del origen

El ejemplo público de IDBIndex de MDN crea una base de datos después de cargar:

python Copy
TARGET_URL = (
    "https://mdn.github.io/dom-examples/"
    "indexeddb-examples/idbindex/"
)

page.goto(TARGET_URL, wait_until="domcontentloaded")
page.wait_for_selector("tbody tr")
databases = page.evaluate("indexedDB.databases()")
print("databases:", databases)
text Copy
databases: [{'name': 'contactsList', 'version': 1}]

indexedDB.databases() se describe en la referencia de bases de datos de IDBFactory. Verifica el soporte del navegador antes de confiar en ello en motores más antiguos.

Paso 2 — Inspeccionar el Almacén de Objetos y los Índices

Abre la base de datos descubierta y devuelve los metadatos del esquema:

python Copy
schema = page.evaluate("""async () => {
    const opened = await new Promise((resolve, reject) => {
        const request = indexedDB.open('contactsList');
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    const storeName = opened.objectStoreNames[0];
    const transaction = opened.transaction(storeName, 'readonly');
    const store = transaction.objectStore(storeName);
    const result = {
        storeName,
        indexes: Array.from(store.indexNames),
    };
    opened.close();
    return result;
}""")

print("almacén de objetos:", schema["storeName"])
print("índices:", schema["indexes"])
text Copy
almacén de objetos: contactsList
índices: ['age', 'company', 'eMail', 'fName', 'jTitle', 'lName', 'phone']

Este fixture utiliza contactsList tanto para la base de datos como para su único almacén de objetos. Enumerar ambos sigue siendo importante: muchas aplicaciones usan nombres diferentes, y asumir que coinciden puede apuntar al almacén equivocado.

Paso 3 — Leer Registros Del Almacén De Objetos

IDBObjectStore.getAll() devuelve una solicitud, no una Promesa. Envuelve sus callbacks de éxito/error para que Playwright pueda esperar:

python Copy
records = page.evaluate("""async () => {
    const database = await new Promise((resolve, reject) => {
        const request = indexedDB.open('contactsList');
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    const transaction = database.transaction('contactsList', 'readonly');
    const store = transaction.objectStore('contactsList');
    const rows = await new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    database.close();
    return rows;
}""")

print("número de registros:", len(records))
print("primero:", records[0])
print("último:", records[-1])

La referencia de getAll señala que devuelve todos los registros cuando no se proporciona ninguna consulta o cuenta. Mantén las lecturas limitadas en aplicaciones reales; un cursor o una cuenta es más seguro para grandes almacenes.

Paso 4 — Comparar IndexedDB Con El DOM Renderizado

python Copy
table_rows = page.locator("tbody tr").count()
arkham_rows = [row for row in records if row["company"] == "Arkham"]
ages = [row["age"] for row in records]

print("filas de la tabla:", table_rows)
print("filas de la base de datos:", len(records))
print("filas de Arkham:", len(arkham_rows))
print("rango de edades:", min(ages), max(ages))
text Copy
filas de la tabla: 10
filas de la base de datos: 10
filas de Arkham: 2
rango de edades: 24 55

Las cuentas coincidentes demuestran que este fixture renderizó la base de datos completa. Una aplicación diferente puede renderizar solo la página actual o un subconjunto filtrado, así que conserva ambas cuentas en lugar de forzar la igualdad.

¿Listo para consultar el estado del navegador en una sesión alojada? Crea una cuenta gratuita de Scrapeless y reemplaza solo la creación del navegador.

Extractor Completamente Ejecutable

python Copy
from playwright.sync_api import sync_playwright

TARGET_URL = (
    "https://mdn.github.io/dom-examples/"
    "indexeddb-examples/idbindex/"
)

QUERY = """async () => {
    const databases = await indexedDB.databases();
    const database = await new Promise((resolve, reject) => {
        const request = indexedDB.open('contactsList');
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    const storeName = database.objectStoreNames[0];
    const transaction = database.transaction(storeName, 'readonly');
    const store = transaction.objectStore(storeName);
    const indexes = Array.from(store.indexNames);
    const rows = await new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    database.close();
    return {databases, storeName, indexes, rows};
}"""

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("tbody tr")

    result = page.evaluate(QUERY)
    rows = result["rows"]
    table_rows = page.locator("tbody tr").count()
es Copy
arkham_rows = [row for row in rows if row["company"] == "Arkham"]
ages = [row["age"] for row in rows]

assert result["databases"] == [{"name": "contactsList", "version": 1}]
assert result["storeName"] == "contactsList"
assert len(result["indexes"]) == 7
assert len(rows) == table_rows == 10
assert len(arkham_rows) == 2

print("título:", page.title())
print("bases de datos:", result["databases"])
print("almacén de objetos:", result["storeName"])
print("índices:", result["indexes"])
print("filas de la base de datos:", len(rows))
print("filas de la tabla:", table_rows)
print("primer contacto:", rows[0]["fName"], rows[0]["lName"])
print("último contacto:", rows[-1]["fName"], rows[-1]["lName"])
print("filas de Arkham:", len(arkham_rows))
print("rango de edad:", min(ages), max(ages))
browser.close()

El resultado en vivo contiene una base de datos de versión 1, un almacén contactsList, siete índices, diez registros de base de datos, diez filas renderizadas, dos contactos de Arkham y edades de 24 a 55.

Lo que obtienes de vuelta

El extractor completo devuelve la identidad de la base de datos, los metadatos del esquema, los registros almacenados y una comparación del DOM de la misma sesión del navegador:

json Copy
{
  "database": {
    "name": "contactsList",
    "version": 1
  },
  "object_store": "contactsList",
  "index_count": 7,
  "database_rows": 10,
  "rendered_rows": 10,
  "matching_company_rows": 2,
  "age_range": {
    "minimum": 24,
    "maximum": 55
  }
}

Los conteos de la base de datos y de la tabla coinciden, lo que muestra que la estructura pública renderizó cada registro almacenado. Las aplicaciones de producción pueden renderizar solo un subconjunto filtrado o paginado, así que guarda la identidad de la base de datos, el nombre del almacén de objetos, la URL de la página y ambos conteos de filas con el resultado extraído.

Problemas comunes de extracción de IndexedDB

La lista de bases de datos está vacía

Espera a que la aplicación abra o cree su base de datos. Una lista vacía inmediatamente después de la navegación puede indicar un problema de temporización, una API de descubrimiento no compatible o un origen incorrecto.

El almacén de objetos es grande

No llames a getAll() sin un límite en un almacén no acotado. Usa un rango de claves, un contador o un cursor y detente después de obtener el conjunto de datos que necesita tu tarea.

Los valores contienen tipos no JSON

Playwright serializa valores compatibles de vuelta a Python. Los blobs, instancias de clase complejas y estructuras cíclicas pueden necesitar un mapeo a nivel de campo dentro de la función evaluada.

La página y la base de datos no coinciden

La página puede estar filtrada, paginada o desactualizada. Guarda la identidad de la base de datos, el nombre del almacén de objetos, la URL de la página y la hora de extracción para que se pueda investigar la discrepancia.

Límites de extracción seguros

IndexedDB contiene frecuentemente datos privados de aplicaciones sin conexión. Usa esta técnica solo en sistemas y sesiones que estás autorizado a inspeccionar. No publiques credenciales, mensajes o registros personales. Los contactos públicos de MDN son datos de estructura sintética.

Conclusión

La extracción de IndexedDB comienza con el descubrimiento y la inspección del esquema, no con un nombre de almacén supuesto. Abre la base de datos del origen, enumera los almacenes y los índices, conecta los callbacks de las solicitudes a una Promesa y valida el resultado contra una superficie visible donde exista. Mover el flujo de trabajo a Scrapeless cambia la creación del navegador mientras se preserva la consulta del lado de la página.

Únete a los desarrolladores que construyen flujos de trabajo de extracción basados en el navegador en la comunidad de Scrapeless: Discord · Telegram.

Comienza con Scrapeless, revisa los precios de Scrapeless y lee lo que expone CDP antes de mover el mismo flujo de trabajo de IndexedDB a un navegador alojado.

Preguntas Frecuentes

P: ¿Puede Playwright leer IndexedDB?

Sí. Evalúa una función asincrónica en el origen de la página y envuelve los callbacks de solicitud de IndexedDB en Promesas.

P: ¿Por qué enumerar las bases de datos primero?

Confirma el origen y la identidad de la base de datos antes de asumir un nombre o versión. No todos los navegadores exponen indexedDB.databases(), así que guarda un nombre conocido como alternativa cuando la compatibilidad lo requiera.

P: ¿Es un almacén de objetos lo mismo que una base de datos?

No. Una base de datos contiene uno o más almacenes de objetos. En la estructura de MDN, tanto la base de datos como su almacén de objetos se llaman contactsList.

P: ¿Cuándo debería usar un cursor en lugar de getAll?

Usa un cursor o una consulta limitada cuando un almacén pueda ser grande, cuando necesites ordenación o cuando puedas detenerte después de un pequeño subconjunto.

P: ¿Por qué comparar las filas de IndexedDB con el DOM?

La comparación revela si la interfaz de usuario muestra cada registro, una vista filtrada, o solo una página. Ninguna superficie debería sobrescribir silenciosamente a la otra.
P: ¿Persisten los datos de IndexedDB a través de las sesiones del navegador?

Puede persistir en un perfil de navegador retenido hasta que el origen o el navegador lo eliminen. Un contexto efímero puede desaparecer cuando se cierra el contexto.

P: ¿Está siempre permitido extraer datos de IndexedDB?

No. Inspeccionar solo las sesiones autorizadas y los datos públicos necesarios, minimizar los campos retenidos, seguir los términos del sitio y la política de robots cuando sea aplicable, y obtener asesoramiento legal para usos sensibles.

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.

Artículos más populares

Catalogar