Skip to main content
ClaudeWave
Skill1.7k repo starsupdated today

web-scraping

This Claude Code skill provides web scraping capabilities through two interfaces: MCP tool wrappers (get, fetch, stealthy_fetch) for straightforward HTTP requests and HTML extraction, and a direct Python API with advanced features like async selectors, session management, and spider crawling. Use the simple synchronous MCP tools for quick single-page fetches and markdown conversion; switch to the Python API and write reusable scrapers to work/ for multi-URL crawls, iteration-heavy projects, or anti-bot scenarios requiring Cloudflare bypass.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/ginlix-ai/LangAlpha /tmp/web-scraping && cp -r /tmp/web-scraping/plugins/alternative_data/skills/web-scraping ~/.claude/skills/web-scraping
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Web Scraping

## Overview

Two ways to scrape in the sandbox:

1. **MCP tools** (`scrape_page`, `scrape_pages`) — recommended for straight "give me this page's content". Synchronous, return dicts.
2. **Direct Scrapling Python API** — for CSS/XPath selectors, sessions, logins, and multi-page spiders. Async, returns Page objects with `.css()` / `.xpath()`.

Quick fetches can run inline via `ExecuteCode`. For spiders, multi-URL crawls, or anything you'll iterate on, write the scraper to `work/<task_name>/scraper.py` and run it via `Bash` — edit-and-rerun beats resubmitting code.

## MCP Tools

Import from `tools.scrape`. **Synchronous** — no `await`.

```python
from tools.scrape import scrape_page, scrape_pages
```

### Signatures

```python
scrape_page(url: str, mode: str = "fast", extraction: str = "markdown",
            timeout: float = 30.0, solve_cloudflare: bool = False) -> dict

scrape_pages(urls: list[str], mode: str = "fast", extraction: str = "markdown",
             timeout: float = 30.0, solve_cloudflare: bool = False) -> dict
```

### Parameters

| Param | Default | Notes |
|---|---|---|
| `mode` | `"fast"` | `"fast"` plain HTTP · `"browser"` JS rendering · `"stealth"` bot-protected sites |
| `extraction` | `"markdown"` | `"markdown"` (article text, cleaned) · `"html"` (raw) · `"text"` (plain) |
| `timeout` | `30.0` | Per-fetch **seconds**, 1–60 — seconds in every mode, not ms |
| `solve_cloudflare` | `False` | Only meaningful with `mode="stealth"` |
| `urls` | — | `scrape_pages` only; **max 10** per call |

Escalate modes only as needed: start `fast`, go to `browser` when the page needs JavaScript, `stealth` when you're getting blocked, and add `solve_cloudflare=True` only if `stealth` still returns a challenge page.

### Return shape

`scrape_page` returns a flat dict:

```python
{
    "url": "https://example.com",
    "status": 200,
    "title": "Example Domain",
    "content": "# Example Domain\n\nThis domain is for use in...",  # str
    "extraction": "markdown",
    "mode": "fast",
}
```

- **`content` is a plain string**, not a list — use it directly, never `content[0]` (that yields a single character).
- `content` is truncated to **400,000 chars**.
- No `.css()` / `.xpath()` / `.body` / `.headers` / `.cookies` — for selectors use the direct Python API below, or parse `extraction="html"` with BeautifulSoup.

`scrape_pages` wraps them:

```python
{
    "results": [ ... ],  # one entry per input URL, in input order
    "count": 3,
}
```

### Errors

Errors are returned, never raised. **Always check for `"error"` before reading `content`.**

```python
res = scrape_page(url="https://example.com")
if "error" in res:
    print(res["error"], res["detail"])
else:
    print(res["content"])
```

Per-URL errors — appear as `{"error", "detail", "url"}` entries inside `scrape_pages["results"]`, or as the whole return of `scrape_page`:

| Code | Meaning |
|---|---|
| `invalid_url` | Not an `http://` / `https://` URL |
| `fetch_failed` | Network, DNS, timeout, or browser failure |
| `extract_failed` | Page fetched but the extractor failed on the markup; the entry still carries `status` |
| `scrape_failed` | Unexpected internal failure for that one URL |

Whole-call errors — the entire return is `{"error", "detail"}`, no `results`:

| Code | Meaning |
|---|---|
| `invalid_mode` / `invalid_extraction` / `invalid_timeout` | Bad argument value |
| `invalid_urls` | `scrape_pages` got an empty list or more than 10 URLs |

**One bad URL never sinks a batch.** `scrape_pages` always returns one entry per input URL, in input order — failures come back as error entries alongside the successes.

### Examples

```python
from tools.scrape import scrape_page, scrape_pages

# Single page → markdown
res = scrape_page(url="https://example.com")
if "error" not in res:
    print(res["title"], res["status"], len(res["content"]))

# JS-rendered page
res = scrape_page(url="https://spa-site.com", mode="browser", timeout=60)

# Bot-protected page
res = scrape_page(url="https://protected-site.com", mode="stealth", solve_cloudflare=True)

# Batch — split successes from failures
batch = scrape_pages(urls=[...], mode="fast")   # <= 10 URLs
pages = [r for r in batch["results"] if "error" not in r]
failed = [(r["url"], r["error"]) for r in batch["results"] if "error" in r]

# Raw HTML when you need to parse structure yourself
res = scrape_page(url="https://example.com", extraction="html")
from bs4 import BeautifulSoup
soup = BeautifulSoup(res["content"], "html.parser")
titles = [h1.get_text() for h1 in soup.find_all("h1")]
```

Batches run concurrently — 8 at a time in `fast` mode, 2 at a time in `browser` / `stealth` (browser sessions are memory-heavy). More than 10 URLs means more than one call.

---

## Direct Python API (Advanced)

For selectors, sessions, spiders, or when you need the full Page object. **Requires imports. Async.**

### Fetcher (Fast HTTP — Tier 1)

```python
from scrapling.fetchers import AsyncFetcher

page = await AsyncFetcher.get("https://example.com", stealthy_headers=True)
print(page.status)       # 200
print(page.body)         # Raw bytes
print(page.headers)      # Response headers

# CSS selectors (Scrapy-style pseudo-elements)
titles = page.css("h1::text").getall()
links = page.css("a::attr(href)").getall()

# XPath
items = page.xpath("//div[@class='item']/text()").getall()

# BeautifulSoup-style
divs = page.find_all("div", class_="content")
```

### DynamicFetcher (Browser — Tier 2)

```python
from scrapling.fetchers import DynamicFetcher

page = await DynamicFetcher.async_fetch(
    "https://spa-website.com",
    headless=True,
    network_idle=True,
    disable_resources=True,
    timeout=30000,          # milliseconds here, unlike the MCP tools
    wait_selector=".data-table",
)
rows = page.css("table.data-table tr")
for row in rows:
    cells = row.css("td::text").getall()
```

### StealthyFetcher (Anti-Bot — Tier 3)

```python
from scrapling.fetchers import StealthyFetcher

pag