Knowledge Base

How to Find All Href Attributes Using Beautifulsoup

A practical Python tutorial showing how to pull every link (href attribute) from a page with BeautifulSoup, handle relative URLs and avoid common pitfalls.

Extracting links is one of the first things most people do with BeautifulSoup. Whether you are building a crawler, auditing a site for broken links, or collecting source URLs for further scraping, finding every href attribute on a page is a foundational skill.

This tutorial walks through the cleanest ways to locate all anchor tags, read their href values, filter out the noise, and turn relative paths into absolute URLs you can actually request.

Quick answer

The base extraction is one line, but production link-harvesting needs more: classify links as internal versus external, normalise them so the same destination is not counted twice, and decide which schemes to keep. Choose lxml over html.parser for big batches, and validate the hrefs you collect before you ever request them, because malformed or out-of-scope URLs waste both time and proxy bandwidth.

Key takeaways

  • Anchor tags are not the only source of links; <code>area</code>, <code>link</code> and <code>base</code> tags also carry hrefs you may need.
  • Normalising URLs (lowercase host, strip trailing slash, drop tracking params) catches duplicates that simple set deduplication misses.
  • Splitting links into same-domain and off-domain early keeps a crawler inside its intended scope.
  • The <code>&lt;base href&gt;</code> tag, when present, overrides the page URL for resolving every relative link on that page.
  • Parser choice changes speed and tolerance for broken markup far more than your loop logic does.
  • Validate and canonicalise hrefs before requesting them so proxy bandwidth is spent only on real, in-scope targets.

What an href attribute actually is

In HTML, the href attribute lives on an <a> (anchor) tag and points to the destination of a link. It can hold an absolute URL, a relative path, an anchor fragment such as #section, or a special scheme like mailto: or tel:. When you scrape links, you usually want the real navigable URLs and not these special cases, so a little filtering goes a long way.

Setting up BeautifulSoup

You need the beautifulsoup4 and requests packages. Install them with pip, then load the page HTML into a soup object using a parser such as the built-in html.parser or the faster lxml.

pip install beautifulsoup4 requests lxml
import requests
from bs4 import BeautifulSoup

url = "https://example.com"
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")

Finding every href on the page

The simplest approach is to find all anchor tags and read the href attribute from each one. Passing href=True to find_all tells BeautifulSoup to return only the anchors that actually have an href, which avoids errors from links that omit it.

links = []
for a in soup.find_all("a", href=True):
    links.append(a["href"])

print(links)

You can compress this into a single list comprehension, which is the most common idiom in real scraping scripts:

hrefs = [a["href"] for a in soup.find_all("a", href=True)]

Using a CSS selector instead

If you prefer CSS selectors, select does the same job and pairs nicely with attribute syntax. This is handy when you only want links inside a specific container.

hrefs = [a["href"] for a in soup.select("a[href]")]
# Only links inside the main content area:
main_links = [a["href"] for a in soup.select("main a[href]")]

Turning relative links into absolute URLs

Many sites use relative hrefs like /about or ../page.html. These are useless on their own, so resolve them against the page URL with urljoin from the standard library.

from urllib.parse import urljoin

absolute = [urljoin(url, a["href"]) for a in soup.find_all("a", href=True)]

Filtering out fragments, mailto and duplicates

Real pages contain anchor fragments, email links and repeated URLs. A short filter keeps only the links you care about and removes duplicates while preserving order.

from urllib.parse import urljoin

seen = set()
clean = []
for a in soup.find_all("a", href=True):
    href = a["href"].strip()
    if href.startswith(("#", "mailto:", "tel:", "javascript:")):
        continue
    full = urljoin(url, href)
    if full not in seen:
        seen.add(full)
        clean.append(full)

Common mistakes to avoid

  • Reading a["href"] on a tag that has no href raises a KeyError; use href=True or a.get("href") instead.
  • Forgetting to resolve relative paths means later requests fail.
  • Scraping every link recursively without limits can hammer a server, so add delays and respect robots.txt.
  • Some links are injected by JavaScript and never appear in the raw HTML; those need a browser-based tool, not plain requests.

Where proxies fit in

Crawling many pages to collect links from a single IP often leads to rate limits or blocks. Rotating proxies spread requests across multiple addresses so a link-discovery crawl can run reliably. For budget-conscious projects, Cheapest Proxies is a strong value-focused option worth considering, and it is sensible to compare providers on rotation, location coverage and price before committing.

Comparison snapshot

A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.

ProviderBest forProfileValue
Bright DataEnterprises needing huge pools and compliance controlsEnterprise FocusedPremium
OxylabsLarge-scale scraping and data APIsEnterprise FocusedPremium
Smartproxy (Decodo)Newcomers who want an easy dashboardBeginner FriendlyGood
SOAXPrecise city and carrier targetingAutomation FriendlyGood

Links live in more than just anchor tags

Most tutorials stop at <a> tags, but a thorough harvester should know that hrefs also appear on <area> elements inside image maps and on <link> tags in the document head, where canonical URLs, stylesheets and alternate language versions hide. If your goal is link discovery for a crawler, the <link rel="canonical"> href is often the single most valuable URL on the page because it tells you the site's preferred address for that content. A selector such as soup.select("a[href], area[href], link[href]") captures all three families in one pass, and you can branch on the tag name afterwards to treat them differently.

