diff --git a/README.md b/README.md index 82c24a6..537d496 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

D4Vinci%2FScrapling | Trendshift
- العربيه | Español | Français | Deutsch | 简体中文 | 日本語 | Русский | 한국어 + العربيه | Español | Português (Brasil) | Français | Deutsch | 简体中文 | 日本語 | Русский | 한국어
Tests diff --git a/agent-skill/Scrapling-Skill.zip b/agent-skill/Scrapling-Skill.zip index 220183b..daa1f08 100644 Binary files a/agent-skill/Scrapling-Skill.zip and b/agent-skill/Scrapling-Skill.zip differ diff --git a/agent-skill/Scrapling-Skill/SKILL.md b/agent-skill/Scrapling-Skill/SKILL.md index e1fc735..2cd1f84 100644 --- a/agent-skill/Scrapling-Skill/SKILL.md +++ b/agent-skill/Scrapling-Skill/SKILL.md @@ -1,7 +1,7 @@ --- name: scrapling-official description: Scrape web pages using Scrapling with anti-bot bypass (like Cloudflare Turnstile), stealth headless browsing, spiders framework, adaptive scraping, and JavaScript rendering. Use when asked to scrape, crawl, or extract data from websites; web_fetch fails; the site has anti-bot protections; write Python code to scrape/crawl; or write spiders. -version: "0.4.6" +version: "0.4.7" license: Complete terms in LICENSE.txt metadata: homepage: "https://scrapling.readthedocs.io/en/latest/index.html" @@ -40,7 +40,7 @@ Blazing fast crawls with real-time stats and streaming. Built by Web Scrapers fo Create a virtual Python environment through any way available, like `venv`, then inside the environment do: -`pip install "scrapling[all]>=0.4.6"` +`pip install "scrapling[all]>=0.4.7"` Then do this to download all the browsers' dependencies: diff --git a/agent-skill/Scrapling-Skill/examples/README.md b/agent-skill/Scrapling-Skill/examples/README.md index 4f645cd..388a594 100644 --- a/agent-skill/Scrapling-Skill/examples/README.md +++ b/agent-skill/Scrapling-Skill/examples/README.md @@ -9,7 +9,7 @@ All examples collect **all 100 quotes across 10 pages**. Make sure Scrapling is installed: ```bash -pip install "scrapling[all]>=0.4.6" +pip install "scrapling[all]>=0.4.7" scrapling install --force ``` diff --git a/agent-skill/Scrapling-Skill/references/mcp-server.md b/agent-skill/Scrapling-Skill/references/mcp-server.md index a860f0a..48a2d10 100644 --- a/agent-skill/Scrapling-Skill/references/mcp-server.md +++ b/agent-skill/Scrapling-Skill/references/mcp-server.md @@ -1,8 +1,8 @@ # Scrapling MCP Server -The Scrapling MCP server exposes nine web scraping tools over the MCP protocol. It supports CSS-selector-based content narrowing (reducing tokens by extracting only relevant elements before returning results), three levels of scraping capability (plain HTTP, browser-rendered, and stealth/anti-bot bypass), and persistent browser session management. +The Scrapling MCP server exposes ten tools over the MCP protocol. It supports CSS-selector-based content narrowing (reducing tokens by extracting only relevant elements before returning results), three levels of scraping capability (plain HTTP, browser-rendered, and stealth/anti-bot bypass), persistent browser session management, and page screenshots returned as real image content blocks. -All scraping tools return a `ResponseModel` with fields: `status` (int), `content` (list of strings), `url` (str). +All scraping tools return a `ResponseModel` with fields: `status` (int), `content` (list of strings), `url` (str). The `screenshot` tool returns a list of MCP content blocks: an `ImageContent` (the screenshot bytes) followed by a `TextContent` (the post-redirect URL). ## Tools @@ -99,17 +99,18 @@ Opens a browser session that stays alive across multiple fetch calls, avoiding t **Key parameters:** -| Parameter | Type | Default | Description | -|--------------------|-----------------------------|--------------|---------------------------------------------------------------------| -| `session_type` | `"dynamic"` / `"stealthy"` | required | Type of browser session to create | -| `headless` | bool | true | Run browser hidden or visible | -| `max_pages` | int | 5 | Max concurrent browser tabs (1-50) | -| `proxy` | str or dict or null | null | Proxy for all requests in this session | -| `timeout` | number | 30000 | Default timeout in ms | -| `solve_cloudflare` | bool | false | (Stealthy only) Auto-solve Cloudflare challenges | -| `hide_canvas` | bool | false | (Stealthy only) Canvas fingerprint noise | -| `block_webrtc` | bool | false | (Stealthy only) Block WebRTC IP leak | -| `allow_webgl` | bool | true | (Stealthy only) Keep WebGL enabled | +| Parameter | Type | Default | Description | +|--------------------|-----------------------------|--------------|-------------------------------------------------------------------------------------------------------| +| `session_type` | `"dynamic"` / `"stealthy"` | required | Type of browser session to create | +| `session_id` | str or null | null | Custom ID for the session. If omitted, a random 12-char hex ID is generated. Raises if already in use | +| `headless` | bool | true | Run browser hidden or visible | +| `max_pages` | int | 5 | Max concurrent browser tabs (1-50) | +| `proxy` | str or dict or null | null | Proxy for all requests in this session | +| `timeout` | number | 30000 | Default timeout in ms | +| `solve_cloudflare` | bool | false | (Stealthy only) Auto-solve Cloudflare challenges | +| `hide_canvas` | bool | false | (Stealthy only) Canvas fingerprint noise | +| `block_webrtc` | bool | false | (Stealthy only) Block WebRTC IP leak | +| `allow_webgl` | bool | true | (Stealthy only) Keep WebGL enabled | Plus all other browser session parameters (`google_search`, `real_chrome`, `cdp_url`, `locale`, `timezone_id`, `useragent`, `extra_headers`, `cookies`, `disable_resources`, `network_idle`, `wait_selector`, `wait_selector_state`). @@ -131,6 +132,25 @@ Returns a list of `SessionInfo` objects, each with `session_id`, `session_type`, No parameters. +### `screenshot` -- Capture a page screenshot + +Navigates to a URL inside an existing browser session and returns the screenshot as an MCP `ImageContent` block (the bytes the model can see directly, not a base64 string in JSON) followed by a `TextContent` block carrying the post-redirect URL. + +Requires an open browser session. Call `open_session` first, then pass the `session_id` here. Both `dynamic` and `stealthy` sessions are accepted. + +| Parameter | Type | Default | Description | +|-----------------------|-----------------------|--------------|--------------------------------------------------------------------------------------| +| `url` | str | required | URL to navigate to and capture | +| `session_id` | str | required | ID of an open browser session created with `open_session` | +| `image_type` | `"png"` / `"jpeg"` | `"png"` | Image format. Use `"jpeg"` for smaller payloads | +| `full_page` | bool | false | Capture the full scrollable page instead of just the viewport | +| `quality` | int or null | null | JPEG quality 0-100. Raises if passed with `image_type="png"` | +| `wait` | number | 0 | Extra wait (ms) after page load before capture | +| `wait_selector` | str or null | null | CSS selector to wait for before capture | +| `wait_selector_state` | str | `"attached"` | State for `wait_selector`: `"attached"` / `"visible"` / `"hidden"` / `"detached"` | +| `network_idle` | bool | false | Wait until no network activity for 500ms | +| `timeout` | number | 30000 | Timeout in milliseconds | + ## Tool selection guide | Scenario | Tool | @@ -142,6 +162,7 @@ No parameters. | Cloudflare or strong anti-bot protection | `stealthy_fetch` (with `solve_cloudflare=true` for Turnstile) | | Multiple protected pages | `bulk_stealthy_fetch` | | Multiple pages from the same site | `open_session` + `fetch`/`stealthy_fetch` with `session_id` | +| Need a screenshot of a page | `open_session` + `screenshot` with `session_id` | Start with `get` (fastest, lowest resource cost). Escalate to `fetch` if content requires JS rendering. Escalate to `stealthy_fetch` only if blocked. For multiple pages from the same site, use a persistent session to avoid browser launch overhead. diff --git a/docs/README_PT_BR.md b/docs/README_PT_BR.md new file mode 100644 index 0000000..8e6273a --- /dev/null +++ b/docs/README_PT_BR.md @@ -0,0 +1,554 @@ + + +

