This project is a high-performance web scraper designed to target Sweden's largest classifieds portal, Blocket.se, and execute completely remotely on your head node server over your secure Tailscale mesh network!
blocket-webscraper/
├── .gitignore # Ignores local DuckDB DBs, test logs, and virtual environments
├── README.md # Setup and execution instructions
├── requirements.txt # Packages: requests, beautifulsoup4, playwright, pydantic, pyyaml
├── run_remote.sh # 🚀 Pipeline Runner (syncs, runs remotely, downloads results)
├── main.py # Main orchestrator entry point
│
├── config/
│ └── scraper_config.yaml # Search keywords, categories, throttles, and db settings
│
└── src/
├── parser.py # Extracts Next.js __NEXT_DATA__ JSON (CSS bypass!)
├── scraper.py # Handles static HTTP loads and dynamic Playwright drivers
└── storage.py # Handles DuckDB deduplicated storage pipelines
The sequence diagram below visualizes how the scraper handles the sharded categories list, paginates dynamically, bypasses pagination caps, performs JIT details parsing, records price changes, and triggers deactivation scans:
sequenceDiagram
autonumber
participant Main as main.py (Orchestrator)
participant Config as scraper_config.yaml
participant Scraper as src/scraper.py (Fetcher)
participant Parser as src/parser.py (Parser)
participant DB as src/storage.py (DuckDB)
participant Blocket as Blocket.se (Search Engine)
Main->>Config: 1. Read Target Shards & Settings
Config-->>Main: Return 20 Type/Price Shards
loop For Each Category Shard (e.g. Touring Budget, Sport)
Note over Main: Initialize page = 1 & empty Scraped_IDs list
loop Page-by-Page Pagination
Main->>Scraper: 2. Request Page Content (URL, page)
Scraper->>Blocket: 3. Fetch HTML (urllib or Playwright)
Blocket-->>Scraper: Return HTML Webpage
Scraper-->>Main: Return HTML String
Main->>Parser: 4. Extract Items (HTML)
Parser->>Parser: Try __NEXT_DATA__ JSON (Fallback to BS4)
Parser-->>Main: Return List of parsed BlocketItems
alt 0 items returned OR duplicate page wrap-around detected
Note over Main: Break Pagination Loop
else New items found
Note over Main: Append new IDs to Scraped_IDs list
Main->>DB: 5. save_listings(items)
DB->>DB: Upsert blocket_listings & motorcycle_details
DB->>DB: Record price_history changes
DB-->>Main: Return number of items saved
Note over Main: Sleep for politeness delay (delay_seconds)
Note over Main: Increment page by 1
end
end
Main->>DB: 6. detect_removals_sweep(Scraped_IDs)
DB->>DB: Flag missing items as is_active = FALSE
DB-->>Main: Sweep Complete
end
Note over Main: Crawl Cycle Complete, Enter Cooldown Sleep
Run a single command on your Command Center:
bash run_remote.shWhat this script automates:
- Syncs Code: Uses
rsyncto sync your local scraper codebase directly to your remote server (101010_remote), ignoring database files or virtualenvs. - Prepares Host: SSHs in, auto-creates a remote virtual environment, and resolves all pip requirements on the server.
- Runs Scraper: Triggers
python3 main.pyon your remote server to run the scraper in the background (avoiding bandwidth or memory consumption on your local machine). - Retrieves Data: Downloads the updated DuckDB database
scraped_listings.duckdbdirectly back to your localdata/folder for analysis.
To test things locally before sending them to the server:
- Initialize a virtual environment & install requirements:
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt - Trigger the run:
(Options:
python3 main.py
--browserforces headless Chromium via Playwright, default is static HTTP)