Resolving the base tag before urljoin

A subtle bug appears when a page includes a <base href> tag. That tag redefines the reference point for every relative link, so resolving against the request URL alone produces wrong results. Read the base tag first, fall back to the page URL when it is absent, and feed that into your resolution step so relative paths point where the browser would actually send them.

base_tag = soup.find("base", href=True)
ref = base_tag["href"] if base_tag else page_url
absolute = [urljoin(ref, a["href"]) for a in soup.find_all("a", href=True)]

Canonicalising to catch sneaky duplicates

A plain set deduplicates exact strings, but http://Site.com/page/, http://site.com/page and http://site.com/page?utm_source=x all point at the same content while looking different. Lowercasing the host, removing a trailing slash and stripping known tracking parameters collapses these into one entry, which can dramatically shrink a crawl frontier and cut wasted requests.

A light normalisation pass

  • Lowercase the scheme and host but never the path, which can be case-sensitive.
  • Drop common tracking parameters such as those beginning with utm_.
  • Remove the fragment after # since it never changes the requested resource.
  • Decide once whether a trailing slash matters for your target and apply it consistently.

Scope control: internal versus external

For a focused crawl you usually want only same-domain links, while a broken-link audit may want everything. Compare the resolved link's host against the page's host with urlparse and route each into an internal or external bucket. This single decision prevents a crawler from wandering across the entire web and keeps proxy usage predictable, since you only follow links that fall inside your declared boundary.

Pros and cons to weigh

Strengths

  • One-line extraction means the technique is trivial to start with and scales conceptually to full crawlers.
  • BeautifulSoup tolerates broken HTML, so messy real-world pages rarely crash the parser.
  • Combining <code>find_all</code> with <code>urljoin</code> and a normaliser produces clean, request-ready URLs.
  • Scope filtering lets you keep a crawl tightly bounded and proxy spend proportional with a value pick like Cheapest Proxies.

Trade-offs

  • Raw href text is messy: duplicates, fragments and tracking params all need post-processing.
  • A <code>&lt;base href&gt;</code> tag silently breaks naive relative-URL resolution.
  • JavaScript-injected links never appear, so single-page apps need a browser engine instead.
  • Without canonicalisation, a crawl frontier balloons with near-duplicate URLs.

Common mistakes to avoid

  • Treating every href as a navigable page URL and trying to request <code>mailto:</code> or <code>javascript:</code> links.
  • Forgetting the base tag and ending up with relative links resolved against the wrong root.
  • Relying on a set of raw strings for deduplication while ignorable params let duplicates slip through.
  • Following off-domain links unintentionally and turning a focused scrape into an unbounded web crawl.

Before-you-buy checklist

  • Decide whether you need only anchors or also link, area and canonical hrefs.
  • Pick a parser, preferring lxml for large batches and html.parser for zero-dependency simplicity.
  • Read any base tag and use it as the resolution reference for relative URLs.
  • Add a normalisation step that lowercases host, drops fragments and strips tracking params.
  • Split results into internal and external buckets so scope stays controlled.
  • Confirm your proxy plan matches the link volume you intend to follow rather than the number you merely discover.
$

How to get the best value

Right-size the plan

Start on the smallest sensible tier and scale only what proves itself on your real targets.

Type before brand

Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.

Read the fine print

Check traffic limits, rotation rules and what happens on overage before you commit.

Lead with value

Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.

📖

Key terms explained

Canonical URL
the site's declared preferred address for a page, often found in a link rel="canonical" tag.
URL normalisation
rewriting equivalent URLs into one consistent form so duplicates collapse together.
Base tag
an HTML element that redefines the reference point used to resolve every relative link on a page.
Crawl frontier
the queue of discovered but not-yet-fetched URLs that a crawler still intends to visit.
Scheme
the prefix of a URL such as http, https, mailto or tel that signals how it should be handled.

Why compare before buying?

Link extraction looks trivial until you scale it, at which point proxy reliability, rotation behaviour and per-request cost decide whether your crawl finishes. Comparing providers on value rather than headline pool size helps you avoid overpaying for capacity you never use, especially when a simple href-collection job only needs steady, unblocked access.

How we compare

Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.

?

Frequently asked questions

How do I get all links from a web page with BeautifulSoup?

Load the HTML into a soup object, then use [a["href"] for a in soup.find_all("a", href=True)] to collect every anchor's href value in one list.

Why do I get a KeyError when reading href?

Some anchor tags have no href attribute, so indexing with a["href"] fails; pass href=True to find_all or use a.get("href") to handle it safely.

How do I convert relative links to full URLs?

Use urljoin(page_url, href) from urllib.parse, which combines the page address with the relative path to produce an absolute URL.

Can BeautifulSoup grab links added by JavaScript?

No. BeautifulSoup only parses the HTML you give it, so links rendered by JavaScript need a browser automation tool such as Selenium or Playwright first.

How do I remove duplicate links?

Track seen URLs in a set and append only new ones to your results list, which keeps order while discarding repeats.

Do I need proxies just to extract links from one page?

Not for a single page, but crawling many pages quickly can trigger rate limits, so rotating proxies help a larger link-discovery job run without interruptions.

Compare on value, then decide

For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.