+ + + + Scrapling Poster + + +
+ Web Scraping sem esforço para a web moderna +

+ +

+ D4Vinci%2FScrapling | Trendshift +
+ + Tests + + PyPI version + PyPI package downloads + + Static Badge + + OpenClaw Skill +
+ + Discord + + + X (formerly Twitter) Follow + +
+ + Supported Python versions +

+ +

+ Métodos de seleção + · + Fetchers + · + Spiders + · + Rotação de proxy + · + CLI + · + MCP +

+ +Scrapling é um framework adaptativo de Web Scraping que lida com tudo, desde uma única requisição até um crawl em larga escala. + +Seu parser aprende com as mudanças nos sites e relocaliza automaticamente seus elementos quando as páginas são atualizadas. Seus fetchers contornam sistemas anti-bot como o Cloudflare Turnstile de forma nativa. E seu framework de spiders permite escalar para crawls concorrentes com múltiplas sessões, pausa/retomada e rotação automática de proxies, tudo em poucas linhas de Python. Uma biblioteca, zero concessões. + +Crawls extremamente rápidos com estatísticas em tempo real e streaming. Feito por Web Scrapers para Web Scrapers e usuários comuns, há algo para todo mundo. + +```python +from scrapling.fetchers import Fetcher, AsyncFetcher, StealthyFetcher, DynamicFetcher +StealthyFetcher.adaptive = True +p = StealthyFetcher.fetch('https://example.com', headless=True, network_idle=True) # Busque o site sem chamar atenção! +products = p.css('.product', auto_save=True) # Extraia dados que sobrevivem a mudanças no design do site! +products = p.css('.product', adaptive=True) # Depois, se a estrutura do site mudar, passe `adaptive=True` para encontrá-los! +``` +Ou escale para crawls completos +```python +from scrapling.spiders import Spider, Response + +class MySpider(Spider): + name = "demo" + start_urls = ["https://example.com/"] + + async def parse(self, response: Response): + for item in response.css('.product'): + yield {"title": item.css('h2::text').get()} + +MySpider().start() +``` + +

+ + At DataImpulse, we specialize in developing custom proxy services for your business. Make requests from anywhere, collect data, and enjoy fast connections with our premium proxies. + +

