Skip to content
datahacks.dev
XML

Analyzing Podcast and Blog Feeds From XML With Python

Turn an RSS podcast feed into cadence, duration and size numbers, handle itunes durations in two formats and RFC 822 dates, and compare with feedparser.

6 min read
Summarize in AI
Analyzing Podcast and Blog Feeds From XML With Python

A podcast feed is an XML file that a few thousand programs fetch every hour. It is also a small data set about how a show is made: when episodes come out, how long they are, how big the files are. The same applies to a blog feed and its posts. If you want to know whether a publication keeps its schedule, or to build a table for a list of shows, the feed has what you need, in a format with a few quirks.

The feed

rss.xmlXML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel><title>Dev Talk</title>
    <item><title>Episode 3: Indexes</title><pubDate>Mon, 05 Oct 2026 09:00:00 +0000</pubDate><itunes:duration>00:42:10</itunes:duration><enclosure url="https://example.test/e3.mp3" length="40500000" type="audio/mpeg"/><guid>e3</guid></item>
    <item><title>Episode 2: Caching</title><pubDate>Mon, 21 Sep 2026 09:00:00 +0000</pubDate><itunes:duration>1850</itunes:duration><enclosure url="https://example.test/e2.mp3" length="30100000" type="audio/mpeg"/><guid>e2</guid></item>
    <item><title>Episode 1: Hello</title><pubDate>Mon, 07 Sep 2026 09:00:00 +0000</pubDate><enclosure url="https://example.test/e1.mp3" length="20000000" type="audio/mpeg"/><guid>e1</guid></item>
  </channel>
</rss>

RSS 2.0 puts the episodes as item elements under channel, newest first by convention. The title, date and guid are plain RSS. The iTunes duration is in an extension namespace, itunes:duration, so it needs a prefix map. The audio file is an enclosure with its address, size and type as attributes, with no text. Two of the quirks are visible already: the durations are written differently in the first two items, and the third has none.

The three conversions

pod.pyPython
import email.utils
from datetime import timedelta
from lxml import etree

NS = {"itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"}

def dur(s):
    if s is None:
        return None
    if ":" in s:
        parts = [int(x) for x in s.split(":")]
        while len(parts) < 3:
            parts.insert(0, 0)
        return timedelta(hours=parts[0], minutes=parts[1], seconds=parts[2])
    return timedelta(seconds=int(s))

r = etree.parse("rss.xml").getroot()
rows = []
for it in r.iterfind("channel/item"):
    d = email.utils.parsedate_to_datetime(it.findtext("pubDate"))
    rows.append((it.findtext("title"), d.date(),
                 dur(it.findtext("itunes:duration", namespaces=NS)),
                 int(it.find("enclosure").get("length"))))
for x in rows:
    print(x)
Text
('Episode 3: Indexes', datetime.date(2026, 10, 5), datetime.timedelta(seconds=2530), 40500000)
('Episode 2: Caching', datetime.date(2026, 9, 21), datetime.timedelta(seconds=1850), 30100000)
('Episode 1: Hello', datetime.date(2026, 9, 7), None, 20000000)

Dates. RSS uses RFC 822 dates (Mon, 05 Oct 2026 09:00:00 +0000), a format ISO 8601 parsers do not understand. The standard library's email.utils.parsedate_to_datetime reads them and keeps the time zone. In real feeds you also meet two-digit years, missing weekday names and zone names like EST; the function handles the common ones, and a feed that sends something else raises ValueError, which you should catch per item and log. Atom feeds use ISO 8601 instead.

Durations. The iTunes tag accepts HH:MM:SS, MM:SS or a plain number of seconds, and real feeds use all three. The function pads a short colon form with leading zeros and treats a bare number as seconds. 00:42:10 is 2,530 seconds and 1850 is 1,850. A missing tag returns None, the third item, and the downstream code must not turn that into zero.

Sizes. enclosure/@length is the byte count the publisher declares. It is an assertion. The real file can differ, and some feeds give 0 or leave it out. For a rough storage estimate it is fine.

What the numbers say

Python
ds = sorted(r[1] for r in rows)
print("gaps days:", [(b - a).days for a, b in zip(ds, ds[1:])])
print("total MB", sum(x[3] for x in rows) / 1e6, "| missing duration:", [x[0] for x in rows if x[2] is None])
Text
gaps days: [14, 14]
total MB 90.6 | missing duration: ['Episode 1: Hello']

Episodes every 14 days, 90.6 MB across three, and one episode without a duration. The gaps are the interesting statistic: a median gap and the longest gap describe consistency far better than an average per month. With real data I add the weekday and hour of publication (all three here are Mondays at 09:00 UTC), the trend of duration over the last twenty episodes (excluding the missing ones, which means the average is over two items here), and the size per minute of audio as a bitrate check, length * 8 / seconds. For episode 3 that is 40.5 million bytes over 2,530 seconds, about 128 kbit/s, which is a sensible bitrate and a quick way to spot an enclosure length that is wrong.

Comparing with feedparser

The Python feedparser library reads RSS and Atom into a uniform structure and tolerates malformed feeds:

Python
import feedparser
f = feedparser.parse(open("rss.xml", "rb").read())
print([(e.title, e.get("itunes_duration")) for e in f.entries])
Text
[('Episode 3: Indexes', '00:42:10'), ('Episode 2: Caching', '1850'), ('Episode 1: Hello', None)]

It found the same three items, mapped the namespaced tag to itunes_duration, and returned the value as written: the two formats are still two formats, and the missing one is None. So feedparser normalizes the structure of the feed (RSS versus Atom, field names, date parsing into a tuple) and leaves value conventions to you. For a one-off look at a feed, or a crawler that must accept hundreds of feeds in different dialects, it is the better tool, because it also copes with feeds that are not well-formed XML. For a feed you control or a precise job on one dialect, lxml gives you exact control and a clear error when something deviates. I use feedparser to discover and lxml to load.

Blog feeds

A blog feed (RSS or Atom) works the same way with different fields: title, link, date, author, categories, and either a summary or the full content in content:encoded. The questions that make sense are posting cadence, the proportion of posts per category, the length of the content (strip tags first; the body is HTML inside the XML, and its escaping is a layer of its own), and the age of the newest item as a liveness signal. Two cautions. A feed holds only the most recent items, often 10 to 50, so it is a window and not the archive. And the dates in a feed are the publisher's, and edited posts sometimes get a new pubDate, which turns a cadence analysis into noise.

If you monitor feeds on a schedule, store each fetched item by its guid, the first time you saw it and the HTTP validators (ETag, Last-Modified), so you can ask for the file only when it has changed (checking feed health with a daily script).

The parsing side is covered in parsing an RSS feed in Python, and RSS and Atom as formats in RSS and Atom feeds explained for developers. The cluster overview is analysis on XML data when nobody has converted it yet. The RSS format is specified in the RSS 2.0 specification, the iTunes tags in Apple's podcast RSS documentation, and the library in the feedparser documentation. Test a path in the XPath tester.

When feedparser is the better reader

You read many feeds you do not control. feedparser accepts malformed XML, strange encodings and both RSS and Atom, and returns one structure. An exact parser would reject a good share of real feeds.

You do not need exact values. If you only want titles, links and dates for a reader or a digest, take what feedparser gives.

The feed is the only thing you can get. For the full history of a show or a blog, ask for an archive or crawl the site. A feed shows the recent window.

You are checking a feed against a specification. For validation, a feed validator and the schema of the dialect are better than a lenient reader.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.