3步搞定网站整站下载器,手写实现避坑指南

发布时间:2026/9/23 7:57:47
3步搞定网站整站下载器,手写实现避坑指南 3步搞定网站整站下载器,手写实现避坑指南 官方文档翻了三遍还是晕?别慌,整站下载看着复杂,其实核心就那几行代码。今天直接上干货,带你手写实现一个轻量级爬虫,不用装一堆重型框架,用 Python 标准库和 requests 就能跑通。 很多新手卡在“怎么递归抓取链接”这一步,其实只要理清 HTTP 请求和文件保存的逻辑,整站镜像并不难。咱们不整虚的,直接拆解实战流程,保证你看完就能在本地跑出一个完整的离线站点。 概念速懂:整站下载到底在抓什么 先搞清楚,网站整站下载器不是把网页源码存下来就完事了。它需要处理三类资源:HTML 页面、静态资源(CSS/JS/图片)、以及动态加载内容。 传统工具如 wget 或 httrack 虽然好用,但面对反爬严格的站点经常失效。而手写实现的优势在于可控性——你可以自定义请求头、处理验证码、甚至模拟人类操作节奏。 核心逻辑很简单:广度优先搜索(BFS):从首页开始,把发现的链接放入队列。 去重机制:用 Set 记录已访问 URL,避免死循环。 本地映射:把网络 URL 转成本地文件路径,保持相对路径一致。这里有个关键细节:很多教程忽略相对路径解析。比如 a 标签指向 ./page2.html,而绝对路径是 https://example.com/page2.html。如果本地保存时不统一处理,页面打开后样式和图片全会裂开。 环境准备:别装错依赖包 咱们只依赖两个库:requests 发请求,urllib.parse 处理 URL。其他全是 Python 内置模块。 pip install requests创建项目结构: downloader/ ├── main.py # 主程序 ├── crawler.py # 核心爬虫类 ├── utils.py # 工具函数 └── downloads/ # 输出目录为什么不用 Scrapy?对于整站下载这种线性任务,Scrapy 太重了。启动慢、配置繁琐,对于个人开发者或轻量级需求,手写实现反而更灵活。你随时可以插入断点调试,而不是被框架黑盒化。 核心语法:三个关键函数拆解 整站下载器的灵魂在 crawl 方法里。我们分三步走:解析链接、下载资源、保存文件。 1. URL 规范化与去重 直接复制网上代码最容易出的 bug 就是 URL 格式不一致。比如带不带斜杠、带不带端口号。 from urllib.parse import urlparse, urljoin, urldefragdef normalize_url(url):统一URL格式,去掉fragment和多余参数# 去掉锚点部分 #sectionurl, _ = urldefrag(url)# 解析URL组件parsed = urlparse(url)# 如果是协议相对路径,补全协议if not parsed.scheme:url = https:// + urlparsed = urlparse(url)# 默认端口80/443可省略netloc = parsed.netlocif parsed.scheme == http and parsed.port == 80:netloc = parsed.hostnameelif parsed.scheme == https and parsed.port == 443:netloc = parsed.hostname# 重建URL,路径为空时补/path = parsed.path or /query = f?{parsed.query} if parsed.query else return f{parsed.scheme}://{netloc}{path}{query}重点:urldefrag 这一步千万别省。很多网站导航栏带锚点,如果不去掉,同一个页面会被当成多个 URL 重复下载。 2. 链接提取与过滤 用正则提取 a 和 img 标签不够稳健,推荐用 lxml 或简单的字符串匹配。为了轻量,这里用正则: import redef extract_links(html_content, base_url):从HTML中提取所有外部链接links = set()# 匹配 href 和 src 属性pattern = r'(?:href|src)\s*=\s*[\']([^\']+)[\']'matches = re.findall(pattern, html_content)for link in matches:# 跳过邮件、javascript、纯锚点if link.startswith(('mailto:', 'javascript:', '#')):continue# 转为绝对路径abs_url = urljoin(base_url, link)abs_url = normalize_url(abs_url)# 只保留同域名的链接(防止爬遍全网)if urlparse(abs_url).netloc == urlparse(base_url).netloc:links.add(abs_url)return links避坑提示:urljoin 是处理相对路径的关键。比如当前页是 https://example.com/a/b.html,链接是 ./c.html,urljoin 会正确解析为 https://example.com/a/c.html,而不是 https://example.com/c.html。 3. 文件路径映射 这是整站下载最容易出错的地方。网络路径 https://example.com/css/style.css 不能直接映射为本地路径,因为域名不能当文件夹名。 import os from urllib.parse import urlparsedef url_to_path(url, base_domain):将URL转换为本地文件路径parsed = urlparse(url)path = parsed.path# 如果路径是 /,映射为 index.htmlif path == '/' or path == '':path = '/index.html'# 清理路径中的特殊字符safe_path = re.sub(r'[:/\\|?*]', '_', path)# 拼接本地目录local_path = os.path.join(downloads, base_domain, safe_path)# 确保父目录存在os.makedirs(os.path.dirname(local_path), exist_ok=True)return local_path注意:Windows 下路径分隔符是 \,但 Python 的 os.path 会自动处理。千万别手动拼接字符串,否则跨平台必挂。 完整代码示例:可运行的整站下载器 把前面片段组装起来,这是一个最小可运行的版本。 import requests import time import os from crawler import normalize_url, extract_links, url_to_path from urllib.parse import urlparseclass SiteDownloader:def __init__(self, start_url, max_pages=100):self.start_url = normalize_url(start_url)self.base_domain = urlparse(self.start_url).netlocself.visited = set()self.queue = [self.start_url]self.max_pages = max_pagesself.session = requests.Session()# 设置请求头,模拟浏览器self.session.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'})def download(self):print(fStarting download from {self.start_url})print(fDomain: {self.base_domain})while self.queue and len(self.visited) self.max_pages:current_url = self.queue.pop(0)if current_url in self.visited:continueprint(fProcessing: {current_url})self.visited.add(current_url)try:response = self.session.get(current_url, timeout=10)if response.status_code != 200:print(f Skipping: {current_url} (Status: {response.status_code}))continue# 判断内容类型content_type = response.headers.get('Content-Type', '')if 'html' in content_type:self._process_html(current_url, response.text)else:self._save_file(current_url, response.content)except requests.RequestException as e:print(f Error: {e})# 礼貌爬取,避免被封time.sleep(0.5)print(fFinished. Downloaded {len(self.visited)} pages.)def _process_html(self, url, html_content):处理HTML页面,提取新链接# 保存HTML文件local_path = url_to_path(url, self.base_domain)with open(local_path, 'w', encoding='utf-8') as f:f.write(html_content)print(f Saved: {local_path})# 提取新链接加入队列new_links = extract_links(html_content, url)for link in new_links:if link not in self.visited:self.queue.append(link)def _save_file(self, url, content):保存非HTML资源(图片、CSS、JS)local_path = url_to_path(url, self.base_domain)with open(local_path, 'wb') as f:f.write(content)print(f Saved: {local_path})if __name__ == __main__:# 测试用:选择一个开放且简单的站点target = https://httpbin.org/htmldownloader = SiteDownloader(target, max_pages=5)downloader.download()运行说明:把代码保存为 main.py,crawler.py 包含前面提到的三个函数。 执行 python main.py。 检查 downloads/ 目录,HTML 文件里引用的 CSS 和图片应该都能正确打开。关键优化点:Session 复用:requests.Session 会保持 TCP 连接,比每次新建连接快 3-5 倍。 超时设置:timeout=10 防止卡在无响应的服务器上。 最大页数限制:max_pages 防止意外爬遍整个 CDN 域名,烧掉你硬盘和带宽。常见报错与避坑指南 实际跑起来,90% 的问题出在这几个地方: 1. 编码乱码 现象:保存的 HTML 打开全是方块或问号。 原因:服务器没返回 Content-Type 的 charset,或者返回的是 gbk 而非 utf-8。 解决: # 在 _process_html 中动态检测编码 if 'charset' not in response.headers.get('Content-Type', '').lower():response.encoding = response.apparent_encoding # 自动检测根据 MDN Web Docs 的建议,当 HTTP 头未指定编码时,浏览器会尝试自动检测。我们在 Python 里也要模拟这个行为,否则中文页面必乱。 2. 死循环爬取 现象:队列无限增长,内存爆炸。 原因:URL 规范化不彻底,比如 ?utm_source=xx 参数每次不同。 解决:在 normalize_url 中过滤掉追踪参数: # 在 normalize_url 中添加 params = parsed.query if params:# 过滤常见追踪参数exclude = ['utm_', 'fbclid', 'gclid']filtered = [p for p in params.split('') if not any(ex in p for ex in exclude)]query = f?{''.join(filtered)} if filtered else 3. 403 Forbidden 现象:部分页面返回 403,下载不完整。 原因:IP 被限流或 User-Agent 被拦截。 解决:随机更换 User-Agent。 增加 time.sleep 间隔,建议 1-3 秒。 对于重要站点,考虑使用代理池。4. 相对路径失效 现象:本地打开页面,样式丢失。 原因:CSS 文件里的 url() 指向相对路径,但本地文件结构没对齐。 解决:目前这个简单版只下载 HTML 和资源文件,不修改 CSS 内容。如果需要完美离线,还需要解析 CSS,把其中的 url() 也转换为本地路径。这是进阶需求,初期可忽略。 小结与实战建议 手写实现整站下载器,核心不在于代码多复杂,而在于URL 规范化和路径映射这两个细节。很多人抄完代码跑不起来,90% 是栽在路径解析上。 给公路工程从业者或全栈开发者的建议:先小后大:先用 httpbin.org 或公司内网测试站点跑通,再上生产。 日志为王:把每个 URL 的处理状态写进日志,出问题时能快速定位。 合规第一:遵守目标网站的 robots.txt。虽然代码里没写,但实际部署时必须加上。根据 MDN Web Docs 关于爬虫的指南,尊重 Disallow 规则是基本底线。你公司项目里是怎么处理整站备份或数据归档的?是用现成工具还是自己写脚本?欢迎评论聊聊你的踩坑经验,特别是那些奇葩的反爬策略。