rss
Fetch and parse RSS, Atom, and RDF feeds with a dependency-free Python helper.
Downloads: 6 ยท ID: 7c5b6af3a7ec86dd13000000
Fetch and parse RSS, Atom, and RDF feeds with a dependency-free Python helper.
Downloads: 6 ยท ID: 7c5b6af3a7ec86dd13000000
<!-- FILE: rss_skill.md -->
# RSS Skill
Fetch and display RSS/Atom feed content from the command line. Handles RSS 2.0, Atom, and RSS 1.0 (RDF) formats.
```
python fetch_rss.py <feed_url> [--items N] [--show-body] [--raw]
```
| Arg | Default | Description |
|-----|---------|-------------|
| `feed_url` | required | URL of the RSS/Atom feed |
| `--items` | 10 | Number of items to show (0 = all) |
| `--show-body` | off | Show full description/body text of each item |
| `--raw` | off | Print raw XML instead of parsed output |
**Examples:**
```bash
# CBC Top Stories (default: 10 items)
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories
# CBC Top Stories, just 5 items with full description
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 5 --show-body
# See all items
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 0
# See raw XML
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --raw
```
**Other CBC feeds you can try:**
- `https://www.cbc.ca/webfeed/rss/rss-topstories` โ Top Stories
- `https://www.cbc.ca/webfeed/rss/rss-world` โ World
- `https://www.cbc.ca/webfeed/rss/rss-canada` โ Canada
- `https://www.cbc.ca/webfeed/rss/rss-politics` โ Politics
- `https://www.cbc.ca/webfeed/rss/rss-business` โ Business
- `https://www.cbc.ca/webfeed/rss/rss-sports` โ Sports
- `https://www.cbc.ca/webfeed/rss/rss-arts` โ Arts & Entertainment
- `https://www.cbc.ca/webfeed/rss/rss-tech` โ Technology
- `https://www.cbc.ca/webfeed/rss/rss-offbeat` โ Offbeat
**Other Canadian news RSS feeds:**
| Source | Feed URL |
|--------|----------|
| **CBC Top Stories** | `https://www.cbc.ca/webfeed/rss/rss-topstories` |
| **CBC World** | `https://www.cbc.ca/webfeed/rss/rss-world` |
| **CBC Canada** | `https://www.cbc.ca/webfeed/rss/rss-canada` |
| **CBC Politics** | `https://www.cbc.ca/webfeed/rss/rss-politics` |
| **CBC Business** | `https://www.cbc.ca/webfeed/rss/rss-business` |
| **CBC Sports** | `https://www.cbc.ca/webfeed/rss/rss-sports` |
| **CBC Arts** | `https://www.cbc.ca/webfeed/rss/rss-arts` |
| **CBC Technology** | `https://www.cbc.ca/webfeed/rss/rss-tech` |
| **CBC Offbeat** | `https://www.cbc.ca/webfeed/rss/rss-offbeat` |
| **BBC Top Stories** | `https://feeds.bbci.co.uk/news/rss.xml` |
| **BBC World** | `https://feeds.bbci.co.uk/news/world/rss.xml` |
| **NPR Top Stories** | `https://feeds.npr.org/1001/rss.xml` |
| **Reuters** | `https://www.reutersagency.com/feed/` |
| **The Guardian** | `https://www.theguardian.com/world/rss` |
| **Hacker News** | `https://hnrss.org/frontpage` |
| **Reddit (r/all)** | `https://www.reddit.com/r/all/.rss` |
**Known limitations:**
- No dependency on `feedparser` library โ uses only Python stdlib (`xml.etree.ElementTree`)
- HTML in descriptions is stripped to plain text
- Some feeds may require True headers (already spoofed as Chrome browser)
## Dependencies
- None (uses Python standard library)
> **Note for macOS/OSX Users:** To avoid environment conflicts when running within a project's virtual environment, it is recommended to execute this skill using `"${UV_BIN:-$(command -v uv || echo /opt/homebrew/bin/uv)}" run <path_to_script>`.
<!-- FILE: fetch_rss.py -->
#!/usr/bin/env python3
"""
fetch_rss.py - Fetch and display RSS/Atom feed content.
Usage:
python fetch_rss.py <feed_url> [--items N] [--show-body] [--raw]
Arguments:
feed_url URL of the RSS/Atom feed
--items N Number of items to show (default: 10, 0 = all)
--show-body Show full description/body of each item
--raw Print raw XML instead of parsed output
Examples:
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 5
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 3 --show-body
"""
import argparse
import sys
import re
import html
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from xml.etree import ElementTree as ET
NS = {
'atom': 'http://www.w3.org/2005/Atom',
'content': 'http://purl.org/rss/1.0/modules/content/',
'dc': 'http://purl.org/dc/elements/1.1/',
'media': 'http://search.yahoo.com/mrss/',
}
def strip_html(text):
"""Remove HTML tags and decode entities, return clean text."""
if not text:
return ''
text = re.sub(r'<[^>]+>', ' ', text)
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def fetch_url(url):
"""Fetch a URL and return the response body as bytes."""
req = Request(url, headers={
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
})
return urlopen(req, timeout=15).read()
def get_text(element, xpath, namespaces=None, default=''):
"""Get text content from an XML element using an XPath-ish tag name."""
child = element.find(xpath, namespaces)
if child is not None and child.text:
return child.text.strip()
return default
def parse_feed(xml_bytes, url):
"""Parse RSS 2.0, Atom, or RSS 1.0 XML into a structured dict."""
root = ET.fromstring(xml_bytes)
tag = root.tag
feed = {
'title': '',
'description': '',
'link': url,
'items': [],
}
# --- RSS 2.0 ---
if tag == 'rss' or tag.endswith('}rss'):
channel = root.find('channel')
if channel is None:
raise ValueError('No <channel> element in RSS 2.0 feed')
feed['title'] = get_text(channel, 'title')
feed['description'] = get_text(channel, 'description')
feed['link'] = get_text(channel, 'link') or url
for item in channel.findall('item'):
entry = {
'title': strip_html(get_text(item, 'title')),
'link': get_text(item, 'link'),
'pubDate': get_text(item, 'pubDate'),
'author': get_text(item, 'author') or get_text(item, 'dc:creator', NS),
'description': strip_html(get_text(item, 'description')),
'guid': get_text(item, 'guid'),
}
# Check for content:encoded (full article HTML)
content_enc = get_text(item, 'content:encoded', NS)
if content_enc:
entry['description'] = strip_html(content_enc)
feed['items'].append(entry)
# --- Atom ---
elif 'feed' in tag:
feed['title'] = get_text(root, 'atom:title', NS) or get_text(root, 'title')
feed['description'] = get_text(root, 'atom:subtitle', NS) or ''
feed['link'] = url
for entry_elem in root.findall('atom:entry', NS) or root.findall('entry'):
entry = {
'title': strip_html(get_text(entry_elem, 'atom:title', NS) or get_text(entry_elem, 'title')),
'link': '',
'pubDate': get_text(entry_elem, 'atom:published', NS) or get_text(entry_elem, 'atom:updated', NS)
or get_text(entry_elem, 'published') or get_text(entry_elem, 'updated'),
'author': '',
'description': strip_html(get_text(entry_elem, 'atom:summary', NS) or get_text(entry_elem, 'summary')
or get_text(entry_elem, 'atom:content', NS) or get_text(entry_elem, 'content')),
'guid': '',
}
# Find link
link_elem = entry_elem.find('atom:link', NS) or entry_elem.find('link')
if link_elem is not None:
entry['link'] = link_elem.get('href', '')
# Author
author_elem = entry_elem.find('atom:author', NS) or entry_elem.find('author')
if author_elem is not None:
entry['author'] = get_text(author_elem, 'atom:name', NS) or get_text(author_elem, 'name') or ''
feed['items'].append(entry)
# --- RSS 1.0 (RDF) ---
else:
# Try namespace
rss_ns = 'http://purl.org/rss/1.0/'
dc_ns = 'http://purl.org/dc/elements/1.1/'
feed['title'] = get_text(root, f'{{{rss_ns}}}channel/{{{rss_ns}}}title') or feed['title']
feed['description'] = get_text(root, f'{{{rss_ns}}}channel/{{{rss_ns}}}description') or ''
for item_elem in root.findall(f'{{{rss_ns}}}item'):
entry = {
'title': strip_html(get_text(item_elem, f'{{{rss_ns}}}title')),
'link': get_text(item_elem, f'{{{rss_ns}}}link'),
'pubDate': get_text(item_elem, f'{{{dc_ns}}}date'),
'author': get_text(item_elem, f'{{{dc_ns}}}creator'),
'description': strip_html(get_text(item_elem, f'{{{rss_ns}}}description')),
'guid': '',
}
feed['items'].append(entry)
return feed
def print_feed(feed, max_items=10, show_body=False):
"""Pretty-print parsed feed."""
print(f"๐ก Feed: {feed['title']}")
if feed['description']:
print(f" {strip_html(feed['description'])}")
print(f" Link: {feed['link']}")
print(f" Items: {len(feed['items'])}")
print()
items = feed['items'][:max_items] if max_items > 0 else feed['items']
if not items:
print("(No items found.)")
return
for i, item in enumerate(items, 1):
print(f"{'โ' * 72}")
print(f" [{i}] {item['title']}")
if item['pubDate']:
print(f" ๐
{item['pubDate']}")
if item['author']:
print(f" ๐ค {item['author']}")
if item['link']:
print(f" ๐ {item['link']}")
if item['guid']:
print(f" ๐ {item['guid']}")
if show_body and item['description']:
desc = item['description']
if len(desc) > 500:
desc = desc[:500] + '...'
print(f" ๐ {desc}")
print(f"{'โ' * 72}")
def main():
parser = argparse.ArgumentParser(
description='Fetch and parse RSS/Atom feeds from the command line.'
)
parser.add_argument('url', help='URL of the RSS/Atom feed')
parser.add_argument('--items', type=int, default=10,
help='Number of items to show (0 = all, default: 10)')
parser.add_argument('--show-body', action='store_true',
help='Show full description/body of each item')
parser.add_argument('--raw', action='store_true',
help='Print raw XML instead of parsed output')
args = parser.parse_args()
try:
print(f"Fetching {args.url} ...", file=sys.stderr)
data = fetch_url(args.url)
if args.raw:
print(data.decode('utf-8', errors='replace'))
return
try:
feed = parse_feed(data, args.url)
except ET.ParseError as e:
print(f"โ XML parse error: {e}", file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(f"โ {e}", file=sys.stderr)
sys.exit(1)
print_feed(feed, max_items=args.items, show_body=args.show_body)
except HTTPError as e:
print(f"โ HTTP {e.code}: {e.reason}", file=sys.stderr)
sys.exit(1)
except URLError as e:
print(f"โ URL error: {e.reason}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"โ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()