AI-assisted web data extractor — paste a URL + plain-English instruction, get structured JSON or CSV. Resilient to DOM changes via semantic LLM extraction.
Project description
spectus
AI-assisted web data extractor. Paste a URL, describe what you want in plain English, get structured JSON or CSV. Resilient to DOM changes — falls back to semantic LLM extraction over a facts bundle (structured data + visible text + anchors + label-value pairs) when CSS selectors fail.
$ spectus extract https://news.ycombinator.com/ "Extract top stories: title, points, author, comments_count, story_url" --output csv
title,points,author,comments_count,story_url
Mercurial, 20 years and counting,70,ibobev,3,https://fosdem.org/...
...
Install — pick one
1. Docker (any OS)
docker compose up -d --build
Server on http://localhost:8000. Volume spectus-data persists DB + artifacts.
One-shot extract via the image:
docker run --rm --env-file .env -v spectus-data:/data spectus:latest \
spectus extract https://example.com/products "extract title, price, link"
2. pip / uv (Python 3.12+ on Win/Linux/Mac)
pip install spectus # or: uv tool install spectus
spectus install-browsers # one-time playwright chromium download
spectus migrate # apply DB migrations
export OPENAI_API_KEY=sk-... # Windows: setx OPENAI_API_KEY sk-...
spectus serve # API on :8000
Or one-shot from the shell, no server:
spectus extract https://example.com "Extract titles and prices" --output csv > out.csv
3. From source
git clone <repo>
cd spectus
uv sync --extra dev --extra notebook
uv run playwright install chromium
uv run alembic upgrade head
cp .env.example .env # add your OPENAI_API_KEY
uv run spectus serve
Use in your codebase
Python — one-shot
from spectus import extract
result = extract(
url="https://example.com/products",
instruction="Extract each product: name, price, rating, link",
openai_api_key="sk-...", # optional; falls back to OPENAI_API_KEY env
max_records=50,
)
print(result["records"]) # list[dict]
print(result["diagnostics"]) # strategy, quality_score, tokens, ...
Python — reusable client (batched, faster)
from spectus import SyncClient
with SyncClient.open(openai_api_key="sk-...") as client:
r1 = client.extract(url1, "extract X, Y, Z")
r2 = client.extract(url2, "another instruction")
Python — async (FastAPI / aiohttp / asyncio)
from spectus import Client
client = await Client.create(openai_api_key="sk-...")
result = await client.extract(url, instruction)
await client.close()
Any language — HTTP API
curl -s http://localhost:8000/api/extractions \
-H 'content-type: application/json' \
-d '{"url":"https://example.com","instruction":"extract titles and prices"}' \
| jq '.records'
Jupyter notebook
make notebook # opens notebooks/personal.ipynb in JupyterLab
CLI reference
spectus serve [--host H] [--port P] [--reload]
spectus extract URL "instruction" [--browser auto|force|never] [--max-records N] [--output table|json|csv]
spectus templates [--status candidate|active|needs_review|deprecated] [--output table|json]
spectus migrate
spectus install-browsers
spectus version
HTTP API
| Method | Path | Purpose |
|---|---|---|
POST |
/api/extractions |
Run extraction (sync, deadline from settings) |
GET |
/api/extractions/{id} |
Fetch prior result |
GET |
/api/extractions/{id}/export.csv |
CSV download |
GET |
/api/templates |
List saved templates |
GET |
/api/templates/{id} |
Get specific template |
GET |
/health |
Liveness probe |
GET |
/metrics |
Counters + p50/p95/p99 histograms |
OpenAPI spec at /docs (Swagger UI) and /redoc.
Request
{
"url": "https://example.com/products",
"instruction": "Extract title, price, rating, and product URL",
"output_format": "json",
"options": {
"use_browser": "auto",
"max_records": 100,
"save_template": true
}
}
Response
{
"job_id": "...",
"status": "success",
"url": "...",
"instruction": "...",
"records": [...],
"diagnostics": {
"strategy_used": "semantic_extraction",
"page_type": "product_listing",
"static_or_browser": "static",
"records_found": 24,
"quality_score": 0.87,
"repair_attempts": 1,
"template_used": false,
"runtime_ms": 13125,
"llm_calls": 3,
"llm_tokens_in": 4865,
"llm_tokens_out": 749,
"warnings": []
}
}
Architecture
POST /api/extractions
-> URL normalize + SSRF + robots + rate-limit (≤ 200 ms)
-> parallel(intent_LLM, static_fetch + analyze)
-> template lookup → on hit, execute + validate → return (<1s warm path)
-> static-sufficient? → planner_LLM → executor → validator
else browser_render → re-analyze → planner → executor → validator
-> repair loop (≤2) if quality_score < 0.80
-> resilience pass: build facts bundle → semantic LLM extraction →
per-field merge with type-aware tie-breakers
-> save winning strategy as template (candidate → active after 3 successes)
-> return JSON or CSV with diagnostics
Seven extraction strategies:
structured_data— JSON-LD / OpenGraph /__NEXT_DATA__/__NUXT__single_dom_selector— page-level CSSrepeated_dom_selector— repeating container CSStable_extraction— HTML tablesarticle_extraction— trafilaturavisible_text_regex— regex over visible textsemantic_extraction— LLM reads facts bundle (text + anchors + labels), no DOM dependency — survives redesigns
Stack: Python 3.12 · FastAPI · Pydantic v2 (strict) · SQLAlchemy 2.0 async · SQLite (swap to Postgres via DB_URL) · selectolax · Playwright · OpenAI Structured Outputs · structlog · trafilatura.
Configuration
All settings in .env (see .env.example). Key vars:
| Var | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
— | Required (or pass via openai_api_key= kwarg) |
OPENAI_MODEL_INTENT |
gpt-4o-mini |
Intent parser model |
OPENAI_MODEL_PLAN |
gpt-4.1 |
Planner + repair + semantic model |
DB_URL |
sqlite+aiosqlite:///./spectus.db |
Swap to postgresql+asyncpg://... for Postgres |
ARTIFACTS_DIR |
./artifacts |
Per-job debug bundles |
BROWSER_POOL_SIZE |
3 |
Playwright contexts |
RATE_LIMIT_RPS |
1.0 |
Per-domain token-bucket refill |
ALLOW_PRIVATE_TARGETS |
false |
Set true only for local fixture testing |
JOB_DEADLINE_SEC |
180 |
Hard wall-time per request |
GPT-5 / o-series support: pass OPENAI_MODEL_*=gpt-5-nano and bump LLM_*_TIMEOUT_SEC (reasoning tokens take longer). Client auto-uses max_completion_tokens + reasoning_effort=low for those models.
Compliance + safety (built-in)
- SSRF: blocks private / loopback / link-local / reserved IPs before fetch.
- Robots.txt: 1h-TTL cache, fail-open on 5xx.
- Per-domain rate-limit token bucket.
- Allowed selector attributes:
text,href,src,alt,title,class,id,value,data-*,aria-*. Anything else rejected at Pydantic boundary. - jQuery extensions (
:has(),:is(),:visible, etc.) rejected before reaching the parser.:contains('text')is translated server-side (lexbor CSS + text filter). - No CAPTCHA solve, no auth bypass, no anti-bot evasion. Out of scope by design.
Development
make dev # uvicorn --reload on :8000
make test # pytest -n auto with coverage
make lint # ruff
make typecheck # mypy strict
Suite: 52 unit tests, runs <1s offline. Plus @pytest.mark.browser for real Chromium.
License
MIT.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spectus-0.1.0.tar.gz.
File metadata
- Download URL: spectus-0.1.0.tar.gz
- Upload date:
- Size: 64.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8de59f2ca57f7bc381ec658ff5dfd5ee17e40377948abc08f6f2ff376c5ddc93
|
|
| MD5 |
13855cdb470a15441177de509080f03c
|
|
| BLAKE2b-256 |
92ca4dec8a096bafbb242bffffe65c7c17353c184abc1133742551e84e3597e8
|
File details
Details for the file spectus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: spectus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 77.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88e73de55e0ad20a8a1c49268af4d4fe8a057d4f7283475ac1ab45e52e96e1c0
|
|
| MD5 |
6515c798572f4ece48ac6470aa3622d7
|
|
| BLAKE2b-256 |
c2e3cff6d21bcf9000893db86a26806717a5bb2c86f613a1efba1ba22c56e03b
|