| name | bank-scraper |
| description | Bank and broker scraping with Playwright and APIs. Use when building or modifying scrapers for IBKR, Trade Republic, Boursobank, or Crédit Agricole. Handles login flows, virtual keyboards, OTP, anti-bot evasion, and financial data extraction (balances, positions, transactions, dividends). |
Bank Scraper
Scraping de données bancaires et brokerage via Playwright headless et APIs.
Architecture Scraper
Chaque scraper implémente l'interface BaseScraper :
from abc import ABC, abstractmethod
from dataclasses import dataclass
from decimal import Decimal
from datetime import date
@dataclass
class Account:
external_id: str
name: str
account_type: str
balance: Decimal
currency: str = 'EUR'
is_pro: bool = False
@dataclass
class Position:
ticker: str
isin: str | None
name: str
quantity: Decimal
avg_cost: Decimal | None
current_price: Decimal
currency: str = 'EUR'
asset_type: str = 'stock'
@dataclass
class Transaction:
external_id: str
date: date
description: str
amount: Decimal
category: str | None = None
class BaseScraper(ABC):
@abstractmethod
async def login(self) -> bool: ...
@abstractmethod
async def get_accounts(self) -> list[Account]: ...
@abstractmethod
async def get_positions(self, account_id: str) -> list[Position]: ...
@abstractmethod
async def get_transactions(self, account_id: str, since: date) -> list[Transaction]: ...
async def sync_all(self) -> dict:
"""Full sync: login → accounts → positions → transactions"""
if not await self.login():
raise AuthError("Login failed")
accounts = await self.get_accounts()
for acc in accounts:
acc.positions = await self.get_positions(acc.external_id)
acc.transactions = await self.get_transactions(acc.external_id, since=date.today() - timedelta(days=90))
return {"accounts": accounts}
Patterns par Établissement
IBKR — Client Portal API (pas Playwright)
import httpx
class IBKRScraper(BaseScraper):
BASE = "https://localhost:5000/v1/api"
async def login(self):
r = await self.client.get(f"{self.BASE}/iserver/auth/status")
return r.json().get("authenticated", False)
async def get_accounts(self):
r = await self.client.get(f"{self.BASE}/portfolio/accounts")
async def get_positions(self, account_id):
r = await self.client.get(f"{self.BASE}/portfolio/{account_id}/positions/0")
Boursobank — Clavier Virtuel
async def solve_virtual_keyboard(page, pin: str):
"""Résoud le clavier virtuel Boursobank."""
for digit in pin:
btn = page.locator(f'button[data-key="{digit}"]')
if await btn.count() == 0:
btn = page.locator(f'button:has-text("{digit}")').first
await btn.click()
await page.wait_for_timeout(random.randint(100, 300))
Crédit Agricole — Deux Espaces
async def switch_space(page, space: str):
"""Switch entre espace perso et pro."""
if space == "pro":
await page.click('[data-testid="switch-pro"]')
await page.wait_for_load_state("networkidle")
Anti-Bot & Robustesse
context = await browser.new_context(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
viewport={"width": 1280, "height": 720},
locale="fr-FR",
timezone_id="Europe/Paris",
)
async def human_delay():
await asyncio.sleep(random.uniform(0.5, 2.0))
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=5, max=60))
async def safe_sync(scraper):
return await scraper.sync_all()
Gestion OTP
import pyotp
totp = pyotp.TOTP(secret)
code = totp.now()
async def wait_for_otp(institution: str, timeout: int = 300):
"""Attend que l'utilisateur saisisse l'OTP via l'API."""
...
Scheduler