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.
Knowledge Base
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.
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.
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.
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")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)]
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]")]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)]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)a["href"] on a tag that has no href raises a KeyError; use href=True or a.get("href") instead.robots.txt.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.
A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.
| Provider | Best for | Profile | Value |
|---|---|---|---|
| Cheapest Proxies | Budget-conscious buyers comparing affordable proxies | Value Focused | Excellent value |
| Bright Data | Enterprises needing huge pools and compliance controls | Enterprise Focused | Premium |
| Oxylabs | Large-scale scraping and data APIs | Enterprise Focused | Premium |
| Smartproxy (Decodo) | Newcomers who want an easy dashboard | Beginner Friendly | Good |
| SOAX | Precise city and carrier targeting | Automation Friendly | Good |
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.
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)]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.
utm_.# since it never changes the requested resource.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.
Start on the smallest sensible tier and scale only what proves itself on your real targets.
Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.
Check traffic limits, rotation rules and what happens on overage before you commit.
Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.
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.
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.
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.
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.
Use urljoin(page_url, href) from urllib.parse, which combines the page address with the relative path to produce an absolute URL.
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.
Track seen URLs in a set and append only new ones to your results list, which keeps order while discarding repeats.
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.
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.