
Arachne — the Scraper That Became a Platform
Context
Arachne started as a ~200-line Python script. Literally a scraper.py that downloaded pages, threw them at BeautifulSoup, extracted text, and saved to JSON. Nothing more.
The problem is that this script broke every week. The site would change its selector, Cloudflare would start blocking, the page would load via JS and BeautifulSoup would grab nothing. I spent more time putting out fires than actually extracting data.
The last straw was when I lost an entire database because an e-commerce scraper changed its HTML without warning. 2000+ corrupted records. That’s when I decided: I’m going to build something that doesn’t break.
What I didn’t know is that this “something” would become a platform with 3763 files, 740 Python modules, 1460 API routes, 2698 tests, and 36 ReAct tools. But let’s take it step by step.
The 200-line scraper
The first commit was ridiculously simple:
import requests
from bs4 import BeautifulSoup
def scrape(url):
resp = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})
soup = BeautifulSoup(resp.text, 'html.parser')
return {
'title': soup.title.text if soup.title else '',
'text': soup.get_text(separator='\n', strip=True),
'links': [a['href'] for a in soup.find_all('a', href=True)]
}
It worked for static sites. But real sites are anything but static: Cloudflare, CAPTCHA, JS rendering, rate limiting, anti-bot. Each obstacle turned into a new try/except. The code became a 2000-line monstrosity in 3 months.
Then came the decision: either I pay for an expensive scraping service (ScrapingBee, ScrapingAnt — like R$200-500/month), or use heavy tools (Scrapy — 15 minutes to set up a project), or build it my way.
I chose the third option. Obviously.
The evolution to multi-engine
Instead of a single scraper, I created progressive fallback layers:
| Engine | When to use | Latency | Coverage |
|---|---|---|---|
| Trafilatura (std) | Static pages, blogs, docs | ~0.5s | 70% of sites |
| Crawl4AI (sidecar) | SPA, React, Vue, heavy JS | ~1.5s | 85% |
| Playwright (stealth) | Cloudflare, light antibot | ~4s | 92% |
| Camoufox (full evasion) | Aggressive WAF, Captcha | ~8s | 98% |
class ProgressiveFetcher:
def __init__(self):
self.engines = [
TrafilaturaEngine(),
Crawl4AIEngine(),
PlaywrightStealthEngine(),
CamoufoxEngine(),
]
def fetch(self, url):
for engine in self.engines:
result = engine.try_fetch(url)
if result and not result.error:
return result
raise AllEnginesFailed(url)
Each engine has a try_fetch() that returns None on failure. The secret isn’t having one perfect scraper — it’s having 4 that together cover 98% of cases.
The result? Zero manual fallback maintenance. If Trafilatura gets a 403, Crawl4AI tries with JS. If Crawl4AI gets blocked, Playwright tries with stealth. If that fails, Camoufox steps in with real Firefox fingerprinting.
RAG with semantic chunking
With data coming in, the next problem was finding what mattered. Having 10 thousand extracted pages is useless if you can’t search them.
Enter RAG — but not the generic “dump everything into a vector store” RAG. Each step was thought through:
| Step | Technique | Result |
|---|---|---|
| Chunking | 6 strategies (recursive, semantic, by paragraph, sliding window, by topic, markdown-aware) | 98% recall |
| Embedding | sentence-transformers + nomic-embed-text (local) | ~200ms latency |
| Search | Hybrid FTS5 + vector (sqlite-vec) | 94% precision vs 78% vector-only |
| Reranking | Cross-encoder | Top-3 accuracy: 96% |
# Six chunking strategies, one line each
strategies = {
'recursive': RecursiveCharacterTextSplitter(500, 50),
'semantic': SemanticChunker(embedding_model),
'paragraph': ParagraphSplitter(min_chars=200),
'sliding_window': SlidingWindowChunker(300, 100),
'topic': TopicAwareSplitter(),
'markdown': MarkdownStructureSplitter(),
}
The killer feature was hybrid search. Vector search alone is good for semantic similarity but terrible for exact keyword matching. FTS5 alone is the opposite. Combining them with adaptive weighting (FTS5 60% + vector 40%) raised precision from 78% to 94%.
def hybrid_search(query, kb_id, top_k=10):
fts5_results = search_fts5(query, kb_id) # keyword match
vector_results = search_vector(query, kb_id) # semantic match
merged = merge_ranked(fts5_results, vector_results,
fts5_weight=0.6, vector_weight=0.4)
return rerank_cross_encoder(query, merged[:top_k])
Pipelines: from CLI to visual builder
With extraction + RAG working, the inevitable demand came to automate workflows. The pipeline executor was born: 38 handlers that chain together.
Handler registry:
├── Input: url, text, file, webhook, cron, api
├── Extraction: scrape, extract, vision, transcribe
├── Processing: chunk, embed, summarize, translate, classify
├── Transformation: format, template, aggregation, filter
├── Output: save, export, notify, webhook_callback
At first it was just YAML config. Then came the visual builder — drag & drop handlers. Then the integrated cron scheduler. Then pipeline creation via chat (“create a pipeline that extracts AI news every week and saves to JSON”).
| Feature | How it was before | How it is now |
|---|---|---|
| Create pipeline | Manual YAML | Chat, drag & drop or YAML |
| Run | Manual CLI | CRON, webhook or manual |
| Monitor | Terminal logs | Dashboard with charts |
| Test | impossible | Sandbox + step debug |
MCP Tools: the scraper becomes a tool server
The cherry on top was turning everything into an MCP Server (Model Context Protocol). This means any AI agent (Claude, Cursor, Hermes) can use Arachne’s tools without configuring anything — just connect via stdio or SSE.
# run_mcp.py — auto-detect mode: SSE if PORT is set, stdio otherwise
import os, sys
from app.mcp.server import MCPServer
server = MCPServer()
server.register_tools([
arachne_scrape, arachne_browser_extract,
arachne_search, arachne_query,
arachne_transcribe, arachne_vision,
arachne_calc, arachne_screenshot,
arachne_format_converter, arachne_record,
arachne_metrics, arachne_plan,
arachne_capabilities,
])
if 'PORT' in os.environ:
server.run_sse(port=int(os.environ['PORT']))
else:
server.run_stdio()
13 MCP tools today. From a 200-line scraper to a tool server that runs inside Cursor, Claude, and Hermes. Each tool has its own fallback chain, timeouts, cache, and automatic evasion.
The real metrics
No theory — production numbers today:
| Metric | Value | Note |
|---|---|---|
| Avg latency (std) | 1.8s | Direct Trafilatura |
| Latency (stealth) | 4.2s | Playwright with evasion |
| Latency (full) | 7.5s | Camoufox with fingerprint |
| Extraction accuracy | 94.7% | Main content vs noise |
| Coverage (std) | 71% | Sites without blocking |
| Coverage (4 engines) | 98.2% | With progressive fallback |
| Coverage (with browser-run) | 99.1% | With manual interaction |
| RAG top-3 precision | 96% | Hybrid + cross-encoder |
| Tests | 2,698 | 740 Python files |
| Pipeline handlers | 38 | 6 categories |
| ReAct tools | 36 | Agent layer |
| i18n keys | 1,131 | PT/EN without diff |
What I learned on this journey
1. Progressive fallback > perfect scraper
I spent months trying to make ONE scraper that solved everything. The real solution was having 4 scrapers that complement each other. 98.2% coverage vs 70% from any single engine. The best engineering is not depending on a single point of failure.
2. RAG without smart chunking is garbage
At first I used fixed 500-character chunks. The result was bizarre — sentences cut in half, lost context, useless embeddings. 6 chunking strategies with automatic content-type detection solved it. For blogs: markdown-aware. For PDFs: paragraph. For code: sliding window.
3. FTS5 + vector > either one alone
Vector search is hype, but FTS5 wins at exact term search by a huge margin. The hybrid combination with a cross-encoder reranker on top gave 96% accuracy. Without the reranker, it dropped to 89%.
4. API-first from the start
Every new feature in Arachne is born as an API. Only later comes UI, CLI, MCP tool. This forces clean design and allows any frontend to consume. The MCP Server came for free because the APIs already existed.
# Every MCP tool is an existing API + wrapper
@router.post("/scrape")
async def scrape_endpoint(url: str = Body(...)):
engine = select_engine(url) # real logic
result = await engine.extract(url) # real logic
return result # full reuse
5. 740 Python files isn’t a feature — it’s a responsibility
Arachne grew more than I planned. 3763 files total. That takes its toll in maintenance. The secret was keeping the layered architecture from the start: API → agent → pipeline → scraper/RAG. Each layer knows the minimum about the other.
What’s next
Arachne is no longer a scraper — it’s a data intelligence platform. But there’s still ground to cover:
- Public Python SDK (
pip install arachne-sdk) — in progress - Bot Platform — no-code bot builder
- BYO LLM — bring your own model
- Official MCP Directory — submit to the official modelcontextprotocol servers
From 200 lines of Python to 3763 files, 36 ReAct tools and 13 MCP tools. All because a BeautifulSoup broke on a rainy day and I had the dumb idea of “I’ll do better.”
TL;DR: A 200-line scraper became a multi-engine platform with 98% coverage, RAG with semantic chunking (96% precision), visual pipeline with 38 handlers, and MCP Server with 13 tools. All in Python + FastAPI, open source, running locally. The lesson: progressive fallback beats perfect scraper, hybrid search beats vector-only, and API-first pays dividends forever.