Implementing Custom Export Pipelines with Scrapling's JSON/JSONL Export: A Complete Guide
Override the on_scraped_item hook in your Scrapling spider to transform, filter, or stream items to custom destinations while retaining the built-in ItemList.to_json() and to_jsonl() methods for final batch exports.
Scrapling provides a high-performance built-in export layer that serializes scraped items to JSON or JSONL formats using the ItemList class. While the default behavior writes the entire collection to disk after the crawl finishes, implementing custom export pipelines with Scrapling's JSON/JSONL export capabilities requires leveraging the on_scraped_item hook to intercept, transform, or redirect individual items before they reach the final collection.
Understanding Scrapling's Built-in Export Architecture
The ItemList Class and JSON/JSONL Methods
The export functionality resides in the ItemList class defined in scrapling/spiders/result.py (lines 10-38). This class maintains an in-memory collection of scraped dictionaries and provides two primary export methods:
to_json(path, indent=False): Serializes the entire collection to a JSON file using the high-performanceorjsonlibrary.to_jsonl(path): Writes each item as a separate JSON line to a JSONL file, ideal for streaming large datasets.
Both methods handle file I/O and encoding automatically, requiring only a file path argument.
The on_scraped_item Hook for Custom Pipelines
The extensibility point for custom export pipelines is the on_scraped_item hook located in scrapling/spiders/spider.py (lines 86-89). This asynchronous method is invoked immediately after an item is yielded from your parse method but before it is appended to the ItemList.
Method signature:
async def on_scraped_item(self, item: dict) -> dict | None
Return behavior:
- Return the
itemdict (potentially modified) to include it in the final collection. - Return
Noneto drop the item entirely, preventing it from reaching the JSON/JSONL export.
Implementing Custom Export Pipelines with on_scraped_item
Transforming Items Before Export
Use the hook to enrich or clean data before it reaches the default JSON export. This is useful for adding timestamps, normalizing fields, or injecting metadata.
from datetime import datetime
from scrapling.spiders import Spider, Response
class TransformSpider(Spider):
name = "transform_demo"
start_urls = ["https://example.com/products"]
async def parse(self, response: Response):
for product in response.css(".product"):
yield {
"name": product.css("h2::text").get(),
"price": product.css(".price::text").get(),
}
async def on_scraped_item(self, item: dict):
# Enrich with metadata
item["scraped_at"] = datetime.utcnow().isoformat()
item["source"] = self.name
return item
After the crawl completes, result.items.to_json("products.json", indent=True) will include the enriched fields.
Filtering and Dropping Unwanted Items
Implement validation logic to exclude incomplete or invalid records from your JSON/JSONL output by returning None from the hook.
class FilterSpider(Spider):
name = "filter_demo"
start_urls = ["https://example.com/listings"]
async def on_scraped_item(self, item: dict):
# Drop items missing critical fields
if not item.get("price") or not item.get("title"):
return None
# Drop items with invalid price format
price = item["price"]
if not isinstance(price, (int, float)) and not price.replace(".", "").isdigit():
return None
return item
Streaming to Incremental JSONL Files
For large-scale crawls where memory constraints prevent holding all items in ItemList, use on_scraped_item to write incrementally to a JSONL file while the spider runs.
import orjson
from pathlib import Path
from scrapling.spiders import Spider, Response
class StreamSpider(Spider):
name = "stream_demo"
start_urls = ["https://quotes.toscrape.com/"]
concurrent_requests = 5
async def on_start(self):
# Initialize file handle once at spider start
self._jsonl_path = Path("quotes_stream.jsonl")
self._jsonl_path.parent.mkdir(parents=True, exist_ok=True)
self._jsonl_file = self._jsonl_path.open("ab")
async def on_close(self):
# Ensure file is closed on spider shutdown
self._jsonl_file.close()
async def parse(self, response: Response):
for quote in response.css(".quote"):
yield {
"text": quote.css(".text::text").get(),
"author": quote.css(".author::text").get(),
"tags": quote.css(".tags .tag::text").getall(),
}
async def on_scraped_item(self, item: dict):
# Serialize and write immediately using orjson for speed
self._jsonl_file.write(orjson.dumps(item, option=orjson.OPT_SERIALIZE_NUMPY))
self._jsonl_file.write(b"\n")
return item # Optional: keep in memory for final dump if needed
This approach leverages orjson (the same library used by ItemList) for consistent performance while bypassing the in-memory collection.
Integrating with External Message Queues
Extend the pattern to send items to RabbitMQ, Redis, or other message brokers instead of (or in addition to) local JSON files.
import aio_pika
import orjson
from scrapling.spiders import Spider
class QueueSpider(Spider):
name = "queue_demo"
start_urls = ["https://example.com/feed"]
async def on_start(self):
self._connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
self._channel = await self._connection.channel()
self._queue = await self._channel.declare_queue("scrapling_items")
async def on_close(self):
await self._connection.close()
async def on_scraped_item(self, item: dict):
# Publish to RabbitMQ
await self._channel.default_exchange.publish(
aio_pika.Message(body=orjson.dumps(item)),
routing_key=self._queue.name
)
# Return None to prevent storing in ItemList (memory optimization)
return None
Combining Default Exports with Custom Pipelines
You can simultaneously use on_scraped_item for real-time processing and retain the built-in ItemList export for a final snapshot. This hybrid approach is useful when you need both a live stream and a complete batch file.
class HybridSpider(Spider):
name = "hybrid"
start_urls = ["https://example.com/data"]
async def on_scraped_item(self, item: dict):
# Real-time validation
if not item.get("id"):
return None
# Enrich data
item["processed"] = True
return item # Keeps item in ItemList
# After crawl:
# 1. Real-time processing handled in hook
# 2. Final export available via result.items.to_json("final.json")
Summary
ItemListinscrapling/spiders/result.pyprovides high-performance batch export viato_json()andto_jsonl()using theorjsonlibrary.on_scraped_iteminscrapling/spiders/spider.pyis the asynchronous hook called after each item is yielded but before it reaches theItemList, enabling real-time transformation, filtering, or streaming.- Return
Nonefromon_scraped_itemto drop items entirely, preventing them from appearing in JSON/JSONL exports. - Combine approaches by using
on_scraped_itemfor custom pipelines (databases, message queues, incremental files) while still callingresult.items.to_json()for a final batch snapshot. - Memory optimization for large crawls is achieved by streaming items to disk in
on_scraped_itemand returningNoneto avoid populating the in-memoryItemList.
Frequently Asked Questions
How do I drop items that fail validation before they reach the JSON export?
Override the on_scraped_item method in your spider class and return None for invalid items. According to the implementation in scrapling/spiders/spider.py (lines 86-89), returning None prevents the item from being appended to the ItemList, effectively excluding it from both the in-memory collection and any subsequent calls to to_json() or to_jsonl().
Can I use both custom streaming pipelines and the default batch export simultaneously?
Yes. When you return the item dict from on_scraped_item (instead of None), the item continues to the ItemList as usual. You can therefore write custom logic inside the hook to stream items to a database or message queue in real-time, then call result.items.to_json("final.json") after start() completes to obtain a complete batch file of all retained items.
What is the performance impact of using on_scraped_item for file I/O?
The hook is asynchronous, so you can use await with non-blocking I/O operations. For maximum performance when writing to JSONL files, use the orjson library (the same serializer used by ItemList) and open the file in binary append mode ("ab") as shown in the streaming example. This approach minimizes memory overhead and maintains high throughput even for millions of items.
How do I handle resource cleanup when using custom pipelines?
Implement the on_start and on_close lifecycle hooks available in the base Spider class. Initialize file handles, database connections, or message queue clients in on_start, and ensure proper cleanup in on_close. This guarantees that resources are released correctly even if the spider is interrupted or encounters an error during the crawl.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →