#################
From feedparser
#################
.. package-meta:: feedparser kurtmckee/feedparser
`feedparser `_ is the canonical Python feed reader. ``feedparser.parse`` accepts a
URL, file, stream, or string, detects the format (RSS 0.9x-2.0, Atom 0.3/1.0, RDF/RSS-1.0, CDF), and returns a
``FeedParserDict`` whose ``feed`` and ``entries`` normalize every dialect's spelling of a field onto shared keys --
``title``, ``link``, ``id``, ``updated``/``published``, ``summary``/``content``, ``author`` -- with parsed ``*_parsed``
time structs, a ``bozo`` flag for malformed input, relative-URL resolution, and HTML sanitization of element content.
turbohtml serves the normalization core from :func:`turbohtml.extract.feed`, a string entry point over
:meth:`turbohtml.Document.feed`: the format is detected from the root element and each field mapped onto one frozen,
typed :class:`~turbohtml.extract.Feed` of :class:`~turbohtml.extract.Entry` records in one C walk of the parsed tree. It
keeps the field set and the precedence rules feedparser established, in the minimal typed shape ``htmlparser2``'s
``parseFeed`` models, and leaves the network, date-parsing, and content-sanitizing surface to the tools that own those
jobs.
*************************
turbohtml vs feedparser
*************************
.. list-table::
:header-rows: 1
:widths: 20 40 40
- - Dimension
- turbohtml
- feedparser
- - Scope
- Full WHATWG parser; feed normalization is one feature of many
- Single-purpose feed reader
- - Feature breadth
- Normalized ``Feed``/``Entry`` for RSS 2.0, Atom 1.0, and RDF/RSS-1.0 off the parsed tree
- Every dialect back to RSS 0.9x and Atom 0.3, plus parsed dates, relative-URL resolution, content sanitizing, and
network fetching
- - Result shape
- Frozen, fully typed :class:`~turbohtml.extract.Feed`/:class:`~turbohtml.extract.Entry` (``NamedTuple``)
- ``FeedParserDict`` with attribute-or-key access and permissive fallbacks
- - Performance
- One C walk of the parsed tree; over 12x faster on a 30-item feed
- Python SAX-style scanner with per-element handlers
- - Dependencies
- Zero runtime deps (self-contained C extension)
- ``sgmllib3k``; optional ``chardet`` for byte streams
- - Typing
- Annotated records, ``py.typed``
- Untyped ``FeedParserDict``
Feature overlap
===============
The shared surface you can port one-to-one:
- ``feedparser.parse(xml)`` -> :func:`turbohtml.extract.feed`, one call returning the normalized feed.
- ``result.feed.title`` / ``.link`` / ``.description`` -> :class:`~turbohtml.extract.Feed` ``.title``, ``.link``,
``.description``; ``result.feed.updated`` -> ``Feed.updated``.
- ``result.entries`` -> ``Feed.entries``, one :class:`~turbohtml.extract.Entry` per item.
- ``entry.title`` / ``.link`` / ``.id`` / ``.summary`` / ``.author`` -> the same names on
:class:`~turbohtml.extract.Entry`; ``entry.published`` / ``.updated`` -> ``Entry.published`` / ``Entry.updated``;
``entry.content`` (feedparser's list) -> ``Entry.content`` (the resolved body string).
What turbohtml adds
===================
- A frozen, fully typed result: :class:`~turbohtml.extract.Feed` and :class:`~turbohtml.extract.Entry` are
``NamedTuple``\ s with ``py.typed`` coverage, where feedparser returns an untyped ``FeedParserDict``.
- One C walk under the per-tree critical section, over 12x faster than feedparser's Python scanner.
- Records that hold no reference back into the tree, so they outlive the document they came from.
- The rest of the read path on the same string: query, main-content, and structured-data extraction, so a feed and the
pages it links are handled by one library.
- Zero third-party runtime dependencies: no ``sgmllib3k`` to install.
What feedparser has that turbohtml does not
===========================================
- **Parsed timestamps** (``published_parsed``, ``updated_parsed`` as ``time.struct_time``): no equivalent. turbohtml
returns the timestamp string verbatim; parse it with :mod:`email.utils` (RSS RFC 822) or
:meth:`datetime.datetime.fromisoformat` (Atom RFC 3339) yourself.
- **Older dialects** (RSS 0.9x, Atom 0.3, CDF) and their legacy element names: turbohtml targets RSS 2.0, Atom 1.0, and
RDF/RSS-1.0.
- **Network and stream input** (``parse(url)`` / file / stream): no equivalent. turbohtml takes a decoded ``str``; fetch
and decode with your own client first.
- **The ``bozo`` flag and relative-URL resolution**: turbohtml does not report a well-formedness bit and returns link
values verbatim; resolve relatives with :func:`turbohtml.extract.normalize_url` when you have a base.
- **Content sanitizing**: feedparser strips unsafe markup from element content. turbohtml returns the text as-is; run it
through :func:`turbohtml.clean.sanitize` if you will render it.
Performance
===========
Both start from the raw feed string and parse before they read it. On a 30-item RSS feed carrying titles, links, guids,
dates, ``dc:creator``, and ``content:encoded`` bodies, the C walk runs over 12 times faster than feedparser's scanner:
.. bench-table::
:file: bench/feedparser.json
****************
How to migrate
****************
Swap the import and the call; read attributes off the typed records instead of a ``FeedParserDict``.
.. list-table::
:header-rows: 1
:widths: 50 50
- - `feedparser `__
- turbohtml
- - ``feedparser.parse(xml)``
- :func:`turbohtml.extract.feed`
- - ``feedparser.parse(url)``
- :func:`~turbohtml.extract.feed` on the body you fetch yourself
- - ``result.feed.title`` / ``.link``
- ``Feed.title`` / ``Feed.link``
- - ``entry.published`` / ``entry.published_parsed``
- ``Entry.published`` (string); parse it yourself for a ``datetime``
- - ``entry.get("summary")``
- ``Entry.summary``
Before, with ``feedparser``, the parse returns a ``FeedParserDict`` you read by key or attribute:
.. code-block:: python
import feedparser
result = feedparser.parse(
''
"Example Blog"
''
"Hello, feeds"
''
"tag:blog.example,2026:hello"
"A. Writer"
)
print(result.feed.title)
print(result.entries[0].title, result.entries[0].author)
After, :func:`~turbohtml.extract.feed` returns the same values from one C walk, as frozen typed records:
.. testcode::
from turbohtml.extract import feed
parsed = feed(
''
"Example Blog"
''
"2026-07-07T09:00:00Z"
"Hello, feeds"
''
"tag:blog.example,2026:hello"
"2026-07-06T08:00:00Z"
"A first post."
"A. Writer"
)
print(parsed.type, parsed.title)
entry = parsed.entries[0]
print(entry.title, entry.author)
print(entry.link)
.. testoutput::
atom Example Blog
Hello, feeds A. Writer
https://blog.example/hello
turbohtml does not fetch URLs; ``feedparser.parse(url)`` becomes your HTTP client plus :func:`~turbohtml.extract.feed`
on the response body. A document with no feed root reads back as ``None`` rather than an empty ``FeedParserDict``:
.. testcode::
print(feed("
not a feed"))
.. testoutput::
None
**********************
Gotchas and pitfalls
**********************
- **Timestamps come back as strings**, not ``time.struct_time``. feedparser exposes both ``published`` (raw) and
``published_parsed`` (parsed); turbohtml returns only the raw string, so parse it yourself with :mod:`email.utils` for
RSS or :meth:`datetime.datetime.fromisoformat` for Atom.
- **Absent fields are ``None``**, not a missing key. feedparser omits an absent key (``entry.get("summary")`` returns
``None``); :class:`~turbohtml.extract.Entry` always carries every field, set to ``None`` when the item lacks it, so
read ``entry.summary`` directly.
- **A non-feed document returns ``None``**, where feedparser returns a ``FeedParserDict`` with ``bozo`` set and empty
``entries``. Guard the ``None`` before reading ``.entries``.
- **``content`` is one string**, the resolved body (``content:encoded`` or Atom ````), not feedparser's list of
content dicts with ``type``/``value``. Read ``Entry.summary`` for the short form.
- **Link values are verbatim**, not resolved against the feed's base. Absolutize relative links yourself with
:func:`turbohtml.extract.normalize_url` when you hold a base URL.