Download utility
PDF Link Downloaders
Three small Python scripts for downloading directly linked PDFs from a webpage, a group of pages on the same site, or a JavaScript-rendered page.
- Format
- Reference
- Published
I wrote these because occasionally I find a page containing a large collection of useful PDFs, and clicking every one individually gets old very quickly.
Single page
Use this when the page already contains direct links to PDF files.
Inspect source download_pdfs.py · 134 lines
import os
import re
import sys
from urllib.parse import urljoin, urlparse, unquote
import requests
from bs4 import BeautifulSoup
def safe_filename_from_url(url: str, fallback: str = "downloaded.pdf") -> str:
"""
Create a safe local filename from a URL.
"""
path = urlparse(url).path
name = os.path.basename(path)
name = unquote(name).strip()
if not name:
name = fallback
name = re.sub(r'[<>:"/\\|?*]+', "_", name)
if not name.lower().endswith(".pdf"):
name += ".pdf"
return name
def is_pdf_link(url: str) -> bool:
"""
Check whether a URL looks like a PDF link.
"""
path = urlparse(url).path.lower()
return path.endswith(".pdf")
def download_file(url: str, output_folder: str, session: requests.Session) -> None:
"""
Download a PDF file to the output folder.
"""
filename = safe_filename_from_url(url)
filepath = os.path.join(output_folder, filename)
base, ext = os.path.splitext(filename)
counter = 1
while os.path.exists(filepath):
filepath = os.path.join(output_folder, f"{base}_{counter}{ext}")
counter += 1
try:
with session.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if "pdf" not in content_type and not url.lower().endswith(".pdf"):
print(f"Skipping non-PDF response: {url}")
return
with open(filepath, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded: {url} -> {filepath}")
except requests.RequestException as e:
print(f"Failed to download {url}: {e}")
def find_pdf_links(webpage_url: str) -> list[str]:
"""
Find all PDF links on a webpage.
"""
headers = {
"User-Agent": "Mozilla/5.0 (compatible; PDFDownloader/1.0)"
}
with requests.Session() as session:
session.headers.update(headers)
response = session.get(webpage_url, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
pdf_links = set()
for link in soup.find_all("a", href=True):
href = link["href"].strip()
absolute_url = urljoin(webpage_url, href)
if is_pdf_link(absolute_url):
pdf_links.add(absolute_url)
return sorted(pdf_links)
def download_all_pdfs(webpage_url: str, output_folder: str = "downloaded_pdfs") -> None:
"""
Find and download all PDF links from the given webpage.
"""
os.makedirs(output_folder, exist_ok=True)
headers = {
"User-Agent": "Mozilla/5.0 (compatible; PDFDownloader/1.0)"
}
try:
pdf_links = find_pdf_links(webpage_url)
except requests.RequestException as e:
print(f"Failed to read webpage: {e}")
return
if not pdf_links:
print("No PDF links found on the page.")
return
print(f"Found {len(pdf_links)} PDF link(s).")
with requests.Session() as session:
session.headers.update(headers)
for pdf_url in pdf_links:
download_file(pdf_url, output_folder, session)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python download_pdfs.py <webpage_url> [output_folder]")
sys.exit(1)
url = sys.argv[1]
folder = sys.argv[2] if len(sys.argv) > 2 else "downloaded_pdfs"
download_all_pdfs(url, folder)
python -m pip install requests beautifulsoup4
python download_pdfs.py "https://example.com/page-with-pdfs"The script finds direct .pdf links on that page and saves the files into downloaded_pdfs.
Same-site crawler
Use this when the PDFs are spread across several pages on the same website.
Inspect source download_pdfs_crawl_site.py · 154 lines
import argparse
import os
import re
from collections import deque
from urllib.parse import urldefrag, urljoin, urlparse, unquote
import requests
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; PDFSiteCrawler/1.0)"
}
def safe_filename_from_url(url: str, fallback: str = "downloaded.pdf") -> str:
path = urlparse(url).path
name = unquote(os.path.basename(path)).strip() or fallback
name = re.sub(r'[<>:"/\\|?*]+', "_", name)
if not name.lower().endswith(".pdf"):
name += ".pdf"
return name
def is_pdf_link(url: str) -> bool:
return urlparse(url).path.lower().endswith(".pdf")
def same_site(url: str, start_url: str) -> bool:
return urlparse(url).netloc == urlparse(start_url).netloc
def normalise_url(url: str) -> str:
url, _fragment = urldefrag(url)
return url
def download_file(url: str, output_folder: str, session: requests.Session) -> None:
filename = safe_filename_from_url(url)
filepath = os.path.join(output_folder, filename)
base, ext = os.path.splitext(filename)
counter = 1
while os.path.exists(filepath):
filepath = os.path.join(output_folder, f"{base}_{counter}{ext}")
counter += 1
try:
with session.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if "pdf" not in content_type and not is_pdf_link(url):
print(f"Skipping non-PDF response: {url}")
return
with open(filepath, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded: {url} -> {filepath}")
except requests.RequestException as e:
print(f"Failed to download {url}: {e}")
def crawl_for_pdfs(
start_url: str,
session: requests.Session,
max_pages: int,
max_depth: int,
) -> list[str]:
seen_pages = set()
seen_pdfs = set()
queue = deque([(normalise_url(start_url), 0)])
while queue and len(seen_pages) < max_pages:
page_url, depth = queue.popleft()
if page_url in seen_pages:
continue
seen_pages.add(page_url)
print(f"Scanning: {page_url}")
try:
response = session.get(page_url, timeout=30)
response.raise_for_status()
except requests.RequestException as e:
print(f"Failed to read page: {page_url} ({e})")
continue
content_type = response.headers.get("Content-Type", "").lower()
if "html" not in content_type:
continue
soup = BeautifulSoup(response.text, "html.parser")
for link in soup.find_all("a", href=True):
href = link["href"].strip()
absolute_url = normalise_url(urljoin(page_url, href))
if is_pdf_link(absolute_url):
seen_pdfs.add(absolute_url)
continue
if depth < max_depth and same_site(absolute_url, start_url):
scheme = urlparse(absolute_url).scheme
if scheme in {"http", "https"} and absolute_url not in seen_pages:
queue.append((absolute_url, depth + 1))
return sorted(seen_pdfs)
def download_site_pdfs(
start_url: str,
output_folder: str = "downloaded_pdfs",
max_pages: int = 50,
max_depth: int = 2,
) -> None:
os.makedirs(output_folder, exist_ok=True)
with requests.Session() as session:
session.headers.update(HEADERS)
pdf_links = crawl_for_pdfs(start_url, session, max_pages, max_depth)
if not pdf_links:
print("No PDF links found.")
return
print(f"Found {len(pdf_links)} PDF link(s).")
for pdf_url in pdf_links:
download_file(pdf_url, output_folder, session)
def main() -> None:
parser = argparse.ArgumentParser(
description="Crawl pages on the same site and download directly linked PDFs."
)
parser.add_argument("url", help="Starting webpage URL")
parser.add_argument("output_folder", nargs="?", default="downloaded_pdfs")
parser.add_argument("--max-pages", type=int, default=50)
parser.add_argument("--max-depth", type=int, default=2)
args = parser.parse_args()
download_site_pdfs(args.url, args.output_folder, args.max_pages, args.max_depth)
if __name__ == "__main__":
main()
python -m pip install requests beautifulsoup4
python download_pdfs_crawl_site.py "https://example.com/start-page"It follows links on the same domain and downloads the PDFs it finds. You can also stop it wandering too far:
python download_pdfs_crawl_site.py "https://example.com/start-page" "my_pdfs" --max-pages 25 --max-depth 2JavaScript-rendered pages
If the PDF links only appear after JavaScript runs, use the Selenium version.
Inspect source download_pdfs_selenium.py · 125 lines
import argparse
import os
import re
import time
from urllib.parse import urljoin, urlparse, unquote
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; PDFSeleniumDownloader/1.0)"
}
def safe_filename_from_url(url: str, fallback: str = "downloaded.pdf") -> str:
path = urlparse(url).path
name = unquote(os.path.basename(path)).strip() or fallback
name = re.sub(r'[<>:"/\\|?*]+', "_", name)
if not name.lower().endswith(".pdf"):
name += ".pdf"
return name
def is_pdf_link(url: str) -> bool:
return urlparse(url).path.lower().endswith(".pdf")
def download_file(url: str, output_folder: str, session: requests.Session) -> None:
filename = safe_filename_from_url(url)
filepath = os.path.join(output_folder, filename)
base, ext = os.path.splitext(filename)
counter = 1
while os.path.exists(filepath):
filepath = os.path.join(output_folder, f"{base}_{counter}{ext}")
counter += 1
try:
with session.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
content_type = response.headers.get("Content-Type", "").lower()
if "pdf" not in content_type and not is_pdf_link(url):
print(f"Skipping non-PDF response: {url}")
return
with open(filepath, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Downloaded: {url} -> {filepath}")
except requests.RequestException as e:
print(f"Failed to download {url}: {e}")
def find_pdf_links_with_selenium(webpage_url: str, wait_seconds: float = 3.0) -> list[str]:
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=chrome_options)
try:
driver.get(webpage_url)
time.sleep(wait_seconds)
soup = BeautifulSoup(driver.page_source, "html.parser")
pdf_links = set()
for link in soup.find_all("a", href=True):
absolute_url = urljoin(webpage_url, link["href"].strip())
if is_pdf_link(absolute_url):
pdf_links.add(absolute_url)
return sorted(pdf_links)
finally:
driver.quit()
def download_all_pdfs(
webpage_url: str,
output_folder: str = "downloaded_pdfs",
wait_seconds: float = 3.0,
) -> None:
os.makedirs(output_folder, exist_ok=True)
pdf_links = find_pdf_links_with_selenium(webpage_url, wait_seconds)
if not pdf_links:
print("No PDF links found on the rendered page.")
return
print(f"Found {len(pdf_links)} PDF link(s).")
with requests.Session() as session:
session.headers.update(HEADERS)
for pdf_url in pdf_links:
download_file(pdf_url, output_folder, session)
def main() -> None:
parser = argparse.ArgumentParser(
description="Render a JavaScript-heavy page with Selenium and download direct PDF links."
)
parser.add_argument("url", help="Webpage URL")
parser.add_argument("output_folder", nargs="?", default="downloaded_pdfs")
parser.add_argument("--wait", type=float, default=3.0, help="Seconds to wait after page load")
args = parser.parse_args()
download_all_pdfs(args.url, args.output_folder, args.wait)
if __name__ == "__main__":
main()
python -m pip install requests beautifulsoup4 selenium
python download_pdfs_selenium.py "https://example.com/javascript-page"This uses headless Chrome to render the page before looking for PDF links. Chrome needs to be installed; recent Selenium versions can normally sort out the matching driver themselves.
For slower pages:
python download_pdfs_selenium.py "https://example.com/javascript-page" "my_pdfs" --wait 8