核心是用BeautifulSoup提取所有含.pdf的a标签href,统一转小写后正则匹配.pdf(?|#|$)并用urljoin补全路径;需额外处理iframe/src等隐藏链接,下载前校验Last-Modified、User-Agent和Referer,文件名须安全脱敏并支持Content-Disposition fallback。

用 requests 和 BeautifulSoup 抓取页面中的PDF链接
核心是识别出页面中所有指向PDF的 <a> 标签,且目标URL以 .pdf 结尾(注意大小写和查询参数干扰)。常见错误是直接用 href.endswith('.pdf'),但实际可能遇到 report.pdf?ver=2 或 /docs/Annual-Report.PDF。建议统一转小写再匹配,并用 urllib.parse.urljoin() 补全相对路径:
from urllib.parse import urljoin
import re
<p>def extract_pdf_urls(html, base_url):
soup = BeautifulSoup(html, 'html.parser')
pdf_urls = []
for link in soup.find_all('a', href=True):
href = link['href'].strip()
if not href:
continue
full_url = urljoin(base_url, href)</p><h1>匹配 .pdf 后缀(忽略大小写,允许末尾带 # 锚点或 ? 查询)</h1><pre class='brush:python;toolbar:false;'> if re.search(r'.pdf(?:?|#|$)', full_url.lower()):
pdf_urls.append(full_url)
return pdf_urls</pre>注意:有些网站把PDF放在 <iframe src="xxx.pdf"> 或 <embed> 里,这类需额外提取 src 属性。
判断PDF是否“过期”——关键在HTTP头和Last-Modified
“过期”不是指文件内容过期,而是你本地已有副本比服务器上的旧。最可靠方式是比对 Last-Modified 响应头。不要依赖文件名里的年份(比如 2023-report.pdf 可能被误更新为2024版但没改名)。步骤如下:
- 先用
HEAD请求获取服务器响应头(避免下载整个文件) - 解析
Last-Modified字段为datetime - 检查本地对应文件是否存在,若存在则读取其
os.path.getmtime() - 若服务器时间 > 本地修改时间,则需重新下载
示例逻辑:
立即学习“Python免费学习笔记(深入)”;
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
import requests
from datetime import datetime
import os
<p>def needs_update(pdf_url, local_path):
try:
resp = requests.head(pdf_url, timeout=5, allow_redirects=True)
resp.raise_for_status()
server_time = datetime.strptime(
resp.headers.get('Last-Modified', ''),
'%a, %d %b %Y %H:%M:%S GMT'
).timestamp()
if os.path.exists(local_path):
local_time = os.path.getmtime(local_path)
return server_time > local_time
return True
except Exception as e:</p><h1>HEAD失败(如服务器禁用)则默认需要下载</h1><pre class='brush:python;toolbar:false;'> return True</pre>注意:Last-Modified 并非所有服务器都返回;部分CDN或静态托管(如GitHub Pages)可能固定返回同一时间戳,此时可退而求其次比对 ETag,或直接用 GET 请求并检查 Content-Length 是否变化。
下载时处理重定向、403和反爬策略
很多PDF链接会跳转(如302到AWS S3),或返回403(因缺少 User-Agent 或 Referer)。直接用 requests.get(url) 容易失败。必须:
- 显式设置
headers={'User-Agent': 'Mozilla/5.0...'} - 启用
allow_redirects=True(默认已开启,但需确认) - 对403/406等状态码,尝试加
Referer头(设为来源网页URL) - 遇到 Cloudflare 或 JS 挑战,
requests无解,得换selenium或playwright,但性能代价大,先排除是否真需要
简化的健壮下载函数:
def download_pdf(pdf_url, local_path, referer=None):
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'}
if referer:
headers['Referer'] = referer
try:
resp = requests.get(pdf_url, headers=headers, timeout=30)
resp.raise_for_status()
with open(local_path, 'wb') as f:
f.write(resp.content)
return True
except Exception as e:
print(f"Failed to download {pdf_url}: {e}")
return False保存路径冲突与文件名安全处理
从URL生成本地文件名时,不能直接用 url.split('/')[-1],因为可能含非法字符(/、?、)、空名或过长名。更糟的是多个PDF链接可能解析出相同文件名(如都叫 download.pdf)。解决方法:
- 用
urllib.parse.unquote()解码URL - 用正则剔除Windows/Linux非法字符(
[:"/\|?*]) - 保留原始域名+路径哈希作为前缀,避免冲突:
f"{hashlib.md5(full_url.encode()).hexdigest()[:8]}_{safe_name}" - 确保目录存在:
os.makedirs(os.path.dirname(local_path), exist_ok=True)
真正容易被忽略的是:某些PDF URL不含文件名(如 https://example.com/api/pdf?id=123),此时必须靠 Content-Disposition 响应头取名,否则只能 fallback 到哈希名。

















