Car.com Scraper
A production-grade, real-time web scraper for Cars.com with adaptive partitioning, live dashboard, and bypass-friendly browser automation.
About the Project
ð Cars.com Scraper Pro
A production-grade, real-time web scraper for Cars.com with adaptive partitioning, live dashboard, and bypass-friendly browser automation.
A modern, professional scraping engine that connects directly to your own Chrome via the Chrome DevTools Protocol (CDP), automatically partitions massive search result sets, and streams every record live to a glass-morphic dashboard.
ð Table of Contents
- ⨠Key Features
- ðï¸ Architecture
- ð§° Tech Stack
- ð¸ Dashboard Preview
- â¡ Quick Start
- ð§ Installation
- ð Running the Application
- ð§ How It Works
- âï¸ Configuration
- ð Project Structure
- ðï¸ Database Schema
- ð API Reference
- ð¤ Data Export
- â° Scheduled Scraping
- ð¡ï¸ Troubleshooting
- ð¤ Contributing
- ð License
- ð Acknowledgments
⨠Key Features
| Feature | Description |
|---|---|
| ð CDP-Powered Scraping | Connects to your own Chrome browser over the Chrome DevTools Protocol on port 9222 â no headless fingerprinting, dramatically lower detection rates. |
| 𧩠Adaptive Partitioning | Intelligently splits massive search results (year à make à model à door à cylinder) and runs partitions in parallel for sub-hour runs on 100k+ listings. |
| â¡ Two Fetch Engines | API Mode uses browser-context fetch() for detail pages (â 10Ã faster). Traditional Mode navigates separate tabs for maximum compatibility. |
| ð Smart Deduplication | Merges with existing records, tracks price history across runs, and never re-fetches fully processed listings. |
| ð Live Dashboard | Glassmorphic, real-time UI powered by Socket.IO â listings stream in as they're scraped, with run statistics, filters, and export. |
| â° Built-in Scheduler | Auto-scrape every N hours with live countdown. Set-and-forget data collection. |
| ð¤ Excel + JSON Export | One-click export of filtered or complete datasets with clean column formatting and control-character sanitization. |
| ð¡ï¸ Captcha-Aware | Detects Cloudflare challenges, waits for human resolution, and resumes automatically once cleared. |
| 𧱠Resource Blocking | Drops images, fonts, media, and tracker scripts for ⥠3à speedup on listings pages. |
| 𧪠Quality Filters | Sanitizes cylinders/doors to reject garbage (e.g. UNIX timestamps) inherited from legacy data. |
| ð¾ Crash-Safe Storage | SQLite with WAL journal mode, batched commits every 50 inserts or 1 second. |
| ð Legacy Migration | Auto-migrates the original cars_cumulative.json into SQLite on first run. |
ðï¸ Architecture
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Dashboard (SPA) â
â templates/dashboard.html ⢠Socket.IO Client â
âââââââââââââââââââââââ¬âââââââââââââââââââââââââââââââââââ
â WebSocket (status, new_car)
â¼
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Flask + Flask-SocketIO Server â
â app.py â
â ââââââââââââââ ââââââââââââââââ ââââââââââââââââ ââââââââââââââââââââ â
â â REST API â â Scheduler â â Chrome Mgr â â Message Broker â â
â â /api/... â â (daemon) â â (CDP 9222) â â queue.Queue â â
â âââââââ¬âââââââ ââââââââ¬ââââââââ ââââââââ¬ââââââââ ââââââââââ¬ââââââââââ â
â ââââââââââââââââââ´ââââââââââââââââââ´ââââââââââââââââââââ â
â â â
â â¼ â
â ââââââââââââââââââââââââââââââââ â
â â CarsScraper Class â â
â â scraper.py â â
â â ⢠Playwright async engine â â
â â ⢠Partition planner â â
â â ⢠Detail-page fetcher â â
â â ⢠Cloudflare detector â â
â ââââââââââââ¬ââââââââââââââââââââ â
â â CDP connect_over_cdp â
â â¼ â
â ââââââââââââââââââââââââââââââââââââââââ â
â â Google Chrome (User's Browser) â â
â â --remote-debugging-port=9222 â â
â â cars.com/shopping/results/... â â
â ââââââââââââââââââââââââââââââââââââââââ â
â â â
â â¼ â
â ââââââââââââââââââââââââââââââââ â
â â SQLite cars.db â â
â â db.py (WAL mode) â â
â ââââââââââââââââââââââââââââââââ â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ð§° Tech Stack
Backend
- ð Python 3.10+ â Core language
- ð¶ï¸ Flask 3.0 â HTTP server
- â¡ Flask-SocketIO 5.3 â Real-time bidirectional events
- ð Playwright (async) â Browser automation over CDP
- ðï¸ SQLite (WAL) â Embedded persistence
- ð¼ pandas + openpyxl â Excel export
Frontend
- ð ±ï¸ Bootstrap 5.3 â Layout
- ð¨ Bootstrap Icons â Iconography
- ð¤ Inter & Outfit â Premium typography
- ⨠Vanilla CSS â Custom glassmorphism design system
Browser
- ð¢ Google Chrome / Chromium â Headful CDP target
ð¸ Dashboard Preview
The dashboard is a single-page, real-time interface with a premium dark theme:
- ð¯ Interactive 5-Step Stepper â Visualizes the workflow (Launch Chrome â Open Link â Connect â Scrape â Export)
- ð Live Stat Cards â Total cars, current run, scheduled next run, average scrape time
- ð Instant Search â Filter by make, year, title, or VIN with debounced live results
- ð Sortable Pagination â Server-side pagination with sortable columns
- ðª Detail Modal â Full vehicle history, dealer info, price history timeline
- â±ï¸ Timing History â Last 50 scrape runs with per-listing averages
- ð Active Partition Tracker â See which (year, make, model) is being scraped right now
â¡ Quick Start
Prerequisites: Python 3.10+, Google Chrome installed.
# 1. Clone the repo git clone https://github.com/RasikhAli/Car.com-Scraper.git cd Car.com-Scraper # 2. Create a virtual environment python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate # 3. Install dependencies pip install -r requirements.txt playwright install chromium # 4. Run the server python app.py
Now open http://localhost:5001, click Ready Chrome Debugger, paste the URL into the auto-launched Chrome, and press Start Scraping. ð
ð§ Installation
1. System Requirements
| Component | Minimum | Recommended | |---|---|---| | Python | 3.10 | 3.11+ | | RAM | 2 GB | 4 GB+ | | Chrome | 110+ | Latest stable | | Disk | 500 MB | 2 GB (for SQLite growth) | | OS | Windows 10 / macOS 12 / Ubuntu 20.04 | Latest LTS |
2. Install Python Packages
pip install -r requirements.txt
The requirements.txt includes:
Flask==3.0.0
Flask-SocketIO==5.3.4
python-socketio==5.10.0
python-engineio>=4.8.0
selenium==4.18.0
requests==2.31.0
pandas==2.2.0
openpyxl==3.1.2
undetected-chromedriver==3.4.6
opencv-python==4.9.0.80
pyautogui==0.9.54
numpy==1.26.4
playwright
3. Install Playwright Browsers
playwright install chromium
ð¡ On Linux/VPS you may also need:
playwright install-deps
4. (Optional) Manually Launch Chrome in Debug Mode
If you prefer to use your existing Chrome profile:
Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
Linux
google-chrome --remote-debugging-port=9222 --no-sandbox
ð Running the Application
python app.py
The server binds to 0.0.0.0:5001. Visit:
- Dashboard: http://localhost:5001
- Status JSON: http://localhost:5001/api/status
- Cars JSON: http://localhost:5001/api/cars
First-Run Behavior
If cars.db is empty, the app automatically migrates any legacy cars_cumulative.json file in the project root.
ð§ How It Works
The scraper follows an elegant 5-step pipeline that mirrors the on-screen stepper:
-
ð Launch Chrome â App spawns a fresh, isolated Chrome instance with
--remote-debugging-port=9222. Uses an isolated user-data-dir (%TEMP%/cars_scraper_profile) so it never collides with your normal profile. -
ð Navigate to Filter URL â A clean
https://www.cars.com/shopping/results/?...URL is built from your filter selections (year, makes, zip, distance, sort). -
ð Connect via CDP â
playwright.chromium.connect_over_cdp(...)attaches to the existing tab â no headless detection, no new browser instance. -
ð§® Adaptive Partitioning â The engine checks the page count for each
(year, make)combination. If ⤠100 pages, it's scraped as one partition. Otherwise, it splits by model, then by door count à cylinder count, running partitions in parallel (default batch = 4). -
ð¡ Stream to Dashboard â Each scraped listing is pushed through
queue.Queueâ SQLite (batched commit) â Socket.IO â live UI insert. No page reloads. No polling.
Scraping Modes
| Mode | Behavior | Best For | |---|---|---| | ð Load All First (default) | Phase 1: Collect every unique listing URL. Phase 2: Parallel detail fetches. Zero duplicates, faster overall. | Large datasets, fresh runs | | ð Scrape As You Go | Streams listings page-by-page in real time. | Watching the run live, partial-stop resumes |
Fetch Methods
| Method | Description | Speed |
|---|---|---|
| â¡ API Mode | Uses browser-context fetch() to hit cars.com's internal detail endpoint directly. | ~10Ã faster |
| ð Traditional | Opens each listing in a separate Playwright tab. | Compatible with all layouts |
âï¸ Configuration
All configuration is driven by Python constants in app.py and runtime filters in the dashboard.
Default Filters (app.py)
filters = { 'year_min': '2021', 'makes': ['BMW', 'Land Rover', 'Mercedes-Benz'], 'zip': '60606', 'maximum_distance': '9999', 'fetch_method': 'api', 'scraping_mode': 'scrape_as_you_go', # or 'load_all_first' 'batch_size': 4, # concurrent partitions }
Scheduling (app.py)
scheduling = { 'enabled': False, 'interval_seconds': 43200 # 12 hours }
You can change these at runtime via the dashboard's Auto-Scrape panel.
CDP Port
Default: 9222. Change CDP_PORT in app.py if you need a different port.
ð Project Structure
Car.com-Scrapper/
â
âââ app.py # Flask + SocketIO server, REST API, scheduler
âââ scraper.py # CarsScraper class (Playwright async, partitioning)
âââ db.py # SQLite schema, migrations, CRUD, exports
âââ requirements.txt # Python dependencies
âââ .gitignore # Git ignore rules
âââ README.md # â you are here
â
âââ templates/
â âââ dashboard.html # Single-page glassmorphic dashboard (Bootstrap 5)
â
âââ static/ # (reserved for future static assets)
â
âââ cars.db # SQLite database (auto-generated, gitignored)
âââ cars_cumulative.json # Legacy data file (auto-migrated on first run)
â
âââ cars_export.json # Generated on demand via /api/export?format=json
âââ cars_export.xlsx # Generated on demand via /api/export?format=excel
ðï¸ Database Schema
cars â Main vehicle table (50+ columns)
| Column | Type | Description |
|---|---|---|
| listing_id | TEXT PK | Primary key from cars.com (or composite fallback) |
| title | TEXT | e.g. "2022 BMW X5 xDrive40i" |
| price | TEXT | Current dealer price |
| link | TEXT | Direct URL to the listing |
| page | INTEGER | Result page it was discovered on |
| vin | TEXT | Vehicle Identification Number |
| mileage | TEXT | Odometer reading |
| msrp | TEXT | Manufacturer's Suggested Retail Price |
| exterior_color / interior_color | TEXT | â |
| features | TEXT (JSON) | Array of feature strings |
| description | TEXT | Dealer description |
| year, make, model, trim | TEXT | â |
| body_style, fuel_type, stock_type, condition | TEXT | â |
| cpo | INTEGER (0/1) | Certified Pre-Owned flag |
| seller_zip, seller_customer_id | TEXT | â |
| trid, delivery_type, financing_type, isa_context | TEXT | Internal tracking fields |
| primary_thumbnail | TEXT | Image URL |
| search_score, ship_price | TEXT | â |
| metadata | TEXT (JSON) | Raw metadata blob |
| monthly_payment, monthly_label | TEXT | Financing estimate |
| seller_notes | TEXT | â |
| dealer_info | TEXT (JSON) | {name, rating, address, phone, ...} |
| drivetrain, transmission, engine, cylinders, doors | TEXT | â |
| vehicle_history | TEXT (JSON) | Accident count, owner count, etc. |
| deal_rating | TEXT | Great / Good / Fair / etc. |
| location, dealership_link | TEXT | â |
| price_history | TEXT (JSON) | [{price, date}, ...] |
| scraped_at | INTEGER | Unix timestamp of last update |
scrape_runs â Run history (audit + analytics)
| Column | Type | Description |
|---|---|---|
| id | INTEGER PK AUTOINCREMENT | â |
| pages_scan_start / pages_scan_end | INTEGER | Unix timestamps |
| scrape_start / scrape_end | INTEGER | Unix timestamps |
| avg_scrape_time_per_listing | REAL | Seconds per listing |
| listings_scraped | INTEGER | Total listings processed |
| mode | TEXT | load_all_first or scrape_as_you_go |
| created_at | INTEGER | Unix timestamp |
Indexes & Optimizations
PRAGMA journal_mode = WALâ Concurrent readers + single writer30-second connection timeoutâ Survives long scrapesBatched commitsâ Every 50 inserts or 1 secondAutomatic rollbackâ On insert failure, the connection is recycled- Allowed sort columns (whitelist) â
scraped_at,price,mileage,year,make,model,monthly_payment(SQL-injection-safe)
Legacy Migration
On first run, if cars.db is empty, the app auto-migrates the legacy cars_cumulative.json file:
def init_db(legacy_json_path=None): # ... creates schema ... if count == 0 and legacy_json_path and os.path.exists(legacy_json_path): with open(legacy_json_path, 'r') as f: data = json.load(f) for car in data.get('results', []): insert_or_update_car(conn, car)
ð API Reference
REST Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | / | Dashboard SPA |
| GET | /api/status | Current scraper state, filters, schedule |
| GET | /api/cars?page=&limit=&search=&make=&year=&sort_by=&sort_order= | Paginated car list |
| POST | /api/launch_chrome | Launch debug-Chrome and return the target URL |
| POST | /api/delete_car | Delete one car (by listing_id) |
| POST | /api/delete_by_make | Delete all cars of a make |
| POST | /api/delete_by_year | Delete all cars of a year |
| POST | /api/delete_all | Wipe the entire database |
| GET | /api/export?format=json\|excel&filter_type=&make=&year= | Download filtered export |
| GET | /api/timing_history | Current + historical run timings |
| GET / POST | /api/timer_config | Get / set auto-schedule interval |
| GET | /favicon.ico | Empty 204 |
WebSocket Events (Socket.IO)
| Event | Direction | Payload |
|---|---|---|
| connect | client â server | (none) â server emits initial status_update |
| start_scraping | client â server | {scraping_mode, fetch_method, batch_size} |
| stop_scraping | client â server | (none) |
| update_filters | client â server | {year_min, makes, ...} |
| toggle_scheduling | client â server | {enabled, interval_hours} |
| status_update | server â client | Full status payload (auto-emitted on changes) |
| new_car | server â client | Single car record |
| scrape_reset | server â client | Emitted at start of each run |
| scrape_complete | server â client | {status, count} |
| scrape_stopped | server â client | {status} |
| scrape_error | server â client | {error} |
| filters_updated | server â client | New filter dict |
Example: Fetch Latest 50 Cars
curl 'http://localhost:5001/api/cars?page=1&limit=50&sort_by=scraped_at&sort_order=desc'
Example: Export All BMWs to Excel
curl -o bmw_export.xlsx 'http://localhost:5001/api/export?format=excel&filter_type=make&make=BMW'
Example: Start a Scrape via WebSocket
import socketio sio = socketio.Client() sio.connect('http://localhost:5001') sio.emit('start_scraping', {'scraping_mode': 'load_all_first', 'batch_size': 8})
ð¤ Data Export
The app generates on-demand exports in two formats:
JSON Export
GET /api/export?format=json[&filter_type=all|make|year|make_year&make=BMW&year=2023]
Returns a JSON file with all columns, properly deserialized JSON fields (features, dealer_info, price_history, etc.).
Excel Export (.xlsx)
GET /api/export?format=excel[&filter_type=...&make=...&year=...]
Generates a clean .xlsx with:
- â All 50+ columns flattened
- â JSON fields serialized to strings (Excel-readable)
- â Control characters stripped (no file corruption)
- â Cell text length-capped at 32,700 chars per cell
- â
Trailing
... [TRUNCATED]marker on overflow
Filter Types
| filter_type | Required Params | Effect |
|---|---|---|
| all | (none) | Export every car |
| make | make=BMW | Export only BMW |
| year | year=2023 | Export only 2023 |
| make_year | make=BMW&year=2023 | Export only 2023 BMWs |
â° Scheduled Scraping
Enable Auto-Scrape from the dashboard's "Auto-Scrape" panel.
Defaults
- Interval: 12 hours (43,200 seconds)
- Configurable range: 1 second to 168 hours (1 week)
- Persistence: Interval value lives in
app.py; toggle state is in-memory
Programmatic Control
# In app.py scheduling = { 'enabled': False, 'interval_seconds': 43200 # 12 hours }
# Via WebSocket sio.emit('toggle_scheduling', {'enabled': True, 'interval_hours': 6})
# Via REST curl -X POST http://localhost:5001/api/timer_config \ -H "Content-Type: application/json" \ -d '{"interval_hours": 6}'
Behavior
- â Live countdown displayed in dashboard header
- â New run automatically starts on schedule
- â
Scheduler uses a daemon thread +
threading.Event(no busy-loop) - â Skips run if a scrape is already in progress
- â Browser tab is kept open between scheduled runs
ð¡ï¸ Troubleshooting
â "Could not connect to Chrome on port 9222"
- Make sure all Chrome windows are closed
- Click Ready Chrome Debugger in the dashboard
- If Chrome still won't connect, manually launch:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 - Verify it's listening: visit
http://localhost:9222/json/versionin any browser
ð Stuck on "Awaiting connection"
- The browser is launched but the dashboard hasn't detected the CDP port. Wait 2â5 seconds.
- The dashboard polls every 2 seconds for up to 15 seconds before timing out.
- Try clicking Ready Chrome Debugger again.
âï¸ Cloudflare "Just a moment..." challenge
The scraper detects the challenge and pauses. Just solve the CAPTCHA in the Chrome window â scraping resumes automatically within 2 seconds.
ðï¸ "database is locked"
- Ensure no other process has
cars.dbopen (e.g., DB Browser for SQLite). - The app uses WAL mode, so multiple readers are fine, but only one writer.
- Restart the server if the issue persists.
ð Scraping is slow
| Cause | Fix |
|---|---|
| Too few partitions | Lower the year/make filter, or increase batch_size (default 4 â try 8) |
| Network latency | Check your internet speed; detail-page fetches are bandwidth-bound |
| Running on HDD | Move cars.db to an SSD |
| Many duplicate fetches | Ensure fetch_method=api is selected (â 10Ã faster) |
| Resource blocking off | Verify block_resources is active (it is by default) |
ð Listings have duplicate VINs
listing_id is the primary key. If two URLs share the same listing_id, only the most recent record is kept. To check for true duplicates, query:
SELECT vin, COUNT(*) c FROM cars GROUP BY vin HAVING c > 1;
ð¥ "playwright not installed"
pip install playwright playwright install chromium
ð macOS: "developer cannot be verified"
When Chrome first launches, macOS Gatekeeper may block it. Go to System Settings â Privacy & Security â Open Anyway.
ð¤ Contributing
Contributions are warmly welcome! Whether it's a bug fix, new feature, or documentation improvement.
Development Setup
# Fork and clone git clone https://github.com/RasikhAli/Car.com-Scraper.git cd Car.com-Scraper # Create branch git checkout -b feature/amazing-thing # Install dev dependencies pip install -r requirements.txt playwright install chromium # Make your changes... # Run the app python app.py
Code Style
- Python: PEP 8, type hints encouraged
- Frontend: Bootstrap utility classes + custom CSS variables (defined in
:root) - Commits: Conventional Commits (
feat:,fix:,docs:,refactor:)
Areas for Contribution
- 𧪠Unit tests for
db.pyand partition logic - ð Multi-site support (autotrader.com, carmax.com, etc.)
- ð¤ ML price-prediction based on scraped history
- ð± Mobile dashboard with reduced layout
- ð³ Docker image for one-command deploys
- ð Grafana / Prometheus exporter for scrape metrics
- ð CAPTCHA solver integration (2Captcha, AntiCaptcha)
Submitting a PR
- Update the README with any new features
- Add/extend docstrings
- Test manually with a small filter (1 make, 1 year)
- Open the PR with a clear description & screenshots
ð License
Distributed under the MIT License. See LICENSE for the full text.
MIT License
Copyright (c) 2026 Rasikh Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
â ï¸ Disclaimer: This project is for educational and personal use only. Scraping cars.com may violate their Terms of Service. The authors are not responsible for any misuse. Always respect the target site's
robots.txtand rate limits.
ð Acknowledgments
This project stands on the shoulders of giants:
- ð Playwright â Powerful, modern browser automation
- ð¶ï¸ Flask & Flask-SocketIO â Elegant real-time web framework
- ðï¸ SQLite â The world's most deployed database
- ð¼ pandas & openpyxl â Seamless Excel export
- ð ±ï¸ Bootstrap & Bootstrap Icons â Beautiful, responsive UI primitives
- ð¤ Inter & Outfit â Premium open-source fonts
- ð Chrome DevTools Protocol â The bridge that makes this whole project possible
Project Gallery
Project Timeline
Technologies
External Links
Related Projects
Projects built with similar technologies.
Online Html Editor And Viewer
The Online HTML Editor and Viewer is a simple web application built with Flask that allows users to write and preview HTML code in real-time.
Qrgen
A premium, feature-rich QR Code Generator engineered with Python (Flask) and a pristine Glassmorphism frontend.
Examina Ai
Using AI, It transforms raw study materials into structured, verified examination sets with support for institutional export formats like Moodle XML.