+ +# Patrocinadores Platina + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Scrapling lida com o Cloudflare Turnstile. Para proteção de nível empresarial, + Hyper Solutions + oferece endpoints de API que geram tokens antibot válidos para Akamai, DataDome, Kasada e Incapsula. Chamadas simples de API, sem necessidade de automação de navegador.
+ + + + Nós criamos a + BirdProxies + porque proxies não deveriam ser complicados nem caros. Proxies residenciais e ISP rápidos em mais de 195 localidades, preços justos e suporte de verdade.
+ Experimente nosso jogo FlappyBird na landing page para ganhar dados grátis! +
+ + + + + + Evomi + : proxies residenciais a partir de US$0.49/GB. Navegador de scraping com Chromium totalmente spoofado, IPs residenciais, resolução automática de CAPTCHA e bypass anti-bot.
+ Scraper API para resultados sem complicação. Integrações com MCP e N8N estão disponíveis. +
+ + + + + TikHub.io oferece mais de 900 APIs estáveis em mais de 16 plataformas, incluindo TikTok, X, YouTube e Instagram, com mais de 40M de datasets.
Também oferece modelos de IA com desconto - Claude, GPT, GEMINI e mais com até 71% de desconto. +
+ + + + + Nsocks fornece proxies residenciais e ISP rápidos para desenvolvedores e scrapers. Cobertura global de IPs, alto anonimato, rotação inteligente e desempenho confiável para automação e extração de dados. Use o Xcrawl para simplificar o crawling web em larga escala. +
+ + + + + Feche o notebook. Seus scrapers continuam rodando.
+ PetroSky VPS - servidores em nuvem feitos para automação ininterrupta. Máquinas Windows e Linux com controle total. A partir de €6.99/mês. +
+ + + + + Leia uma análise completa do Scrapling no The Web Scraping Club (nov. 2025), a newsletter número 1 dedicada a Web Scraping. +
+ + + + + Proxy-Seller fornece infraestrutura de proxy confiável para web scraping, oferecendo proxies IPv4, IPv6, ISP, residenciais e móveis com desempenho estável, ampla cobertura geográfica e planos flexíveis para coleta de dados em escala empresarial. +
+ + + + + Proxies estáveis para scraping, automação e multi-accounting. IPs limpos, resposta rápida e desempenho confiável sob carga. Feito para fluxos de trabalho escaláveis. +
+ + + + + Swiftproxy fornece proxies residenciais escaláveis com mais de 80M de IPs em mais de 195 países, entregando conexões rápidas e confiáveis, rotação automática e forte desempenho anti-bloqueio. Teste grátis disponível. +
+ +Quer mostrar seu anúncio aqui? Clique [aqui](https://github.com/sponsors/D4Vinci/sponsorships?tier_id=586646) +# Patrocinadores + + + + + + + + + + + + + + + +Quer mostrar seu anúncio aqui? Clique [aqui](https://github.com/sponsors/D4Vinci) e escolha o plano que fizer mais sentido para você! + +--- + +## Principais Recursos + +### Spiders - Um Framework Completo de Crawling +- 🕷️ **API de Spider estilo Scrapy**: Defina spiders com `start_urls`, callbacks assíncronos `parse` e objetos `Request`/`Response`. +- ⚡ **Crawling Concorrente**: Limites de concorrência configuráveis, throttling por domínio e delays de download. +- 🔄 **Suporte Multi-Sessão**: Interface unificada para requisições HTTP e navegadores headless furtivos em uma única spider - direcione requisições para diferentes sessões por ID. +- 💾 **Pausa e Retomada**: Persistência de crawl baseada em checkpoints. Pressione Ctrl+C para um encerramento gracioso; reinicie para continuar de onde parou. +- 📡 **Modo Streaming**: Faça streaming dos itens extraídos conforme chegam com `async for item in spider.stream()` e estatísticas em tempo real - ideal para UI, pipelines e crawls de longa duração. +- 🛡️ **Detecção de Requisições Bloqueadas**: Detecção automática e retry de requisições bloqueadas com lógica personalizável. +- 🤖 **Conformidade com robots.txt**: Flag opcional `robots_txt_obey` que respeita as diretivas `Disallow`, `Crawl-delay` e `Request-rate` com cache por domínio. +- 🧪 **Modo de Desenvolvimento**: Armazene respostas em disco na primeira execução e reproduza-as nas seguintes - itere sobre sua lógica de `parse()` sem reenviar requisições aos servidores-alvo. +- 📦 **Exportação Nativa**: Exporte resultados via hooks, seu próprio pipeline ou JSON/JSONL nativos com `result.items.to_json()` / `result.items.to_jsonl()` respectivamente. + +### Busca Avançada de Sites com Suporte a Sessões +- **Requisições HTTP**: Requisições HTTP rápidas e furtivas com a classe `Fetcher`. Pode imitar fingerprint TLS de navegadores, cabeçalhos e usar HTTP/3. +- **Carregamento Dinâmico**: Busque sites dinâmicos com automação completa de navegador através da classe `DynamicFetcher`, compatível com o Chromium do Playwright e o Google Chrome. +- **Bypass Anti-Bot**: Capacidades avançadas de stealth com `StealthyFetcher` e spoofing de fingerprint. Pode contornar facilmente todos os tipos de Turnstile/Interstitial do Cloudflare com automação. +- **Gerenciamento de Sessão**: Suporte a sessões persistentes com as classes `FetcherSession`, `StealthySession` e `DynamicSession` para gerenciar cookies e estado entre requisições. +- **Rotação de Proxy**: `ProxyRotator` nativo com estratégias cíclicas ou personalizadas em todos os tipos de sessão, além de sobrescritas de proxy por requisição. +- **Bloqueio de Domínios e Anúncios**: Bloqueie requisições para domínios específicos (e seus subdomínios) ou habilite o bloqueio nativo de anúncios (~3.500 domínios conhecidos de anúncios/rastreadores) nos fetchers baseados em navegador. +- **Prevenção de Vazamento de DNS**: Suporte opcional a DNS-over-HTTPS para rotear consultas DNS através do DoH da Cloudflare, evitando vazamentos de DNS ao usar proxies. +- **Suporte Async**: Suporte assíncrono completo em todos os fetchers e classes dedicadas de sessão async. + +### Scraping Adaptativo e Integração com IA +- 🔄 **Rastreamento Inteligente de Elementos**: Relocalize elementos após mudanças no site usando algoritmos inteligentes de similaridade. +- 🎯 **Seleção Flexível Inteligente**: Seletores CSS, seletores XPath, busca baseada em filtros, busca por texto, busca por regex e muito mais. +- 🔍 **Encontrar Elementos Semelhantes**: Localize automaticamente elementos parecidos com os elementos encontrados. +- 🤖 **Servidor MCP para uso com IA**: Servidor MCP nativo para Web Scraping assistido por IA e extração de dados. O servidor MCP oferece capacidades poderosas e personalizadas que usam o Scrapling para extrair conteúdo direcionado antes de passá-lo à IA (Claude/Cursor/etc), acelerando as operações e reduzindo custos ao minimizar o uso de tokens. ([vídeo demo](https://www.youtube.com/watch?v=qyFk3ZNwOxE)) + +### Arquitetura de Alto Desempenho e Testada em Batalha +- 🚀 **Muito Rápido**: Desempenho otimizado que supera a maioria das bibliotecas Python de scraping. +- 🔋 **Eficiente em Memória**: Estruturas de dados otimizadas e lazy loading para um uso mínimo de memória. +- ⚡ **Serialização JSON Rápida**: 10x mais rápido que a biblioteca padrão. +- 🏗️ **Testado em batalha**: O Scrapling não apenas tem 92% de cobertura de testes e cobertura completa de type hints, como também vem sendo usado diariamente por centenas de Web Scrapers ao longo do último ano. + +### Experiência Amigável para Desenvolvedores/Web Scrapers +- 🎯 **Shell Interativo de Web Scraping**: Shell opcional embutido em IPython com integração ao Scrapling, atalhos e novas ferramentas para acelerar o desenvolvimento de scripts de Web Scraping, como converter requisições curl em requisições Scrapling e visualizar resultados no navegador. +- 🚀 **Use diretamente no Terminal**: Opcionalmente, você pode usar o Scrapling para extrair uma URL sem escrever uma única linha de código! +- 🛠️ **API Rica de Navegação**: Travessia avançada do DOM com métodos de navegação por pais, irmãos e filhos. +- 🧬 **Processamento de Texto Aprimorado**: Métodos nativos de regex, limpeza e operações de string otimizadas. +- 📝 **Geração Automática de Seletores**: Gere seletores CSS/XPath robustos para qualquer elemento. +- 🔌 **API Familiar**: Semelhante a Scrapy/BeautifulSoup, com os mesmos pseudo-elementos usados em Scrapy/Parsel. +- 📘 **Cobertura Completa de Tipos**: Type hints completos para excelente suporte em IDEs e autocompletar de código. Todo o codebase é escaneado automaticamente com **PyRight** e **MyPy** a cada alteração. +- 🔋 **Imagem Docker Pronta**: A cada release, uma imagem Docker contendo todos os navegadores é construída e publicada automaticamente. + +## Primeiros Passos + +Vamos dar uma visão rápida do que o Scrapling pode fazer sem entrar em muitos detalhes. + +### Uso Básico +Requisições HTTP com suporte a sessões +```python +from scrapling.fetchers import Fetcher, FetcherSession + +with FetcherSession(impersonate='chrome') as session: # Use a versão mais recente da fingerprint TLS do Chrome + page = session.get('https://quotes.toscrape.com/', stealthy_headers=True) + quotes = page.css('.quote .text::text').getall() + +# Ou use requisições avulsas +page = Fetcher.get('https://quotes.toscrape.com/') +quotes = page.css('.quote .text::text').getall() +``` +Modo stealth avançado +```python +from scrapling.fetchers import StealthyFetcher, StealthySession + +with StealthySession(headless=True, solve_cloudflare=True) as session: # Mantenha o navegador aberto até terminar + page = session.fetch('https://nopecha.com/demo/cloudflare', google_search=False) + data = page.css('#padded_content a').getall() + +# Ou use o estilo de requisição avulsa, ele abre o navegador para esta requisição e o fecha ao finalizar +page = StealthyFetcher.fetch('https://nopecha.com/demo/cloudflare') +data = page.css('#padded_content a').getall() +``` +Automação completa de navegador +```python +from scrapling.fetchers import DynamicFetcher, DynamicSession + +with DynamicSession(headless=True, disable_resources=False, network_idle=True) as session: # Mantenha o navegador aberto até terminar + page = session.fetch('https://quotes.toscrape.com/', load_dom=False) + data = page.xpath('//span[@class="text"]/text()').getall() # Se preferir, use seletor XPath + +# Ou use o estilo de requisição avulsa, ele abre o navegador para esta requisição e o fecha ao finalizar +page = DynamicFetcher.fetch('https://quotes.toscrape.com/') +data = page.css('.quote .text::text').getall() +``` + +### Spiders +Construa crawlers completos com requisições concorrentes, múltiplos tipos de sessão e pausa/retomada: +```python +from scrapling.spiders import Spider, Request, 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() +print(f"Extraídas {len(result.items)} citações") +result.items.to_json("quotes.json") +``` +Use múltiplos tipos de sessão em uma única spider: +```python +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(): + # Direcione páginas protegidas através da sessão stealth + if "protected" in link: + yield Request(link, sid="stealth") + else: + yield Request(link, sid="fast", callback=self.parse) # callback explícito +``` +Pause e retome crawls longos com checkpoints executando a spider assim: +```python +QuotesSpider(crawldir="./crawl_data").start() +``` +Pressione Ctrl+C para pausar de forma graciosa - o progresso é salvo automaticamente. Depois, quando você iniciar a spider novamente, passe o mesmo `crawldir` e ela continuará de onde parou. + +### Parsing Avançado e Navegação +```python +from scrapling.fetchers import Fetcher + +# Seleção rica de elementos e navegação +page = Fetcher.get('https://quotes.toscrape.com/') + +# Obtenha citações com múltiplos métodos de seleção +quotes = page.css('.quote') # Seletor CSS +quotes = page.xpath('//div[@class="quote"]') # XPath +quotes = page.find_all('div', {'class': 'quote'}) # Estilo BeautifulSoup +# O mesmo que +quotes = page.find_all('div', class_='quote') +quotes = page.find_all(['div'], class_='quote') +quotes = page.find_all(class_='quote') # e assim por diante... +# Encontre elementos por conteúdo de texto +quotes = page.find_by_text('quote', tag='div') + +# Navegação avançada +quote_text = page.css('.quote')[0].css('.text::text').get() +quote_text = page.css('.quote').css('.text::text').getall() # Seletores encadeados +first_quote = page.css('.quote')[0] +author = first_quote.next_sibling.css('.author::text') +parent_container = first_quote.parent + +# Relações e similaridade entre elementos +similar_elements = first_quote.find_similar() +below_elements = first_quote.below_elements() +``` +Você pode usar o parser imediatamente se não quiser buscar sites, como abaixo: +```python +from scrapling.parser import Selector + +page = Selector("...") +``` +E ele funciona exatamente da mesma maneira! + +### Exemplos de Gerenciamento de Sessão Assíncrona +```python +import asyncio +from scrapling.fetchers import FetcherSession, AsyncStealthySession, AsyncDynamicSession + +async with FetcherSession(http3=True) as session: # `FetcherSession` entende o contexto e funciona tanto em padrões sync quanto async + page1 = session.get('https://quotes.toscrape.com/') + page2 = session.get('https://quotes.toscrape.com/', impersonate='firefox135') + +# Uso de sessão assíncrona +async with AsyncStealthySession(max_pages=2) as session: + tasks = [] + urls = ['https://example.com/page1', 'https://example.com/page2'] + + for url in urls: + task = session.fetch(url) + tasks.append(task) + + print(session.get_pool_stats()) # Opcional - O estado do pool de abas do navegador (ocupada/livre/erro) + results = await asyncio.gather(*tasks) + print(session.get_pool_stats()) +``` + +## CLI e Shell Interativo + +O Scrapling inclui uma poderosa interface de linha de comando: + +[![asciicast](https://asciinema.org/a/736339.svg)](https://asciinema.org/a/736339) + +Inicie o shell interativo de Web Scraping +```bash +scrapling shell +``` +Extraia páginas diretamente para um arquivo sem programar (por padrão, extrai o conteúdo dentro da tag `body`). Se o arquivo de saída terminar com `.txt`, então o conteúdo em texto do alvo será extraído. Se terminar com `.md`, será uma representação em Markdown do conteúdo HTML; se terminar com `.html`, será o próprio conteúdo HTML. +```bash +scrapling extract get 'https://example.com' content.md +scrapling extract get 'https://example.com' content.txt --css-selector '#fromSkipToProducts' --impersonate 'chrome' # Todos os elementos que correspondem ao seletor CSS '#fromSkipToProducts' +scrapling extract fetch 'https://example.com' content.md --css-selector '#fromSkipToProducts' --no-headless +scrapling extract stealthy-fetch 'https://nopecha.com/demo/cloudflare' captchas.html --css-selector '#padded_content a' --solve-cloudflare +``` + +> [!NOTE] +> Existem muitos recursos adicionais, mas queremos manter esta página concisa, incluindo o servidor MCP e o Shell Interativo de Web Scraping. Confira a documentação completa [aqui](https://scrapling.readthedocs.io/en/latest/) + +## Benchmarks de Desempenho + +O Scrapling não é apenas poderoso - ele também é extremamente rápido. Os benchmarks abaixo comparam o parser do Scrapling com as versões mais recentes de outras bibliotecas populares. + +### Teste de Velocidade de Extração de Texto (5000 elementos aninhados) + +| # | Biblioteca | Tempo (ms) | vs Scrapling | +|---|:-----------------:|:----------:|:------------:| +| 1 | Scrapling | 2.02 | 1.0x | +| 2 | Parsel/Scrapy | 2.04 | 1.01 | +| 3 | Raw Lxml | 2.54 | 1.257 | +| 4 | PyQuery | 24.17 | ~12x | +| 5 | Selectolax | 82.63 | ~41x | +| 6 | MechanicalSoup | 1549.71 | ~767.1x | +| 7 | BS4 with Lxml | 1584.31 | ~784.3x | +| 8 | BS4 with html5lib | 3391.91 | ~1679.1x | + + +### Desempenho de Similaridade de Elementos e Busca por Texto + +Os recursos de localização adaptativa de elementos do Scrapling superam significativamente as alternativas: + +| Biblioteca | Tempo (ms) | vs Scrapling | +|-------------|:----------:|:------------:| +| Scrapling | 2.39 | 1.0x | +| AutoScraper | 12.45 | 5.209x | + + +> Todos os benchmarks representam médias de 100+ execuções. Veja [benchmarks.py](https://github.com/D4Vinci/Scrapling/blob/main/benchmarks.py) para a metodologia. + +## Instalação + +O Scrapling requer Python 3.10 ou superior: + +```bash +pip install scrapling +``` + +Esta instalação inclui apenas o motor de parsing e suas dependências, sem fetchers nem dependências de linha de comando. + +### Dependências Opcionais + +1. Se você vai usar qualquer um dos recursos extras abaixo, os fetchers ou suas classes, precisará instalar as dependências dos fetchers e as dependências de navegador deles da seguinte forma: + ```bash + pip install "scrapling[fetchers]" + + scrapling install # instalação normal + scrapling install --force # forçar reinstalação + ``` + + Isso baixa todos os navegadores, juntamente com suas dependências de sistema e dependências de manipulação de fingerprint. + + Ou você pode instalá-los a partir do código em vez de executar um comando como este: + ```python + from scrapling.cli import install + + install([], standalone_mode=False) # instalação normal + install(["--force"], standalone_mode=False) # forçar reinstalação + ``` + +2. Recursos extras: + - Instale o recurso do servidor MCP: + ```bash + pip install "scrapling[ai]" + ``` + - Instale os recursos do shell (Shell de Web Scraping e o comando `extract`): + ```bash + pip install "scrapling[shell]" + ``` + - Instale tudo: + ```bash + pip install "scrapling[all]" + ``` + Lembre-se de que você precisa instalar as dependências de navegador com `scrapling install` depois de qualquer um desses extras (caso ainda não tenha feito isso) + +### Docker +Você também pode baixar uma imagem Docker com todos os extras e navegadores com o seguinte comando a partir do DockerHub: +```bash +docker pull pyd4vinci/scrapling +``` +Ou baixá-la do registro do GitHub: +```bash +docker pull ghcr.io/d4vinci/scrapling:latest +``` +Essa imagem é construída e publicada automaticamente usando GitHub Actions e o branch principal do repositório. + +## Contribuindo + +Contribuições são bem-vindas! Leia nossas [diretrizes de contribuição](https://github.com/D4Vinci/Scrapling/blob/main/CONTRIBUTING.md) antes de começar. + +## Aviso Legal + +> [!CAUTION] +> Esta biblioteca é fornecida apenas para fins educacionais e de pesquisa. Ao usar esta biblioteca, você concorda em cumprir as leis locais e internacionais de scraping de dados e privacidade. Os autores e contribuidores não se responsabilizam por qualquer uso indevido deste software. Sempre respeite os termos de serviço dos sites e os arquivos robots.txt. + +## 🎓 Citações +Se você usou nossa biblioteca para fins de pesquisa, cite-nos com a seguinte referência: +```text + @misc{scrapling, + author = {Karim Shoair}, + title = {Scrapling}, + year = {2024}, + url = {https://github.com/D4Vinci/Scrapling}, + note = {An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!} + } +``` + +## Licença + +Este trabalho está licenciado sob a licença BSD-3-Clause. + +## Agradecimentos + +Este projeto inclui código adaptado de: +- Parsel (Licença BSD) - usado para o submódulo [translator](https://github.com/D4Vinci/Scrapling/blob/main/scrapling/core/translator.py) + +--- +
Projetado e desenvolvido com ❤️ por Karim Shoair.

diff --git a/docs/ai/mcp-server.md b/docs/ai/mcp-server.md index 08c7b61..534c525 100644 --- a/docs/ai/mcp-server.md +++ b/docs/ai/mcp-server.md @@ -6,7 +6,7 @@ The **Scrapling MCP Server** is a new feature that brings Scrapling's powerful W ## Features -The Scrapling MCP Server provides nine powerful tools for web scraping: +The Scrapling MCP Server provides ten powerful tools for web scraping: ### 🚀 Basic HTTP Scraping - **`get`**: Fast HTTP requests with browser fingerprint impersonation, generating real browser headers matching the TLS version, HTTP/3, and more! @@ -20,6 +20,9 @@ The Scrapling MCP Server provides nine powerful tools for web scraping: - **`stealthy_fetch`**: Uses our Stealthy browser to bypass Cloudflare Turnstile/Interstitial and other anti-bot systems with complete control over the request/browser! - **`bulk_stealthy_fetch`**: An async version of the above tool that allows stealth scraping of multiple URLs in different browser tabs at the same time! +### 📸 Screenshots +- **`screenshot`**: Capture a PNG or JPEG screenshot of a page using an open browser session, returned as an image content block the model can actually see (not a base64 string blob). Supports full-page captures, JPEG quality, and the usual readiness controls (`wait`, `wait_selector`, `network_idle`). + ### 🔌 Session Management - **`open_session`**: Create a persistent browser session (dynamic or stealthy) that stays open across multiple fetch calls, avoiding the overhead of launching a new browser each time. - **`close_session`**: Close a persistent browser session and free its resources. @@ -331,6 +334,14 @@ This protection runs automatically on all MCP tool responses. Keep `main_content - Always close sessions with `close_session` when done to free resources - Use `list_sessions` to check which sessions are still active - A `session_id` from a dynamic session can only be used with `fetch`/`bulk_fetch`, and a stealthy session can only be used with `stealthy_fetch`/`bulk_stealthy_fetch` +- Pass a custom `session_id` to `open_session` to give sessions meaningful names (e.g. `"search"`, `"checkout"`) instead of the random hex default. `open_session` raises if the chosen ID is already in use, so you can detect collisions up front + +### 7. Capturing Screenshots +- `screenshot` only works through an existing browser session, so call `open_session` first (either `dynamic` or `stealthy` works) +- The image is returned as a real `ImageContent` block, not a base64 string in JSON, so the model sees the page directly +- Use `full_page=True` when you need everything below the fold; the default captures only the visible viewport +- Pick `image_type="jpeg"` with a `quality` value (0-100) for smaller payloads when pixel-perfect color isn't needed +- The same `wait`, `wait_selector`, `network_idle`, and `timeout` controls used by `fetch` are available here too ## Legal and Ethical Considerations diff --git a/pyproject.toml b/pyproject.toml index e8a5500..ada11a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "scrapling" # Static version instead of a dynamic version so we can get better layer caching while building docker, check the docker file to understand -version = "0.4.6" +version = "0.4.7" description = "Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be!" readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} @@ -77,8 +77,8 @@ fetchers = [ "patchright==1.58.2", "browserforge>=1.2.4", "apify-fingerprint-datapoints>=0.12.0", - "msgspec>=0.21.0", - "anyio>=4.12.1", + "msgspec>=0.21.1", + "anyio>=4.13.0", "protego>=0.6.0", ] ai = [ diff --git a/scrapling/__init__.py b/scrapling/__init__.py index c0c66ff..97af0c5 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -1,5 +1,5 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" -__version__ = "0.4.6" +__version__ = "0.4.7" __copyright__ = "Copyright (c) 2024 Karim Shoair" from typing import Any, TYPE_CHECKING diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index 060cb20..464d35b 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -3,7 +3,8 @@ from asyncio import gather from datetime import datetime, timezone from dataclasses import dataclass, field -from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp import FastMCP, Image +from mcp.types import ImageContent, TextContent from pydantic import BaseModel, Field from scrapling.core.shell import Convertor @@ -31,6 +32,7 @@ from scrapling.core._types import ( ) SessionType = Literal["dynamic", "stealthy"] +ScreenshotType = Literal["png", "jpeg"] class ResponseModel(BaseModel): @@ -106,14 +108,14 @@ class ScraplingMCPServer: def __init__(self): self._sessions: Dict[str, _SessionEntry] = {} - def _get_session(self, session_id: str, expected_type: SessionType) -> _SessionEntry: - """Look up a session by ID and validate its type.""" + def _get_session(self, session_id: str, expected_type: Optional[SessionType]) -> _SessionEntry: + """Look up a session by ID, optionally validating its type. Pass `None` to skip the type check.""" entry = self._sessions.get(session_id) if entry is None: raise ValueError(f"Session '{session_id}' not found. Use list_sessions to see active sessions.") if not entry.session._is_alive: raise ValueError(f"Session '{session_id}' is no longer alive. Open a new session.") - if entry.session_type != expected_type: + if expected_type is not None and entry.session_type != expected_type: raise ValueError( f"Session '{session_id}' is a '{entry.session_type}' session, but this tool requires a " f"'{expected_type}' session. Use the matching fetch tool for your session type." @@ -123,6 +125,7 @@ class ScraplingMCPServer: async def open_session( self, session_type: SessionType, + session_id: Optional[str] = None, headless: bool = True, google_search: bool = True, real_chrome: bool = False, @@ -152,6 +155,7 @@ class ScraplingMCPServer: Use close_session to close the session when done, and list_sessions to see all active sessions. :param session_type: The type of session to open. Use "dynamic" for standard Playwright browser, or "stealthy" for anti-bot bypass with fingerprint spoofing. + :param session_id: Optional custom session ID. If not provided, a random 12-character hex ID will be generated. Useful for naming sessions for easier management. :param headless: Run the browser in headless/hidden (default), or headful/visible mode. :param google_search: Enabled by default, Scrapling will set a Google referer header. :param real_chrome: If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. @@ -175,6 +179,12 @@ class ScraplingMCPServer: :param solve_cloudflare: (Stealthy only) Solves all types of the Cloudflare's Turnstile/Interstitial challenges. :param additional_args: (Stealthy only) Additional arguments to be passed to Playwright's context as additional settings. """ + session_id = session_id or uuid4().hex[:12] + if session_id in self._sessions: + raise ValueError( + f"Session '{session_id}' already exists. Use a different ID or close the existing session first." + ) + common_kwargs: Dict[str, Any] = dict( wait=wait, proxy=proxy, @@ -211,7 +221,6 @@ class ScraplingMCPServer: await session.start() - session_id = uuid4().hex[:12] entry = _SessionEntry(session=session, session_type=session_type) self._sessions[session_id] = entry @@ -253,6 +262,69 @@ class ScraplingMCPServer: for sid, entry in self._sessions.items() ] + async def screenshot( + self, + url: str, + session_id: str, + image_type: ScreenshotType = "png", + full_page: bool = False, + quality: Optional[int] = None, + wait: int | float = 0, + wait_selector: Optional[str] = None, + wait_selector_state: SelectorWaitStates = "attached", + network_idle: bool = False, + timeout: int | float = 30000, + ) -> List[ImageContent | TextContent]: + """Capture a screenshot of a web page using an existing browser session and return it as an image. + A browser session must be opened first with `open_session` (either `dynamic` or `stealthy`); the session ID is then passed here. + + :param url: The URL to navigate to and capture. + :param session_id: ID of an open browser session created with `open_session`. + :param image_type: Image format. Defaults to "png". Use "jpeg" for smaller file sizes. + :param full_page: When True, captures the full scrollable page instead of just the viewport. Defaults to False. + :param quality: Image quality (0-100) for JPEG only. Raises if passed with `image_type="png"`. + :param wait: Time in milliseconds to wait after page load before capturing. Defaults to 0. + :param wait_selector: Optional CSS selector to wait for before capturing. + :param wait_selector_state: State to wait for the selector. Defaults to "attached". + :param network_idle: Wait for the page until there are no network connections for at least 500 ms. + :param timeout: Timeout in milliseconds for page operations. Defaults to 30,000. + """ + if quality is not None and image_type != "jpeg": + raise ValueError("'quality' is only valid when 'image_type' is 'jpeg'.") + + entry = self._get_session(session_id, expected_type=None) + + screenshot_kwargs: Dict[str, Any] = {"type": image_type, "full_page": full_page} + if quality is not None: + screenshot_kwargs["quality"] = quality + + captured: Dict[str, Any] = {} + + async def _capture(page: Any) -> None: + try: + captured["bytes"] = await page.screenshot(**screenshot_kwargs) + captured["url"] = page.url + except Exception as exc: + captured["error"] = exc + + await entry.session.fetch( + url, + wait=wait, + timeout=timeout, + network_idle=network_idle, + wait_selector=wait_selector, + wait_selector_state=wait_selector_state, + page_action=_capture, + ) + + if "error" in captured: + raise captured["error"] + if "bytes" not in captured: + raise RuntimeError(f"Failed to capture screenshot for {url}") + + image = Image(data=captured["bytes"], format=image_type).to_image_content() + return [image, TextContent(type="text", text=captured["url"])] + @staticmethod async def get( url: str, @@ -291,7 +363,8 @@ class ScraplingMCPServer: :param headers: Headers to include in the request. :param cookies: Cookies to use in the request. :param timeout: Number of seconds to wait before timing out. - :param follow_redirects: Whether to follow redirects. Defaults to "safe", which follows redirects but rejects those targeting internal/private IPs (SSRF protection). Pass True to follow all redirects without restriction. + :param follow_redirects: Whether to follow redirects. Defaults to "safe", which follows redirects but rejects those targeting internal/private IPs (SSRF protection). + Pass True to follow all redirects without restriction. :param max_redirects: Maximum number of redirects. Default 30, use -1 for unlimited. :param retries: Number of retry attempts. Defaults to 3. :param retry_delay: Number of seconds to wait between retry attempts. Defaults to 1 second. @@ -364,7 +437,8 @@ class ScraplingMCPServer: :param headers: Headers to include in the request. :param cookies: Cookies to use in the request. :param timeout: Number of seconds to wait before timing out. - :param follow_redirects: Whether to follow redirects. Defaults to "safe", which follows redirects but rejects those targeting internal/private IPs (SSRF protection). Pass True to follow all redirects without restriction. + :param follow_redirects: Whether to follow redirects. Defaults to "safe", which follows redirects but rejects those targeting internal/private IPs (SSRF protection). + Pass True to follow all redirects without restriction. :param max_redirects: Maximum number of redirects. Default 30, use -1 for unlimited. :param retries: Number of retry attempts. Defaults to 3. :param retry_delay: Number of seconds to wait between retry attempts. Defaults to 1 second. @@ -828,4 +902,6 @@ class ScraplingMCPServer: description=self.bulk_stealthy_fetch.__doc__, structured_output=True, ) + # Screenshot tool (returns image + url content blocks, not structured JSON) + server.add_tool(self.screenshot, title="screenshot", description=self.screenshot.__doc__) server.run(transport="stdio" if not http else "streamable-http") diff --git a/scrapling/engines/static.py b/scrapling/engines/static.py index fae9f83..9b3e951 100644 --- a/scrapling/engines/static.py +++ b/scrapling/engines/static.py @@ -149,6 +149,7 @@ class _ConfigurationLogic(ABC): # Browser session params (ignored by HTTP sessions) "extra_headers", "google_search", + "block_ads", } for k, v in method_kwargs.items(): if k not in skip_keys and v is not None: @@ -718,8 +719,13 @@ class FetcherSession: config["selector_config"] = self.selector_config config["proxy_rotator"] = self._proxy_rotator self._client = _SyncSessionLogic(**config) + try: + result = self._client.__enter__() + except Exception: + self._client = None + raise self._is_alive = True - return self._client.__enter__() + return result raise RuntimeError("This FetcherSession instance already has an active synchronous session.") def __exit__(self, exc_type, exc_val, exc_tb): @@ -739,8 +745,13 @@ class FetcherSession: config["selector_config"] = self.selector_config config["proxy_rotator"] = self._proxy_rotator self._client = _ASyncSessionLogic(**config) + try: + result = await self._client.__aenter__() + except Exception: + self._client = None + raise self._is_alive = True - return await self._client.__aenter__() + return result raise RuntimeError("This FetcherSession instance already has an active asynchronous session.") async def __aexit__(self, exc_type, exc_val, exc_tb): diff --git a/scrapling/spiders/session.py b/scrapling/spiders/session.py index 536be6d..5799e8c 100644 --- a/scrapling/spiders/session.py +++ b/scrapling/spiders/session.py @@ -93,7 +93,9 @@ class SessionManager: async def close(self) -> None: """Close all registered sessions.""" - for session in self._sessions.values(): + for sid, session in self._sessions.items(): + if sid in self._lazy_sessions and not session._is_alive: + continue _ = await session.__aexit__(None, None, None) self._started = False diff --git a/server.json b/server.json index 36f60e7..5415056 100644 --- a/server.json +++ b/server.json @@ -14,12 +14,12 @@ "mimeType": "image/png" } ], - "version": "0.4.6", + "version": "0.4.7", "packages": [ { "registryType": "pypi", "identifier": "scrapling", - "version": "0.4.6", + "version": "0.4.7", "runtimeHint": "uvx", "packageArguments": [ { diff --git a/setup.cfg b/setup.cfg index 0794d59..72d64fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scrapling -version = 0.4.6 +version = 0.4.7 author = Karim Shoair author_email = karim.shoair@pm.me description = Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be! diff --git a/tests/ai/test_ai_mcp.py b/tests/ai/test_ai_mcp.py index d897bb5..19e2f02 100644 --- a/tests/ai/test_ai_mcp.py +++ b/tests/ai/test_ai_mcp.py @@ -1,5 +1,12 @@ +import base64 +import struct +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread + import pytest import pytest_httpbin +from mcp.types import ImageContent, TextContent from scrapling.core.ai import ( ScraplingMCPServer, @@ -177,6 +184,137 @@ class TestSessionManagement: with pytest.raises(ValueError, match="not found"): await server.fetch(url=test_url, session_id=session_id) + @pytest.mark.asyncio + async def test_open_session_with_custom_id(self, server): + """Test opening a session with a custom session_id""" + result = await server.open_session(session_type="dynamic", session_id="my-session", headless=True) + assert isinstance(result, SessionCreatedModel) + assert result.session_id == "my-session" + + await server.close_session("my-session") + + @pytest.mark.asyncio + async def test_open_session_duplicate_id_raises(self, server): + """Test that opening a session with a duplicate session_id raises an error""" + await server.open_session(session_type="dynamic", session_id="dupe", headless=True) + + with pytest.raises(ValueError, match="already exists"): + await server.open_session(session_type="dynamic", session_id="dupe", headless=True) + + await server.close_session("dupe") + + +def _png_height(data: bytes) -> int: + """Read the height field from a PNG IHDR chunk.""" + return struct.unpack(">I", data[20:24])[0] + + +@contextmanager +def _serve_html(body: bytes): + """Serve a fixed HTML body on localhost, yielding its URL.""" + + class _Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args, **kwargs): + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_address[1]}/" + finally: + server.shutdown() + server.server_close() + + +@pytest_httpbin.use_class_based_httpbin +class TestScreenshot: + """Test the screenshot tool""" + + @pytest.fixture(scope="class") + def test_url(self, httpbin): + return f"{httpbin.url}/html" + + @pytest.fixture + def server(self): + return ScraplingMCPServer() + + @pytest.mark.asyncio + async def test_screenshot_png_with_dynamic_session(self, server, test_url): + """PNG screenshot via a dynamic session returns image and url content blocks""" + opened = await server.open_session(session_type="dynamic", headless=True) + try: + result = await server.screenshot(url=test_url, session_id=opened.session_id) + assert isinstance(result, list) and len(result) == 2 + assert isinstance(result[0], ImageContent) + assert result[0].mimeType == "image/png" + assert isinstance(result[1], TextContent) + assert result[1].text == test_url + finally: + await server.close_session(opened.session_id) + + @pytest.mark.asyncio + async def test_screenshot_jpeg_with_quality(self, server, test_url): + """JPEG screenshot with quality parameter via a dynamic session""" + opened = await server.open_session(session_type="dynamic", headless=True) + try: + result = await server.screenshot(url=test_url, session_id=opened.session_id, image_type="jpeg", quality=80) + assert isinstance(result[0], ImageContent) + assert result[0].mimeType == "image/jpeg" + finally: + await server.close_session(opened.session_id) + + @pytest.mark.asyncio + async def test_screenshot_with_stealthy_session(self, server, test_url): + """PNG screenshot via a stealthy session""" + opened = await server.open_session(session_type="stealthy", headless=True) + try: + result = await server.screenshot(url=test_url, session_id=opened.session_id) + assert isinstance(result[0], ImageContent) + assert result[0].mimeType == "image/png" + finally: + await server.close_session(opened.session_id) + + @pytest.mark.asyncio + async def test_screenshot_full_page_taller_than_viewport(self, server): + """full_page=True produces an image taller than the viewport-only capture""" + tall_html = b"
" + with _serve_html(tall_html) as tall_url: + opened = await server.open_session(session_type="dynamic", headless=True) + try: + viewport_result = await server.screenshot(url=tall_url, session_id=opened.session_id, full_page=False) + full_result = await server.screenshot(url=tall_url, session_id=opened.session_id, full_page=True) + + viewport_png = base64.b64decode(viewport_result[0].data) + full_png = base64.b64decode(full_result[0].data) + + assert _png_height(full_png) > _png_height(viewport_png) + finally: + await server.close_session(opened.session_id) + + @pytest.mark.asyncio + async def test_screenshot_invalid_session_id_raises(self, server, test_url): + """Unknown session_id raises ValueError""" + with pytest.raises(ValueError, match="not found"): + await server.screenshot(url=test_url, session_id="does-not-exist") + + @pytest.mark.asyncio + async def test_screenshot_quality_with_png_raises(self, server, test_url): + """quality is rejected when image_type is png""" + opened = await server.open_session(session_type="dynamic", headless=True) + try: + with pytest.raises(ValueError, match="quality"): + await server.screenshot(url=test_url, session_id=opened.session_id, image_type="png", quality=90) + finally: + await server.close_session(opened.session_id) + class TestNormalizeCredentials: """Test the _normalize_credentials helper""" diff --git a/tests/fetchers/test_merge_request_args.py b/tests/fetchers/test_merge_request_args.py new file mode 100644 index 0000000..6b1e101 --- /dev/null +++ b/tests/fetchers/test_merge_request_args.py @@ -0,0 +1,44 @@ +"""Tests for _merge_request_args to ensure browser-only kwargs are excluded. + +Regression tests for https://github.com/D4Vinci/Scrapling/issues/247 +""" + +import pytest + +from scrapling.engines.static import FetcherClient + + +class TestMergeRequestArgsSkipsBrowserParams: + """Verify that browser-only keyword arguments are stripped before + the request dict is forwarded to curl_cffi's Session.request().""" + + def _build_args(self, **extra_kwargs): + """Helper: instantiate a FetcherClient and call _merge_request_args.""" + client = FetcherClient() + return client._merge_request_args(url="https://example.com", **extra_kwargs) + + def test_block_ads_excluded(self): + """block_ads is a browser-engine param and must not leak into the + HTTP request dict (fixes #247).""" + args = self._build_args(block_ads=True) + assert "block_ads" not in args + + def test_google_search_excluded(self): + """google_search is a browser-engine param and should be stripped.""" + args = self._build_args(google_search=True) + assert "google_search" not in args + + def test_extra_headers_excluded(self): + """extra_headers is a browser-engine param and should be stripped.""" + args = self._build_args(extra_headers={"X-Custom": "val"}) + assert "extra_headers" not in args + + def test_url_present(self): + """The url must always be present in the output dict.""" + args = self._build_args() + assert args["url"] == "https://example.com" + + def test_valid_kwargs_passed_through(self): + """Arbitrary curl_cffi-compatible kwargs should survive.""" + args = self._build_args(cookies={"session": "abc"}) + assert args.get("cookies") == {"session": "abc"}