scrapling — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited scrapling (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Cible : Scrapling v0.4.x (vérifié sur 0.4.9) · Python 3.10+ · Licence BSD-3-Clause (auteur : Karim Shoair / D4Vinci) Docs : https://scrapling.readthedocs.io · Repo : https://github.com/D4Vinci/Scrapling
Épingler la version : pip install "scrapling==0.4.9". La série 0.4 a introduit des changements d'API majeurs (spiders async, ProxyRotator). Un skill non versionné se périme silencieusement.Utiliser pour : extraction HTML/DOM, contournement d'anti-bot, crawl multi-pages, self-healing de sélecteurs, migration depuis BS4/Scrapy, intégration MCP.
Ne pas utiliser pour : APIs JSON publiques (un simple httpx/requests suffit), automatisation d'actions authentifiées complexes (Playwright brut), ou tout scraping qui violerait les CGU / le robots.txt / le RGPD. Scrapling est fourni pour usage éducatif et recherche : respecter robots.txt, les CGU et ne pas collecter de données personnelles sans base légale.
pip install "scrapling[fetchers]" # recommandé : parser + fetchers
scrapling install # OBLIGATOIRE avant tout fetcher navigateur (Chromium + deps)
# après un upgrade : scrapling install --force (rafraîchit les fingerprints)
pip install scrapling # parser seul, sans réseau
pip install "scrapling[all]" # fetchers + MCP + shell
pip install "scrapling[ai]" # serveur MCP uniquement
pip install "scrapling[shell]" # CLI shell + commande extract
docker pull pyd4vinci/scrapling # ou : ghcr.io/d4vinci/scrapling:latestPiège n°1 : oublierscrapling install. Sans lui,StealthyFetcher/DynamicFetcheréchouent (pas de navigateur).Fetcher(HTTP pur) fonctionne sans.
Le HTML voulu arrive-t-il via une simple requête HTTP ?
├─ OUI ─ besoin d'un fingerprint navigateur (TLS + headers) ?
│ ├─ NON → Fetcher / AsyncFetcher
│ └─ OUI → Fetcher(impersonate='chrome') # curl_cffi, AUCUN navigateur lancé
└─ NON (JS requis OU anti-bot)
├─ anti-bot (Cloudflare/Turnstile, DataDome…) → StealthyFetcher(solve_cloudflare=True)
└─ SPA / rendu JS, sans anti-bot → DynamicFetcherPuis : multi-pages ou état partagé ? → passer en classe Session (cookies + navigateur conservés entre appels). Crawl full-scale ? → Spider.
| Besoin | One-shot | Session persistante | Async |
|---|---|---|---|
| HTTP rapide | Fetcher | FetcherSession | AsyncFetcher (one-shot async) |
| Bypass anti-bot | StealthyFetcher | StealthySession | AsyncStealthySession |
| JS lourd / SPA | DynamicFetcher | DynamicSession | AsyncDynamicSession |
Note : il n'existe pas d'AsyncFetcherSession. Pour de l'HTTP async, utiliserAsyncFetcherdans unasyncio.gather().
from scrapling.fetchers import Fetcher, FetcherSession
page = Fetcher.get('https://quotes.toscrape.com/') # one-shot
with FetcherSession(impersonate='chrome') as session: # session persistante
p1 = session.get('https://site.com/page1', stealthy_headers=True)
p2 = session.get('https://site.com/page2', impersonate='firefox135')
with FetcherSession(http3=True) as session: # HTTP/3
page = session.get('https://site.com/')from scrapling.fetchers import StealthyFetcher, StealthySession
page = StealthyFetcher.fetch('https://nopecha.com/demo/cloudflare') # one-shot
with StealthySession(headless=True, solve_cloudflare=True) as session:
page = session.fetch('https://protected-site.com', google_search=False)
data = page.css('#content a').getall()solve_cloudflare=True lance un vrai navigateur (coût temps/CPU). Ne l'activer que si la cible est réellement protégée.from scrapling.fetchers import DynamicSession
with DynamicSession(headless=True, network_idle=True) as session:
page = session.fetch('https://react-app.com', load_dom=False)
data = page.xpath('//span[@class="text"]/text()').getall()adaptive=True ne sait relocaliser un élément que s'il a été enregistré lors d'un run précédent. C'est un workflow en deux phases, pas un drapeau magique.
# Phase 1 — premier run : on enregistre la signature de l'élément
products = page.css('.product-card', auto_save=True)
# Phase 2 — après redesign du site : on retrouve l'élément par similarité
products = page.css('.product-card', adaptive=True)
# Au niveau du fetcher entier
StealthyFetcher.adaptive = True
page = StealthyFetcher.fetch('https://example.com', headless=True)percentage (défaut 40) règle le seuil minimal de similarité accepté en mode adaptive. Ne pas y toucher sans raison : le calcul dépend de la structure de la page.# CSS (pseudo-éléments Parsel/Scrapy compatibles)
titles = page.css('h2.title::text').getall()
href = page.css('a::attr(href)').get()
# XPath
price = page.xpath('//span[@class="price"]/text()').get()
# Style BeautifulSoup
items = page.find_all('div', class_='product')
items = page.find_all(['div', 'article'], class_='card')
# Recherche par texte
btn = page.find_by_text('add to cart', tag='button')
# Navigation DOM
parent = element.parent
siblings = element.siblings
children = element.children
below = element.below_elements()
similar = element.find_similar()
# Chaînage
text = page.css('.quote')[0].css('.text::text').get()
# Génération auto de sélecteurs (utile en shell pour figer un sélecteur robuste)
css_sel = element.generate_css_selector
xpath_sel = element.generate_xpath_selectorfrom scrapling.spiders import Spider, Response
class QuotesSpider(Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/"]
concurrent_requests = 10
async def parse(self, response: Response):
for quote in response.css('.quote'):
yield {
"text": quote.css('.text::text').get(),
"author": quote.css('.author::text').get(),
}
next_page = response.css('.next a')
if next_page:
yield response.follow(next_page[0].attrib['href'])
result = QuotesSpider().start() # -> CrawlResult
print(f"Scraped {len(result.items)} items")
result.items.to_json("output.json")
result.items.to_jsonl("output.jsonl")Scrapling n'a pas de hook process_item. Les hooks réels, à surcharger sur la classe Spider :
class MySpider(Spider):
async def on_scraped_item(self, item: dict) -> dict | None:
"""Pipeline post-extraction : nettoyage, validation, push DB/API.
Retourner None pour drop l'item."""
await save_to_db(item)
return item
async def is_blocked(self, response: Response) -> bool:
return response.status in (403, 429) or "captcha" in response.body.lower()
async def retry_blocked_request(self, request, response):
return request # ré-essai (ex. via une autre session)
async def on_error(self, request, error: Exception): ...
async def on_start(self, resuming: bool = False): ...
async def on_close(self): ...from scrapling.spiders import Spider, Request, Response
from scrapling.fetchers import FetcherSession, AsyncStealthySession
class MultiSessionSpider(Spider):
name = "multi"
start_urls = ["https://example.com/"]
def configure_sessions(self, manager):
manager.add("fast", FetcherSession(impersonate="chrome"))
manager.add("stealth", AsyncStealthySession(headless=True), lazy=True)
async def parse(self, response: Response):
for link in response.css('a::attr(href)').getall():
sid = "stealth" if "protected" in link else "fast"
yield Request(link, sid=sid, callback=self.parse)Le moteur de crawl est async. Pour les cibles lourdes, privilégier des sessions async (AsyncStealthySession,AsyncDynamicSession) ;lazy=Truediffère l'ouverture du navigateur au premier usage.
QuotesSpider(crawldir="./crawl_data").start()
# Ctrl+C -> checkpoint sauvegardé · relancer la même commande -> reprise autospider = QuotesSpider()
async for item in spider.stream(): # items au fil de l'eau + stats live
await push_to_pipeline(item)L'import top-level from scrapling import ProxyRotator n'existe pas. La rotation se branche via le paramètre proxy_rotator= du fetcher/session, et est exclusive avec proxy= / proxies= (sinon erreur).
from scrapling.fetchers import StealthyFetcher, StealthySession
from scrapling.engines.toolbelt.proxy_rotation import ProxyRotator
rotator = ProxyRotator(proxies=[
'http://user:pass@proxy1:8080',
'http://user:pass@proxy2:8080',
])
# Brancher le rotator sur la session (PAS proxy=rotator.next())
with StealthySession(proxy_rotator=rotator, headless=True) as session:
page = session.fetch('https://target.com', block_ads=True) # block_ads ~3500 domaines
# Récupérer un proxy manuellement si besoin : rotator.get_proxy() (il n'y a pas de .next())Cloudflare entreprise → proxies résidentiels (NodeMaven, BirdProxies, Evomi, DataImpulse). Akamai/DataDome/Kasada/Incapsula → service dédié type Hyper Solutions (hypersolutions.co). v0.4.9 corrige un bug où le proxy de session HTTP était ignoré silencieusement (fuite d'IP réelle). Mettre à jour si vous étiez < 0.4.9.pip install "scrapling[ai]"
scrapling mcp # transport stdio (défaut)
scrapling mcp --http --host 0.0.0.0 --port 8000 # transport streamable-httpAvantage tokens : l'agent reçoit les données extraites, jamais le HTML brut.
mcp.json (config Claude/Cursor, transport stdio) :
{
"mcpServers": {
"scrapling": {
"command": "scrapling",
"args": ["mcp"]
}
}
}Le modulescrapling.mcpet les variables d'envSCRAPLING_*n'existent pas : c'est la commande CLIscrapling mcp(classescrapling.core.ai.ScraplingMCPServer). Docs MCP : https://scrapling.readthedocs.io/en/latest/ai/mcp-server/
scrapling shell # IPython interactif : debug sélecteurs, preview navigateur
scrapling shell https://example.com
scrapling extract get 'https://example.com' out.md
scrapling extract get 'https://example.com' out.txt --css-selector '#content' --impersonate 'chrome'
scrapling extract fetch 'https://example.com' out.md --no-headless
scrapling extract stealthy-fetch 'https://cf-site.com' out.html --solve-cloudflare
scrapling --version| BeautifulSoup4 | Scrapling |
|---|---|
requests.get(url) + BS4 | Fetcher.get(url) (tout-en-un) |
soup.find_all('div', class_='x') | page.find_all('div', class_='x') |
soup.find('h2').text | page.css('h2::text').get() |
el.get('href') | el.attrib['href'] ou page.css('a::attr(href)').get() |
el.find_next_sibling() | el.next_sibling |
| (aucun anti-bot) | StealthyFetcher.fetch(url, solve_cloudflare=True) |
| (aucun self-healing) | page.css('.x', adaptive=True) |
scrapling install exécuté avant tout fetcher navigateur ?==0.4.9) — l'API 0.4 a changé vs 0.3 ?proxy_rotator=, jamais avec proxy= ?adaptive=True précédé d'un run auto_save=True ?on_scraped_item, pas process_item ?solve_cloudflare=True réservé aux cibles réellement protégées (coûteux) ?| Librairie | Temps | vs Scrapling |
|---|---|---|
| Scrapling / Parsel | ~2 ms | 1x |
| Raw lxml | ~2.5 ms | ~1.3x |
| PyQuery | ~24 ms | ~12x |
| Selectolax | ~83 ms | ~41x |
| BS4 + lxml | ~1.6 s | ~780x |
Chiffres officiels du repo (susceptibles d'évoluer entre versions). Le gain réel dépend de votre charge ; benchmarkez votre cas.
from scrapling.parser import Selector
page = Selector("<html>...</html>")
titles = page.css('h2::text').getall()→ references/patterns.md — patterns d'intégration réutilisables (monitoring prix, crawl → PostgreSQL, MCP → agent, async batch).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.