Skip to main content

scrapy-ingest

Scrapy Ingest

PyPI version Python versions Downloads Documentation License: MIT Changelog

One pipeline. Full crawl observability in SQL or Elasticsearch.

Set DB_URL, SEARCH_URL, or both — or use no DB settings and ingest writes to ingest.db (SQLite) in your project folder. Items, requests, logs, stats, parent URLs, and errors are captured automatically.

Install

pip install scrapy-ingest

This installs all runtime dependencies, including psycopg2-binary, PyMySQL, and opensearch-py.

Minimal setup (settings.py)

Only the item pipeline is required — requests, logs, stats, parent_url, and error logging are enabled automatically.

Local dev (SQLite, no server):

ITEM_PIPELINES = {
    "scrapy_ingest.pipelines.IngestPipeline": 300,
}
CREATE_TABLES = True  # creates ingest.db in the project directory

Postgres / MySQL:

ITEM_PIPELINES = {
    "scrapy_ingest.pipelines.IngestPipeline": 300,
}

DB_URL = "postgresql://user:password@localhost:5432/database"

Run your spider:

scrapy crawl your_spider

Log level follows Scrapy LOG_LEVEL.

Choose your destination

Set DB_URL (or DB_* fields) for Postgres/MySQL/SQLite, SEARCH_URL for Elasticsearch/OpenSearch, or both. If nothing is set, ingest.db (SQLite) is used unless INGEST_SQLITE_ENABLED = False.

Mode Settings Result
Default (dev) (none) SQLite (ingest.db)
Database only DB_URL or DB_* SQL only
Elasticsearch / OpenSearch only SEARCH_URL Indexes only
Both DB_URL + SEARCH_URL SQL first, then Elasticsearch/OpenSearch
# Database only
DB_URL = "postgresql://user:password@localhost:5432/database"

# Elasticsearch / OpenSearch only
SEARCH_URL = "http://localhost:9200"

# Both
DB_URL = "postgresql://user:password@localhost:5432/database"
SEARCH_URL = "http://localhost:9200"

When both are configured, SQL is written first, then Elasticsearch/OpenSearch indexes. An indexing failure after a successful SQL write is logged but does not roll back database rows.

Database configuration

PostgreSQL or MySQL — pick one connection style.

Single URL:

DB_URL = "postgresql://user:password@localhost:5432/database"
# DB_URL = "mysql://user:password@localhost:3306/database"

Discrete fields (no URL encoding for special characters in passwords):

DB_TYPE = "postgres"   # or "mysql" / "mariadb"
DB_HOST = "localhost"
DB_PORT = 5432         # MySQL: 3306
DB_USER = "user"
DB_PASSWORD = "password"
DB_NAME = "database"

Tables are created automatically when CREATE_TABLES = True (default).

Elasticsearch / OpenSearch configuration

Elasticsearch and OpenSearch use the same REST bulk API. The package connects via opensearch-py, which works with both.

Minimum:

SEARCH_URL = "http://localhost:9200"

With authentication (optional):

SEARCH_URL = "https://search.example.com:9200"
SEARCH_USER = "elastic"
SEARCH_PASSWORD = "secret"
SEARCH_SSL_VERIFY = True   # set False only for self-signed certs in dev

Index names match SQL table names by default:

Table / index Contents
ingest_jobs Job metadata: status, counts, elapsed time, stats
ingest_items Scraped items as JSON documents
ingest_requests Requests: url, fingerprint, parent_url, status, error
ingest_logs Log lines: level, logger, message, exception

Override table/index names with JOBS_TABLE, ITEMS_TABLE, REQUESTS_TABLE, and LOGS_TABLE (same setting for SQL and Elasticsearch/OpenSearch).

Indexes are created on first write if they do not exist.

What is stored

Relational database (Postgres / MySQL)

Table Contents
ingest_jobs One row per crawl: id, unique job_id string, spider, status, start/finish, counts, items/min, stats. errors_count = failed requests + standalone ERROR/CRITICAL logs (not double-counted from scrapy.core.scraper when the request is already failed)
ingest_items JSON items (crawled_at added). job_id = ingest_jobs.id (CASCADE)
ingest_requests url, parent_url, parent_id, status, response_time_secs, fingerprint, error, success (download errors, HTTP 4xx/5xx, spider callback tracebacks)
ingest_logs time, logger, level, message, exception

Request parent_url is the page that scheduled the request (e.g. sitemap → product). Start URLs are null. The request fingerprint is a SHA1 hash of method + canonical URL for parent lookup.

