PlaywrightとScrapelessを使用してIndexedDBデータを抽出する方法
Specialist in Anti-Bot Strategies
TL;DR:
- IndexedDBは、初期HTMLには表示されない構造化されたブラウザデータを含むことができます。 それを読み取るには、アプリケーションがデータベースを開いた後にページのオリジン内でコードを実行する必要があります。
- IndexedDBの抽出はデータベースとスキーマの発見から始めるべきです。 レコードを読み取る前に、データベース、オブジェクトストア、およびインデックスを列挙し、それらの名前を仮定するべきではありません。
- Playwrightは、IndexedDBのコールバックベースのリクエストを待機可能な抽出ワークフローに橋渡しすることができます。
indexedDB.open()やgetAll()のようなリクエストを、page.evaluate()内の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 Storageに対して大きすぎるまたは構造化されすぎる状態に使用します。ブラウザエクストラクターは、ページがデータベースを開いた後に公開アプリケーションの状態を読み取ることができます。そのアクセスは、プライベートなユーザーデータベースを適切な収集対象にはしません。このチュートリアルでは、MDNの公開連絡先フィクスチャのみを使用します。
PlaywrightをScrapelessと組み合わせて使用する理由
Playwrightは、ページのオリジンで非同期関数を評価できるため、その関数はindexedDB.openを呼び出し、リクエストコールバックを待機し、データをPythonに返すことができます。Scrapeless Scraping Browserは、そのページ側APIをホステッドChromiumセッション内に保ちます。
Playwrightはconnect_over_cdpを介して接続します。ターゲットページがロードされると、IndexedDBクエリは変更されません。
前提条件
- Python 3.10以降。
playwright1.59.0または適合する現在のバージョン。- 完全な実行可能なパスのためのローカルChrome/Chromium。
- クラウド接続のための資金提供されたScrapelessアカウントと
SCRAPELESS_API_KEY。検証アカウントは新しいブラウザセッションのために14500のコードを返したため、その接続はラベル付きの前提条件のギャップとなります。
インストール
bash
python -m pip install "playwright==1.59.0"
Scrapeless Scraping Browserに接続する
注: この接続には資金提供されたScraping Browserの残高が必要です。最終的な検証アカウントは
残高不足です。最初にチャージしてください。というメッセージを返しました。以下の完全なIndexedDB抽出はローカルChromeで実行されました。
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]
# 以下のIndexedDBクエリをナビゲートして評価します。
browser.close()
現在の接続パラメータについては、Scrapeless開発者ドキュメントを参照してください。
ステップ1 — オリジンのデータベースを発見する
MDNの公開IDBIndexの例は、読み込み後に1つのデータベースを作成します。
python
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
databases: [{'name': 'contactsList', 'version': 1}]
indexedDB.databases() はIDBFactory databasesリファレンス で説明されています。古いエンジンで使用する前に、ブラウザのサポートを確認してください。
ステップ2 — オブジェクトストアとインデックスを調査する
発見されたデータベースを開いて、スキーマメタデータを返します:
python
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
オブジェクトストア: contactsList
インデックス: ['age', 'company', 'eMail', 'fName', 'jTitle', 'lName', 'phone']
このフィクスチャは、データベースとその単一のオブジェクトストアの両方に contactsList を使用しています。両方を列挙することは重要です:多くのアプリケーションは異なる名前を使用し、それらが一致するとは限らないため、間違ったストアをターゲットにすることがあります。
ステップ3 — オブジェクトストアからレコードを読み取る
IDBObjectStore.getAll() はリクエストを返し、Promise ではありません。その成功/エラーコールバックをラップして、Playwright が待機できるようにします:
python
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
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
テーブル行数: 10
データベース行数: 10
Arkham 行数: 2
年齢範囲: 24 55
一致したカウントは、このフィクスチャが完全なデータベースをレンダリングしたことを証明します。異なるアプリケーションは現在のページやフィルタリングされたサブセットのみをレンダリングする場合があるため、両方のカウントを維持し、強制的な一致を避けるべきです。
ホストされたセッションでブラウザの状態をクエリする準備はできましたか? 無料の Scrapeless アカウントを作成し、ブラウザの作成だけを置き換えてください。
完全な実行可能エクストラクター
python
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
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のデータベース1つ(contactsListストア)、7つのインデックス、10のデータベースレコード、10のレンダリングされた行、2つのアーカムの連絡先、24歳から55歳までの年齢を含んでいます。
返ってくるもの
完全なエクストラクタは、データベースのアイデンティティ、スキーマメタデータ、保存されたレコード、および同じブラウザセッションからのDOM比較を返します:
json
{
"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ワークフローをホストされたブラウザに移動します。
FAQ
Q: PlaywrightはIndexedDBを読み取ることができますか?
はい。ページのオリジンで非同期関数を評価し、IndexedDBリクエストコールバックをPromiseでラップします。
Q: なぜデータベースを最初に列挙するのですか?
名前やバージョンを仮定する前に、オリジンとデータベースのアイデンティティを確認します。すべてのブラウザがindexedDB.databases()を公開しているわけではないため、互換性が必要な場合は既知の名前のフォールバックを保持してください。
Q: オブジェクトストアはデータベースと同じですか?
いいえ。データベースは1つ以上のオブジェクトストアを含みます。MDNのフィクスチャでは、データベースとそのオブジェクトストアは両方ともcontactsListと名付けられています。
Q: なぜgetAllの代わりにカーソルを使用すべきですか?
ストアが大きくなる可能性がある場合、順序が必要な場合、または小さなサブセットの後に停止できる場合は、カーソルまたは制限付きクエリを使用してください。
Q: なぜIndexedDBの行をDOMと比較するのですか?
比較により、UIがすべてのレコード、フィルタされたビュー、または1ページのみを表示するかが明らかになります。いずれの表面も静かにもう一方を上書きしてはなりません。
**Q: IndexedDBはブラウザセッションを超えて持続しますか?**
ブラウザのプロファイルが保持されている限り持続しますが、オリジンまたはブラウザがそれをクリアすると消えます。一時的なコンテキストは、コンテキストが閉じると消える可能性があります。
**Q: IndexedDBデータの抽出は常に許可されていますか?**
いいえ。認可されたセッションと必要な公開データのみを検査し、保持されるフィールドを最小限に抑え、適用可能な場合はサイトの利用規約と<a href="https://datatracker.ietf.org/doc/html/rfc9309" rel="nofollow"><strong>ロボットポリシー</strong></a>に従い、機密用途については法的助言を受けてください。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



