🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

如何使用 Playwright 和 Scrapeless 提取 IndexedDB 数据

Sophia Martinez
Sophia Martinez

Specialist in Anti-Bot Strategies

30-Jul-2026

TL;DR:

  • IndexedDB可以包含结构化的浏览器数据,这些数据在初始HTML中并不会出现。 读取这些数据需要在应用程序打开其数据库后,在页面来源内执行代码。
  • IndexedDB的提取应从数据库和架构发现开始。 在读取记录之前,枚举数据库、对象存储和索引,而不是假设它们的名称。
  • Playwright可以将IndexedDB的基于回调的请求转换为可等待的提取工作流程。page.evaluate()内部将请求如 indexedDB.open()getAll() 包装在Promise中。
  • 呈现的DOM和IndexedDB可能会暴露相同应用状态的不同子集。 保留两个计数以检测过滤、分页或过时的UI数据。
  • 大型或敏感的存储需要有限的提取。 优先使用密钥范围、计数或游标,而不是无限制的getAll()调用,并只检查授权的浏览器会话。
  • 相同的IndexedDB查询在本地和通过Scrapeless都能正常工作。 转向Scrapeless Scraping Browser更改的是浏览器创建,而不是页面侧的数据库逻辑。
  • 免费开始。 新的Scrapeless账户包含免费的Scraping Browser运行时——请在app.scrapeless.com上注册。

引言:浏览器数据并不止于DOM

IndexedDB可以保存在页面初始HTML中从未出现的结构化记录。本指南使用Playwright来枚举一个真实的公共数据库,检查其对象存储和索引,读取所有记录,并在将相同的提取移动到Scrapeless Scraping Browser之前与渲染的表进行比较。

IndexedDB为网页提取增添了什么

IndexedDB是一个异步的、来源作用域的数据库,内置于浏览器中。与localStorage的字符串映射不同,它在对象存储中存储结构化的JavaScript值,并支持索引、键和事务。MDN的IndexedDB指南描述了该模型。

应用程序使用它来存储离线记录、缓存的API数据、队列以及可能对Web存储过大或结构化的状态。浏览器提取程序可以在页面打开其数据库后读取公共应用状态。这种访问并不使私人用户数据库成为合适的收集目标;本教程仅使用MDN的公共联系示例。

为什么在Scrapeless中使用Playwright

Playwright在页面来源中评估一个异步函数,因此该函数可以调用indexedDB.open,等待请求回调,并将普通数据返回给Python。 Scrapeless Scraping Browser在托管的Chromium会话中保留该页面侧API。

Playwright通过 connect_over_cdp 进行连接。一旦目标页面加载完毕,IndexedDB查询保持不变。

前提条件

  • Python 3.10或更高版本。
  • playwright 1.59.0或当前兼容版本。
  • 本地Chrome/Chromium以获得完整的可运行路径。
  • 一个有资金的Scrapeless账户和SCRAPELESS_API_KEY用于云连接。验证账户为新浏览器会话返回了代码14500,因此该连接是标记的前提条件。

安装

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

连接到Scrapeless Scraping Browser

注意:此连接需要有资金的Scraping Browser余额。最终验证账户返回了“余额不足,请先充值”。以下完整的IndexedDB提取在本地Chrome中运行。

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]
    # 导航并评估下面的IndexedDB查询。
    browser.close()

使用 Scrapeless开发者文档 获取当前连接参数。

第一步 — 发现来源的数据库

MDN的公共IDBIndex示例在加载后创建一个数据库:

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()IDBFactory数据库参考描述。在依赖旧引擎之前,请检查浏览器的支持情况。

步骤2 — 检查对象存储和索引

打开发现的数据库并返回架构元数据:

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("对象存储:", schema["storeName"])
print("索引:", schema["indexes"])
text Copy
对象存储: contactsList
索引: ['age', 'company', 'eMail', 'fName', 'jTitle', 'lName', 'phone']

这个示例使用contactsList作为数据库及其单个对象存储。列举两者仍然很重要:许多应用程序使用不同的名称,假设它们匹配可能会指向错误的存储。

步骤3 — 从对象存储中读取记录

IDBObjectStore.getAll()返回一个请求,而不是一个Promise。将其成功/错误回调包裹起来,以便Playwright可以等待:

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("记录数量:", len(records))
print("第一条:", records[0])
print("最后一条:", records[-1])

getAll参考指出,当没有提供查询或计数时,它将返回所有记录。在真实应用中,保持读取的范围;对于大型存储,使用游标或计数会更安全。

步骤4 — 比较IndexedDB与渲染的DOM

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("表格行数:", table_rows)
print("数据库行数:", len(records))
print("Arkham行数:", len(arkham_rows))
print("年龄范围:", min(ages), max(ages))
text Copy
表格行数: 10
数据库行数: 10
Arkham行数: 2
年龄范围: 24 55

匹配的计数证明此示例渲染了完整的数据库。不同的应用程序可能仅渲染当前页面或过滤后的子集,因此保留两个计数而不是强制等值。

准备在托管会话中查询浏览器状态吗?创建一个免费的Scrapeless账户并仅替换浏览器创建部分。

完整可运行的提取器

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()
python 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("标题:", page.title())
print("数据库:", result["databases"])
print("对象存储:", result["storeName"])
print("索引:", result["indexes"])
print("数据库行:", len(rows))
print("表行:", table_rows)
print("第一个联系人:", rows[0]["fName"], rows[0]["lName"])
print("最后一个联系人:", rows[-1]["fName"], rows[-1]["lName"])
print("阿卡姆行:", len(arkham_rows))
print("年龄范围:", min(ages), max(ages))
browser.close()

结果包含一个版本-1 的数据库,一个 contactsList 存储,七个索引,十条数据库记录,十条渲染行,两位阿卡姆联系人,和年龄范围从 24 到 55。

您得到的结果

完整的提取器返回数据库身份、模式元数据、存储记录,以及来自同一浏览器会话的DOM比较:

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
  }
}

匹配的数据库和表数量显示公共基准渲染了每条存储记录。生产应用程序可能只渲染过滤或分页的子集,因此请将数据库身份、对象存储名称、页面 URL 和两个行数与提取结果一起保留。

常见的 IndexedDB 提取问题

数据库列表为空

等待应用程序打开或创建其数据库。在导航后立即显示空列表可能表示时间问题,或不支持的发现 API,或错误的来源。

对象存储较大

在无界存储上不要调用 getAll(),而是使用键范围、计数或游标,并在完成数据集切片所需的任务后停止。

值包含非 JSON 类型

Playwright 将支持的值序列化回 Python。Blob、复杂类实例和循环结构可能需要在被评估函数内进行字段级映射。

页面与数据库不一致

页面可能被过滤、分页或过期。存储数据库身份、对象存储名称、页面 URL 和提取时间,以便调查不匹配情况。

安全提取边界

IndexedDB 通常包含私人离线应用数据。仅在您有权检查的系统和会话中使用此技术。不要发布凭证、消息或个人记录。公共 MDN 联系人是合成的基准数据。

结论

IndexedDB 提取始于发现和模式检查,而不是猜测的存储名称。打开来源的数据库,枚举存储和索引,将请求回调桥接到 Promise,并针对可见表面验证结果(如果存在)。将工作流程迁移到 Scrapeless 改变了浏览器创建,同时保留了页面端查询。

准备好提取结构化浏览器状态了吗?

加入正在构建基于浏览器的提取工作流程的开发者社区 Scrapeless:Discord · Telegram

开始使用 Scrapeless,审查 Scrapeless 定价,并阅读 CDP 曝露的内容,在将相同的 IndexedDB 工作流程迁移到托管浏览器之前。

常见问题

问:Playwright 可以读取 IndexedDB 吗?

可以。在页面源中评估异步函数,并将 IndexedDB 请求回调包装在 Promise 中。

问:为什么先枚举数据库?

这可以确认来源和数据库身份,而不是在假设名称或版本之前。并不是每个浏览器都暴露 indexedDB.databases(),因此在兼容性需要时,请保留已知名称的回退。

问:对象存储和数据库是同一个吗?

不。数据库包含一个或多个对象存储。在 MDN 基准中,数据库及其对象存储均命名为 contactsList

问:我应该在何时使用游标而不是 getAll?

当存储可能很大、需要排序或在完成小子集后可以停止时,使用游标或有界查询。

问:为什么将 IndexedDB 行与 DOM 进行比较?

比较显示 UI 是否显示每条记录、过滤视图或仅一页。两个表面不应相互静默覆盖。

问:IndexedDB 在浏览器会话之间是否持久化?

它可以在保留的浏览器配置文件中持久化,直到来源或浏览器将其清除。临时上下文可能在上下文关闭时消失。

问:提取 IndexedDB 数据总是被允许吗?

不允许。仅检查授权的会话和必要的公共数据,尽量减少保留字段,遵循网站的条款和 机器人政策(如适用),并为敏感用途获取法律建议。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录