|
NEXUS is a self-contained, cross-domain web crawler built for generating AI training datasets at scale. It routes each URL through a 32-method API discovery chain before touching HTML. When it does scrape HTML, it uses a multi-stage extraction pipeline with adaptive scoring to keep the richest text. Every request ships with a 23-layer spoofing stack — TLS, HTTP/2, client hints, header ordering, timing jitter — so it can navigate around bot walls without a headless browser, proxy account, or paid API key. |
record = {
"id": "4191156ae84f0a2c",
"url": "https://en.wikipedia.org/...",
"title": "Online streamer - Wikipedia",
"quality": {"word_count": 659, "...": "..."},
"chunks": [{"text": "...", "tokens": 510}],
"entities": [{"type": "PERSON", "...": "..."}],
"keywords": [{"term": "streaming", "...": "..."}],
"summary": "Online streaming arose in...",
"kind": "html"
} |
|
32 API Detectors RFC well-known, OpenAPI, GraphQL, WSDL, source maps, CT logs, Wayback, APIs.guru |
23 Spoofing Layers TLS/JA3, GREASE, ALPS, HTTP/2 SETTINGS, pseudo-headers, header order, jitter |
Adaptive Extraction Trafilatura → justext → selectolax with scoring; fast path for clean pages |
MOS Escalation 22 policies, 23 keyless services, hedged race, per-host sticky preference |
|
Error Containment 20 error kinds, 6 recovery strategies, persistent loop, noise suppressor |
Folder Manager 10-tier data tree, atomic writes, cross-process locks, corrupt-file backup |
Mercator Frontier 8-band priority queues, per-host back queues, decay for starved items |
RAG-Ready Output Token-bounded chunks, entity extraction, summaries, readability scores |
graph TD
A[scraper.py<br/>entrypoint] --> B[Preloader<br/>20 warmup steps]
B --> C[crawler.py<br/>orchestrator]
C --> D[MercatorFrontier<br/>priority bands]
C --> E[Worker Pool<br/>8 processes]
E --> F[api_router.py<br/>32 detectors]
E --> G[se_api.py<br/>Stack Exchange]
E --> H[spoof.py<br/>23-layer]
H --> I[scraper.py<br/>fetch + extract]
F --> J[api_router cache]
G --> K[SE quota + breaker]
I --> L[extract_html/pdf/json/xml]
L --> M[NLP-lite<br/>keywords, entities, summary]
M --> N[chunker<br/>512-token windows]
N --> O[ShardedWriter<br/>NDJSON shards]
O --> P[.data/output/shards/]
C --> Q[FolderManager<br/>10 tiers]
C --> R[error_fast<br/>LoopManager]
R --> S[NoiseSuppressor]
Click to expand
Scraper/
├── scraper.py ~3000 lines fetch + extract + chunk
├── crawler.py ~2000 lines orchestration, workers, frontier
├── spoof.py ~800 lines 23-layer spoofing, SOD pool
├── api_router.py ~1900 lines 32-method API discovery
├── se_api.py ~1200 lines Stack Exchange API client
├── error_fast.py ~430 lines error classification + noise suppression
├── folder_manager.py ~470 lines 10-tier data tree
├── config.py ~1400 lines 300+ tunables
├── debug.py ~2300 lines 180-test harness
├── libraries.txt dependency list
├── expectederrors.txt 500 HTTP error categories
├── resultsexample.json reference output schema
├── CHANGELOG.md project history
├── USAGE.md usage guide
├── README.md this file
└── .data/ runtime tree
├── state/ crawl.state.json, queue.txt, done.txt, ...
├── db/ dedup.sqlite, bloom.bin, mos_cache.sqlite
├── cache/ domain_rates.json, mos_budget.json, ...
├── logs/ crawl.log
├── agent/ embeddings.bin, entities.jsonl
├── output/
│ ├── manifest.json
│ ├── shards/ shard-00000.ndjson, ...
│ └── quarantine/ empty.ndjson, low_quality.ndjson, ...
├── cookies/ per-domain cookie jars
└── tmp/ scratch
# 1. Clone
git clone https://github.com/YOUR_USERNAME/nexus-crawler.git
cd nexus-crawler
# 2. Install dependencies
pip install -r libraries.txt
pip install psutil
# 3. Verify preload
python scraper.py --preload-onlyDependency list
babel==2.18.0 # locale parsing
certifi # SSL certificates
cffi # C foreign function interface
charset-normalizer # encoding detection
click # CLI (optional)
cloudpickle # multiprocessing support
colorama # Windows terminal colors
courlan # URL cleaning and normalization
curl-cffi==0.16.3 # TLS/HTTP fingerprint impersonation
dateparser # multilingual date parsing
defusedxml # safe XML parsing
htmldate # article date extraction
joblib # parallel batch processing
justext # boilerplate removal
lxml # XML/HTML tree processing
lxml-html-clean # HTML sanitization
nltk # stopwords only
orjson # 10-14x faster JSON
pip # package manager
psutil # process memory monitoring
pycparser # cffi dependency
pymupdf # PDF text + OCR
pyphen # hyphenation
python-dateutil # date manipulation
pytz # timezone database
regex # Unicode property regex
selectolax # fastest HTML parser
setuptools # build tools
six # Python 2/3 compat
textstat # readability metrics
tld # top-level domain extraction
tqdm # progress bars (optional)
trafilatura # article extraction
tzdata # IANA timezone data
tzlocal # local timezone
urllib3 # HTTP client
python scraper.py --link "https://en.wikipedia.org/wiki/Online_streamer" --depth 2 --workers 4| Flag | Default | Description |
|---|---|---|
--link URL |
— | Seed URL (repeatable for multi-seed) |
--sitemap URL |
— | Fetch sitemap.xml and use all <loc> entries |
--depth N |
4 | Max crawl depth from seed |
--workers N |
CPU count | Worker processes |
--queue-cap N |
500 | Frontier size cap |
--fast |
— | Max concurrency, no jitter |
--inf |
— | Infinite mode (never auto-stops) |
--antiblock |
— | Retry blocked URLs via blocked queue |
--verify |
— | Run verification on each record |
--enrich |
— | Run enrichment on each record |
--clean TIER |
— | Wipe a data tier before starting |
--dry-run |
— | With --clean, show without removing |
--preload-only |
— | Run preloader and exit |
--api URL |
— | Fetch a JSON endpoint directly |
Single domain, moderate depth
python scraper.py --link "https://en.wikipedia.org/wiki/Online_streamer" --depth 3 --workers 8Multi-seed mixed sites
python scraper.py \
--link "https://stackoverflow.com/questions/4260280" \
--link "https://en.wikipedia.org/wiki/HTTP" \
--link "https://news.ycombinator.com/" \
--depth 1 --workers 6 --queue-cap 5000Fresh start every time
python scraper.py --link "https://site.com/" --clean all --depth 2 --workers 4Sitemap-driven crawl
python scraper.py --sitemap "https://site.com/sitemap.xml" --depth 1 --workers 8API mode (fetch raw JSON)
python scraper.py --api "https://api.github.com/repos/torvalds/linux" --out linux.jsonFull debug suite
python debug.py --log-file .data/logs/debug.jsonRun system tests only (fast)
python debug.py --categories T0 T1 T2 T3 T4 T5 T5b T5c T5d --no-console-logsEach record is emitted as a single JSON line. The structure is shown below.
Full record schema
{
"id": "4191156ae84f0a2cb14cad78c2ce47f8",
"url": "https://en.wikipedia.org/wiki/Online_streamer",
"canonical_url": "https://en.wikipedia.org/wiki/Online_streamer",
"domain": "en.wikipedia.org",
"tld": "org",
"title": "Online streamer - Wikipedia",
"language": "en",
"published_date": "2018-12-20",
"updated_date": null,
"crawled_at": 1789219800.123,
"content_hash": "4191156ae84f0a2cb14cad78c2ce47f8",
"simhash": 2872093965255146031,
"source_signature": "6b51e6036f048846",
"idempotency_key": "974162d418a576fd37f1a0fdade027be",
"extractor_version": "4.0.0",
"config_hash": "662f354bdd475186a126827c0adeecc5",
"license": null,
"quality": {
"word_count": 659,
"sentence_count": 35,
"syllable_count": 1143,
"char_count": 4578,
"reading_time_seconds": 197.7,
"flesch_reading_ease": 40.99,
"flesch_kincaid_grade": 12.22,
"gunning_fog": 14.45,
"smog_index": 13.71,
"automated_readability_index": 15.87,
"coleman_liau_index": 15.71,
"dale_chall_score": 11.76,
"text_standard": "11th and 12th grade",
"difficulty": 188
},
"word_count": 659,
"token_count": 907,
"char_count": 4578,
"sentence_count": 35,
"topics": [],
"entities": [
{"type": "MONEY", "value": "$10,000", "weight": 1.0},
{"type": "PERSON", "value": "South Korea", "weight": 0.4}
],
"keywords": [
{"term": "streaming", "score": 0.035885},
{"term": "live streaming", "score": 0.015401}
],
"summary": "Online streaming arose in the mid-to-late 2000s...",
"sections": [
{"heading": "History", "text": "...", "subsections": []}
],
"chunks": [
{
"index": 0,
"total": 2,
"text": "...",
"token_count": 510,
"heading_context": "Contents",
"parent_url": "https://en.wikipedia.org/wiki/Online_streamer",
"parent_title": "Online streamer - Wikipedia",
"offset": null
}
],
"links": ["https://en.wikipedia.org/wiki/Main_Page"],
"metadata": {"source_type": "html", "language": "en"},
"provenance": {"service": "direct"},
"content": {
"site_navigation": {"primary": [], "help": [], "actions": []},
"announcement": null,
"table_of_contents": [],
"article": {
"heading": "Online streamer",
"intro": "...",
"sections": [],
"references": [],
"categories": []
},
"metadata": {},
"footer_links": []
},
"schema_version": "4.0.0",
"kind": "html"
}.data/
├── state/ # resume state
│ ├── crawl.state.json
│ ├── queue.txt
│ ├── done.txt
│ ├── errors.txt
│ ├── blocked.txt
│ ├── journal.ndjson
│ └── crawl.pid
├── db/ # SQLite + Bloom
│ ├── dedup.sqlite
│ ├── bloom.bin
│ ├── mos_cache.sqlite
│ └── extraction.sqlite
├── cache/ # JSON caches and budgets
│ ├── domain_rates.json
│ ├── mos_budget.json
│ ├── mos_host_prefs.json
│ ├── se_quota.json
│ └── error_fast.json
├── logs/
│ └── crawl.log
├── output/
│ ├── manifest.json
│ ├── shards/
│ │ └── shard-00000.ndjson
│ └── quarantine/
│ ├── empty.ndjson
│ ├── low_quality.ndjson
│ └── js_required.ndjson
└── cookies/ # per-domain cookie jars
|
32 API detection methods |
23 spoofing layers |
180+ tests passing |
87% success rate on easy tier |
| Metric | Value |
|---|---|
| Preload time | 0.72s |
| Full debug suite | 6.7s |
| Concurrent test suite | 11.5s |
| Wikipedia crawl (depth 2, 4 workers) | ~45s, 60-100 records |
| Mixed crawl (3 seeds, depth 1, 6 workers) | ~48s, 22 records |
| Logger throughput | 147,584 msg/s |
| Trap detection | 34,466 URLs/s |
| SimHash (100k tokens) | 1.52s |
| Memory per worker | 60-90 MB |
T6–T26 live URL tiers
| Tier | Category | URLs | Success |
|---|---|---|---|
| T6 | Easy | 10 | 60%+ |
| T7 | Moderate | 10 | 100% |
| T8 | Dynamic | 10 | 92% |
| T9 | JS-heavy | 8 | 62% |
| T10 | Protected | 4 | 50% |
| T13 | Docs | 8 | 100% |
| T14 | Blogs | 8 | 100% |
| T15 | News | 7 | 86% |
| T17 | Academic | 6 | 100% |
| T18 | Gov/Edu | 7 | 86% |
| T26 | SE network | 6 | 83% |
# Full suite
python debug.py
# Categories
python debug.py --categories T0 T1 T2 T3 T4 T5
# By name
python debug.py --name "MOS classifier"
# List all categories
python debug.py --list
# Structured log
python debug.py --log-file .data/logs/debug.jsonAll 20 categories with counts
| Category | Count | Covers |
|---|---|---|
| T0 | 3 | Environment, cleanup, folder manager |
| T1 | 7 | Module imports |
| T2 | 15 | Config invariants |
| T3 | 10 | Spoof engine, SOD pool |
| T4 | 17 | Crawler internals |
| T5 | 25 | Scraper internals |
| T5b | 11 | Folder manager |
| T5c | 5 | SE API |
| T5d | 4 | API router |
| T6_easy | 10 | Easy URLs |
| T7_moderate | 10 | Wikipedia, HN, docs |
| T8_dynamic | 10 | GitHub, news sites |
| T9_js_heavy | 8 | Social media |
| T10_protected | 4 | Turnstile sites |
| T13_docs | 8 | Technical docs |
| T14_blogs | 8 | Personal + company blogs |
| T15_news | 7 | International news |
| T17_academic | 6 | Papers + journals |
| T18_gov_edu | 7 | Government + university |
| T26_se_network | 6 | Stack Exchange network |
| T41 | 3 | Concurrency |
| T42 | 5 | Stress |
| T43 | 7 | Regression |
Edit config.py directly. All 300+ tunables are grouped by feature area.
| Area | Key examples |
|---|---|
| Crawling | MAX_WORKERS, MAX_DEPTH, MAX_QUEUE_SIZE, DOMAIN_TOKEN_BUCKET_RATE |
| Spoofing | TLS_GREASE, TLS_PERMUTE_EXTENSIONS, SOD_WORKER_POOL_SIZE |
| Blocked / antiblack | BLOCKED_RETRY_MIN/MAX, MAX_BLOCKED_RETRIES, BLOCKED_STATUS_CODES |
| Extraction | OCR_MIN_TEXT_LEN, QUALITY_MIN_WORDS, CONTENT_MAX_CHARS |
| Chunking | AGENT_CHUNK_SIZE_TOKENS, AGENT_CHUNK_OVERLAP_TOKENS |
| MOS | MOS_ENABLED, MOS_DAILY_BUDGET, MOS_FIRST_HOSTS |
ModuleNotFoundError
pip install -r libraries.txt
pip install psutilEvent loop is closed flood
This should not appear. error_fast.NoiseSuppressor silences it. If it does:
python error_fast.py --testCrawl never finishes
Check queue_dropped in .data/state/crawl.state.json. Raise MAX_QUEUE_SIZE if it is high.
High memory
Lower MAX_RSS_MB in config.py. The monitor kills workers that exceed it.
Sites consistently blocked
Add to MOS_FIRST_HOSTS in config.py:
MOS_FIRST_HOSTS = (..., "yourhost.com",)SE quota exhausted
python -c "from se_api import se_quota_status; print(se_quota_status())"Wait for UTC midnight reset or register a free Stack Apps key.
- Keyless by default — every core path works without accounts, API keys, or paid services.
- API-first — check for a public API before touching HTML.
- Polite by design — per-domain token buckets, health scoring, and circuit breakers.
- Self-contained — no Docker, no browsers, no external orchestration.
- Adaptive — extraction scoring, hardware profiling, and worker tuning based on observation.
- Observable — structured NDJSON output, JSON logs, and per-tier manifests.
| Status | Items |
|---|---|
| ✅ Shipped | 32-method API discovery, 23-layer spoofing stack, MOS escalation, Stack Exchange API client, folder manager with 10 tiers, error classification, Mercator frontier, host health scoring, domain bootstrap, RAG-ready chunking, 180+ test harness |
| 🚧 Planned | Adaptive concurrency, asyncio.get_event_loop_policy() deprecation fix, per-site extractors, vector embedding attachment, semantic dedup via local embedding model, Wayback SPN2 pre-flight |
- Headless browsers (Playwright, Selenium, Puppeteer)
- Account-based services (Cloudflare Workers, ScrapingBee)
- Docker / containerization
- Turnstile/CAPTCHA solving (no keyless bypass exists)
Contributions are welcome. Before submitting:
- Run
python debug.py --categories T0 T1 T2 T3 T4 T5and confirm it is 100% pass. - Run
python debug.py --categories T41 T42 T43and confirm it is 100% pass. - Run
python scraper.py --preload-onlyand confirm it reportsfailed=0. - Run
python error_fast.py --testand confirm it reportsall checks passed.
Add new tests in the appropriate debug.py category for new functionality.
MIT. See LICENSE for details.
Built by PurpleXPurple.
Powered by:
curl-cffiselectolaxtrafilaturapymupdforjson
⭐ Star this repo if it saves you a scraper
