In the era of information overload, staying updated with the latest news, blog posts, and podcasts can feel like a full‑time job. That’s where an RSS feed parser and aggregator built with Python shines – it automatically pulls content from multiple sources, normalizes the data, and delivers it straight to your application or inbox. In this guide we’ll explore why Python is a top choice for RSS handling, walk through the most popular libraries, and build a simple yet powerful aggregator that you can extend for personal projects or production‑grade services.
Why Choose Python for RSS Parsing?
Python’s clean syntax, extensive standard library, and vibrant ecosystem make it ideal for working with XML‑based formats like RSS and Atom. Here are a few reasons developers consistently pick Python for this task:
- Readability: Code is easy to understand and maintain, which is crucial when dealing with varied feed structures.
- Rich libraries: Modules such as
feedparser,requests, andaiohttphandle the heavy lifting. - Cross‑platform support: Write once, run on Windows, macOS, or Linux without modification.
- Scalability: Combine synchronous and asynchronous approaches to scrape hundreds of feeds efficiently.
Core Concepts Behind RSS Feeds
Before diving into code, it helps to understand the anatomy of an RSS feed. Most feeds are XML documents that follow one of two specifications:
- RSS 2.0: The classic format, featuring
<channel>,<item>,<title>,<link>, and optional elements like<author>or<category>. - Atom: A newer, more flexible standard that uses
<feed>and<entry>tags.
Regardless of the version, the goal of a parser is to extract the same core fields – title, URL, publication date, and description – and present them in a uniform Python data structure.
Getting Started: Installing the Essentials
For most projects, the feedparser library is the go‑to solution. It abstracts away the differences between RSS and Atom, handling malformed XML gracefully. Pair it with requests for HTTP fetching, or aiohttp if you need asynchronous performance.
pip install feedparser requests aiohttp
Basic Synchronous Parser Example
The following snippet demonstrates a minimal synchronous RSS fetcher using requests and feedparser:
import requests
import feedparser
def fetch_feed(url):
response = requests.get(url, timeout=10)
response.raise_for_status() # Raise an error for bad responses
return feedparser.parse(response.content)
def print_titles(feed):
for entry in feed.entries[:5]: # Show first 5 items
print(f"- {entry.title}")
if __name__ == "__main__":
rss_url = "https://realpython.com/atom.xml"
feed = fetch_feed(rss_url)
print_titles(feed)
This script pulls the feed, parses it, and prints the titles of the latest five entries – a solid foundation for any aggregator.
Scaling Up with Asynchronous Requests
When aggregating dozens or hundreds of feeds, blocking I/O becomes a bottleneck. aiohttp combined with feedparser lets you fetch many feeds concurrently:
import asyncio
import aiohttp
import feedparser
async def fetch(session, url):
async with session.get(url) as resp:
content = await resp.read()
return feedparser.parse(content)
async def gather_feeds(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, u) for u in urls]
return await asyncio.gather(*tasks)
if __name__ == "__main__":
urls = [
"https://realpython.com/atom.xml",
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/python/.rss",
]
feeds = asyncio.run(gather_feeds(urls))
for feed in feeds:
print(f"Feed: {feed.feed.title}")
for entry in feed.entries[:3]:
print(f" • {entry.title}")
The asynchronous version can retrieve dozens of feeds in a fraction of the time required by a synchronous loop.
Designing a Full‑Featured Aggregator
Beyond simple fetching, a production‑ready aggregator typically includes:
- Feed storage: Persist raw XML or parsed entries in a database (SQLite, PostgreSQL, or NoSQL).
- Duplicate detection: Avoid re‑processing the same article by tracking GUIDs or hashes.
- Scheduling: Periodically refresh feeds using
cron,APScheduler, or cloud‑based task queues. - Filtering & enrichment: Apply keyword filters, sentiment analysis, or add custom tags.
- Output formats: Serve JSON APIs, generate HTML newsletters, or push to messaging platforms (Slack, Discord).
Sample Data Model with SQLite
Below is a lightweight schema that captures the essential fields for each article:
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
feed_url TEXT NOT NULL,
guid TEXT UNIQUE,
title TEXT,
link TEXT,
published DATETIME,
summary TEXT,
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
When inserting a new entry, the UNIQUE constraint on guid guarantees that duplicates are ignored.
Putting It All Together – A Minimal Aggregator Script
Here’s a compact example that combines async fetching, duplicate checks, and SQLite persistence. It can serve as a starter template for more complex systems.
import asyncio
import aiohttp
import feedparser
import sqlite3
from datetime import datetime
DB_PATH = "rss_aggregator.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
feed_url TEXT,
guid TEXT UNIQUE,
title TEXT,
link TEXT,
published TEXT,
summary TEXT,
fetched_at TEXT
)""")
conn.commit()
conn.close()
async def fetch(session, url):
async with session.get(url, timeout=15) as resp:
content = await resp.read()
return url, feedparser.parse(content)
def save_entry(conn, feed_url, entry):
guid = entry.get("id") or entry.get("guid") or entry.link
published = entry.get("published", datetime.utcnow().isoformat())
try:
conn.execute(
"INSERT INTO articles (feed_url, guid, title, link, published, summary) "
"VALUES (?, ?, ?, ?, ?, ?)",
(feed_url, guid, entry.title, entry.link, published, entry.summary)
)
except sqlite3.IntegrityError:
# Duplicate GUID – skip insertion
pass
async def aggregate(urls):
init_db()
conn = sqlite3.connect(DB_PATH)
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, u) for u in urls]
for feed_url, feed in await asyncio.gather(*tasks):
for entry in feed.entries:
save_entry(conn, feed_url, entry)
conn.commit()
conn.close()
if __name__ == "__main__":
feed_list = [
"https://realpython.com/atom.xml",
"https://news.ycombinator.com/rss",
"https://www.reddit.com/r/python/.rss",
# Add more URLs as needed
]
asyncio.run(aggregate(feed_list))
print("Aggregation complete. Check the SQLite DB for stored articles.")
This script demonstrates the core workflow:
- Initialize the database.
- Fetch all feeds concurrently.
- Iterate over each entry, compute a reliable GUID, and insert it if it’s new.
- Close the connection cleanly.
Advanced Topics and Best Practices
Handling Authentication and Rate Limits
Some premium feeds require HTTP Basic Auth, OAuth, or API keys. aiohttp supports custom headers, while requests offers built‑in auth helpers. Remember to respect RFC 5005 guidelines and honor Retry-After headers to avoid being blocked.
Parsing Media Enclosures
Podcast feeds often embed audio files using the <enclosure> tag. feedparser exposes these via entry.enclosures. You can extract URLs, MIME types, and lengths to build a download queue or streaming service.
Testing Your Parser
Automated tests protect against breaking changes in feed formats. Use pytest together with sample XML fixtures:
def test_parse_realpython():
with open("tests/fixtures/realpython.xml", "rb") as f:
feed = feedparser.parse(f.read())
assert feed.feed.title == "Real Python"
assert len(feed.entries) > 0
assert "Python" in feed.entries[0].title
Deploying at Scale
When
Leave a Reply