Elasticsearch / OpenSearch

Same crawl data as JSON documents in the indexes listed above. Documents include the string job_id (not the integer SQL primary key). Elasticsearch/OpenSearch mode does not link parent_id; parent_url is stored on each request document.

Ingest flow and connections

One connection per destination per crawl — not connect → insert → disconnect for every product. Items, requests, and logs are buffered in memory and written in batches using the same Postgres/MySQL connection and/or Elasticsearch/OpenSearch client for the whole spider run.

Full diagrams: docs/ingest-flow.rst (Read the Docs: Ingest flow).

Crawl lifecycle

flowchart TB
    START([scrapy crawl starts]) --> OPEN[IngestPipeline open_spider]
    OPEN --> FLSTART[IngestFlusher.start]

    FLSTART --> DB{DB configured?}
    DB -->|yes| DBCONN[Open one Postgres/MySQL connection<br/>application_name = Ingest]
    DB -->|no| SKIPDB[Skip SQL]

    FLSTART --> SRCH{SEARCH_URL set?}
    SRCH -->|yes| ESCONN[Open one SearchClient<br/>X-Opaque-Id: Ingest]
    SRCH -->|no| SKIPES[Skip search]

    DBCONN --> JOB[Create job row / index doc]
    ESCONN --> JOB
    JOB --> TIMER[Start 10s periodic flush timer]
    TIMER --> CRAWL([Spider crawls])

    CRAWL --> END([Engine stops])
    END --> FINAL[Final flush + finish_job]
    FINAL --> SUMMARY[Print crawl summary]
    SUMMARY --> CLOSE[Close DB + search clients]

When data is flushed

flowchart TB
    subgraph inputs [Buffered in memory]
        I1[Items]
        I2[Requests]
        I3[Logs]
    end

    inputs --> BUF[(Collector buffer)]

    BUF --> T1{Size ≥ INGEST_BATCH_SIZE?<br/>default 50}
    BUF --> T2{Timer every 10s?}

    T1 -->|yes| FLUSH[flush batch]
    T2 -->|yes| FLUSH

    FLUSH --> SQL[SQL: INSERT + COMMIT<br/>same connection]
    FLUSH --> ES[Search: bulk index<br/>same HTTP client]
Setting Default Effect
INGEST_BATCH_SIZE 50 Flush when buffer reaches this many rows
INGEST_FLUSH_INTERVAL 10 Also flush every N seconds (set 0 to disable)

Example: 100,000 products → ~2,000 batch flushes (not 100,000 connections).

When both SQL and search are configured, SQL commits first; a search failure after SQL success is logged but does not roll back database rows.

When the spider closes, a crawl summary is printed to stderr (visible even when LOG_LEVEL is ERROR) with job id, spider, database URL, Elasticsearch/OpenSearch URL, table/index names, counts, and elapsed time. Set INGEST_SHOW_SUMMARY = False to hide it.

CLI

Run from your Scrapy project directory (loads settings.py via Scrapy).

check-config

Validate settings and ping configured destinations before running a spider:

scrapy-ingest check-config
scrapy-ingest check-config --db-url "postgresql://user:pass@localhost:5432/db"
scrapy-ingest check-config --search-url "http://localhost:9200"

Shows a spinner per step, prints a summary table to stderr, and exits 0 when all configured destinations are reachable.

jobs show

List recent jobs or print one job summary from SQL or Elasticsearch/OpenSearch (requires DB_URL and/or SEARCH_URL). When both are configured, queries use SQL; crawls still dual-write to both.

scrapy-ingest jobs show                                    # list recent jobs
scrapy-ingest jobs show --limit 20                         # list up to 20 jobs
scrapy-ingest jobs show --status running                   # filter by status (list mode)
scrapy-ingest jobs show --spider Rs_Spider                 # filter by spider (list mode)
scrapy-ingest jobs show Rs_Spider-178826754-a1b2            # one job (Field | Value table)
scrapy-ingest jobs show --search-url "http://localhost:9200"

List mode (no job id): table of recent jobs — job id, spider, status, timestamps, counts. Detail mode (with job id): Field | Value table from the jobs row. --status and --spider apply in list mode only.

The job id is the string shown in the end-of-crawl summary (or your JOB_ID setting). Exit 0 on success (including an empty list); exit 1 if a specific job id is missing or the destination is unreachable.

Typical workflow:

scrapy-ingest check-config
scrapy crawl your_spider
scrapy-ingest jobs show
scrapy-ingest jobs show --status finished --spider your_spider
scrapy-ingest jobs show <job_id>

