How to Check Broken Links on a Website Programmatically Using Python
A lightweight, multi-threaded Python script blueprint to crawl a website domain, check HTTP status codes, and output all broken 404 links.
A broken link (404 Not Found) harms user experience and degrades your website's search engine indexing status. Manually hunting for dead links across hundreds of static flat-files or dynamic routes is highly inefficient, and many cloud-based auditing tools force you into premium monthly tiers.
Building your own bare-metal link crawler in Python provides an automated, free way to audit your technical SEO architecture.
How the Auditing Engine Works
To efficiently map a site without getting blocked or bottlenecked, a program must execute four specific phases:
- Domain Locking: The script normalizes all crawled URLs and ensures it stays locked to your root domain, avoiding external resource blackholes.
- Asynchronous/Multi-threaded State Verification: It maintains an internal memory matrix of
visited_urlsanddiscovered_linksto prevent infinite loops caused by cyclical navigation menus. - DOM Content Scraping: It parses the HTML structure specifically targeting the standard markup link anchors (
<a href="...">). - Network Response Extraction: It fires an HTTP
HEADrequest (which downloads only the server headers rather than the entire page layout) to lightning-fast determine if a status code returns200 OKor404 Broken.
The Python automation blueprint below utilizes requests for fast connection handling and BeautifulSoup for clear DOM parsing to build a multi-threaded link validator.
Python Broken Link Checker Automation Script
import sys import threading from queue import Queue from urllib.parse import urljoin, urlparse import requests from bs4 import BeautifulSoup
class LinkCrawler: def __init__(self, base_url, max_threads=10): self.base_url = base_url self.domain = urlparse(base_url).netloc self.visited_urls = set() self.broken_links = [] self.queue = Queue() self.max_threads = max_threads self.lock = threading.Lock()
# Seed the queue with the entry point self.queue.put(base_url)
def is_internal(self, url): """Ensure the script stays locked to the target domain.""" return urlparse(url).netloc == self.domain
def check_url_status(self, url): """Fire a fast HEAD request to evaluate link validity.""" try: # Use HEAD request for speed; fallback to GET if headers are restricted response = requests.head(url, timeout=5, allow_redirects=True) if response.status_code >= 400: # Retry with GET just in case the server blocks HEAD requests response = requests.get(url, timeout=5, stream=True) return response.status_code except requests.RequestException: return 999 # Connection error code replacement
def crawl_worker(self): while True: try: url = self.queue.get(timeout=2) except: break
with self.lock: if url in self.visited_urls: self.queue.task_done() continue self.visited_urls.add(url)
print(f"[Auditing]: {url}") status = self.check_url_status(url)
if status >= 400: with self.lock: self.broken_links.append({"url": url, "status": status}) else: # If internal and valid, extract child links to expand search depth if self.is_internal(url) and status == 200: try: response = requests.get(url, timeout=5) soup = BeautifulSoup(response.text, 'html.parser') for anchor in soup.find_all('a', href=True): href = anchor['href'] full_url = urljoin(url, href) # Remove trailing fragments or queries for normalization clean_url = full_url.split('#')[0]
with self.lock: if clean_url not in self.visited_urls: self.queue.put(clean_url) except Exception: pass
self.queue.task_done()
def run(self): threads = [] for _ in range(self.max_threads): t = threading.Thread(target=self.crawl_worker) t.daemon = True t.start() threads.append(t)
self.queue.join()
print("\n--- AUDIT COMPLETE ---") if not self.broken_links: print("Success: Zero broken links found across the domain matrix.") else: print(f"Alert: Found {len(self.broken_links)} broken connections:\n") for item in self.broken_links: print(f"Status Code [{item['status']}] -> Dead Reference: {item['url']}")
if __name__ == "__main__": # Replace with the domain you intend to programmatically inspect TARGET_SITE = "https://example.com"
crawler = LinkCrawler(TARGET_SITE, max_threads=12) crawler.run()
Community Engineering Notes
No technical implementations have been appended yet.