Skip to content
Python Basics: Extract HTML Details with HTMLParser

Python Basics: Extract HTML Details with HTMLParser

October 24, 2026

A local HTML report may need a link inventory, a title, or a quick check for images without alternative text. HTMLParser exposes tags and text through callbacks, so a small script can collect those details without regular expressions. These examples parse fixed strings only; they do not fetch pages or launch a browser.

Prepare a scratch directory

These standalone examples were tested on Linux with Python 3.12.14. Use Python 3.6 or newer for the syntax shown here. External HTML should be processed with a maintained, patched Python runtime. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run without -O so the assertions remain enabled.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named source below as UTF-8. Avoid filenames that shadow standard-library modules.

1. Collect link targets from start tags

Save as collect_report_links.py:

from html.parser import HTMLParser

class LinkCollector(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.targets = []

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            href = dict(attrs).get("href")
            if href is not None:
                self.targets.append(href)

document = (
    '<A HREF="/build?mode=full&amp;page=2">Build</A>'
    '<a href="#notes">Notes</a><a id="end">End</a>'
)
parser = LinkCollector()
parser.feed(document)
parser.close()
assert parser.targets == ["/build?mode=full&page=2", "#notes"]
for target in parser.targets:
    print(target)

Run it:

python3 -B collect_report_links.py

Output:

/build?mode=full&page=2
#notes

Tag and attribute names arrive in lowercase, and attribute character references are decoded. An anchor without href is absent from this inventory. The returned strings are not validated URLs: collecting a link does not make it safe to visit. See handle_starttag.

2. Accumulate a title across input chunks

Save as read_chunked_html_title.py:

from html.parser import HTMLParser

class TitleReader(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.in_title = False
        self.parts = []

    def handle_starttag(self, tag, attrs):
        if tag == "title":
            self.in_title = True

    def handle_endtag(self, tag):
        if tag == "title":
            self.in_title = False

    def handle_data(self, data):
        if self.in_title:
            self.parts.append(data)

parser = TitleReader()
for chunk in ("<head><tit", "le>Release &am", "p; Notes</title></head>"):
    parser.feed(chunk)
parser.close()
title = " ".join("".join(parser.parts).split())
assert title == "Release & Notes"
print("Title:", title)

Run it:

python3 -B read_chunked_html_title.py

Output:

Title: Release & Notes

feed accepts partial input and buffers incomplete constructs. Join callback fragments before normalizing whitespace; adding a space between every fragment could split a word. This reader assumes one ordinary title element, as in the fixture. Call close after the last chunk.

3. Distinguish missing and empty alt attributes

Save as check_image_alt_attributes.py:

from html.parser import HTMLParser

class ImageInventory(HTMLParser):
    def __init__(self):
        super().__init__()
        self.rows = []

    def handle_starttag(self, tag, attrs):
        if tag != "img":
            return
        attributes = dict(attrs)
        alt = attributes.get("alt")
        if alt is None:
            status = "missing alt value"
        elif alt == "":
            status = "empty alt value"
        else:
            status = "nonempty alt value"
        line, column = self.getpos()
        self.rows.append((line, column, status))

document = (
    '<img src="chart.png" alt="Checks by day">\n'
    '<img src="divider.png" alt="">\n'
    '<img src="preview.png">'
)
parser = ImageInventory()
parser.feed(document)
parser.close()
assert parser.rows == [
    (1, 0, "nonempty alt value"),
    (2, 0, "empty alt value"),
    (3, 0, "missing alt value"),
]
for line, column, status in parser.rows:
    print(f"line {line}, column {column}: {status}")

Run it:

python3 -B check_image_alt_attributes.py

Output:

line 1, column 0: nonempty alt value
line 2, column 0: empty alt value
line 3, column 0: missing alt value

This inventory records the source position of each image and separates an absent value from an intentionally empty string. It does not judge whether text is useful or an image is decorative. getpos uses one-based line numbers and zero-based offsets; the fixture starts every tag in column zero.

HTMLParser is an event parser, not a browser DOM or an HTML sanitizer. It does not enforce matching start and end tags. These small collectors assume the illustrated markup shape; malformed or unusual documents need deliberate handling. Keep input sizes bounded, and never treat extracted text, attributes, or links as trusted instructions.

Clean up

In the same terminal, remove only the named sources, return to the original directory, and remove the empty scratch directory:

rm -- collect_report_links.py read_chunked_html_title.py check_image_alt_attributes.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports remaining files, inspect them instead of deleting unknown contents.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on