See CLI docs for details.

Useful settings

Setting Default Description
DB_URL / DB_* — Database connection (enables SQL ingest)
SEARCH_URL — Elasticsearch or OpenSearch URL (enables indexing)
SEARCH_USER / SEARCH_PASSWORD — HTTP basic auth for Elasticsearch/OpenSearch (optional)
SEARCH_SSL_VERIFY True Verify HTTPS certificates
CREATE_TABLES True Auto-create SQL tables on startup
INGEST_BATCH_SIZE 50 Flush when this many rows are buffered
INGEST_FLUSH_INTERVAL 10 Periodic flush in seconds
INGEST_SHOW_SUMMARY True Print crawl summary when the spider closes
DB_TYPE postgres postgres / mysql / mariadb for discrete DB_* fields
ITEMS_TABLE, REQUESTS_TABLE, LOGS_TABLE, JOBS_TABLE see defaults Override SQL table and index names (same value for both)
TIMEZONE Asia/Karachi Timezone for created_at
JOB_ID auto Omit to auto-generate (Rs_Spider-178826754-a1b2)

Troubleshooting

  • Password has @ or $ in DB_URL? Encode them: @ → %40, $ → %24. Or use discrete DB_* fields.
  • Yield items from callbacks (not only return inside a generator).
  • Elasticsearch / OpenSearch connection refused? Check SEARCH_URL, firewall, and that the cluster is running. Test with curl http://localhost:9200.
  • HTTPS / self-signed cert? Set SEARCH_SSL_VERIFY = False in development only.

Standalone components

If you only want part of the collection:

# Items only
ITEM_PIPELINES = {"scrapy_ingest.pipelines.ItemsPipeline": 300}

# Requests only (parent_url + errors)
ITEM_PIPELINES = {"scrapy_ingest.pipelines.RequestsPipeline": 300}

# Logs only
EXTENSIONS = {"scrapy_ingest.extensions.LoggingExtension": 500}

Benchmarks and comparisons

How scrapy-ingest compares to other Scrapy storage packages — feature matrices, architecture notes, and a reproducible write-path benchmark.

Package SQL ES/OS Mongo Kafka Requests / logs Batched
scrapy-ingest ✓ PG/MySQL ✓ — — auto ✓ (50)
scrapy-elasticsearch — ✓ — — — ✓ (500)
scrapy-mongodb — — ✓ — — per-item
scrapy-sql ✓ FEEDS — — — — bulk
os-scrapy-kafka-pipeline — — — ✓ — per-item
Manual psycopg2 pipeline DIY — — — DIY often ✗
Scrapy FEEDS files — — — — stream

Also in docs: scrapy-pipelines (Scrapedia/Mongo), Scrapy MongoPipeline (official example).

Synthetic benchmark (SQLite, 100k rows, batch 50): ~3.3× faster write path vs per-row COMMIT on a sample run — real crawls are usually network-bound. Run yourself:

python scripts/benchmark_ingest.py --rows 100000 --batch-size 50

Full comparison, methodology, and production tuning: docs/benchmarks.rst · Read the Docs.

License

MIT License. See LICENSE.

Metadata

Release files for scrapy-ingest 1.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for scrapy-ingest 1.6.0
File Size Uploaded
scrapy_ingest-1.6.0.tar.gz 232.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for scrapy-ingest 1.6.0
File Interpreter ABI Platform
scrapy_ingest-1.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 284.5 kB

Release files / scrapy_ingest-1.6.0.tar.gz

Download URL scrapy_ingest-1.6.0.tar.gz
Size 232.3 kB
Tags Source
SHA-256 checksum
How to use checksums
8527e216f316febf3ec595393c06f8ebd0380f7bb1540d401403d0785dca59ed
BLAKE2b-256 checksum
How to use checksums
db89bd2b9fc2f9a05c58497088408e87d95b22992c6a1be322b48b5fa94a2f71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release files / scrapy_ingest-1.6.0-py3-none-any.whl

Download URL scrapy_ingest-1.6.0-py3-none-any.whl
Size 52.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
347a43f5b31f3f70c2944f5511eef2a7a557482b22b28c759f3f745843f78f57
BLAKE2b-256 checksum
How to use checksums
2155ac8c6e974c58d2a570e2684aff323f823d98133f27da882658c8b8474807
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release history Release notifications | RSS feed

1.8.0

2 release files

1.7.0

2 release files

This release

1.6.0 This release

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page